tlsreq 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tlsreq/__init__.py +18 -0
- tlsreq/backends/__init__.py +61 -0
- tlsreq/backends/_httpx_patch.py +122 -0
- tlsreq/backends/_niquests_patch.py +327 -0
- tlsreq/backends/base.py +58 -0
- tlsreq/backends/curl_cffi.py +149 -0
- tlsreq/backends/httpx.py +262 -0
- tlsreq/backends/niquests.py +209 -0
- tlsreq/backends/wreq.py +241 -0
- tlsreq/chrome_h2.py +40 -0
- tlsreq/errors.py +14 -0
- tlsreq/fingerprints.py +131 -0
- tlsreq/install_utls.py +19 -0
- tlsreq/py.typed +0 -0
- tlsreq/response.py +119 -0
- tlsreq/session.py +203 -0
- tlsreq/utls_release.py +98 -0
- tlsreq-0.1.0.dist-info/METADATA +82 -0
- tlsreq-0.1.0.dist-info/RECORD +23 -0
- tlsreq-0.1.0.dist-info/WHEEL +5 -0
- tlsreq-0.1.0.dist-info/entry_points.txt +2 -0
- tlsreq-0.1.0.dist-info/licenses/LICENSE +21 -0
- tlsreq-0.1.0.dist-info/top_level.txt +1 -0
tlsreq/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .errors import BackendNotInstalled, TlsReqError, UnknownBackend, UnknownFingerprint
|
|
2
|
+
from .response import Response
|
|
3
|
+
from .session import AsyncSession, Session
|
|
4
|
+
from .utls_release import UTLS_RELEASE, UTLS_RELEASE_PAGE
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AsyncSession",
|
|
9
|
+
"Session",
|
|
10
|
+
"Response",
|
|
11
|
+
"TlsReqError",
|
|
12
|
+
"UnknownBackend",
|
|
13
|
+
"BackendNotInstalled",
|
|
14
|
+
"UnknownFingerprint",
|
|
15
|
+
"UTLS_RELEASE",
|
|
16
|
+
"UTLS_RELEASE_PAGE",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from ..errors import UnknownBackend
|
|
6
|
+
from .base import AsyncBackend, SyncBackend
|
|
7
|
+
|
|
8
|
+
_ALIASES = {
|
|
9
|
+
"curl": "curl_cffi",
|
|
10
|
+
"cffi": "curl_cffi",
|
|
11
|
+
"nirequest": "niquests",
|
|
12
|
+
"nirequests": "niquests",
|
|
13
|
+
"nio": "niquests",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_backend(name: str) -> str:
|
|
18
|
+
key = name.strip().lower().replace("-", "_")
|
|
19
|
+
return _ALIASES.get(key, key)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_sync_backend(
|
|
23
|
+
backend: str,
|
|
24
|
+
impersonate: Optional[str] = None,
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> SyncBackend:
|
|
27
|
+
name = _normalize_backend(backend)
|
|
28
|
+
if name == "curl_cffi":
|
|
29
|
+
from .curl_cffi import CurlCffiSync
|
|
30
|
+
return CurlCffiSync(impersonate=impersonate, **kwargs)
|
|
31
|
+
if name == "wreq":
|
|
32
|
+
from .wreq import WreqSync
|
|
33
|
+
return WreqSync(impersonate=impersonate, **kwargs)
|
|
34
|
+
if name == "niquests":
|
|
35
|
+
from .niquests import NiquestsSync
|
|
36
|
+
return NiquestsSync(impersonate=impersonate, **kwargs)
|
|
37
|
+
if name == "httpx":
|
|
38
|
+
from .httpx import HttpxSync
|
|
39
|
+
return HttpxSync(impersonate=impersonate, **kwargs)
|
|
40
|
+
raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_async_backend(
|
|
44
|
+
backend: str,
|
|
45
|
+
impersonate: Optional[str] = None,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
) -> AsyncBackend:
|
|
48
|
+
name = _normalize_backend(backend)
|
|
49
|
+
if name == "curl_cffi":
|
|
50
|
+
from .curl_cffi import CurlCffiAsync
|
|
51
|
+
return CurlCffiAsync(impersonate=impersonate, **kwargs)
|
|
52
|
+
if name == "wreq":
|
|
53
|
+
from .wreq import WreqAsync
|
|
54
|
+
return WreqAsync(impersonate=impersonate, **kwargs)
|
|
55
|
+
if name == "niquests":
|
|
56
|
+
from .niquests import NiquestsAsync
|
|
57
|
+
return NiquestsAsync(impersonate=impersonate, **kwargs)
|
|
58
|
+
if name == "httpx":
|
|
59
|
+
from .httpx import HttpxAsync
|
|
60
|
+
return HttpxAsync(impersonate=impersonate, **kwargs)
|
|
61
|
+
raise UnknownBackend(f"未知 backend {backend!r}。可选: curl_cffi, wreq, niquests, httpx")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""httpcore/h2:Chrome 152 SETTINGS、连接窗口、HEADERS Priority。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import collections
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import h2.settings
|
|
8
|
+
|
|
9
|
+
from ..chrome_h2 import (
|
|
10
|
+
CONNECTION_WINDOW_INCREMENT,
|
|
11
|
+
ENABLE_PUSH,
|
|
12
|
+
H2_HOP_BY_HOP,
|
|
13
|
+
HEADER_TABLE_SIZE,
|
|
14
|
+
INITIAL_WINDOW_SIZE,
|
|
15
|
+
MAX_HEADER_LIST_SIZE,
|
|
16
|
+
PRIORITY_DEPENDS_ON,
|
|
17
|
+
PRIORITY_EXCLUSIVE,
|
|
18
|
+
PRIORITY_WEIGHT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_PATCHED = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _chrome_local_settings() -> h2.settings.Settings:
|
|
25
|
+
settings = h2.settings.Settings(
|
|
26
|
+
client=True,
|
|
27
|
+
initial_values={
|
|
28
|
+
h2.settings.SettingCodes.HEADER_TABLE_SIZE: HEADER_TABLE_SIZE,
|
|
29
|
+
h2.settings.SettingCodes.ENABLE_PUSH: ENABLE_PUSH,
|
|
30
|
+
h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: INITIAL_WINDOW_SIZE,
|
|
31
|
+
h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: MAX_HEADER_LIST_SIZE,
|
|
32
|
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
del settings[h2.settings.SettingCodes.MAX_FRAME_SIZE]
|
|
36
|
+
del settings[h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL]
|
|
37
|
+
del settings[h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS]
|
|
38
|
+
return settings
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _restore_local_only(settings: h2.settings.Settings) -> None:
|
|
42
|
+
"""SETTINGS 帧发出后再补回 h2/httpcore 内部会读的项,避免再发一帧。"""
|
|
43
|
+
codes = h2.settings.SettingCodes
|
|
44
|
+
settings._settings.setdefault(codes.MAX_CONCURRENT_STREAMS, collections.deque([100]))
|
|
45
|
+
settings._settings.setdefault(codes.MAX_FRAME_SIZE, collections.deque([16384]))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def patch_httpcore_chrome_h2() -> None:
|
|
49
|
+
global _PATCHED
|
|
50
|
+
if _PATCHED:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
from httpcore._async.http2 import AsyncHTTP2Connection
|
|
54
|
+
from httpcore._async.http2 import has_body_headers as async_has_body_headers
|
|
55
|
+
from httpcore._sync.http2 import HTTP2Connection
|
|
56
|
+
from httpcore._sync.http2 import has_body_headers as sync_has_body_headers
|
|
57
|
+
|
|
58
|
+
async def async_send_connection_init(self: Any, request: Any) -> None:
|
|
59
|
+
self._h2_state.local_settings = _chrome_local_settings()
|
|
60
|
+
self._h2_state.initiate_connection()
|
|
61
|
+
_restore_local_only(self._h2_state.local_settings)
|
|
62
|
+
self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
|
|
63
|
+
await self._write_outgoing_data(request)
|
|
64
|
+
|
|
65
|
+
def sync_send_connection_init(self: Any, request: Any) -> None:
|
|
66
|
+
self._h2_state.local_settings = _chrome_local_settings()
|
|
67
|
+
self._h2_state.initiate_connection()
|
|
68
|
+
_restore_local_only(self._h2_state.local_settings)
|
|
69
|
+
self._h2_state.increment_flow_control_window(CONNECTION_WINDOW_INCREMENT)
|
|
70
|
+
self._write_outgoing_data(request)
|
|
71
|
+
|
|
72
|
+
async def async_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
|
|
73
|
+
end_stream = not async_has_body_headers(request)
|
|
74
|
+
authority = [value for key, value in request.headers if key.lower() == b"host"][0]
|
|
75
|
+
headers = [
|
|
76
|
+
(b":method", request.method),
|
|
77
|
+
(b":authority", authority),
|
|
78
|
+
(b":scheme", request.url.scheme),
|
|
79
|
+
(b":path", request.url.target),
|
|
80
|
+
] + [
|
|
81
|
+
(key.lower(), value)
|
|
82
|
+
for key, value in request.headers
|
|
83
|
+
if key.lower() not in H2_HOP_BY_HOP
|
|
84
|
+
]
|
|
85
|
+
self._h2_state.send_headers(
|
|
86
|
+
stream_id,
|
|
87
|
+
headers,
|
|
88
|
+
end_stream=end_stream,
|
|
89
|
+
priority_weight=PRIORITY_WEIGHT,
|
|
90
|
+
priority_depends_on=PRIORITY_DEPENDS_ON,
|
|
91
|
+
priority_exclusive=PRIORITY_EXCLUSIVE,
|
|
92
|
+
)
|
|
93
|
+
await self._write_outgoing_data(request)
|
|
94
|
+
|
|
95
|
+
def sync_send_request_headers(self: Any, request: Any, stream_id: int) -> None:
|
|
96
|
+
end_stream = not sync_has_body_headers(request)
|
|
97
|
+
authority = [value for key, value in request.headers if key.lower() == b"host"][0]
|
|
98
|
+
headers = [
|
|
99
|
+
(b":method", request.method),
|
|
100
|
+
(b":authority", authority),
|
|
101
|
+
(b":scheme", request.url.scheme),
|
|
102
|
+
(b":path", request.url.target),
|
|
103
|
+
] + [
|
|
104
|
+
(key.lower(), value)
|
|
105
|
+
for key, value in request.headers
|
|
106
|
+
if key.lower() not in H2_HOP_BY_HOP
|
|
107
|
+
]
|
|
108
|
+
self._h2_state.send_headers(
|
|
109
|
+
stream_id,
|
|
110
|
+
headers,
|
|
111
|
+
end_stream=end_stream,
|
|
112
|
+
priority_weight=PRIORITY_WEIGHT,
|
|
113
|
+
priority_depends_on=PRIORITY_DEPENDS_ON,
|
|
114
|
+
priority_exclusive=PRIORITY_EXCLUSIVE,
|
|
115
|
+
)
|
|
116
|
+
self._write_outgoing_data(request)
|
|
117
|
+
|
|
118
|
+
AsyncHTTP2Connection._send_connection_init = async_send_connection_init
|
|
119
|
+
AsyncHTTP2Connection._send_request_headers = async_send_request_headers
|
|
120
|
+
HTTP2Connection._send_connection_init = sync_send_connection_init
|
|
121
|
+
HTTP2Connection._send_request_headers = sync_send_request_headers
|
|
122
|
+
_PATCHED = True
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""niquests HTTP/2 帧序 / HTTP/1 头与写合并,对齐 Chrome。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_H2_PATCHED = False
|
|
8
|
+
_H1_PATCHED = False
|
|
9
|
+
|
|
10
|
+
_HTTP1_KEEP_LOWER = frozenset({
|
|
11
|
+
b"sec-ch-ua",
|
|
12
|
+
b"sec-ch-ua-mobile",
|
|
13
|
+
b"sec-ch-ua-platform",
|
|
14
|
+
b"sec-fetch-site",
|
|
15
|
+
b"sec-fetch-mode",
|
|
16
|
+
b"sec-fetch-dest",
|
|
17
|
+
b"sec-fetch-user",
|
|
18
|
+
b"dnt",
|
|
19
|
+
b"priority",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
_H2_FRAME_HEADER = 9
|
|
23
|
+
_H2_HEADERS = 0x01
|
|
24
|
+
_H2_SETTINGS = 0x04
|
|
25
|
+
_H2_ACK = 0x01
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _import_module(name: str) -> Any:
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
mod = sys.modules.get(name)
|
|
32
|
+
if mod is not None:
|
|
33
|
+
return mod
|
|
34
|
+
try:
|
|
35
|
+
__import__(name)
|
|
36
|
+
except ImportError:
|
|
37
|
+
return None
|
|
38
|
+
return sys.modules.get(name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _should_skip_h2_settings_wait(self: Any, event_type: Any) -> bool:
|
|
42
|
+
names = (
|
|
43
|
+
(getattr(event_type, "__name__", ""),)
|
|
44
|
+
if not isinstance(event_type, tuple)
|
|
45
|
+
else tuple(getattr(t, "__name__", "") for t in event_type)
|
|
46
|
+
)
|
|
47
|
+
svn = getattr(self._svn, "name", None) or getattr(self._svn, "value", self._svn)
|
|
48
|
+
return "HandshakeCompleted" in names and svn in {"h2", "HTTP/2.0"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _wrap_exchange_until(orig: Any) -> Any:
|
|
52
|
+
if inspect.iscoroutinefunction(orig):
|
|
53
|
+
async def __exchange_until(self: Any, event_type: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> Any:
|
|
54
|
+
if _should_skip_h2_settings_wait(self, event_type):
|
|
55
|
+
return []
|
|
56
|
+
return await _orig(self, event_type, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return __exchange_until
|
|
59
|
+
|
|
60
|
+
def __exchange_until(self: Any, event_type: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> Any:
|
|
61
|
+
if _should_skip_h2_settings_wait(self, event_type):
|
|
62
|
+
return []
|
|
63
|
+
return _orig(self, event_type, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
return __exchange_until
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _iter_h2_frames(data: bytes) -> tuple[list[bytes], bytes]:
|
|
69
|
+
frames: list[bytes] = []
|
|
70
|
+
i = 0
|
|
71
|
+
n = len(data)
|
|
72
|
+
while i + _H2_FRAME_HEADER <= n:
|
|
73
|
+
length = int.from_bytes(data[i:i + 3], "big")
|
|
74
|
+
end = i + _H2_FRAME_HEADER + length
|
|
75
|
+
if end > n:
|
|
76
|
+
break
|
|
77
|
+
frames.append(data[i:end])
|
|
78
|
+
i = end
|
|
79
|
+
return frames, data[i:]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _park_settings_ack(protocol: Any, data: bytes) -> bytes:
|
|
83
|
+
frames, rest = _iter_h2_frames(data)
|
|
84
|
+
kept: list[bytes] = []
|
|
85
|
+
pending = getattr(protocol, "_tlsreq_pending_ack", b"")
|
|
86
|
+
for frame in frames:
|
|
87
|
+
if frame[3] == _H2_SETTINGS and frame[4] & _H2_ACK:
|
|
88
|
+
pending += frame
|
|
89
|
+
continue
|
|
90
|
+
if frame[3] == _H2_HEADERS:
|
|
91
|
+
protocol._tlsreq_headers_sent = True
|
|
92
|
+
kept.append(frame)
|
|
93
|
+
protocol._tlsreq_pending_ack = pending
|
|
94
|
+
return b"".join(kept) + rest
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _wrap_bytes_received(orig: Any) -> Any:
|
|
98
|
+
def bytes_received(self: Any, data: bytes, _orig: Any = orig) -> None:
|
|
99
|
+
_orig(self, data)
|
|
100
|
+
buf = getattr(self._connection, "_data_to_send", None)
|
|
101
|
+
if buf:
|
|
102
|
+
self._connection._data_to_send = bytearray(_park_settings_ack(self, bytes(buf)))
|
|
103
|
+
|
|
104
|
+
return bytes_received
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _swap_python_hpack(connection: Any) -> None:
|
|
108
|
+
from jh2.hpack.hpack import Encoder as PyHpackEncoder
|
|
109
|
+
|
|
110
|
+
if type(connection.encoder).__module__ == "jh2.hpack.hpack":
|
|
111
|
+
return
|
|
112
|
+
connection.encoder = PyHpackEncoder()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _wrap_protocol_init(orig: Any) -> Any:
|
|
116
|
+
def __init__(self: Any, *args: Any, _orig: Any = orig, **kwargs: Any) -> None:
|
|
117
|
+
_orig(self, *args, **kwargs)
|
|
118
|
+
_swap_python_hpack(self._connection)
|
|
119
|
+
|
|
120
|
+
return __init__
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _wrap_bytes_to_send(orig: Any) -> Any:
|
|
124
|
+
def bytes_to_send(self: Any, _orig: Any = orig) -> bytes:
|
|
125
|
+
already_flushed = getattr(self, "_tlsreq_headers_flushed", False)
|
|
126
|
+
data = _park_settings_ack(self, _orig(self))
|
|
127
|
+
if getattr(self, "_tlsreq_headers_sent", False):
|
|
128
|
+
self._tlsreq_headers_flushed = True
|
|
129
|
+
if already_flushed:
|
|
130
|
+
writes = getattr(self, "_tlsreq_post_headers_writes", 0) + 1
|
|
131
|
+
self._tlsreq_post_headers_writes = writes
|
|
132
|
+
pending = getattr(self, "_tlsreq_pending_ack", b"")
|
|
133
|
+
if pending and writes >= 2:
|
|
134
|
+
self._tlsreq_pending_ack = b""
|
|
135
|
+
return data + pending
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
return bytes_to_send
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def patch_http2_chrome_frames() -> None:
|
|
142
|
+
global _H2_PATCHED
|
|
143
|
+
if _H2_PATCHED:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
priority_ok = False
|
|
147
|
+
for name in (
|
|
148
|
+
"niquests.packages.urllib3.contrib.hface.protocols.http2._h2",
|
|
149
|
+
"urllib3_future.contrib.hface.protocols.http2._h2",
|
|
150
|
+
"urllib3.contrib.hface.protocols.http2._h2",
|
|
151
|
+
):
|
|
152
|
+
mod = _import_module(name)
|
|
153
|
+
cls = getattr(mod, "HTTP2ProtocolHyperImpl", None) if mod else None
|
|
154
|
+
if cls is None:
|
|
155
|
+
continue
|
|
156
|
+
if getattr(cls, "_tlsreq_h2_patched", False) or getattr(cls, "_chrome151_ack_held", False):
|
|
157
|
+
priority_ok = True
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
def submit_headers(self: Any, stream_id: int, headers: Any, end_stream: bool = False) -> None:
|
|
161
|
+
self._connection.send_headers(
|
|
162
|
+
stream_id,
|
|
163
|
+
headers,
|
|
164
|
+
end_stream,
|
|
165
|
+
priority_weight=256,
|
|
166
|
+
priority_depends_on=0,
|
|
167
|
+
priority_exclusive=True,
|
|
168
|
+
)
|
|
169
|
+
self._open_stream_count += 1
|
|
170
|
+
|
|
171
|
+
cls.submit_headers = submit_headers
|
|
172
|
+
cls.bytes_received = _wrap_bytes_received(cls.bytes_received)
|
|
173
|
+
cls.bytes_to_send = _wrap_bytes_to_send(cls.bytes_to_send)
|
|
174
|
+
cls.__init__ = _wrap_protocol_init(cls.__init__)
|
|
175
|
+
cls._tlsreq_h2_patched = True
|
|
176
|
+
priority_ok = True
|
|
177
|
+
|
|
178
|
+
preface_ok = False
|
|
179
|
+
for name, cls_name, mangled in (
|
|
180
|
+
("niquests.packages.urllib3.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
|
|
181
|
+
("urllib3_future.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
|
|
182
|
+
("urllib3.backend.hface", "HfaceBackend", "_HfaceBackend__exchange_until"),
|
|
183
|
+
("niquests.packages.urllib3.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
|
|
184
|
+
("urllib3_future.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
|
|
185
|
+
("urllib3.backend._async.hface", "AsyncHfaceBackend", "_AsyncHfaceBackend__exchange_until"),
|
|
186
|
+
):
|
|
187
|
+
mod = _import_module(name)
|
|
188
|
+
cls = getattr(mod, cls_name, None) if mod else None
|
|
189
|
+
if cls is None:
|
|
190
|
+
continue
|
|
191
|
+
if getattr(cls, "_tlsreq_skip_h2_settings_wait", False) or getattr(cls, "_chrome151_skip_h2_settings_wait", False):
|
|
192
|
+
preface_ok = True
|
|
193
|
+
continue
|
|
194
|
+
orig = getattr(cls, mangled, None)
|
|
195
|
+
if orig is None:
|
|
196
|
+
continue
|
|
197
|
+
setattr(cls, mangled, _wrap_exchange_until(orig))
|
|
198
|
+
cls._tlsreq_skip_h2_settings_wait = True
|
|
199
|
+
preface_ok = True
|
|
200
|
+
|
|
201
|
+
if not priority_ok or not preface_ok:
|
|
202
|
+
raise RuntimeError("could not patch niquests HTTP/2 to match Chrome frame order")
|
|
203
|
+
_H2_PATCHED = True
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _chrome_http1_header_name(name: bytes) -> bytes:
|
|
207
|
+
lower = name.lower().replace(b"_", b"-")
|
|
208
|
+
if lower in _HTTP1_KEEP_LOWER or lower.startswith(b"sec-"):
|
|
209
|
+
return lower
|
|
210
|
+
return b"-".join(part.capitalize() for part in lower.split(b"-"))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _wrap_http1_bytes_to_send(orig: Any) -> Any:
|
|
214
|
+
def bytes_to_send(self: Any, _orig: Any = orig) -> bytes:
|
|
215
|
+
if getattr(self, "_tlsreq_hold", False):
|
|
216
|
+
return b""
|
|
217
|
+
return _orig(self)
|
|
218
|
+
|
|
219
|
+
return bytes_to_send
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _wrap_http1_submit_data(orig: Any) -> Any:
|
|
223
|
+
def submit_data(self: Any, stream_id: int, data: bytes, end_stream: bool = False, _orig: Any = orig) -> None:
|
|
224
|
+
_orig(self, stream_id, data, end_stream)
|
|
225
|
+
if end_stream:
|
|
226
|
+
self._tlsreq_hold = False
|
|
227
|
+
|
|
228
|
+
return submit_data
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _wrap_async_endheaders(orig: Any) -> Any:
|
|
232
|
+
async def endheaders(
|
|
233
|
+
self: Any,
|
|
234
|
+
message_body: Any = None,
|
|
235
|
+
*,
|
|
236
|
+
encode_chunked: bool = False,
|
|
237
|
+
expect_body_afterward: bool = False,
|
|
238
|
+
_orig: Any = orig,
|
|
239
|
+
) -> Any:
|
|
240
|
+
from urllib3 import HttpVersion
|
|
241
|
+
|
|
242
|
+
proto = getattr(self, "_protocol", None)
|
|
243
|
+
if expect_body_afterward and getattr(self, "_svn", None) == HttpVersion.h11 and proto is not None:
|
|
244
|
+
proto._tlsreq_hold = True
|
|
245
|
+
return await _orig(
|
|
246
|
+
self,
|
|
247
|
+
message_body,
|
|
248
|
+
encode_chunked=encode_chunked,
|
|
249
|
+
expect_body_afterward=expect_body_afterward,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return endheaders
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def patch_http1_chrome_headers() -> None:
|
|
256
|
+
global _H1_PATCHED
|
|
257
|
+
if _H1_PATCHED:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
import h11
|
|
261
|
+
|
|
262
|
+
header_ok = False
|
|
263
|
+
for name in (
|
|
264
|
+
"niquests.packages.urllib3.contrib.hface.protocols.http1._h11",
|
|
265
|
+
"urllib3_future.contrib.hface.protocols.http1._h11",
|
|
266
|
+
"urllib3.contrib.hface.protocols.http1._h11",
|
|
267
|
+
):
|
|
268
|
+
mod = _import_module(name)
|
|
269
|
+
if mod is None:
|
|
270
|
+
continue
|
|
271
|
+
cls = getattr(mod, "HTTP1ProtocolHyperImpl", None)
|
|
272
|
+
if cls is not None and not getattr(cls, "_tlsreq_h11_hold", False) and not getattr(cls, "_chrome151_h11_hold", False):
|
|
273
|
+
cls.bytes_to_send = _wrap_http1_bytes_to_send(cls.bytes_to_send)
|
|
274
|
+
cls.submit_data = _wrap_http1_submit_data(cls.submit_data)
|
|
275
|
+
cls._tlsreq_h11_hold = True
|
|
276
|
+
if getattr(mod, "_tlsreq_http1_headers", False) or getattr(mod, "_chrome151_http1_headers", False):
|
|
277
|
+
header_ok = True
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
orig_headers_to_request = mod.headers_to_request
|
|
281
|
+
|
|
282
|
+
def headers_to_request(headers: Any, _orig: Any = orig_headers_to_request) -> Any:
|
|
283
|
+
request = _orig(headers)
|
|
284
|
+
present = {key.lower() for key, _value in request.headers}
|
|
285
|
+
if b"connection" in present:
|
|
286
|
+
return request
|
|
287
|
+
rewritten = list(request.headers)
|
|
288
|
+
insert_at = 0
|
|
289
|
+
for index, (key, _value) in enumerate(rewritten):
|
|
290
|
+
if key.lower() == b"host":
|
|
291
|
+
insert_at = index + 1
|
|
292
|
+
break
|
|
293
|
+
rewritten.insert(insert_at, (b"Connection", b"keep-alive"))
|
|
294
|
+
return h11.Request(
|
|
295
|
+
method=request.method,
|
|
296
|
+
headers=rewritten,
|
|
297
|
+
target=request.target,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def capitalize_header_name(header: bytes) -> bytes:
|
|
301
|
+
return _chrome_http1_header_name(header)
|
|
302
|
+
|
|
303
|
+
mod.capitalize_header_name = capitalize_header_name
|
|
304
|
+
mod.headers_to_request = headers_to_request
|
|
305
|
+
mod._tlsreq_http1_headers = True
|
|
306
|
+
header_ok = True
|
|
307
|
+
|
|
308
|
+
coalesce_ok = False
|
|
309
|
+
for name, cls_name in (
|
|
310
|
+
("niquests.packages.urllib3.backend._async.hface", "AsyncHfaceBackend"),
|
|
311
|
+
("urllib3_future.backend._async.hface", "AsyncHfaceBackend"),
|
|
312
|
+
("urllib3.backend._async.hface", "AsyncHfaceBackend"),
|
|
313
|
+
):
|
|
314
|
+
mod = _import_module(name)
|
|
315
|
+
cls = getattr(mod, cls_name, None) if mod else None
|
|
316
|
+
if cls is None:
|
|
317
|
+
continue
|
|
318
|
+
if getattr(cls, "_tlsreq_h11_coalesce", False) or getattr(cls, "_chrome151_h11_coalesce", False):
|
|
319
|
+
coalesce_ok = True
|
|
320
|
+
continue
|
|
321
|
+
cls.endheaders = _wrap_async_endheaders(cls.endheaders)
|
|
322
|
+
cls._tlsreq_h11_coalesce = True
|
|
323
|
+
coalesce_ok = True
|
|
324
|
+
|
|
325
|
+
if not header_ok or not coalesce_ok:
|
|
326
|
+
raise RuntimeError("could not patch niquests HTTP/1 to match Chrome request write")
|
|
327
|
+
_H1_PATCHED = True
|
tlsreq/backends/base.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from ..response import Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def merge_extra(mapped: dict[str, Any], extra: Optional[dict[str, Any]]) -> dict[str, Any]:
|
|
9
|
+
if not extra:
|
|
10
|
+
return mapped
|
|
11
|
+
out = dict(mapped)
|
|
12
|
+
out.update(extra)
|
|
13
|
+
return out
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def split_data(data: Any) -> dict[str, Any]:
|
|
17
|
+
"""httpx: dict 走 data= 表单,str/bytes 走 content=。"""
|
|
18
|
+
if data is None:
|
|
19
|
+
return {}
|
|
20
|
+
if isinstance(data, (dict, list, tuple)):
|
|
21
|
+
return {"data": data}
|
|
22
|
+
return {"content": data}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SyncBackend:
|
|
26
|
+
def request(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def get_cookies(self) -> dict[str, str]:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
def set_cookies(self, cookies: dict[str, str], url: Optional[str] = None) -> None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def raw(self) -> Any:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AsyncBackend:
|
|
44
|
+
async def request(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
async def aclose(self) -> None:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def get_cookies(self) -> dict[str, str]:
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
def set_cookies(self, cookies: dict[str, str], url: Optional[str] = None) -> None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def raw(self) -> Any:
|
|
58
|
+
raise NotImplementedError
|