29#include <glib/gi18n.h>
35#include <unordered_set>
38#include <nlohmann/json.hpp>
40namespace fs = std::filesystem;
43#ifndef BastionGuard_WARNING_DEFAULT_HTTP
44# define BastionGuard_WARNING_DEFAULT_HTTP 81
46#ifndef BastionGuard_WARNING_DEFAULT_HTTPS
47# define BastionGuard_WARNING_DEFAULT_HTTPS 444
49#ifndef BastionGuard_LOCAL_WARNING_IP
50# define BastionGuard_LOCAL_WARNING_IP "127.0.0.2"
55 std::transform(s.begin(), s.end(), s.begin(), [](
unsigned char c){ return std::tolower(c); });
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();
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)");
72 file.seekg(0, std::ios::end);
73 auto size = file.tellg();
74 if (size <= 0)
return _(
"(Log vuoto)");
76 content.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
78 std::vector<std::string> lines;
80 for (
size_t i = 0; i < content.size(); ++i) {
81 if (content[i] ==
'\n') {
82 lines.emplace_back(content.substr(start, i - start));
86 if (start < content.size()) lines.emplace_back(content.substr(start));
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;
100 case '&': o +=
"&";
break;
101 case '<': o +=
"<";
break;
102 case '>': o +=
">";
break;
103 case '"': o +=
""";
break;
104 case '\'': o +=
"'";
break;
105 case '\n': o +=
"<br/>";
break;
114 if (raw.empty())
return {};
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 {};
122 std::transform(s.begin(), s.end(), s.begin(), ::tolower);
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);
128 strip_scheme(
"http://");
129 strip_scheme(
"https://");
130 strip_scheme(
"ftp://");
132 auto atpos = s.find(
'@');
133 if (atpos != std::string::npos) s.erase(0, atpos + 1);
135 size_t pos = s.find_first_of(
"/?#");
136 if (pos != std::string::npos) s = s.substr(0, pos);
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);
144 if (!s.empty() && s.back() ==
'.') s.pop_back();
152static void load_list_file(
const std::string& path, std::vector<std::string>& out) {
153 if (!fs::exists(path))
return;
155 std::ifstream f(path);
156 if (!f.good())
return;
161 auto push = [&](
const std::string& s){
163 if(!d.empty()) out.push_back(d);
167 for (
auto& v : j)
if (v.is_string()) push(v.get<std::string>());
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>());
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);
191 std::cerr <<
_(
"[LocalWarningServer] ✅ Whitelist caricata: ")
192 <<
g_trusted.size() <<
_(
" domini trusted\n");
198 if (h.empty())
return false;
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);
210static bool listen_one(SoupServer* srv,
const char* addr_str,
int port,
bool https, GError** error) {
211 if (!srv || !addr_str)
return false;
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);
219 ok = soup_server_listen(srv, sa4,
220 https ? SOUP_SERVER_LISTEN_HTTPS : (SoupServerListenOptions)0, error);
224 if (!ok && error && *error) {
225 std::cerr <<
_(
"[LocalWarningServer] ⚠️ Porta ")
226 << port <<
_(
" non disponibile: ") << (*error)->message <<
"\n";
227 g_error_free(*error);
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)
258 server_ = soup_server_new(
nullptr,
nullptr);
260 std::cerr <<
_(
"[LocalWarningServer] ❌ Errore creazione SoupServer\n");
264 g_object_set(
server_,
"server-header",
"BastionGuard-warning-server",
nullptr);
268 bool have_cert =
false;
270 GError* cert_err =
nullptr;
271 GTlsCertificate* cert = g_tls_certificate_new_from_files(
cert_file_.c_str(),
key_file_.c_str(), &cert_err);
273 soup_server_set_tls_certificate(
server_, cert);
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);
282 GError* err =
nullptr;
284 if (!ok_http && err) { g_error_free(err); err =
nullptr; }
286 if (!ok_https && err) { g_error_free(err); err =
nullptr; }
288 if (!(ok_http || ok_https)) {
291 std::cerr <<
_(
"[LocalWarningServer] ⚠️ Nessuna porta disponibile su ") <<
bind_address_ <<
"\n";
295 loop_ = g_main_loop_new(
nullptr, FALSE);
298 std::cout <<
_(
"[LocalWarningServer] ✅ Avviato su ") <<
bind_address_
306 g_main_loop_quit(
loop_);
308 g_main_loop_unref(
loop_);
312 soup_server_disconnect(
server_);
316 std::cout <<
_(
"[LocalWarningServer] 🛑 Arrestato\n");
320 const char* path, GHashTable*, gpointer user_data)
323 if (!self || !msg)
return;
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;
329 std::string requested_host = host_header ?
extract_host(host_header) :
_(
"dominio-sconosciuto");
333 std::string p = path;
334 if (!p.empty() && p.front() ==
'/') p.erase(0,1);
337 requested_host =
_(
"dominio-sconosciuto");
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);
347 std::cout <<
"[BastionGuard-DNS] 🟢 Trusted: redirect a " << location <<
"\n";
351 std::cout <<
"[BastionGuard-DNS] ⚠️ Dominio bloccato: " << requested_host << std::endl;
353 std::ostringstream html;
355html << R
"HTML(<!DOCTYPE html>
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">
363 background: linear-gradient(135deg, #ff6f61, #d6a4a4);
366 justify-content: center;
370 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
374 background: rgba(0, 0, 0, 0.55);
378 box-shadow: 0 0 20px rgba(0,0,0,0.4);
380h1 { font-size: 2rem; margin-bottom: 1rem; }
381p { font-size: 1.1rem; margin-bottom: 1.5rem; }
383 display: inline-block;
386 text-decoration: none;
387 padding: 0.7rem 1.4rem;
391.btn:hover { background: #f8f9fa; }
392small { opacity: .8; }
396<div class="container">
397 <h1>⚠️ )HTML" << _("Dominio bloccato") << R
"HTML(</h1>
399 << _("L'accesso al dominio") <<
" <strong>"
402 <<
_(
"è stato bloccato dal sistema DNS di BastionGuard (dnsmasq).")
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>
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);
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()
static std::string to_lower(std::string s)
std::string bind_address_
static void on_request(SoupServer *, SoupServerMessage *, const char *, GHashTable *, gpointer)
std::string page_warning_path_
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)