boring-http 2.0.0__tar.gz → 2.0.2__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-2.0.0 → boring_http-2.0.2}/PKG-INFO +6 -6
- {boring_http-2.0.0 → boring_http-2.0.2}/README.md +4 -4
- {boring_http-2.0.0 → boring_http-2.0.2}/pyproject.toml +2 -2
- {boring_http-2.0.0 → boring_http-2.0.2}/pyproject.toml.orig +2 -2
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/client.py +27 -7
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/http3.py +11 -1
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/pool.py +2 -1
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/connection.py +69 -21
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/timing.py +3 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/websocket.py +2 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/__init__.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/codings.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/cookies.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/errors.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/hpack.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/hpack_tables.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/http1.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/http2.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/message.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/py.typed +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/qpack.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/qpack_tables.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/__init__.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/packet.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/protocol.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/recovery.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/quic/streams.py +0 -0
- {boring_http-2.0.0 → boring_http-2.0.2}/src/boring_http/revocation.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: boring-http
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Asyncio HTTP client on BoringSSL: HTTP/1.1, HTTP/2 and HTTP/3 over QUIC v1 and v2, WebSocket on all three, HTTP and SOCKS proxies, headers in your order
|
|
5
5
|
Requires-Dist: boring-ssl2>=1.2.0
|
|
6
|
-
Requires-Dist: ssl2>=
|
|
6
|
+
Requires-Dist: ssl2>=2.0.1
|
|
7
7
|
Requires-Python: >=3.14
|
|
8
8
|
Project-URL: Repository, https://github.com/baobien-dev/boring-http
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -98,7 +98,7 @@ async def main() -> None:
|
|
|
98
98
|
asyncio.run(main())
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
|
|
101
|
+
A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `connect` covers opening a connection, the proxy, TCP and the TLS or QUIC handshake included, `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
|
|
102
102
|
|
|
103
103
|
## HTTP/3
|
|
104
104
|
|
|
@@ -152,7 +152,7 @@ async def main() -> None:
|
|
|
152
152
|
asyncio.run(main())
|
|
153
153
|
```
|
|
154
154
|
|
|
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
|
|
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.
|
|
156
156
|
|
|
157
157
|
## Proxies
|
|
158
158
|
|
|
@@ -425,7 +425,7 @@ async def main() -> None:
|
|
|
425
425
|
asyncio.run(main())
|
|
426
426
|
```
|
|
427
427
|
|
|
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
|
|
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.
|
|
429
429
|
|
|
430
430
|
## WebSocket
|
|
431
431
|
|
|
@@ -453,7 +453,7 @@ async def main() -> None:
|
|
|
453
453
|
asyncio.run(main())
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
-
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen.
|
|
456
|
+
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
457
457
|
|
|
458
458
|
## uvloop
|
|
459
459
|
|
|
@@ -88,7 +88,7 @@ async def main() -> None:
|
|
|
88
88
|
asyncio.run(main())
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
|
|
91
|
+
A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `connect` covers opening a connection, the proxy, TCP and the TLS or QUIC handshake included, `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
|
|
92
92
|
|
|
93
93
|
## HTTP/3
|
|
94
94
|
|
|
@@ -142,7 +142,7 @@ async def main() -> None:
|
|
|
142
142
|
asyncio.run(main())
|
|
143
143
|
```
|
|
144
144
|
|
|
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
|
|
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; 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.
|
|
146
146
|
|
|
147
147
|
## Proxies
|
|
148
148
|
|
|
@@ -415,7 +415,7 @@ async def main() -> None:
|
|
|
415
415
|
asyncio.run(main())
|
|
416
416
|
```
|
|
417
417
|
|
|
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
|
|
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 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.
|
|
419
419
|
|
|
420
420
|
## WebSocket
|
|
421
421
|
|
|
@@ -443,7 +443,7 @@ async def main() -> None:
|
|
|
443
443
|
asyncio.run(main())
|
|
444
444
|
```
|
|
445
445
|
|
|
446
|
-
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen.
|
|
446
|
+
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
447
447
|
|
|
448
448
|
## uvloop
|
|
449
449
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
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
8
|
"boring-ssl2>=1.2.0",
|
|
9
|
-
"ssl2>=
|
|
9
|
+
"ssl2>=2.0.1",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
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
8
|
"boring-ssl2>=1.2.0",
|
|
9
|
-
"ssl2>=
|
|
9
|
+
"ssl2>=2.0.1",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
@@ -60,6 +60,10 @@ ALPN = {
|
|
|
60
60
|
HTTPVersion.HTTP_2: NextProtocol.H2,
|
|
61
61
|
HTTPVersion.HTTP_1: NextProtocol.HTTP1,
|
|
62
62
|
}
|
|
63
|
+
ALPS_EXTENSIONS = (
|
|
64
|
+
ExtensionType.application_settings,
|
|
65
|
+
ExtensionType.application_settings_old,
|
|
66
|
+
)
|
|
63
67
|
ACCEPTED_ENCODINGS = "gzip, deflate, br, zstd"
|
|
64
68
|
USER_AGENT = "boring-http"
|
|
65
69
|
DEFAULT_TIMEOUT = Timeout()
|
|
@@ -119,10 +123,15 @@ class Client:
|
|
|
119
123
|
self._http3 = http3
|
|
120
124
|
self._alt_svc: dict[tuple[str, int], float] = {}
|
|
121
125
|
self._versions = tuple(HTTPVersion(version) for version in versions)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
# over TCP the ClientHello's own ALPN goes out when one is given, else the ALPN of versions
|
|
127
|
+
offered = (
|
|
128
|
+
tls.application_protocols
|
|
129
|
+
if tls is not None
|
|
130
|
+
else tuple(
|
|
131
|
+
ALPN[version].value for version in self._versions if version in ALPN
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
if http2 is None and NextProtocol.H2.value in offered:
|
|
126
135
|
raise ValueError("h2 is offered, so http2= needs an Http2Settings")
|
|
127
136
|
if http3 is None and HTTPVersion.HTTP_3 in self._versions:
|
|
128
137
|
raise ValueError("h3 is in versions, so http3= needs an Http3Settings")
|
|
@@ -460,11 +469,19 @@ class Client:
|
|
|
460
469
|
return self._tls
|
|
461
470
|
# an HTTP/1.1-only connection keeps the ClientHello but can offer only its own protocol
|
|
462
471
|
names = tuple(protocol.value for protocol in protocols)
|
|
472
|
+
settings = tuple(
|
|
473
|
+
name for name in self._tls.application_settings if name in names
|
|
474
|
+
)
|
|
463
475
|
return replace(
|
|
464
476
|
self._tls,
|
|
465
477
|
application_protocols=names,
|
|
466
|
-
application_settings=
|
|
467
|
-
|
|
478
|
+
application_settings=settings,
|
|
479
|
+
extensions=self._tls.extensions
|
|
480
|
+
if settings
|
|
481
|
+
else tuple(
|
|
482
|
+
extension
|
|
483
|
+
for extension in self._tls.extensions
|
|
484
|
+
if extension not in ALPS_EXTENSIONS
|
|
468
485
|
),
|
|
469
486
|
)
|
|
470
487
|
|
|
@@ -516,6 +533,9 @@ class Client:
|
|
|
516
533
|
self._context((NextProtocol.HTTP1,) if http1_only else None),
|
|
517
534
|
url.address,
|
|
518
535
|
proxy=proxy,
|
|
536
|
+
proxy_context=self._context((NextProtocol.HTTP1,))
|
|
537
|
+
if proxy is not None and proxy.scheme is ProxyScheme.HTTPS
|
|
538
|
+
else None,
|
|
519
539
|
)
|
|
520
540
|
await self._checked(stream, url.authority)
|
|
521
541
|
if (
|
|
@@ -533,7 +553,7 @@ class Client:
|
|
|
533
553
|
transport: AsyncTransport
|
|
534
554
|
if proxy.scheme is ProxyScheme.HTTPS:
|
|
535
555
|
secured = await TLSStream.connect(
|
|
536
|
-
self._context(), (proxy.host, proxy.port)
|
|
556
|
+
self._context((NextProtocol.HTTP1,)), (proxy.host, proxy.port)
|
|
537
557
|
)
|
|
538
558
|
await self._checked(secured, f"{proxy.host}:{proxy.port}")
|
|
539
559
|
transport = secured
|
|
@@ -15,7 +15,7 @@ from boring_http.errors import (
|
|
|
15
15
|
)
|
|
16
16
|
from boring_http.message import Headers, HTTPVersion, Request, Response
|
|
17
17
|
from boring_http.qpack import Qpack
|
|
18
|
-
from boring_http.quic.connection import TransportParameters
|
|
18
|
+
from boring_http.quic.connection import Parameter, TransportParameters
|
|
19
19
|
from boring_http.quic.packet import Reader, Writer
|
|
20
20
|
from boring_http.quic.protocol import OriginMemory, QuicClient
|
|
21
21
|
from boring_http.timing import Deadline, Timeout
|
|
@@ -76,6 +76,16 @@ class Http3Settings:
|
|
|
76
76
|
tls: ClientHello | None = None
|
|
77
77
|
grease: bool = False
|
|
78
78
|
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
# RFC 9368 section 2.1: the first packet is v1 and the other version is offered only in version_information
|
|
81
|
+
if (
|
|
82
|
+
self.quic_version is not QuicVersion.V1
|
|
83
|
+
and Parameter.VERSION_INFORMATION not in self.transport.order
|
|
84
|
+
):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"quic_version {self.quic_version.name} needs VERSION_INFORMATION in transport.order"
|
|
87
|
+
)
|
|
88
|
+
|
|
79
89
|
def setting(self, identifier: Setting, default: int) -> int:
|
|
80
90
|
return next(
|
|
81
91
|
(value for key, value in self.settings if key == identifier), default
|
|
@@ -68,7 +68,8 @@ class Pool:
|
|
|
68
68
|
await limit.acquire()
|
|
69
69
|
await self._total.acquire()
|
|
70
70
|
try:
|
|
71
|
-
|
|
71
|
+
async with deadline.phase("connect"):
|
|
72
|
+
connection = await self._connector(url, proxy, quic)
|
|
72
73
|
except BaseException:
|
|
73
74
|
limit.release()
|
|
74
75
|
self._total.release()
|
|
@@ -113,6 +113,21 @@ NEVER_REMEMBERED = (
|
|
|
113
113
|
Parameter.ORIGINAL_DESTINATION_CONNECTION_ID,
|
|
114
114
|
Parameter.RETRY_SOURCE_CONNECTION_ID,
|
|
115
115
|
)
|
|
116
|
+
# RFC 9000 section 18.2 and RFC 9221 section 3: the values a peer assumes when a parameter is absent
|
|
117
|
+
ABSENT = {
|
|
118
|
+
Parameter.MAX_IDLE_TIMEOUT: 0,
|
|
119
|
+
Parameter.MAX_UDP_PAYLOAD_SIZE: 65527,
|
|
120
|
+
Parameter.INITIAL_MAX_DATA: 0,
|
|
121
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_LOCAL: 0,
|
|
122
|
+
Parameter.INITIAL_MAX_STREAM_DATA_BIDI_REMOTE: 0,
|
|
123
|
+
Parameter.INITIAL_MAX_STREAM_DATA_UNI: 0,
|
|
124
|
+
Parameter.INITIAL_MAX_STREAMS_BIDI: 0,
|
|
125
|
+
Parameter.INITIAL_MAX_STREAMS_UNI: 0,
|
|
126
|
+
Parameter.ACK_DELAY_EXPONENT: 3,
|
|
127
|
+
Parameter.MAX_ACK_DELAY: 25,
|
|
128
|
+
Parameter.ACTIVE_CONNECTION_ID_LIMIT: 2,
|
|
129
|
+
Parameter.MAX_DATAGRAM_FRAME_SIZE: 0,
|
|
130
|
+
}
|
|
116
131
|
|
|
117
132
|
|
|
118
133
|
@dataclass(frozen=True, slots=True)
|
|
@@ -126,26 +141,37 @@ class TransportParameters:
|
|
|
126
141
|
initial_max_streams_bidi: int
|
|
127
142
|
initial_max_streams_uni: int
|
|
128
143
|
order: tuple[Parameter, ...]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
max_datagram_frame_size: int = 0
|
|
144
|
+
ack_delay_exponent: int = ABSENT[Parameter.ACK_DELAY_EXPONENT]
|
|
145
|
+
max_ack_delay: int = ABSENT[Parameter.MAX_ACK_DELAY]
|
|
146
|
+
active_connection_id_limit: int = ABSENT[Parameter.ACTIVE_CONNECTION_ID_LIMIT]
|
|
147
|
+
max_datagram_frame_size: int = ABSENT[Parameter.MAX_DATAGRAM_FRAME_SIZE]
|
|
134
148
|
google_connection_options: tuple[bytes, ...] = ()
|
|
135
149
|
initial_round_trip_time: int | None = None
|
|
136
150
|
grease: bool = False
|
|
137
151
|
grease_version: bool = False
|
|
138
152
|
shuffle: bool = False
|
|
139
153
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
def __post_init__(self) -> None:
|
|
155
|
+
# a value left out of order never reaches the peer, which then works from the absent value instead
|
|
156
|
+
applied = {
|
|
157
|
+
parameter: value != ABSENT[parameter]
|
|
158
|
+
for parameter, value in self._integers.items()
|
|
159
|
+
} | {
|
|
160
|
+
Parameter.VERSION_INFORMATION: self.grease_version,
|
|
161
|
+
Parameter.GOOGLE_CONNECTION_OPTIONS: bool(self.google_connection_options),
|
|
162
|
+
Parameter.INITIAL_ROUND_TRIP_TIME: self.initial_round_trip_time is not None,
|
|
163
|
+
}
|
|
164
|
+
unsent = [
|
|
165
|
+
parameter.name
|
|
166
|
+
for parameter, changed in applied.items()
|
|
167
|
+
if changed and parameter not in self.order
|
|
168
|
+
]
|
|
169
|
+
if unsent:
|
|
170
|
+
raise ValueError(f"{', '.join(unsent)} must be in order to take effect")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def _integers(self) -> dict[Parameter, int]:
|
|
174
|
+
return {
|
|
149
175
|
Parameter.MAX_IDLE_TIMEOUT: self.max_idle_timeout,
|
|
150
176
|
Parameter.MAX_UDP_PAYLOAD_SIZE: self.max_udp_payload_size,
|
|
151
177
|
Parameter.INITIAL_MAX_DATA: self.initial_max_data,
|
|
@@ -159,6 +185,16 @@ class TransportParameters:
|
|
|
159
185
|
Parameter.ACTIVE_CONNECTION_ID_LIMIT: self.active_connection_id_limit,
|
|
160
186
|
Parameter.MAX_DATAGRAM_FRAME_SIZE: self.max_datagram_frame_size,
|
|
161
187
|
}
|
|
188
|
+
|
|
189
|
+
def encode(
|
|
190
|
+
self,
|
|
191
|
+
source_connection_id: bytes,
|
|
192
|
+
version: QuicVersion,
|
|
193
|
+
versions: tuple[QuicVersion, ...],
|
|
194
|
+
round_trip_time: int | None = None,
|
|
195
|
+
) -> bytes:
|
|
196
|
+
writer = Writer()
|
|
197
|
+
integers = self._integers
|
|
162
198
|
pairs: list[tuple[int, bytes]] = []
|
|
163
199
|
for parameter in self.order:
|
|
164
200
|
if parameter in integers:
|
|
@@ -361,7 +397,7 @@ class QuicConnection:
|
|
|
361
397
|
self._closing: ConnectionClose | None = None
|
|
362
398
|
self._close_sent = False
|
|
363
399
|
self._terminated = False
|
|
364
|
-
self._idle_timeout = parameters.max_idle_timeout / 1000
|
|
400
|
+
self._idle_timeout: float | None = parameters.max_idle_timeout / 1000 or None
|
|
365
401
|
self._last_activity = now
|
|
366
402
|
self._ack_timer: float | None = None
|
|
367
403
|
self._peer_connection_ids: dict[int, tuple[bytes, bytes]] = {}
|
|
@@ -586,18 +622,23 @@ class QuicConnection:
|
|
|
586
622
|
self._mtu_probe = None
|
|
587
623
|
|
|
588
624
|
def next_timer(self) -> float | None:
|
|
589
|
-
candidates = [
|
|
625
|
+
candidates: list[float] = []
|
|
626
|
+
if self._idle_timeout is not None:
|
|
627
|
+
candidates.append(self._last_activity + self._idle_timeout)
|
|
590
628
|
if self._ack_timer is not None:
|
|
591
629
|
candidates.append(self._ack_timer)
|
|
592
630
|
timer = self._recovery.loss_timer(EncryptionLevel.HANDSHAKE in self._keys)
|
|
593
631
|
if timer is not None:
|
|
594
632
|
candidates.append(timer[0])
|
|
595
|
-
return min(candidates)
|
|
633
|
+
return min(candidates, default=None)
|
|
596
634
|
|
|
597
635
|
def handle_timer(self, now: float) -> None:
|
|
598
636
|
if self._terminated:
|
|
599
637
|
return
|
|
600
|
-
if
|
|
638
|
+
if (
|
|
639
|
+
self._idle_timeout is not None
|
|
640
|
+
and now >= self._last_activity + self._idle_timeout
|
|
641
|
+
):
|
|
601
642
|
self._terminated = True
|
|
602
643
|
self._events.append(
|
|
603
644
|
ConnectionTerminated(TransportError.NO_ERROR, "idle timeout", False)
|
|
@@ -1010,9 +1051,16 @@ class QuicConnection:
|
|
|
1010
1051
|
self._peer_max_streams[False] = peer.integer(
|
|
1011
1052
|
Parameter.INITIAL_MAX_STREAMS_UNI, 0
|
|
1012
1053
|
)
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1054
|
+
# RFC 9000 section 10.1: the smaller advertised timeout, where 0 means that side sets none
|
|
1055
|
+
timeouts = [
|
|
1056
|
+
value / 1000
|
|
1057
|
+
for value in (
|
|
1058
|
+
peer.integer(Parameter.MAX_IDLE_TIMEOUT, 0),
|
|
1059
|
+
self._parameters.max_idle_timeout,
|
|
1060
|
+
)
|
|
1061
|
+
if value
|
|
1062
|
+
]
|
|
1063
|
+
self._idle_timeout = min(timeouts) if timeouts else None
|
|
1016
1064
|
self._recovery.max_ack_delay = peer.integer(Parameter.MAX_ACK_DELAY, 25) / 1000
|
|
1017
1065
|
|
|
1018
1066
|
def _reject_early_data(self) -> None:
|
|
@@ -45,6 +45,9 @@ class Deadline:
|
|
|
45
45
|
def phase(self, name: Phase) -> PhaseTimer:
|
|
46
46
|
return PhaseTimer(self, name)
|
|
47
47
|
|
|
48
|
+
def drop_total(self) -> None:
|
|
49
|
+
self._end = None
|
|
50
|
+
|
|
48
51
|
def limit(self, name: Phase) -> tuple[float | None, bool]:
|
|
49
52
|
# the total budget wins when it leaves less time than the phase's own limit, and the error then names the total
|
|
50
53
|
limit = self._limits[name]
|
|
@@ -37,6 +37,8 @@ class WebSocket:
|
|
|
37
37
|
) -> None:
|
|
38
38
|
self._transport = transport
|
|
39
39
|
self._buffer = bytearray(leftover)
|
|
40
|
+
# the total covered the handshake; from here each read and write keeps only its phase limit
|
|
41
|
+
deadline.drop_total()
|
|
40
42
|
self._deadline = deadline
|
|
41
43
|
self._limit = max_message_size
|
|
42
44
|
self._fragments: list[bytes] = []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|