# webutils/index_utils.py from __future__ import annotations import os import json import time import socket import platform import glob import subprocess import psutil import resource import logging from logger import Logger from typing import Optional from datetime import datetime from typing import Any, Dict, Tuple # --------- Singleton module (évite recréation à chaque requête) ---------- logger = Logger(name="index_utils.py", level=logging.DEBUG) class IndexUtils: def __init__(self, shared_data): self.shared_data = shared_data self.logger = logger self.db = shared_data.db # Cache pour l'assemblage de stats (champs dynamiques) self._last_stats: Dict[str, Any] = {} self._last_stats_ts: float = 0.0 self._cache_ttl: float = 5.0 # 5s # Cache pour l'info système (rarement changeant) self._system_info_cache: Dict[str, Any] = {} self._system_info_ts: float = 0.0 self._system_cache_ttl: float = 300.0 # 5 min # Cache wardrive (compte Wi-Fi connus) self._wardrive_cache_mem: Optional[int] = None self._wardrive_ts_mem: float = 0.0 self._wardrive_ttl: float = 600.0 # 10 min def _fds_usage(self): try: used = len(os.listdir(f"/proc/{os.getpid()}/fd")) soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) # self.logger.info(f"FD usage: {used} used / {soft} soft limit / {hard} hard limit") return used, soft except Exception as e: # self.logger.debug(f"FD usage error: {e}") return 0, 0 def _open_fds_count(self) -> int: """Compte le nombre de file descriptors ouverts (proc global).""" try: return len(glob.glob("/proc/*/fd/*")) except Exception as e: # self.logger.debug(f"FD probe error: {e}") return 0 # ---------------------- Helpers JSON ---------------------- def _to_jsonable(self, obj): if obj is None or isinstance(obj, (bool, int, float, str)): return obj if isinstance(obj, bytes): import base64 return {"_b64": base64.b64encode(obj).decode("ascii")} if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, dict): return {k: self._to_jsonable(v) for k, v in obj.items()} if isinstance(obj, (list, tuple, set)): return [self._to_jsonable(v) for v in obj] return str(obj) def _json(self, handler, code: int, obj): payload = json.dumps(self._to_jsonable(obj), ensure_ascii=False).encode("utf-8") handler.send_response(code) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(payload))) handler.end_headers() try: handler.wfile.write(payload) except BrokenPipeError: pass # ---------------------- Helpers FS ---------------------- def _read_text(self, path: str) -> Optional[str]: try: with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read().strip() except Exception: return None # ---------------------- Config store ---------------------- def _cfg_get(self, key: str, default=None): try: row = self.db.query_one("SELECT value FROM config WHERE key=? LIMIT 1;", (key,)) if not row or row.get("value") is None: return default raw = row["value"] try: return json.loads(raw) except Exception: return raw except Exception: return default def _cfg_set(self, key: str, value) -> None: try: s = json.dumps(value, ensure_ascii=False) except Exception: s = json.dumps(str(value), ensure_ascii=False) self.db.execute( """ INSERT INTO config(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value """, (key, s), ) # ---------------------- Info système ---------------------- def _get_system_info(self) -> Dict[str, Any]: now = time.time() if self._system_info_cache and (now - self._system_info_ts) < self._system_cache_ttl: return self._system_info_cache os_name, os_ver = self._os_release() arch = self._arch_bits() model = self._pi_model() epd_connected = self._check_epd_connected() epd_type = self._cfg_get("epd_type", "epd2in13_V4") self._system_info_cache = { "os_name": os_name, "os_version": os_ver, "arch": arch, "model": model, "waveshare_epd_connected": epd_connected, "waveshare_epd_type": epd_type if epd_connected else None, } self._system_info_ts = now return self._system_info_cache def _os_release(self) -> Tuple[str, str]: data = {} txt = self._read_text("/etc/os-release") or "" for line in txt.splitlines(): if "=" in line: k, v = line.split("=", 1) data[k.strip()] = v.strip().strip('"') name = data.get("PRETTY_NAME") or data.get("NAME") or platform.system() ver = data.get("VERSION_ID") or data.get("VERSION") or platform.version() return (name, ver) def _arch_bits(self) -> str: try: a = platform.architecture()[0] return "64-bit" if "64" in a else "32-bit" except Exception: return "unknown" def _pi_model(self) -> str: dt_model = self._read_text("/proc/device-tree/model") if dt_model: return dt_model.replace("\x00", "").strip() return platform.machine() def _check_epd_connected(self) -> bool: # I2C puis fallback SPI try: result = subprocess.run(["i2cdetect", "-y", "1"], capture_output=True, text=True, timeout=1) if result.returncode == 0: output = result.stdout if any(addr in output for addr in ["3c", "3d", "48"]): return True except Exception: pass return os.path.exists("/dev/spidev0.0") def _uptime_str(self) -> str: try: up = self._read_text("/proc/uptime") seconds = int(float(up.split()[0])) if up else 0 except Exception: seconds = 0 d, r = divmod(seconds, 86400) h, r = divmod(r, 3600) m, s = divmod(r, 60) return f"{d}d {h:02d}:{m:02d}:{s:02d}" if d else f"{h:02d}:{m:02d}:{s:02d}" def _first_init_ts(self) -> int: v = self._cfg_get("first_init_ts") if isinstance(v, (int, float)) and v > 0: return int(v) try: row = self.db.query_one( """ SELECT strftime('%s', MIN(created_at)) AS ts FROM ( SELECT created_at FROM comments UNION SELECT created_at FROM action_queue UNION SELECT first_seen FROM hostnames_history ) """ ) if row and row.get("ts"): ts = int(row["ts"]) self._cfg_set("first_init_ts", ts) return ts except Exception: pass ts = int(time.time()) self._cfg_set("first_init_ts", ts) return ts # ---------------------- Monitoring ressources ---------------------- def _cpu_pct(self) -> int: try: return int(psutil.cpu_percent(interval=0.5)) except Exception: return 0 def _mem_bytes(self) -> Tuple[int, int]: try: vm = psutil.virtual_memory() return int(vm.total - vm.available), int(vm.total) except Exception: try: info = self._read_text("/proc/meminfo") or "" def kb(k): line = next((l for l in info.splitlines() if l.startswith(k + ":")), None) return int(line.split()[1]) * 1024 if line else 0 total = kb("MemTotal") free = kb("MemFree") + kb("Buffers") + kb("Cached") used = max(0, total - free) return used, total except Exception: return 0, 0 def _disk_bytes(self) -> Tuple[int, int]: try: usage = psutil.disk_usage("/") return int(usage.used), int(usage.total) except Exception: try: st = os.statvfs("/") total = st.f_frsize * st.f_blocks free = st.f_frsize * st.f_bavail return int(total - free), int(total) except Exception: return 0, 0 def _battery_probe(self) -> Dict[str, Any]: base = "/sys/class/power_supply" try: if not os.path.isdir(base): return {"present": False} bat = None for n in os.listdir(base): if n.startswith("BAT"): bat = os.path.join(base, n) break if not bat: return {"present": False} cap = self._read_text(os.path.join(bat, "capacity")) stat = (self._read_text(os.path.join(bat, "status")) or "Unknown").lower() lvl = int(cap) if cap and cap.isdigit() else 0 if stat.startswith("full"): state = "Full" elif stat.startswith("char"): state = "Charging" elif stat.startswith("dis"): state = "Discharging" else: state = "Unknown" return {"present": True, "level_pct": max(0, min(100, lvl)), "state": state} except Exception as e: # self.logger.debug(f"Battery probe error: {e}") return {"present": False} # ---------------------- Réseau ---------------------- def _quick_internet(self, timeout: float = 1.0) -> bool: try: for server in ["1.1.1.1", "8.8.8.8"]: try: with socket.create_connection((server, 53), timeout=timeout): return True except Exception: continue return False except Exception: return False def _ip_for(self, ifname: str) -> Optional[str]: try: result = subprocess.run( ["ip", "-4", "addr", "show", "dev", ifname], capture_output=True, text=True, timeout=1 ) if result.returncode == 0: for line in result.stdout.splitlines(): line = line.strip() if line.startswith("inet "): return line.split()[1].split("/")[0] except Exception: pass return None def _gw_dns(self) -> Tuple[Optional[str], Optional[str]]: gw = None dns = None try: out = subprocess.run(["ip", "route"], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): line = line.strip() if line.startswith("default "): parts = line.split() if "via" in parts: idx = parts.index("via") if idx + 1 < len(parts): gw = parts[idx + 1] break except Exception: pass rc = self._read_text("/etc/resolv.conf") or "" for line in rc.splitlines(): line = line.strip() if line.startswith("nameserver "): dns = line.split()[1] break return gw, dns def _wifi_ssid(self) -> Optional[str]: try: out = subprocess.run(["iw", "dev", "wlan0", "link"], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): line = line.strip() if line.lower().startswith("ssid:"): return line.split(":", 1)[1].strip() except Exception: pass try: out = subprocess.run(["wpa_cli", "status"], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): if line.startswith("ssid="): return line.split("=", 1)[1] except Exception: pass return None def _detect_gadget_lease(self, prefix: str) -> Optional[str]: for iface in ["usb0", "usb1", "rndis0", "eth1", "bnep0", "pan0"]: ip = self._ip_for(iface) if ip and ip.startswith(prefix): return ip try: out = subprocess.run(["ip", "neigh", "show"], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): parts = line.split() if parts and parts[0].startswith(prefix): return parts[0] except Exception: pass return None def _bt_connected_device(self) -> Optional[str]: try: out = subprocess.run(["bluetoothctl", "info"], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): line = line.strip() if line.startswith("Name:"): return line.split(":", 1)[1].strip() except Exception: pass return None # ---------------------- GPS ---------------------- def _gps_status(self) -> Dict[str, Any]: gps_enabled = self._cfg_get("gps_enabled", False) if not gps_enabled: return {"connected": False, "status": "Not configured"} gps_device = self._cfg_get("gps_device", "/dev/ttyUSB0") if not os.path.exists(gps_device): return {"connected": False, "status": "Device not found"} try: import gpsd gpsd.connect() packet = gpsd.get_current() return { "connected": True, "fix_quality": packet.mode, "sats": packet.sats, "lat": round(packet.lat, 6) if packet.lat else None, "lon": round(packet.lon, 6) if packet.lon else None, "alt": round(packet.alt, 1) if packet.alt else None, "speed": round(packet.hspeed, 1) if packet.hspeed else None, } except Exception: return {"connected": True, "status": "No fix", "fix_quality": 0} # ---------------------- Stats DB (fallback) ---------------------- def _count_open_ports_total_db(self) -> int: try: row = self.db.query_one("SELECT COUNT(*) AS c FROM ports WHERE state='open';") return int(row["c"]) if row else 0 except Exception: return 0 def _alive_hosts_db(self) -> Tuple[int, int]: try: row = self.db.query_one( """ SELECT SUM(CASE WHEN alive=1 THEN 1 ELSE 0 END) AS alive, COUNT(*) AS total FROM hosts """ ) if row: return int(row["alive"] or 0), int(row["total"] or 0) except Exception: pass return 0, 0 def _vulns_total_db(self) -> int: try: row = self.db.query_one("SELECT COUNT(*) AS c FROM vulnerabilities WHERE is_active=1;") return int(row["c"]) if row else 0 except Exception: return 0 def _credentials_count_db(self) -> int: try: row = self.db.query_one("SELECT COUNT(*) AS c FROM creds;") return int(row["c"]) if row else 0 except Exception: return 0 def _files_count_fs(self) -> int: try: data_dir = "/home/bjorn/Bjorn/data/output/data_stolen" if os.path.exists(data_dir): return sum(len(files) for _, _, files in os.walk(data_dir)) return 0 except Exception: return 0 def _scripts_count_db(self) -> int: try: row = self.db.query_one("SELECT COUNT(*) AS c FROM scripts;") return int(row["c"]) if row else 0 except Exception: return 0 def _zombies_count_db(self) -> int: try: row = self.db.query_one("SELECT COUNT(*) AS c FROM stats WHERE id=1;") if row and row.get("c") is not None: return int(row["c"]) except Exception: pass try: row = self.db.query_one("SELECT COUNT(*) AS c FROM agents WHERE LOWER(status)='online';") return int(row["c"]) if row else 0 except Exception: return 0 # ---------------------- Wi-Fi connus (profils NM) ---------------------- def _run_nmcli(self, args: list[str], timeout: float = 4.0) -> Optional[str]: import shutil, os as _os nmcli_path = shutil.which("nmcli") or "/usr/bin/nmcli" env = _os.environ.copy() env.setdefault("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") env.setdefault("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/run/dbus/system_bus_socket") env.setdefault("LC_ALL", "C") try: out = subprocess.run( [nmcli_path, *args], capture_output=True, text=True, timeout=timeout, env=env ) if out.returncode == 0: stdout = out.stdout or "" # self.logger.debug(f"nmcli ok args={args} -> {len(stdout.splitlines())} lines") return stdout # self.logger.debug(f"nmcli rc={out.returncode} args={args} stderr={(out.stderr or '').strip()}") return None except FileNotFoundError: # self.logger.debug("nmcli introuvable") return None except Exception as e: # self.logger.debug(f"nmcli exception {args}: {e}") return None def _known_wifi_count_nmcli(self) -> int: # Try 1: simple (une valeur par ligne) out = self._run_nmcli(["-t", "-g", "TYPE", "connection", "show"]) if out: cnt = sum(1 for line in out.splitlines() if (line or "").strip().lower() in ("wifi", "802-11-wireless")) if cnt > 0: # self.logger.debug(f"known wifi via nmcli TYPE = {cnt}") return cnt # Try 2: NAME,TYPE out = self._run_nmcli(["-t", "-f", "NAME,TYPE", "connection", "show"]) if out: cnt = 0 for line in out.splitlines(): line = (line or "").strip() if not line: continue typ = line.rsplit(":", 1)[-1].strip().lower() if typ in ("wifi", "802-11-wireless"): cnt += 1 if cnt > 0: # self.logger.debug(f"known wifi via nmcli NAME,TYPE = {cnt}") return cnt # Try 3: connection.type out = self._run_nmcli(["-t", "-g", "connection.type", "connection", "show"]) if out: cnt = sum(1 for line in out.splitlines() if (line or "").strip().lower() in ("wifi", "802-11-wireless")) if cnt > 0: # self.logger.debug(f"known wifi via nmcli connection.type = {cnt}") return cnt # Fallback: wpa_supplicant.conf try: conf = self._read_text("/etc/wpa_supplicant/wpa_supplicant.conf") or "" if conf: blocks = conf.count("\nnetwork={") if conf.strip().startswith("network={"): blocks += 1 if blocks > 0: # self.logger.debug(f"known wifi via wpa_supplicant.conf = {blocks}") return blocks except Exception: pass # Dernier recours: config persistée v = self._cfg_get("wardrive_known", 0) # self.logger.debug(f"known wifi via cfg fallback = {v}") return int(v) if isinstance(v, (int, float)) else 0 # Cache wardrive: mémoire (par process) + DB (partagé multi-workers) def _wardrive_known_cached(self) -> int: now = time.time() # 1) cache mémoire if self._wardrive_cache_mem is not None and (now - self._wardrive_ts_mem) < self._wardrive_ttl: return int(self._wardrive_cache_mem) # 2) cache partagé en DB try: row = self.db.query_one("SELECT value FROM config WHERE key='wardrive_cache' LIMIT 1;") if row and row.get("value"): d = json.loads(row["value"]) ts = float(d.get("ts", 0)) if now - ts < self._wardrive_ttl: val = int(d.get("val", 0)) self._wardrive_cache_mem = val self._wardrive_ts_mem = now return val except Exception: pass # 3) refresh si nécessaire val = int(self._known_wifi_count_nmcli()) # maj caches self._wardrive_cache_mem = val self._wardrive_ts_mem = now self._cfg_set("wardrive_cache", {"val": val, "ts": now}) return val # ---------------------- Accès direct shared_data ---------------------- def _count_open_ports_total(self) -> int: try: val = int(getattr(self.shared_data, "port_count", -1)) return val if val >= 0 else self._count_open_ports_total_db() except Exception: return self._count_open_ports_total_db() def _alive_hosts(self) -> Tuple[int, int]: try: alive = int(getattr(self.shared_data, "target_count", -1)) total = int(getattr(self.shared_data, "network_kb_count", -1)) if alive >= 0 and total >= 0: return alive, total except Exception: pass return self._alive_hosts_db() def _vulns_total(self) -> int: try: val = int(getattr(self.shared_data, "vuln_count", -1)) return val if val >= 0 else self._vulns_total_db() except Exception: return self._vulns_total_db() def _credentials_count(self) -> int: try: val = int(getattr(self.shared_data, "cred_count", -1)) return val if val >= 0 else self._credentials_count_db() except Exception: return self._credentials_count_db() def _files_count(self) -> int: try: val = int(getattr(self.shared_data, "data_count", -1)) return val if val >= 0 else self._files_count_fs() except Exception: return self._files_count_fs() def _scripts_count(self) -> int: try: val = int(getattr(self.shared_data, "attacks_count", -1)) return val if val >= 0 else self._scripts_count_db() except Exception: return self._scripts_count_db() def _zombies_count(self) -> int: try: val = int(getattr(self.shared_data, "zombie_count", -1)) return val if val >= 0 else self._zombies_count_db() except Exception: return self._zombies_count_db() def level_bjorn(self) -> int: try: val = int(getattr(self.shared_data, "level_count", -1)) return val if val >= 0 else int(self._cfg_get("level_count", 1)) except Exception: return int(self._cfg_get("level_count", 1)) def _mode_str(self) -> str: try: manual = bool(getattr(self.shared_data, "manual_mode", False)) return "MANUAL" if manual else "AUTO" except Exception: return str(self._cfg_get("bjorn_mode", "AUTO")).upper() # ---------------------- Delta vuln depuis dernier scan ---------------------- def _vulns_delta(self) -> int: last_scan_ts = self._cfg_get("vuln_last_scan_ts") if not last_scan_ts: return 0 try: row = self.db.query_one( """ SELECT COUNT(*) AS c FROM vulnerability_history WHERE event='new' AND CAST(strftime('%s', seen_at) AS INTEGER) >= ? """, (int(last_scan_ts),), ) new_count = int(row["c"]) if row else 0 row = self.db.query_one( """ SELECT COUNT(*) AS c FROM vulnerability_history WHERE event='inactive' AND CAST(strftime('%s', seen_at) AS INTEGER) >= ? """, (int(last_scan_ts),), ) removed_count = int(row["c"]) if row else 0 return new_count - removed_count except Exception: return 0 # ---------------------- Assemblage principal ---------------------- def _assemble_stats(self) -> Dict[str, Any]: now = time.time() if self._last_stats and (now - self._last_stats_ts) < self._cache_ttl: return self._last_stats try: # Comptages rapides via shared_data (+ fallback DB) alive, total = self._alive_hosts() open_ports_total = self._count_open_ports_total() vulns_total = self._vulns_total() vulns_delta = self._vulns_delta() creds = self._credentials_count() zombies = self._zombies_count() files_count = self._files_count() scripts_count = self._scripts_count() wardrive = self._wardrive_known_cached() # Système sys_info = self._get_system_info() uptime = self._uptime_str() first_init = self._first_init_ts() # Meta Bjorn bjorn_level = self.level_bjorn() # Ressources cpu_pct = self._cpu_pct() ram_used, ram_total = self._mem_bytes() sto_used, sto_total = self._disk_bytes() # Batterie batt = self._battery_probe() # Réseau internet_ok = self._quick_internet() gw, dns = self._gw_dns() wifi_ip = self._ip_for("wlan0") wifi_ssid = self._wifi_ssid() if wifi_ip else None eth_ip = self._ip_for("eth0") wifi_radio = self._wifi_radio_on() bt_radio = self._bt_radio_on() eth_link = self._eth_link_up("eth0") usb_phys = self._usb_gadget_active() # USB/BT gadgets usb_lease = self._detect_gadget_lease("172.20.1.") bt_lease = self._detect_gadget_lease("172.20.2.") bt_device = self._bt_connected_device() if bt_lease else None # GPS gps_data = self._gps_status() # Mode mode = self._mode_str() # FDs fds_count = self._open_fds_count() fds_used, fds_limit = self._fds_usage() stats = { "timestamp": int(time.time()), "first_init_ts": int(first_init), "mode": mode, "uptime": uptime, "bjorn_level": bjorn_level, "internet_access": bool(internet_ok), # Hôtes & ports "known_hosts_total": int(total), "alive_hosts": int(alive), "open_ports_alive_total": int(open_ports_total), # Comptes sécurité "wardrive_known": int(wardrive), "vulnerabilities": int(vulns_total), "vulns_delta": int(vulns_delta), "attack_scripts": int(scripts_count), "zombies": int(zombies), "credentials": int(creds), "files_found": int(files_count), "system": { "os_name": sys_info["os_name"], "os_version": sys_info["os_version"], "arch": sys_info["arch"], "model": sys_info["model"], "waveshare_epd_connected": sys_info["waveshare_epd_connected"], "waveshare_epd_type": sys_info["waveshare_epd_type"], "cpu_pct": int(cpu_pct), "ram_used_bytes": int(ram_used), "ram_total_bytes": int(ram_total), "storage_used_bytes": int(sto_used), "storage_total_bytes": int(sto_total), "open_fds": int(fds_used), "fds_limit": int(fds_limit), "fds_global": int(fds_count), }, "gps": {**gps_data}, "battery": {**batt}, "connectivity": { "wifi": bool(wifi_ip), "wifi_radio_on": bool(wifi_radio), # <--- NEW "wifi_ssid": wifi_ssid, "wifi_ip": wifi_ip, "wifi_gw": gw if wifi_ip else None, "wifi_dns": dns if wifi_ip else None, "ethernet": bool(eth_ip), "eth_link_up": bool(eth_link), # <--- NEW "eth_ip": eth_ip, "eth_gw": gw if eth_ip else None, "eth_dns": dns if eth_ip else None, "usb_gadget": bool(usb_lease), "usb_phys_on": bool(usb_phys), # <--- NEW (radio/phys) "usb_lease_ip": usb_lease, "usb_mode": self._get_usb_mode(), "bt_gadget": bool(bt_lease), "bt_radio_on": bool(bt_radio), # <--- NEW "bt_lease_ip": bt_lease, "bt_connected_to": bt_device, }, } self._last_stats = stats self._last_stats_ts = now return stats except Exception as e: if hasattr(self.logger, "error"): self.logger.error(f"Error assembling stats: {e}") if self._last_stats: return self._last_stats return self._get_fallback_stats() def _get_usb_mode(self) -> str: try: udc = self._read_text("/sys/kernel/config/usb_gadget/g1/UDC") if udc: return "OTG" except Exception: pass return "Device" def _get_fallback_stats(self) -> Dict[str, Any]: return { "timestamp": int(time.time()), "status": "error", "message": "Stats collection error - using fallback", "alive_hosts": 0, "known_hosts_total": 0, "open_ports_alive_total": 0, "vulnerabilities": 0, "internet_access": False, "system": { "os_name": "Unknown", "cpu_pct": 0, "ram_used_bytes": 0, "ram_total_bytes": 0, "storage_used_bytes": 0, "storage_total_bytes": 0, }, "connectivity": {"wifi": False, "ethernet": False, "usb_gadget": False, "bt_gadget": False}, "gps": {"connected": False}, "battery": {"present": False}, } # ---------------------- REST ---------------------- def dashboard_stats(self, handler): try: stats = self._assemble_stats() return self._json(handler, 200, stats) except Exception as e: if hasattr(self.logger, "error"): self.logger.error(f"/api/bjorn/stats error: {e}") self.logger.error("Serving cached stats after error") if self._last_stats: return self._json(handler, 200, self._last_stats) return self._json( handler, 500, {"status": "error", "message": str(e), "fallback": self._get_fallback_stats()}, ) def set_config(self, handler, data: Dict[str, Any]): key = (data.get("key") or "").strip() if not key: return self._json(handler, 400, {"status": "error", "message": "key required"}) try: self._cfg_set(key, data.get("value")) if key in ["epd_type", "bjorn_mode", "gps_enabled"]: self._system_info_cache = {} self._last_stats = {} return self._json(handler, 200, {"status": "ok", "key": key}) except Exception as e: return self._json(handler, 400, {"status": "error", "message": str(e)}) def mark_vuln_scan_baseline(self, handler): now = int(time.time()) self._cfg_set("vuln_last_scan_ts", now) return self._json(handler, 200, {"status": "ok", "vuln_last_scan_ts": now}) def reload_generate_actions_json(self, handler): """Recharge le fichier actions.json en exécutant generate_actions_json.""" try: self.shared_data.generate_actions_json() handler.send_response(200) handler.send_header('Content-Type', 'application/json') handler.end_headers() handler.wfile.write(json.dumps({'status': 'success', 'message': 'actions.json reloaded successfully.'}).encode('utf-8')) except Exception as e: self.logger.error(f"Error in reload_generate_actions_json: {e}") handler.send_response(500) handler.send_header('Content-Type', 'application/json') handler.end_headers() handler.wfile.write(json.dumps({'status': 'error', 'message': str(e)}).encode('utf-8')) def clear_shared_config_json(self, handler, restart=True): """Reset config à partir des defaults, en DB.""" try: self.shared_data.config = self.shared_data.get_default_config() self.shared_data.save_config() # -> DB if restart: self.restart_bjorn_service(handler) handler.send_response(200) handler.send_header("Content-type","application/json") handler.end_headers() handler.wfile.write(json.dumps({"status":"success","message":"Configuration reset to defaults"}).encode("utf-8")) except Exception as e: handler.send_response(500) handler.send_header("Content-type","application/json") handler.end_headers() handler.wfile.write(json.dumps({"status":"error","message":str(e)}).encode("utf-8")) def serve_favicon(self, handler): handler.send_response(200) handler.send_header("Content-type", "image/x-icon") handler.end_headers() favicon_path = os.path.join(self.shared_data.web_dir, '/images/favicon.ico') self.logger.info(f"Serving favicon from {favicon_path}") try: with open(favicon_path, 'rb') as file: handler.wfile.write(file.read()) except FileNotFoundError: self.logger.error(f"Favicon not found at {favicon_path}") handler.send_response(404) handler.end_headers() def serve_manifest(self, handler): handler.send_response(200) handler.send_header("Content-type", "application/json") handler.end_headers() manifest_path = os.path.join(self.shared_data.web_dir, 'manifest.json') try: with open(manifest_path, 'r') as file: handler.wfile.write(file.read().encode('utf-8')) except FileNotFoundError: handler.send_response(404) handler.end_headers() def serve_apple_touch_icon(self, handler): handler.send_response(200) handler.send_header("Content-type", "image/png") handler.end_headers() icon_path = os.path.join(self.shared_data.web_dir, 'icons/apple-touch-icon.png') try: with open(icon_path, 'rb') as file: handler.wfile.write(file.read()) except FileNotFoundError: handler.send_response(404) handler.end_headers() # --- Nouveaux probes "radio / link" --- def _wifi_radio_on(self) -> bool: # nmcli (NetworkManager) try: out = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True, timeout=1) if out.returncode == 0: return out.stdout.strip().lower().startswith("enabled") except Exception: pass # rfkill (fallback) try: out = subprocess.run(["rfkill", "list"], capture_output=True, text=True, timeout=1) if out.returncode == 0: block = out.stdout.lower() if "wireless" in block or "wlan" in block or "wifi" in block: if "soft blocked: yes" in block or "hard blocked: yes" in block: return False return True except Exception: pass return False def _bt_radio_on(self) -> bool: import shutil, os as _os btctl = shutil.which("bluetoothctl") or "/usr/bin/bluetoothctl" env = _os.environ.copy() env.setdefault("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") # important quand on tourne en service systemd env.setdefault("DBUS_SYSTEM_BUS_ADDRESS", "unix:path=/run/dbus/system_bus_socket") try: out = subprocess.run([btctl, "show"], capture_output=True, text=True, timeout=1.2, env=env) if out.returncode == 0: txt = (out.stdout or "").lower() if "no default controller available" in txt: # Essayer de lister et cibler le premier contrôleur ls = subprocess.run([btctl, "list"], capture_output=True, text=True, timeout=1.2, env=env) if ls.returncode == 0: for line in (ls.stdout or "").splitlines(): # ex: "Controller AA:BB:CC:DD:EE:FF host [default]" if "controller " in line.lower(): mac = line.split()[1] sh = subprocess.run([btctl, "-a", mac, "show"], capture_output=True, text=True, timeout=1.2, env=env) if sh.returncode == 0 and "powered: yes" in (sh.stdout or "").lower(): return True return False # cas normal if "powered: yes" in txt: return True except Exception: pass # Fallback rfkill try: out = subprocess.run(["rfkill", "list"], capture_output=True, text=True, timeout=1.0, env=env) if out.returncode == 0: block = (out.stdout or "").lower() if "bluetooth" in block: if "soft blocked: yes" in block or "hard blocked: yes" in block: return False return True except Exception: pass return False def _eth_link_up(self, ifname: str = "eth0") -> bool: # ip link show eth0 try: out = subprocess.run(["ip", "link", "show", ifname], capture_output=True, text=True, timeout=1) if out.returncode == 0: # "state UP" ou "LOWER_UP" t = out.stdout.upper() return ("STATE UP" in t) or ("LOWER_UP" in t) except Exception: pass # ethtool (si dispo) try: out = subprocess.run(["ethtool", ifname], capture_output=True, text=True, timeout=1) if out.returncode == 0: for line in out.stdout.splitlines(): if line.strip().lower().startswith("link detected:"): return line.split(":",1)[1].strip().lower() == "yes" except Exception: pass return False def _usb_gadget_active(self) -> bool: # actif si un UDC est attaché try: udc = self._read_text("/sys/kernel/config/usb_gadget/g1/UDC") return bool(udc and udc.strip()) except Exception: return False