Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit 185254b

Browse files
authored
Merge pull request #322 from KostyaEsmukov/secure_proxy
Implement HTTPS proxy support
2 parents 0af177d + 2223f0d commit 185254b

13 files changed

+775
-93
lines changed

hyper/common/connection.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ class HTTPConnection(object):
4444
:param proxy_host: (optional) The proxy to connect to. This can be an IP
4545
address or a host name and may include a port.
4646
:param proxy_port: (optional) The proxy port to connect to. If not provided
47-
and one also isn't provided in the ``proxy`` parameter, defaults to
48-
8080.
47+
and one also isn't provided in the ``proxy_host`` parameter, defaults
48+
to 8080.
49+
:param proxy_headers: (optional) The headers to send to a proxy.
4950
"""
5051
def __init__(self,
5152
host,
@@ -56,19 +57,21 @@ def __init__(self,
5657
ssl_context=None,
5758
proxy_host=None,
5859
proxy_port=None,
60+
proxy_headers=None,
5961
**kwargs):
6062

6163
self._host = host
6264
self._port = port
6365
self._h1_kwargs = {
6466
'secure': secure, 'ssl_context': ssl_context,
6567
'proxy_host': proxy_host, 'proxy_port': proxy_port,
66-
'enable_push': enable_push
68+
'proxy_headers': proxy_headers, 'enable_push': enable_push
6769
}
6870
self._h2_kwargs = {
6971
'window_manager': window_manager, 'enable_push': enable_push,
7072
'secure': secure, 'ssl_context': ssl_context,
71-
'proxy_host': proxy_host, 'proxy_port': proxy_port
73+
'proxy_host': proxy_host, 'proxy_port': proxy_port,
74+
'proxy_headers': proxy_headers
7275
}
7376

7477
# Add any unexpected kwargs to both dictionaries.

hyper/common/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,22 @@ class MissingCertFile(Exception):
7171
The certificate file could not be found.
7272
"""
7373
pass
74+
75+
76+
# Create our own ConnectionError.
77+
try: # pragma: no cover
78+
ConnectionError = ConnectionError
79+
except NameError: # pragma: no cover
80+
class ConnectionError(Exception):
81+
"""
82+
An error occurred during connection to a host.
83+
"""
84+
85+
86+
class ProxyError(ConnectionError):
87+
"""
88+
An error occurred during connection to a proxy.
89+
"""
90+
def __init__(self, message, response):
91+
self.response = response
92+
super(ProxyError, self).__init__(message)

hyper/contrib.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from requests.adapters import HTTPAdapter
1010
from requests.models import Response
1111
from requests.structures import CaseInsensitiveDict
12-
from requests.utils import get_encoding_from_headers
12+
from requests.utils import (
13+
get_encoding_from_headers, select_proxy, prepend_scheme_if_needed
14+
)
1315
from requests.cookies import extract_cookies_to_jar
1416
except ImportError: # pragma: no cover
1517
HTTPAdapter = object
@@ -29,7 +31,8 @@ def __init__(self, *args, **kwargs):
2931
#: A mapping between HTTP netlocs and ``HTTP20Connection`` objects.
3032
self.connections = {}
3133

32-
def get_connection(self, host, port, scheme, cert=None, verify=True):
34+
def get_connection(self, host, port, scheme, cert=None, verify=True,
35+
proxy=None):
3336
"""
3437
Gets an appropriate HTTP/2 connection object based on
3538
host/port/scheme/cert tuples.
@@ -50,29 +53,51 @@ def get_connection(self, host, port, scheme, cert=None, verify=True):
5053
elif verify is not True:
5154
ssl_context = init_context(cert_path=verify, cert=cert)
5255

56+
if proxy:
57+
proxy_headers = self.proxy_headers(proxy)
58+
proxy_netloc = urlparse(proxy).netloc
59+
else:
60+
proxy_headers = None
61+
proxy_netloc = None
62+
63+
# We put proxy headers in the connection_key, because
64+
# ``proxy_headers`` method might be overridden, so we can't
65+
# rely on proxy headers being the same for the same proxies.
66+
proxy_headers_key = (frozenset(proxy_headers.items())
67+
if proxy_headers else None)
68+
connection_key = (host, port, scheme, cert, verify,
69+
proxy_netloc, proxy_headers_key)
5370
try:
54-
conn = self.connections[(host, port, scheme, cert, verify)]
71+
conn = self.connections[connection_key]
5572
except KeyError:
5673
conn = HTTPConnection(
5774
host,
5875
port,
5976
secure=secure,
60-
ssl_context=ssl_context)
61-
self.connections[(host, port, scheme, cert, verify)] = conn
77+
ssl_context=ssl_context,
78+
proxy_host=proxy_netloc,
79+
proxy_headers=proxy_headers)
80+
self.connections[connection_key] = conn
6281

6382
return conn
6483

65-
def send(self, request, stream=False, cert=None, verify=True, **kwargs):
84+
def send(self, request, stream=False, cert=None, verify=True, proxies=None,
85+
**kwargs):
6686
"""
6787
Sends a HTTP message to the server.
6888
"""
89+
proxy = select_proxy(request.url, proxies)
90+
if proxy:
91+
proxy = prepend_scheme_if_needed(proxy, 'http')
92+
6993
parsed = urlparse(request.url)
7094
conn = self.get_connection(
7195
parsed.hostname,
7296
parsed.port,
7397
parsed.scheme,
7498
cert=cert,
75-
verify=verify)
99+
verify=verify,
100+
proxy=proxy)
76101

77102
# Build the selector.
78103
selector = parsed.path
@@ -97,7 +122,7 @@ def send(self, request, stream=False, cert=None, verify=True, **kwargs):
97122
def build_response(self, request, resp):
98123
"""
99124
Builds a Requests' response object. This emulates most of the logic of
100-
the standard fuction but deals with the lack of the ``.headers``
125+
the standard function but deals with the lack of the ``.headers``
101126
property on the HTTP20Response object.
102127
103128
Additionally, this function builds in a number of features that are

hyper/http11/connection.py

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
from .response import HTTP11Response
1919
from ..tls import wrap_socket, H2C_PROTOCOL
2020
from ..common.bufsocket import BufferedSocket
21-
from ..common.exceptions import TLSUpgrade, HTTPUpgrade
21+
from ..common.exceptions import TLSUpgrade, HTTPUpgrade, ProxyError
2222
from ..common.headers import HTTPHeaderMap
23-
from ..common.util import to_bytestring, to_host_port_tuple, HTTPVersion
23+
from ..common.util import (
24+
to_bytestring, to_host_port_tuple, to_native_string, HTTPVersion
25+
)
2426
from ..compat import bytes
2527

2628
# We prefer pycohttpparser to the pure-Python interpretation
@@ -36,6 +38,43 @@
3638
BODY_FLAT = 2
3739

3840

41+
def _create_tunnel(proxy_host, proxy_port, target_host, target_port,
42+
proxy_headers=None):
43+
"""
44+
Sends CONNECT method to a proxy and returns a socket with established
45+
connection to the target.
46+
47+
:returns: socket
48+
"""
49+
conn = HTTP11Connection(proxy_host, proxy_port)
50+
conn.request('CONNECT', '%s:%d' % (target_host, target_port),
51+
headers=proxy_headers)
52+
53+
resp = conn.get_response()
54+
if resp.status != 200:
55+
raise ProxyError(
56+
"Tunnel connection failed: %d %s" %
57+
(resp.status, to_native_string(resp.reason)),
58+
response=resp
59+
)
60+
return conn._sock
61+
62+
63+
def _headers_to_http_header_map(headers):
64+
# TODO turn this to a classmethod of HTTPHeaderMap
65+
headers = headers or {}
66+
if not isinstance(headers, HTTPHeaderMap):
67+
if isinstance(headers, Mapping):
68+
headers = HTTPHeaderMap(headers.items())
69+
elif isinstance(headers, Iterable):
70+
headers = HTTPHeaderMap(headers)
71+
else:
72+
raise ValueError(
73+
'Header argument must be a dictionary or an iterable'
74+
)
75+
return headers
76+
77+
3978
class HTTP11Connection(object):
4079
"""
4180
An object representing a single HTTP/1.1 connection to a server.
@@ -53,14 +92,16 @@ class HTTP11Connection(object):
5392
:param proxy_host: (optional) The proxy to connect to. This can be an IP
5493
address or a host name and may include a port.
5594
:param proxy_port: (optional) The proxy port to connect to. If not provided
56-
and one also isn't provided in the ``proxy`` parameter,
95+
and one also isn't provided in the ``proxy_host`` parameter,
5796
defaults to 8080.
97+
:param proxy_headers: (optional) The headers to send to a proxy.
5898
"""
5999

60100
version = HTTPVersion.http11
61101

62102
def __init__(self, host, port=None, secure=None, ssl_context=None,
63-
proxy_host=None, proxy_port=None, **kwargs):
103+
proxy_host=None, proxy_port=None, proxy_headers=None,
104+
**kwargs):
64105
if port is None:
65106
self.host, self.port = to_host_port_tuple(host, default_port=80)
66107
else:
@@ -83,17 +124,21 @@ def __init__(self, host, port=None, secure=None, ssl_context=None,
83124
self.ssl_context = ssl_context
84125
self._sock = None
85126

127+
# Keep the current request method in order to be able to know
128+
# in get_response() what was the request verb.
129+
self._current_request_method = None
130+
86131
# Setup proxy details if applicable.
87-
if proxy_host:
88-
if proxy_port is None:
89-
self.proxy_host, self.proxy_port = to_host_port_tuple(
90-
proxy_host, default_port=8080
91-
)
92-
else:
93-
self.proxy_host, self.proxy_port = proxy_host, proxy_port
132+
if proxy_host and proxy_port is None:
133+
self.proxy_host, self.proxy_port = to_host_port_tuple(
134+
proxy_host, default_port=8080
135+
)
136+
elif proxy_host:
137+
self.proxy_host, self.proxy_port = proxy_host, proxy_port
94138
else:
95139
self.proxy_host = None
96140
self.proxy_port = None
141+
self.proxy_headers = proxy_headers
97142

98143
#: The size of the in-memory buffer used to store data from the
99144
#: network. This is used as a performance optimisation. Increase buffer
@@ -113,19 +158,28 @@ def connect(self):
113158
:returns: Nothing.
114159
"""
115160
if self._sock is None:
116-
if not self.proxy_host:
117-
host = self.host
118-
port = self.port
119-
else:
120-
host = self.proxy_host
121-
port = self.proxy_port
122161

123-
sock = socket.create_connection((host, port), 5)
162+
if self.proxy_host and self.secure:
163+
# Send http CONNECT method to a proxy and acquire the socket
164+
sock = _create_tunnel(
165+
self.proxy_host,
166+
self.proxy_port,
167+
self.host,
168+
self.port,
169+
proxy_headers=self.proxy_headers
170+
)
171+
elif self.proxy_host:
172+
# Simple http proxy
173+
sock = socket.create_connection(
174+
(self.proxy_host, self.proxy_port),
175+
5
176+
)
177+
else:
178+
sock = socket.create_connection((self.host, self.port), 5)
124179
proto = None
125180

126181
if self.secure:
127-
assert not self.proxy_host, "Proxy with HTTPS not supported."
128-
sock, proto = wrap_socket(sock, host, self.ssl_context)
182+
sock, proto = wrap_socket(sock, self.host, self.ssl_context)
129183

130184
log.debug("Selected protocol: %s", proto)
131185
sock = BufferedSocket(sock, self.network_buffer_size)
@@ -154,33 +208,37 @@ def request(self, method, url, body=None, headers=None):
154208
:returns: Nothing.
155209
"""
156210

157-
headers = headers or {}
158-
159211
method = to_bytestring(method)
212+
is_connect_method = b'CONNECT' == method.upper()
213+
self._current_request_method = method
214+
215+
if self.proxy_host and not self.secure:
216+
# As per https://tools.ietf.org/html/rfc2068#section-5.1.2:
217+
# The absoluteURI form is required when the request is being made
218+
# to a proxy.
219+
url = self._absolute_http_url(url)
160220
url = to_bytestring(url)
161221

162-
if not isinstance(headers, HTTPHeaderMap):
163-
if isinstance(headers, Mapping):
164-
headers = HTTPHeaderMap(headers.items())
165-
elif isinstance(headers, Iterable):
166-
headers = HTTPHeaderMap(headers)
167-
else:
168-
raise ValueError(
169-
'Header argument must be a dictionary or an iterable'
170-
)
222+
headers = _headers_to_http_header_map(headers)
223+
224+
# Append proxy headers.
225+
if self.proxy_host and not self.secure:
226+
headers.update(
227+
_headers_to_http_header_map(self.proxy_headers).items()
228+
)
171229

172230
if self._sock is None:
173231
self.connect()
174232

175-
if self._send_http_upgrade:
233+
if not is_connect_method and self._send_http_upgrade:
176234
self._add_upgrade_headers(headers)
177235
self._send_http_upgrade = False
178236

179237
# We may need extra headers.
180238
if body:
181239
body_type = self._add_body_headers(headers, body)
182240

183-
if b'host' not in headers:
241+
if not is_connect_method and b'host' not in headers:
184242
headers[b'host'] = self.host
185243

186244
# Begin by emitting the header block.
@@ -192,13 +250,20 @@ def request(self, method, url, body=None, headers=None):
192250

193251
return
194252

253+
def _absolute_http_url(self, url):
254+
port_part = ':%d' % self.port if self.port != 80 else ''
255+
return 'http://%s%s%s' % (self.host, port_part, url)
256+
195257
def get_response(self):
196258
"""
197259
Returns a response object.
198260
199261
This is an early beta, so the response object is pretty stupid. That's
200262
ok, we'll fix it later.
201263
"""
264+
method = self._current_request_method
265+
self._current_request_method = None
266+
202267
headers = HTTPHeaderMap()
203268

204269
response = None
@@ -228,7 +293,8 @@ def get_response(self):
228293
response.msg.tobytes(),
229294
headers,
230295
self._sock,
231-
self
296+
self,
297+
method
232298
)
233299

234300
def _send_headers(self, method, url, headers):

0 commit comments

Comments
 (0)