BastionGuard/src/cef/tls_intercept.hpp

875 lines
32 KiB
C++

/*
* BastionGuard™
* Copyright (C) 2025–2026 Calogero Scarnà
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* BastionGuard™ is a trademark of Calogero Scarnà.
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#pragma once
#include <string>
#include <memory>
#include <iostream>
#include <sstream>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <functional>
#include <cstdlib>
#include <algorithm>
#include <array>
#include <cctype>
#include <cerrno>
#include <limits>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/bn.h>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <glib/gi18n.h>
#include "block_page.hpp"
#include "ca_installer.hpp"
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
namespace fs = std::filesystem;
namespace TlsIntercept {
static constexpr long CTX_CACHE_TTL_SEC = 30L * 24 * 3600;
static inline void set_no_expiry(X509* cert) {
ASN1_TIME* not_after = X509_getm_notAfter(cert);
ASN1_GENERALIZEDTIME_set_string(not_after, "99991231235959Z");
}
static inline void set_realistic_validity(X509* cert) {
X509_gmtime_adj(X509_getm_notBefore(cert), -43200L);
X509_gmtime_adj(X509_getm_notAfter(cert), 397L * 24 * 3600);
}
namespace detail {
using Clock = std::chrono::steady_clock;
static inline fs::path ca_dir() {
const char* h = std::getenv("HOME");
return fs::path(h ? h : "/tmp") / ".local/share/BastionGuard/certs";
}
static inline fs::path ca_cert_path() { return ca_dir() / "intercept-ca.crt.pem"; }
static inline fs::path ca_key_path() { return ca_dir() / "intercept-ca.key.pem"; }
static inline std::string drain_errors() {
std::ostringstream o; unsigned long e; bool first = true;
while ((e = ERR_get_error())) {
char b[256]; ERR_error_string_n(e, b, sizeof(b));
if (!first) o << " | "; o << b; first = false;
}
return first ? "no error" : o.str();
}
static inline EVP_PKEY* gen_rsa(int bits) {
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
if (!ctx) return nullptr;
EVP_PKEY* k = nullptr;
if (EVP_PKEY_keygen_init(ctx) > 0 &&
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) > 0 &&
EVP_PKEY_keygen(ctx, &k) > 0) {
EVP_PKEY_CTX_free(ctx); return k;
}
EVP_PKEY_CTX_free(ctx); if (k) EVP_PKEY_free(k); return nullptr;
}
static inline bool add_ext(X509* cert, X509V3_CTX* v3ctx, int nid, const char* val) {
X509_EXTENSION* ext = X509V3_EXT_conf_nid(nullptr, v3ctx, nid, const_cast<char*>(val));
if (!ext) return false;
bool ok = X509_add_ext(cert, ext, -1) == 1;
X509_EXTENSION_free(ext); return ok;
}
static inline bool write_pem(const fs::path& path, std::function<int(BIO*)> writer) {
fs::create_directories(path.parent_path());
fs::path tmp = path.string() + ".tmp";
BIO* bio = BIO_new_file(tmp.c_str(), "w");
if (!bio) return false;
bool ok = writer(bio) > 0;
BIO_free(bio);
if (!ok) { fs::remove(tmp); return false; }
fs::permissions(tmp,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace);
fs::rename(tmp, path);
return true;
}
struct IntermediateCA {
X509* cert = nullptr;
EVP_PKEY* key = nullptr;
~IntermediateCA() {
if (cert) X509_free(cert);
if (key) EVP_PKEY_free(key);
}
};
static inline std::shared_ptr<IntermediateCA> generate_ca() {
std::cerr << _("[tls-intercept] generazione CA intermedia RSA 4096...\n");
EVP_PKEY* key = gen_rsa(4096);
if (!key) { std::cerr << "[tls-intercept] CA keygen failed\n"; return nullptr; }
X509* cert = X509_new();
if (!cert) { EVP_PKEY_free(key); return nullptr; }
X509_set_version(cert, 2);
unsigned char sbuf[20]; RAND_bytes(sbuf, sizeof(sbuf)); sbuf[0] &= 0x7F;
BIGNUM* bn = BN_bin2bn(sbuf, sizeof(sbuf), nullptr);
BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert));
BN_free(bn);
X509_gmtime_adj(X509_getm_notBefore(cert), -3600L);
set_no_expiry(cert);
X509_set_pubkey(cert, key);
X509_NAME* name = X509_get_subject_name(cert);
X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC,
reinterpret_cast<const unsigned char*>("BastionGuard"), -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
reinterpret_cast<const unsigned char*>("BastionGuard Intercept CA"), -1, -1, 0);
X509_set_issuer_name(cert, name);
X509V3_CTX v3ctx;
X509V3_set_ctx(&v3ctx, cert, cert, nullptr, nullptr, 0);
add_ext(cert, &v3ctx, NID_basic_constraints, "critical,CA:TRUE,pathlen:0");
add_ext(cert, &v3ctx, NID_key_usage, "critical,keyCertSign,cRLSign");
add_ext(cert, &v3ctx, NID_subject_key_identifier, "hash");
add_ext(cert, &v3ctx, NID_authority_key_identifier, "keyid:always");
if (X509_sign(cert, key, EVP_sha256()) <= 0) {
std::cerr << "[tls-intercept] CA sign failed: " << drain_errors() << "\n";
X509_free(cert); EVP_PKEY_free(key); return nullptr;
}
// Salva su disco
if (!write_pem(ca_cert_path(), [&](BIO* b){ return PEM_write_bio_X509(b, cert); }) ||
!write_pem(ca_key_path(), [&](BIO* b){
return PEM_write_bio_PrivateKey(b, key, nullptr, nullptr, 0, nullptr, nullptr); }))
{
std::cerr << "[tls-intercept] CA disk write failed\n";
X509_free(cert); EVP_PKEY_free(key); return nullptr;
}
std::cerr << _("[tls-intercept] ✅ CA intermedia generata\n");
auto ca = std::make_shared<IntermediateCA>(); ca->cert = cert; ca->key = key;
return ca;
}
// ── Carica CA dal disco ───────────────────────────────────────────────────────
static inline std::shared_ptr<IntermediateCA> load_ca() {
FILE* fc = fopen(ca_cert_path().c_str(), "r");
FILE* fk = fopen(ca_key_path().c_str(), "r");
if (!fc || !fk) { if (fc) fclose(fc); if (fk) fclose(fk); return nullptr; }
X509* cert = PEM_read_X509(fc, nullptr, nullptr, nullptr);
EVP_PKEY* key = PEM_read_PrivateKey(fk, nullptr, nullptr, nullptr);
fclose(fc); fclose(fk);
if (!cert || !key) { if (cert) X509_free(cert); if (key) EVP_PKEY_free(key); return nullptr; }
auto ca = std::make_shared<IntermediateCA>(); ca->cert = cert; ca->key = key;
return ca;
}
static inline bool ca_needs_rotation() {
if (!fs::exists(ca_cert_path()) || !fs::exists(ca_key_path())) return true;
FILE* f = fopen(ca_cert_path().c_str(), "r");
if (!f) return true;
X509* cert = PEM_read_X509(f, nullptr, nullptr, nullptr);
fclose(f);
if (!cert) return true;
X509_free(cert);
return false;
}
static inline void install_generated_ca() {
const fs::path system_cert = "/etc/BastionGuard/certs/BastionGuard-ca.crt.pem";
try {
fs::create_directories(system_cert.parent_path());
fs::copy_file(ca_cert_path(), system_cert,
fs::copy_options::overwrite_existing);
fs::permissions(system_cert,
fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read | fs::perms::others_read,
fs::perm_options::replace);
std::cerr << _("[tls-intercept] CA copiata in /etc/BastionGuard/certs/\n");
} catch (const std::exception& e) {
std::cerr << "[tls-intercept] CA copy to /etc failed: " << e.what() << "\n";
}
CaInstaller::ensure_trusted();
}
static std::shared_ptr<IntermediateCA> g_ca;
static std::mutex g_ca_mutex;
static inline std::shared_ptr<IntermediateCA> get_ca() {
std::lock_guard lock(g_ca_mutex);
if (g_ca) return g_ca;
if (!ca_needs_rotation()) {
g_ca = load_ca();
if (g_ca) {
std::cerr << _("[tls-intercept] CA caricata dal disco\n");
install_generated_ca();
return g_ca;
}
}
g_ca = generate_ca();
if (g_ca) install_generated_ca();
return g_ca;
}
static inline std::shared_ptr<asio::ssl::context>
make_ssl_ctx_for_host(const std::string& host, const std::shared_ptr<IntermediateCA>& ca)
{
EVP_PKEY* leaf_key = gen_rsa(2048);
if (!leaf_key) return nullptr;
X509* cert = X509_new();
if (!cert) { EVP_PKEY_free(leaf_key); return nullptr; }
X509_set_version(cert, 2);
unsigned char sbuf[20];
RAND_bytes(sbuf, sizeof(sbuf));
sbuf[0] &= 0x7F;
BIGNUM* bn = BN_bin2bn(sbuf, sizeof(sbuf), nullptr);
BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert));
BN_free(bn);
set_realistic_validity(cert);
X509_set_pubkey(cert, leaf_key);
X509_NAME* subject = X509_get_subject_name(cert);
X509_NAME_add_entry_by_txt(subject, "CN", MBSTRING_ASC,
reinterpret_cast<const unsigned char*>(host.c_str()), -1, -1, 0);
X509_set_issuer_name(cert, X509_get_subject_name(ca->cert));
X509V3_CTX v3ctx;
X509V3_set_ctx(&v3ctx, ca->cert, cert, nullptr, nullptr, 0);
add_ext(cert, &v3ctx, NID_basic_constraints, "critical,CA:FALSE");
add_ext(cert, &v3ctx, NID_key_usage, "critical,digitalSignature,keyEncipherment");
add_ext(cert, &v3ctx, NID_ext_key_usage, "serverAuth");
add_ext(cert, &v3ctx, NID_subject_key_identifier, "hash");
add_ext(cert, &v3ctx, NID_authority_key_identifier, "keyid:always");
std::string san = "DNS:" + host;
if (host.rfind("www.", 0) == 0) san += ",DNS:" + host.substr(4);
else san += ",DNS:www." + host;
add_ext(cert, &v3ctx, NID_subject_alt_name, san.c_str());
if (X509_sign(cert, ca->key, EVP_sha256()) <= 0) {
std::cerr << "[tls-intercept] leaf sign failed: " << drain_errors() << "\n";
X509_free(cert); EVP_PKEY_free(leaf_key); return nullptr;
}
auto ssl_ctx = std::make_shared<asio::ssl::context>(asio::ssl::context::tls_server);
SSL_CTX* native = ssl_ctx->native_handle();
SSL_CTX_set_min_proto_version(native, TLS1_2_VERSION);
SSL_CTX_set_options(native,
SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
SSL_OP_NO_COMPRESSION | SSL_OP_CIPHER_SERVER_PREFERENCE);
SSL_CTX_set_mode(native, SSL_MODE_AUTO_RETRY);
SSL_CTX_set_cipher_list(native, "HIGH:!aNULL:!MD5");
#ifdef TLS1_3_VERSION
SSL_CTX_set_ciphersuites(native,
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256");
#endif
bool ok = SSL_CTX_use_certificate(native, cert) > 0 &&
SSL_CTX_use_PrivateKey(native, leaf_key) > 0 &&
SSL_CTX_check_private_key(native) > 0;
if (ok) SSL_CTX_add_extra_chain_cert(native, X509_dup(ca->cert));
X509_free(cert);
EVP_PKEY_free(leaf_key);
if (!ok) {
std::cerr << "[tls-intercept] SSL_CTX setup failed: " << drain_errors() << "\n";
return nullptr;
}
char buf[256];
std::snprintf(buf, sizeof(buf),
_("[tls-intercept] leaf cert pronto per: %s\n"), host.c_str());
std::cerr << buf;
return ssl_ctx;
}
struct CacheEntry {
std::shared_ptr<asio::ssl::context> ctx;
Clock::time_point expires_at;
};
static std::shared_mutex g_ctx_mutex;
static std::unordered_map<std::string, CacheEntry> g_ctx_cache;
static inline void evict_expired() {
const auto now = Clock::now();
for (auto it = g_ctx_cache.begin(); it != g_ctx_cache.end(); )
it = (it->second.expires_at <= now) ? g_ctx_cache.erase(it) : ++it;
}
static inline std::shared_ptr<asio::ssl::context>
get_or_create_ctx(const std::string& host)
{
{
std::shared_lock lock(g_ctx_mutex);
auto it = g_ctx_cache.find(host);
if (it != g_ctx_cache.end() && it->second.expires_at > Clock::now())
return it->second.ctx;
}
auto ca = get_ca();
if (!ca) return nullptr;
auto new_ctx = make_ssl_ctx_for_host(host, ca);
if (!new_ctx) return nullptr;
{
std::unique_lock lock(g_ctx_mutex);
evict_expired();
g_ctx_cache[host] = {
new_ctx,
Clock::now() + std::chrono::seconds(CTX_CACHE_TTL_SEC)
};
}
return new_ctx;
}
static inline std::string make_tls_response(const std::string& host,
const std::string& type)
{
const std::string full = BlockPage::make(host, type);
const auto sep = full.find("\r\n\r\n");
const std::string body = (sep != std::string::npos) ? full.substr(sep + 4) : full;
std::ostringstream oss;
oss << "HTTP/1.1 200 OK\r\n"
<< "Content-Type: text/html; charset=UTF-8\r\n"
<< "Content-Length: " << body.size() << "\r\n"
<< "Cache-Control: no-store, no-cache\r\n"
<< "X-Frame-Options: DENY\r\n"
<< "X-BastionGuard: intercepted\r\n"
<< "Connection: close\r\n\r\n"
<< body;
return oss.str();
}
} // namespace detail
static inline std::string extract_path_from_request(const std::string& raw) {
// Cerca la prima riga (fino a \r\n o \n)
const auto eol = raw.find('\n');
const std::string line = (eol != std::string::npos)
? raw.substr(0, eol) : raw;
const auto s1 = line.find(' ');
if (s1 == std::string::npos) return "/";
const auto s2 = line.find(' ', s1 + 1);
const std::string path = (s2 != std::string::npos)
? line.substr(s1 + 1, s2 - s1 - 1)
: line.substr(s1 + 1);
if (!path.empty() && path.back() == '\r')
return path.substr(0, path.size() - 1);
return path.empty() ? "/" : path;
}
static inline std::shared_ptr<asio::ssl::context>
make_stealth_upstream_ctx(const std::string& host)
{
auto ctx = std::make_shared<asio::ssl::context>(asio::ssl::context::tls_client);
SSL_CTX* native = ctx->native_handle();
// Disabilita protocolli obsoleti e opzioni che non usa Chrome
SSL_CTX_set_options(native,
SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 |
SSL_OP_NO_COMPRESSION);
SSL_CTX_set_min_proto_version(native, TLS1_2_VERSION);
SSL_CTX_set_cipher_list(native,
"TLS_AES_128_GCM_SHA256:"
"TLS_AES_256_GCM_SHA384:"
"TLS_CHACHA20_POLY1305_SHA256:"
"ECDHE-ECDSA-AES128-GCM-SHA256:"
"ECDHE-RSA-AES128-GCM-SHA256:"
"ECDHE-ECDSA-AES256-GCM-SHA384:"
"ECDHE-RSA-AES256-GCM-SHA384:"
"ECDHE-ECDSA-CHACHA20-POLY1305:"
"ECDHE-RSA-CHACHA20-POLY1305:"
"ECDHE-RSA-AES128-SHA:"
"ECDHE-RSA-AES256-SHA:"
"AES128-GCM-SHA256:"
"AES256-GCM-SHA384:"
"AES128-SHA:"
"AES256-SHA");
#ifdef TLS1_3_VERSION
SSL_CTX_set_ciphersuites(native,
"TLS_AES_128_GCM_SHA256:"
"TLS_AES_256_GCM_SHA384:"
"TLS_CHACHA20_POLY1305_SHA256");
#endif
// Il proxy trasparente sotto usa HTTP/1.1 byte-for-byte. Non negoziare
// h2 upstream: inviare una request HTTP/1.1 su una connessione ALPN h2
// corromperebbe il flusso.
static const unsigned char alpn[] = "\x08http/1.1";
SSL_CTX_set_alpn_protos(native, alpn, sizeof(alpn) - 1);
SSL_CTX_set_default_verify_paths(native);
SSL_CTX_set_verify(native, SSL_VERIFY_PEER, nullptr);
(void)host;
return ctx;
}
// -----------------------------------------------------------------------------
// Fetch Metadata / transaction passthrough
//
// Una navigazione "sicura" non e' sempre una pagina top-level. 3-D Secure,
// SCA, OTP e diversi flow bancari usano iframe, fetch/XHR o form POST verso
// ACS/issuer. Trasformare queste richieste in una nuova navigazione CEF perde
// POST body, cookie e contesto della transazione.
//
// Regola generica:
// * top-level GET/HEAD document -> comportamento BastionGuard tradizionale
// (apri SecureBrowser e mostra block page)
// * iframe/subresource -> inoltro HTTPS trasparente e verificato
// * qualunque metodo stateful -> inoltro HTTPS trasparente e verificato
// (POST/PUT/PATCH/DELETE, anche popup)
//
// In questo modo non servono eccezioni per "3ds4.*", "acs.*", ecc.
// -----------------------------------------------------------------------------
static inline std::string lower_ascii(std::string v) {
std::transform(v.begin(), v.end(), v.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return v;
}
static inline std::string trim_ascii(std::string v) {
while (!v.empty() && std::isspace(static_cast<unsigned char>(v.front())))
v.erase(v.begin());
while (!v.empty() && std::isspace(static_cast<unsigned char>(v.back())))
v.pop_back();
return v;
}
static inline std::string request_method_from_raw(const std::string& raw) {
const auto eol = raw.find("\r\n");
const std::string line = raw.substr(0, eol);
const auto sp = line.find(' ');
if (sp == std::string::npos) return {};
return lower_ascii(line.substr(0, sp));
}
static inline std::string header_value_ci(const std::string& raw,
const std::string& wanted) {
const std::string wanted_l = lower_ascii(wanted);
const auto first_eol = raw.find("\r\n");
if (first_eol == std::string::npos) return {};
std::size_t pos = first_eol + 2;
while (pos < raw.size()) {
const auto eol = raw.find("\r\n", pos);
if (eol == std::string::npos || eol == pos) break;
const auto colon = raw.find(':', pos);
if (colon != std::string::npos && colon < eol) {
std::string name = lower_ascii(raw.substr(pos, colon - pos));
if (name == wanted_l)
return trim_ascii(raw.substr(colon + 1, eol - colon - 1));
}
pos = eol + 2;
}
return {};
}
static inline bool is_stateful_method(const std::string& method_l) {
return !(method_l == "get" || method_l == "head" || method_l == "options");
}
static inline bool should_passthrough_transaction(const std::string& raw) {
const std::string method = request_method_from_raw(raw);
if (method.empty()) return false; // fail verso il comportamento storico
// Mai trasformare una POST/PUT/PATCH/DELETE in una GET dentro un altro
// browser: perderemmo i dati della transazione.
if (is_stateful_method(method)) return true;
const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest"));
if (dest == "iframe" || dest == "frame" || dest == "embed" ||
dest == "object" || dest == "empty" || dest == "script" ||
dest == "style" || dest == "image" || dest == "font" ||
dest == "audio" || dest == "video" || dest == "track" ||
dest == "worker" || dest == "sharedworker" ||
dest == "serviceworker" || dest == "manifest") {
return true;
}
const std::string mode = lower_ascii(header_value_ci(raw, "sec-fetch-mode"));
if (mode == "cors" || mode == "no-cors" || mode == "same-origin" ||
mode == "websocket") {
return true;
}
const std::string purpose = lower_ascii(header_value_ci(raw, "purpose"));
const std::string sec_purpose = lower_ascii(header_value_ci(raw, "sec-purpose"));
if (purpose.find("prefetch") != std::string::npos ||
sec_purpose.find("prefetch") != std::string::npos) {
return true;
}
// dest=document + mode=navigate, oppure header Fetch Metadata assenti:
// conserva il comportamento top-level esistente.
return false;
}
static inline bool parse_content_length(const std::string& raw,
std::size_t& out_len) {
const std::string v = trim_ascii(header_value_ci(raw, "content-length"));
if (v.empty()) return false;
errno = 0;
char* end = nullptr;
const unsigned long long n = std::strtoull(v.c_str(), &end, 10);
if (errno != 0 || end == v.c_str() || (end && *end != '\0') ||
n > std::numeric_limits<std::size_t>::max()) {
return false;
}
out_len = static_cast<std::size_t>(n);
return true;
}
static inline bool is_chunked_request(const std::string& raw) {
const std::string te = lower_ascii(header_value_ci(raw, "transfer-encoding"));
return te.find("chunked") != std::string::npos;
}
static inline bool read_more(asio::ssl::stream<tcp::socket>& tls,
std::string& raw,
std::size_t min_total,
std::size_t max_total) {
std::array<char, 16 * 1024> buf{};
while (raw.size() < min_total) {
if (raw.size() >= max_total) return false;
boost::system::error_code ec;
const std::size_t room = std::min<std::size_t>(buf.size(), max_total - raw.size());
const std::size_t n = tls.read_some(asio::buffer(buf.data(), room), ec);
if (n) raw.append(buf.data(), n);
if (ec) return false;
}
return true;
}
static inline bool read_complete_chunked(asio::ssl::stream<tcp::socket>& tls,
std::string& raw,
std::size_t body_start,
std::size_t max_total) {
std::size_t cur = body_start;
std::array<char, 16 * 1024> buf{};
auto ensure_line = [&](std::size_t from, std::size_t& eol) -> bool {
for (;;) {
eol = raw.find("\r\n", from);
if (eol != std::string::npos) return true;
if (raw.size() >= max_total) return false;
boost::system::error_code ec;
const std::size_t room = std::min<std::size_t>(buf.size(), max_total - raw.size());
const std::size_t n = tls.read_some(asio::buffer(buf.data(), room), ec);
if (n) raw.append(buf.data(), n);
if (ec) return false;
}
};
for (;;) {
std::size_t eol = 0;
if (!ensure_line(cur, eol)) return false;
std::string size_line = raw.substr(cur, eol - cur);
const auto semi = size_line.find(';');
if (semi != std::string::npos) size_line.erase(semi);
size_line = trim_ascii(size_line);
if (size_line.empty()) return false;
errno = 0;
char* end = nullptr;
const unsigned long long chunk = std::strtoull(size_line.c_str(), &end, 16);
if (errno != 0 || end == size_line.c_str() || (end && *end != '\0') ||
chunk > max_total) {
return false;
}
cur = eol + 2;
if (chunk == 0) {
// Trailer section: zero o piu' header, terminati da una riga vuota.
for (;;) {
if (!ensure_line(cur, eol)) return false;
if (eol == cur) return true;
cur = eol + 2;
}
}
const std::size_t need = cur + static_cast<std::size_t>(chunk) + 2;
if (need < cur || need > max_total) return false;
if (!read_more(tls, raw, need, max_total)) return false;
if (raw.compare(cur + static_cast<std::size_t>(chunk), 2, "\r\n") != 0)
return false;
cur = need;
}
}
static inline bool read_complete_http1_request(asio::ssl::stream<tcp::socket>& tls,
std::string& raw) {
static constexpr std::size_t MAX_REQUEST = 32u * 1024u * 1024u;
const auto hdr_end_pos = raw.find("\r\n\r\n");
if (hdr_end_pos == std::string::npos) return false;
const std::size_t body_start = hdr_end_pos + 4;
std::size_t content_len = 0;
if (parse_content_length(raw, content_len)) {
if (content_len > MAX_REQUEST || body_start > MAX_REQUEST - content_len)
return false;
return read_more(tls, raw, body_start + content_len, MAX_REQUEST);
}
if (is_chunked_request(raw))
return read_complete_chunked(tls, raw, body_start, MAX_REQUEST);
return true;
}
static inline std::string force_connection_close(const std::string& raw) {
const auto hdr_end = raw.find("\r\n\r\n");
if (hdr_end == std::string::npos) return raw;
const std::string headers = raw.substr(0, hdr_end);
const std::string body = raw.substr(hdr_end + 4);
std::istringstream in(headers);
std::ostringstream out;
std::string line;
bool first = true;
while (std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (first) {
out << line << "\r\n";
first = false;
continue;
}
const auto colon = line.find(':');
const std::string name = colon == std::string::npos
? std::string{} : lower_ascii(trim_ascii(line.substr(0, colon)));
if (name == "connection" || name == "proxy-connection" || name == "keep-alive")
continue;
out << line << "\r\n";
}
out << "Connection: close\r\n\r\n";
out << body;
return out.str();
}
static inline bool forward_transaction_https(asio::ssl::stream<tcp::socket>& client_tls,
const std::string& host,
std::string raw_request) {
if (!read_complete_http1_request(client_tls, raw_request)) {
std::cerr << "[tls-intercept] transaction request incomplete/too large for "
<< host << "\n";
return false;
}
raw_request = force_connection_close(raw_request);
try {
asio::io_context io;
tcp::resolver resolver(io);
auto endpoints = resolver.resolve(host, "443");
tcp::socket upstream_socket(io);
asio::connect(upstream_socket, endpoints);
auto upstream_ctx = make_stealth_upstream_ctx(host);
asio::ssl::stream<tcp::socket> upstream(std::move(upstream_socket), *upstream_ctx);
SSL* ssl = upstream.native_handle();
if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) {
std::cerr << "[tls-intercept] SNI setup failed for " << host << "\n";
return false;
}
X509_VERIFY_PARAM* param = SSL_get0_param(ssl);
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (X509_VERIFY_PARAM_set1_host(param, host.c_str(), 0) != 1) {
std::cerr << "[tls-intercept] hostname verification setup failed for "
<< host << "\n";
return false;
}
upstream.set_verify_mode(asio::ssl::verify_peer);
upstream.handshake(asio::ssl::stream_base::client);
asio::write(upstream, asio::buffer(raw_request));
std::array<char, 64 * 1024> buf{};
for (;;) {
boost::system::error_code rec;
const std::size_t n = upstream.read_some(asio::buffer(buf), rec);
if (n) {
boost::system::error_code wec;
asio::write(client_tls, asio::buffer(buf.data(), n), wec);
if (wec) return false;
}
if (rec) {
if (rec != asio::error::eof &&
rec != asio::ssl::error::stream_truncated) {
std::cerr << "[tls-intercept] upstream read failed for " << host
<< ": " << rec.message() << "\n";
}
break;
}
}
boost::system::error_code sd_ec;
upstream.shutdown(sd_ec);
return true;
} catch (const std::exception& e) {
std::cerr << "[tls-intercept] transaction passthrough failed for " << host
<< ": " << e.what() << "\n";
return false;
}
}
static inline void send_passthrough_failure(asio::ssl::stream<tcp::socket>& tls) {
static constexpr char body[] = "BastionGuard: upstream secure transaction failed\n";
std::ostringstream oss;
oss << "HTTP/1.1 502 Bad Gateway\r\n"
<< "Content-Type: text/plain; charset=UTF-8\r\n"
<< "Content-Length: " << (sizeof(body) - 1) << "\r\n"
<< "Cache-Control: no-store\r\n"
<< "Connection: close\r\n\r\n"
<< body;
const std::string resp = oss.str();
boost::system::error_code ec;
asio::write(tls, asio::buffer(resp), ec);
}
static inline void do_intercept(tcp::socket sock,
const std::string& host,
const std::string& type,
std::function<void(const std::string&)> on_url_ready = nullptr)
{
std::cerr << "[tls-intercept] intercepting " << host << "\n";
auto ssl_ctx = detail::get_or_create_ctx(host);
if (!ssl_ctx) {
std::cerr << "[tls-intercept] ctx setup failed for " << host << "\n";
boost::system::error_code ignore;
sock.close(ignore);
return;
}
try {
asio::ssl::stream<tcp::socket> tls(std::move(sock), *ssl_ctx);
boost::system::error_code hs_ec;
tls.handshake(asio::ssl::stream_base::server, hs_ec);
if (hs_ec) {
std::cerr << "[tls-intercept] handshake failed for " << host
<< ": " << hs_ec.message() << "\n";
boost::system::error_code ignore;
tls.lowest_layer().close(ignore);
return;
}
std::cerr << "[tls-intercept] handshake ok for " << host << "\n";
asio::streambuf rbuf;
boost::system::error_code read_ec;
asio::read_until(tls, rbuf, "\r\n\r\n", read_ec);
if (read_ec) {
std::cerr << "[tls-intercept] request header read failed for " << host
<< ": " << read_ec.message() << "\n";
boost::system::error_code ignore;
tls.lowest_layer().close(ignore);
return;
}
const std::string raw{
asio::buffers_begin(rbuf.data()),
asio::buffers_end(rbuf.data())
};
if (should_passthrough_transaction(raw)) {
const std::string method = request_method_from_raw(raw);
const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest"));
std::cerr << "[tls-intercept] iframe/stateful passthrough: host=" << host
<< " method=" << method << " dest=" << dest << "\n";
if (!forward_transaction_https(tls, host, raw))
send_passthrough_failure(tls);
boost::system::error_code sd_ec;
tls.shutdown(sd_ec);
boost::system::error_code ignore;
tls.lowest_layer().close(ignore);
return;
}
// Solo una vera navigazione top-level GET/HEAD viene trasformata in
// apertura SecureBrowser. Il callback NON viene chiamato per iframe,
// fetch/XHR o POST, quindi nessuna sessione/gate fittizia viene creata.
if (on_url_ready) {
const std::string path = extract_path_from_request(raw);
const std::string full_url = "https://" + host + path;
std::cerr << "[tls-intercept] top-level secure URL: " << full_url << "\n";
on_url_ready(full_url);
}
const std::string resp = detail::make_tls_response(host, type);
boost::system::error_code write_ec;
asio::write(tls, asio::buffer(resp), write_ec);
if (write_ec)
std::cerr << "[tls-intercept] write error for " << host
<< ": " << write_ec.message() << "\n";
else
std::cerr << "[tls-intercept] page served for " << host << "\n";
boost::system::error_code sd_ec;
tls.shutdown(sd_ec);
boost::system::error_code ignore;
tls.lowest_layer().close(ignore);
} catch (const std::exception& e) {
std::cerr << "[tls-intercept] exception for " << host
<< ": " << e.what() << "\n";
boost::system::error_code ignore;
sock.close(ignore);
}
}
} // namespace TlsIntercept