#!/usr/bin/env python3
"""
HTTPS test server for the PreviewTab Sec-Fetch-Site / LNA bypass POC.

Serves:
  /poc.html        -> the attacker page (load via https://localhost:8443/poc.html)
  /victim          -> the cross-site "private network" target; logs request headers
  /gen/...         -> auto-generated MojoJS bindings from the Chromium build dir

The /victim handler prints the Sec-Fetch-* headers it receives.  When the
request is triggered via SpeculationHost.InitiatePreview the header is
"Sec-Fetch-Site: none" (browser-initiated), proving the renderer-supplied URL
was laundered into a browser-initiated navigation.

A control request via window.open() to the same /victim URL shows
"Sec-Fetch-Site: cross-site", which is the correct value.
"""

import http.server
import os
import ssl
import subprocess
import sys
import threading

HERE = os.path.dirname(os.path.abspath(__file__))
PORT = 8443

# Locate the Chromium gen/ directory so we can serve the MojoJS bindings.
# Override with env CHROMIUM_GEN_DIR if your build dir differs.
GEN_DIR = os.environ.get(
    "CHROMIUM_GEN_DIR",
    "/chromium/chromium/src/out/asan/gen",
)

CERT_FILE = os.path.join(HERE, "server.pem")

LOG_FILE = os.path.join(HERE, "request-log.txt")
LOG_LOCK = threading.Lock()


def ensure_cert():
    if os.path.exists(CERT_FILE):
        return
    subprocess.check_call([
        "openssl", "req", "-x509", "-newkey", "rsa:2048",
        "-keyout", CERT_FILE, "-out", CERT_FILE,
        "-days", "7", "-nodes",
        "-subj", "/CN=localhost",
        "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1",
    ])


class Handler(http.server.SimpleHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def _send_bytes(self, body, content_type="text/html; charset=utf-8"):
        self.send_response(200)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        path = self.path.split("?", 1)[0]

        if path.startswith("/victim"):
            sfs = self.headers.get("Sec-Fetch-Site", "<missing>")
            sfm = self.headers.get("Sec-Fetch-Mode", "<missing>")
            sfd = self.headers.get("Sec-Fetch-Dest", "<missing>")
            spp = self.headers.get("Sec-Purpose", "<missing>")
            host = self.headers.get("Host", "<missing>")
            tag = "?"
            if "via=preview" in self.path:
                tag = "InitiatePreview (vulnerable path)"
            elif "via=open" in self.path:
                tag = "window.open (control)"
            lines = [
                "=" * 72,
                f"[REQUEST] {self.command} {self.path}",
                f"  trigger:        {tag}",
                f"  Host:           {host}",
                f"  Sec-Fetch-Site: {sfs}",
                f"  Sec-Fetch-Mode: {sfm}",
                f"  Sec-Fetch-Dest: {sfd}",
                f"  Sec-Purpose:    {spp}",
            ]
            if sfs == "none" and "via=preview" in self.path:
                lines.append("  >>> BUG CONFIRMED: renderer-chosen URL fetched with "
                             "Sec-Fetch-Site: none (browser-initiated)")
            msg = "\n".join(lines) + "\n"
            with LOG_LOCK:
                sys.stderr.write(msg)
                sys.stderr.flush()
                with open(LOG_FILE, "a") as f:
                    f.write(msg)
            self._send_bytes(b"<!doctype html><title>victim</title>ok")
            return

        if path == "/" or path == "/poc.html":
            with open(os.path.join(HERE, "poc.html"), "rb") as f:
                self._send_bytes(f.read())
            return

        if path.startswith("/gen/"):
            rel = path[len("/gen/"):]
            full = os.path.normpath(os.path.join(GEN_DIR, rel))
            if not full.startswith(os.path.normpath(GEN_DIR)):
                self.send_error(403)
                return
            if not os.path.isfile(full):
                self.send_error(404)
                return
            with open(full, "rb") as f:
                self._send_bytes(f.read(), "text/javascript")
            return

        self.send_error(404)

    def log_message(self, fmt, *args):
        # Keep stdout quiet except for our explicit logging above.
        pass


def main():
    ensure_cert()
    if os.path.exists(LOG_FILE):
        os.remove(LOG_FILE)
    httpd = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(CERT_FILE)
    httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
    sys.stderr.write(
        f"[*] HTTPS server on https://127.0.0.1:{PORT}/ "
        f"(also reachable as https://localhost:{PORT}/)\n"
        f"[*] Load https://localhost:{PORT}/poc.html in Chrome.\n"
        f"[*] Logging /victim requests to {LOG_FILE}\n"
    )
    httpd.serve_forever()


if __name__ == "__main__":
    main()
