boring-http 1.0.1__tar.gz → 1.2.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.
- {boring_http-1.0.1 → boring_http-1.2.0}/PKG-INFO +99 -23
- {boring_http-1.0.1 → boring_http-1.2.0}/README.md +96 -20
- {boring_http-1.0.1 → boring_http-1.2.0}/pyproject.toml +9 -6
- {boring_http-1.0.1 → boring_http-1.2.0}/pyproject.toml.orig +6 -3
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/__init__.py +11 -6
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/client.py +57 -34
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/codings.py +0 -2
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/cookies.py +2 -4
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/errors.py +4 -0
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/hpack.py +7 -12
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/hpack_tables.py +2 -4
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/http1.py +12 -6
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/http2.py +101 -89
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/http3.py +26 -14
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/message.py +24 -25
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/pool.py +4 -5
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/qpack.py +4 -6
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/qpack_tables.py +1 -3
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/connection.py +257 -105
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/packet.py +75 -80
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/protocol.py +67 -47
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/recovery.py +28 -21
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/streams.py +23 -39
- boring_http-1.2.0/src/boring_http/revocation.py +56 -0
- boring_http-1.2.0/src/boring_http/timing.py +99 -0
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/websocket.py +3 -4
- boring_http-1.0.1/src/boring_http/timing.py +0 -63
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/py.typed +0 -0
- {boring_http-1.0.1 → boring_http-1.2.0}/src/boring_http/quic/__init__.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: boring-http
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.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
|
-
Requires-Dist: boring-ssl2
|
|
6
|
-
Requires-Dist: ssl2
|
|
5
|
+
Requires-Dist: boring-ssl2>=1.2.0
|
|
6
|
+
Requires-Dist: ssl2>=1.2.0
|
|
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
|
|
@@ -39,7 +39,7 @@ async def main() -> None:
|
|
|
39
39
|
asyncio.run(main())
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
42
|
+
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
43
43
|
|
|
44
44
|
## Responses
|
|
45
45
|
|
|
@@ -96,7 +96,7 @@ A single number sets every phase and the total, so the request gives up after th
|
|
|
96
96
|
```python
|
|
97
97
|
import asyncio
|
|
98
98
|
|
|
99
|
-
from boring_http import
|
|
99
|
+
from boring_http import Client, Http3Profile, QuicVersion
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
async def main() -> None:
|
|
@@ -105,7 +105,7 @@ async def main() -> None:
|
|
|
105
105
|
second = await client.get("https://pingly.us.kg/api/all")
|
|
106
106
|
print(first.version, second.version) # h2 h3, the Alt-Svc header taught the client that this origin speaks HTTP/3
|
|
107
107
|
|
|
108
|
-
async with Client(versions=("h3",), http3=Http3Profile(quic_version=
|
|
108
|
+
async with Client(versions=("h3",), http3=Http3Profile(quic_version=QuicVersion.V2)) as client:
|
|
109
109
|
response = await client.get("https://pingly.us.kg/api/all")
|
|
110
110
|
print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
|
|
111
111
|
|
|
@@ -113,7 +113,7 @@ async def main() -> None:
|
|
|
113
113
|
asyncio.run(main())
|
|
114
114
|
```
|
|
115
115
|
|
|
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. Retry packets, version negotiation, loss recovery, pacing, flow control, key updates, 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
|
|
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.
|
|
117
117
|
|
|
118
118
|
## Proxies
|
|
119
119
|
|
|
@@ -152,7 +152,7 @@ asyncio.run(main())
|
|
|
152
152
|
```python
|
|
153
153
|
import asyncio
|
|
154
154
|
|
|
155
|
-
from boring_http import
|
|
155
|
+
from boring_http import CHROME_HTTP2, Client
|
|
156
156
|
from ssl2 import (
|
|
157
157
|
JA4,
|
|
158
158
|
CertificateCompressionAlgorithm,
|
|
@@ -227,16 +227,92 @@ profile = (
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
async def main() -> None:
|
|
230
|
-
async with Client(tls=profile, http2=
|
|
230
|
+
async with Client(tls=profile, http2=CHROME_HTTP2) as client:
|
|
231
231
|
report = (await client.get("https://pingly.us.kg/api/all")).json()
|
|
232
232
|
print(report["tls"]["ja4"] == JA4.of(profile).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
|
|
233
|
-
print(report["http2"]["akamai_fingerprint"] ==
|
|
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
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
asyncio.run(main())
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
Over HTTP/3 the same holds, with the ClientHello Chrome 155 sends on QUIC: TLS 1.3 only, the `trust_anchors` extension with the 28 identifiers of Chrome's root store, and `pre_shared_key` once a session exists.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
import asyncio
|
|
243
|
+
|
|
244
|
+
from boring_http import Client, Http3Profile
|
|
245
|
+
from ssl2 import (
|
|
246
|
+
CHROME_TRUST_ANCHOR_IDS,
|
|
247
|
+
JA4,
|
|
248
|
+
CertificateCompressionAlgorithm,
|
|
249
|
+
CipherSuite,
|
|
250
|
+
ClientHelloBuilder,
|
|
251
|
+
ExtensionType,
|
|
252
|
+
JA4Transport,
|
|
253
|
+
NamedGroup,
|
|
254
|
+
NextProtocol,
|
|
255
|
+
ProtocolVersion,
|
|
256
|
+
PskKeyExchangeMode,
|
|
257
|
+
SignatureScheme,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
quic_profile = (
|
|
261
|
+
ClientHelloBuilder()
|
|
262
|
+
.supported_versions(ProtocolVersion.TLS_1_3)
|
|
263
|
+
.cipher_suites(CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256)
|
|
264
|
+
.extensions(
|
|
265
|
+
ExtensionType.signature_algorithms,
|
|
266
|
+
ExtensionType.application_settings,
|
|
267
|
+
ExtensionType.application_layer_protocol_negotiation,
|
|
268
|
+
ExtensionType.compress_certificate,
|
|
269
|
+
ExtensionType.trust_anchors,
|
|
270
|
+
ExtensionType.server_name,
|
|
271
|
+
ExtensionType.psk_key_exchange_modes,
|
|
272
|
+
ExtensionType.key_share,
|
|
273
|
+
ExtensionType.quic_transport_parameters,
|
|
274
|
+
ExtensionType.supported_versions,
|
|
275
|
+
ExtensionType.supported_groups,
|
|
276
|
+
ExtensionType.encrypted_client_hello,
|
|
277
|
+
ExtensionType.pre_shared_key,
|
|
278
|
+
)
|
|
279
|
+
.supported_groups(NamedGroup.X25519MLKEM768, NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1)
|
|
280
|
+
.key_shares(NamedGroup.X25519MLKEM768, NamedGroup.x25519)
|
|
281
|
+
.signature_algorithms(
|
|
282
|
+
SignatureScheme.ecdsa_secp256r1_sha256,
|
|
283
|
+
SignatureScheme.rsa_pss_rsae_sha256,
|
|
284
|
+
SignatureScheme.rsa_pkcs1_sha256,
|
|
285
|
+
SignatureScheme.ecdsa_secp384r1_sha384,
|
|
286
|
+
SignatureScheme.rsa_pss_rsae_sha384,
|
|
287
|
+
SignatureScheme.rsa_pkcs1_sha384,
|
|
288
|
+
SignatureScheme.rsa_pss_rsae_sha512,
|
|
289
|
+
SignatureScheme.rsa_pkcs1_sha512,
|
|
290
|
+
SignatureScheme.rsa_pkcs1_sha1,
|
|
291
|
+
)
|
|
292
|
+
.application_protocols(NextProtocol.H3)
|
|
293
|
+
.application_settings(NextProtocol.H3)
|
|
294
|
+
.certificate_compression(CertificateCompressionAlgorithm.brotli)
|
|
295
|
+
.psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
|
|
296
|
+
.trust_anchor_ids(*CHROME_TRUST_ANCHOR_IDS)
|
|
297
|
+
.grease()
|
|
298
|
+
.permute_extensions()
|
|
299
|
+
.build()
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def main() -> None:
|
|
304
|
+
async with Client(versions=("h3",), http3=Http3Profile(tls=quic_profile), headers=[("priority", "u=0, i")]) as client:
|
|
305
|
+
_ = await client.get("https://pingly.us.kg/api/all")
|
|
306
|
+
await client.close() # the next request opens a new connection, which resumes the session
|
|
307
|
+
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
|
|
309
|
+
print(report["http3"]["h3_text"]) # 1:65536;6:262144;7:100;51:1;GREASE|m,a,s,p
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
asyncio.run(main())
|
|
313
|
+
```
|
|
314
|
+
|
|
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.
|
|
240
316
|
|
|
241
317
|
## WebSocket
|
|
242
318
|
|
|
@@ -287,17 +363,17 @@ Against a Go server (net/http and quic-go) on loopback, so the server is never t
|
|
|
287
363
|
|
|
288
364
|
| scenario | boring-http | boring-http+uvloop | wreq | curl_cffi | pycurl | httpx | aiohttp | requests |
|
|
289
365
|
| ----------- | ----------: | -----------------: | ----: | --------: | -----: | ----: | ------: | -------: |
|
|
290
|
-
| h1-seq |
|
|
291
|
-
| h2-seq |
|
|
292
|
-
| h3-seq |
|
|
293
|
-
| h1-conc |
|
|
294
|
-
| h2-conc |
|
|
295
|
-
| h3-conc |
|
|
296
|
-
| h1-download |
|
|
297
|
-
| h2-download |
|
|
298
|
-
| h3-download |
|
|
299
|
-
| h1-upload |
|
|
300
|
-
| h2-upload |
|
|
301
|
-
| h3-upload |
|
|
366
|
+
| h1-seq | 14713 | 14354 | 20116 | 6042 | 29137 | 2248 | 11831 | 3539 |
|
|
367
|
+
| h2-seq | 7307 | 7359 | 13404 | 5096 | 16023 | 1880 | - | - |
|
|
368
|
+
| h3-seq | 4679 | 4308 | - | 4725 | 14041 | - | - | - |
|
|
369
|
+
| h1-conc | 11594 | 12391 | 61212 | 7443 | 33861 | 939 | 24836 | 2438 |
|
|
370
|
+
| h2-conc | 19234 | 20085 | 41075 | 11354 | 30053 | 2164 | - | - |
|
|
371
|
+
| h3-conc | 10121 | 9788 | - | 11447 | 29598 | - | - | - |
|
|
372
|
+
| h1-download | 734 | 772 | 823 | 767 | 1229 | 257 | 1081 | 594 |
|
|
373
|
+
| h2-download | 319 | 303 | 443 | 466 | 484 | 120 | - | - |
|
|
374
|
+
| h3-download | 199 | 207 | - | 542 | 491 | - | - | - |
|
|
375
|
+
| h1-upload | 2513 | 3093 | 3276 | 2106 | 1998 | 694 | 713 | 2376 |
|
|
376
|
+
| h2-upload | 786 | 839 | 2206 | 1085 | 1323 | 186 | - | - |
|
|
377
|
+
| h3-upload | 132 | 132 | - | 819 | 434 | - | - | - |
|
|
302
378
|
|
|
303
379
|
HTTP/1.1 and HTTP/2 are Python framing over BoringSSL records; HTTP/3 is a QUIC stack written in Python with BoringSSL doing the cryptography, which is where the gap to libcurl's HTTP/3 comes from. The same code holds 1000 open clients per protocol, 20000 requests with 256 in flight, parallel multi-megabyte transfers and 5% datagram loss on the QUIC path.
|
|
@@ -29,7 +29,7 @@ async def main() -> None:
|
|
|
29
29
|
asyncio.run(main())
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
32
|
+
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
33
33
|
|
|
34
34
|
## Responses
|
|
35
35
|
|
|
@@ -86,7 +86,7 @@ A single number sets every phase and the total, so the request gives up after th
|
|
|
86
86
|
```python
|
|
87
87
|
import asyncio
|
|
88
88
|
|
|
89
|
-
from boring_http import
|
|
89
|
+
from boring_http import Client, Http3Profile, QuicVersion
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
async def main() -> None:
|
|
@@ -95,7 +95,7 @@ async def main() -> None:
|
|
|
95
95
|
second = await client.get("https://pingly.us.kg/api/all")
|
|
96
96
|
print(first.version, second.version) # h2 h3, the Alt-Svc header taught the client that this origin speaks HTTP/3
|
|
97
97
|
|
|
98
|
-
async with Client(versions=("h3",), http3=Http3Profile(quic_version=
|
|
98
|
+
async with Client(versions=("h3",), http3=Http3Profile(quic_version=QuicVersion.V2)) as client:
|
|
99
99
|
response = await client.get("https://pingly.us.kg/api/all")
|
|
100
100
|
print(response.version, response.json()["http_version"]) # h3 HTTP/3.0
|
|
101
101
|
|
|
@@ -103,7 +103,7 @@ async def main() -> None:
|
|
|
103
103
|
asyncio.run(main())
|
|
104
104
|
```
|
|
105
105
|
|
|
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. Retry packets, version negotiation, loss recovery, pacing, flow control, key updates, 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
|
|
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.
|
|
107
107
|
|
|
108
108
|
## Proxies
|
|
109
109
|
|
|
@@ -142,7 +142,7 @@ asyncio.run(main())
|
|
|
142
142
|
```python
|
|
143
143
|
import asyncio
|
|
144
144
|
|
|
145
|
-
from boring_http import
|
|
145
|
+
from boring_http import CHROME_HTTP2, Client
|
|
146
146
|
from ssl2 import (
|
|
147
147
|
JA4,
|
|
148
148
|
CertificateCompressionAlgorithm,
|
|
@@ -217,16 +217,92 @@ profile = (
|
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
async def main() -> None:
|
|
220
|
-
async with Client(tls=profile, http2=
|
|
220
|
+
async with Client(tls=profile, http2=CHROME_HTTP2) as client:
|
|
221
221
|
report = (await client.get("https://pingly.us.kg/api/all")).json()
|
|
222
222
|
print(report["tls"]["ja4"] == JA4.of(profile).text, report["tls"]["ja4"]) # True t13d1516h2_8daaf6152771_d8a2da3f94cd
|
|
223
|
-
print(report["http2"]["akamai_fingerprint"] ==
|
|
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
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
asyncio.run(main())
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
Over HTTP/3 the same holds, with the ClientHello Chrome 155 sends on QUIC: TLS 1.3 only, the `trust_anchors` extension with the 28 identifiers of Chrome's root store, and `pre_shared_key` once a session exists.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
import asyncio
|
|
233
|
+
|
|
234
|
+
from boring_http import Client, Http3Profile
|
|
235
|
+
from ssl2 import (
|
|
236
|
+
CHROME_TRUST_ANCHOR_IDS,
|
|
237
|
+
JA4,
|
|
238
|
+
CertificateCompressionAlgorithm,
|
|
239
|
+
CipherSuite,
|
|
240
|
+
ClientHelloBuilder,
|
|
241
|
+
ExtensionType,
|
|
242
|
+
JA4Transport,
|
|
243
|
+
NamedGroup,
|
|
244
|
+
NextProtocol,
|
|
245
|
+
ProtocolVersion,
|
|
246
|
+
PskKeyExchangeMode,
|
|
247
|
+
SignatureScheme,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
quic_profile = (
|
|
251
|
+
ClientHelloBuilder()
|
|
252
|
+
.supported_versions(ProtocolVersion.TLS_1_3)
|
|
253
|
+
.cipher_suites(CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256)
|
|
254
|
+
.extensions(
|
|
255
|
+
ExtensionType.signature_algorithms,
|
|
256
|
+
ExtensionType.application_settings,
|
|
257
|
+
ExtensionType.application_layer_protocol_negotiation,
|
|
258
|
+
ExtensionType.compress_certificate,
|
|
259
|
+
ExtensionType.trust_anchors,
|
|
260
|
+
ExtensionType.server_name,
|
|
261
|
+
ExtensionType.psk_key_exchange_modes,
|
|
262
|
+
ExtensionType.key_share,
|
|
263
|
+
ExtensionType.quic_transport_parameters,
|
|
264
|
+
ExtensionType.supported_versions,
|
|
265
|
+
ExtensionType.supported_groups,
|
|
266
|
+
ExtensionType.encrypted_client_hello,
|
|
267
|
+
ExtensionType.pre_shared_key,
|
|
268
|
+
)
|
|
269
|
+
.supported_groups(NamedGroup.X25519MLKEM768, NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1)
|
|
270
|
+
.key_shares(NamedGroup.X25519MLKEM768, NamedGroup.x25519)
|
|
271
|
+
.signature_algorithms(
|
|
272
|
+
SignatureScheme.ecdsa_secp256r1_sha256,
|
|
273
|
+
SignatureScheme.rsa_pss_rsae_sha256,
|
|
274
|
+
SignatureScheme.rsa_pkcs1_sha256,
|
|
275
|
+
SignatureScheme.ecdsa_secp384r1_sha384,
|
|
276
|
+
SignatureScheme.rsa_pss_rsae_sha384,
|
|
277
|
+
SignatureScheme.rsa_pkcs1_sha384,
|
|
278
|
+
SignatureScheme.rsa_pss_rsae_sha512,
|
|
279
|
+
SignatureScheme.rsa_pkcs1_sha512,
|
|
280
|
+
SignatureScheme.rsa_pkcs1_sha1,
|
|
281
|
+
)
|
|
282
|
+
.application_protocols(NextProtocol.H3)
|
|
283
|
+
.application_settings(NextProtocol.H3)
|
|
284
|
+
.certificate_compression(CertificateCompressionAlgorithm.brotli)
|
|
285
|
+
.psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
|
|
286
|
+
.trust_anchor_ids(*CHROME_TRUST_ANCHOR_IDS)
|
|
287
|
+
.grease()
|
|
288
|
+
.permute_extensions()
|
|
289
|
+
.build()
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def main() -> None:
|
|
294
|
+
async with Client(versions=("h3",), http3=Http3Profile(tls=quic_profile), headers=[("priority", "u=0, i")]) as client:
|
|
295
|
+
_ = await client.get("https://pingly.us.kg/api/all")
|
|
296
|
+
await client.close() # the next request opens a new connection, which resumes the session
|
|
297
|
+
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
|
|
299
|
+
print(report["http3"]["h3_text"]) # 1:65536;6:262144;7:100;51:1;GREASE|m,a,s,p
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
asyncio.run(main())
|
|
303
|
+
```
|
|
304
|
+
|
|
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.
|
|
230
306
|
|
|
231
307
|
## WebSocket
|
|
232
308
|
|
|
@@ -277,17 +353,17 @@ Against a Go server (net/http and quic-go) on loopback, so the server is never t
|
|
|
277
353
|
|
|
278
354
|
| scenario | boring-http | boring-http+uvloop | wreq | curl_cffi | pycurl | httpx | aiohttp | requests |
|
|
279
355
|
| ----------- | ----------: | -----------------: | ----: | --------: | -----: | ----: | ------: | -------: |
|
|
280
|
-
| h1-seq |
|
|
281
|
-
| h2-seq |
|
|
282
|
-
| h3-seq |
|
|
283
|
-
| h1-conc |
|
|
284
|
-
| h2-conc |
|
|
285
|
-
| h3-conc |
|
|
286
|
-
| h1-download |
|
|
287
|
-
| h2-download |
|
|
288
|
-
| h3-download |
|
|
289
|
-
| h1-upload |
|
|
290
|
-
| h2-upload |
|
|
291
|
-
| h3-upload |
|
|
356
|
+
| h1-seq | 14713 | 14354 | 20116 | 6042 | 29137 | 2248 | 11831 | 3539 |
|
|
357
|
+
| h2-seq | 7307 | 7359 | 13404 | 5096 | 16023 | 1880 | - | - |
|
|
358
|
+
| h3-seq | 4679 | 4308 | - | 4725 | 14041 | - | - | - |
|
|
359
|
+
| h1-conc | 11594 | 12391 | 61212 | 7443 | 33861 | 939 | 24836 | 2438 |
|
|
360
|
+
| h2-conc | 19234 | 20085 | 41075 | 11354 | 30053 | 2164 | - | - |
|
|
361
|
+
| h3-conc | 10121 | 9788 | - | 11447 | 29598 | - | - | - |
|
|
362
|
+
| h1-download | 734 | 772 | 823 | 767 | 1229 | 257 | 1081 | 594 |
|
|
363
|
+
| h2-download | 319 | 303 | 443 | 466 | 484 | 120 | - | - |
|
|
364
|
+
| h3-download | 199 | 207 | - | 542 | 491 | - | - | - |
|
|
365
|
+
| h1-upload | 2513 | 3093 | 3276 | 2106 | 1998 | 694 | 713 | 2376 |
|
|
366
|
+
| h2-upload | 786 | 839 | 2206 | 1085 | 1323 | 186 | - | - |
|
|
367
|
+
| h3-upload | 132 | 132 | - | 819 | 434 | - | - | - |
|
|
292
368
|
|
|
293
369
|
HTTP/1.1 and HTTP/2 are Python framing over BoringSSL records; HTTP/3 is a QUIC stack written in Python with BoringSSL doing the cryptography, which is where the gap to libcurl's HTTP/3 comes from. The same code holds 1000 open clients per protocol, 20000 requests with 256 in flight, parallel multi-megabyte transfers and 5% datagram loss on the QUIC path.
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.2.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"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"boring-ssl2",
|
|
9
|
-
"ssl2",
|
|
8
|
+
"boring-ssl2>=1.2.0",
|
|
9
|
+
"ssl2>=1.2.0",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
13
13
|
Repository = "https://github.com/baobien-dev/boring-http"
|
|
14
14
|
|
|
15
|
-
[
|
|
16
|
-
|
|
17
|
-
build-backend = "uv_build"
|
|
15
|
+
[tool.basedpyright]
|
|
16
|
+
typeCheckingMode = "strict"
|
|
18
17
|
|
|
19
18
|
[tool.uv.sources.ssl2]
|
|
20
19
|
path = "../ssl2"
|
|
@@ -23,3 +22,7 @@ editable = true
|
|
|
23
22
|
[tool.uv.sources.boring-ssl2]
|
|
24
23
|
path = "../boring-ssl2"
|
|
25
24
|
editable = true
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.2.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"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"boring-ssl2",
|
|
9
|
-
"ssl2",
|
|
8
|
+
"boring-ssl2>=1.2.0",
|
|
9
|
+
"ssl2>=1.2.0",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
13
13
|
Repository = "https://github.com/baobien-dev/boring-http"
|
|
14
14
|
|
|
15
|
+
[tool.basedpyright]
|
|
16
|
+
typeCheckingMode = "strict"
|
|
17
|
+
|
|
15
18
|
[build-system]
|
|
16
19
|
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
17
20
|
build-backend = "uv_build"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from boring_ssl2 import QuicVersion
|
|
2
|
+
|
|
1
3
|
from boring_http.client import Client
|
|
2
4
|
from boring_http.cookies import Cookie, CookieJar
|
|
3
5
|
from boring_http.errors import (
|
|
@@ -5,14 +7,15 @@ from boring_http.errors import (
|
|
|
5
7
|
HTTPError,
|
|
6
8
|
ProtocolError,
|
|
7
9
|
RemoteDisconnected,
|
|
10
|
+
RevocationError,
|
|
8
11
|
StreamRefused,
|
|
9
12
|
StreamResetError,
|
|
10
13
|
TimeoutError,
|
|
11
14
|
TooManyRedirects,
|
|
12
15
|
WebSocketClosed,
|
|
13
16
|
)
|
|
14
|
-
from boring_http.http2 import
|
|
15
|
-
from boring_http.http3 import Http3Profile
|
|
17
|
+
from boring_http.http2 import CHROME_HTTP2, Http2Profile
|
|
18
|
+
from boring_http.http3 import CHROME_HTTP3, Http3Profile
|
|
16
19
|
from boring_http.message import (
|
|
17
20
|
URL,
|
|
18
21
|
Body,
|
|
@@ -23,14 +26,13 @@ from boring_http.message import (
|
|
|
23
26
|
Response,
|
|
24
27
|
)
|
|
25
28
|
from boring_http.quic.connection import TransportParameters
|
|
26
|
-
from boring_http.
|
|
29
|
+
from boring_http.revocation import Revocation
|
|
27
30
|
from boring_http.timing import Timeout
|
|
28
31
|
from boring_http.websocket import WebSocket
|
|
29
32
|
|
|
30
33
|
__all__ = (
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"QUIC_V2",
|
|
34
|
+
"CHROME_HTTP2",
|
|
35
|
+
"CHROME_HTTP3",
|
|
34
36
|
"URL",
|
|
35
37
|
"Body",
|
|
36
38
|
"Client",
|
|
@@ -44,9 +46,12 @@ __all__ = (
|
|
|
44
46
|
"Http2Profile",
|
|
45
47
|
"Http3Profile",
|
|
46
48
|
"ProtocolError",
|
|
49
|
+
"QuicVersion",
|
|
47
50
|
"RemoteDisconnected",
|
|
48
51
|
"Request",
|
|
49
52
|
"Response",
|
|
53
|
+
"Revocation",
|
|
54
|
+
"RevocationError",
|
|
50
55
|
"StreamRefused",
|
|
51
56
|
"StreamResetError",
|
|
52
57
|
"Timeout",
|