Adding support for HTTP2 connection reusage

This commit is contained in:
Miroslav Štampar 2026-07-02 13:39:47 +02:00
parent 47b8b6ed07
commit a7c9b721fd
3 changed files with 117 additions and 33 deletions

View file

@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
2c37b4a614c1d64facc5cf9d22b423316722a41768f57d9c2913dc23d30a7b21 lib/core/settings.py
0b0a122d3ae6f64c2af2aab91b72ecf6573e9cc1fd250f41ba441be60d8dd464 lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py
@ -215,7 +215,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch
4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
3afb06089f2801d5a12458a313b278db62c17a8d8fd3b8c46f07670699119af3 lib/request/http2.py
7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.14"
VERSION = "1.10.7.15"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

View file

@ -14,6 +14,7 @@ import base64
import socket
import ssl
import struct
import threading
try:
from http.client import responses as _HTTP_RESPONSES
@ -431,44 +432,86 @@ def _connect_socket(host, port, proxy, timeout):
pass
raise
def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None):
authority = authority or host
ctx = ssl._create_unverified_context()
ctx.set_alpn_protocols(["h2"])
sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host)
try:
if sock.selected_alpn_protocol() != "h2":
raise IOError("server did not negotiate h2 (ALPN=%r)" % sock.selected_alpn_protocol())
sock.settimeout(timeout)
class _UnprocessedStream(IOError):
"""Raised when the server made it clear our stream was NOT processed (GOAWAY with last-stream-id below
ours), so the request is always safe to retry on a fresh connection."""
# connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window
sock.sendall(CONNECTION_PREFACE)
sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW)))
sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535)))
class _H2Connection(object):
"""A single HTTP/2 connection reused for sequential (one-stream-at-a-time) requests within a thread.
Multiplexing is intentionally NOT used - one stream is fully consumed before the next is opened - which
preserves request<->response isolation (clean time-based latency, no desync), exactly like the
thread-local HTTP/1.1 keep-alive pool. Reuse amortizes the TCP+TLS+preface cost across all of a thread's
requests to a host. Correctness note: only the HPACK Decoder (server->client dynamic table) is stateful,
so it is kept per-connection and fed responses in order; the Encoder is literal-without-indexing
(stateless), hence a fresh one per request is safe on a reused socket."""
def __init__(self, host, port, proxy, timeout):
self.host, self.port, self.proxy = host, port, proxy
self.dec = Decoder() # persistent server->client HPACK table
self.next_sid = 1 # odd, strictly increasing per RFC 7540
self.usable = True
ctx = ssl._create_unverified_context()
ctx.set_alpn_protocols(["h2"])
self.sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host)
try:
if self.sock.selected_alpn_protocol() != "h2":
raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol())
self.sock.settimeout(timeout)
# connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window
self.sock.sendall(CONNECTION_PREFACE)
self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW)))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535)))
except Exception:
self.close()
raise
def close(self):
self.usable = False
try:
self.sock.close()
except Exception:
pass
def __del__(self):
self.close()
def exchange(self, method, path, authority, headers, body, timeout):
if not self.usable:
raise IOError("HTTP/2 connection no longer usable")
sid = self.next_sid
self.next_sid += 2
if self.next_sid >= BIG_WINDOW: # stream-id space nearly exhausted -> retire after this
self.usable = False
self.sock.settimeout(timeout)
req = [(b":method", _tob(method)), (b":scheme", b"https"), (b":path", _tob(path)), (b":authority", _tob(authority))]
for k, v in (headers or {}).items():
req.append((_tob(k).lower(), _tob(v)))
hblock = Encoder().encode(req)
sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), 1, hblock))
self.sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, hblock))
if body:
sock.sendall(encode_frame(DATA, FLAG_END_STREAM, 1, _tob(body)))
self.sock.sendall(encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body)))
dec = Decoder()
header_block, resp_headers, resp_body, done = b"", None, bytearray(), False
while not done:
ftype, flags, sid, payload = _read_frame(sock)
ftype, flags, fsid, payload = _read_frame(self.sock)
if ftype == SETTINGS:
if not (flags & FLAG_ACK):
sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b""))
self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b""))
elif ftype == PING:
if not (flags & FLAG_ACK):
sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
elif ftype == GOAWAY:
done = True
elif ftype == RST_STREAM and sid == 1:
self.usable = False # server won't accept new streams -> retire connection
last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0
if sid > last_sid: # our stream was not processed -> safe to retry fresh
raise _UnprocessedStream("GOAWAY (last stream %d) before stream %d was processed" % (last_sid, sid))
elif ftype == RST_STREAM and fsid == sid:
self.usable = False
raise IOError("stream reset by server (error %d)" % struct.unpack("!I", payload[:4])[0])
elif ftype in (HEADERS, CONTINUATION) and sid == 1:
elif ftype in (HEADERS, CONTINUATION) and fsid == sid:
p = payload
if ftype == HEADERS:
if flags & FLAG_PADDED:
@ -477,17 +520,17 @@ def h2_request(host, port=443, method="GET", path="/", authority=None, headers=N
p = p[5:]
header_block += p
if flags & FLAG_END_HEADERS:
resp_headers = dec.decode(header_block)
resp_headers = self.dec.decode(header_block)
if flags & FLAG_END_STREAM:
done = True
elif ftype == DATA and sid == 1:
elif ftype == DATA and fsid == sid:
p = payload
if flags & FLAG_PADDED:
p = p[1:len(p) - bytearray(payload)[0]]
resp_body += p
if payload: # replenish stream + connection windows
sock.sendall(encode_frame(WINDOW_UPDATE, 0, 1, struct.pack("!I", len(payload))))
sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload))))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
if flags & FLAG_END_STREAM:
done = True
status = None
@ -496,9 +539,50 @@ def h2_request(host, port=443, method="GET", path="/", authority=None, headers=N
status = int(v)
break
return status, resp_headers, bytes(resp_body)
# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model
# (one connection per host per thread) so streams never interleave across threads and time-based
# measurements stay clean.
_h2_pool = threading.local()
def _pooledExchange(host, port, proxy, method, path, authority, headers, body, timeout):
pool = getattr(_h2_pool, "connections", None)
if pool is None:
pool = _h2_pool.connections = {}
key = (host, port, proxy)
conn = pool.get(key)
reused = conn is not None and conn.usable
if not reused:
if conn is not None:
conn.close()
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
try:
result = conn.exchange(method, path, authority, headers, body, timeout)
except _UnprocessedStream: # explicitly not processed -> always safe to retry fresh
conn.close(); pool.pop(key, None)
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
result = conn.exchange(method, path, authority, headers, body, timeout)
except (socket.error, ssl.SSLError, IOError):
conn.close(); pool.pop(key, None)
if reused: # stale keep-alive socket (server closed idle conn) -> reopen once
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
result = conn.exchange(method, path, authority, headers, body, timeout)
else:
raise
if not conn.usable: # GOAWAY / id-exhaustion mid-exchange -> don't keep it pooled
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):
"""One-shot request on a throwaway connection (kept for direct/back-compat callers; the engine path
goes through open_url -> the reusing pool)."""
conn = _H2Connection(host, port, proxy, timeout)
try:
return conn.exchange(method, path, authority or host, headers, body, timeout)
finally:
try: sock.close()
except Exception: pass
conn.close()
class H2Response(object):
@ -567,8 +651,8 @@ def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redi
path = parts.path or "/"
if parts.query:
path += "?" + parts.query
status, resp_headers, resp_body = h2_request(parts.hostname, parts.port or 443, method=method, path=path,
authority=parts.netloc.split("@")[-1], headers=req_headers, body=body, timeout=timeout, proxy=proxy)
status, resp_headers, resp_body = _pooledExchange(parts.hostname, parts.port or 443, proxy, method, path,
parts.netloc.split("@")[-1], req_headers, body, timeout)
if follow_redirects and status in REDIRECT_CODES:
location = None
for name, value in (resp_headers or []):