"""Einstiegspunkt: startet HTTP-/SOCKS-Proxy, Weiterleitungen und WebUI in einer Event-Loop.""" import asyncio import logging import secrets import socket import time import uvicorn from . import config from .api import app from .auth import hash_password, users from .core import history, log, peer_ip from .db import db from .forwarder import forwards from .policy import policy from .proxy_http import handle_http from .proxy_socks import handle_socks5 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") def bootstrap() -> None: if db.one("SELECT COUNT(*) n FROM users")["n"] == 0: password = config.ADMIN_PASSWORD or secrets.token_urlsafe(12) db.execute("INSERT INTO users(username,password_hash,is_admin,enabled,policy,comment,created_at) " "VALUES(?,?,1,1,'inherit','Initialer Administrator',?)", (config.ADMIN_USER, hash_password(password), time.time())) log.warning("=" * 60) log.warning("Admin-Benutzer angelegt: %s", config.ADMIN_USER) if not config.ADMIN_PASSWORD: log.warning("Generiertes Passwort: %s", password) log.warning("=" * 60) users.reload() policy.reload() async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: """Erkennt anhand des ersten Bytes, ob SOCKS5 oder HTTP gesprochen wird – beide Proxy-Ports verstehen daher beide Protokolle.""" client_ip = peer_ip(writer) sock = writer.get_extra_info("socket") if sock is not None: try: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) except OSError: pass try: first = await asyncio.wait_for(reader.readexactly(1), timeout=60) if first == b"\x05": await handle_socks5(reader, writer, client_ip) elif first == b"\x04": pass # SOCKS4 wird nicht unterstützt else: await handle_http(reader, writer, first, client_ip) except (asyncio.IncompleteReadError, asyncio.TimeoutError, ConnectionError, OSError): pass except Exception: # noqa: BLE001 log.exception("Fehler bei Verbindung von %s", client_ip) finally: try: writer.close() except Exception: # noqa: BLE001 pass async def maintenance() -> None: last_cleanup = 0.0 while True: await asyncio.sleep(1) try: await asyncio.to_thread(history.flush) if time.time() - last_cleanup > 3600: last_cleanup = time.time() users.flush_last_seen() days = int(policy.settings.get("log_retention_days", "90") or 0) if days > 0: n = db.execute("DELETE FROM history WHERE ts None: bootstrap() servers = [] ports = dict.fromkeys([config.HTTP_PORT, config.SOCKS_PORT]) for port in ports: srv = await asyncio.start_server(handle_client, host=config.LISTEN_HOST, port=port, limit=256 * 1024, backlog=1024, reuse_address=True) servers.append(srv) log.info("Proxy lauscht auf %s:%d (HTTP + SOCKS5)", config.LISTEN_HOST, port) await forwards.reload() web = uvicorn.Server(uvicorn.Config(app, host=config.LISTEN_HOST, port=config.WEB_PORT, log_level="warning", proxy_headers=True, forwarded_allow_ips="*")) log.info("WebUI/API auf http://%s:%d (API-Doku: /api/docs)", config.LISTEN_HOST, config.WEB_PORT) background = [asyncio.create_task(c) for c in (maintenance(), forwards.housekeeping(), *(s.serve_forever() for s in servers))] try: await web.serve() # beendet sich bei SIGTERM/SIGINT finally: for t in background: t.cancel() history.flush() users.flush_last_seen() if __name__ == "__main__": asyncio.run(main())