first commit

This commit is contained in:
duffyduck
2026-10-05 13:33:59 +02:00
commit e0b4eb3fe5
18 changed files with 3166 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
"""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())