Dink0 commited on
Commit
31bd2b5
·
verified ·
1 Parent(s): 0c8eb15

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +8 -0
  2. receiver.py +90 -0
Dockerfile ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ RUN useradd --create-home --uid 1000 appuser
3
+ WORKDIR /app
4
+ COPY --chown=appuser:appuser receiver.py /app/receiver.py
5
+ USER appuser
6
+ ENV PYTHONUNBUFFERED=1
7
+ EXPOSE 7860
8
+ CMD ["python", "receiver.py"]
receiver.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Temporary webhook payload inspector. No Telegram/LLM calls are made."""
2
+ import json
3
+ import os
4
+ import secrets
5
+ import sqlite3
6
+ from datetime import datetime, timezone
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from pathlib import Path
9
+ from urllib.parse import urlsplit
10
+
11
+ MAX_BYTES = 1024 * 1024
12
+
13
+
14
+ def make_server(host="0.0.0.0", port=7860, *, secret, db_path):
15
+ if len(secret) < 24 or not secret.isascii() or not all(c.isalnum() or c in "-_" for c in secret):
16
+ raise ValueError("WEBHOOK_SECRET must contain at least 24 ASCII letters, digits, '-' or '_'")
17
+ path = Path(db_path)
18
+ path.parent.mkdir(parents=True, exist_ok=True)
19
+ with sqlite3.connect(path) as db:
20
+ db.execute("CREATE TABLE IF NOT EXISTS samples (id INTEGER PRIMARY KEY, received_at TEXT, content_type TEXT, body TEXT)")
21
+
22
+ class Handler(BaseHTTPRequestHandler):
23
+ def log_message(self, *_args):
24
+ pass # Request paths contain the secret. Never log them.
25
+
26
+ def reply(self, code, payload):
27
+ raw = json.dumps(payload, ensure_ascii=False, indent=2).encode()
28
+ self.send_response(code)
29
+ self.send_header("Content-Type", "application/json; charset=utf-8")
30
+ self.send_header("Content-Length", str(len(raw)))
31
+ self.send_header("Cache-Control", "no-store")
32
+ self.send_header("X-Content-Type-Options", "nosniff")
33
+ self.end_headers()
34
+ self.wfile.write(raw)
35
+
36
+ def authorized(self, prefix):
37
+ return secrets.compare_digest(urlsplit(self.path).path.encode("utf-8"), (prefix + secret).encode("utf-8"))
38
+
39
+ def do_GET(self):
40
+ if urlsplit(self.path).path in ("/", "/health"):
41
+ return self.reply(200, {"status": "receiver_ready", "mode": "payload_inspection_only"})
42
+ if not self.authorized("/samples/"):
43
+ return self.reply(404, {"error": "not_found"})
44
+ with sqlite3.connect(path, timeout=10) as db:
45
+ rows = db.execute("SELECT id, received_at, content_type, body FROM samples ORDER BY id DESC LIMIT 3").fetchall()
46
+ result = []
47
+ for sample_id, received_at, content_type, body in rows:
48
+ try:
49
+ payload = json.loads(body)
50
+ except ValueError:
51
+ payload = body
52
+ result.append({"id": sample_id, "received_at": received_at, "content_type": content_type, "payload": payload})
53
+ return self.reply(200, {"count": len(result), "samples": result})
54
+
55
+ def do_POST(self):
56
+ if not self.authorized("/medialogia/"):
57
+ return self.reply(404, {"error": "not_found"})
58
+ try:
59
+ size = int(self.headers["Content-Length"])
60
+ except (ValueError, TypeError):
61
+ return self.reply(411, {"error": "content_length_required"})
62
+ if size < 0 or size > MAX_BYTES:
63
+ return self.reply(413, {"error": "payload_too_large"})
64
+ self.connection.settimeout(20)
65
+ try:
66
+ raw = self.rfile.read(size)
67
+ except (OSError, TimeoutError):
68
+ return self.reply(408, {"error": "request_timeout"})
69
+ if len(raw) != size:
70
+ return self.reply(400, {"error": "incomplete_body"})
71
+ try:
72
+ body = raw.decode("utf-8")
73
+ except UnicodeDecodeError:
74
+ return self.reply(400, {"error": "expected_utf8"})
75
+ try:
76
+ with sqlite3.connect(path, timeout=10) as db:
77
+ db.execute("INSERT INTO samples(received_at,content_type,body) VALUES(?,?,?)", (datetime.now(timezone.utc).isoformat(), self.headers.get("Content-Type", ""), body))
78
+ db.execute("DELETE FROM samples WHERE id NOT IN (SELECT id FROM samples ORDER BY id DESC LIMIT 20)")
79
+ except sqlite3.Error:
80
+ return self.reply(503, {"error": "storage_unavailable"})
81
+ print("Webhook sample saved", flush=True)
82
+ return self.reply(200, {"ok": True, "mode": "payload_inspection_only"})
83
+
84
+ return ThreadingHTTPServer((host, port), Handler)
85
+
86
+
87
+ if __name__ == "__main__":
88
+ server = make_server(port=int(os.getenv("PORT", "7860")), secret=os.getenv("WEBHOOK_SECRET", ""), db_path=os.getenv("PROBE_DB_PATH", "/tmp/webhook-probe/samples.sqlite3"))
89
+ print("Receiver ready on port 7860; inspection only; local samples are temporary", flush=True)
90
+ server.serve_forever()