Skip to content

Commit 5a13194

Browse files
committed
aiohttp proxy response headers
1 parent 3901e03 commit 5a13194

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ r.headers['X-ProxyMesh-IP']
114114
```
115115

116116
10. Is there a requests async library worth extending? aiohttp
117+
118+
proxy headers works
119+
tested with aiohttp 3.11.12 and python3.12
120+
``` python
121+
from python_proxy_headers import aiohttp_proxy
122+
async with aiohttp_proxy.ProxyClientSession() as session:
123+
async with session.get('https://proxymesh.com/api/headers/', proxy="http://de.proxymesh.com:31280", proxy_headers={'X-ProxyMesh-IP': '46.101.236.88'}) as r:
124+
await r.text()
125+
126+
r.headers['X-ProxyMesh-IP']
127+
```
128+
117129
11. Update proxy-examples repository
118130

119131
**TODO: rename modules to be more clear**
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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

Comments
 (0)