/* * 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; }