boring-http 1.2.0__tar.gz → 2.0.1__tar.gz
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.
- {boring_http-1.2.0 → boring_http-2.0.1}/PKG-INFO +178 -34
- {boring_http-1.2.0 → boring_http-2.0.1}/README.md +176 -32
- {boring_http-1.2.0 → boring_http-2.0.1}/pyproject.toml +2 -2
- {boring_http-1.2.0 → boring_http-2.0.1}/pyproject.toml.orig +2 -2
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/__init__.py +7 -7
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/client.py +41 -11
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http2.py +14 -35
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http3.py +29 -31
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/connection.py +85 -53
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/codings.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/cookies.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/errors.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/hpack.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/hpack_tables.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http1.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/message.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/pool.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/py.typed +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/qpack.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/qpack_tables.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/__init__.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/packet.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/protocol.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/recovery.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/streams.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/revocation.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/timing.py +0 -0
- {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/websocket.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: boring-http
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Asyncio HTTP client on BoringSSL: HTTP/1.1, HTTP/2 and HTTP/3 over QUIC v1 and v2, WebSocket on all three, HTTP and SOCKS proxies, headers in your order
|
|
5
5
|
Requires-Dist: boring-ssl2>=1.2.0
|
|
6
|
-
Requires-Dist: ssl2>=
|
|
6
|
+
Requires-Dist: ssl2>=2.0.1
|
|
7
7
|
Requires-Python: >=3.14
|
|
8
8
|
Project-URL: Repository, https://github.com/baobien-dev/boring-http
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -21,11 +21,14 @@ uv add boring-http
|
|
|
21
21
|
```python
|
|
22
22
|
import asyncio
|
|
23
23
|
|
|
24
|
-
from boring_http import Client
|
|
24
|
+
from boring_http import Client, Http2Settings
|
|
25
|
+
from ssl2 import AkamaiFingerprint
|
|
26
|
+
|
|
27
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
async def main() -> None:
|
|
28
|
-
async with Client(headers=[("User-Agent", "boring-http/0.1"), ("Accept", "application/json")]) as client:
|
|
31
|
+
async with Client(http2=http2, headers=[("User-Agent", "boring-http/0.1"), ("Accept", "application/json")]) as client:
|
|
29
32
|
response = await client.get("https://pingly.us.kg/api/all", params={"page": 2})
|
|
30
33
|
print(response.status, response.version, response.content_type) # 200 h2 application/json
|
|
31
34
|
report = response.json()
|
|
@@ -46,11 +49,14 @@ Certificates are checked against the system trust store, or against `trust_store
|
|
|
46
49
|
```python
|
|
47
50
|
import asyncio
|
|
48
51
|
|
|
49
|
-
from boring_http import Client
|
|
52
|
+
from boring_http import Client, Http2Settings
|
|
53
|
+
from ssl2 import AkamaiFingerprint
|
|
54
|
+
|
|
55
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
async def main() -> None:
|
|
53
|
-
async with Client() as client:
|
|
59
|
+
async with Client(http2=http2) as client:
|
|
54
60
|
response = await client.get("https://httpbingo.org/redirect/2")
|
|
55
61
|
print(response.status, response.url.path, [step.status for step in response.history]) # 200 /get [302, 302]
|
|
56
62
|
|
|
@@ -71,11 +77,14 @@ asyncio.run(main())
|
|
|
71
77
|
```python
|
|
72
78
|
import asyncio
|
|
73
79
|
|
|
74
|
-
from boring_http import Client, Timeout, TimeoutError
|
|
80
|
+
from boring_http import Client, Http2Settings, Timeout, TimeoutError
|
|
81
|
+
from ssl2 import AkamaiFingerprint
|
|
82
|
+
|
|
83
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
75
84
|
|
|
76
85
|
|
|
77
86
|
async def main() -> None:
|
|
78
|
-
async with Client(timeout=Timeout(connect=5, read=10, write=10, total=30)) as client:
|
|
87
|
+
async with Client(http2=http2, timeout=Timeout(connect=5, read=10, write=10, total=30)) as client:
|
|
79
88
|
async with client.stream("GET", "https://httpbingo.org/stream-bytes/100000") as response:
|
|
80
89
|
size = sum([len(chunk) async for chunk in response.aiter_bytes()])
|
|
81
90
|
print(response.status, size) # 200 100000
|
|
@@ -95,17 +104,47 @@ A single number sets every phase and the total, so the request gives up after th
|
|
|
95
104
|
|
|
96
105
|
```python
|
|
97
106
|
import asyncio
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
from dataclasses import replace
|
|
108
|
+
|
|
109
|
+
from boring_http import Client, Http2Settings, Http3Settings, Parameter, QuicVersion, Setting, TransportParameters
|
|
110
|
+
from ssl2 import AkamaiFingerprint, PseudoHeader
|
|
111
|
+
|
|
112
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
113
|
+
http3 = Http3Settings(
|
|
114
|
+
settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
|
|
115
|
+
pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
|
|
116
|
+
transport=TransportParameters(
|
|
117
|
+
max_idle_timeout=30000,
|
|
118
|
+
max_udp_payload_size=1472,
|
|
119
|
+
initial_max_data=15728640,
|
|
120
|
+
initial_max_stream_data_bidi_local=6291456,
|
|
121
|
+
initial_max_stream_data_bidi_remote=6291456,
|
|
122
|
+
initial_max_stream_data_uni=6291456,
|
|
123
|
+
initial_max_streams_bidi=100,
|
|
124
|
+
initial_max_streams_uni=100,
|
|
125
|
+
order=(
|
|
126
|
+
Parameter.MAX_IDLE_TIMEOUT,
|
|
127
|
+
Parameter.MAX_UDP_PAYLOAD_SIZE,
|
|
128
|
+
Parameter.INITIAL_MAX_DATA,
|
|
129
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
|
|
130
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
|
|
131
|
+
Parameter.INITIAL_MAX_STREAM_DATA_UNI,
|
|
132
|
+
Parameter.INITIAL_MAX_STREAMS_BIDI,
|
|
133
|
+
Parameter.INITIAL_MAX_STREAMS_UNI,
|
|
134
|
+
Parameter.INITIAL_SOURCE_CONNECTION_ID,
|
|
135
|
+
Parameter.VERSION_INFORMATION,
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
100
139
|
|
|
101
140
|
|
|
102
141
|
async def main() -> None:
|
|
103
|
-
async with Client(versions=("h3", "h2", "h1")) as client:
|
|
142
|
+
async with Client(versions=("h3", "h2", "h1"), http2=http2, http3=http3) as client:
|
|
104
143
|
first = await client.get("https://pingly.us.kg/api/all")
|
|
105
144
|
second = await client.get("https://pingly.us.kg/api/all")
|
|
106
145
|
print(first.version, second.version) # h2 h3, the Alt-Svc header taught the client that this origin speaks HTTP/3
|
|
107
146
|
|
|
108
|
-
async with Client(versions=("h3",), http3=
|
|
147
|
+
async with Client(versions=("h3",), http3=replace(http3, quic_version=QuicVersion.V2)) as client:
|
|
109
148
|
response = await client.get("https://pingly.us.kg/api/all")
|
|
110
149
|
print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
|
|
111
150
|
|
|
@@ -113,30 +152,58 @@ async def main() -> None:
|
|
|
113
152
|
asyncio.run(main())
|
|
114
153
|
```
|
|
115
154
|
|
|
116
|
-
The default is `versions=("h2", "h1")`. With `"h3"` in the list the client switches an origin to QUIC after an `Alt-Svc: h3` header and falls back to TCP when the QUIC route stops answering; `versions=("h3",)` goes straight to QUIC. QUIC v1 is the default; `quic_version=QuicVersion.V2` offers v2 through compatible version negotiation (RFC 9368), so the first packet is still v1 and a server that speaks v2, such as aioquic, ngtcp2, haproxy or haskell quic, switches to it, while every other server simply stays on v1. Retry packets, version negotiation, loss recovery, pacing, flow control, key updates, path MTU probing from 1200 up to 1452 bytes with the don't-fragment flag set, QPACK with
|
|
155
|
+
The default is `versions=("h2", "h1")`. With `"h3"` in the list the client switches an origin to QUIC after an `Alt-Svc: h3` header and falls back to TCP when the QUIC route stops answering; `versions=("h3",)` goes straight to QUIC. QUIC v1 is the default; `quic_version=QuicVersion.V2` offers v2 through compatible version negotiation (RFC 9368), so the first packet is still v1 and a server that speaks v2, such as aioquic, ngtcp2, haproxy or haskell quic, switches to it, while every other server simply stays on v1; v2 needs `Parameter.VERSION_INFORMATION` in `order`. Retry packets, version negotiation, loss recovery, pacing, flow control, key updates, path MTU probing from 1200 up to 1452 bytes with the don't-fragment flag set, QPACK with the dynamic table the SETTINGS allow on the decoding side and 0-RTT are handled; connection migration is not. A second connection to an origin resumes the TLS session and sends GET, HEAD and OPTIONS requests before the handshake has finished, under the limits and SETTINGS the server gave last time; other methods wait for the handshake, a 425 answer is retried after it, and a server that declines the early data gets it again under the 1-RTT keys. `TransportParameters` sends the parameters in `order`, in that order, and refuses a value the peer would never see: one that differs from the RFC's absent value, or a GREASE version, connection options or round trip time, with its parameter missing from `order`. `max_idle_timeout=0` sets no idle timeout. `grease=True` adds a GREASE entry, `grease_version=True` a GREASE version, `shuffle=True` a fresh random order on every connection, and `INITIAL_ROUND_TRIP_TIME` in `order` sends `initial_round_trip_time`, or else the round trip time measured on the last connection to that origin. The control stream carries the SETTINGS of the `Http3Settings`, a GREASE entry and a GREASE frame with `grease=True`, and a PRIORITY_UPDATE for every request that has a `priority` header.
|
|
117
156
|
|
|
118
157
|
## Proxies
|
|
119
158
|
|
|
120
159
|
```python
|
|
121
160
|
import asyncio
|
|
122
161
|
|
|
123
|
-
from boring_http import Client
|
|
124
|
-
from ssl2 import ProxyError
|
|
162
|
+
from boring_http import Client, Http2Settings, Http3Settings, Parameter, Setting, TransportParameters
|
|
163
|
+
from ssl2 import AkamaiFingerprint, ProxyError, PseudoHeader
|
|
164
|
+
|
|
165
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
166
|
+
http3 = Http3Settings(
|
|
167
|
+
settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
|
|
168
|
+
pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
|
|
169
|
+
transport=TransportParameters(
|
|
170
|
+
max_idle_timeout=30000,
|
|
171
|
+
max_udp_payload_size=1472,
|
|
172
|
+
initial_max_data=15728640,
|
|
173
|
+
initial_max_stream_data_bidi_local=6291456,
|
|
174
|
+
initial_max_stream_data_bidi_remote=6291456,
|
|
175
|
+
initial_max_stream_data_uni=6291456,
|
|
176
|
+
initial_max_streams_bidi=100,
|
|
177
|
+
initial_max_streams_uni=100,
|
|
178
|
+
order=(
|
|
179
|
+
Parameter.MAX_IDLE_TIMEOUT,
|
|
180
|
+
Parameter.MAX_UDP_PAYLOAD_SIZE,
|
|
181
|
+
Parameter.INITIAL_MAX_DATA,
|
|
182
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
|
|
183
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
|
|
184
|
+
Parameter.INITIAL_MAX_STREAM_DATA_UNI,
|
|
185
|
+
Parameter.INITIAL_MAX_STREAMS_BIDI,
|
|
186
|
+
Parameter.INITIAL_MAX_STREAMS_UNI,
|
|
187
|
+
Parameter.INITIAL_SOURCE_CONNECTION_ID,
|
|
188
|
+
Parameter.VERSION_INFORMATION,
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
125
192
|
|
|
126
193
|
|
|
127
194
|
async def main() -> None:
|
|
128
|
-
async with Client(proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
|
|
195
|
+
async with Client(http2=http2, proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
|
|
129
196
|
response = await client.get("https://pingly.us.kg/api/all") # SOCKS5 CONNECT, HTTP/2 inside
|
|
130
197
|
print(response.version, response.json()["http_version"]) # h2 HTTP/2.0
|
|
131
198
|
response = await client.get("https://pingly.us.kg/api/all", proxy="http://alice:secret@127.0.0.1:3128") # per request
|
|
132
199
|
print(response.version) # h2
|
|
133
200
|
|
|
134
|
-
async with Client(versions=("h3",), proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
|
|
201
|
+
async with Client(versions=("h3",), http3=http3, proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
|
|
135
202
|
response = await client.get("https://pingly.us.kg/api/all") # SOCKS5 UDP ASSOCIATE, QUIC datagrams through the relay
|
|
136
203
|
print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
|
|
137
204
|
|
|
138
205
|
try:
|
|
139
|
-
async with Client(proxy="socks5h://alice:wrong@127.0.0.1:1080") as client:
|
|
206
|
+
async with Client(http2=http2, proxy="socks5h://alice:wrong@127.0.0.1:1080") as client:
|
|
140
207
|
await client.get("https://pingly.us.kg/api/all")
|
|
141
208
|
except ProxyError as refusal:
|
|
142
209
|
print(refusal) # the SOCKS5 proxy rejected the username and password
|
|
@@ -152,14 +219,16 @@ asyncio.run(main())
|
|
|
152
219
|
```python
|
|
153
220
|
import asyncio
|
|
154
221
|
|
|
155
|
-
from boring_http import
|
|
222
|
+
from boring_http import Client, Http2Settings
|
|
156
223
|
from ssl2 import (
|
|
157
224
|
JA4,
|
|
225
|
+
AkamaiFingerprint,
|
|
158
226
|
CertificateCompressionAlgorithm,
|
|
159
227
|
CipherSuite,
|
|
160
228
|
ClientHelloBuilder,
|
|
161
229
|
ECPointFormat,
|
|
162
230
|
ExtensionType,
|
|
231
|
+
Http2Priority,
|
|
163
232
|
NamedGroup,
|
|
164
233
|
NextProtocol,
|
|
165
234
|
ProtocolVersion,
|
|
@@ -167,7 +236,7 @@ from ssl2 import (
|
|
|
167
236
|
SignatureScheme,
|
|
168
237
|
)
|
|
169
238
|
|
|
170
|
-
|
|
239
|
+
hello = (
|
|
171
240
|
ClientHelloBuilder()
|
|
172
241
|
.supported_versions(ProtocolVersion.TLS_1_3, ProtocolVersion.TLS_1_2)
|
|
173
242
|
.cipher_suites(
|
|
@@ -224,13 +293,17 @@ profile = (
|
|
|
224
293
|
.psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
|
|
225
294
|
.build()
|
|
226
295
|
)
|
|
296
|
+
http2 = Http2Settings(
|
|
297
|
+
AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"),
|
|
298
|
+
headers_priority=Http2Priority(stream_id=0, exclusive=True, dependency=0, weight=256),
|
|
299
|
+
)
|
|
227
300
|
|
|
228
301
|
|
|
229
302
|
async def main() -> None:
|
|
230
|
-
async with Client(tls=
|
|
303
|
+
async with Client(tls=hello, http2=http2) as client:
|
|
231
304
|
report = (await client.get("https://pingly.us.kg/api/all")).json()
|
|
232
|
-
print(report["tls"]["ja4"] == JA4.of(
|
|
233
|
-
print(report["http2"]["akamai_fingerprint"] ==
|
|
305
|
+
print(report["tls"]["ja4"] == JA4.of(hello).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
|
|
306
|
+
print(report["http2"]["akamai_fingerprint"] == http2.fingerprint.text, report["http2"]["akamai_fingerprint"]) # True 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
|
|
234
307
|
|
|
235
308
|
|
|
236
309
|
asyncio.run(main())
|
|
@@ -241,9 +314,8 @@ Over HTTP/3 the same holds, with the ClientHello Chrome 155 sends on QUIC: TLS 1
|
|
|
241
314
|
```python
|
|
242
315
|
import asyncio
|
|
243
316
|
|
|
244
|
-
from boring_http import Client,
|
|
317
|
+
from boring_http import Client, Http3Settings, Parameter, Setting, TransportParameters
|
|
245
318
|
from ssl2 import (
|
|
246
|
-
CHROME_TRUST_ANCHOR_IDS,
|
|
247
319
|
JA4,
|
|
248
320
|
CertificateCompressionAlgorithm,
|
|
249
321
|
CipherSuite,
|
|
@@ -253,11 +325,12 @@ from ssl2 import (
|
|
|
253
325
|
NamedGroup,
|
|
254
326
|
NextProtocol,
|
|
255
327
|
ProtocolVersion,
|
|
328
|
+
PseudoHeader,
|
|
256
329
|
PskKeyExchangeMode,
|
|
257
330
|
SignatureScheme,
|
|
258
331
|
)
|
|
259
332
|
|
|
260
|
-
|
|
333
|
+
quic_hello = (
|
|
261
334
|
ClientHelloBuilder()
|
|
262
335
|
.supported_versions(ProtocolVersion.TLS_1_3)
|
|
263
336
|
.cipher_suites(CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256)
|
|
@@ -293,37 +366,80 @@ quic_profile = (
|
|
|
293
366
|
.application_settings(NextProtocol.H3)
|
|
294
367
|
.certificate_compression(CertificateCompressionAlgorithm.brotli)
|
|
295
368
|
.psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
|
|
296
|
-
.trust_anchor_ids(
|
|
369
|
+
.trust_anchor_ids(
|
|
370
|
+
"44947.2.1", "44947.2.6", "44947.2.13", "44947.2.14", "44947.2.15", "44947.2.18", "44947.2.19", "44947.2.20",
|
|
371
|
+
"52580.200109.1.7", "52580.200109.1.8", "52580.200109.1.9", "52580.200109.1.10", "52580.200109.1.11", "52580.200109.1.12", "52580.200109.1.13", "52580.200109.1.18", "52580.200109.1.19",
|
|
372
|
+
"11129.9.1", "11129.9.4", "11129.9.5", "11129.9.6", "11129.9.7", "11129.9.8", "11129.9.10", "11129.9.11", "11129.9.12", "11129.9.13", "11129.9.15",
|
|
373
|
+
)
|
|
297
374
|
.grease()
|
|
298
375
|
.permute_extensions()
|
|
299
376
|
.build()
|
|
300
377
|
)
|
|
378
|
+
http3 = Http3Settings(
|
|
379
|
+
settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100), (Setting.H3_DATAGRAM, 1)),
|
|
380
|
+
pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
|
|
381
|
+
transport=TransportParameters(
|
|
382
|
+
max_idle_timeout=30000,
|
|
383
|
+
max_udp_payload_size=1472,
|
|
384
|
+
initial_max_data=15728640,
|
|
385
|
+
initial_max_stream_data_bidi_local=6291456,
|
|
386
|
+
initial_max_stream_data_bidi_remote=6291456,
|
|
387
|
+
initial_max_stream_data_uni=6291456,
|
|
388
|
+
initial_max_streams_bidi=100,
|
|
389
|
+
initial_max_streams_uni=103,
|
|
390
|
+
order=(
|
|
391
|
+
Parameter.MAX_IDLE_TIMEOUT,
|
|
392
|
+
Parameter.MAX_UDP_PAYLOAD_SIZE,
|
|
393
|
+
Parameter.INITIAL_MAX_DATA,
|
|
394
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
|
|
395
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
|
|
396
|
+
Parameter.INITIAL_MAX_STREAM_DATA_UNI,
|
|
397
|
+
Parameter.INITIAL_MAX_STREAMS_BIDI,
|
|
398
|
+
Parameter.INITIAL_MAX_STREAMS_UNI,
|
|
399
|
+
Parameter.INITIAL_SOURCE_CONNECTION_ID,
|
|
400
|
+
Parameter.VERSION_INFORMATION,
|
|
401
|
+
Parameter.MAX_DATAGRAM_FRAME_SIZE,
|
|
402
|
+
Parameter.GOOGLE_CONNECTION_OPTIONS,
|
|
403
|
+
Parameter.INITIAL_ROUND_TRIP_TIME,
|
|
404
|
+
),
|
|
405
|
+
max_datagram_frame_size=65536,
|
|
406
|
+
google_connection_options=(b"ORIG",),
|
|
407
|
+
grease=True,
|
|
408
|
+
grease_version=True,
|
|
409
|
+
shuffle=True,
|
|
410
|
+
),
|
|
411
|
+
tls=quic_hello,
|
|
412
|
+
grease=True,
|
|
413
|
+
)
|
|
301
414
|
|
|
302
415
|
|
|
303
416
|
async def main() -> None:
|
|
304
|
-
async with Client(versions=("h3",), http3=
|
|
417
|
+
async with Client(versions=("h3",), http3=http3, headers=[("priority", "u=0, i")]) as client:
|
|
305
418
|
_ = await client.get("https://pingly.us.kg/api/all")
|
|
306
419
|
await client.close() # the next request opens a new connection, which resumes the session
|
|
307
420
|
report = (await client.get("https://pingly.us.kg/api/all")).json()
|
|
308
|
-
print(report["tls"]["ja4"] == JA4.of(
|
|
421
|
+
print(report["tls"]["ja4"] == JA4.of(quic_hello, JA4Transport.QUIC).text, report["tls"]["ja4"]) # True q13d0313h3_55b375c5d22e_b0954bf1abdf
|
|
309
422
|
print(report["http3"]["h3_text"]) # 1:65536;6:262144;7:100;51:1;GREASE|m,a,s,p
|
|
310
423
|
|
|
311
424
|
|
|
312
425
|
asyncio.run(main())
|
|
313
426
|
```
|
|
314
427
|
|
|
315
|
-
`tls` is a `ssl2.ClientHello`; BoringSSL sends it byte for byte or the client refuses it with the reason. `http2` is an `
|
|
428
|
+
`tls` is a `ssl2.ClientHello`; BoringSSL sends it byte for byte or the client refuses it with the reason. `http2` is an `Http2Settings` with the SETTINGS, WINDOW_UPDATE, priorities and pseudo-header order. `http3` is an `Http3Settings` with the HTTP/3 SETTINGS, pseudo-header order, QUIC transport parameters and QUIC version. Neither has a default: `h2` in the ALPN offered over TCP, which is the ClientHello's when `tls` is given and that of `versions` otherwise, needs `http2`, and `h3` in `versions` needs `http3`. A connection that must be HTTP/1.1, such as the TLS hop to an `https` proxy or a WebSocket fallback, offers only `http/1.1` and drops the ALPS extension.
|
|
316
429
|
|
|
317
430
|
## WebSocket
|
|
318
431
|
|
|
319
432
|
```python
|
|
320
433
|
import asyncio
|
|
321
434
|
|
|
322
|
-
from boring_http import Client
|
|
435
|
+
from boring_http import Client, Http2Settings
|
|
436
|
+
from ssl2 import AkamaiFingerprint
|
|
437
|
+
|
|
438
|
+
http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
|
|
323
439
|
|
|
324
440
|
|
|
325
441
|
async def main() -> None:
|
|
326
|
-
async with Client() as client:
|
|
442
|
+
async with Client(http2=http2) as client:
|
|
327
443
|
websocket = await client.websocket("wss://echo.websocket.org/")
|
|
328
444
|
print((await websocket.receive())[:17]) # Request served by
|
|
329
445
|
await websocket.send("hello")
|
|
@@ -344,11 +460,39 @@ Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extend
|
|
|
344
460
|
```python
|
|
345
461
|
import uvloop
|
|
346
462
|
|
|
347
|
-
from boring_http import Client
|
|
463
|
+
from boring_http import Client, Http3Settings, Parameter, Setting, TransportParameters
|
|
464
|
+
from ssl2 import PseudoHeader
|
|
465
|
+
|
|
466
|
+
http3 = Http3Settings(
|
|
467
|
+
settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
|
|
468
|
+
pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
|
|
469
|
+
transport=TransportParameters(
|
|
470
|
+
max_idle_timeout=30000,
|
|
471
|
+
max_udp_payload_size=1472,
|
|
472
|
+
initial_max_data=15728640,
|
|
473
|
+
initial_max_stream_data_bidi_local=6291456,
|
|
474
|
+
initial_max_stream_data_bidi_remote=6291456,
|
|
475
|
+
initial_max_stream_data_uni=6291456,
|
|
476
|
+
initial_max_streams_bidi=100,
|
|
477
|
+
initial_max_streams_uni=100,
|
|
478
|
+
order=(
|
|
479
|
+
Parameter.MAX_IDLE_TIMEOUT,
|
|
480
|
+
Parameter.MAX_UDP_PAYLOAD_SIZE,
|
|
481
|
+
Parameter.INITIAL_MAX_DATA,
|
|
482
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
|
|
483
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
|
|
484
|
+
Parameter.INITIAL_MAX_STREAM_DATA_UNI,
|
|
485
|
+
Parameter.INITIAL_MAX_STREAMS_BIDI,
|
|
486
|
+
Parameter.INITIAL_MAX_STREAMS_UNI,
|
|
487
|
+
Parameter.INITIAL_SOURCE_CONNECTION_ID,
|
|
488
|
+
Parameter.VERSION_INFORMATION,
|
|
489
|
+
),
|
|
490
|
+
),
|
|
491
|
+
)
|
|
348
492
|
|
|
349
493
|
|
|
350
494
|
async def main() -> None:
|
|
351
|
-
async with Client(versions=("h3",)) as client:
|
|
495
|
+
async with Client(versions=("h3",), http3=http3) as client:
|
|
352
496
|
print((await client.get("https://pingly.us.kg/api/all")).version) # h3
|
|
353
497
|
|
|
354
498
|
|