""" Shared helpers for building raw HTTP/2 frames. All PoCs operate below any HTTP/2 library to send intentionally malformed or adversarial frame sequences. """ import struct import socket import ssl as _ssl # ── H2 constants ────────────────────────────────────────────── H2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" # Frame types DATA = 0x00 HEADERS = 0x01 PRIORITY = 0x02 RST_STREAM = 0x03 SETTINGS = 0x04 PUSH_PROMISE = 0x05 PING = 0x06 GOAWAY = 0x07 WINDOW_UPDATE = 0x08 CONTINUATION = 0x09 # Flags FLAG_ACK = 0x01 FLAG_END_STREAM = 0x01 FLAG_END_HEADERS = 0x04 FLAG_PADDED = 0x08 FLAG_PRIORITY = 0x20 # Settings IDs SETTINGS_HEADER_TABLE_SIZE = 0x01 SETTINGS_ENABLE_PUSH = 0x02 SETTINGS_MAX_CONCURRENT_STREAMS = 0x03 SETTINGS_INITIAL_WINDOW_SIZE = 0x04 SETTINGS_MAX_FRAME_SIZE = 0x05 # ── frame builders ──────────────────────────────────────────── def frame(ftype: int, flags: int, stream_id: int, payload: bytes) -> bytes: """Build a raw 9-byte-header + payload H2 frame.""" length = len(payload) header = struct.pack("!I", length)[1:] # 3-byte length header += struct.pack("!BB", ftype, flags) # type + flags header += struct.pack("!I", stream_id & 0x7FFFFFFF) # stream id return header + payload def settings_frame(pairs: list[tuple[int,int]] = None, ack=False) -> bytes: """SETTINGS frame. pairs = [(id, value), ...]""" flags = FLAG_ACK if ack else 0 payload = b"" if pairs and not ack: for sid, val in pairs: payload += struct.pack("!HI", sid, val) return frame(SETTINGS, flags, 0, payload) def window_update_frame(stream_id: int, increment: int) -> bytes: """WINDOW_UPDATE frame.""" payload = struct.pack("!I", increment & 0x7FFFFFFF) return frame(WINDOW_UPDATE, 0, stream_id, payload) def ping_frame(payload: bytes = b"AAAAAAAA", ack=False) -> bytes: flags = FLAG_ACK if ack else 0 return frame(PING, flags, 0, payload[:8].ljust(8, b"\x00")) def rst_stream_frame(stream_id: int, error_code: int = 0) -> bytes: return frame(RST_STREAM, 0, stream_id, struct.pack("!I", error_code)) def data_frame(stream_id: int, payload: bytes, flags: int = 0) -> bytes: return frame(DATA, flags, stream_id, payload) def goaway_frame(last_stream_id: int, error_code: int = 0, debug: bytes = b"") -> bytes: payload = struct.pack("!II", last_stream_id, error_code) + debug return frame(GOAWAY, 0, 0, payload) def headers_frame(stream_id: int, hpack_payload: bytes, flags: int = FLAG_END_HEADERS) -> bytes: return frame(HEADERS, flags, stream_id, hpack_payload) # ── Minimal HPACK for GET / ────────────────────────────────── def hpack_get_root() -> bytes: """ HPACK-encoded pseudo-headers for GET / HTTP/2 over h2c. Static table indices: :method GET → 0x82 (idx 2, fully indexed) :path / → 0x84 (idx 4, fully indexed) :scheme http → 0x86 (idx 6, fully indexed) :authority localhost → literal, idx 1 """ # :method GET out = b"\x82" # :path / out += b"\x84" # :scheme http out += b"\x86" # :authority localhost (literal with indexing, name index 1) authority = b"localhost" out += b"\x41" # literal indexed, name idx 1 out += bytes([len(authority)]) + authority # value return out # ── connection helpers ──────────────────────────────────────── def h2_connect(host: str, port: int, use_tls: bool = False, sni: str = None) -> socket.socket: """ Open a TCP (or TLS) socket and complete the H2 connection preface (client preface + empty SETTINGS). Returns the connected socket ready for sending frames. """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((host, port)) if use_tls: ctx = _ssl.SSLContext(_ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = _ssl.CERT_NONE ctx.set_alpn_protocols(["h2"]) sock = ctx.wrap_socket(sock, server_hostname=sni or host) # Send client connection preface sock.sendall(H2_PREFACE) # Send our SETTINGS (empty) sock.sendall(settings_frame()) # Read server preface (SETTINGS + WINDOW_UPDATE usually) # We don't fully parse — just drain enough to get going try: _initial = sock.recv(4096) except socket.timeout: pass # ACK the server's SETTINGS sock.sendall(settings_frame(ack=True)) return sock def recv_frames(sock: socket.socket, timeout: float = 2.0) -> bytes: """Best-effort non-blocking receive.""" sock.settimeout(timeout) buf = b"" try: while True: chunk = sock.recv(65536) if not chunk: break buf += chunk except (socket.timeout, ConnectionError, OSError): pass return buf def parse_frames(data: bytes): """Yield (type, flags, stream_id, payload) from raw bytes.""" off = 0 while off + 9 <= len(data): length = (data[off] << 16) | (data[off+1] << 8) | data[off+2] ftype = data[off+3] flags = data[off+4] sid = struct.unpack("!I", data[off+5:off+9])[0] & 0x7FFFFFFF off += 9 if off + length > len(data): break payload = data[off:off+length] off += length yield (ftype, flags, sid, payload)