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

1import logging 

2import ssl 

3from base64 import b64encode 

4from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union 

5 

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 

24 

25HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] 

26HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] 

27 

28 

29logger = logging.getLogger("httpcore.proxy") 

30 

31 

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 

49 

50 

51def build_auth_header(username: bytes, password: bytes) -> bytes: 

52 userpass = username + b":" + password 

53 return b"Basic " + b64encode(userpass) 

54 

55 

56class HTTPProxy(ConnectionPool): 

57 """ 

58 A connection pool that sends requests via an HTTP proxy. 

59 """ 

60 

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. 

80 

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 

135 

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 ) 

155 

156 

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 

176 

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) 

193 

194 def can_handle_request(self, origin: Origin) -> bool: 

195 return origin == self._remote_origin 

196 

197 def close(self) -> None: 

198 self._connection.close() 

199 

200 def info(self) -> str: 

201 return self._connection.info() 

202 

203 def is_available(self) -> bool: 

204 return self._connection.is_available() 

205 

206 def has_expired(self) -> bool: 

207 return self._connection.has_expired() 

208 

209 def is_idle(self) -> bool: 

210 return self._connection.is_idle() 

211 

212 def is_closed(self) -> bool: 

213 return self._connection.is_closed() 

214 

215 def __repr__(self) -> str: 

216 return f"<{self.__class__.__name__} [{self.info()}]>" 

217 

218 

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 

247 

248 def handle_request(self, request: Request) -> Response: 

249 timeouts = request.extensions.get("timeout", {}) 

250 timeout = timeouts.get("connect", None) 

251 

252 with self._connect_lock: 

253 if not self._connected: 

254 target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port) 

255 

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 ) 

274 

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) 

281 

282 stream = connect_response.extensions["network_stream"] 

283 

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) 

292 

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 

301 

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 ) 

308 

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 

312 

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 ) 

324 

325 self._connected = True 

326 return self._connection.handle_request(request) 

327 

328 def can_handle_request(self, origin: Origin) -> bool: 

329 return origin == self._remote_origin 

330 

331 def close(self) -> None: 

332 self._connection.close() 

333 

334 def info(self) -> str: 

335 return self._connection.info() 

336 

337 def is_available(self) -> bool: 

338 return self._connection.is_available() 

339 

340 def has_expired(self) -> bool: 

341 return self._connection.has_expired() 

342 

343 def is_idle(self) -> bool: 

344 return self._connection.is_idle() 

345 

346 def is_closed(self) -> bool: 

347 return self._connection.is_closed() 

348 

349 def __repr__(self) -> str: 

350 return f"<{self.__class__.__name__} [{self.info()}]>"