Files
2026-10-05 13:33:59 +02:00

114 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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<?", (time.time() - days * 86400,)).rowcount
if n:
log.info("Verlauf bereinigt: %d Einträge älter als %d Tage gelöscht", n, days)
db.execute("DELETE FROM tokens WHERE expires_at IS NOT NULL AND expires_at<?", (time.time(),))
except Exception: # noqa: BLE001
log.exception("Wartungsfehler")
async def main() -> 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())