283 lines
9.8 KiB
C++
283 lines
9.8 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 <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cstdint>
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <fstream>
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <unordered_set>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
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<char*>(buf);
|
|
while (off < n) {
|
|
in.read(p + off, n - off);
|
|
std::streamsize got = in.gcount();
|
|
if (got <= 0) return false;
|
|
off += static_cast<std::size_t>(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<char*>(&len)[0]
|
|
| ((uint8_t)reinterpret_cast<char*>(&len)[1] << 8)
|
|
| ((uint8_t)reinterpret_cast<char*>(&len)[2] << 16)
|
|
| ((uint8_t)reinterpret_cast<char*>(&len)[3] << 24);
|
|
std::vector<char> 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<uint32_t>(s.size());
|
|
unsigned char lenbuf[4];
|
|
lenbuf[0] = static_cast<unsigned char>(L & 0xff);
|
|
lenbuf[1] = static_cast<unsigned char>((L >> 8) & 0xff);
|
|
lenbuf[2] = static_cast<unsigned char>((L >> 16) & 0xff);
|
|
lenbuf[3] = static_cast<unsigned char>((L >> 24) & 0xff);
|
|
std::cout.write(reinterpret_cast<char*>(lenbuf), 4);
|
|
std::cout.write(s.data(), s.size());
|
|
std::cout.flush();
|
|
}
|
|
|
|
static std::unordered_set<std::string> 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<system_clock::duration>(
|
|
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<std::string> 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<std::string>();
|
|
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<std::string>();
|
|
|
|
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;
|
|
}
|