#!/usr/bin/env python3 import argparse import socket import ssl import sys import h2.connection import h2.config import h2.events def make_authority(host: str, port: int) -> str: if port == 443: return host return f"{host}:{port}" def run_probe( host: str, port: int, path: str, regular_header_count: int, insecure: bool, ) -> None: context = ssl.create_default_context() context.set_alpn_protocols(["h2"]) if insecure: context.check_hostname = False context.verify_mode = ssl.CERT_NONE configuration = h2.config.H2Configuration( client_side=True, header_encoding="utf-8", ) connection = h2.connection.H2Connection(config=configuration) with socket.create_connection((host, port), timeout=5) as tcp_socket: with context.wrap_socket( tcp_socket, server_hostname=host, ) as tls_socket: negotiated_protocol = tls_socket.selected_alpn_protocol() if negotiated_protocol != "h2": raise RuntimeError( f"The server negotiated {negotiated_protocol!r}, " "not HTTP/2 ('h2')." ) print(f"[+] Negotiated ALPN protocol: {negotiated_protocol}") connection.initiate_connection() tls_socket.sendall(connection.data_to_send()) stream_id = connection.get_next_available_stream_id() request_headers = [ (":method", "GET"), (":scheme", "https"), (":authority", make_authority(host, port)), (":path", path), ] request_headers.extend( (f"x-fooooooo-{index:04d}", "a") for index in range(regular_header_count) ) print( f"[+] Sending one stream with " f"{regular_header_count} regular headers and " f"4 pseudo-headers" ) connection.send_headers( stream_id=stream_id, headers=request_headers, end_stream=True, ) tls_socket.sendall(connection.data_to_send()) tls_socket.settimeout(5) stream_finished = False while not stream_finished: try: received = tls_socket.recv(65535) except socket.timeout: print("[-] Timed out waiting for a response.") break if not received: print("[-] The peer closed the TLS connection.") break events = connection.receive_data(received) for event in events: if isinstance(event, h2.events.ResponseReceived): print("[+] Response headers:") for name, value in event.headers: print(f" {name}: {value}") elif isinstance(event, h2.events.DataReceived): print( f"[+] Received {len(event.data)} response bytes" ) connection.acknowledge_received_data( event.flow_controlled_length, event.stream_id, ) elif isinstance(event, h2.events.StreamEnded): print(f"[+] Stream {event.stream_id} ended normally.") stream_finished = True elif isinstance(event, h2.events.StreamReset): print( f"[+] Stream {event.stream_id} was reset; " f"error code={event.error_code}" ) stream_finished = True elif isinstance(event, h2.events.ConnectionTerminated): print( "[+] HTTP/2 connection terminated; " f"error code={event.error_code}, " f"last stream={event.last_stream_id}" ) stream_finished = True pending_data = connection.data_to_send() if pending_data: tls_socket.sendall(pending_data) def main() -> int: parser = argparse.ArgumentParser( description="Send one bounded HTTP/2 header-count probe." ) parser.add_argument("host", help="Laboratory HTTP/2 server hostname or IP") parser.add_argument("--port", type=int, default=443) parser.add_argument("--path", default="/") parser.add_argument( "--headers", type=int, default=20, help="Number of regular test headers.", ) parser.add_argument( "--insecure", action="store_true", help="Disable certificate verification for a self-signed lab server", ) args = parser.parse_args() try: run_probe( host=args.host, port=args.port, path=args.path, regular_header_count=args.headers, insecure=args.insecure, ) except (OSError, ssl.SSLError, RuntimeError, ValueError) as exc: print(f"[!] Error: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())