BastionGuard/src/SambaPage.cpp

553 lines
15 KiB
C++

/*
* BastionGuard™
* Copyright (C) 2025–2026 Calogero Scarnà
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* BastionGuard™ is a trademark of Calogero Scarnà.
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "SafeProcess.hpp"
#include "SambaPage.hpp"
#include "Utils.hpp"
#include "Backend.hpp"
#include "Resource.hpp"
#include <gtkmm.h>
#include <giomm.h>
#include <glibmm/main.h>
#include <glibmm/i18n.h>
#include <filesystem>
#include <iostream>
#include <thread>
#include <atomic>
#include <regex>
static bool clamdscan_running()
{
return ((bg_process::run({"pgrep", "-f", "clamdscan"}).exit_code) == 0);
}
static bool samba_temp_exists()
{
namespace fs = std::filesystem;
try {
fs::path base = fs::temp_directory_path() / "BastionGuard";
if (!fs::exists(base) || !fs::is_directory(base))
return false;
for (auto& p : fs::directory_iterator(base)) {
if (p.is_directory()) {
auto name = p.path().filename().string();
if (name.rfind("samba_scan_", 0) == 0)
return true;
}
}
} catch (...) {
return false;
}
return false;
}
SambaPage::SambaPage()
: Gtk::Box(Gtk::Orientation::VERTICAL, 12)
{
set_margin(12);
sambaStore = Gtk::ListStore::create(sambaCols);
sambaView.set_model(sambaStore);
auto hboxSwitch = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
auto lbl = Gtk::make_managed<Gtk::Label>(_("<b>Abilita scansione Samba</b>"));
lbl->set_use_markup(true);
lbl->set_hexpand(true);
lbl->set_halign(Gtk::Align::START);
switchEnable.set_valign(Gtk::Align::CENTER);
switchEnable.signal_state_set().connect(
[this](bool){
if (!loading_)
saveConfig();
return false;
},
false
);
hboxSwitch->append(*lbl);
hboxSwitch->append(switchEnable);
append(*hboxSwitch);
auto col = Gtk::make_managed<Gtk::TreeViewColumn>(_("Percorso"));
auto cell = Gtk::make_managed<Gtk::CellRendererText>();
col->pack_start(*cell, true);
col->add_attribute(cell->property_text(), sambaCols.col_path);
sambaView.append_column(*col);
auto scroll = Gtk::make_managed<Gtk::ScrolledWindow>();
scroll->set_child(sambaView);
scroll->set_vexpand(true);
scroll->set_min_content_height(180);
append(*scroll);
auto btnBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
btnAdd.set_label(_("Aggiungi"));
btnRemove.set_label(_("Rimuovi"));
btnScan.set_label(_("Scansiona ora"));
btnSave.set_label(_("Salva configurazione"));
btnBox->append(btnAdd);
btnBox->append(btnRemove);
btnBox->append(btnScan);
btnBox->append(btnSave);
btnBox->set_halign(Gtk::Align::END);
append(*btnBox);
// Signals
btnAdd.signal_clicked().connect(sigc::mem_fun(*this, &SambaPage::onAddDir));
btnRemove.signal_clicked().connect(sigc::mem_fun(*this, &SambaPage::onRemoveDir));
btnSave.signal_clicked().connect(sigc::mem_fun(*this, &SambaPage::saveConfig));
btnScan.signal_clicked().connect(sigc::mem_fun(*this, &SambaPage::onScanNow));
loadConfig();
refreshUI();
}
void SambaPage::loadConfig()
{
loading_ = true;
sambaStore->clear();
switchEnable.set_active(Backend::instance().isSambaEnabled());
for (const auto &dir : Backend::instance().getSambaDirs())
{
if (dir.empty()) continue;
auto row = *(sambaStore->append());
row[sambaCols.col_path] = dir;
row[sambaCols.col_color] = "white";
}
loading_ = false;
}
void SambaPage::saveConfig()
{
std::vector<std::string> dirs;
for (auto &row : sambaStore->children())
{
Glib::ustring p = row[sambaCols.col_path];
if (!p.empty())
dirs.push_back(p);
}
Backend::instance().saveSambaConfig(
switchEnable.get_active(),
dirs
);
refreshUI();
Utils::show_message(
dynamic_cast<Gtk::Window*>(get_root()),
_("Configurazione Samba salvata correttamente."),
Gtk::MessageType::INFO);
}
void SambaPage::refreshUI()
{
btnScan.set_sensitive(
switchEnable.get_active() &&
sambaStore->children().size() > 0
);
}
void SambaPage::onAddDir()
{
auto dlg = Gtk::make_managed<Gtk::Dialog>();
Gtk::Dialog* dialog = dlg;
dialog->set_title("");
dialog->set_modal(false);
dialog->set_destroy_with_parent(true);
dialog->set_default_size(480, 260);
dialog->set_resizable(false);
auto ctx = dialog->get_style_context();
ctx->add_class("app-dialog");
ctx->add_class("main-window");
ctx->add_class("app-window");
auto header = Gtk::make_managed<Gtk::HeaderBar>();
header->set_show_title_buttons(false);
header->set_decoration_layout("");
header->add_css_class("custom-headerbar");
auto close_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
close_box->set_margin_end(6);
try {
auto px = Gdk::Pixbuf::create_from_file(resource("icon-close.png"), 28, 28, true);
auto btnCloseIcon = Gtk::make_managed<Gtk::Button>();
btnCloseIcon->set_child(*Gtk::make_managed<Gtk::Image>(px));
btnCloseIcon->add_css_class("header-button");
btnCloseIcon->signal_clicked().connect([dialog]() { dialog->close(); });
close_box->append(*btnCloseIcon);
} catch (...) {}
header->pack_end(*close_box);
auto header_wrapper = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
header_wrapper->append(*header);
dialog->set_titlebar(*header_wrapper);
auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 14);
box->set_margin(20);
dialog->get_content_area()->append(*box);
auto lblTitle = Gtk::make_managed<Gtk::Label>(_("<b>Aggiungi percorso SMB</b>"));
lblTitle->set_use_markup(true);
lblTitle->set_halign(Gtk::Align::CENTER);
lblTitle->add_css_class("title");
box->append(*lblTitle);
auto entryUrl = Gtk::make_managed<Gtk::Entry>();
entryUrl->set_placeholder_text("smb://server/share/percorso");
auto entryUser = Gtk::make_managed<Gtk::Entry>();
entryUser->set_placeholder_text(_("Username (opzionale)"));
auto entryPass = Gtk::make_managed<Gtk::Entry>();
entryPass->set_placeholder_text(_("Password (opzionale)"));
entryPass->set_visibility(false);
box->append(*entryUrl);
box->append(*entryUser);
box->append(*entryPass);
auto infoAlert = Gtk::make_managed<Gtk::Label>(_(
"<i>Nota: in caso di rilevamento malware su file remoti,\n"
"sarà necessario intervenire manualmente per la rimozione.</i>")
);
infoAlert->set_use_markup(true);
infoAlert->add_css_class("alert-info");
infoAlert->set_halign(Gtk::Align::CENTER);
box->append(*infoAlert);
auto btnRow = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 12);
btnRow->set_halign(Gtk::Align::CENTER);
box->append(*btnRow);
auto btnOk = Gtk::make_managed<Gtk::Button>(_("Aggiungi"));
auto btnCancel = Gtk::make_managed<Gtk::Button>(_("Annulla"));
btnOk->add_css_class("btn-success");
btnCancel->add_css_class("btn-danger");
btnRow->append(*btnOk);
btnRow->append(*btnCancel);
btnOk->signal_clicked().connect([=, this]() {
std::string url = entryUrl->get_text();
url = Backend::resolveSambaDir(url);
std::replace(url.begin(), url.end(), '\\', '/');
if (url.size() > 7 && url.back() == '/')
url.pop_back();
if (!Backend::isValidSMB(url))
{
Utils::show_message(
dynamic_cast<Gtk::Window*>(get_root()),
_("Percorso SMB non valido:\n") + url,
Gtk::MessageType::ERROR);
return;
}
auto row = *(sambaStore->append());
row[sambaCols.col_path] = url;
Backend::instance().saveSMBCredentials(
url,
entryUser->get_text(),
entryPass->get_text()
);
saveConfig();
dialog->close();
});
btnCancel->signal_clicked().connect([dialog]() {
dialog->close();
});
dialog->present();
}
void SambaPage::onRemoveDir()
{
auto sel = sambaView.get_selection();
if (!sel) return;
auto it = sel->get_selected();
if (it)
{
sambaStore->erase(it);
saveConfig();
}
}
void SambaPage::onScanNow()
{
Backend::instance().resetSambaStop();
auto active = std::make_shared<std::atomic<bool>>(true);
auto dialog_open = std::make_shared<bool>(true);
auto dialog = Gtk::make_managed<Gtk::Dialog>();
Gtk::Dialog* dlg = dialog;
dlg->set_title("");
dlg->set_modal(false);
dlg->set_default_size(520, 420);
dlg->set_resizable(false);
auto ctx = dlg->get_style_context();
ctx->add_class("app-dialog");
ctx->add_class("main-window");
ctx->add_class("app-window");
auto header = Gtk::make_managed<Gtk::HeaderBar>();
header->set_show_title_buttons(false);
header->set_decoration_layout("");
header->add_css_class("custom-headerbar");
auto close_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
close_box->set_margin_end(6);
try {
auto px = Gdk::Pixbuf::create_from_file(resource("icon-close.png"), 28, 28, true);
auto btnCloseIcon = Gtk::make_managed<Gtk::Button>();
btnCloseIcon->set_child(*Gtk::make_managed<Gtk::Image>(px));
btnCloseIcon->add_css_class("header-button");
btnCloseIcon->signal_clicked().connect([dlg]() { dlg->close(); });
close_box->append(*btnCloseIcon);
} catch (...) {}
header->pack_end(*close_box);
auto header_wrapper = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
header_wrapper->append(*header);
dlg->set_titlebar(*header_wrapper);
auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 20);
box->set_margin(20);
dlg->get_content_area()->append(*box);
auto lbl = Gtk::make_managed<Gtk::Label>(_("Scansione Samba avviata…"));
lbl->set_halign(Gtk::Align::CENTER);
lbl->add_css_class("title");
box->append(*lbl);
auto lblCurrent = Gtk::make_managed<Gtk::Label>(_("File corrente: --"));
lblCurrent->set_halign(Gtk::Align::CENTER);
lblCurrent->add_css_class("subtitle");
box->append(*lblCurrent);
auto prog = Gtk::make_managed<Gtk::ProgressBar>();
prog->set_show_text(true);
prog->set_text("0%");
prog->set_fraction(0.0);
prog->set_halign(Gtk::Align::CENTER);
prog->set_margin_bottom(10);
box->append(*prog);
auto fileList = Gtk::make_managed<Gtk::ListBox>();
fileList->set_vexpand(true);
fileList->add_css_class("scan-file-list");
auto scroll = Gtk::make_managed<Gtk::ScrolledWindow>();
scroll->set_child(*fileList);
scroll->set_vexpand(true);
scroll->set_min_content_height(140);
box->append(*scroll);
auto buttonRow = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 20);
buttonRow->set_halign(Gtk::Align::CENTER);
box->append(*buttonRow);
auto btnStop = Gtk::make_managed<Gtk::Button>(_("Interrompi scansione"));
btnStop->add_css_class("btn-danger");
auto btnClose = Gtk::make_managed<Gtk::Button>(_("Chiudi"));
btnClose->add_css_class("btn-success");
buttonRow->append(*btnStop);
buttonRow->append(*btnClose);
dlg->present();
btnStop->signal_clicked().connect([dlg, active]() {
Backend::instance().requestStopSambaScan();
if (active->load()) {
active->store(false);
dlg->close();
}
});
btnClose->signal_clicked().connect([dlg]() { dlg->close(); });
auto timeoutFiles = std::make_shared<sigc::connection>();
*timeoutFiles = Glib::signal_timeout().connect(
[lbl, lblCurrent, prog, fileList, btnStop, dialog_open]() -> bool
{
if (!*dialog_open)
return false;
auto st = Backend::instance().get_current_scan_files();
if (!st.scan_active)
{
const size_t logCount = st.files.size();
const size_t infected = st.infected;
std::string summary =
Glib::ustring::compose(
_("Scansione completata.\n"
"File analizzati: %1\n"
"File infetti: %2"),
logCount,
infected
);
lbl->set_text(summary);
lblCurrent->set_text(_("File corrente: --"));
prog->set_fraction(1.0);
prog->set_text("100%");
btnStop->set_sensitive(false);
btnStop->remove_css_class("btn-danger");
btnStop->add_css_class("btn-grey");
while (auto row = fileList->get_row_at_index(0))
fileList->remove(*row);
for (const auto& f : st.files)
{
auto lblLine = Gtk::make_managed<Gtk::Label>(f);
lblLine->set_halign(Gtk::Align::START);
auto row = Gtk::make_managed<Gtk::ListBoxRow>();
row->set_child(*lblLine);
fileList->append(*row);
}
return true;
}
lblCurrent->set_text(_("File corrente: ") +
(st.current_scanning.empty() ? "--" : st.current_scanning));
prog->set_fraction(st.progress);
prog->set_text(std::to_string(int(st.progress * 100)) + "%");
while (auto row = fileList->get_row_at_index(0))
fileList->remove(*row);
for (const auto& f : st.files)
{
auto lblLine = Gtk::make_managed<Gtk::Label>(f);
lblLine->set_halign(Gtk::Align::START);
auto row = Gtk::make_managed<Gtk::ListBoxRow>();
row->set_child(*lblLine);
fileList->append(*row);
}
return true;
},
250
);
dlg->signal_close_request().connect(
[dialog_open, timeoutFiles]() -> bool {
*dialog_open = false;
if (timeoutFiles->connected())
timeoutFiles->disconnect();
Backend::instance().requestStopSambaScan();
return false;
},
false
);
Backend::instance().samba_progress_callback =
[lbl, dialog_open](const std::string& summary, float pct)
{
Glib::signal_idle().connect_once(
[lbl, dialog_open, summary]()
{
if (!*dialog_open) return;
lbl->set_text(summary);
}
);
};
std::thread([]() {
auto dirs = Backend::instance().getSambaDirs();
for (const auto& d : dirs)
Backend::instance().scanSambaDir(d);
}).detach();
}