114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
"""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())
|