#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ # Native, dependency-free HTTP/2 client (RFC 9113) with HPACK (RFC 7541). Runtime code uses only # the standard library. The accompanying tests optionally use python-hyper/hpack and hyper-h2 for # differential and local peer validation. The implementation keeps Python 2.7-compatible syntax, # but this rewrite was executed and tested on Python 3.13 only. import base64 import socket import ssl import struct import threading from collections import OrderedDict try: from http.client import responses as _HTTP_RESPONSES except ImportError: from httplib import responses as _HTTP_RESPONSES try: from urllib.parse import quote, urljoin, urlsplit except ImportError: from urllib import quote from urlparse import urljoin, urlsplit from email.message import Message as _Message REDIRECT_CODES = (301, 302, 303, 307, 308) HUFFMAN_CODES = [ 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee, 0x3fffffff ] HUFFMAN_LENGTHS = [ 0xd, 0x17, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x18, 0x1e, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x6, 0xa, 0xa, 0xc, 0xd, 0x6, 0x8, 0xb, 0xa, 0xa, 0x8, 0xb, 0x8, 0x6, 0x6, 0x6, 0x5, 0x5, 0x5, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x7, 0x8, 0xf, 0x6, 0xc, 0xa, 0xd, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x8, 0x7, 0x8, 0xd, 0x13, 0xd, 0xe, 0x6, 0xf, 0x5, 0x6, 0x5, 0x6, 0x5, 0x6, 0x6, 0x6, 0x5, 0x7, 0x7, 0x6, 0x6, 0x6, 0x5, 0x6, 0x7, 0x6, 0x5, 0x5, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0xf, 0xb, 0xe, 0xd, 0x1c, 0x14, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18, 0x17, 0x18, 0x18, 0x16, 0x17, 0x18, 0x17, 0x17, 0x17, 0x17, 0x15, 0x16, 0x17, 0x16, 0x17, 0x17, 0x18, 0x16, 0x15, 0x14, 0x16, 0x16, 0x17, 0x17, 0x15, 0x17, 0x16, 0x16, 0x18, 0x15, 0x16, 0x17, 0x17, 0x15, 0x15, 0x16, 0x15, 0x17, 0x16, 0x17, 0x17, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x16, 0x17, 0x1a, 0x1a, 0x14, 0x13, 0x16, 0x17, 0x16, 0x19, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1a, 0x18, 0x19, 0x13, 0x15, 0x1a, 0x1b, 0x1b, 0x1a, 0x1b, 0x18, 0x15, 0x15, 0x1a, 0x1a, 0x1c, 0x1b, 0x1b, 0x1b, 0x14, 0x18, 0x14, 0x15, 0x16, 0x15, 0x15, 0x17, 0x16, 0x16, 0x19, 0x19, 0x18, 0x18, 0x1a, 0x17, 0x1a, 0x1b, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1c, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1a, 0x1e ] STATIC_TABLE = ( (b':authority', b''), (b':method', b'GET'), (b':method', b'POST'), (b':path', b'/'), (b':path', b'/index.html'), (b':scheme', b'http'), (b':scheme', b'https'), (b':status', b'200'), (b':status', b'204'), (b':status', b'206'), (b':status', b'304'), (b':status', b'400'), (b':status', b'404'), (b':status', b'500'), (b'accept-charset', b''), (b'accept-encoding', b'gzip, deflate'), (b'accept-language', b''), (b'accept-ranges', b''), (b'accept', b''), (b'access-control-allow-origin', b''), (b'age', b''), (b'allow', b''), (b'authorization', b''), (b'cache-control', b''), (b'content-disposition', b''), (b'content-encoding', b''), (b'content-language', b''), (b'content-length', b''), (b'content-location', b''), (b'content-range', b''), (b'content-type', b''), (b'cookie', b''), (b'date', b''), (b'etag', b''), (b'expect', b''), (b'expires', b''), (b'from', b''), (b'host', b''), (b'if-match', b''), (b'if-modified-since', b''), (b'if-none-match', b''), (b'if-range', b''), (b'if-unmodified-since', b''), (b'last-modified', b''), (b'link', b''), (b'location', b''), (b'max-forwards', b''), (b'proxy-authenticate', b''), (b'proxy-authorization', b''), (b'range', b''), (b'referer', b''), (b'refresh', b''), (b'retry-after', b''), (b'server', b''), (b'set-cookie', b''), (b'strict-transport-security', b''), (b'transfer-encoding', b''), (b'user-agent', b''), (b'vary', b''), (b'via', b''), (b'www-authenticate', b''), ) STATIC_LEN = len(STATIC_TABLE) # HTTP/2 frame codec and client implementation (RFC 9113; HPACK remains RFC 7541). # The public API is compatible with the original module. Optional `verify` and # `ssl_context` arguments were added so TLS verification is opt-in. By default # verification is disabled (the tool is used against test/self-signed targets). try: text_type = unicode except NameError: text_type = str try: integer_types = (int, long) except NameError: integer_types = (int,) # Frame types DATA = 0x0 HEADERS = 0x1 PRIORITY = 0x2 RST_STREAM = 0x3 SETTINGS = 0x4 PUSH_PROMISE = 0x5 PING = 0x6 GOAWAY = 0x7 WINDOW_UPDATE = 0x8 CONTINUATION = 0x9 # Flags FLAG_END_STREAM = 0x1 FLAG_ACK = 0x1 FLAG_END_HEADERS = 0x4 FLAG_PADDED = 0x8 FLAG_PRIORITY = 0x20 # Settings SETTINGS_HEADER_TABLE_SIZE = 0x1 SETTINGS_ENABLE_PUSH = 0x2 SETTINGS_MAX_CONCURRENT_STREAMS = 0x3 SETTINGS_INITIAL_WINDOW_SIZE = 0x4 SETTINGS_MAX_FRAME_SIZE = 0x5 SETTINGS_MAX_HEADER_LIST_SIZE = 0x6 # Error codes NO_ERROR = 0x0 PROTOCOL_ERROR = 0x1 INTERNAL_ERROR = 0x2 FLOW_CONTROL_ERROR = 0x3 SETTINGS_TIMEOUT = 0x4 STREAM_CLOSED = 0x5 FRAME_SIZE_ERROR = 0x6 REFUSED_STREAM = 0x7 CANCEL = 0x8 COMPRESSION_ERROR = 0x9 CONNECT_ERROR = 0xa ENHANCE_YOUR_CALM = 0xb INADEQUATE_SECURITY = 0xc HTTP_1_1_REQUIRED = 0xd CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" DEFAULT_MAX_FRAME_SIZE = 16384 MAX_FRAME_SIZE = 0xffffff MAX_WINDOW = (1 << 31) - 1 DEFAULT_WINDOW = 65535 LOCAL_INITIAL_WINDOW = 16 * 1024 * 1024 MAX_RESPONSE_SIZE = 100 * 1024 * 1024 MAX_HEADER_BLOCK_SIZE = 1024 * 1024 MAX_HEADER_LIST_SIZE = 1024 * 1024 MAX_HPACK_INTEGER = MAX_WINDOW MAX_STREAM_ID = (1 << 31) - 1 H2_POOL_LIMIT = 32 MAX_INFORMATIONAL_RESPONSES = 32 MAX_INFORMATIONAL_HEADER_BYTES = 1024 * 1024 MAX_RESPONSE_FRAMES = 100000 _TOKEN_BYTES = frozenset(bytearray(b"!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyz")) _METHOD_TOKEN_BYTES = frozenset(bytearray( b"!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" )) _CONNECTION_FIELDS = frozenset(( b"connection", b"proxy-connection", b"keep-alive", b"transfer-encoding", b"upgrade" )) _SENSITIVE_REDIRECT_FIELDS = frozenset((b"authorization", b"cookie", b"proxy-authorization")) _CONTENT_REDIRECT_FIELDS = frozenset((b"content-length", b"content-type", b"content-encoding")) class H2Error(IOError): pass class H2ProtocolError(H2Error): pass class H2CompressionError(H2ProtocolError): pass class H2TransportError(H2Error): pass class _UnprocessedStream(H2Error): """The peer explicitly guaranteed that the request stream was not processed.""" def _as_bytes(value): if isinstance(value, bytes): return value if isinstance(value, bytearray): return bytes(value) if isinstance(value, text_type): return value.encode("latin-1") raise TypeError("expected bytes/text value, got %s" % type(value).__name__) def encode_frame(ftype, flags, stream_id, payload=b""): """Serialize an HTTP/2 frame. >>> decode_frame_header(encode_frame(HEADERS, FLAG_END_HEADERS, 1, b'abc')[:9]) (3, 1, 4, 1) """ payload = _as_bytes(payload) if not (0 <= ftype <= 0xff and 0 <= flags <= 0xff): raise ValueError("frame type and flags must fit in one octet") if not (0 <= stream_id <= MAX_STREAM_ID): raise ValueError("stream id must be between 0 and 2^31-1") if len(payload) > MAX_FRAME_SIZE: raise ValueError("frame payload exceeds 24-bit length") header = struct.pack("!I", len(payload))[1:] header += struct.pack("!BBI", ftype, flags, stream_id) return header + payload def decode_frame_header(nine): """Parse a 9-byte frame header into (length, type, flags, stream_id). >>> decode_frame_header(encode_frame(DATA, 0, 1, b'')[:9]) (0, 0, 0, 1) """ if len(nine) != 9: raise ValueError("frame header must be exactly 9 bytes") length = struct.unpack("!I", b"\x00" + nine[:3])[0] ftype, flags, stream_id = struct.unpack("!BBI", nine[3:9]) return length, ftype, flags, stream_id & MAX_STREAM_ID # ---------- Huffman ---------- def huffman_encode(data): """Huffman-encode a byte string using the RFC 7541 static code. >>> huffman_decode(huffman_encode(b'www.example.com')) == b'www.example.com' True """ data = _as_bytes(data) if not data: return b"" acc = 0 nbits = 0 for item in bytearray(data): acc = (acc << HUFFMAN_LENGTHS[item]) | HUFFMAN_CODES[item] nbits += HUFFMAN_LENGTHS[item] pad = (8 - nbits % 8) % 8 acc = (acc << pad) | ((1 << pad) - 1) total = (nbits + pad) // 8 out = bytearray() for index in range(total - 1, -1, -1): out.append((acc >> (8 * index)) & 0xff) return bytes(out) _HUFF_ROOT = {} def _build_huffman_trie(): for sym in range(256): code = HUFFMAN_CODES[sym] length = HUFFMAN_LENGTHS[sym] node = _HUFF_ROOT for index in range(length - 1, -1, -1): bit = (code >> index) & 1 if index == 0: node[bit] = sym else: child = node.get(bit) if child is None: child = {} node[bit] = child elif not isinstance(child, dict): raise AssertionError("invalid prefix Huffman table") node = child _build_huffman_trie() def huffman_decode(data): data = _as_bytes(data) out = bytearray() node = _HUFF_ROOT consumed = 0 tail = 0 for item in bytearray(data): for index in range(7, -1, -1): bit = (item >> index) & 1 nxt = node.get(bit) if nxt is None: raise H2CompressionError("invalid Huffman sequence") consumed += 1 tail = (tail << 1) | bit if isinstance(nxt, dict): node = nxt else: out.append(nxt) node = _HUFF_ROOT consumed = 0 tail = 0 if node is not _HUFF_ROOT: if consumed > 7 or tail != (1 << consumed) - 1: raise H2CompressionError("invalid Huffman EOS padding") return bytes(out) # ---------- HPACK integer/string ---------- def encode_integer(value, prefix_bits, first_byte=0): if not isinstance(value, integer_types) or value < 0: raise ValueError("HPACK integer must be non-negative") if not 1 <= prefix_bits <= 8: raise ValueError("invalid HPACK prefix width") mask = (1 << prefix_bits) - 1 if value < mask: return bytearray([first_byte | value]) out = bytearray([first_byte | mask]) value -= mask while value >= 0x80: out.append((value & 0x7f) | 0x80) value >>= 7 out.append(value) return out def decode_integer(data, pos, prefix_bits, max_value=MAX_HPACK_INTEGER): if not 1 <= prefix_bits <= 8: raise H2CompressionError("invalid HPACK prefix width") if pos < 0 or pos >= len(data): raise H2CompressionError("truncated HPACK integer") mask = (1 << prefix_bits) - 1 value = data[pos] & mask pos += 1 if value < mask: return value, pos shift = 0 while True: if pos >= len(data): raise H2CompressionError("truncated HPACK integer") item = data[pos] pos += 1 addend = (item & 0x7f) << shift if addend > max_value - value: raise H2CompressionError("HPACK integer exceeds implementation limit") value += addend if not (item & 0x80): return value, pos shift += 7 if shift > 28: raise H2CompressionError("HPACK integer encoding is too long") def encode_string(value, huffman=True): value = _as_bytes(value) if huffman: encoded = huffman_encode(value) if len(encoded) < len(value): return encode_integer(len(encoded), 7, 0x80) + bytearray(encoded) return encode_integer(len(value), 7, 0x00) + bytearray(value) def decode_string(data, pos): if pos < 0 or pos >= len(data): raise H2CompressionError("truncated HPACK string") use_huffman = bool(data[pos] & 0x80) length, pos = decode_integer(data, pos, 7, max_value=MAX_HEADER_BLOCK_SIZE) end = pos + length if end > len(data): raise H2CompressionError("truncated HPACK string payload") raw = bytes(data[pos:end]) return (huffman_decode(raw) if use_huffman else raw), end class Decoder(object): def __init__(self, max_size=4096, max_header_list_size=MAX_HEADER_LIST_SIZE): self.max_allowed_size = max_size self.max_size = max_size self.max_header_list_size = max_header_list_size self.dynamic = [] self._size = 0 @staticmethod def _entry_size(name, value): return 32 + len(name) + len(value) def _add(self, name, value): entry_size = self._entry_size(name, value) if entry_size > self.max_size: self.dynamic = [] self._size = 0 return self.dynamic.insert(0, (name, value)) self._size += entry_size self._evict() def _evict(self): while self._size > self.max_size and self.dynamic: name, value = self.dynamic.pop() self._size -= self._entry_size(name, value) def _get(self, index): if index <= 0: raise H2CompressionError("invalid HPACK header index 0") if index <= STATIC_LEN: return STATIC_TABLE[index - 1] dynamic_index = index - STATIC_LEN - 1 if dynamic_index < 0 or dynamic_index >= len(self.dynamic): raise H2CompressionError("HPACK dynamic index out of range") return self.dynamic[dynamic_index] def decode(self, data): data = bytearray(_as_bytes(data)) pos = 0 headers = [] list_size = 0 saw_header = False while pos < len(data): first = data[pos] if first & 0x80: saw_header = True index, pos = decode_integer(data, pos, 7) name, value = self._get(index) headers.append((name, value)) elif first & 0x40: saw_header = True index, pos = decode_integer(data, pos, 6) if index: name = self._get(index)[0] else: name, pos = decode_string(data, pos) value, pos = decode_string(data, pos) self._add(name, value) headers.append((name, value)) elif first & 0x20: if saw_header: raise H2CompressionError("dynamic table size update is not at block start") new_size, pos = decode_integer(data, pos, 5) if new_size > self.max_allowed_size: raise H2CompressionError("dynamic table size exceeds advertised maximum") self.max_size = new_size self._evict() continue else: saw_header = True index, pos = decode_integer(data, pos, 4) if index: name = self._get(index)[0] else: name, pos = decode_string(data, pos) value, pos = decode_string(data, pos) headers.append((name, value)) name, value = headers[-1] list_size += self._entry_size(name, value) if list_size > self.max_header_list_size: raise H2CompressionError("decoded header list exceeds configured limit") return headers class Encoder(object): """A stateless, literal-without-indexing HPACK encoder.""" def encode(self, headers): out = bytearray() for name, value in headers: name = _as_bytes(name) value = _as_bytes(value) out += encode_integer(0, 4, 0x00) out += encode_string(name) out += encode_string(value) return bytes(out) def _recv_exact(sock, count): if count < 0: raise ValueError("negative receive size") chunks = [] received = 0 while received < count: chunk = sock.recv(count - received) if not chunk: raise H2TransportError("connection closed by peer") chunks.append(chunk) received += len(chunk) return b"".join(chunks) def _read_frame(sock, max_frame_size=DEFAULT_MAX_FRAME_SIZE): length, ftype, flags, sid = decode_frame_header(_recv_exact(sock, 9)) if length > max_frame_size: raise H2ProtocolError("received frame exceeds local maximum frame size") payload = _recv_exact(sock, length) if length else b"" return ftype, flags, sid, payload def _idna_host(host): if isinstance(host, bytes): try: host = host.decode("ascii") except UnicodeDecodeError: try: host = host.decode("utf-8") except UnicodeDecodeError: raise H2Error("hostname bytes are neither ASCII nor UTF-8") if not isinstance(host, text_type): host = text_type(host) if host.startswith("[") and host.endswith("]"): host = host[1:-1] if not host: raise H2Error("hostname is required") if ":" in host: # IPv6 literal, optionally with a zone identifier return host try: return host.encode("idna").decode("ascii") except (UnicodeError, AttributeError): raise H2Error("invalid internationalized hostname %r" % host) def _format_authority_host(host): host = _idna_host(host) if ":" in host: return "[%s]" % host return host def _authority_for_host(host, port): authority = _format_authority_host(host) if port != 443: authority += ":%d" % port return authority def _connect_socket(host, port, proxy, timeout): if not proxy: return socket.create_connection((host, port), timeout=timeout) proxy_host, proxy_port, proxy_cred = proxy raw = socket.create_connection((proxy_host, proxy_port), timeout=timeout) try: tunnel_host = _format_authority_host(host) authority = "%s:%d" % (tunnel_host, port) request = "CONNECT %s HTTP/1.1\r\nHost: %s\r\n" % (authority, authority) if proxy_cred: token = base64.b64encode(_as_bytes(proxy_cred)).decode("ascii") request += "Proxy-Authorization: Basic %s\r\n" % token request += "\r\n" raw.sendall(request.encode("latin-1")) # Read only through the CONNECT header terminator. A large recv() can consume # bytes from the tunneled TLS handshake that immediately follows the proxy # response, and those bytes cannot be pushed back before wrap_socket(). response = bytearray() while not bytes(response).endswith(b"\r\n\r\n"): chunk = raw.recv(1) if not chunk: raise H2Error("proxy closed connection during CONNECT") response.extend(chunk) if len(response) > 65536: raise H2Error("oversized proxy CONNECT response") status_line = bytes(response).split(b"\r\n", 1)[0].decode("latin-1", "replace") fields = status_line.split(None, 2) code = int(fields[1]) if len(fields) >= 2 and fields[1].isdigit() else 0 if not 200 <= code < 300: raise H2Error("proxy CONNECT failed: %s" % status_line) return raw except Exception: try: raw.close() except Exception: pass raise def _make_ssl_context(verify, ssl_context): context = ssl_context if context is None: if verify: creator = getattr(ssl, "create_default_context", None) if creator is None: raise H2Error("certificate verification requires ssl.create_default_context") context = creator() else: creator = getattr(ssl, "_create_unverified_context", None) if creator is not None: context = creator() else: context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) context.verify_mode = ssl.CERT_NONE tls_version = getattr(ssl, "TLSVersion", None) if tls_version is not None and hasattr(context, "minimum_version"): if context.minimum_version < tls_version.TLSv1_2: context.minimum_version = tls_version.TLSv1_2 else: for option_name in ("OP_NO_SSLv2", "OP_NO_SSLv3", "OP_NO_TLSv1", "OP_NO_TLSv1_1"): option = getattr(ssl, option_name, 0) if option: context.options |= option no_compression = getattr(ssl, "OP_NO_COMPRESSION", 0) if no_compression: context.options |= no_compression setter = getattr(context, "set_alpn_protocols", None) if setter is None: raise H2Error("this Python/OpenSSL build does not support ALPN") setter(["h2"]) return context def _iter_header_items(headers): if headers is None: return [] if hasattr(headers, "items"): return list(headers.items()) return list(headers) def _validate_regular_name(name): if not name or name.startswith(b":"): raise H2ProtocolError("invalid regular field name") for item in bytearray(name): if item not in _TOKEN_BYTES: raise H2ProtocolError("invalid HTTP field name %r" % name) def _validate_field_value(value): if b"\x00" in value or b"\r" in value or b"\n" in value: raise H2ProtocolError("invalid control character in HTTP field value") if value[:1] in (b" ", b"\t") or value[-1:] in (b" ", b"\t"): raise H2ProtocolError("HTTP/2 field values may not have leading/trailing whitespace") def _validate_pseudo_value(name, value): _validate_field_value(value) for item in bytearray(value): if item < 0x21 or item > 0x7e: raise H2ProtocolError("invalid octet in %s pseudo-field" % name) def _validate_method(method): if not method: raise H2ProtocolError("method is required") for item in bytearray(method): if item not in _METHOD_TOKEN_BYTES: raise H2ProtocolError("invalid HTTP method token") def _normalize_request_headers(headers, body): raw_items = [] connection_tokens = set() for key, value in _iter_header_items(headers): name = _as_bytes(key).lower() val = _as_bytes(value) if name.startswith(b":"): raise H2ProtocolError("callers may not inject HTTP/2 pseudo-fields") _validate_regular_name(name) _validate_field_value(val) raw_items.append((name, val)) if name == b"connection": for token in val.split(b","): token = token.strip().lower() if token: _validate_regular_name(token) connection_tokens.add(token) normalized = [] content_lengths = [] content_length_added = False for name, value in raw_items: if name == b"host" or name == b"proxy-authorization": continue if name in _CONNECTION_FIELDS or name in connection_tokens: continue if name == b"te" and value.strip().lower() != b"trailers": raise H2ProtocolError("HTTP/2 permits TE only with value 'trailers'") if name == b"content-length": content_lengths.append(value.strip()) if content_length_added: continue content_length_added = True normalized.append((name, value)) if content_lengths: if len(set(content_lengths)) != 1 or not content_lengths[0].isdigit(): raise H2ProtocolError("invalid or conflicting content-length fields") expected = len(body) if int(content_lengths[0]) != expected: raise H2ProtocolError("content-length does not match request body") return normalized def _request_header_list(method, path, authority, headers, body): method = _as_bytes(method) authority = _as_bytes(authority) path = _as_bytes(path) _validate_method(method) if not authority: raise H2ProtocolError("authority is required") _validate_pseudo_value(":authority", authority) if method == b"CONNECT": request = [(b":method", method), (b":authority", authority)] else: if not path: raise H2ProtocolError("non-CONNECT requests require a non-empty :path") _validate_pseudo_value(":path", path) request = [(b":method", method), (b":scheme", b"https"), (b":path", path), (b":authority", authority)] request.extend(_normalize_request_headers(headers, body)) return request def _field_section_size(headers): return sum(32 + len(name) + len(value) for name, value in headers) def _header_frames(stream_id, block, max_frame_size, end_stream): if max_frame_size < DEFAULT_MAX_FRAME_SIZE: raise H2ProtocolError("peer advertised an invalid maximum frame size") if not block: return [encode_frame(HEADERS, FLAG_END_HEADERS | (FLAG_END_STREAM if end_stream else 0), stream_id, b"")] frames = [] pos = 0 first = True while pos < len(block): chunk = block[pos:pos + max_frame_size] pos += len(chunk) final = pos == len(block) if first: flags = (FLAG_END_STREAM if end_stream else 0) | (FLAG_END_HEADERS if final else 0) frames.append(encode_frame(HEADERS, flags, stream_id, chunk)) first = False else: flags = FLAG_END_HEADERS if final else 0 frames.append(encode_frame(CONTINUATION, flags, stream_id, chunk)) return frames def _data_frames(stream_id, body, max_frame_size): frames = [] pos = 0 while pos < len(body): chunk = body[pos:pos + max_frame_size] pos += len(chunk) flags = FLAG_END_STREAM if pos == len(body) else 0 frames.append(encode_frame(DATA, flags, stream_id, chunk)) return frames def _extract_header_fragment(ftype, flags, payload): if ftype == CONTINUATION: return payload pos = 0 pad_length = 0 if flags & FLAG_PADDED: if not payload: raise H2ProtocolError("padded HEADERS frame has no pad length") pad_length = bytearray(payload)[0] pos = 1 if flags & FLAG_PRIORITY: if len(payload) - pos < 5: raise H2ProtocolError("truncated HEADERS priority fields") pos += 5 if pad_length > len(payload) - pos: raise H2ProtocolError("HEADERS padding exceeds frame payload") end = len(payload) - pad_length return payload[pos:end] def _extract_data(flags, payload): if not (flags & FLAG_PADDED): return payload if not payload: raise H2ProtocolError("padded DATA frame has no pad length") pad_length = bytearray(payload)[0] if pad_length > len(payload) - 1: raise H2ProtocolError("DATA padding exceeds frame payload") return payload[1:len(payload) - pad_length] def _parse_content_length(headers, trailers=False): values = [] for name, value in headers: if _as_bytes(name).lower() != b"content-length": continue if trailers: raise H2ProtocolError("content-length is not permitted in trailers") for part in _as_bytes(value).split(b","): part = part.strip() if not part or not part.isdigit(): raise H2ProtocolError("invalid content-length field") values.append(int(part)) if not values: return None expected = values[0] if any(value != expected for value in values[1:]): raise H2ProtocolError("conflicting content-length fields") return expected def _validate_response_field_section(headers, trailers=False): regular_seen = False status = None for name, value in headers: name = _as_bytes(name) value = _as_bytes(value) _validate_field_value(value) if name.startswith(b":"): if trailers or regular_seen or name != b":status" or status is not None: raise H2ProtocolError("invalid response pseudo-field ordering or duplication") if len(value) != 3 or not value.isdigit(): raise H2ProtocolError("invalid :status pseudo-field") status = int(value) if not 100 <= status <= 599: raise H2ProtocolError("response status is outside the valid range") if status == 101: raise H2ProtocolError("101 Switching Protocols is not valid in HTTP/2") else: regular_seen = True if name.lower() != name: raise H2ProtocolError("uppercase HTTP/2 field name") _validate_regular_name(name) if name in _CONNECTION_FIELDS: raise H2ProtocolError("connection-specific field in HTTP/2 response") content_length = _parse_content_length(headers, trailers=trailers) if trailers: return None, None if status is None: raise H2ProtocolError("response field section lacks :status") return status, content_length class _ResponseState(object): def __init__(self, stream_id, request_method): self.stream_id = stream_id self.request_method = _as_bytes(request_method) self.block = None self.block_end_stream = False self.headers = None self.trailers = [] self.informational_count = 0 self.informational_bytes = 0 self.status = None self.expected_content_length = None self.body_forbidden = False self.body = bytearray() self.recv_window = LOCAL_INITIAL_WINDOW self.complete = False class _H2Connection(object): """A sequential HTTP/2 connection with an explicit two-stream timing primitive.""" def __init__(self, host, port, proxy, timeout, verify=False, ssl_context=None): self.host = _idna_host(host) self.port = port self.proxy = proxy self.dec = Decoder() self.next_sid = 1 self.usable = True self.sock = None self.peer_max_frame_size = DEFAULT_MAX_FRAME_SIZE self.peer_initial_window = DEFAULT_WINDOW self.peer_max_concurrent_streams = None self.peer_max_header_list_size = None self.conn_send_window = DEFAULT_WINDOW self.conn_recv_window = LOCAL_INITIAL_WINDOW self.stream_send_windows = {} self.local_max_frame_size = DEFAULT_MAX_FRAME_SIZE self._our_settings_acked = False context = _make_ssl_context(verify, ssl_context) raw = _connect_socket(self.host, port, proxy, timeout) try: try: raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) except (OSError, socket.error): pass self.sock = context.wrap_socket(raw, server_hostname=self.host) except Exception: try: raw.close() except Exception: pass raise try: selected = getattr(self.sock, "selected_alpn_protocol", lambda: None)() if selected != "h2": raise H2Error("server did not negotiate h2 (ALPN=%r)" % selected) tls_version = getattr(self.sock, "version", lambda: None)() if tls_version not in ("TLSv1.2", "TLSv1.3"): raise H2Error("HTTP/2 requires TLS 1.2 or newer (negotiated %r)" % tls_version) compression = getattr(self.sock, "compression", lambda: None)() if compression is not None: raise H2Error("TLS compression is not permitted for HTTP/2") self.sock.settimeout(timeout) settings_payload = struct.pack( "!HIHIHI", SETTINGS_ENABLE_PUSH, 0, SETTINGS_INITIAL_WINDOW_SIZE, LOCAL_INITIAL_WINDOW, SETTINGS_MAX_HEADER_LIST_SIZE, MAX_HEADER_LIST_SIZE, ) self.sock.sendall(CONNECTION_PREFACE + encode_frame(SETTINGS, 0, 0, settings_payload)) self.sock.sendall(encode_frame( WINDOW_UPDATE, 0, 0, struct.pack("!I", LOCAL_INITIAL_WINDOW - DEFAULT_WINDOW) )) self._receive_initial_settings() except Exception: self.close() raise def close(self): self.usable = False sock = getattr(self, "sock", None) if sock is not None: try: sock.close() except Exception: pass self.sock = None def __del__(self): self.close() def _connection_error(self, message): self.usable = False raise H2ProtocolError(message) def _receive_initial_settings(self): ftype, flags, sid, payload = _read_frame(self.sock, self.local_max_frame_size) if ftype != SETTINGS or sid != 0 or (flags & FLAG_ACK): self._connection_error("the server connection preface did not start with SETTINGS") self._handle_settings(flags, sid, payload) def _handle_settings(self, flags, sid, payload): if sid != 0: self._connection_error("SETTINGS frame used a non-zero stream id") if flags & FLAG_ACK: if payload: self._connection_error("SETTINGS acknowledgement carried a payload") self._our_settings_acked = True return if len(payload) % 6: self._connection_error("SETTINGS payload length is not a multiple of 6") data = bytearray(payload) for pos in range(0, len(data), 6): setting_id, value = struct.unpack("!HI", bytes(data[pos:pos + 6])) if setting_id == SETTINGS_ENABLE_PUSH: if value != 0: self._connection_error("a server may set SETTINGS_ENABLE_PUSH only to 0") elif setting_id == SETTINGS_MAX_CONCURRENT_STREAMS: self.peer_max_concurrent_streams = value elif setting_id == SETTINGS_INITIAL_WINDOW_SIZE: if value > MAX_WINDOW: self._connection_error("invalid SETTINGS_INITIAL_WINDOW_SIZE") delta = value - self.peer_initial_window for stream_id in list(self.stream_send_windows): updated = self.stream_send_windows[stream_id] + delta if updated > MAX_WINDOW: self._connection_error("stream send window overflow after SETTINGS") self.stream_send_windows[stream_id] = updated self.peer_initial_window = value elif setting_id == SETTINGS_MAX_FRAME_SIZE: if not DEFAULT_MAX_FRAME_SIZE <= value <= MAX_FRAME_SIZE: self._connection_error("invalid SETTINGS_MAX_FRAME_SIZE") self.peer_max_frame_size = value elif setting_id == SETTINGS_MAX_HEADER_LIST_SIZE: self.peer_max_header_list_size = value elif setting_id == SETTINGS_HEADER_TABLE_SIZE: # Our encoder never uses the dynamic table, so every value is safe. pass self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) def _handle_ping(self, flags, sid, payload): if sid != 0 or len(payload) != 8: self._connection_error("invalid PING frame") if not (flags & FLAG_ACK): self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) def _handle_window_update(self, sid, payload): if len(payload) != 4: self._connection_error("invalid WINDOW_UPDATE length") increment = struct.unpack("!I", payload)[0] & MAX_WINDOW if increment == 0: self._connection_error("WINDOW_UPDATE increment is zero") if sid == 0: if self.conn_send_window > MAX_WINDOW - increment: self._connection_error("connection flow-control window overflow") self.conn_send_window += increment elif sid in self.stream_send_windows: current = self.stream_send_windows[sid] if current > MAX_WINDOW - increment: self._connection_error("stream flow-control window overflow") self.stream_send_windows[sid] = current + increment elif sid % 2 == 0 or sid >= self.next_sid: self._connection_error("WINDOW_UPDATE referenced an idle stream") def _allocate_stream_ids(self, count): if count <= 0: raise ValueError("stream count must be positive") last = self.next_sid + 2 * (count - 1) if last > MAX_STREAM_ID: self.usable = False raise H2Error("HTTP/2 stream-id space exhausted") sids = [self.next_sid + 2 * index for index in range(count)] self.next_sid += 2 * count if self.next_sid > MAX_STREAM_ID: self.usable = False return sids def _send_headers(self, sid, request_headers, end_stream): if self.peer_max_header_list_size is not None: if _field_section_size(request_headers) > self.peer_max_header_list_size: raise H2Error("request header list exceeds peer-advertised limit") block = Encoder().encode(request_headers) return b"".join(_header_frames(sid, block, self.peer_max_frame_size, end_stream)) def _finish_response(self, state): actual = len(state.body) if state.body_forbidden and actual: self._connection_error("response semantics forbid a message body") if (not state.body_forbidden and state.expected_content_length is not None and actual != state.expected_content_length): self._connection_error( "response content-length mismatch (expected %d, received %d)" % (state.expected_content_length, actual) ) state.complete = True def _complete_header_block(self, state): try: headers = self.dec.decode(bytes(state.block)) except H2CompressionError: self.usable = False raise end_stream = state.block_end_stream state.block = None state.block_end_stream = False if state.headers is None: status, content_length = _validate_response_field_section(headers, trailers=False) if 100 <= status < 200: if end_stream: self._connection_error("informational response ended the stream") if content_length is not None: self._connection_error("informational response carried content-length") state.informational_count += 1 state.informational_bytes += _field_section_size(headers) if state.informational_count > MAX_INFORMATIONAL_RESPONSES: self.usable = False raise H2Error("too many informational responses") if state.informational_bytes > MAX_INFORMATIONAL_HEADER_BYTES: self.usable = False raise H2Error("informational response headers exceed configured limit") return state.headers = headers state.status = status state.expected_content_length = content_length state.body_forbidden = state.request_method == b"HEAD" or status in (204, 205, 304) if status == 204 and content_length is not None: self._connection_error("204 response must not carry content-length") if status == 205 and content_length not in (None, 0): self._connection_error("205 response content-length must be zero") if state.request_method == b"CONNECT" and 200 <= status < 300 and content_length is not None: self._connection_error("successful CONNECT response must not carry content-length") if (not state.body_forbidden and content_length is not None and content_length > MAX_RESPONSE_SIZE): self.usable = False raise H2Error("declared response body exceeds configured limit") if end_stream: self._finish_response(state) else: _validate_response_field_section(headers, trailers=True) if not end_stream: self._connection_error("trailing field section did not end the stream") if _field_section_size(state.headers) + _field_section_size(headers) > MAX_HEADER_LIST_SIZE: self.usable = False raise H2Error("aggregate response headers exceed configured limit") state.trailers.extend(headers) state.headers.extend(headers) self._finish_response(state) def _process_header_frame(self, ftype, flags, sid, payload, states, continuation_sid): if continuation_sid is not None: if ftype != CONTINUATION or sid != continuation_sid: self._connection_error("frame interleaved inside a CONTINUATION sequence") elif ftype == CONTINUATION: self._connection_error("unexpected CONTINUATION frame") state = states.get(sid) if state is None: self._connection_error("header frame for an unknown stream") if state.complete: self._connection_error("header frame received after END_STREAM") if ftype == HEADERS: if state.block is not None: self._connection_error("new HEADERS before the prior field section completed") state.block = bytearray() state.block_end_stream = bool(flags & FLAG_END_STREAM) state.block.extend(_extract_header_fragment(ftype, flags, payload)) if len(state.block) > MAX_HEADER_BLOCK_SIZE: self.usable = False raise H2Error("compressed response header block exceeds configured limit") if flags & FLAG_END_HEADERS: self._complete_header_block(state) return None return sid def _process_data_frame(self, flags, sid, payload, states): state = states.get(sid) if state is None: self._connection_error("DATA frame for an unknown stream") if state.headers is None: self._connection_error("DATA received before final response headers") if state.complete: self._connection_error("DATA received after END_STREAM") flow_len = len(payload) if flow_len > self.conn_recv_window: self._connection_error("peer exceeded the connection receive window") if flow_len > state.recv_window: self._connection_error("peer exceeded the stream receive window") self.conn_recv_window -= flow_len state.recv_window -= flow_len data = _extract_data(flags, payload) if state.body_forbidden and data: self._connection_error("response semantics forbid a message body") if (state.expected_content_length is not None and not state.body_forbidden and len(state.body) + len(data) > state.expected_content_length): self._connection_error("response body exceeds declared content-length") if len(state.body) + len(data) > MAX_RESPONSE_SIZE: self.usable = False raise H2Error("response body exceeds configured limit") state.body.extend(data) end_stream = bool(flags & FLAG_END_STREAM) if flow_len: if end_stream: try: self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", flow_len))) self.conn_recv_window += flow_len except (socket.error, ssl.SSLError, IOError): self.usable = False else: self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", flow_len))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", flow_len))) self.conn_recv_window += flow_len state.recv_window += flow_len if end_stream: self._finish_response(state) def _process_frame(self, frame, states, continuation_sid): ftype, flags, sid, payload = frame if continuation_sid is not None and not (ftype == CONTINUATION and sid == continuation_sid): self._connection_error("non-CONTINUATION frame interleaved in a field section") if ftype == SETTINGS: self._handle_settings(flags, sid, payload) elif ftype == PING: self._handle_ping(flags, sid, payload) elif ftype == WINDOW_UPDATE: self._handle_window_update(sid, payload) elif ftype == GOAWAY: if sid != 0 or len(payload) < 8: self._connection_error("invalid GOAWAY frame") self.usable = False last_sid = struct.unpack("!I", payload[:4])[0] & MAX_STREAM_ID active = sorted(states) unprocessed = [stream_id for stream_id in active if stream_id > last_sid] if len(unprocessed) == len(active): raise _UnprocessedStream("GOAWAY confirms active stream(s) were not processed") if unprocessed: raise H2Error("GOAWAY partially processed a multi-stream exchange") elif ftype == RST_STREAM: if sid == 0 or len(payload) != 4: self._connection_error("invalid RST_STREAM frame") if sid in states: error_code = struct.unpack("!I", payload)[0] if len(states) == 1 and error_code == REFUSED_STREAM: raise _UnprocessedStream("peer refused the stream before processing") raise H2Error("stream %d reset by peer (error %d)" % (sid, error_code)) if sid % 2 == 0 or sid >= self.next_sid: self._connection_error("RST_STREAM referenced an idle stream") elif ftype == PUSH_PROMISE: self._connection_error("server push received despite SETTINGS_ENABLE_PUSH=0") elif ftype in (HEADERS, CONTINUATION): continuation_sid = self._process_header_frame( ftype, flags, sid, payload, states, continuation_sid ) elif ftype == DATA: if sid == 0: self._connection_error("DATA frame used stream 0") self._process_data_frame(flags, sid, payload, states) elif ftype == PRIORITY: if sid == 0 or len(payload) != 5: self._connection_error("invalid PRIORITY frame") # Unknown extension frames are ignored, as required by HTTP/2. return continuation_sid def exchange(self, method, path, authority, headers, body, timeout): if not self.usable: raise H2Error("HTTP/2 connection is not usable") if self.peer_max_concurrent_streams == 0: raise H2Error("peer currently permits no new streams") self.sock.settimeout(timeout) body = b"" if body is None else _as_bytes(body) method = _as_bytes(method) sid = self._allocate_stream_ids(1)[0] self.stream_send_windows[sid] = self.peer_initial_window try: state = _ResponseState(sid, method) states = {sid: state} request_headers = _request_header_list(method, path, authority, headers, body) self.sock.sendall(self._send_headers(sid, request_headers, not body)) body_pos = 0 continuation_sid = None frames_seen = 0 while not state.complete: while body_pos < len(body): available = min( self.peer_max_frame_size, self.conn_send_window, self.stream_send_windows[sid], len(body) - body_pos, ) if available <= 0: break chunk = body[body_pos:body_pos + available] body_pos += len(chunk) flags = FLAG_END_STREAM if body_pos == len(body) else 0 self.sock.sendall(encode_frame(DATA, flags, sid, chunk)) self.conn_send_window -= len(chunk) self.stream_send_windows[sid] -= len(chunk) if state.complete: break frame = _read_frame(self.sock, self.local_max_frame_size) frames_seen += 1 if frames_seen > MAX_RESPONSE_FRAMES: self.usable = False raise H2Error("response exceeded the frame-count limit") continuation_sid = self._process_frame(frame, states, continuation_sid) if continuation_sid is not None: self._connection_error("stream ended with an incomplete field section") if body_pos < len(body): self.usable = False try: self.sock.sendall(encode_frame(RST_STREAM, 0, sid, struct.pack("!I", CANCEL))) except Exception: pass if state.status is None: self._connection_error("stream ended without final response headers") return state.status, state.headers, bytes(state.body) finally: self.stream_send_windows.pop(sid, None) def exchange_pair(self, requests, timeout): """Send exactly two small requests in one application-level write. A single sendall() reduces userspace scheduling skew, but TCP/TLS may still split the bytes into multiple records or packets; packet-level coalescing is not guaranteed. """ if not self.usable: raise H2Error("HTTP/2 connection is not usable") requests = list(requests) if len(requests) != 2: raise ValueError("exchange_pair requires exactly two requests") if self.peer_max_concurrent_streams is not None and self.peer_max_concurrent_streams < 2: raise H2Error("peer does not permit two concurrent streams") self.sock.settimeout(timeout) sids = self._allocate_stream_ids(2) for sid in sids: self.stream_send_windows[sid] = self.peer_initial_window try: states = {} outbound = [] total_body = 0 for sid, request in zip(sids, requests): body = b"" if request.get("body") is None else _as_bytes(request.get("body")) method = _as_bytes(request.get("method", "GET")) states[sid] = _ResponseState(sid, method) if len(body) > self.peer_initial_window: raise H2Error("timeless-pair body exceeds the peer stream window") total_body += len(body) request_headers = _request_header_list( method, request["path"], request.get("authority") or _authority_for_host(self.host, self.port), request.get("headers"), body, ) outbound.append(self._send_headers(sid, request_headers, not body)) if body: outbound.extend(_data_frames(sid, body, self.peer_max_frame_size)) self.stream_send_windows[sid] -= len(body) if total_body > self.conn_send_window: raise H2Error("timeless-pair bodies exceed the connection flow-control window") self.conn_send_window -= total_body self.sock.sendall(b"".join(outbound)) finish_order = [] continuation_sid = None frames_seen = 0 while len(finish_order) < 2: before = dict((sid, states[sid].complete) for sid in sids) frame = _read_frame(self.sock, self.local_max_frame_size) frames_seen += 1 if frames_seen > MAX_RESPONSE_FRAMES: self.usable = False raise H2Error("timeless pair exceeded the frame-count limit") continuation_sid = self._process_frame(frame, states, continuation_sid) for sid in sids: if states[sid].complete and not before[sid] and sid not in finish_order: finish_order.append(sid) if continuation_sid is not None: self._connection_error("pair ended with an incomplete field section") results = {} for sid in sids: state = states[sid] if state.status is None: self._connection_error("pair stream ended without final response headers") results[sid] = (state.status, state.headers, bytes(state.body)) return finish_order, results finally: for sid in sids: self.stream_send_windows.pop(sid, None) _h2_pool = threading.local() def _pool_key(host, port, proxy, verify, ssl_context): proxy_key = tuple(proxy) if isinstance(proxy, list) else proxy context_key = ssl_context if ssl_context is not None else bool(verify) return _idna_host(host), port, proxy_key, context_key def _get_pool(): pool = getattr(_h2_pool, "connections", None) if pool is None or not isinstance(pool, OrderedDict): pool = OrderedDict() _h2_pool.connections = pool return pool def _pool_put(pool, key, conn): old = pool.pop(key, None) if old is not None and old is not conn: old.close() pool[key] = conn while len(pool) > H2_POOL_LIMIT: _, stale = pool.popitem(last=False) stale.close() def close_pooled_connections(): pool = _get_pool() for conn in list(pool.values()): conn.close() pool.clear() def _retryable_after_stale_connection(method): return _as_bytes(method) in (b"GET", b"HEAD") def _pooledExchange(host, port, proxy, method, path, authority, headers, body, timeout, verify=False, ssl_context=None): pool = _get_pool() key = _pool_key(host, port, proxy, verify, ssl_context) conn = pool.pop(key, None) reused = conn is not None and conn.usable if conn is None or not conn.usable: if conn is not None: conn.close() conn = _H2Connection(host, port, proxy, timeout, verify=verify, ssl_context=ssl_context) _pool_put(pool, key, conn) try: result = conn.exchange(method, path, authority, headers, body, timeout) except _UnprocessedStream: conn.close() pool.pop(key, None) replacement = _H2Connection(host, port, proxy, timeout, verify=verify, ssl_context=ssl_context) _pool_put(pool, key, replacement) try: result = replacement.exchange(method, path, authority, headers, body, timeout) except Exception: replacement.close() pool.pop(key, None) raise conn = replacement except (socket.error, ssl.SSLError, H2TransportError): conn.close() pool.pop(key, None) if not (reused and _retryable_after_stale_connection(method)): raise replacement = _H2Connection(host, port, proxy, timeout, verify=verify, ssl_context=ssl_context) _pool_put(pool, key, replacement) try: result = replacement.exchange(method, path, authority, headers, body, timeout) except Exception: replacement.close() pool.pop(key, None) raise conn = replacement except (H2Error, IOError): conn.close() pool.pop(key, None) raise if not conn.usable: conn.close() pool.pop(key, None) return result def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None, verify=False, ssl_context=None): conn = _H2Connection(host, port, proxy, timeout, verify=verify, ssl_context=ssl_context) try: return conn.exchange(method, path, authority or _authority_for_host(host, port), headers, body, timeout) finally: conn.close() class H2Response(object): """A small urllib-response-compatible wrapper for a buffered HTTP/2 response.""" def __init__(self, url, status, headers, body): self.url = url self.code = self.status = status self.msg = _HTTP_RESPONSES.get(status, "") self.http_version = "HTTP/2.0" self._body = body self._offset = 0 self._info = _Message() for name, value in headers or []: name = name.decode("latin-1") if isinstance(name, bytes) else name value = value.decode("latin-1") if isinstance(value, bytes) else value if not name.startswith(":"): self._info[name] = value self._info.headers = ["%s: %s\r\n" % item for item in self._info.items()] def info(self): return self._info def geturl(self): return self.url def getcode(self): return self.code def read(self, amt=None): if amt is None or amt < 0: data = self._body[self._offset:] self._offset = len(self._body) else: data = self._body[self._offset:self._offset + amt] self._offset += len(data) return data def close(self): pass def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() return False def _headers_without(headers, names): names = set(names) result = [] for key, value in _iter_header_items(headers): name = _as_bytes(key).lower() if name not in names: result.append((key, value)) return result def _url_port(parts): try: return parts.port or 443 except ValueError as ex: raise H2Error("invalid URL port: %s" % ex) def _origin_tuple(parts): host = _idna_host(parts.hostname or "").lower() return parts.scheme.lower(), host, _url_port(parts) def _quote_target_component(value, safe): if isinstance(value, text_type): try: value.encode("ascii") except UnicodeEncodeError: value = value.encode("utf-8") return quote(value, safe=safe) def proxy_tuple(proxy, cred=None): """Convert a sqlmap '--proxy' value into the (host, port, cred) tuple the `proxy` parameter of this client expects. Returns None when unset. SOCKS is not supported by the native client, so it raises ValueError and leaves it to the caller to decide how to surface that (hard error / skip the feature). >>> proxy_tuple("http://127.0.0.1:8080") ('127.0.0.1', 8080, None) >>> proxy_tuple("127.0.0.1") ('127.0.0.1', 8080, None) """ if not proxy: return None parts = urlsplit(proxy if "://" in proxy else "http://%s" % proxy) if (parts.scheme or "").lower().startswith("socks"): raise ValueError("native HTTP/2 client does not support SOCKS proxies") return (parts.hostname, parts.port or 8080, cred or None) def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True, max_redirects=10, proxy=None, verify=False, ssl_context=None): req_headers = headers or {} current_method = method current_body = body for redirect_count in range(max_redirects + 1): parts = urlsplit(url) if parts.scheme.lower() != "https": raise H2Error("native HTTP/2 client supports only https URLs") if not parts.hostname: raise H2Error("URL has no hostname") host = _idna_host(parts.hostname) port = _url_port(parts) path = _quote_target_component(parts.path or "/", "/%:@!$&'()*+,;=-._~") if parts.query: path += "?" + _quote_target_component(parts.query, "/?%:@!$&'()*+,;=-._~") authority = _authority_for_host(host, port) status, response_headers, response_body = _pooledExchange( host, port, proxy, current_method, path, authority, req_headers, current_body, timeout, verify=verify, ssl_context=ssl_context ) if not (follow_redirects and status in REDIRECT_CODES): return H2Response(url, status, response_headers, response_body) location = None for name, value in response_headers or []: if _as_bytes(name).lower() == b"location": location = value.decode("latin-1") if isinstance(value, bytes) else value break if not location: return H2Response(url, status, response_headers, response_body) if redirect_count == max_redirects: break new_url = urljoin(url, location) old_parts = parts new_parts = urlsplit(new_url) if _origin_tuple(old_parts) != _origin_tuple(new_parts): req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS) method_bytes = _as_bytes(current_method) change_to_get = status == 303 and method_bytes != b"HEAD" change_to_get = change_to_get or (status in (301, 302) and method_bytes == b"POST") if change_to_get: current_method = "GET" current_body = None req_headers = _headers_without(req_headers, _CONTENT_REDIRECT_FIELDS) url = new_url raise H2Error("too many HTTP/2 redirects")