boring-http 1.2.0__tar.gz → 2.0.0__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.
Files changed (28) hide show
  1. {boring_http-1.2.0 → boring_http-2.0.0}/PKG-INFO +177 -33
  2. {boring_http-1.2.0 → boring_http-2.0.0}/README.md +176 -32
  3. {boring_http-1.2.0 → boring_http-2.0.0}/pyproject.toml +1 -1
  4. {boring_http-1.2.0 → boring_http-2.0.0}/pyproject.toml.orig +1 -1
  5. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/__init__.py +7 -7
  6. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/client.py +18 -8
  7. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/http2.py +14 -35
  8. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/http3.py +18 -30
  9. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/connection.py +19 -35
  10. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/codings.py +0 -0
  11. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/cookies.py +0 -0
  12. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/errors.py +0 -0
  13. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/hpack.py +0 -0
  14. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/hpack_tables.py +0 -0
  15. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/http1.py +0 -0
  16. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/message.py +0 -0
  17. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/pool.py +0 -0
  18. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/py.typed +0 -0
  19. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/qpack.py +0 -0
  20. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/qpack_tables.py +0 -0
  21. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/__init__.py +0 -0
  22. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/packet.py +0 -0
  23. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/protocol.py +0 -0
  24. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/recovery.py +0 -0
  25. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/quic/streams.py +0 -0
  26. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/revocation.py +0 -0
  27. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/timing.py +0 -0
  28. {boring_http-1.2.0 → boring_http-2.0.0}/src/boring_http/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boring-http
3
- Version: 1.2.0
3
+ Version: 2.0.0
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
6
  Requires-Dist: ssl2>=1.2.0
@@ -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
- from boring_http import Client, Http3Profile, QuicVersion
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=Http3Profile(quic_version=QuicVersion.V2)) as client:
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 a 64 KiB dynamic table 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. The transport parameters are Chrome 155's set, in a fresh random order with a GREASE entry, a GREASE version, Chrome's connection options and, on a resumed connection, the round trip time measured last time; `TransportParameters` on the `Http3Profile` changes any of it. The control stream carries SETTINGS with a GREASE entry, a GREASE frame, and a PRIORITY_UPDATE for every request that has a `priority` header, as Chrome sends them.
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. 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; `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 the round trip time measured last time on a resumed connection. 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 CHROME_HTTP2, Client
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
- profile = (
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=profile, http2=CHROME_HTTP2) as client:
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(profile).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
233
- print(report["http2"]["akamai_fingerprint"] == CHROME_HTTP2.fingerprint.text, report["http2"]["akamai_fingerprint"]) # True 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
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, Http3Profile
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
- quic_profile = (
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(*CHROME_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=Http3Profile(tls=quic_profile), headers=[("priority", "u=0, i")]) as client:
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(quic_profile, JA4Transport.QUIC).text, report["tls"]["ja4"]) # True q13d0313h3_55b375c5d22e_b0954bf1abdf
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 `Http2Profile` with the SETTINGS, WINDOW_UPDATE, priorities and pseudo-header order; `CHROME_HTTP2` is the default. `http3` is an `Http3Profile` with the HTTP/3 SETTINGS, QUIC transport parameters and QUIC version; `CHROME_HTTP3` is the default.
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 `versions` or in the ClientHello needs `http2`, and `h3` in `versions` needs `http3`.
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
 
@@ -11,11 +11,14 @@ uv add boring-http
11
11
  ```python
12
12
  import asyncio
13
13
 
14
- from boring_http import Client
14
+ from boring_http import Client, Http2Settings
15
+ from ssl2 import AkamaiFingerprint
16
+
17
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
15
18
 
16
19
 
17
20
  async def main() -> None:
18
- async with Client(headers=[("User-Agent", "boring-http/0.1"), ("Accept", "application/json")]) as client:
21
+ async with Client(http2=http2, headers=[("User-Agent", "boring-http/0.1"), ("Accept", "application/json")]) as client:
19
22
  response = await client.get("https://pingly.us.kg/api/all", params={"page": 2})
20
23
  print(response.status, response.version, response.content_type) # 200 h2 application/json
21
24
  report = response.json()
@@ -36,11 +39,14 @@ Certificates are checked against the system trust store, or against `trust_store
36
39
  ```python
37
40
  import asyncio
38
41
 
39
- from boring_http import Client
42
+ from boring_http import Client, Http2Settings
43
+ from ssl2 import AkamaiFingerprint
44
+
45
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
40
46
 
41
47
 
42
48
  async def main() -> None:
43
- async with Client() as client:
49
+ async with Client(http2=http2) as client:
44
50
  response = await client.get("https://httpbingo.org/redirect/2")
45
51
  print(response.status, response.url.path, [step.status for step in response.history]) # 200 /get [302, 302]
46
52
 
@@ -61,11 +67,14 @@ asyncio.run(main())
61
67
  ```python
62
68
  import asyncio
63
69
 
64
- from boring_http import Client, Timeout, TimeoutError
70
+ from boring_http import Client, Http2Settings, Timeout, TimeoutError
71
+ from ssl2 import AkamaiFingerprint
72
+
73
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
65
74
 
66
75
 
67
76
  async def main() -> None:
68
- async with Client(timeout=Timeout(connect=5, read=10, write=10, total=30)) as client:
77
+ async with Client(http2=http2, timeout=Timeout(connect=5, read=10, write=10, total=30)) as client:
69
78
  async with client.stream("GET", "https://httpbingo.org/stream-bytes/100000") as response:
70
79
  size = sum([len(chunk) async for chunk in response.aiter_bytes()])
71
80
  print(response.status, size) # 200 100000
@@ -85,17 +94,47 @@ A single number sets every phase and the total, so the request gives up after th
85
94
 
86
95
  ```python
87
96
  import asyncio
88
-
89
- from boring_http import Client, Http3Profile, QuicVersion
97
+ from dataclasses import replace
98
+
99
+ from boring_http import Client, Http2Settings, Http3Settings, Parameter, QuicVersion, Setting, TransportParameters
100
+ from ssl2 import AkamaiFingerprint, PseudoHeader
101
+
102
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
103
+ http3 = Http3Settings(
104
+ settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
105
+ pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
106
+ transport=TransportParameters(
107
+ max_idle_timeout=30000,
108
+ max_udp_payload_size=1472,
109
+ initial_max_data=15728640,
110
+ initial_max_stream_data_bidi_local=6291456,
111
+ initial_max_stream_data_bidi_remote=6291456,
112
+ initial_max_stream_data_uni=6291456,
113
+ initial_max_streams_bidi=100,
114
+ initial_max_streams_uni=100,
115
+ order=(
116
+ Parameter.MAX_IDLE_TIMEOUT,
117
+ Parameter.MAX_UDP_PAYLOAD_SIZE,
118
+ Parameter.INITIAL_MAX_DATA,
119
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
120
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
121
+ Parameter.INITIAL_MAX_STREAM_DATA_UNI,
122
+ Parameter.INITIAL_MAX_STREAMS_BIDI,
123
+ Parameter.INITIAL_MAX_STREAMS_UNI,
124
+ Parameter.INITIAL_SOURCE_CONNECTION_ID,
125
+ Parameter.VERSION_INFORMATION,
126
+ ),
127
+ ),
128
+ )
90
129
 
91
130
 
92
131
  async def main() -> None:
93
- async with Client(versions=("h3", "h2", "h1")) as client:
132
+ async with Client(versions=("h3", "h2", "h1"), http2=http2, http3=http3) as client:
94
133
  first = await client.get("https://pingly.us.kg/api/all")
95
134
  second = await client.get("https://pingly.us.kg/api/all")
96
135
  print(first.version, second.version) # h2 h3, the Alt-Svc header taught the client that this origin speaks HTTP/3
97
136
 
98
- async with Client(versions=("h3",), http3=Http3Profile(quic_version=QuicVersion.V2)) as client:
137
+ async with Client(versions=("h3",), http3=replace(http3, quic_version=QuicVersion.V2)) as client:
99
138
  response = await client.get("https://pingly.us.kg/api/all")
100
139
  print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
101
140
 
@@ -103,30 +142,58 @@ async def main() -> None:
103
142
  asyncio.run(main())
104
143
  ```
105
144
 
106
- 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 a 64 KiB dynamic table 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. The transport parameters are Chrome 155's set, in a fresh random order with a GREASE entry, a GREASE version, Chrome's connection options and, on a resumed connection, the round trip time measured last time; `TransportParameters` on the `Http3Profile` changes any of it. The control stream carries SETTINGS with a GREASE entry, a GREASE frame, and a PRIORITY_UPDATE for every request that has a `priority` header, as Chrome sends them.
145
+ 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 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; `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 the round trip time measured last time on a resumed connection. 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.
107
146
 
108
147
  ## Proxies
109
148
 
110
149
  ```python
111
150
  import asyncio
112
151
 
113
- from boring_http import Client
114
- from ssl2 import ProxyError
152
+ from boring_http import Client, Http2Settings, Http3Settings, Parameter, Setting, TransportParameters
153
+ from ssl2 import AkamaiFingerprint, ProxyError, PseudoHeader
154
+
155
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
156
+ http3 = Http3Settings(
157
+ settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
158
+ pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
159
+ transport=TransportParameters(
160
+ max_idle_timeout=30000,
161
+ max_udp_payload_size=1472,
162
+ initial_max_data=15728640,
163
+ initial_max_stream_data_bidi_local=6291456,
164
+ initial_max_stream_data_bidi_remote=6291456,
165
+ initial_max_stream_data_uni=6291456,
166
+ initial_max_streams_bidi=100,
167
+ initial_max_streams_uni=100,
168
+ order=(
169
+ Parameter.MAX_IDLE_TIMEOUT,
170
+ Parameter.MAX_UDP_PAYLOAD_SIZE,
171
+ Parameter.INITIAL_MAX_DATA,
172
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
173
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
174
+ Parameter.INITIAL_MAX_STREAM_DATA_UNI,
175
+ Parameter.INITIAL_MAX_STREAMS_BIDI,
176
+ Parameter.INITIAL_MAX_STREAMS_UNI,
177
+ Parameter.INITIAL_SOURCE_CONNECTION_ID,
178
+ Parameter.VERSION_INFORMATION,
179
+ ),
180
+ ),
181
+ )
115
182
 
116
183
 
117
184
  async def main() -> None:
118
- async with Client(proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
185
+ async with Client(http2=http2, proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
119
186
  response = await client.get("https://pingly.us.kg/api/all") # SOCKS5 CONNECT, HTTP/2 inside
120
187
  print(response.version, response.json()["http_version"]) # h2 HTTP/2.0
121
188
  response = await client.get("https://pingly.us.kg/api/all", proxy="http://alice:secret@127.0.0.1:3128") # per request
122
189
  print(response.version) # h2
123
190
 
124
- async with Client(versions=("h3",), proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
191
+ async with Client(versions=("h3",), http3=http3, proxy="socks5h://alice:secret@127.0.0.1:1080") as client:
125
192
  response = await client.get("https://pingly.us.kg/api/all") # SOCKS5 UDP ASSOCIATE, QUIC datagrams through the relay
126
193
  print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
127
194
 
128
195
  try:
129
- async with Client(proxy="socks5h://alice:wrong@127.0.0.1:1080") as client:
196
+ async with Client(http2=http2, proxy="socks5h://alice:wrong@127.0.0.1:1080") as client:
130
197
  await client.get("https://pingly.us.kg/api/all")
131
198
  except ProxyError as refusal:
132
199
  print(refusal) # the SOCKS5 proxy rejected the username and password
@@ -142,14 +209,16 @@ asyncio.run(main())
142
209
  ```python
143
210
  import asyncio
144
211
 
145
- from boring_http import CHROME_HTTP2, Client
212
+ from boring_http import Client, Http2Settings
146
213
  from ssl2 import (
147
214
  JA4,
215
+ AkamaiFingerprint,
148
216
  CertificateCompressionAlgorithm,
149
217
  CipherSuite,
150
218
  ClientHelloBuilder,
151
219
  ECPointFormat,
152
220
  ExtensionType,
221
+ Http2Priority,
153
222
  NamedGroup,
154
223
  NextProtocol,
155
224
  ProtocolVersion,
@@ -157,7 +226,7 @@ from ssl2 import (
157
226
  SignatureScheme,
158
227
  )
159
228
 
160
- profile = (
229
+ hello = (
161
230
  ClientHelloBuilder()
162
231
  .supported_versions(ProtocolVersion.TLS_1_3, ProtocolVersion.TLS_1_2)
163
232
  .cipher_suites(
@@ -214,13 +283,17 @@ profile = (
214
283
  .psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
215
284
  .build()
216
285
  )
286
+ http2 = Http2Settings(
287
+ AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"),
288
+ headers_priority=Http2Priority(stream_id=0, exclusive=True, dependency=0, weight=256),
289
+ )
217
290
 
218
291
 
219
292
  async def main() -> None:
220
- async with Client(tls=profile, http2=CHROME_HTTP2) as client:
293
+ async with Client(tls=hello, http2=http2) as client:
221
294
  report = (await client.get("https://pingly.us.kg/api/all")).json()
222
- print(report["tls"]["ja4"] == JA4.of(profile).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
223
- print(report["http2"]["akamai_fingerprint"] == CHROME_HTTP2.fingerprint.text, report["http2"]["akamai_fingerprint"]) # True 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
295
+ print(report["tls"]["ja4"] == JA4.of(hello).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
296
+ 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
224
297
 
225
298
 
226
299
  asyncio.run(main())
@@ -231,9 +304,8 @@ Over HTTP/3 the same holds, with the ClientHello Chrome 155 sends on QUIC: TLS 1
231
304
  ```python
232
305
  import asyncio
233
306
 
234
- from boring_http import Client, Http3Profile
307
+ from boring_http import Client, Http3Settings, Parameter, Setting, TransportParameters
235
308
  from ssl2 import (
236
- CHROME_TRUST_ANCHOR_IDS,
237
309
  JA4,
238
310
  CertificateCompressionAlgorithm,
239
311
  CipherSuite,
@@ -243,11 +315,12 @@ from ssl2 import (
243
315
  NamedGroup,
244
316
  NextProtocol,
245
317
  ProtocolVersion,
318
+ PseudoHeader,
246
319
  PskKeyExchangeMode,
247
320
  SignatureScheme,
248
321
  )
249
322
 
250
- quic_profile = (
323
+ quic_hello = (
251
324
  ClientHelloBuilder()
252
325
  .supported_versions(ProtocolVersion.TLS_1_3)
253
326
  .cipher_suites(CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256)
@@ -283,37 +356,80 @@ quic_profile = (
283
356
  .application_settings(NextProtocol.H3)
284
357
  .certificate_compression(CertificateCompressionAlgorithm.brotli)
285
358
  .psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
286
- .trust_anchor_ids(*CHROME_TRUST_ANCHOR_IDS)
359
+ .trust_anchor_ids(
360
+ "44947.2.1", "44947.2.6", "44947.2.13", "44947.2.14", "44947.2.15", "44947.2.18", "44947.2.19", "44947.2.20",
361
+ "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",
362
+ "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",
363
+ )
287
364
  .grease()
288
365
  .permute_extensions()
289
366
  .build()
290
367
  )
368
+ http3 = Http3Settings(
369
+ settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100), (Setting.H3_DATAGRAM, 1)),
370
+ pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
371
+ transport=TransportParameters(
372
+ max_idle_timeout=30000,
373
+ max_udp_payload_size=1472,
374
+ initial_max_data=15728640,
375
+ initial_max_stream_data_bidi_local=6291456,
376
+ initial_max_stream_data_bidi_remote=6291456,
377
+ initial_max_stream_data_uni=6291456,
378
+ initial_max_streams_bidi=100,
379
+ initial_max_streams_uni=103,
380
+ order=(
381
+ Parameter.MAX_IDLE_TIMEOUT,
382
+ Parameter.MAX_UDP_PAYLOAD_SIZE,
383
+ Parameter.INITIAL_MAX_DATA,
384
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
385
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
386
+ Parameter.INITIAL_MAX_STREAM_DATA_UNI,
387
+ Parameter.INITIAL_MAX_STREAMS_BIDI,
388
+ Parameter.INITIAL_MAX_STREAMS_UNI,
389
+ Parameter.INITIAL_SOURCE_CONNECTION_ID,
390
+ Parameter.VERSION_INFORMATION,
391
+ Parameter.MAX_DATAGRAM_FRAME_SIZE,
392
+ Parameter.GOOGLE_CONNECTION_OPTIONS,
393
+ Parameter.INITIAL_ROUND_TRIP_TIME,
394
+ ),
395
+ max_datagram_frame_size=65536,
396
+ google_connection_options=(b"ORIG",),
397
+ grease=True,
398
+ grease_version=True,
399
+ shuffle=True,
400
+ ),
401
+ tls=quic_hello,
402
+ grease=True,
403
+ )
291
404
 
292
405
 
293
406
  async def main() -> None:
294
- async with Client(versions=("h3",), http3=Http3Profile(tls=quic_profile), headers=[("priority", "u=0, i")]) as client:
407
+ async with Client(versions=("h3",), http3=http3, headers=[("priority", "u=0, i")]) as client:
295
408
  _ = await client.get("https://pingly.us.kg/api/all")
296
409
  await client.close() # the next request opens a new connection, which resumes the session
297
410
  report = (await client.get("https://pingly.us.kg/api/all")).json()
298
- print(report["tls"]["ja4"] == JA4.of(quic_profile, JA4Transport.QUIC).text, report["tls"]["ja4"]) # True q13d0313h3_55b375c5d22e_b0954bf1abdf
411
+ print(report["tls"]["ja4"] == JA4.of(quic_hello, JA4Transport.QUIC).text, report["tls"]["ja4"]) # True q13d0313h3_55b375c5d22e_b0954bf1abdf
299
412
  print(report["http3"]["h3_text"]) # 1:65536;6:262144;7:100;51:1;GREASE|m,a,s,p
300
413
 
301
414
 
302
415
  asyncio.run(main())
303
416
  ```
304
417
 
305
- `tls` is a `ssl2.ClientHello`; BoringSSL sends it byte for byte or the client refuses it with the reason. `http2` is an `Http2Profile` with the SETTINGS, WINDOW_UPDATE, priorities and pseudo-header order; `CHROME_HTTP2` is the default. `http3` is an `Http3Profile` with the HTTP/3 SETTINGS, QUIC transport parameters and QUIC version; `CHROME_HTTP3` is the default.
418
+ `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 `versions` or in the ClientHello needs `http2`, and `h3` in `versions` needs `http3`.
306
419
 
307
420
  ## WebSocket
308
421
 
309
422
  ```python
310
423
  import asyncio
311
424
 
312
- from boring_http import Client
425
+ from boring_http import Client, Http2Settings
426
+ from ssl2 import AkamaiFingerprint
427
+
428
+ http2 = Http2Settings(AkamaiFingerprint.parse("1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"))
313
429
 
314
430
 
315
431
  async def main() -> None:
316
- async with Client() as client:
432
+ async with Client(http2=http2) as client:
317
433
  websocket = await client.websocket("wss://echo.websocket.org/")
318
434
  print((await websocket.receive())[:17]) # Request served by
319
435
  await websocket.send("hello")
@@ -334,11 +450,39 @@ Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extend
334
450
  ```python
335
451
  import uvloop
336
452
 
337
- from boring_http import Client
453
+ from boring_http import Client, Http3Settings, Parameter, Setting, TransportParameters
454
+ from ssl2 import PseudoHeader
455
+
456
+ http3 = Http3Settings(
457
+ settings=((Setting.QPACK_MAX_TABLE_CAPACITY, 65536), (Setting.MAX_FIELD_SECTION_SIZE, 262144), (Setting.QPACK_BLOCKED_STREAMS, 100)),
458
+ pseudo_header_order=(PseudoHeader.METHOD, PseudoHeader.AUTHORITY, PseudoHeader.SCHEME, PseudoHeader.PATH),
459
+ transport=TransportParameters(
460
+ max_idle_timeout=30000,
461
+ max_udp_payload_size=1472,
462
+ initial_max_data=15728640,
463
+ initial_max_stream_data_bidi_local=6291456,
464
+ initial_max_stream_data_bidi_remote=6291456,
465
+ initial_max_stream_data_uni=6291456,
466
+ initial_max_streams_bidi=100,
467
+ initial_max_streams_uni=100,
468
+ order=(
469
+ Parameter.MAX_IDLE_TIMEOUT,
470
+ Parameter.MAX_UDP_PAYLOAD_SIZE,
471
+ Parameter.INITIAL_MAX_DATA,
472
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
473
+ Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
474
+ Parameter.INITIAL_MAX_STREAM_DATA_UNI,
475
+ Parameter.INITIAL_MAX_STREAMS_BIDI,
476
+ Parameter.INITIAL_MAX_STREAMS_UNI,
477
+ Parameter.INITIAL_SOURCE_CONNECTION_ID,
478
+ Parameter.VERSION_INFORMATION,
479
+ ),
480
+ ),
481
+ )
338
482
 
339
483
 
340
484
  async def main() -> None:
341
- async with Client(versions=("h3",)) as client:
485
+ async with Client(versions=("h3",), http3=http3) as client:
342
486
  print((await client.get("https://pingly.us.kg/api/all")).version) # h3
343
487
 
344
488
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boring-http"
3
- version = "1.2.0"
3
+ version = "2.0.0"
4
4
  description = "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
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boring-http"
3
- version = "1.2.0"
3
+ version = "2.0.0"
4
4
  description = "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
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -14,8 +14,8 @@ from boring_http.errors import (
14
14
  TooManyRedirects,
15
15
  WebSocketClosed,
16
16
  )
17
- from boring_http.http2 import CHROME_HTTP2, Http2Profile
18
- from boring_http.http3 import CHROME_HTTP3, Http3Profile
17
+ from boring_http.http2 import Http2Settings
18
+ from boring_http.http3 import Http3Settings, Setting
19
19
  from boring_http.message import (
20
20
  URL,
21
21
  Body,
@@ -25,14 +25,12 @@ from boring_http.message import (
25
25
  Request,
26
26
  Response,
27
27
  )
28
- from boring_http.quic.connection import TransportParameters
28
+ from boring_http.quic.connection import Parameter, TransportParameters
29
29
  from boring_http.revocation import Revocation
30
30
  from boring_http.timing import Timeout
31
31
  from boring_http.websocket import WebSocket
32
32
 
33
33
  __all__ = (
34
- "CHROME_HTTP2",
35
- "CHROME_HTTP3",
36
34
  "URL",
37
35
  "Body",
38
36
  "Client",
@@ -43,8 +41,9 @@ __all__ = (
43
41
  "HTTPStatusError",
44
42
  "HTTPVersion",
45
43
  "Headers",
46
- "Http2Profile",
47
- "Http3Profile",
44
+ "Http2Settings",
45
+ "Http3Settings",
46
+ "Parameter",
48
47
  "ProtocolError",
49
48
  "QuicVersion",
50
49
  "RemoteDisconnected",
@@ -52,6 +51,7 @@ __all__ = (
52
51
  "Response",
53
52
  "Revocation",
54
53
  "RevocationError",
54
+ "Setting",
55
55
  "StreamRefused",
56
56
  "StreamResetError",
57
57
  "Timeout",
@@ -35,8 +35,8 @@ from boring_http.errors import (
35
35
  TooManyRedirects,
36
36
  )
37
37
  from boring_http.http1 import Http1Connection
38
- from boring_http.http2 import CHROME_HTTP2, Http2Connection, Http2Profile
39
- from boring_http.http3 import CHROME_HTTP3, Http3Connection, Http3Profile
38
+ from boring_http.http2 import Http2Connection, Http2Settings
39
+ from boring_http.http3 import Http3Connection, Http3Settings
40
40
  from boring_http.message import (
41
41
  URL,
42
42
  Body,
@@ -94,8 +94,8 @@ class Client:
94
94
  timeout: float | Timeout | None = DEFAULT_TIMEOUT,
95
95
  proxy: Proxy | str | None = None,
96
96
  tls: ClientHello | None = None,
97
- http2: Http2Profile = CHROME_HTTP2,
98
- http3: Http3Profile = CHROME_HTTP3,
97
+ http2: Http2Settings | None = None,
98
+ http3: Http3Settings | None = None,
99
99
  versions: Sequence[HTTPVersion | str] = (
100
100
  HTTPVersion.HTTP_2,
101
101
  HTTPVersion.HTTP_1,
@@ -119,6 +119,13 @@ class Client:
119
119
  self._http3 = http3
120
120
  self._alt_svc: dict[tuple[str, int], float] = {}
121
121
  self._versions = tuple(HTTPVersion(version) for version in versions)
122
+ if http2 is None and (
123
+ HTTPVersion.HTTP_2 in self._versions
124
+ or (tls is not None and NextProtocol.H2.value in tls.application_protocols)
125
+ ):
126
+ raise ValueError("h2 is offered, so http2= needs an Http2Settings")
127
+ if http3 is None and HTTPVersion.HTTP_3 in self._versions:
128
+ raise ValueError("h3 is in versions, so http3= needs an Http3Settings")
122
129
  self._trust_store = trust_store
123
130
  self._verify = verify
124
131
  self._revocations = Revocations(revocation, self._fetch_crl)
@@ -418,7 +425,7 @@ class Client:
418
425
  if protocols not in self._contexts:
419
426
  hello = (
420
427
  self._http3.tls
421
- if protocols == (NextProtocol.H3,)
428
+ if protocols == (NextProtocol.H3,) and self._http3 is not None
422
429
  else self._adapted(protocols)
423
430
  )
424
431
  if hello is None:
@@ -451,7 +458,7 @@ class Client:
451
458
  ) -> ClientHello | None:
452
459
  if self._tls is None or protocols is None:
453
460
  return self._tls
454
- # an HTTP/1.1-only connection keeps the profile but can offer only its own protocol
461
+ # an HTTP/1.1-only connection keeps the ClientHello but can offer only its own protocol
455
462
  names = tuple(protocol.value for protocol in protocols)
456
463
  return replace(
457
464
  self._tls,
@@ -483,7 +490,7 @@ class Client:
483
490
  http1_only: bool = False,
484
491
  ) -> Connection:
485
492
  try:
486
- if quic:
493
+ if quic and self._http3 is not None:
487
494
  memory = self._memory.setdefault(url.address, OriginMemory())
488
495
  client = await QuicClient.connect(
489
496
  self._context((NextProtocol.H3,)),
@@ -511,7 +518,10 @@ class Client:
511
518
  proxy=proxy,
512
519
  )
513
520
  await self._checked(stream, url.authority)
514
- if stream.negotiated_protocol() is NextProtocol.H2:
521
+ if (
522
+ stream.negotiated_protocol() is NextProtocol.H2
523
+ and self._http2 is not None
524
+ ):
515
525
  connection = Http2Connection(stream, self._http2)
516
526
  await connection.start()
517
527
  return connection
@@ -8,7 +8,6 @@ from ssl2 import (
8
8
  AkamaiFingerprint,
9
9
  AsyncTransport,
10
10
  Http2Priority,
11
- Http2Setting,
12
11
  Http2SettingIdentifier,
13
12
  PseudoHeader,
14
13
  )
@@ -78,7 +77,7 @@ class ErrorCode(IntEnum):
78
77
 
79
78
 
80
79
  @dataclass(frozen=True, slots=True)
81
- class Http2Profile:
80
+ class Http2Settings:
82
81
  fingerprint: AkamaiFingerprint
83
82
  headers_priority: Http2Priority | None = None
84
83
 
@@ -93,29 +92,6 @@ class Http2Profile:
93
92
  )
94
93
 
95
94
 
96
- CHROME_HTTP2 = Http2Profile(
97
- AkamaiFingerprint(
98
- settings=(
99
- Http2Setting(Http2SettingIdentifier.HEADER_TABLE_SIZE, 65536),
100
- Http2Setting(Http2SettingIdentifier.ENABLE_PUSH, 0),
101
- Http2Setting(Http2SettingIdentifier.INITIAL_WINDOW_SIZE, 6291456),
102
- Http2Setting(Http2SettingIdentifier.MAX_HEADER_LIST_SIZE, 262144),
103
- ),
104
- window_update=15663105,
105
- priorities=(),
106
- pseudo_header_order=(
107
- PseudoHeader.METHOD,
108
- PseudoHeader.AUTHORITY,
109
- PseudoHeader.SCHEME,
110
- PseudoHeader.PATH,
111
- ),
112
- ),
113
- headers_priority=Http2Priority(
114
- stream_id=0, exclusive=True, dependency=0, weight=256
115
- ),
116
- )
117
-
118
-
119
95
  class Http2Stream:
120
96
  def __init__(self, identifier: int, send_window: int) -> None:
121
97
  self.identifier = identifier
@@ -156,15 +132,15 @@ class Http2Stream:
156
132
 
157
133
 
158
134
  class Http2Connection:
159
- def __init__(self, transport: AsyncTransport, profile: Http2Profile) -> None:
135
+ def __init__(self, transport: AsyncTransport, settings: Http2Settings) -> None:
160
136
  self._transport = transport
161
- self._profile = profile
137
+ self._local_settings = settings
162
138
  self._encoder = Encoder()
163
139
  self._decoder = Decoder(
164
- profile.setting(
140
+ settings.setting(
165
141
  Http2SettingIdentifier.HEADER_TABLE_SIZE, DEFAULT_TABLE_SIZE
166
142
  ),
167
- profile.setting(
143
+ settings.setting(
168
144
  Http2SettingIdentifier.MAX_HEADER_LIST_SIZE, DEFAULT_HEADER_LIST_SIZE
169
145
  ),
170
146
  )
@@ -177,10 +153,10 @@ class Http2Connection:
177
153
  self._peer_frame_size = DEFAULT_FRAME_SIZE
178
154
  self._peer_window = DEFAULT_WINDOW
179
155
  self._peer_max_streams = 2**31
180
- self._local_window = profile.setting(
156
+ self._local_window = settings.setting(
181
157
  Http2SettingIdentifier.INITIAL_WINDOW_SIZE, DEFAULT_WINDOW
182
158
  )
183
- self._connection_window = DEFAULT_WINDOW + profile.fingerprint.window_update
159
+ self._connection_window = DEFAULT_WINDOW + settings.fingerprint.window_update
184
160
  self._connection_received = 0
185
161
  self._goaway: int | None = None
186
162
  self._reader: asyncio.Task[None] | None = None
@@ -208,7 +184,7 @@ class Http2Connection:
208
184
  pass
209
185
 
210
186
  async def start(self) -> None:
211
- fingerprint = self._profile.fingerprint
187
+ fingerprint = self._local_settings.fingerprint
212
188
  settings = b"".join(
213
189
  struct.pack("!HI", int(setting.identifier), setting.value)
214
190
  for setting in fingerprint.settings
@@ -241,9 +217,11 @@ class Http2Connection:
241
217
  Flag.END_STREAM if request.body is None else Flag(0)
242
218
  )
243
219
  payload = block
244
- if self._profile.headers_priority is not None:
220
+ if self._local_settings.headers_priority is not None:
245
221
  flags |= Flag.PRIORITY
246
- payload = self._priority_bytes(self._profile.headers_priority) + block
222
+ payload = (
223
+ self._priority_bytes(self._local_settings.headers_priority) + block
224
+ )
247
225
  try:
248
226
  async with deadline.phase("write"):
249
227
  await self._write_headers(stream.identifier, payload, flags)
@@ -359,7 +337,8 @@ class Http2Connection:
359
337
  PseudoHeader.PATH: (b":path", request.url.target.encode()),
360
338
  }
361
339
  fields = [
362
- pseudo[header] for header in self._profile.fingerprint.pseudo_header_order
340
+ pseudo[header]
341
+ for header in self._local_settings.fingerprint.pseudo_header_order
363
342
  ]
364
343
  if protocol is not None:
365
344
  fields.insert(1, (b":protocol", protocol))
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import secrets
3
3
  from collections.abc import AsyncGenerator, Callable
4
- from dataclasses import dataclass, field
4
+ from dataclasses import dataclass
5
5
  from enum import IntEnum
6
6
 
7
7
  from boring_ssl2 import QuicVersion
@@ -68,32 +68,20 @@ class ErrorCode(IntEnum):
68
68
 
69
69
 
70
70
  @dataclass(frozen=True, slots=True)
71
- class Http3Profile:
72
- settings: tuple[tuple[int, int], ...] = (
73
- (Setting.QPACK_MAX_TABLE_CAPACITY, 65536),
74
- (Setting.MAX_FIELD_SECTION_SIZE, 262144),
75
- (Setting.QPACK_BLOCKED_STREAMS, 100),
76
- (Setting.H3_DATAGRAM, 1),
77
- )
71
+ class Http3Settings:
72
+ settings: tuple[tuple[int, int], ...]
73
+ pseudo_header_order: tuple[PseudoHeader, ...]
74
+ transport: TransportParameters
75
+ quic_version: QuicVersion = QuicVersion.V1
76
+ tls: ClientHello | None = None
77
+ grease: bool = False
78
78
 
79
79
  def setting(self, identifier: Setting, default: int) -> int:
80
80
  return next(
81
81
  (value for key, value in self.settings if key == identifier), default
82
82
  )
83
83
 
84
- pseudo_header_order: tuple[PseudoHeader, ...] = (
85
- PseudoHeader.METHOD,
86
- PseudoHeader.AUTHORITY,
87
- PseudoHeader.SCHEME,
88
- PseudoHeader.PATH,
89
- )
90
- transport: TransportParameters = field(default_factory=TransportParameters)
91
- quic_version: QuicVersion = QuicVersion.V1
92
- tls: ClientHello | None = None
93
- grease: bool = True
94
-
95
84
 
96
- CHROME_HTTP3 = Http3Profile()
97
85
  # RFC 8470 section 5.2: the requests a client may send before the handshake, since a server may see them twice
98
86
  SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
99
87
 
@@ -152,12 +140,12 @@ class Http3Connection:
152
140
  def __init__(
153
141
  self,
154
142
  quic: QuicClient,
155
- profile: Http3Profile,
143
+ settings: Http3Settings,
156
144
  memory: OriginMemory | None = None,
157
145
  ) -> None:
158
146
  self._quic = quic
159
- self._profile = profile
160
- self._qpack = Qpack(profile.setting(Setting.QPACK_MAX_TABLE_CAPACITY, 0))
147
+ self._local_settings = settings
148
+ self._qpack = Qpack(settings.setting(Setting.QPACK_MAX_TABLE_CAPACITY, 0))
161
149
  self._control_stream = -1
162
150
  self._decoder_stream = -1
163
151
  self._peer_settings: dict[int, int] = {}
@@ -197,9 +185,9 @@ class Http3Connection:
197
185
  writer = Writer()
198
186
  writer.varint(StreamType.CONTROL)
199
187
  settings = Writer()
200
- entries = list(self._profile.settings)
201
- if self._profile.grease:
202
- # RFC 9114 section 7.2.4.1: a reserved identifier the peer must ignore, placed at random like Chrome does
188
+ entries = list(self._local_settings.settings)
189
+ if self._local_settings.grease:
190
+ # RFC 9114 section 7.2.4.1: a reserved identifier the peer must ignore, placed at random
203
191
  entries.insert(
204
192
  secrets.randbelow(len(entries) + 1),
205
193
  (0x1F * secrets.randbelow(1 << 16) + 0x21, secrets.randbelow(1 << 20)),
@@ -210,8 +198,8 @@ class Http3Connection:
210
198
  writer.varint(FrameType.SETTINGS)
211
199
  writer.varint(len(settings))
212
200
  writer.put(settings.finish())
213
- if self._profile.grease:
214
- # RFC 9114 section 7.2.8: an empty frame of a reserved type right after SETTINGS, as Chrome sends it
201
+ if self._local_settings.grease:
202
+ # RFC 9114 section 7.2.8: an empty frame of a reserved type right after SETTINGS
215
203
  writer.varint(0x1F * secrets.randbelow(1 << 32) + 0x21)
216
204
  writer.varint(0)
217
205
  self._quic.send(self._control_stream, writer.finish())
@@ -252,7 +240,7 @@ class Http3Connection:
252
240
  stream_id = self._quic.open_stream()
253
241
  priority = request.headers.get("priority")
254
242
  if priority is not None:
255
- # RFC 9218 section 7.2: Chrome announces a request's priority on the control stream before its HEADERS
243
+ # RFC 9218 section 7.2: a request's priority goes on the control stream before its HEADERS
256
244
  self._quic.send(
257
245
  self._control_stream,
258
246
  self._frame(
@@ -341,7 +329,7 @@ class Http3Connection:
341
329
  PseudoHeader.SCHEME: (b":scheme", scheme.encode()),
342
330
  PseudoHeader.PATH: (b":path", request.url.target.encode()),
343
331
  }
344
- fields = [pseudo[header] for header in self._profile.pseudo_header_order]
332
+ fields = [pseudo[header] for header in self._local_settings.pseudo_header_order]
345
333
  if protocol is not None:
346
334
  fields.insert(1, (b":protocol", protocol))
347
335
  return fields + [(name.encode(), value.encode()) for name, value in wire]
@@ -48,7 +48,7 @@ from boring_http.quic.recovery import GRANULARITY, Recovery, SentPacket
48
48
  from boring_http.quic.streams import ReceiveStream, SendStream
49
49
 
50
50
  DATAGRAM_SIZE = 1200
51
- # RFC 9000 section 14.2: what a 1500 byte link carries under IPv6 and UDP headers; Chrome probes up to this size too
51
+ # RFC 9000 section 14.2: what a 1500 byte link carries under IPv6 and UDP headers
52
52
  PROBED_DATAGRAM_SIZE = 1452
53
53
  TAG_LENGTH = 16
54
54
  SAMPLE_LENGTH = 16
@@ -117,40 +117,25 @@ NEVER_REMEMBERED = (
117
117
 
118
118
  @dataclass(frozen=True, slots=True)
119
119
  class TransportParameters:
120
- max_idle_timeout: int = 30000
121
- max_udp_payload_size: int = 1472
122
- initial_max_data: int = 15728640
123
- initial_max_stream_data_bidi_local: int = 6291456
124
- initial_max_stream_data_bidi_remote: int = 6291456
125
- initial_max_stream_data_uni: int = 6291456
126
- initial_max_streams_bidi: int = 100
127
- initial_max_streams_uni: int = 103
120
+ max_idle_timeout: int
121
+ max_udp_payload_size: int
122
+ initial_max_data: int
123
+ initial_max_stream_data_bidi_local: int
124
+ initial_max_stream_data_bidi_remote: int
125
+ initial_max_stream_data_uni: int
126
+ initial_max_streams_bidi: int
127
+ initial_max_streams_uni: int
128
+ order: tuple[Parameter, ...]
129
+ # RFC 9000 section 18.2 and RFC 9221 section 3: the values a peer assumes when a parameter is absent
128
130
  ack_delay_exponent: int = 3
129
131
  max_ack_delay: int = 25
130
- active_connection_id_limit: int = 8
131
- max_datagram_frame_size: int = 65536
132
- # Chrome's connection option tags; a remembered round trip time goes out with them on a resumed connection
133
- google_connection_options: tuple[bytes, ...] = (b"ORIG",)
132
+ active_connection_id_limit: int = 2
133
+ max_datagram_frame_size: int = 0
134
+ google_connection_options: tuple[bytes, ...] = ()
134
135
  initial_round_trip_time: int | None = None
135
- grease: bool = True
136
- grease_version: bool = True
137
- shuffle: bool = True
138
- # the set Chrome 155 sends: the parameters at their defaults and grease_quic_bit are left out
139
- order: tuple[Parameter, ...] = (
140
- Parameter.MAX_IDLE_TIMEOUT,
141
- Parameter.MAX_UDP_PAYLOAD_SIZE,
142
- Parameter.INITIAL_MAX_DATA,
143
- Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
144
- Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
145
- Parameter.INITIAL_MAX_STREAM_DATA_UNI,
146
- Parameter.INITIAL_MAX_STREAMS_BIDI,
147
- Parameter.INITIAL_MAX_STREAMS_UNI,
148
- Parameter.INITIAL_SOURCE_CONNECTION_ID,
149
- Parameter.VERSION_INFORMATION,
150
- Parameter.MAX_DATAGRAM_FRAME_SIZE,
151
- Parameter.GOOGLE_CONNECTION_OPTIONS,
152
- Parameter.INITIAL_ROUND_TRIP_TIME,
153
- )
136
+ grease: bool = False
137
+ grease_version: bool = False
138
+ shuffle: bool = False
154
139
 
155
140
  def encode(
156
141
  self,
@@ -183,7 +168,7 @@ class TransportParameters:
183
168
  elif parameter is Parameter.VERSION_INFORMATION:
184
169
  available: list[int] = list(versions)
185
170
  if self.grease_version:
186
- # RFC 9368 section 2.1 / Chrome: a reserved version at a random place among the available ones
171
+ # RFC 9368 section 2.1: a reserved version at a random place among the available ones
187
172
  available.insert(
188
173
  secrets.randbelow(len(available) + 1),
189
174
  secrets.randbits(32) & 0xF0F0F0F0 | 0x0A0A0A0A,
@@ -204,7 +189,7 @@ class TransportParameters:
204
189
  raise ValueError(f"{parameter.name} is not a parameter a client sends")
205
190
  pairs.append((parameter, value))
206
191
  if self.grease:
207
- # RFC 9000 section 18.1: a reserved identifier the peer must ignore, drawn the way Chrome draws it
192
+ # RFC 9000 section 18.1: a reserved identifier the peer must ignore
208
193
  pairs.insert(
209
194
  secrets.randbelow(len(pairs) + 1),
210
195
  (
@@ -213,7 +198,6 @@ class TransportParameters:
213
198
  ),
214
199
  )
215
200
  if self.shuffle:
216
- # Chrome sends its transport parameters in a fresh random order on every connection
217
201
  secrets.SystemRandom().shuffle(pairs)
218
202
  for identifier, value in pairs:
219
203
  writer.varint(identifier)