Skip to content
API Documentation
BastionGuard™
local_warning_server.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
22#include <iostream>
23#include <sstream>
24#include <cstring>
25#include <fstream>
26#include <filesystem>
27#include <algorithm>
28#include <cctype>
29#include <glib/gi18n.h>
30#include <gio/gio.h>
31#include <array>
32#include <sys/stat.h>
33#include <unistd.h>
34#include <vector>
35#include <unordered_set>
36#include <libintl.h>
37
38#include <nlohmann/json.hpp>
39
40namespace fs = std::filesystem;
41
42
43#ifndef BastionGuard_WARNING_DEFAULT_HTTP
44# define BastionGuard_WARNING_DEFAULT_HTTP 81
45#endif
46#ifndef BastionGuard_WARNING_DEFAULT_HTTPS
47# define BastionGuard_WARNING_DEFAULT_HTTPS 444
48#endif
49#ifndef BastionGuard_LOCAL_WARNING_IP
50# define BastionGuard_LOCAL_WARNING_IP "127.0.0.2"
51#endif
52
53
54static std::string to_lower(std::string s) {
55 std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
56 return s;
57}
58
59static std::string clean_domain(std::string d) {
60 d = to_lower(d);
61 if (!d.empty() && d.front() == '.') d.erase(0, 1);
62 if (d.rfind("www.", 0) == 0) d.erase(0, 4);
63 auto p = d.find(':'); if (p != std::string::npos) d.erase(p);
64 if (!d.empty() && d.back() == '.') d.pop_back();
65 return d;
66}
67
68static std::string read_last_lines(const std::string& path, size_t max_lines = 50) {
69 std::ifstream file(path, std::ios::binary);
70 if (!file.is_open()) return _("(Log non disponibile)");
71 std::string content;
72 file.seekg(0, std::ios::end);
73 auto size = file.tellg();
74 if (size <= 0) return _("(Log vuoto)");
75 file.seekg(0);
76 content.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
77
78 std::vector<std::string> lines;
79 size_t start = 0;
80 for (size_t i = 0; i < content.size(); ++i) {
81 if (content[i] == '\n') {
82 lines.emplace_back(content.substr(start, i - start));
83 start = i + 1;
84 }
85 }
86 if (start < content.size()) lines.emplace_back(content.substr(start));
87
88 std::ostringstream out;
89 size_t from = lines.size() > max_lines ? lines.size() - max_lines : 0;
90 for (size_t i = from; i < lines.size(); ++i) out << lines[i] << '\n';
91 std::string res = out.str();
92 return res.empty() ? _("(Nessuna attività recente)") : res;
93}
94
95static std::string html_escape(const std::string& s) {
96 std::string o;
97 o.reserve(s.size());
98 for (char c : s) {
99 switch (c) {
100 case '&': o += "&amp;"; break;
101 case '<': o += "&lt;"; break;
102 case '>': o += "&gt;"; break;
103 case '"': o += "&quot;"; break;
104 case '\'': o += "&#39;"; break;
105 case '\n': o += "<br/>"; break;
106 default: o += c;
107 }
108 }
109 return o;
110}
111
112
113static std::string extract_host(const std::string& raw) {
114 if (raw.empty()) return {};
115 std::string s = raw;
116
117 auto notspace = [](int ch){ return !std::isspace(ch); };
118 s.erase(s.begin(), std::find_if(s.begin(), s.end(), notspace));
119 s.erase(std::find_if(s.rbegin(), s.rend(), notspace).base(), s.end());
120 if (s.empty()) return {};
121
122 std::transform(s.begin(), s.end(), s.begin(), ::tolower);
123
124 auto strip_scheme = [&](const char* prefix) {
125 size_t n = std::strlen(prefix);
126 if (s.rfind(prefix, 0) == 0) s.erase(0, n);
127 };
128 strip_scheme("http://");
129 strip_scheme("https://");
130 strip_scheme("ftp://");
131
132 auto atpos = s.find('@');
133 if (atpos != std::string::npos) s.erase(0, atpos + 1);
134
135 size_t pos = s.find_first_of("/?#");
136 if (pos != std::string::npos) s = s.substr(0, pos);
137
138 size_t colon = s.rfind(':');
139 if (colon != std::string::npos) {
140 std::string after = s.substr(colon + 1);
141 bool all_digits = !after.empty() && std::all_of(after.begin(), after.end(), ::isdigit);
142 if (all_digits) s = s.substr(0, colon);
143 }
144 if (!s.empty() && s.back() == '.') s.pop_back();
145 return s;
146}
147
148
149static std::unordered_set<std::string> g_trusted;
150static bool g_trusted_loaded = false;
151
152static void load_list_file(const std::string& path, std::vector<std::string>& out) {
153 if (!fs::exists(path)) return;
154 try {
155 std::ifstream f(path);
156 if (!f.good()) return;
157
158 nlohmann::json j;
159 f >> j;
160
161 auto push = [&](const std::string& s){
162 auto d = clean_domain(s);
163 if(!d.empty()) out.push_back(d);
164 };
165
166 if (j.is_array()) {
167 for (auto& v : j) if (v.is_string()) push(v.get<std::string>());
168 } else {
169 if (j.contains("list") && j["list"].is_array())
170 for (auto& v : j["list"]) if (v.is_string()) push(v.get<std::string>());
171 if (j.contains("domains") && j["domains"].is_array())
172 for (auto& v : j["domains"]) if (v.is_string()) push(v.get<std::string>());
173 if (j.contains("whitelist") && j["whitelist"].is_array())
174 for (auto& v : j["whitelist"]) if (v.is_string()) push(v.get<std::string>());
175 }
176 } catch (...) {
177 }
178}
179
181 if (g_trusted_loaded) return;
182
183 std::vector<std::string> tmp;
184 load_list_file("/usr/share/BastionGuard/data/bank/banks.json", tmp);
185 if (const char* home = std::getenv("HOME")) {
186 load_list_file(std::string(home) + "/.config/BastionGuard/whitelist.json", tmp);
187 }
188 for (auto& d : tmp) g_trusted.insert(d);
189 g_trusted_loaded = true;
190
191 std::cerr << _("[LocalWarningServer] ✅ Whitelist caricata: ")
192 << g_trusted.size() << _(" domini trusted\n");
193}
194
195static bool is_trusted_domain(const std::string& host) {
197 std::string h = clean_domain(host);
198 if (h.empty()) return false;
199
200 if (g_trusted.count(h)) return true;
201 auto pos = h.find('.');
202 while (pos != std::string::npos) {
203 std::string parent = h.substr(pos + 1);
204 if (g_trusted.count(parent)) return true;
205 pos = h.find('.', pos + 1);
206 }
207 return false;
208}
209
210static bool listen_one(SoupServer* srv, const char* addr_str, int port, bool https, GError** error) {
211 if (!srv || !addr_str) return false;
212
213 GInetAddress* ia4 = g_inet_address_new_from_string(addr_str);
214 GSocketAddress* sa4 = ia4 ? g_inet_socket_address_new(ia4, port) : nullptr;
215 if (ia4) g_object_unref(ia4);
216
217 bool ok = false;
218 if (sa4) {
219 ok = soup_server_listen(srv, sa4,
220 https ? SOUP_SERVER_LISTEN_HTTPS : (SoupServerListenOptions)0, error);
221 g_object_unref(sa4);
222 }
223
224 if (!ok && error && *error) {
225 std::cerr << _("[LocalWarningServer] ⚠️ Porta ")
226 << port << _(" non disponibile: ") << (*error)->message << "\n";
227 g_error_free(*error);
228 *error = nullptr;
229 }
230 return ok;
231}
232
233
235 int https_port,
236 const std::string& bind_address,
237 const std::string& cert_file,
238 const std::string& key_file,
239 const std::string& page_warning_path)
240 : server_(nullptr),
241 http_port_(http_port > 0 ? http_port : BastionGuard_WARNING_DEFAULT_HTTP),
242 https_port_(https_port > 0 ? https_port : BastionGuard_WARNING_DEFAULT_HTTPS),
243 bind_address_(bind_address.empty() ? BastionGuard_LOCAL_WARNING_IP : bind_address),
244 cert_file_(cert_file),
245 key_file_(key_file),
246 page_warning_path_(page_warning_path),
247 running_(false),
248 loop_(nullptr)
249{}
250
254
256 if (running_) return;
257
258 server_ = soup_server_new(nullptr, nullptr);
259 if (!server_) {
260 std::cerr << _("[LocalWarningServer] ❌ Errore creazione SoupServer\n");
261 return;
262 }
263
264 g_object_set(server_, "server-header", "BastionGuard-warning-server", nullptr);
265 soup_server_add_handler(server_, "/", LocalWarningServer::on_request, this, nullptr);
266
267 // HTTPS opzionale
268 bool have_cert = false;
269 if (!cert_file_.empty() && !key_file_.empty()) {
270 GError* cert_err = nullptr;
271 GTlsCertificate* cert = g_tls_certificate_new_from_files(cert_file_.c_str(), key_file_.c_str(), &cert_err);
272 if (cert) {
273 soup_server_set_tls_certificate(server_, cert);
274 have_cert = true;
275 g_object_unref(cert);
276 } else if (cert_err) {
277 std::cerr << _("[LocalWarningServer] TLS error: ") << cert_err->message << "\n";
278 g_error_free(cert_err);
279 }
280 }
281
282 GError* err = nullptr;
283 bool ok_http = listen_one(server_, bind_address_.c_str(), http_port_, false, &err);
284 if (!ok_http && err) { g_error_free(err); err = nullptr; }
285 bool ok_https = have_cert && listen_one(server_, bind_address_.c_str(), https_port_, true, &err);
286 if (!ok_https && err) { g_error_free(err); err = nullptr; }
287
288 if (!(ok_http || ok_https)) {
289 g_object_unref(server_);
290 server_ = nullptr;
291 std::cerr << _("[LocalWarningServer] ⚠️ Nessuna porta disponibile su ") << bind_address_ << "\n";
292 return;
293 }
294
295 loop_ = g_main_loop_new(nullptr, FALSE);
296 loop_thread_ = std::thread([this]() { g_main_loop_run(loop_); });
297 running_ = true;
298 std::cout << _("[LocalWarningServer] ✅ Avviato su ") << bind_address_
299 << " (" << http_port_ << (have_cert ? ", HTTPS " + std::to_string(https_port_) : "") << ")\n";
300}
301
303 if (!running_) return;
304 running_ = false;
305 if (loop_) {
306 g_main_loop_quit(loop_);
307 if (loop_thread_.joinable()) loop_thread_.join();
308 g_main_loop_unref(loop_);
309 loop_ = nullptr;
310 }
311 if (server_) {
312 soup_server_disconnect(server_);
313 g_object_unref(server_);
314 server_ = nullptr;
315 }
316 std::cout << _("[LocalWarningServer] 🛑 Arrestato\n");
317}
318
319void LocalWarningServer::on_request(SoupServer*, SoupServerMessage* msg,
320 const char* path, GHashTable*, gpointer user_data)
321{
322 auto* self = static_cast<LocalWarningServer*>(user_data);
323 if (!self || !msg) return;
324
325 SoupMessageHeaders* req_headers =
326 const_cast<SoupMessageHeaders*>(soup_server_message_get_request_headers(msg));
327 const char* host_header = req_headers ? soup_message_headers_get_one(req_headers, "Host") : nullptr;
328
329 std::string requested_host = host_header ? extract_host(host_header) : _("dominio-sconosciuto");
330
331 if (requested_host.empty() || requested_host == BastionGuard_LOCAL_WARNING_IP) {
332 if (path && *path) {
333 std::string p = path;
334 if (!p.empty() && p.front() == '/') p.erase(0,1);
335 requested_host = p;
336 } else {
337 requested_host = _("dominio-sconosciuto");
338 }
339 }
340
341 if (is_trusted_domain(requested_host)) {
342 std::string location = "https://" + clean_domain(requested_host) + "/";
343 SoupMessageHeaders* res_headers =
344 const_cast<SoupMessageHeaders*>(soup_server_message_get_response_headers(msg));
345 soup_message_headers_append(res_headers, "Location", location.c_str());
346 soup_server_message_set_status(msg, SOUP_STATUS_TEMPORARY_REDIRECT, nullptr); // 307
347 std::cout << "[BastionGuard-DNS] 🟢 Trusted: redirect a " << location << "\n";
348 return;
349 }
350
351 std::cout << "[BastionGuard-DNS] ⚠️ Dominio bloccato: " << requested_host << std::endl;
352
353 std::ostringstream html;
354
355html << R"HTML(<!DOCTYPE html>
356<html lang="it">
357<head>
358<meta charset="UTF-8">
359<title>⚠️ )HTML" << _("Accesso bloccato - BastionGuard DNS") << R"HTML(</title>
360<meta name="viewport" content="width=device-width, initial-scale=1">
361<style>
362body {
363 background: linear-gradient(135deg, #ff6f61, #d6a4a4);
364 color: #fff;
365 display: flex;
366 justify-content: center;
367 align-items: center;
368 height: 100vh;
369 margin: 0;
370 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
371 text-align: center;
372}
373.container {
374 background: rgba(0, 0, 0, 0.55);
375 border-radius: 15px;
376 padding: 3rem 2rem;
377 max-width: 720px;
378 box-shadow: 0 0 20px rgba(0,0,0,0.4);
379}
380h1 { font-size: 2rem; margin-bottom: 1rem; }
381p { font-size: 1.1rem; margin-bottom: 1.5rem; }
382.btn {
383 display: inline-block;
384 background: #fff;
385 color: #ff6f61;
386 text-decoration: none;
387 padding: 0.7rem 1.4rem;
388 border-radius: 6px;
389 font-weight: 600;
390}
391.btn:hover { background: #f8f9fa; }
392small { opacity: .8; }
393</style>
394</head>
395<body>
396<div class="container">
397 <h1>⚠️ )HTML" << _("Dominio bloccato") << R"HTML(</h1>
398 <p>)HTML"
399 << _("L'accesso al dominio") << " <strong>"
400 << html_escape(requested_host)
401 << "</strong> "
402 << _("è stato bloccato dal sistema DNS di BastionGuard (dnsmasq).")
403 << R"HTML(</p>
404 <a href="about:blank" class="btn">🔒 )HTML" << _("Torna al sicuro") << R"HTML(</a>
405 <p><small>)HTML" << _("BastionGuard — protezione DNS attiva") << R"HTML(</small></p>
406</div>
407</body>
408</html>)HTML";
409
410 std::string body = html.str();
411 soup_server_message_set_response(msg, "text/html", SOUP_MEMORY_COPY, body.c_str(), body.size());
412 soup_server_message_set_status(msg, SOUP_STATUS_OK, nullptr);
413}
static void load_list_file(const std::string &path, std::vector< std::string > &out)
static bool is_trusted_domain(const std::string &host)
static std::unordered_set< std::string > g_trusted
static std::string clean_domain(std::string d)
static bool g_trusted_loaded
static void ensure_trusted_loaded()
#define _(s)
static std::string to_lower(std::string s)
static void on_request(SoupServer *, SoupServerMessage *, const char *, GHashTable *, gpointer)
LocalWarningServer(int http_port=81, int https_port=444, const std::string &bind_address="127.0.0.2", const std::string &cert_file="", const std::string &key_file="", const std::string &page_warning_path="")
#define BastionGuard_LOCAL_WARNING_IP
static bool is_trusted_domain(const std::string &host)
static std::string clean_domain(std::string d)
#define BastionGuard_WARNING_DEFAULT_HTTPS
#define BastionGuard_WARNING_DEFAULT_HTTP
static std::string extract_host(const std::string &raw)
static std::string html_escape(const std::string &s)
static bool listen_one(SoupServer *srv, const char *addr_str, int port, bool https, GError **error)
static std::string read_last_lines(const std::string &path, size_t max_lines=50)