/*
* 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 .
*
* BastionGuard™ is a trademark of Calogero Scarnà.
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using json = nlohmann::json;
namespace fs = std::filesystem;
static std::string now_iso() {
using namespace std::chrono;
auto t = system_clock::now();
std::time_t tt = system_clock::to_time_t(t);
std::tm tm{};
gmtime_r(&tt, &tm);
std::ostringstream ss;
ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
return ss.str();
}
static bool read_exact(std::istream &in, void* buf, std::size_t n) {
std::size_t off = 0;
char* p = static_cast(buf);
while (off < n) {
in.read(p + off, n - off);
std::streamsize got = in.gcount();
if (got <= 0) return false;
off += static_cast(got);
}
return true;
}
json read_message() {
uint32_t len = 0;
if (!read_exact(std::cin, &len, sizeof(len))) throw std::runtime_error("EOF reading length");
uint32_t L = (uint8_t)reinterpret_cast(&len)[0]
| ((uint8_t)reinterpret_cast(&len)[1] << 8)
| ((uint8_t)reinterpret_cast(&len)[2] << 16)
| ((uint8_t)reinterpret_cast(&len)[3] << 24);
std::vector buf(L);
if (!read_exact(std::cin, buf.data(), L)) throw std::runtime_error("EOF reading payload");
std::string s(buf.begin(), buf.end());
return json::parse(s, nullptr, false);
}
void send_message(const json &j) {
std::string s = j.dump();
uint32_t L = static_cast(s.size());
unsigned char lenbuf[4];
lenbuf[0] = static_cast(L & 0xff);
lenbuf[1] = static_cast((L >> 8) & 0xff);
lenbuf[2] = static_cast((L >> 16) & 0xff);
lenbuf[3] = static_cast((L >> 24) & 0xff);
std::cout.write(reinterpret_cast(lenbuf), 4);
std::cout.write(s.data(), s.size());
std::cout.flush();
}
static std::unordered_set cached_bl;
static std::time_t cached_bl_mtime = 0;
static fs::path cached_bl_path;
static inline std::string str_tolower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return std::tolower(c); });
return s;
}
static inline std::string str_trim(std::string s) {
const char* ws = " \t\r\n";
size_t start = s.find_first_not_of(ws);
if (start==std::string::npos) return "";
size_t end = s.find_last_not_of(ws);
return s.substr(start, end-start+1);
}
static std::time_t to_time_t(fs::file_time_type ftime) {
using namespace std::chrono;
auto sctp = time_point_cast(
ftime - fs::file_time_type::clock::now()
+ system_clock::now()
);
return system_clock::to_time_t(sctp);
}
static void reload_blacklist_if_needed() {
const char* home = std::getenv("HOME");
if (!home) return;
fs::path bl = fs::path(home) / ".local/share/BastionGuard/data/phishing/blacklist.txt";
std::error_code ec;
if (!fs::exists(bl, ec)) {
std::cerr << "[native-host] blacklist not found: " << bl << std::endl;
cached_bl.clear();
cached_bl_mtime = 0;
return;
}
auto ftime = fs::last_write_time(bl, ec);
if (ec) return;
std::time_t mtime = to_time_t(ftime);
if (mtime == cached_bl_mtime && !cached_bl.empty()) return;
std::ifstream f(bl);
if (!f.is_open()) {
std::cerr << "[native-host] cannot open blacklist file" << std::endl;
return;
}
std::unordered_set newset;
std::string line;
while (std::getline(f, line)) {
line = str_trim(line);
if (line.empty() || line[0]=='#') continue;
if (!line.empty() && line.back()=='\r') line.pop_back();
newset.insert(str_tolower(line));
}
cached_bl = std::move(newset);
cached_bl_mtime = mtime;
cached_bl_path = bl;
std::cerr << "[native-host] loaded blacklist entries=" << cached_bl.size() << std::endl;
}
static std::string extract_host_from_url(const std::string &url) {
size_t p = url.find("://");
size_t start = (p==std::string::npos) ? 0 : p+3;
size_t at = url.find('@', start);
if (at != std::string::npos) start = at+1;
size_t end = url.find_first_of("/:?#", start);
std::string host = (end==std::string::npos) ? url.substr(start) : url.substr(start, end-start);
size_t colon = host.find(':');
if (colon != std::string::npos) host = host.substr(0, colon);
return str_tolower(str_trim(host));
}
json action_set_lang(const json &req) { return {{"ok", true}}; } // stub
json action_restart_scanner(const json &) { return {{"ok", true}}; } // stub
json action_open_page(const json &req) { return {{"ok", true}}; } // stub
json action_ping(const json &) { return { {"ok", true}, {"pong_at", now_iso()} }; }
json action_check_url(const json &req) {
try {
if (!req.contains("url") || !req["url"].is_string())
return { {"ok", false}, {"error", "missing 'url' string"} };
std::string url = req["url"].get();
reload_blacklist_if_needed();
json extra;
extra["block"] = false;
extra["reason"] = "not_in_blacklist";
if (!cached_bl.empty()) {
std::string host = extract_host_from_url(url);
if (!host.empty()) {
if (cached_bl.find(host) != cached_bl.end()) {
extra["block"] = true;
extra["reason"] = "host_exact: " + host;
} else {
for (const auto &entry : cached_bl) {
if (entry.empty()) continue;
if (host.size() >= entry.size() &&
host.compare(host.size()-entry.size(), entry.size(), entry) == 0) {
extra["block"] = true;
extra["reason"] = "host_suffix: " + entry;
break;
}
}
}
} else {
for (const auto &entry : cached_bl) {
if (!entry.empty() && url.find(entry)!=std::string::npos) {
extra["block"] = true;
extra["reason"] = "url_substr: " + entry;
break;
}
}
}
}
return { {"ok", true}, {"url", url}, {"extra", extra} };
} catch (const std::exception &ex) {
return { {"ok", false}, {"error", std::string("exception: ")+ex.what()} };
}
}
json action_get_mail_config(const json &) {
try {
const char* home = std::getenv("HOME");
if (!home) return { {"ok", false}, {"error", "HOME non impostata"} };
fs::path config_path = fs::path(home) / ".config/BastionGuard/mail.json";
std::error_code ec;
if (!fs::exists(config_path, ec))
return { {"ok", false}, {"error", "mail.json non trovato: " + config_path.string()} };
std::ifstream f(config_path);
if (!f.is_open())
return { {"ok", false}, {"error", "impossibile aprire mail.json"} };
json config = json::parse(f, nullptr, false);
if (config.is_discarded())
return { {"ok", false}, {"error", "mail.json non è JSON valido"} };
return { {"ok", true}, {"config", config} };
} catch (const std::exception &ex) {
return { {"ok", false}, {"error", std::string("eccezione: ") + ex.what()} };
}
}
json dispatch(const json &req) {
if (!req.is_object()) return { {"ok", false}, {"error", "request not an object"} };
if (!req.contains("action") || !req["action"].is_string()) return { {"ok", false}, {"error", "missing 'action'"} };
std::string act = req["action"].get();
if (act == "set_lang") return action_set_lang(req);
if (act == "restart_scanner") return action_restart_scanner(req);
if (act == "open_page") return action_open_page(req);
if (act == "ping") return action_ping(req);
if (act == "check_url") return action_check_url(req);
if (act == "get_mail_config") return action_get_mail_config(req);
return { {"ok", false}, {"error", "unknown action: " + act} };
}
int main() {
try {
while (true) {
json req = read_message();
if (req.is_discarded()) break;
std::cerr << "[native-host] req: " << req.dump() << std::endl;
json resp = dispatch(req);
resp["ts"] = now_iso();
send_message(resp);
std::cerr << "[native-host] resp: " << resp.dump() << std::endl;
}
} catch (const std::exception &ex) {
std::cerr << "[native-host] fatal: " << ex.what() << std::endl;
try {
json e = { {"ok", false}, {"error", std::string("fatal: ") + ex.what()} };
send_message(e);
} catch(...) {}
return 1;
}
return 0;
}