"""SQLite-Datenbank (thread-sicher über ein Lock).""" import os import sqlite3 import threading import time from . import config SCHEMA = """ CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL COLLATE NOCASE, password_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, policy TEXT NOT NULL DEFAULT 'inherit', comment TEXT NOT NULL DEFAULT '', created_at REAL NOT NULL, last_seen REAL ); CREATE TABLE IF NOT EXISTS rules( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, action TEXT NOT NULL CHECK(action IN ('allow','deny')), host TEXT NOT NULL DEFAULT '', ports TEXT NOT NULL DEFAULT '', comment TEXT NOT NULL DEFAULT '', enabled INTEGER NOT NULL DEFAULT 1, created_at REAL NOT NULL ); CREATE INDEX IF NOT EXISTS idx_rules_user ON rules(user_id); CREATE TABLE IF NOT EXISTS forwards( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, protocol TEXT NOT NULL CHECK(protocol IN ('tcp','udp')), listen_port INTEGER NOT NULL, target_host TEXT NOT NULL, target_port INTEGER NOT NULL, allowed_sources TEXT NOT NULL DEFAULT '', enabled INTEGER NOT NULL DEFAULT 1, comment TEXT NOT NULL DEFAULT '', created_at REAL NOT NULL, UNIQUE(protocol, listen_port) ); CREATE TABLE IF NOT EXISTS history( id INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, username TEXT, client_ip TEXT, proto TEXT, method TEXT, host TEXT, port INTEGER, url TEXT, status TEXT, detail TEXT, bytes_up INTEGER NOT NULL DEFAULT 0, bytes_down INTEGER NOT NULL DEFAULT 0, duration REAL NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_history_ts ON history(ts); CREATE INDEX IF NOT EXISTS idx_history_user ON history(username, ts); CREATE INDEX IF NOT EXISTS idx_history_host ON history(host); CREATE TABLE IF NOT EXISTS tokens( id INTEGER PRIMARY KEY AUTOINCREMENT, token_hash TEXT UNIQUE NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT '', kind TEXT NOT NULL, created_at REAL NOT NULL, expires_at REAL, last_used REAL ); CREATE TABLE IF NOT EXISTS settings( key TEXT PRIMARY KEY, value TEXT NOT NULL ); """ DEFAULT_SETTINGS = { "default_policy": "allow", # allow = Blacklist-Modus, deny = Whitelist-Modus "log_enabled": "1", "log_retention_days": "90", "anonymous_networks": "", # CIDRs, die ohne Login surfen dürfen "block_loopback": "1", # Zugriff auf 127.0.0.0/8 / ::1 immer sperren "auth_realm": "Proxy", "public_host": "", # Hostname/IP für PAC-Datei } class Database: def __init__(self, path: str): os.makedirs(os.path.dirname(path), exist_ok=True) self.lock = threading.RLock() self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None) self.conn.row_factory = sqlite3.Row self.conn.execute("PRAGMA journal_mode=WAL") self.conn.execute("PRAGMA synchronous=NORMAL") self.conn.execute("PRAGMA foreign_keys=ON") with self.lock: self.conn.executescript(SCHEMA) for k, v in DEFAULT_SETTINGS.items(): self.conn.execute("INSERT OR IGNORE INTO settings(key,value) VALUES(?,?)", (k, v)) def query(self, sql: str, params=()) -> list[dict]: with self.lock: return [dict(r) for r in self.conn.execute(sql, params).fetchall()] def one(self, sql: str, params=()) -> dict | None: with self.lock: r = self.conn.execute(sql, params).fetchone() return dict(r) if r else None def execute(self, sql: str, params=()) -> sqlite3.Cursor: with self.lock: return self.conn.execute(sql, params) def executemany(self, sql: str, rows) -> None: with self.lock: self.conn.execute("BEGIN") try: self.conn.executemany(sql, rows) self.conn.execute("COMMIT") except Exception: self.conn.execute("ROLLBACK") raise # Einstellungen def settings(self) -> dict: return {r["key"]: r["value"] for r in self.query("SELECT key, value FROM settings")} def set_setting(self, key: str, value: str) -> None: self.execute("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, value)) db = Database(config.DB_PATH) def now() -> float: return time.time()