142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
"""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()
|