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.
Files changed (28) hide show
  1. {boring_http-1.2.0 → boring_http-2.0.1}/PKG-INFO +178 -34
  2. {boring_http-1.2.0 → boring_http-2.0.1}/README.md +176 -32
  3. {boring_http-1.2.0 → boring_http-2.0.1}/pyproject.toml +2 -2
  4. {boring_http-1.2.0 → boring_http-2.0.1}/pyproject.toml.orig +2 -2
  5. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/__init__.py +7 -7
  6. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/client.py +41 -11
  7. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http2.py +14 -35
  8. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http3.py +29 -31
  9. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/connection.py +85 -53
  10. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/codings.py +0 -0
  11. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/cookies.py +0 -0
  12. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/errors.py +0 -0
  13. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/hpack.py +0 -0
  14. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/hpack_tables.py +0 -0
  15. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/http1.py +0 -0
  16. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/message.py +0 -0
  17. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/pool.py +0 -0
  18. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/py.typed +0 -0
  19. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/qpack.py +0 -0
  20. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/qpack_tables.py +0 -0
  21. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/__init__.py +0 -0
  22. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/packet.py +0 -0
  23. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/protocol.py +0 -0
  24. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/recovery.py +0 -0
  25. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/quic/streams.py +0 -0
  26. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/revocation.py +0 -0
  27. {boring_http-1.2.0 → boring_http-2.0.1}/src/boring_http/timing.py +0 -0
  28. {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: 1.2.0
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>=1.2.0
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
- 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; 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 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 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