1+ from http import HTTPStatus
2+ from aiohttp .client_reqrep import ClientRequest , ClientResponse
3+ from aiohttp .connector import TCPConnector , Connection
4+ from aiohttp .client_exceptions import ClientHttpProxyError , ClientProxyConnectionError
5+ from aiohttp .client import ClientSession
6+ from aiohttp .helpers import reify
7+ from aiohttp import hdrs
8+ from multidict import CIMultiDict , CIMultiDictProxy
9+
10+ class ProxyTCPConnector (TCPConnector ):
11+ async def _create_proxy_connection (self , req : ClientRequest , traces , timeout ):
12+ self ._fail_on_no_start_tls (req )
13+ runtime_has_start_tls = self ._loop_supports_start_tls ()
14+
15+ headers = {}
16+ if req .proxy_headers is not None :
17+ headers = req .proxy_headers # type: ignore[assignment]
18+ headers [hdrs .HOST ] = req .headers [hdrs .HOST ]
19+
20+ url = req .proxy
21+ assert url is not None
22+ proxy_req = ClientRequest (
23+ hdrs .METH_GET ,
24+ url ,
25+ headers = headers ,
26+ auth = req .proxy_auth ,
27+ loop = self ._loop ,
28+ ssl = req .ssl ,
29+ )
30+
31+ # create connection to proxy server
32+ transport , proto = await self ._create_direct_connection (
33+ proxy_req , [], timeout , client_error = ClientProxyConnectionError
34+ )
35+
36+ auth = proxy_req .headers .pop (hdrs .AUTHORIZATION , None )
37+ if auth is not None :
38+ if not req .is_ssl ():
39+ req .headers [hdrs .PROXY_AUTHORIZATION ] = auth
40+ else :
41+ proxy_req .headers [hdrs .PROXY_AUTHORIZATION ] = auth
42+
43+ if req .is_ssl ():
44+ if runtime_has_start_tls :
45+ self ._warn_about_tls_in_tls (transport , req )
46+
47+ # For HTTPS requests over HTTP proxy
48+ # we must notify proxy to tunnel connection
49+ # so we send CONNECT command:
50+ # CONNECT www.python.org:443 HTTP/1.1
51+ # Host: www.python.org
52+ #
53+ # next we must do TLS handshake and so on
54+ # to do this we must wrap raw socket into secure one
55+ # asyncio handles this perfectly
56+ proxy_req .method = hdrs .METH_CONNECT
57+ proxy_req .url = req .url
58+ key = req .connection_key ._replace (
59+ proxy = None , proxy_auth = None , proxy_headers_hash = None
60+ )
61+ conn = Connection (self , key , proto , self ._loop )
62+ proxy_resp = await proxy_req .send (conn )
63+ try :
64+ protocol = conn ._protocol
65+ assert protocol is not None
66+
67+ # read_until_eof=True will ensure the connection isn't closed
68+ # once the response is received and processed allowing
69+ # START_TLS to work on the connection below.
70+ protocol .set_response_params (
71+ read_until_eof = runtime_has_start_tls ,
72+ timeout_ceil_threshold = self ._timeout_ceil_threshold ,
73+ )
74+ resp = await proxy_resp .start (conn )
75+ except BaseException :
76+ proxy_resp .close ()
77+ conn .close ()
78+ raise
79+ else :
80+ conn ._protocol = None
81+ try :
82+ if resp .status != 200 :
83+ message = resp .reason
84+ if message is None :
85+ message = HTTPStatus (resp .status ).phrase
86+ raise ClientHttpProxyError (
87+ proxy_resp .request_info ,
88+ resp .history ,
89+ status = resp .status ,
90+ message = message ,
91+ headers = resp .headers ,
92+ )
93+ if not runtime_has_start_tls :
94+ rawsock = transport .get_extra_info ("socket" , default = None )
95+ if rawsock is None :
96+ raise RuntimeError (
97+ "Transport does not expose socket instance"
98+ )
99+ # Duplicate the socket, so now we can close proxy transport
100+ rawsock = rawsock .dup ()
101+ except BaseException :
102+ # It shouldn't be closed in `finally` because it's fed to
103+ # `loop.start_tls()` and the docs say not to touch it after
104+ # passing there.
105+ transport .close ()
106+ raise
107+ finally :
108+ if not runtime_has_start_tls :
109+ transport .close ()
110+
111+ # TODO: try adding resp.headers to the proto returned as 2nd tuple element below
112+ if not runtime_has_start_tls :
113+ # HTTP proxy with support for upgrade to HTTPS
114+ sslcontext = self ._get_ssl_context (req )
115+ transport , proto = await self ._wrap_existing_connection (
116+ self ._factory ,
117+ timeout = timeout ,
118+ ssl = sslcontext ,
119+ sock = rawsock ,
120+ server_hostname = req .host ,
121+ req = req ,
122+ )
123+
124+ transport , proto = await self ._start_tls_connection (
125+ # Access the old transport for the last time before it's
126+ # closed and forgotten forever:
127+ transport ,
128+ req = req ,
129+ timeout = timeout ,
130+ )
131+ finally :
132+ proxy_resp .close ()
133+
134+ proto ._proxy_headers = resp .headers
135+ return transport , proto
136+
137+
138+ class ProxyClientRequest (ClientRequest ):
139+ async def send (self , conn ):
140+ resp = await super ().send (conn )
141+ if hasattr (conn .protocol , '_proxy_headers' ):
142+ resp ._proxy_headers = conn .protocol ._proxy_headers
143+ return resp
144+
145+ class ProxyClientResponse (ClientResponse ):
146+ @reify
147+ def headers (self ):
148+ proxy_headers = getattr (self , '_proxy_headers' , None )
149+
150+ if proxy_headers :
151+ headers = CIMultiDict (self ._headers )
152+ headers .extend (proxy_headers )
153+ return CIMultiDictProxy (headers )
154+ else :
155+ return self ._headers
156+
157+ class ProxyClientSession (ClientSession ):
158+ def __init__ (self , * args , ** kwargs ):
159+ super ().__init__ (connector = ProxyTCPConnector (), response_class = ProxyClientResponse ,request_class = ProxyClientRequest , * args , ** kwargs )
0 commit comments