25#include <boost/asio.hpp>
26#include <boost/asio/signal_set.hpp>
28#include <nlohmann/json.hpp>
44namespace fs = std::filesystem;
45namespace asio = boost::asio;
46using tcp = asio::ip::tcp;
47using json = nlohmann::json;
51 const char* h = std::getenv(
"HOME");
52 return (h && *h) ? std::string(h) : std::string();
56 if (
const char* xdg = std::getenv(
"XDG_DATA_HOME"); xdg && *xdg) {
60 if (!h.empty())
return fs::path(h) /
".local" /
"share";
66 if (base.empty())
return {};
67 return base /
"BastionGuard" /
"payments" /
"payments.json";
71 return fs::path(
"/usr/share/BastionGuard/data/payments/payments.json");
74static bool read_all(
const fs::path& p, std::string& out) {
75 std::ifstream in(p, std::ios::binary);
76 if (!in.good())
return false;
77 std::ostringstream ss;
85 fs::create_directories(p.parent_path(), ec);
90 std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
91 if (!out.good())
return false;
92 out.write(data.data(), (std::streamsize)data.size());
94 if (!out.good())
return false;
96 fs::rename(tmp, p, ec);
100 std::ofstream out2(p, std::ios::binary | std::ios::trunc);
101 if (!out2.good())
return false;
102 out2.write(data.data(), (std::streamsize)data.size());
105 fs::remove(tmp, ec2);
113 std::transform(s.begin(), s.end(), s.begin(),
114 [](
unsigned char c){ return (char)std::tolower(c); });
119 auto l = in.find_first_not_of(
" \t\r\n");
120 auto r = in.find_last_not_of(
" \t\r\n");
121 if (l == std::string::npos)
return {};
122 in = in.substr(l, r - l + 1);
124 auto p = in.find(
"://");
125 if (p != std::string::npos) in.erase(0, p + 3);
128 if (p != std::string::npos) in.erase(p);
131 if (p != std::string::npos) in = in.substr(p + 1);
134 if (p != std::string::npos) in.erase(p);
136 if (in.rfind(
"www.", 0) == 0) in.erase(0, 4);
140 auto is_ok = [](
char c){
141 return (c >=
'a' && c <=
'z') || (c >=
'0' && c <=
'9') || c ==
'.' || c ==
'-';
143 if (in.size() < 4)
return {};
144 for (
char c : in)
if (!is_ok(c))
return {};
145 if (in.find(
'.') == std::string::npos)
return {};
146 if (in.front() ==
'.' || in.back() ==
'.')
return {};
153 if (u.empty())
return;
156 bool u_exists = fs::exists(u, ec) && !ec;
158 bool s_exists = fs::exists(s, ec) && !ec;
160 if (u_exists)
return;
172 j[
"updated_at"] =
"1970-01-01";
173 j[
"enabled"] =
false;
174 j[
"list"] = json::array({
181 "braintreepayments.com",
182 "braintreegateway.com",
195 std::vector<std::string> out;
197 std::ifstream in(u, std::ios::binary);
198 if (!in.good())
return out;
201 if (j.contains(
"list") && j[
"list"].is_array()) {
202 for (
const auto& v : j[
"list"]) {
203 if (!v.is_string())
continue;
205 if (!d.empty()) out.push_back(d);
209 std::sort(out.begin(), out.end());
210 out.erase(std::unique(out.begin(), out.end()), out.end());
222static std::string
generate_pac(
const std::vector<std::string>& pay_domains,
223 const std::string& stub_proxy_host,
226 std::ostringstream pac;
228 pac <<
"// BastionGuard - Secure Payments PAC\n";
229 pac <<
"// Generated from payments.json (served by bastionguard-pacd)\n\n";
231 pac <<
"function dnsDomainIs(host, domain) {\n";
232 pac <<
" return host === domain || (host.length > domain.length && host.substring(host.length - domain.length - 1) === \".\" + domain);\n";
235 pac <<
"function FindProxyForURL(url, host) {\n";
236 pac <<
" host = host.toLowerCase();\n\n";
238 pac <<
" // bypass locali\n";
239 pac <<
" if (isPlainHostName(host) ||\n";
240 pac <<
" shExpMatch(host, \"localhost\") ||\n";
241 pac <<
" shExpMatch(host, \"127.*\") ||\n";
242 pac <<
" shExpMatch(host, \"10.*\") ||\n";
243 pac <<
" shExpMatch(host, \"192.168.*\") ||\n";
244 pac <<
" shExpMatch(host, \"172.16.*\") || shExpMatch(host, \"172.17.*\") || shExpMatch(host, \"172.18.*\") ||\n";
245 pac <<
" shExpMatch(host, \"172.19.*\") || shExpMatch(host, \"172.2?.*\") || shExpMatch(host, \"172.3?.*\")) {\n";
246 pac <<
" return \"DIRECT\";\n";
249 pac <<
" var pay = [\n";
250 for (
size_t i = 0; i < pay_domains.size(); ++i) {
251 pac <<
" \"" << pay_domains[i] <<
"\"";
252 if (i + 1 < pay_domains.size()) pac <<
",";
257 pac <<
" for (var i = 0; i < pay.length; i++) {\n";
258 pac <<
" if (dnsDomainIs(host, pay[i])) {\n";
259 pac <<
" return \"PROXY " << stub_proxy_host <<
":" << stub_proxy_port <<
"\";\n";
263 pac <<
" return \"DIRECT\";\n";
271 std::ostringstream oss;
272 oss <<
"HTTP/1.1 200 OK\r\n";
273 oss <<
"Content-Type: " << content_type <<
"\r\n";
274 oss <<
"Cache-Control: no-cache, no-store, must-revalidate\r\n";
275 oss <<
"Pragma: no-cache\r\n";
276 oss <<
"Expires: 0\r\n";
277 oss <<
"Content-Length: " << body.size() <<
"\r\n";
278 oss <<
"Connection: close\r\n\r\n";
284 const std::string body =
"Not Found\n";
285 std::ostringstream oss;
286 oss <<
"HTTP/1.1 404 Not Found\r\n";
287 oss <<
"Content-Type: text/plain\r\n";
288 oss <<
"Content-Length: " << body.size() <<
"\r\n";
289 oss <<
"Connection: close\r\n\r\n";
295 const std::string body =
"Method Not Allowed\n";
296 std::ostringstream oss;
297 oss <<
"HTTP/1.1 405 Method Not Allowed\r\n";
298 oss <<
"Content-Type: text/plain\r\n";
299 oss <<
"Content-Length: " << body.size() <<
"\r\n";
300 oss <<
"Connection: close\r\n\r\n";
309 if (cmd.empty())
return 0;
310 if (cmd !=
"/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service")
312 return bg_process::run({
"/usr/libexec/bastionguard/bastionguard-service",
"--user",
313 "start",
"BastionGuard-cef.service"}).exit_code;
316static bool wait_backend_up(
const std::string& backend_host,
int backend_port,
int wait_ms) {
319 tcp::resolver res(io);
320 auto eps = res.resolve(backend_host, std::to_string(backend_port));
322 auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(wait_ms);
323 while (std::chrono::steady_clock::now() < deadline) {
324 for (
auto& ep : eps) {
326 boost::system::error_code ec;
327 s.connect(ep.endpoint(), ec);
328 if (!ec)
return true;
330 std::this_thread::sleep_for(std::chrono::milliseconds(50));
337 auto pump = [](tcp::socket& src, tcp::socket& dst) {
339 std::array<char, 16 * 1024> buf{};
341 boost::system::error_code ec;
342 std::size_t n = src.read_some(asio::buffer(buf), ec);
343 if (ec || n == 0)
break;
344 asio::write(dst, asio::buffer(buf.data(), n));
348 boost::system::error_code ignore;
349 dst.shutdown(tcp::socket::shutdown_send, ignore);
352 std::thread t1(pump, std::ref(a), std::ref(b));
353 std::thread t2(pump, std::ref(b), std::ref(a));
357 boost::system::error_code ignore;
370 const std::string& trigger_cmd,
371 const std::string& backend_host,
376 std::lock_guard<std::mutex> lk(trig.
mtx);
377 auto now = std::chrono::steady_clock::now();
378 bool should_start = !trig.
started.load();
381 if (trig.
last_start.time_since_epoch().count() != 0 &&
382 now - trig.
last_start < std::chrono::seconds(1)) {
383 should_start =
false;
392 std::cerr <<
"[pacd] trigger executed: " << trigger_cmd <<
"\n";
394 std::cerr <<
"[pacd] trigger failed rc=" << rc <<
": " << trigger_cmd <<
"\n";
400 std::cerr <<
"[pacd] backend not ready " << backend_host <<
":" << backend_port
401 <<
" after " << backend_wait_ms <<
"ms\n";
402 boost::system::error_code ignore;
403 client.close(ignore);
409 tcp::resolver res(io);
410 auto eps = res.resolve(backend_host, std::to_string(backend_port));
412 tcp::socket backend(io);
413 boost::system::error_code ec;
414 asio::connect(backend, eps, ec);
416 std::cerr <<
"[pacd] connect backend failed: " << ec.message() <<
"\n";
417 boost::system::error_code ignore;
418 client.close(ignore);
423 }
catch (
const std::exception& ex) {
424 std::cerr <<
"[pacd] forward fatal: " << ex.what() <<
"\n";
425 boost::system::error_code ignore;
426 client.close(ignore);
433 std::mutex& cache_mtx,
434 const std::string& stub_proxy_host,
439 asio::read_until(sock, buf,
"\r\n\r\n");
441 std::istream is(&buf);
442 std::string request_line;
443 std::getline(is, request_line);
444 if (!request_line.empty() && request_line.back() ==
'\r')
445 request_line.pop_back();
447 std::string method, path, ver;
449 std::istringstream rl(request_line);
450 rl >> method >> path >> ver;
453 if (method !=
"GET") {
455 asio::write(sock, asio::buffer(resp));
458 if (path !=
"/proxy.pac") {
460 asio::write(sock, asio::buffer(resp));
468 fs::file_time_type mtime = fs::last_write_time(pj, ec);
470 std::vector<std::string> domains;
472 std::lock_guard<std::mutex> lk(cache_mtx);
481 std::string pac =
generate_pac(domains, stub_proxy_host, stub_proxy_port);
483 asio::write(sock, asio::buffer(resp));
490int main(
int argc,
char** argv) {
492 std::string listen_host =
"127.0.0.1";
493 int listen_port = 8765;
495 std::string stub_host =
"127.0.0.1";
496 int stub_port = 3129;
498 std::string backend_host =
"127.0.0.1";
499 int backend_port = 3130;
501 std::string trigger_cmd =
"/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service";
503 int backend_wait_ms = 2000;
506 for (
int i = 1; i < argc; ++i) {
507 std::string a = argv[i];
508 auto take = [&](std::string& s){
if (i + 1 < argc) s = argv[++i]; };
509 auto take_int = [&](
int& v){
512 v = std::stoi(argv[++i]);
514 std::cerr <<
"[pacd] ⚠️ argomento non numerico per " << a
515 <<
": " << argv[i] <<
" (uso default " << v <<
")\n";
520 if (a ==
"--listen") take(listen_host);
521 else if (a ==
"--port") take_int(listen_port);
522 else if (a ==
"--stub-host") take(stub_host);
523 else if (a ==
"--stub-port") take_int(stub_port);
524 else if (a ==
"--backend-host") take(backend_host);
525 else if (a ==
"--backend-port") take_int(backend_port);
526 else if (a ==
"--trigger-cmd") take(trigger_cmd);
527 else if (a ==
"--backend-wait-ms") take_int(backend_wait_ms);
528 else if (a ==
"--help") {
530 "bastionguard-pacd\n"
531 " --listen 127.0.0.1 --port 8765 (PAC server)\n"
532 " --stub-host 127.0.0.1 --stub-port 3129 (stub proxy - PAC points here)\n"
533 " --backend-host 127.0.0.1 --backend-port 3130 (CEF proxy backend)\n"
534 " --trigger-cmd \"/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service\"\n"
535 " --backend-wait-ms 2000\n";
542 std::cerr <<
"[pacd] CEF disabilitato: il comando di avvio backend è stato rimosso.\n";
547 std::cerr <<
"[pacd] using payments file: " << u.string() <<
"\n";
548 std::cerr <<
"[pacd] PAC: http://" << listen_host <<
":" << listen_port <<
"/proxy.pac\n";
549 std::cerr <<
"[pacd] stub proxy listening " << stub_host <<
":" << stub_port <<
"\n";
550 std::cerr <<
"[pacd] backend proxy target " << backend_host <<
":" << backend_port <<
"\n";
551 std::cerr <<
"[pacd] trigger: " << trigger_cmd <<
"\n";
553 std::atomic<bool>
running{
true};
556 asio::io_context sigio;
557 asio::signal_set signals(sigio, SIGINT, SIGTERM, SIGHUP);
558 signals.async_wait([&](
const boost::system::error_code&,
int sig){
560 std::cerr <<
"[pacd] SIGHUP received, exiting for restart\n";
565 std::thread signal_thread([&]{ sigio.run(); });
568 std::mutex cache_mtx;
572 std::thread pac_thread([&]{
575 tcp::endpoint ep(asio::ip::make_address(listen_host), (
unsigned short)listen_port);
576 tcp::acceptor acceptor(io, ep);
579 tcp::socket sock(io);
580 boost::system::error_code ec;
581 acceptor.accept(sock, ec);
586 serve_pac_one(std::move(sock), cache, cache_mtx, stub_host, stub_port);
588 }
catch (
const std::exception& ex) {
589 std::cerr <<
"[pacd] PAC server fatal: " << ex.what() <<
"\n";
593 std::thread proxy_thread([&]{
596 tcp::endpoint ep(asio::ip::make_address(stub_host), (
unsigned short)stub_port);
597 tcp::acceptor acceptor(io, ep);
600 tcp::socket client(io);
601 boost::system::error_code ec;
602 acceptor.accept(client, ec);
617 }
catch (
const std::exception& ex) {
618 std::cerr <<
"[pacd] proxy stub fatal: " << ex.what() <<
"\n";
625 if (signal_thread.joinable()) signal_thread.join();
static std::string to_lower(std::string s)
static std::string generate_pac(const std::vector< std::string > &pay_domains, const std::string &stub_proxy_host, int stub_proxy_port)
static void forward_bidirectional(tcp::socket a, tcp::socket b)
static std::vector< std::string > load_payments_domains_from_user_json()
static int run_trigger_cmd(const std::string &cmd)
static void serve_pac_one(tcp::socket sock, PaymentsCache &cache, std::mutex &cache_mtx, const std::string &stub_proxy_host, int stub_proxy_port)
static std::string http_response_404()
static std::string http_response_200(const std::string &body, const std::string &content_type)
static bool read_all(const fs::path &p, std::string &out)
static std::string http_response_405()
static void handle_proxy_client(tcp::socket client, TriggerState &trig, const std::string &trigger_cmd, const std::string &backend_host, int backend_port, int backend_wait_ms)
static fs::path xdg_data_home()
static fs::path payments_json_user_path()
static void seed_user_payments_from_system_if_missing()
static std::string normalize_domain_or_url(std::string in)
static std::string home_dir()
static bool wait_backend_up(const std::string &backend_host, int backend_port, int wait_ms)
static bool write_all_atomic(const fs::path &p, const std::string &data)
static fs::path payments_json_system_path()
static std::atomic< bool > running
int main(int argc, char *argv[])
Result run(const std::vector< std::string > &args, bool merge_stderr=false, std::size_t limit=8U *1024U *1024U, const std::map< std::string, std::string > &environment={}, const std::function< bool(const std::string &)> &on_line={})
std::vector< std::string > domains
fs::file_time_type last_mtime
std::atomic< bool > started
std::chrono::steady_clock::time_point last_start