#!/usr/bin/env python3
"""Tiny request-intake service. Stdlib only. Listens on loopback; nginx fronts it."""
import json, os, re, time, fcntl, html
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
QUEUE = "/home/agent/work/site/queue.jsonl"
RATE = {} # ip -> [timestamps]
MAX_BODY = 8192 # bytes
WINDOW, LIMIT = 3600, 5 # 5 submissions per IP per hour
def rate_ok(ip: str) -> bool:
now = time.time()
hits = [t for t in RATE.get(ip, []) if now - t < WINDOW]
RATE[ip] = hits
if len(hits) >= LIMIT:
return False
hits.append(now)
return True
def append(rec: dict) -> None:
with open(QUEUE, "a") as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
fh.write(json.dumps(rec) + "\n")
fcntl.flock(fh, fcntl.LOCK_UN)
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
server_version = "aaw"
def _send(self, code: int, body: bytes, ctype="application/json"):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): # keep nginx as the access log
pass
def do_POST(self):
if self.path != "/api/request":
return self._send(404, b'{"error":"not found"}')
# real client IP comes from nginx
ip = self.headers.get("X-Real-IP", self.client_address[0])
if not rate_ok(ip):
return self._send(429, b'{"error":"rate limited - try again later"}')
try:
n = int(self.headers.get("Content-Length", 0))
except ValueError:
return self._send(400, b'{"error":"bad length"}')
if n <= 0 or n > MAX_BODY:
return self._send(413, b'{"error":"too large"}')
try:
data = json.loads(self.rfile.read(n))
task = str(data.get("task", "")).strip()
contact = str(data.get("contact", "")).strip()
except Exception:
return self._send(400, b'{"error":"bad json"}')
if not (10 <= len(task) <= 4000):
return self._send(400, b'{"error":"task must be 10-4000 chars"}')
if not (3 <= len(contact) <= 200):
return self._send(400, b'{"error":"contact required"}')
append({
"ts": int(time.time()),
"iso": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"ip": ip,
"ua": self.headers.get("User-Agent", "")[:200],
"contact": html.escape(contact)[:200],
"task": task[:4000],
"status": "new",
})
return self._send(200, b'{"ok":true}')
if __name__ == "__main__":
os.makedirs(os.path.dirname(QUEUE), exist_ok=True)
ThreadingHTTPServer(("127.0.0.1", 8080), H).serve_forever()