mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
1611 lines
62 KiB
Python
1611 lines
62 KiB
Python
#!/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 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")
|