Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpcore/_sync/http_proxy.py: 34%
128 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 07:19 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 07:19 +0000
1import logging
2import ssl
3from base64 import b64encode
4from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union
6from .._exceptions import ProxyError
7from .._models import (
8 URL,
9 Origin,
10 Request,
11 Response,
12 enforce_bytes,
13 enforce_headers,
14 enforce_url,
15)
16from .._ssl import default_ssl_context
17from .._synchronization import Lock
18from .._trace import Trace
19from ..backends.base import SOCKET_OPTION, NetworkBackend
20from .connection import HTTPConnection
21from .connection_pool import ConnectionPool
22from .http11 import HTTP11Connection
23from .interfaces import ConnectionInterface
25HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]]
26HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]]
29logger = logging.getLogger("httpcore.proxy")
32def merge_headers(
33 default_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
34 override_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
35) -> List[Tuple[bytes, bytes]]:
36 """
37 Append default_headers and override_headers, de-duplicating if a key exists
38 in both cases.
39 """
40 default_headers = [] if default_headers is None else list(default_headers)
41 override_headers = [] if override_headers is None else list(override_headers)
42 has_override = set(key.lower() for key, value in override_headers)
43 default_headers = [
44 (key, value)
45 for key, value in default_headers
46 if key.lower() not in has_override
47 ]
48 return default_headers + override_headers
51def build_auth_header(username: bytes, password: bytes) -> bytes:
52 userpass = username + b":" + password
53 return b"Basic " + b64encode(userpass)
56class HTTPProxy(ConnectionPool):
57 """
58 A connection pool that sends requests via an HTTP proxy.
59 """
61 def __init__(
62 self,
63 proxy_url: Union[URL, bytes, str],
64 proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] = None,
65 proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
66 ssl_context: Optional[ssl.SSLContext] = None,
67 max_connections: Optional[int] = 10,
68 max_keepalive_connections: Optional[int] = None,
69 keepalive_expiry: Optional[float] = None,
70 http1: bool = True,
71 http2: bool = False,
72 retries: int = 0,
73 local_address: Optional[str] = None,
74 uds: Optional[str] = None,
75 network_backend: Optional[NetworkBackend] = None,
76 socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
77 ) -> None:
78 """
79 A connection pool for making HTTP requests.
81 Parameters:
82 proxy_url: The URL to use when connecting to the proxy server.
83 For example `"http://127.0.0.1:8080/"`.
84 proxy_auth: Any proxy authentication as a two-tuple of
85 (username, password). May be either bytes or ascii-only str.
86 proxy_headers: Any HTTP headers to use for the proxy requests.
87 For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
88 ssl_context: An SSL context to use for verifying connections.
89 If not specified, the default `httpcore.default_ssl_context()`
90 will be used.
91 max_connections: The maximum number of concurrent HTTP connections that
92 the pool should allow. Any attempt to send a request on a pool that
93 would exceed this amount will block until a connection is available.
94 max_keepalive_connections: The maximum number of idle HTTP connections
95 that will be maintained in the pool.
96 keepalive_expiry: The duration in seconds that an idle HTTP connection
97 may be maintained for before being expired from the pool.
98 http1: A boolean indicating if HTTP/1.1 requests should be supported
99 by the connection pool. Defaults to True.
100 http2: A boolean indicating if HTTP/2 requests should be supported by
101 the connection pool. Defaults to False.
102 retries: The maximum number of retries when trying to establish
103 a connection.
104 local_address: Local address to connect from. Can also be used to
105 connect using a particular address family. Using
106 `local_address="0.0.0.0"` will connect using an `AF_INET` address
107 (IPv4), while using `local_address="::"` will connect using an
108 `AF_INET6` address (IPv6).
109 uds: Path to a Unix Domain Socket to use instead of TCP sockets.
110 network_backend: A backend instance to use for handling network I/O.
111 """
112 super().__init__(
113 ssl_context=ssl_context,
114 max_connections=max_connections,
115 max_keepalive_connections=max_keepalive_connections,
116 keepalive_expiry=keepalive_expiry,
117 http1=http1,
118 http2=http2,
119 network_backend=network_backend,
120 retries=retries,
121 local_address=local_address,
122 uds=uds,
123 socket_options=socket_options,
124 )
125 self._ssl_context = ssl_context
126 self._proxy_url = enforce_url(proxy_url, name="proxy_url")
127 self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
128 if proxy_auth is not None:
129 username = enforce_bytes(proxy_auth[0], name="proxy_auth")
130 password = enforce_bytes(proxy_auth[1], name="proxy_auth")
131 authorization = build_auth_header(username, password)
132 self._proxy_headers = [
133 (b"Proxy-Authorization", authorization)
134 ] + self._proxy_headers
136 def create_connection(self, origin: Origin) -> ConnectionInterface:
137 if origin.scheme == b"http":
138 return ForwardHTTPConnection(
139 proxy_origin=self._proxy_url.origin,
140 proxy_headers=self._proxy_headers,
141 remote_origin=origin,
142 keepalive_expiry=self._keepalive_expiry,
143 network_backend=self._network_backend,
144 )
145 return TunnelHTTPConnection(
146 proxy_origin=self._proxy_url.origin,
147 proxy_headers=self._proxy_headers,
148 remote_origin=origin,
149 ssl_context=self._ssl_context,
150 keepalive_expiry=self._keepalive_expiry,
151 http1=self._http1,
152 http2=self._http2,
153 network_backend=self._network_backend,
154 )
157class ForwardHTTPConnection(ConnectionInterface):
158 def __init__(
159 self,
160 proxy_origin: Origin,
161 remote_origin: Origin,
162 proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
163 keepalive_expiry: Optional[float] = None,
164 network_backend: Optional[NetworkBackend] = None,
165 socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
166 ) -> None:
167 self._connection = HTTPConnection(
168 origin=proxy_origin,
169 keepalive_expiry=keepalive_expiry,
170 network_backend=network_backend,
171 socket_options=socket_options,
172 )
173 self._proxy_origin = proxy_origin
174 self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
175 self._remote_origin = remote_origin
177 def handle_request(self, request: Request) -> Response:
178 headers = merge_headers(self._proxy_headers, request.headers)
179 url = URL(
180 scheme=self._proxy_origin.scheme,
181 host=self._proxy_origin.host,
182 port=self._proxy_origin.port,
183 target=bytes(request.url),
184 )
185 proxy_request = Request(
186 method=request.method,
187 url=url,
188 headers=headers,
189 content=request.stream,
190 extensions=request.extensions,
191 )
192 return self._connection.handle_request(proxy_request)
194 def can_handle_request(self, origin: Origin) -> bool:
195 return origin == self._remote_origin
197 def close(self) -> None:
198 self._connection.close()
200 def info(self) -> str:
201 return self._connection.info()
203 def is_available(self) -> bool:
204 return self._connection.is_available()
206 def has_expired(self) -> bool:
207 return self._connection.has_expired()
209 def is_idle(self) -> bool:
210 return self._connection.is_idle()
212 def is_closed(self) -> bool:
213 return self._connection.is_closed()
215 def __repr__(self) -> str:
216 return f"<{self.__class__.__name__} [{self.info()}]>"
219class TunnelHTTPConnection(ConnectionInterface):
220 def __init__(
221 self,
222 proxy_origin: Origin,
223 remote_origin: Origin,
224 ssl_context: Optional[ssl.SSLContext] = None,
225 proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
226 keepalive_expiry: Optional[float] = None,
227 http1: bool = True,
228 http2: bool = False,
229 network_backend: Optional[NetworkBackend] = None,
230 socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
231 ) -> None:
232 self._connection: ConnectionInterface = HTTPConnection(
233 origin=proxy_origin,
234 keepalive_expiry=keepalive_expiry,
235 network_backend=network_backend,
236 socket_options=socket_options,
237 )
238 self._proxy_origin = proxy_origin
239 self._remote_origin = remote_origin
240 self._ssl_context = ssl_context
241 self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
242 self._keepalive_expiry = keepalive_expiry
243 self._http1 = http1
244 self._http2 = http2
245 self._connect_lock = Lock()
246 self._connected = False
248 def handle_request(self, request: Request) -> Response:
249 timeouts = request.extensions.get("timeout", {})
250 timeout = timeouts.get("connect", None)
252 with self._connect_lock:
253 if not self._connected:
254 target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port)
256 connect_url = URL(
257 scheme=self._proxy_origin.scheme,
258 host=self._proxy_origin.host,
259 port=self._proxy_origin.port,
260 target=target,
261 )
262 connect_headers = merge_headers(
263 [(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers
264 )
265 connect_request = Request(
266 method=b"CONNECT",
267 url=connect_url,
268 headers=connect_headers,
269 extensions=request.extensions,
270 )
271 connect_response = self._connection.handle_request(
272 connect_request
273 )
275 if connect_response.status < 200 or connect_response.status > 299:
276 reason_bytes = connect_response.extensions.get("reason_phrase", b"")
277 reason_str = reason_bytes.decode("ascii", errors="ignore")
278 msg = "%d %s" % (connect_response.status, reason_str)
279 self._connection.close()
280 raise ProxyError(msg)
282 stream = connect_response.extensions["network_stream"]
284 # Upgrade the stream to SSL
285 ssl_context = (
286 default_ssl_context()
287 if self._ssl_context is None
288 else self._ssl_context
289 )
290 alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
291 ssl_context.set_alpn_protocols(alpn_protocols)
293 kwargs = {
294 "ssl_context": ssl_context,
295 "server_hostname": self._remote_origin.host.decode("ascii"),
296 "timeout": timeout,
297 }
298 with Trace("start_tls", logger, request, kwargs) as trace:
299 stream = stream.start_tls(**kwargs)
300 trace.return_value = stream
302 # Determine if we should be using HTTP/1.1 or HTTP/2
303 ssl_object = stream.get_extra_info("ssl_object")
304 http2_negotiated = (
305 ssl_object is not None
306 and ssl_object.selected_alpn_protocol() == "h2"
307 )
309 # Create the HTTP/1.1 or HTTP/2 connection
310 if http2_negotiated or (self._http2 and not self._http1):
311 from .http2 import HTTP2Connection
313 self._connection = HTTP2Connection(
314 origin=self._remote_origin,
315 stream=stream,
316 keepalive_expiry=self._keepalive_expiry,
317 )
318 else:
319 self._connection = HTTP11Connection(
320 origin=self._remote_origin,
321 stream=stream,
322 keepalive_expiry=self._keepalive_expiry,
323 )
325 self._connected = True
326 return self._connection.handle_request(request)
328 def can_handle_request(self, origin: Origin) -> bool:
329 return origin == self._remote_origin
331 def close(self) -> None:
332 self._connection.close()
334 def info(self) -> str:
335 return self._connection.info()
337 def is_available(self) -> bool:
338 return self._connection.is_available()
340 def has_expired(self) -> bool:
341 return self._connection.has_expired()
343 def is_idle(self) -> bool:
344 return self._connection.is_idle()
346 def is_closed(self) -> bool:
347 return self._connection.is_closed()
349 def __repr__(self) -> str:
350 return f"<{self.__class__.__name__} [{self.info()}]>"