Skip to content
API Documentation
BastionGuard™
BastionGuard-pac.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21
22#include "../DistroUtils.hpp"
23#include "../SafeProcess.hpp"
24
25#include <boost/asio.hpp>
26#include <boost/asio/signal_set.hpp>
27
28#include <nlohmann/json.hpp>
29
30#include <algorithm>
31#include <atomic>
32#include <cctype>
33#include <chrono>
34#include <cstdlib>
35#include <filesystem>
36#include <fstream>
37#include <iostream>
38#include <mutex>
39#include <sstream>
40#include <string>
41#include <thread>
42#include <vector>
43
44namespace fs = std::filesystem;
45namespace asio = boost::asio;
46using tcp = asio::ip::tcp;
47using json = nlohmann::json;
48
49
50static std::string home_dir() {
51 const char* h = std::getenv("HOME");
52 return (h && *h) ? std::string(h) : std::string();
53}
54
55static fs::path xdg_data_home() {
56 if (const char* xdg = std::getenv("XDG_DATA_HOME"); xdg && *xdg) {
57 return fs::path(xdg);
58 }
59 const std::string h = home_dir();
60 if (!h.empty()) return fs::path(h) / ".local" / "share";
61 return {};
62}
63
64static fs::path payments_json_user_path() {
65 fs::path base = xdg_data_home();
66 if (base.empty()) return {};
67 return base / "BastionGuard" / "payments" / "payments.json";
68}
69
70static fs::path payments_json_system_path() {
71 return fs::path("/usr/share/BastionGuard/data/payments/payments.json");
72}
73
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;
78 ss << in.rdbuf();
79 out = ss.str();
80 return true;
81}
82
83static bool write_all_atomic(const fs::path& p, const std::string& data) {
84 std::error_code ec;
85 fs::create_directories(p.parent_path(), ec);
86
87 fs::path tmp = p;
88 tmp += ".tmp";
89
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());
93 out.close();
94 if (!out.good()) return false;
95
96 fs::rename(tmp, p, ec);
97 if (!ec) return true;
98
99 try {
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());
103 out2.close();
104 std::error_code ec2;
105 fs::remove(tmp, ec2);
106 return out2.good();
107 } catch (...) {
108 return false;
109 }
110}
111
112static std::string to_lower(std::string s) {
113 std::transform(s.begin(), s.end(), s.begin(),
114 [](unsigned char c){ return (char)std::tolower(c); });
115 return s;
116}
117
118static std::string normalize_domain_or_url(std::string in) {
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);
123
124 auto p = in.find("://");
125 if (p != std::string::npos) in.erase(0, p + 3);
126
127 p = in.find('/');
128 if (p != std::string::npos) in.erase(p);
129
130 p = in.find('@');
131 if (p != std::string::npos) in = in.substr(p + 1);
132
133 p = in.find(':');
134 if (p != std::string::npos) in.erase(p);
135
136 if (in.rfind("www.", 0) == 0) in.erase(0, 4);
137
138 in = to_lower(in);
139
140 auto is_ok = [](char c){
141 return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '-';
142 };
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 {};
147 return in;
148}
149
151 fs::path u = payments_json_user_path();
152 fs::path s = payments_json_system_path();
153 if (u.empty()) return;
154
155 std::error_code ec;
156 bool u_exists = fs::exists(u, ec) && !ec;
157 ec.clear();
158 bool s_exists = fs::exists(s, ec) && !ec;
159
160 if (u_exists) return;
161
162 if (s_exists) {
163 std::string data;
164 if (read_all(s, data)) {
165 (void)write_all_atomic(u, data);
166 }
167 return;
168 }
169
170 json j;
171 j["version"] = 1;
172 j["updated_at"] = "1970-01-01";
173 j["enabled"] = false;
174 j["list"] = json::array({
175 "paypal.com",
176 "stripe.com",
177 "adyen.com",
178 "klarna.com",
179 "checkout.com",
180 "worldpay.com",
181 "braintreepayments.com",
182 "braintreegateway.com",
183 "amazonpay.com",
184 "pay.google.com",
185 "pay.apple.com",
186 "amazon.com"
187 });
188 (void)write_all_atomic(u, j.dump(2));
189}
190
191static std::vector<std::string> load_payments_domains_from_user_json() {
193 fs::path u = payments_json_user_path();
194
195 std::vector<std::string> out;
196 try {
197 std::ifstream in(u, std::ios::binary);
198 if (!in.good()) return out;
199
200 json j; in >> j;
201 if (j.contains("list") && j["list"].is_array()) {
202 for (const auto& v : j["list"]) {
203 if (!v.is_string()) continue;
204 auto d = normalize_domain_or_url(v.get<std::string>());
205 if (!d.empty()) out.push_back(d);
206 }
207 }
208
209 std::sort(out.begin(), out.end());
210 out.erase(std::unique(out.begin(), out.end()), out.end());
211 } catch (...) {
212 }
213 return out;
214}
215
217 std::vector<std::string> domains;
218 fs::file_time_type last_mtime{};
219 bool loaded = false;
220};
221
222static std::string generate_pac(const std::vector<std::string>& pay_domains,
223 const std::string& stub_proxy_host,
224 int stub_proxy_port)
225{
226 std::ostringstream pac;
227
228 pac << "// BastionGuard - Secure Payments PAC\n";
229 pac << "// Generated from payments.json (served by bastionguard-pacd)\n\n";
230
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";
233 pac << "}\n\n";
234
235 pac << "function FindProxyForURL(url, host) {\n";
236 pac << " host = host.toLowerCase();\n\n";
237
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";
247 pac << " }\n\n";
248
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 << ",";
253 pac << "\n";
254 }
255 pac << " ];\n\n";
256
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";
260 pac << " }\n";
261 pac << " }\n\n";
262
263 pac << " return \"DIRECT\";\n";
264 pac << "}\n";
265
266 return pac.str();
267}
268
269
270static std::string http_response_200(const std::string& body, const std::string& content_type) {
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";
279 oss << body;
280 return oss.str();
281}
282
283static std::string http_response_404() {
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";
290 oss << body;
291 return oss.str();
292}
293
294static std::string http_response_405() {
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";
301 oss << body;
302 return oss.str();
303}
304
305
306static int run_trigger_cmd(const std::string& cmd) {
307 // A trigger is reached from a localhost TCP listener. Never pass its
308 // configurable command string to /bin/sh. Only allow the packaged action.
309 if (cmd.empty()) return 0;
310 if (cmd != "/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service")
311 return 64;
312 return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user",
313 "start", "BastionGuard-cef.service"}).exit_code;
314}
315
316static bool wait_backend_up(const std::string& backend_host, int backend_port, int wait_ms) {
317 try {
318 asio::io_context io;
319 tcp::resolver res(io);
320 auto eps = res.resolve(backend_host, std::to_string(backend_port));
321
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) {
325 tcp::socket s(io);
326 boost::system::error_code ec;
327 s.connect(ep.endpoint(), ec);
328 if (!ec) return true;
329 }
330 std::this_thread::sleep_for(std::chrono::milliseconds(50));
331 }
332 } catch (...) {}
333 return false;
334}
335
336static void forward_bidirectional(tcp::socket a, tcp::socket b) {
337 auto pump = [](tcp::socket& src, tcp::socket& dst) {
338 try {
339 std::array<char, 16 * 1024> buf{};
340 for (;;) {
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));
345 }
346 } catch (...) {}
347 // chiusura best-effort
348 boost::system::error_code ignore;
349 dst.shutdown(tcp::socket::shutdown_send, ignore);
350 };
351
352 std::thread t1(pump, std::ref(a), std::ref(b));
353 std::thread t2(pump, std::ref(b), std::ref(a));
354 t1.join();
355 t2.join();
356
357 boost::system::error_code ignore;
358 a.close(ignore);
359 b.close(ignore);
360}
361
363 std::atomic<bool> started{false};
364 std::mutex mtx;
365 std::chrono::steady_clock::time_point last_start{};
366};
367
368static void handle_proxy_client(tcp::socket client,
369 TriggerState& trig,
370 const std::string& trigger_cmd,
371 const std::string& backend_host,
372 int backend_port,
373 int backend_wait_ms)
374{
375 {
376 std::lock_guard<std::mutex> lk(trig.mtx);
377 auto now = std::chrono::steady_clock::now();
378 bool should_start = !trig.started.load();
379
380 if (!should_start) {
381 if (trig.last_start.time_since_epoch().count() != 0 &&
382 now - trig.last_start < std::chrono::seconds(1)) {
383 should_start = false;
384 }
385 }
386
387 if (should_start) {
388 trig.last_start = now;
389 int rc = run_trigger_cmd(trigger_cmd);
390 if (rc == 0) {
391 trig.started.store(true);
392 std::cerr << "[pacd] trigger executed: " << trigger_cmd << "\n";
393 } else {
394 std::cerr << "[pacd] trigger failed rc=" << rc << ": " << trigger_cmd << "\n";
395 }
396 }
397 }
398
399 if (!wait_backend_up(backend_host, backend_port, backend_wait_ms)) {
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);
404 return;
405 }
406
407 try {
408 asio::io_context io;
409 tcp::resolver res(io);
410 auto eps = res.resolve(backend_host, std::to_string(backend_port));
411
412 tcp::socket backend(io);
413 boost::system::error_code ec;
414 asio::connect(backend, eps, ec);
415 if (ec) {
416 std::cerr << "[pacd] connect backend failed: " << ec.message() << "\n";
417 boost::system::error_code ignore;
418 client.close(ignore);
419 return;
420 }
421
422 forward_bidirectional(std::move(client), std::move(backend));
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);
427 }
428}
429
430
431static void serve_pac_one(tcp::socket sock,
432 PaymentsCache& cache,
433 std::mutex& cache_mtx,
434 const std::string& stub_proxy_host,
435 int stub_proxy_port)
436{
437 try {
438 asio::streambuf buf;
439 asio::read_until(sock, buf, "\r\n\r\n");
440
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();
446
447 std::string method, path, ver;
448 {
449 std::istringstream rl(request_line);
450 rl >> method >> path >> ver;
451 }
452
453 if (method != "GET") {
454 auto resp = http_response_405();
455 asio::write(sock, asio::buffer(resp));
456 return;
457 }
458 if (path != "/proxy.pac") {
459 auto resp = http_response_404();
460 asio::write(sock, asio::buffer(resp));
461 return;
462 }
463
465 fs::path pj = payments_json_user_path();
466
467 std::error_code ec;
468 fs::file_time_type mtime = fs::last_write_time(pj, ec);
469
470 std::vector<std::string> domains;
471 {
472 std::lock_guard<std::mutex> lk(cache_mtx);
473 if (!cache.loaded || (!ec && mtime != cache.last_mtime)) {
475 cache.last_mtime = mtime;
476 cache.loaded = true;
477 }
478 domains = cache.domains;
479 }
480
481 std::string pac = generate_pac(domains, stub_proxy_host, stub_proxy_port);
482 auto resp = http_response_200(pac, "application/x-ns-proxy-autoconfig");
483 asio::write(sock, asio::buffer(resp));
484 } catch (...) {
485 }
486}
487
488
489
490int main(int argc, char** argv) {
491 // PAC server
492 std::string listen_host = "127.0.0.1";
493 int listen_port = 8765;
494
495 std::string stub_host = "127.0.0.1";
496 int stub_port = 3129;
497
498 std::string backend_host = "127.0.0.1";
499 int backend_port = 3130;
500
501 std::string trigger_cmd = "/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service";
502
503 int backend_wait_ms = 2000;
504
505 // args
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){
510 if (i + 1 < argc) {
511 try {
512 v = std::stoi(argv[++i]);
513 } catch (...) {
514 std::cerr << "[pacd] ⚠️ argomento non numerico per " << a
515 << ": " << argv[i] << " (uso default " << v << ")\n";
516 }
517 }
518 };
519
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") {
529 std::cout <<
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";
536 return 0;
537 }
538 }
539
541 trigger_cmd.clear();
542 std::cerr << "[pacd] CEF disabilitato: il comando di avvio backend è stato rimosso.\n";
543 }
544
546 fs::path u = payments_json_user_path();
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";
552
553 std::atomic<bool> running{true};
554
555 // Gestione segnali (stop pulito)
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){
559 if (sig == SIGHUP) {
560 std::cerr << "[pacd] SIGHUP received, exiting for restart\n";
561 }
562 running.store(false);
563 sigio.stop();
564 });
565 std::thread signal_thread([&]{ sigio.run(); });
566
567 PaymentsCache cache;
568 std::mutex cache_mtx;
569
570 TriggerState trig;
571
572 std::thread pac_thread([&]{
573 try {
574 asio::io_context io;
575 tcp::endpoint ep(asio::ip::make_address(listen_host), (unsigned short)listen_port);
576 tcp::acceptor acceptor(io, ep);
577
578 while (running.load()) {
579 tcp::socket sock(io);
580 boost::system::error_code ec;
581 acceptor.accept(sock, ec);
582 if (ec) {
583 if (!running.load()) break;
584 continue;
585 }
586 serve_pac_one(std::move(sock), cache, cache_mtx, stub_host, stub_port);
587 }
588 } catch (const std::exception& ex) {
589 std::cerr << "[pacd] PAC server fatal: " << ex.what() << "\n";
590 }
591 });
592
593 std::thread proxy_thread([&]{
594 try {
595 asio::io_context io;
596 tcp::endpoint ep(asio::ip::make_address(stub_host), (unsigned short)stub_port);
597 tcp::acceptor acceptor(io, ep);
598
599 while (running.load()) {
600 tcp::socket client(io);
601 boost::system::error_code ec;
602 acceptor.accept(client, ec);
603 if (ec) {
604 if (!running.load()) break;
605 continue;
606 }
607 std::thread(
609 std::move(client),
610 std::ref(trig),
611 trigger_cmd,
612 backend_host,
613 backend_port,
614 backend_wait_ms
615 ).detach();
616 }
617 } catch (const std::exception& ex) {
618 std::cerr << "[pacd] proxy stub fatal: " << ex.what() << "\n";
619 }
620 });
621
622 pac_thread.join();
623 proxy_thread.join();
624
625 if (signal_thread.joinable()) signal_thread.join();
626 return 0;
627}
nlohmann::json json
Definition Backend.cpp:2908
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[])
asio::ip::tcp tcp
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