| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- from __future__ import annotations
- import base64
- import logging
- import ssl
- import typing
- from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
- from .._exceptions import ProxyError
- from .._models import (
- URL,
- Origin,
- Request,
- Response,
- enforce_bytes,
- enforce_headers,
- enforce_url,
- )
- from .._ssl import default_ssl_context
- from .._synchronization import AsyncLock
- from .._trace import Trace
- from .connection import AsyncHTTPConnection
- from .connection_pool import AsyncConnectionPool
- from .http11 import AsyncHTTP11Connection
- from .interfaces import AsyncConnectionInterface
- ByteOrStr = typing.Union[bytes, str]
- HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
- HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
- logger = logging.getLogger("httpcore.proxy")
- def merge_headers(
- default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
- override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
- ) -> list[tuple[bytes, bytes]]:
- """
- Append default_headers and override_headers, de-duplicating if a key exists
- in both cases.
- """
- default_headers = [] if default_headers is None else list(default_headers)
- override_headers = [] if override_headers is None else list(override_headers)
- has_override = set(key.lower() for key, value in override_headers)
- default_headers = [
- (key, value)
- for key, value in default_headers
- if key.lower() not in has_override
- ]
- return default_headers + override_headers
- class AsyncHTTPProxy(AsyncConnectionPool): # pragma: nocover
- """
- A connection pool that sends requests via an HTTP proxy.
- """
- def __init__(
- self,
- proxy_url: URL | bytes | str,
- proxy_auth: tuple[bytes | str, bytes | str] | None = None,
- proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
- ssl_context: ssl.SSLContext | None = None,
- proxy_ssl_context: ssl.SSLContext | None = None,
- max_connections: int | None = 10,
- max_keepalive_connections: int | None = None,
- keepalive_expiry: float | None = None,
- http1: bool = True,
- http2: bool = False,
- retries: int = 0,
- local_address: str | None = None,
- uds: str | None = None,
- network_backend: AsyncNetworkBackend | None = None,
- socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
- ) -> None:
- """
- A connection pool for making HTTP requests.
- Parameters:
- proxy_url: The URL to use when connecting to the proxy server.
- For example `"http://127.0.0.1:8080/"`.
- proxy_auth: Any proxy authentication as a two-tuple of
- (username, password). May be either bytes or ascii-only str.
- proxy_headers: Any HTTP headers to use for the proxy requests.
- For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
- ssl_context: An SSL context to use for verifying connections.
- If not specified, the default `httpcore.default_ssl_context()`
- will be used.
- proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
- max_connections: The maximum number of concurrent HTTP connections that
- the pool should allow. Any attempt to send a request on a pool that
- would exceed this amount will block until a connection is available.
- max_keepalive_connections: The maximum number of idle HTTP connections
- that will be maintained in the pool.
- keepalive_expiry: The duration in seconds that an idle HTTP connection
- may be maintained for before being expired from the pool.
- http1: A boolean indicating if HTTP/1.1 requests should be supported
- by the connection pool. Defaults to True.
- http2: A boolean indicating if HTTP/2 requests should be supported by
- the connection pool. Defaults to False.
- retries: The maximum number of retries when trying to establish
- a connection.
- local_address: Local address to connect from. Can also be used to
- connect using a particular address family. Using
- `local_address="0.0.0.0"` will connect using an `AF_INET` address
- (IPv4), while using `local_address="::"` will connect using an
- `AF_INET6` address (IPv6).
- uds: Path to a Unix Domain Socket to use instead of TCP sockets.
- network_backend: A backend instance to use for handling network I/O.
- """
- super().__init__(
- ssl_context=ssl_context,
- max_connections=max_connections,
- max_keepalive_connections=max_keepalive_connections,
- keepalive_expiry=keepalive_expiry,
- http1=http1,
- http2=http2,
- network_backend=network_backend,
- retries=retries,
- local_address=local_address,
- uds=uds,
- socket_options=socket_options,
- )
- self._proxy_url = enforce_url(proxy_url, name="proxy_url")
- if (
- self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
- ): # pragma: no cover
- raise RuntimeError(
- "The `proxy_ssl_context` argument is not allowed for the http scheme"
- )
- self._ssl_context = ssl_context
- self._proxy_ssl_context = proxy_ssl_context
- self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
- if proxy_auth is not None:
- username = enforce_bytes(proxy_auth[0], name="proxy_auth")
- password = enforce_bytes(proxy_auth[1], name="proxy_auth")
- userpass = username + b":" + password
- authorization = b"Basic " + base64.b64encode(userpass)
- self._proxy_headers = [
- (b"Proxy-Authorization", authorization)
- ] + self._proxy_headers
- def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
- if origin.scheme == b"http":
- return AsyncForwardHTTPConnection(
- proxy_origin=self._proxy_url.origin,
- proxy_headers=self._proxy_headers,
- remote_origin=origin,
- keepalive_expiry=self._keepalive_expiry,
- network_backend=self._network_backend,
- proxy_ssl_context=self._proxy_ssl_context,
- )
- return AsyncTunnelHTTPConnection(
- proxy_origin=self._proxy_url.origin,
- proxy_headers=self._proxy_headers,
- remote_origin=origin,
- ssl_context=self._ssl_context,
- proxy_ssl_context=self._proxy_ssl_context,
- keepalive_expiry=self._keepalive_expiry,
- http1=self._http1,
- http2=self._http2,
- network_backend=self._network_backend,
- )
- class AsyncForwardHTTPConnection(AsyncConnectionInterface):
- def __init__(
- self,
- proxy_origin: Origin,
- remote_origin: Origin,
- proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
- keepalive_expiry: float | None = None,
- network_backend: AsyncNetworkBackend | None = None,
- socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
- proxy_ssl_context: ssl.SSLContext | None = None,
- ) -> None:
- self._connection = AsyncHTTPConnection(
- origin=proxy_origin,
- keepalive_expiry=keepalive_expiry,
- network_backend=network_backend,
- socket_options=socket_options,
- ssl_context=proxy_ssl_context,
- )
- self._proxy_origin = proxy_origin
- self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
- self._remote_origin = remote_origin
- async def handle_async_request(self, request: Request) -> Response:
- headers = merge_headers(self._proxy_headers, request.headers)
- url = URL(
- scheme=self._proxy_origin.scheme,
- host=self._proxy_origin.host,
- port=self._proxy_origin.port,
- target=bytes(request.url),
- )
- proxy_request = Request(
- method=request.method,
- url=url,
- headers=headers,
- content=request.stream,
- extensions=request.extensions,
- )
- return await self._connection.handle_async_request(proxy_request)
- def can_handle_request(self, origin: Origin) -> bool:
- return origin == self._remote_origin
- async def aclose(self) -> None:
- await self._connection.aclose()
- def info(self) -> str:
- return self._connection.info()
- def is_available(self) -> bool:
- return self._connection.is_available()
- def has_expired(self) -> bool:
- return self._connection.has_expired()
- def is_idle(self) -> bool:
- return self._connection.is_idle()
- def is_closed(self) -> bool:
- return self._connection.is_closed()
- def __repr__(self) -> str:
- return f"<{self.__class__.__name__} [{self.info()}]>"
- class AsyncTunnelHTTPConnection(AsyncConnectionInterface):
- def __init__(
- self,
- proxy_origin: Origin,
- remote_origin: Origin,
- ssl_context: ssl.SSLContext | None = None,
- proxy_ssl_context: ssl.SSLContext | None = None,
- proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
- keepalive_expiry: float | None = None,
- http1: bool = True,
- http2: bool = False,
- network_backend: AsyncNetworkBackend | None = None,
- socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
- ) -> None:
- self._connection: AsyncConnectionInterface = AsyncHTTPConnection(
- origin=proxy_origin,
- keepalive_expiry=keepalive_expiry,
- network_backend=network_backend,
- socket_options=socket_options,
- ssl_context=proxy_ssl_context,
- )
- self._proxy_origin = proxy_origin
- self._remote_origin = remote_origin
- self._ssl_context = ssl_context
- self._proxy_ssl_context = proxy_ssl_context
- self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
- self._keepalive_expiry = keepalive_expiry
- self._http1 = http1
- self._http2 = http2
- self._connect_lock = AsyncLock()
- self._connected = False
- async def handle_async_request(self, request: Request) -> Response:
- timeouts = request.extensions.get("timeout", {})
- timeout = timeouts.get("connect", None)
- async with self._connect_lock:
- if not self._connected:
- target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port)
- connect_url = URL(
- scheme=self._proxy_origin.scheme,
- host=self._proxy_origin.host,
- port=self._proxy_origin.port,
- target=target,
- )
- connect_headers = merge_headers(
- [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers
- )
- connect_request = Request(
- method=b"CONNECT",
- url=connect_url,
- headers=connect_headers,
- extensions=request.extensions,
- )
- connect_response = await self._connection.handle_async_request(
- connect_request
- )
- if connect_response.status < 200 or connect_response.status > 299:
- reason_bytes = connect_response.extensions.get("reason_phrase", b"")
- reason_str = reason_bytes.decode("ascii", errors="ignore")
- msg = "%d %s" % (connect_response.status, reason_str)
- await self._connection.aclose()
- raise ProxyError(msg)
- stream = connect_response.extensions["network_stream"]
- # Upgrade the stream to SSL
- ssl_context = (
- default_ssl_context()
- if self._ssl_context is None
- else self._ssl_context
- )
- alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
- ssl_context.set_alpn_protocols(alpn_protocols)
- kwargs = {
- "ssl_context": ssl_context,
- "server_hostname": self._remote_origin.host.decode("ascii"),
- "timeout": timeout,
- }
- async with Trace("start_tls", logger, request, kwargs) as trace:
- stream = await stream.start_tls(**kwargs)
- trace.return_value = stream
- # Determine if we should be using HTTP/1.1 or HTTP/2
- ssl_object = stream.get_extra_info("ssl_object")
- http2_negotiated = (
- ssl_object is not None
- and ssl_object.selected_alpn_protocol() == "h2"
- )
- # Create the HTTP/1.1 or HTTP/2 connection
- if http2_negotiated or (self._http2 and not self._http1):
- from .http2 import AsyncHTTP2Connection
- self._connection = AsyncHTTP2Connection(
- origin=self._remote_origin,
- stream=stream,
- keepalive_expiry=self._keepalive_expiry,
- )
- else:
- self._connection = AsyncHTTP11Connection(
- origin=self._remote_origin,
- stream=stream,
- keepalive_expiry=self._keepalive_expiry,
- )
- self._connected = True
- return await self._connection.handle_async_request(request)
- def can_handle_request(self, origin: Origin) -> bool:
- return origin == self._remote_origin
- async def aclose(self) -> None:
- await self._connection.aclose()
- def info(self) -> str:
- return self._connection.info()
- def is_available(self) -> bool:
- return self._connection.is_available()
- def has_expired(self) -> bool:
- return self._connection.has_expired()
- def is_idle(self) -> bool:
- return self._connection.is_idle()
- def is_closed(self) -> bool:
- return self._connection.is_closed()
- def __repr__(self) -> str:
- return f"<{self.__class__.__name__} [{self.info()}]>"
|