Skip to content
API Documentation
BastionGuard™
MainWindow.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Secure Connection
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * GPL-3.0 — BastionGuard™ is a trademark of Calogero Scarnà.
5 */
6
7#include "MainWindow.hpp"
8#include "Resource.hpp"
10
11#include <glibmm/i18n.h>
12#include <glibmm/miscutils.h>
13#include <gdkmm/pixbuf.h>
14#include <gio/gio.h>
15#include <gtk/gtk.h>
16
17#include <cstdlib> // setenv
18#include <algorithm>
19#include <filesystem>
20#include <fstream>
21#include <iostream>
22#include <locale.h>
23#include <optional>
24#include <sstream>
25#include <libintl.h>
26
27#define _(text) gettext(text)
28
29#ifndef BSC_SERVICECTL_PATH
30#define BSC_SERVICECTL_PATH "/usr/libexec/bastionguard/bastionguard-service"
31#endif
32namespace {
33
34 constexpr int kNotifDesktopActive = 15;
35 constexpr int kNotifDesktopIdle = 16;
36
37 std::optional<bool> queryScreensaverActiveOnce(const char* bus_name,
38 const char* object_path,
39 const char* interface_name)
40 {
41 GError* error = nullptr;
42 GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
43 if (!conn) {
44 if (error) g_error_free(error);
45 return std::nullopt;
46 }
47
48 GVariant* reply = g_dbus_connection_call_sync(
49 conn,
50 bus_name,
51 object_path,
52 interface_name,
53 "GetActive",
54 nullptr,
55 G_VARIANT_TYPE("(b)"),
56 G_DBUS_CALL_FLAGS_NONE,
57 180,
58 nullptr,
59 &error);
60
61 g_object_unref(conn);
62
63 if (!reply) {
64 if (error) g_error_free(error);
65 return std::nullopt;
66 }
67
68 gboolean active = FALSE;
69 g_variant_get(reply, "(b)", &active);
70 g_variant_unref(reply);
71 return active == TRUE;
72 }
73
74 std::optional<bool> queryScreensaverActive()
75 {
76 struct Candidate {
77 const char* bus;
78 const char* path;
79 const char* iface;
80 };
81
82 static constexpr Candidate candidates[] = {
83 {"org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver"},
84 {"org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", "org.gnome.ScreenSaver"},
85 {"org.cinnamon.ScreenSaver", "/org/cinnamon/ScreenSaver", "org.cinnamon.ScreenSaver"},
86 {"org.mate.ScreenSaver", "/org/mate/ScreenSaver", "org.mate.ScreenSaver"},
87 };
88
89 for (const auto& c : candidates) {
90 if (auto v = queryScreensaverActiveOnce(c.bus, c.path, c.iface))
91 return v;
92 }
93 return std::nullopt;
94 }
95
96 constexpr const char* kUISocketPath = "unix:///tmp/bsd-daemon.sock";
97
98 constexpr const char* kServiceName = "bsc-daemon.service";
99 constexpr const char* kServiceCtlPath = BSC_SERVICECTL_PATH;
100 constexpr const char* kSystemAutostartDesktop = "/etc/xdg/autostart/bastionguard-sc-autostart.desktop";
101 constexpr const char* kFallbackSystemAutostartDesktop = "/etc/xdg/autostart/bastionguard-sc.desktop";
102 constexpr const char* kSystemAutostartBackupDesktop = "/etc/xdg/autostart/bastionguard-sc-autostart.desktop.bak";
103
105 {
106 return std::string(
107 "[Desktop Entry]\n"
108 "Name=BastionGuard Secure Connection\n"
109 "Name[it]=BastionGuard Connessione Sicura\n"
110 "Comment=Interactive application firewall\n"
111 "Comment[it]=Firewall applicativo interattivo\n"
112 "Exec=bastionguard-sc-ui --start-minimized\n"
113 "Icon=/usr/share/bastionguard-sc/data/logo.svg\n"
114 "Terminal=false\n"
115 "Type=Application\n"
116 "Categories=Network;Security;System;\n"
117 "Keywords=firewall;network;security;connection;\n"
118 "StartupNotify=true\n");
119 }
120
121 bool writeServiceControlPreference(bool enabled, const std::string& backupFile)
122 {
123 try {
124 const std::filesystem::path cfgDir =
125 std::filesystem::path(Glib::get_user_config_dir()) / "bastionguard-sc";
126 std::filesystem::create_directories(cfgDir);
127
128 std::ofstream out(cfgDir / "service-control.ini", std::ios::trunc);
129 out << "[ServiceControl]\n";
130 out << "enabled=" << (enabled ? "true" : "false") << "\n";
131 out << "autostart_enabled=" << (enabled ? "true" : "false") << "\n";
132 out << "daemon_enabled=" << (enabled ? "true" : "false") << "\n";
133 out << "service=" << kServiceName << "\n";
134 out << "autostart_path=" << kSystemAutostartDesktop << "\n";
135 out << "fallback_autostart_path=" << kFallbackSystemAutostartDesktop << "\n";
136 out << "generated_autostart_file=" << (cfgDir / "bastionguard-sc-autostart.desktop").string() << "\n";
137 out << "backup_path=" << backupFile << "\n";
138 return static_cast<bool>(out);
139 } catch (...) {
140 return false;
141 }
142 }
143
144 bool writeConfigAutostartDesktop(const std::filesystem::path& path)
145 {
146 try {
147 std::filesystem::create_directories(path.parent_path());
148 std::ofstream out(path, std::ios::trunc);
150 return static_cast<bool>(out);
151 } catch (...) {
152 return false;
153 }
154 }
155
156 std::string shellQuote(const std::string& s)
157 {
158 std::string out = "'";
159 for (char c : s) {
160 if (c == '\'') out += "'\\''";
161 else out += c;
162 }
163 out += "'";
164 return out;
165 }
166
167 bool runCommand(const std::string& cmd)
168 {
169 const int rc = std::system(cmd.c_str());
170 return rc == 0;
171 }
172
173 std::string firstExistingPath(const std::initializer_list<std::string>& candidates)
174 {
175 for (const auto& p : candidates) {
176 if (std::filesystem::exists(p)) return p;
177 }
178 return {};
179 }
180
181 std::string displayNameForStatus(const ConnectionInfo& info)
182 {
184 info.app_comm, info.app_path, info.app_args, info.dest_ip, info.src_ip);
185 }
186
187}
188
190{
191 set_title("");
192 set_resizable(true);
193 set_default_size(1158, 803);
194 set_size_request(1158, 803);
195
196 m_configFile = Glib::get_user_config_dir() + "/bastionguard-sc/settings.ini";
197 m_cssProvider = Gtk::CssProvider::create();
198
199 loadConfig();
200 applyLanguage(m_language); // applica lingua prima di costruire la UI
204
205 m_stack.add(m_connectionsPage, "connections");
206 m_stack.add(m_appsPage, "apps");
207 m_stack.add(m_networkPage, "network");
208 m_stack.add(m_rulesPage, "rules");
209 m_stack.add(m_statsPage, "stats");
210 m_stack.add(m_aboutPage, "about");
211 m_stack.add(m_settingsPage, "settings");
213 sigc::mem_fun(*this, &MainWindow::applyProtectionEnabled));
214
215 // Transizione animata al cambio pagina
216 m_stack.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
217 m_stack.set_transition_duration(220); // ms — abbastanza veloce da non dare fastidio
218
219 m_stack.set_hexpand(true);
220 m_stack.set_vexpand(true);
221
222 buildSidebar();
223 m_mainBox.append(m_stack);
224
225 // Carica il log delle connessioni precedenti (se esiste). Retention
226 // 7 giorni applicata da loadLog(): record piu' vecchi scartati.
229
230 m_statusCtx = m_statusbar.get_context_id("daemon");
231 setStatus(_("In attesa del daemon..."));
232
233 auto outerBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
234 outerBox->append(m_mainBox);
235 outerBox->append(m_statusbar);
236 set_child(*outerBox);
237
238 // ── Segnali UIServer ──────────────────────────────────────────────
240 sigc::mem_fun(*this, &MainWindow::onDaemonConnected));
242 sigc::mem_fun(*this, &MainWindow::onDaemonDisconnected));
244 sigc::mem_fun(*this, &MainWindow::onNewConnection));
245 m_uiServer.signal_alert.connect(
246 sigc::mem_fun(*this, &MainWindow::onDaemonAlert));
247
248 m_uiServer.start(kUISocketPath);
250
251 // ── Systray D-Bus ─────────────────────────────────────────────────
253 m_tray.set_on_activate([this]() {
255 if (is_visible() && is_active()) {
256 hide();
257 } else {
258 // Torna sempre alla pagina Connessioni quando si riapre
259 m_stack.set_transition_type(Gtk::StackTransitionType::NONE);
260 m_stack.set_visible_child("connections");
261 m_stack.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
262 if (!m_sidebarButtons.empty())
264 present();
265 }
266 });
267
271 });
272
273 // FIX: "Esci" dalla systray ora chiude davvero il programma
274 // Prima chiamava solo get_application()->quit(), che in certi DE non
275 // funziona se l'app è in hold(). Chiamiamo prima close() sulla finestra
276 // (che triggera on_close_request), poi forziamo il rilascio dell'hold
277 // e infine quit() sull'applicazione.
278 m_tray.set_on_quit([this]() {
279 m_tray.stop();
280 close(); // bypassa on_close_request → vedi sotto
281 if (auto app = get_application()) {
282 app->release(); // bilancia il hold() in Application::on_activate
283 app->quit();
284 }
285 });
286 m_tray.start();
287
288 if (!m_sidebarButtons.empty())
290}
291
293{
294 if (m_desktopActivityTimer != 0) {
295 g_source_remove(m_desktopActivityTimer);
297 }
298
299 // Salva i log — vengono letti al prossimo avvio.
300 // Il file viene cancellato solo da "Pulisci" nelle rispettive pagine.
303
304 m_tray.stop();
306}
307
308// ---------------------------------------------------------------------------
309// Config
310// ---------------------------------------------------------------------------
311
313{
314 try {
315 std::filesystem::create_directories(
316 std::filesystem::path(m_configFile).parent_path());
317 std::ofstream f(m_configFile);
318 f << "[General]\n";
319 f << "dark_mode=" << (m_darkMode ? "true" : "false") << "\n";
320 if (!m_language.empty())
321 f << "language=" << m_language << "\n";
322 f << "protection_enabled=" << (m_protectionEnabled ? "true" : "false") << "\n";
323 if (!m_autostartBackupFile.empty())
324 f << "autostart_backup=" << m_autostartBackupFile << "\n";
325 } catch (const std::exception& e) {
326 std::cerr << "⚠ Impossibile salvare config: " << e.what() << "\n";
327 }
328}
329
331{
332 m_darkMode = false;
333 m_language.clear();
334 m_protectionEnabled = true;
335 m_autostartBackupFile = kSystemAutostartBackupDesktop;
336
337 std::ifstream f(m_configFile);
338 if (f.is_open()) {
339 std::string line;
340 while (std::getline(f, line)) {
341 if (line.find("dark_mode=true") != std::string::npos)
342 m_darkMode = true;
343
344 // ── Lingua ──────────────────────────────────────────────────────
345 const std::string langKey = "language=";
346 auto pos = line.find(langKey);
347 if (pos != std::string::npos)
348 m_language = line.substr(pos + langKey.size());
349
350 if (line.find("protection_enabled=false") != std::string::npos)
351 m_protectionEnabled = false;
352
353 const std::string backupKey = "autostart_backup=";
354 pos = line.find(backupKey);
355 if (pos != std::string::npos)
356 m_autostartBackupFile = line.substr(pos + backupKey.size());
357 }
358 }
359
360 // service-control.ini è la sorgente esplicita per autostart e daemon.
361 // Se esiste, sincronizza lo stato UI anche dopo modifiche fatte da Settings.
362 const std::filesystem::path serviceControlFile =
363 std::filesystem::path(m_configFile).parent_path() / "service-control.ini";
364 std::ifstream sc(serviceControlFile);
365 bool sawServiceControlState = false;
366 bool serviceControlEnabled = true;
367 std::string line;
368 while (std::getline(sc, line)) {
369 if (line == "enabled=true" || line == "autostart_enabled=true" || line == "daemon_enabled=true") {
370 sawServiceControlState = true;
371 }
372 if (line == "enabled=false" || line == "autostart_enabled=false" || line == "daemon_enabled=false") {
373 sawServiceControlState = true;
374 serviceControlEnabled = false;
375 }
376
377 const std::string backupPathKey = "backup_path=";
378 auto backupPathPos = line.find(backupPathKey);
379 if (backupPathPos == 0) {
380 m_autostartBackupFile = line.substr(backupPathKey.size());
381 }
382 }
383 if (sawServiceControlState) {
384 m_protectionEnabled = serviceControlEnabled;
385 }
386}
387
388// ---------------------------------------------------------------------------
389// Lingua (gettext)
390// ---------------------------------------------------------------------------
391
392/**
393 * Imposta la variabile d'ambiente LANGUAGE e reinizializza gettext.
394 * Deve essere chiamata PRIMA che qualsiasi stringa tradotta venga costruita.
395 * Poiché GTK/GLib non supportano il cambio di lingua a runtime senza riavvio,
396 * il metodo si limita a impostare l'ambiente: l'effetto si vedrà al prossimo
397 * avvio (il LanguageDialog avvisa l'utente di questo).
398 */
399void MainWindow::applyLanguage(const std::string& lang)
400{
401 if (!lang.empty()) {
402 ::setenv("LANGUAGE", lang.c_str(), 1);
403 ::setenv("LANG", (lang + ".UTF-8").c_str(), 1);
404 ::setlocale(LC_ALL, (lang + ".UTF-8").c_str());
405 } else {
406 ::unsetenv("LANGUAGE");
407 ::setlocale(LC_ALL, "");
408 }
409
410 // Reinizializza gettext (il dominio e la dir devono già essere configurati in main.cpp)
411 ::textdomain("bastionguard-sc");
412}
413
415{
416 m_langDialog = std::make_unique<LanguageDialog>(m_configFile, m_language);
417 m_langDialog->set_transient_for(*this);
418 m_langDialog->set_modal(true);
419 m_langDialog->signal_hide().connect([this]() {
420 // Se la lingua è cambiata aggiorna il membro (la UI si aggiorna al prossimo avvio)
421 const std::string chosen = m_langDialog->selectedLanguage();
422 if (chosen != m_language) {
423 m_language = chosen;
424 // Il file è già stato scritto da LanguageDialog::onApply()
425 setStatus(_("Lingua aggiornata — riavvia per applicare la modifica."));
426 }
427 });
428 m_langDialog->present();
429}
430
431// ---------------------------------------------------------------------------
432// Tema
433// ---------------------------------------------------------------------------
434
436{
437 const std::string cssFile = dark ? resource("BSC-dark.css") : resource("BSC.css");
438 try {
439 m_cssProvider->load_from_path(cssFile);
440 Gtk::StyleContext::add_provider_for_display(
441 get_display(),
443 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
444 } catch (const Glib::Error& e) {
445 std::cerr << "⚠ CSS: " << e.what() << "\n";
446 }
447}
448
450{
451 m_darkMode = dark;
452 loadTheme(dark);
453 saveConfig();
454
455 if (m_btnLight && m_btnDark) {
456 if (dark) {
457 m_btnLight->get_style_context()->remove_class("theme-btn-active");
458 m_btnDark->get_style_context()->add_class("theme-btn-active");
459 } else {
460 m_btnDark->get_style_context()->remove_class("theme-btn-active");
461 m_btnLight->get_style_context()->add_class("theme-btn-active");
462 }
463 }
464}
465
466// ---------------------------------------------------------------------------
467// Header bar
468// ---------------------------------------------------------------------------
469
471{
472 auto header = Gtk::make_managed<Gtk::HeaderBar>();
473 header->set_show_title_buttons(false);
474 header->set_decoration_layout("");
475 header->get_style_context()->add_class("custom-headerbar");
476
477 auto headerLeft = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
478 headerLeft->set_size_request(240, -1);
479 headerLeft->set_hexpand(false);
480 headerLeft->get_style_context()->add_class("header-left");
481 header->pack_start(*headerLeft);
482
483 auto spacer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
484 spacer->set_hexpand(true);
485 header->pack_start(*spacer);
486
487 auto headerRight = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 4);
488 headerRight->set_hexpand(false);
489 headerRight->get_style_context()->add_class("header-right");
490
491 // ── Bottone selezione lingua ────────────────────────────────────────
492 auto btnLang = Gtk::make_managed<Gtk::Button>("🌐");
493 btnLang->get_style_context()->add_class("theme-btn");
494 btnLang->set_tooltip_text(_("Cambia lingua"));
495 btnLang->signal_clicked().connect(
496 sigc::mem_fun(*this, &MainWindow::showLanguageDialog));
497 headerRight->append(*btnLang);
498
499 // ── Tema ────────────────────────────────────────────────────────────
500 m_btnLight = Gtk::make_managed<Gtk::Button>("☀");
501 m_btnDark = Gtk::make_managed<Gtk::Button>("🌙");
502
503 m_btnLight->get_style_context()->add_class("theme-btn");
504 m_btnDark->get_style_context()->add_class("theme-btn");
505
506 m_btnLight->set_tooltip_text(_("Tema chiaro"));
507 m_btnDark->set_tooltip_text(_("Tema scuro"));
508
509 if (m_darkMode)
510 m_btnDark->get_style_context()->add_class("theme-btn-active");
511 else
512 m_btnLight->get_style_context()->add_class("theme-btn-active");
513
514 m_btnLight->signal_clicked().connect([this]() { applyTheme(false); });
515 m_btnDark->signal_clicked().connect([this]() { applyTheme(true); });
516
517 headerRight->append(*m_btnLight);
518 headerRight->append(*m_btnDark);
519
520 auto addWinBtn = [&](const std::string& iconRes,
521 const std::string& fallback,
522 auto cb)
523 {
524 auto btn = Gtk::make_managed<Gtk::Button>();
525 btn->get_style_context()->add_class("header-button");
526
527 try {
528 auto pix = Gdk::Pixbuf::create_from_file(resource(iconRes), 32, 32, true);
529 btn->set_child(*Gtk::make_managed<Gtk::Image>(pix));
530 } catch (...) {
531 btn->set_label(fallback);
532 }
533
534 btn->signal_clicked().connect(cb);
535 headerRight->append(*btn);
536 };
537
538 addWinBtn("icon-minimize.png", "—", [this]() { minimize(); });
539 addWinBtn("icon-close.png", "✕", [this]() { hide(); });
540
541 header->pack_end(*headerRight);
542 set_titlebar(*header);
543}
544
545// ---------------------------------------------------------------------------
546// Sidebar
547// ---------------------------------------------------------------------------
548
550{
551 auto sidebar = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 10);
552 sidebar->set_size_request(240, -1);
553 sidebar->set_hexpand(false);
554 sidebar->set_vexpand(true);
555 sidebar->get_style_context()->add_class("sidebar");
556
557 // Header logo + titolo
558 {
559 auto hdr = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
560 hdr->set_hexpand(false);
561 hdr->get_style_context()->add_class("sidebar-header");
562
563 auto ico = Gtk::make_managed<Gtk::Image>();
564 try {
565 auto pix = Gdk::Pixbuf::create_from_file(
566 resource("logo-sidebar.png"), 32, 32, true);
567 ico->set(pix);
568 } catch (...) {
569 ico->set_from_icon_name("security-high-symbolic");
570 ico->set_pixel_size(28);
571 }
572
573 ico->set_hexpand(false);
574 hdr->append(*ico);
575
576 auto lbl = Gtk::make_managed<Gtk::Label>(
577 "BastionGuard™\nSecure Connection");
578 lbl->set_justify(Gtk::Justification::LEFT);
579 lbl->set_wrap(true);
580 lbl->set_max_width_chars(16);
581 lbl->set_width_chars(16);
582 lbl->get_style_context()->add_class("sidebar-title");
583 lbl->set_halign(Gtk::Align::START);
584 lbl->set_hexpand(false);
585 hdr->append(*lbl);
586
587 sidebar->append(*hdr);
588 }
589
590 sidebar->append(*makeSidebarButton(
591 resource("icons/connections.svg"), _("Connessioni"), "connections"));
592 sidebar->append(*makeSidebarButton(
593 resource("icons/apps.svg"), _("Applicazioni"), "apps"));
594 sidebar->append(*makeSidebarButton(
595 resource("icons/network.svg"), _("Monitor di rete"), "network"));
596 sidebar->append(*makeSidebarButton(
597 resource("icons/rules.svg"), _("Regole"), "rules"));
598 sidebar->append(*makeSidebarButton(
599 resource("icons/stats.svg"), _("Statistiche"), "stats"));
600 sidebar->append(*makeSidebarButton(
601 resource("icons/about.svg"), _("Impostazioni"), "settings"));
602
603 sidebar->append(*makeSidebarButton(
604 resource("icons/about.svg"), _("Informazioni"), "about"));
605
606 auto spacer = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
607 spacer->set_vexpand(true);
608 sidebar->append(*spacer);
609
610 auto sep = Gtk::make_managed<Gtk::Separator>(Gtk::Orientation::HORIZONTAL);
611 sep->get_style_context()->add_class("sidebar-separator");
612 sidebar->append(*sep);
613
614 m_daemonLabel = Gtk::make_managed<Gtk::Label>(_("⚙ Daemon: in attesa..."));
615 m_daemonLabel->get_style_context()->add_class("daemon-status");
616 m_daemonLabel->set_halign(Gtk::Align::START);
617 m_daemonLabel->set_margin_top(6);
618 m_daemonLabel->set_margin_start(8);
619 m_daemonLabel->set_margin_bottom(6);
620 sidebar->append(*m_daemonLabel);
621
622 m_mainBox.append(*sidebar);
623}
624
625Gtk::Button* MainWindow::makeSidebarButton(const std::string& iconPath,
626 const std::string& label,
627 const std::string& pageName)
628{
629 auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
630
631 auto pic = Gtk::make_managed<Gtk::Picture>();
632 try {
633 auto pb = Gdk::Pixbuf::create_from_file(iconPath);
634 auto scaled = pb->scale_simple(16, 16, Gdk::InterpType::BILINEAR);
635 auto tex = Gdk::Texture::create_for_pixbuf(scaled);
636 pic->set_paintable(tex);
637 pic->set_content_fit(Gtk::ContentFit::FILL);
638 } catch (...) {}
639
640 auto iconBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
641 iconBox->set_size_request(16, 16);
642 iconBox->set_hexpand(false);
643 iconBox->set_valign(Gtk::Align::CENTER);
644 iconBox->append(*pic);
645 box->append(*iconBox);
646
647 auto lbl = Gtk::make_managed<Gtk::Label>(label);
648 lbl->set_halign(Gtk::Align::START);
649 lbl->set_hexpand(false);
650 box->append(*lbl);
651
652 auto btn = Gtk::make_managed<Gtk::Button>();
653 btn->set_child(*box);
654 btn->set_hexpand(true);
655 btn->get_style_context()->add_class("sidebar-button");
656 m_sidebarButtons.push_back(btn);
657
658 btn->signal_clicked().connect([this, btn, pageName]() {
659 // Determina direzione animazione in base all'ordine delle pagine
660 static const std::vector<std::string> pageOrder = {
661 "connections", "apps", "network", "rules", "stats", "settings", "about"
662 };
663 const std::string currentPage = m_stack.get_visible_child_name();
664 auto itCur = std::find(pageOrder.begin(), pageOrder.end(), currentPage);
665 auto itDst = std::find(pageOrder.begin(), pageOrder.end(), pageName);
666 if (itCur != pageOrder.end() && itDst != pageOrder.end()) {
667 if (itDst > itCur)
668 m_stack.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT);
669 else if (itDst < itCur)
670 m_stack.set_transition_type(Gtk::StackTransitionType::SLIDE_RIGHT);
671 // stessa pagina: nessun cambio di tipo
672 }
674 m_stack.set_visible_child(pageName);
675 });
676
677 return btn;
678}
679
681{
682 for (auto* b : m_sidebarButtons)
683 b->get_style_context()->remove_class("active-sidebar-button");
684
685 if (btn) {
686 btn->get_style_context()->add_class("active-sidebar-button");
687 m_activeButton = btn;
688 }
689}
690
691// ---------------------------------------------------------------------------
692// Daemon handlers
693// ---------------------------------------------------------------------------
694
695
697{
698 m_lastUiActivity = std::chrono::steady_clock::now();
699
700 auto install_activity_controller = [this](GtkEventController* controller) {
701 gtk_event_controller_set_propagation_phase(controller, GTK_PHASE_CAPTURE);
702 gtk_widget_add_controller(GTK_WIDGET(gobj()), controller);
703 };
704
705 auto* motion = gtk_event_controller_motion_new();
706 g_signal_connect(
707 motion,
708 "motion",
709 G_CALLBACK(+[](GtkEventControllerMotion*, double, double, gpointer data) {
710 static_cast<MainWindow*>(data)->noteDesktopActivity();
711 }),
712 this);
713 install_activity_controller(GTK_EVENT_CONTROLLER(motion));
714
715 auto* click = gtk_gesture_click_new();
716 g_signal_connect(
717 click,
718 "pressed",
719 G_CALLBACK(+[](GtkGestureClick*, int, double, double, gpointer data) {
720 static_cast<MainWindow*>(data)->noteDesktopActivity();
721 }),
722 this);
723 install_activity_controller(GTK_EVENT_CONTROLLER(click));
724
725 auto* key = gtk_event_controller_key_new();
726 g_signal_connect(
727 key,
728 "key-pressed",
729 G_CALLBACK(+[](GtkEventControllerKey*, guint, guint, GdkModifierType, gpointer data) -> gboolean {
730 static_cast<MainWindow*>(data)->noteDesktopActivity();
731 return GDK_EVENT_PROPAGATE;
732 }),
733 this);
734 install_activity_controller(GTK_EVENT_CONTROLLER(key));
735
736 // Il polling non usa “inattività della finestra BSC” perché la UI può
737 // restare nel tray mentre l’utente lavora su altre app. Usiamo invece lo
738 // stato del desktop/screen saver quando il DE lo espone via D-Bus.
739 m_desktopActivityTimer = g_timeout_add_seconds(
740 5,
741 +[](gpointer data) -> gboolean {
742 return static_cast<MainWindow*>(data)->pollDesktopActivity()
743 ? G_SOURCE_CONTINUE
744 : G_SOURCE_REMOVE;
745 },
746 this);
747
749}
750
752{
753 const auto screensaver_active = queryScreensaverActive();
754
755 // Se il DE non espone lo stato screen saver/lock, restiamo attivi:
756 // meglio non spegnere il monitoraggio mentre l’utente sta lavorando.
757 const bool desktop_active = screensaver_active ? !*screensaver_active : true;
758 sendDesktopActivityState(desktop_active);
759 return true;
760}
761
763{
764 m_lastUiActivity = std::chrono::steady_clock::now();
766}
767
768void MainWindow::sendDesktopActivityState(bool active, bool force)
769{
770 if (!force && active == m_desktopActiveSent) return;
771 m_desktopActiveSent = active;
772
773 m_uiServer.queueNotification(active ? kNotifDesktopActive : kNotifDesktopIdle, "{}");
774 if (!active) {
775 setStatus(_("⏸ Desktop inattivo: monitoraggio connessioni sospeso"));
776 }
777}
778
780{
781 setStatus(_("✅ Daemon connesso"));
782 if (m_daemonLabel)
783 m_daemonLabel->set_text(_("⚙ Daemon: connesso"));
784 m_tray.set_tooltip("BastionGuard™ SC — Daemon connesso");
785
786 // Allineamento immediato: il daemon parte "idle" per sicurezza finché
787 // la UI non conferma che il desktop è attivo.
790}
791
793{
794 setStatus(_("⚠ Daemon disconnesso — in attesa..."));
795 if (m_daemonLabel)
796 m_daemonLabel->set_text(_("⚙ Daemon: in attesa..."));
797 m_tray.set_tooltip("BastionGuard™ SC — Daemon disconnesso");
798}
799
800// ---------------------------------------------------------------------------
801// Nuova connessione → mostra AlertDialog
802// ---------------------------------------------------------------------------
803
804// ---------------------------------------------------------------------------
805// Cache verdetti — chiave di lookup
806// ---------------------------------------------------------------------------
807
808std::string MainWindow::cacheKey(const std::string& app,
809 const std::string& host,
810 const std::string& ip)
811{
812 return app + "|" + host + "|" + ip;
813}
814
816{
817 // ── Controlla la cache dei verdetti "always" ─────────────────────────
818 // Se questa combinazione app+host+ip è già stata decisa con ALLOW_ALWAYS
819 // o DENY_ALWAYS, rispondi immediatamente senza mostrare il popup.
820 const std::string key = cacheKey(!info.app_path.empty() ? info.app_path : info.app_comm, info.dest_host, info.dest_ip);
821 auto it = m_verdictCache.find(key);
822 if (it != m_verdictCache.end()) {
823 const bool allowed = (it->second == ReplyAction::ALLOW_ALWAYS);
824 const std::string esito = allowed
825 ? std::string{"✅ "} + _("Consentito (sempre)")
826 : std::string{"❌ "} + _("Negato (sempre)");
827
828 m_uiServer.sendReply(it->second);
829 m_connectionsPage.onNewConnection(info, /*pending=*/false);
831
832 // Anche i verdetti ricavati da cache devono aggiornare le
833 // viste aggregate e la mappa, altrimenti dopo il primo "allow
834 // always" una app scompare dai grafici.
835 m_appsPage.noteConnection(info, esito);
837
838 if (allowed) ++m_statAllowed; else ++m_statDenied;
841 setStatus(esito + " — " + displayNameForStatus(info));
842 return;
843 }
844
845 // ── Nessuna regola in cache: mostra il popup ─────────────────────────
846 m_connectionsPage.onNewConnection(info, /*pending=*/true);
848
849 if (m_alertDialog && m_alertDialog->is_visible())
850 return;
851
852 m_alertDialog = std::make_unique<AlertDialog>(
853 info.app_comm, info.app_path, info.app_args, info.dest_host, info.dest_ip, info.dest_port);
854
855 // IMPORTANTE: NON rendere l'AlertDialog transient_for della main window.
856 // Se lo fosse:
857 // - su X11/Wayland, presentarlo presenterebbe anche la main window
858 // - la main window in tray verrebbe forzata visibile
859 // Qui vogliamo che l'alert sia una finestra completamente indipendente:
860 // il tray resta nel tray, il dialog appare sopra al desktop.
861
862 m_alertDialog->signal_verdict.connect(
863 [this, info](Verdict v) {
864 onVerdict(v, info);
865 });
866
867 // IMPORTANTE: NON chiamare present() sulla main window.
868 // Prima c'era `if (!is_visible()) present()` che apriva la finestra
869 // principale ogni volta che arrivava un alert — comportamento sbagliato:
870 // l'utente viene disturbato abbastanza dal popup, la main window deve
871 // restare discreta nel tray.
872 m_alertDialog->present();
873}
874
876{
877 std::string text = alert.text.empty() ? _("Alert dal daemon") : alert.text;
878
879 // Mapping dei codici `what` e `type` come definiti in ipc/UIServer.cpp:
880 // what: 0 generic, 2 firewall, 3 connection
881 // type: 0 error, 1 warning, 2 info
882 constexpr int kWhatConnection = 3;
883 constexpr int kTypeWarning = 1;
884
885 const bool is_connection_alert = alert.what == kWhatConnection && alert.has_connection;
886
887 if (is_connection_alert) {
888 m_connectionsPage.onNewConnection(alert.connection, /*pending=*/false);
890 std::string esito;
891 if (alert.type == kTypeWarning) {
892 ++m_statDenied;
893 esito = _("❌ Negato");
895 } else {
897 esito = _("✅ Consentito");
899 }
903 setStatus(text);
904 m_tray.set_tooltip("BastionGuard™ SC — " + text);
905 return;
906 }
907
908 setStatus(text);
909 m_tray.set_tooltip("BastionGuard™ SC — " + text);
910
911 // NON chiamare present() qui: gli alert informativi passano tramite il
912 // tray, non forzano l'apertura della main window. L'utente apre lei
913 // attivamente cliccando sull'icona del tray.
914}
915
917{
919 std::string esito;
920
921 switch (v) {
924 esito = std::string{"✅ "} + _("Consentito (una volta)");
926 break;
927
930 esito = std::string{"✅ "} + _("Consentito (sempre)");
932 break;
933
935 action = ReplyAction::DENY_ONCE;
936 esito = std::string{"❌ "} + _("Negato (una volta)");
937 ++m_statDenied;
938 break;
939
942 esito = std::string{"❌ "} + _("Negato (sempre)");
943 ++m_statDenied;
944 break;
945 }
946
947 m_uiServer.sendReply(action);
948
949 // Memorizza in cache solo le regole "always"
951 m_verdictCache[cacheKey(!info.app_path.empty() ? info.app_path : info.app_comm, info.dest_host, info.dest_ip)] = action;
952
953 // Aggiorna la riga nella pagina Connessioni e salva il log
956
957 // Aggiorna vista per-applicazione
958 m_appsPage.noteConnection(info, esito);
959
960 // Aggiorna Monitor di rete (geo lookup async)
962
967
968 setStatus(esito + " — " + displayNameForStatus(info));
969}
970
971// ---------------------------------------------------------------------------
972// Statusbar
973// ---------------------------------------------------------------------------
974
975void MainWindow::setStatus(const std::string& msg)
976{
978 m_statusbar.push(msg, m_statusCtx);
979}
980
981
983{
984 const std::filesystem::path cfgDir = std::filesystem::path(m_configFile).parent_path();
985 const std::filesystem::path generatedAutostart = cfgDir / "bastionguard-sc-autostart.desktop";
986 const std::string backupFile = kSystemAutostartBackupDesktop;
987
988 // Settings e systray passano da qui. About resta solo informativa, quindi
989 // l'attivazione/disattivazione non è duplicata in più pagine.
990 bool ok = !enabled || writeConfigAutostartDesktop(generatedAutostart);
991 std::string status;
992
993 if (ok) {
994 std::ostringstream script;
995 script << "set -eu; ";
996
997 if (!enabled) {
998 // Prima di rimuovere l'autostart attivo da /etc/xdg/autostart,
999 // conserva un backup nello stesso percorso di sistema con estensione .bak.
1000 script << "if [ -f " << shellQuote(kSystemAutostartDesktop) << " ]; then "
1001 << "install -m 0644 " << shellQuote(kSystemAutostartDesktop) << " " << shellQuote(backupFile) << "; fi; ";
1002 script << "if [ ! -f " << shellQuote(backupFile) << " ] && [ -f " << shellQuote(kFallbackSystemAutostartDesktop) << " ]; then "
1003 << "install -m 0644 " << shellQuote(kFallbackSystemAutostartDesktop) << " " << shellQuote(backupFile) << "; fi; ";
1004 script << "rm -f " << shellQuote(kSystemAutostartDesktop) << " " << shellQuote(kFallbackSystemAutostartDesktop) << "; ";
1005 script << "test -x " << shellQuote(kServiceCtlPath) << "; ";
1006 script << shellQuote(kServiceCtlPath) << " --system disable --now "
1007 << shellQuote(kServiceName) << "; ";
1008 } else {
1009 script << "mkdir -p " << shellQuote(std::filesystem::path(kSystemAutostartDesktop).parent_path().string()) << "; ";
1010 script << "if [ -f " << shellQuote(backupFile) << " ]; then "
1011 << "install -D -m 0644 " << shellQuote(backupFile) << " " << shellQuote(kSystemAutostartDesktop) << "; "
1012 << "else install -D -m 0644 " << shellQuote(generatedAutostart.string()) << " " << shellQuote(kSystemAutostartDesktop) << "; fi; ";
1013 script << "rm -f " << shellQuote(kFallbackSystemAutostartDesktop) << "; ";
1014 script << "test -x " << shellQuote(kServiceCtlPath) << "; ";
1015 script << shellQuote(kServiceCtlPath) << " --system enable --now "
1016 << shellQuote(kServiceName) << "; ";
1017 }
1018
1019 ok = runCommand("pkexec /bin/sh -c " + shellQuote(script.str()));
1020 }
1021
1022 if (ok) {
1023 ok = writeServiceControlPreference(enabled, backupFile);
1024 if (!ok) {
1025 status = _("Operazione di sistema completata, ma salvataggio della preferenza in ~/.config non riuscito.");
1026 }
1027 }
1028
1029 if (ok) {
1030 m_protectionEnabled = enabled;
1031 m_autostartBackupFile = backupFile;
1032 saveConfig();
1033 status = enabled
1034 ? _("Avvio persistente attivato: autostart_enabled=true e daemon_enabled=true salvati, autostart installato e bsc-daemon abilitato.")
1035 : _("Protezione disattivata: enabled=false, autostart_enabled=false e daemon_enabled=false salvati, bsc-daemon fermato/disabilitato e autostart rimosso. Backup creato in /etc/xdg/autostart.");
1036 } else if (status.empty()) {
1037 status = enabled
1038 ? _("Riattivazione non completata. Controlla PolicyKit, il gestore servizi, il backup in /etc/xdg/autostart e il salvataggio config.")
1039 : _("Disattivazione non completata. Controlla PolicyKit, il gestore servizi, il backup in /etc/xdg/autostart, bsc-daemon e il salvataggio config.");
1040 }
1041
1045 setStatus(status);
1046}
1047
1049{
1050 const bool serviceEnabled = runCommand(
1051 shellQuote(kServiceCtlPath) + " --system is-enabled " +
1052 shellQuote(kServiceName) + " >/dev/null 2>&1");
1053 const bool autostartPresent = std::filesystem::exists(kSystemAutostartDesktop) ||
1054 std::filesystem::exists(kFallbackSystemAutostartDesktop);
1055 std::string text;
1056 if (m_protectionEnabled) {
1057 text = _("Protezione attiva nelle impostazioni utente.");
1058 } else {
1059 text = _("Protezione disattivata nelle impostazioni utente.");
1060 }
1061 text += std::string(" ") + (serviceEnabled ? _("Servizio di sistema abilitato.") : _("Servizio di sistema disabilitato."));
1062 text += std::string(" ") + (autostartPresent ? _("Avvio automatico presente.") : _("Avvio automatico assente."));
1063 if (!m_autostartBackupFile.empty() && std::filesystem::exists(m_autostartBackupFile)) {
1064 text += std::string(" ") + _("Backup autostart disponibile.");
1065 }
1069}
1070
1071// ---------------------------------------------------------------------------
1072// on_close_request
1073//
1074// La X sulla header bar e il minimize nascondono la finestra nella systray.
1075// Il "Esci" nella systray chiama close() direttamente (bypassa questo override
1076// perché imposta prima m_tray.stop() e poi quit()), ma per sicurezza tracciamo
1077// lo stato con un flag interno.
1078// ---------------------------------------------------------------------------
1079
1081{
1082 // Nascondi nella systray invece di chiudere
1083 hide();
1084 return true; // true = non distruggere la finestra
1085}
Verdict
#define _(s)
ReplyAction
Definition UIServer.hpp:42
void noteConnection(const ConnectionInfo &info, const std::string &verdict)
Definition AppsPage.cpp:109
void setLastVerdict(const std::string &verdict)
void onNewConnection(const ConnectionInfo &info, bool pending=true)
Gtk::Stack m_stack
ConnectionsPage m_connectionsPage
Gtk::Box m_mainBox
bool m_darkMode
void applyLanguage(const std::string &lang)
UIServer m_uiServer
static std::string cacheKey(const std::string &app, const std::string &host, const std::string &ip)
void loadConfig()
Gtk::Button * makeSidebarButton(const std::string &iconPath, const std::string &label, const std::string &pageName)
void onVerdict(Verdict v, const ConnectionInfo &info)
void buildHeaderBar()
bool on_close_request() override
~MainWindow() override=default
void refreshSystemIntegrationStatus()
void minimize()
RulesPage m_rulesPage
std::map< std::string, ReplyAction > m_verdictCache
void onDaemonDisconnected()
void applyProtectionEnabled(bool enabled)
void applyTheme(bool dark)
bool m_protectionEnabled
void activateSidebarButton(Gtk::Button *btn)
void saveConfig()
void showLanguageDialog()
bool m_desktopActiveSent
std::unique_ptr< LanguageDialog > m_langDialog
SettingsPage m_settingsPage
TrayIcon m_tray
uint64_t m_statAllowed
uint64_t m_statDenied
std::unique_ptr< AlertDialog > m_alertDialog
Gtk::Button * m_btnLight
void buildSidebar()
Gtk::Label * m_daemonLabel
void setStatus(const std::string &msg)
guint m_statusCtx
void noteDesktopActivity()
void onDaemonAlert(const AlertInfo &alert)
Gtk::Statusbar m_statusbar
std::chrono::steady_clock::time_point m_lastUiActivity
bool pollDesktopActivity()
AboutPage m_aboutPage
std::string m_language
guint m_desktopActivityTimer
Gtk::Button * m_btnDark
Gtk::Button * m_activeButton
void loadTheme(bool dark)
std::string m_autostartBackupFile
void sendDesktopActivityState(bool active, bool force=false)
AppsPage m_appsPage
NetworkPage m_networkPage
std::string m_configFile
void onDaemonConnected()
void startDesktopActivityMonitor()
Glib::RefPtr< Gtk::CssProvider > m_cssProvider
StatsPage m_statsPage
void onNewConnection(const ConnectionInfo &info)
std::vector< Gtk::Button * > m_sidebarButtons
void noteConnection(const ConnectionInfo &info)
void saveLog() const
void setProtectionEnabled(bool enabled)
sigc::signal< void(bool)> & signal_toggle_protection_requested()
void setSystemIntegrationStatus(const std::string &text)
void updateStats(uint64_t allowed, uint64_t denied, uint64_t total)
Definition StatsPage.cpp:50
void set_on_quit(std::function< void()> cb)
Definition TrayIcon.hpp:56
void bind_ui_context()
Definition TrayIcon.cpp:297
bool start()
Definition TrayIcon.cpp:135
void set_on_make_startup_persistent(std::function< void()> cb)
Definition TrayIcon.hpp:55
void stop()
Definition TrayIcon.cpp:282
void set_tooltip(const std::string &text)
Definition TrayIcon.cpp:368
void set_on_activate(std::function< void()> cb)
Definition TrayIcon.hpp:54
sigc::signal< void(const ConnectionInfo &)> signal_new_connection
Definition UIServer.hpp:91
sigc::signal< void()> signal_daemon_disconnected
Definition UIServer.hpp:90
void sendReply(ReplyAction action)
Definition UIServer.cpp:422
void stop()
Definition UIServer.cpp:421
void queueNotification(int type, const std::string &data={})
Definition UIServer.cpp:423
sigc::signal< void(const AlertInfo &)> signal_alert
Definition UIServer.hpp:92
sigc::signal< void()> signal_daemon_connected
Definition UIServer.hpp:89
void start(const std::string &socketPath="unix:///tmp/bsd-daemon.sock")
Definition UIServer.cpp:420
constexpr const char * kSystemAutostartDesktop
bool runCommand(const std::string &cmd)
std::string shellQuote(const std::string &s)
std::optional< bool > queryScreensaverActive()
constexpr const char * kUISocketPath
constexpr const char * kServiceCtlPath
std::optional< bool > queryScreensaverActiveOnce(const char *bus_name, const char *object_path, const char *interface_name)
constexpr const char * kFallbackSystemAutostartDesktop
constexpr const char * kServiceName
std::string firstExistingPath(const std::initializer_list< std::string > &candidates)
constexpr const char * kSystemAutostartBackupDesktop
bool writeServiceControlPreference(bool enabled, const std::string &backupFile)
std::string displayNameForStatus(const ConnectionInfo &info)
bool writeConfigAutostartDesktop(const std::filesystem::path &path)
std::string best_connection_display_name(const std::string &app_comm, const std::string &app_path, const std::vector< std::string > &app_args, const std::string &dst_ip, const std::string &src_ip)
std::string resource(const std::string &rel)
Definition Resource.hpp:31
ConnectionInfo connection
Definition UIServer.hpp:68
bool has_connection
Definition UIServer.hpp:69
std::string text
Definition UIServer.hpp:64
std::string src_ip
Definition UIServer.hpp:35
std::string dest_host
Definition UIServer.hpp:37
uint32_t dest_port
Definition UIServer.hpp:39
std::string app_path
Definition UIServer.hpp:27
std::vector< std::string > app_args
Definition UIServer.hpp:30
std::string dest_ip
Definition UIServer.hpp:38
std::string app_comm
Definition UIServer.hpp:28
#define BSC_SERVICECTL_PATH