boring-http 2.2.0__tar.gz → 2.3.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 (29) hide show
  1. {boring_http-2.2.0 → boring_http-2.3.0}/PKG-INFO +5 -5
  2. {boring_http-2.2.0 → boring_http-2.3.0}/README.md +2 -2
  3. {boring_http-2.2.0 → boring_http-2.3.0}/pyproject.toml +3 -3
  4. {boring_http-2.2.0 → boring_http-2.3.0}/pyproject.toml.orig +3 -3
  5. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/client.py +16 -4
  6. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/http2.py +5 -1
  7. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/http3.py +11 -3
  8. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/__init__.py +0 -0
  9. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/codings.py +0 -0
  10. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/cookies.py +0 -0
  11. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/errors.py +0 -0
  12. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/hpack.py +0 -0
  13. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/hpack_tables.py +0 -0
  14. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/http1.py +0 -0
  15. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/message.py +0 -0
  16. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/pool.py +0 -0
  17. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/public_suffixes.py +0 -0
  18. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/py.typed +0 -0
  19. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/qpack.py +0 -0
  20. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/qpack_tables.py +0 -0
  21. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/__init__.py +0 -0
  22. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/connection.py +0 -0
  23. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/packet.py +0 -0
  24. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/protocol.py +0 -0
  25. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/recovery.py +0 -0
  26. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/quic/streams.py +0 -0
  27. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/revocation.py +0 -0
  28. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/timing.py +0 -0
  29. {boring_http-2.2.0 → boring_http-2.3.0}/src/boring_http/websocket.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boring-http
3
- Version: 2.2.0
3
+ Version: 2.3.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>=1.2.0
6
- Requires-Dist: ssl2>=2.0.1
5
+ Requires-Dist: boring-ssl2>=1.3.0
6
+ Requires-Dist: ssl2>=2.1.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
@@ -42,7 +42,7 @@ async def main() -> None:
42
42
  asyncio.run(main())
43
43
  ```
44
44
 
45
- 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. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. 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`.
45
+ 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. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. `key_log="keys.txt"` appends the secrets of every TLS and QUIC handshake in the NSS key log format, for Wireshark. `resolve={("example.com", 443): ("203.0.113.7", 443)}` sends a connection without a proxy to that address instead of resolving the name, while SNI and certificate verification keep the name from the URL. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. 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`.
46
46
 
47
47
  ## Responses
48
48
 
@@ -455,7 +455,7 @@ async def main() -> None:
455
455
  asyncio.run(main())
456
456
  ```
457
457
 
458
- 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.
458
+ 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. Each WebSocket has a connection of its own, which `close()` closes after the closing handshake. `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.
459
459
 
460
460
  ## uvloop
461
461
 
@@ -32,7 +32,7 @@ async def main() -> None:
32
32
  asyncio.run(main())
33
33
  ```
34
34
 
35
- 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. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. 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`.
35
+ 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. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. `key_log="keys.txt"` appends the secrets of every TLS and QUIC handshake in the NSS key log format, for Wireshark. `resolve={("example.com", 443): ("203.0.113.7", 443)}` sends a connection without a proxy to that address instead of resolving the name, while SNI and certificate verification keep the name from the URL. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. 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`.
36
36
 
37
37
  ## Responses
38
38
 
@@ -445,7 +445,7 @@ async def main() -> None:
445
445
  asyncio.run(main())
446
446
  ```
447
447
 
448
- 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.
448
+ 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. Each WebSocket has a connection of its own, which `close()` closes after the closing handshake. `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.
449
449
 
450
450
  ## uvloop
451
451
 
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "boring-http"
3
- version = "2.2.0"
3
+ version = "2.3.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>=1.2.0",
9
- "ssl2>=2.0.1",
8
+ "boring-ssl2>=1.3.0",
9
+ "ssl2>=2.1.0",
10
10
  ]
11
11
 
12
12
  [project.urls]
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "boring-http"
3
- version = "2.2.0"
3
+ version = "2.3.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>=1.2.0",
9
- "ssl2>=2.0.1",
8
+ "boring-ssl2>=1.3.0",
9
+ "ssl2>=2.1.0",
10
10
  ]
11
11
 
12
12
  [project.urls]
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import os
2
3
  from collections.abc import (
3
4
  AsyncGenerator,
4
5
  AsyncIterable,
@@ -117,6 +118,8 @@ class Client:
117
118
  revocation: Revocation = Revocation.SOFT,
118
119
  certificate_chain: SigningChain | None = None,
119
120
  check_peer: Callable[[tuple[bytes, ...], str], Awaitable[None]] | None = None,
121
+ key_log: str | os.PathLike[str] | None = None,
122
+ resolve: Mapping[tuple[str, int], tuple[str, int]] | None = None,
120
123
  cookies: CookieJar | None = None,
121
124
  base_url: str | None = None,
122
125
  max_connections: int = 100,
@@ -151,6 +154,8 @@ class Client:
151
154
  self._revocations = Revocations(revocation, self._fetch_crl)
152
155
  self._certificate_chain = certificate_chain
153
156
  self._check_peer = check_peer
157
+ self._key_log = key_log
158
+ self._resolve = {} if resolve is None else dict(resolve)
154
159
  self._base_url = base_url
155
160
  self._max_redirects = max_redirects
156
161
  self._max_message_size = max_message_size
@@ -475,7 +480,7 @@ class Client:
475
480
  else None
476
481
  )
477
482
  self._contexts[protocols] = ClientContext(
478
- configuration, session_cache=cache
483
+ configuration, session_cache=cache, key_log=self._key_log
479
484
  )
480
485
  return self._contexts[protocols]
481
486
 
@@ -528,13 +533,19 @@ class Client:
528
533
  *,
529
534
  http1_only: bool = False,
530
535
  ) -> Connection:
536
+ # a resolve= entry sends a direct connection elsewhere, while the URL's host stays the name sent and verified
537
+ address = (
538
+ self._resolve.get(url.address, url.address)
539
+ if proxy is None
540
+ else url.address
541
+ )
531
542
  try:
532
543
  if quic and self._http3 is not None:
533
544
  memory = self._memory.setdefault(url.address, OriginMemory())
534
545
  client = await QuicClient.connect(
535
546
  self._context((NextProtocol.H3,)),
536
547
  url.host,
537
- url.address,
548
+ address,
538
549
  self._http3.transport,
539
550
  version=self._http3.quic_version,
540
551
  proxy=proxy,
@@ -551,11 +562,12 @@ class Client:
551
562
  if url.secure:
552
563
  stream = await TLSStream.connect(
553
564
  self._context((NextProtocol.HTTP1,) if http1_only else None),
554
- url.address,
565
+ address,
555
566
  proxy=proxy,
556
567
  proxy_context=self._context((NextProtocol.HTTP1,))
557
568
  if proxy is not None and proxy.scheme is ProxyScheme.HTTPS
558
569
  else None,
570
+ server_hostname=url.host,
559
571
  )
560
572
  await self._checked(stream, url.authority)
561
573
  if (
@@ -587,7 +599,7 @@ class Client:
587
599
  return Http1Connection(
588
600
  transport, absolute_form=True, proxy_authorization=authorization
589
601
  )
590
- tunnel = await TLSStream.tunnel(self._context(), url.address, proxy)
602
+ tunnel = await TLSStream.tunnel(self._context(), address, proxy)
591
603
  return Http1Connection(tunnel.transport, leftover=tunnel.leftover)
592
604
  except ProxyError, ConfigurationError, RevocationError:
593
605
  raise
@@ -687,4 +687,8 @@ class Http2StreamTransport:
687
687
  async def close(self) -> None:
688
688
  if not self._closed:
689
689
  self._closed = True
690
- await self._connection.write_data(self._stream, b"", end=True)
690
+ # Client.websocket opens a connection for the tunnel alone, so the connection ends with it
691
+ try:
692
+ await self._connection.write_data(self._stream, b"", end=True)
693
+ finally:
694
+ await self._connection.close()
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import secrets
3
- from collections.abc import AsyncGenerator, Callable
3
+ from collections.abc import AsyncGenerator, Awaitable, Callable
4
4
  from dataclasses import dataclass
5
5
  from enum import IntEnum
6
6
 
@@ -311,7 +311,7 @@ class Http3Connection:
311
311
  return (
312
312
  status,
313
313
  headers,
314
- Http3StreamTransport(self._quic, stream_id, deadline, frames),
314
+ Http3StreamTransport(self._quic, stream_id, deadline, frames, self.close),
315
315
  )
316
316
 
317
317
  async def close(self) -> None:
@@ -531,12 +531,18 @@ class Http3Connection:
531
531
 
532
532
  class Http3StreamTransport:
533
533
  def __init__(
534
- self, quic: QuicClient, stream_id: int, deadline: Deadline, frames: StreamFrames
534
+ self,
535
+ quic: QuicClient,
536
+ stream_id: int,
537
+ deadline: Deadline,
538
+ frames: StreamFrames,
539
+ close_connection: Callable[[], Awaitable[None]],
535
540
  ) -> None:
536
541
  self._quic = quic
537
542
  self._stream_id = stream_id
538
543
  self._frames = frames
539
544
  self._deadline = deadline
545
+ self._close_connection = close_connection
540
546
  self._pending = b""
541
547
  self._closed = False
542
548
 
@@ -564,3 +570,5 @@ class Http3StreamTransport:
564
570
  if not self._closed:
565
571
  self._closed = True
566
572
  self._quic.send(self._stream_id, b"", end=True)
573
+ # Client.websocket opens a connection for the tunnel alone, so the connection ends with it
574
+ await self._close_connection()