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

Commit 8c4a13b

Browse files
committed
Merge pull request #164 from fredthomsen/add_proxy_support
Add proxy support
2 parents 75ac14f + ca04c73 commit 8c4a13b

File tree

10 files changed

+273
-17
lines changed

10 files changed

+273
-17
lines changed

CONTRIBUTORS.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ In chronological order:
3636
- Fred Thomsen (@fredthomsen)
3737

3838
- Added support for upgrade of plaintext HTTP/1.1 to plaintext HTTP/2.
39-
39+
- Added proxy support.

hyper/common/connection.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class HTTPConnection(object):
4040
:meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`).
4141
:param ssl_context: (optional) A class with custom certificate settings.
4242
If not provided then hyper's default ``SSLContext`` is used instead.
43+
:param proxy_host: (optional) The proxy to connect to. This can be an IP address
44+
or a host name and may include a port.
45+
:param proxy_port: (optional) The proxy port to connect to. If not provided
46+
and one also isn't provided in the ``proxy`` parameter, defaults to 8080.
4347
"""
4448
def __init__(self,
4549
host,
@@ -48,14 +52,20 @@ def __init__(self,
4852
window_manager=None,
4953
enable_push=False,
5054
ssl_context=None,
55+
proxy_host=None,
56+
proxy_port=None,
5157
**kwargs):
5258

5359
self._host = host
5460
self._port = port
55-
self._h1_kwargs = {'secure': secure, 'ssl_context': ssl_context}
61+
self._h1_kwargs = {
62+
'secure': secure, 'ssl_context': ssl_context,
63+
'proxy_host': proxy_host, 'proxy_port': proxy_port
64+
}
5665
self._h2_kwargs = {
5766
'window_manager': window_manager, 'enable_push': enable_push,
58-
'secure': secure, 'ssl_context': ssl_context
67+
'secure': secure, 'ssl_context': ssl_context,
68+
'proxy_host': proxy_host, 'proxy_port': proxy_port
5969
}
6070

6171
# Add any unexpected kwargs to both dictionaries.

hyper/http11/connection.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ class HTTP11Connection(object):
4747
port 443.
4848
:param ssl_context: (optional) A class with custom certificate settings.
4949
If not provided then hyper's default ``SSLContext`` is used instead.
50+
:param proxy_host: (optional) The proxy to connect to. This can be an IP
51+
address or a host name and may include a port.
52+
:param proxy_port: (optional) The proxy port to connect to. If not provided
53+
and one also isn't provided in the ``proxy`` parameter,
54+
defaults to 8080.
5055
"""
51-
def __init__(self, host, port=None, secure=None, ssl_context=None,
52-
**kwargs):
56+
def __init__(self, host, port=None, secure=None, ssl_context=None,
57+
proxy_host=None, proxy_port=None, **kwargs):
5358
if port is None:
5459
try:
5560
self.host, self.port = host.split(':')
@@ -75,6 +80,21 @@ def __init__(self, host, port=None, secure=None, ssl_context=None,
7580
self.ssl_context = ssl_context
7681
self._sock = None
7782

83+
# Setup proxy details if applicable.
84+
if proxy_host:
85+
if proxy_port is None:
86+
try:
87+
self.proxy_host, self.proxy_port = proxy_host.split(':')
88+
except ValueError:
89+
self.proxy_host, self.proxy_port = proxy_host, 8080
90+
else:
91+
self.proxy_port = int(self.proxy_port)
92+
else:
93+
self.proxy_host, self.proxy_port = proxy_host, proxy_port
94+
else:
95+
self.proxy_host = None
96+
self.proxy_port = None
97+
7898
#: The size of the in-memory buffer used to store data from the
7999
#: network. This is used as a performance optimisation. Increase buffer
80100
#: size to improve performance: decrease it to conserve memory.
@@ -93,11 +113,19 @@ def connect(self):
93113
:returns: Nothing.
94114
"""
95115
if self._sock is None:
96-
sock = socket.create_connection((self.host, self.port), 5)
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
122+
123+
sock = socket.create_connection((host, port), 5)
97124
proto = None
98125

99126
if self.secure:
100-
sock, proto = wrap_socket(sock, self.host, self.ssl_context)
127+
assert not self.proxy_host, "Using a proxy with HTTPS not yet supported."
128+
sock, proto = wrap_socket(sock, host, self.ssl_context)
101129

102130
log.debug("Selected protocol: %s", proto)
103131
sock = BufferedSocket(sock, self.network_buffer_size)
@@ -176,7 +204,8 @@ def get_response(self):
176204
self._sock.advance_buffer(response.consumed)
177205

178206
if (response.status == 101 and
179-
b'upgrade' in headers['connection'] and H2C_PROTOCOL.encode('utf-8') in headers['upgrade']):
207+
b'upgrade' in headers['connection'] and
208+
H2C_PROTOCOL.encode('utf-8') in headers['upgrade']):
180209
raise HTTPUpgrade(H2C_PROTOCOL, self._sock)
181210

182211
return HTTP11Response(

hyper/http20/connection.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,13 @@ class HTTP20Connection(object):
5656
:meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`).
5757
:param ssl_context: (optional) A class with custom certificate settings.
5858
If not provided then hyper's default ``SSLContext`` is used instead.
59+
:param proxy_host: (optional) The proxy to connect to. This can be an IP address
60+
or a host name and may include a port.
61+
:param proxy_port: (optional) The proxy port to connect to. If not provided
62+
and one also isn't provided in the ``proxy`` parameter, defaults to 8080.
5963
"""
6064
def __init__(self, host, port=None, secure=None, window_manager=None, enable_push=False,
61-
ssl_context=None, **kwargs):
65+
ssl_context=None, proxy_host=None, proxy_port=None, **kwargs):
6266
"""
6367
Creates an HTTP/2 connection to a specific server.
6468
"""
@@ -81,6 +85,21 @@ def __init__(self, host, port=None, secure=None, window_manager=None, enable_pus
8185
self._enable_push = enable_push
8286
self.ssl_context = ssl_context
8387

88+
# Setup proxy details if applicable.
89+
if proxy_host:
90+
if proxy_port is None:
91+
try:
92+
self.proxy_host, self.proxy_port = proxy_host.split(':')
93+
except ValueError:
94+
self.proxy_host, self.proxy_port = proxy_host, 8080
95+
else:
96+
self.proxy_port = int(self.proxy_port)
97+
else:
98+
self.proxy_host, self.proxy_port = proxy_host, proxy_port
99+
else:
100+
self.proxy_host = None
101+
self.proxy_port = None
102+
84103
#: The size of the in-memory buffer used to store data from the
85104
#: network. This is used as a performance optimisation. Increase buffer
86105
#: size to improve performance: decrease it to conserve memory.
@@ -222,10 +241,18 @@ def connect(self):
222241
:returns: Nothing.
223242
"""
224243
if self._sock is None:
225-
sock = socket.create_connection((self.host, self.port), 5)
244+
if not self.proxy_host:
245+
host = self.host
246+
port = self.port
247+
else:
248+
host = self.proxy_host
249+
port = self.proxy_port
250+
251+
sock = socket.create_connection((host, port), 5)
226252

227253
if self.secure:
228-
sock, proto = wrap_socket(sock, self.host, self.ssl_context)
254+
assert not self.proxy_host, "Using a proxy with HTTPS not yet supported."
255+
sock, proto = wrap_socket(sock, host, self.ssl_context)
229256
else:
230257
proto = H2C_PROTOCOL
231258

test/server.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ class SocketLevelTest(object):
8787
A test-class that defines a few helper methods for running socket-level
8888
tests.
8989
"""
90-
def set_up(self, secure=True):
90+
def set_up(self, secure=True, proxy=False):
9191
self.host = None
9292
self.port = None
93-
self.secure = secure
93+
self.secure = secure if not proxy else False
94+
self.proxy = proxy
9495
self.server_thread = None
9596

9697
def _start_server(self, socket_handler):
@@ -106,15 +107,27 @@ def _start_server(self, socket_handler):
106107
)
107108
self.server_thread.start()
108109
ready_event.wait()
110+
109111
self.host = self.server_thread.host
110112
self.port = self.server_thread.port
111113
self.secure = self.server_thread.secure
112114

113115
def get_connection(self):
114116
if self.h2:
115-
return HTTP20Connection(self.host, self.port, self.secure)
117+
if not self.proxy:
118+
return HTTP20Connection(self.host, self.port, self.secure)
119+
else:
120+
return HTTP20Connection('http2bin.org', secure=self.secure,
121+
proxy_host=self.host,
122+
proxy_port=self.port)
116123
else:
117-
return HTTP11Connection(self.host, self.port, self.secure)
124+
if not self.proxy:
125+
return HTTP11Connection(self.host, self.port, self.secure)
126+
else:
127+
return HTTP11Connection('httpbin.org', secure=self.secure,
128+
proxy_host=self.host,
129+
proxy_port=self.port)
130+
118131

119132
def get_encoder(self):
120133
"""

test/test_abstraction.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ class TestHTTPConnection(object):
88
def test_h1_kwargs(self):
99
c = HTTPConnection(
1010
'test', 443, secure=False, window_manager=True, enable_push=True,
11-
ssl_context=False, other_kwarg=True
11+
ssl_context=False, proxy_host=False, proxy_port=False, other_kwarg=True
1212
)
1313

1414
assert c._h1_kwargs == {
1515
'secure': False,
1616
'ssl_context': False,
17+
'proxy_host': False,
18+
'proxy_port': False,
1719
'other_kwarg': True,
1820
}
1921

2022
def test_h2_kwargs(self):
2123
c = HTTPConnection(
2224
'test', 443, secure=False, window_manager=True, enable_push=True,
23-
ssl_context=True, other_kwarg=True
25+
ssl_context=True, proxy_host=False, proxy_port=False, other_kwarg=True
2426
)
2527

2628
assert c._h2_kwargs == {
2729
'window_manager': True,
2830
'enable_push': True,
2931
'secure': False,
3032
'ssl_context': True,
33+
'proxy_host': False,
34+
'proxy_port': False,
3135
'other_kwarg': True,
3236
}
3337

test/test_http11.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,59 @@ def test_initialization_no_port(self):
4343
assert c.host == 'httpbin.org'
4444
assert c.port == 80
4545
assert not c.secure
46+
assert not c.proxy_host
4647

4748
def test_initialization_inline_port(self):
4849
c = HTTP11Connection('httpbin.org:443')
4950

5051
assert c.host == 'httpbin.org'
5152
assert c.port == 443
5253
assert c.secure
54+
assert not c.proxy_host
5355

5456
def test_initialization_separate_port(self):
5557
c = HTTP11Connection('localhost', 8080)
5658

5759
assert c.host == 'localhost'
5860
assert c.port == 8080
5961
assert not c.secure
62+
assert not c.proxy_host
6063

6164
def test_can_override_security(self):
6265
c = HTTP11Connection('localhost', 443, secure=False)
6366

6467
assert c.host == 'localhost'
6568
assert c.port == 443
6669
assert not c.secure
70+
assert not c.proxy_host
71+
72+
def test_initialization_proxy(self):
73+
c = HTTP11Connection('httpbin.org', proxy_host='localhost')
74+
75+
assert c.host == 'httpbin.org'
76+
assert c.port == 80
77+
assert not c.secure
78+
assert c.proxy_host == 'localhost'
79+
assert c.proxy_port == 8080
80+
81+
def test_initialization_proxy_with_inline_port(self):
82+
c = HTTP11Connection('httpbin.org', proxy_host='localhost:8443')
83+
84+
assert c.host == 'httpbin.org'
85+
assert c.port == 80
86+
assert not c.secure
87+
assert c.proxy_host == 'localhost'
88+
assert c.proxy_port == 8443
89+
90+
def test_initialization_proxy_with_separate_port(self):
91+
c = HTTP11Connection('httpbin.org', proxy_host='localhost', proxy_port=8443)
92+
93+
assert c.host == 'httpbin.org'
94+
assert c.port == 80
95+
assert not c.secure
96+
assert c.proxy_host == 'localhost'
97+
assert c.proxy_port == 8443
98+
6799

68100
def test_basic_request(self):
69101
c = HTTP11Connection('httpbin.org')
@@ -84,6 +116,25 @@ def test_basic_request(self):
84116

85117
assert received == expected
86118

119+
def test_proxy_request(self):
120+
c = HTTP11Connection('httpbin.org', proxy_host='localhost')
121+
c._sock = sock = DummySocket()
122+
123+
c.request('GET', '/get', headers={'User-Agent': 'hyper'})
124+
125+
expected = (
126+
b"GET /get HTTP/1.1\r\n"
127+
b"User-Agent: hyper\r\n"
128+
b"connection: Upgrade, HTTP2-Settings\r\n"
129+
b"upgrade: h2c\r\n"
130+
b"HTTP2-Settings: AAQAAP//\r\n"
131+
b"host: httpbin.org\r\n"
132+
b"\r\n"
133+
)
134+
received = b''.join(sock.queue)
135+
136+
assert received == expected
137+
87138
def test_request_with_bytestring_body(self):
88139
c = HTTP11Connection('httpbin.org')
89140
c._sock = sock = DummySocket()

test/test_hyper.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,27 @@ def test_connections_accept_hosts_and_ports(self):
4141
c = HTTP20Connection(host='www.google.com', port=8080)
4242
assert c.host =='www.google.com'
4343
assert c.port == 8080
44+
assert c.proxy_host == None
4445

4546
def test_connections_can_parse_hosts_and_ports(self):
4647
c = HTTP20Connection('www.google.com:8080')
4748
assert c.host == 'www.google.com'
4849
assert c.port == 8080
50+
assert c.proxy_host == None
51+
52+
def test_connections_accept_proxy_hosts_and_ports(self):
53+
c = HTTP20Connection('www.google.com', proxy_host='localhost:8443')
54+
assert c.host == 'www.google.com'
55+
assert c.proxy_host == 'localhost'
56+
assert c.proxy_port == 8443
57+
58+
def test_connections_can_parse_proxy_hosts_and_ports(self):
59+
c = HTTP20Connection('www.google.com',
60+
proxy_host='localhost',
61+
proxy_port=8443)
62+
assert c.host == 'www.google.com'
63+
assert c.proxy_host == 'localhost'
64+
assert c.proxy_port == 8443
4965

5066
def test_putrequest_establishes_new_stream(self):
5167
c = HTTP20Connection("www.google.com")
@@ -469,6 +485,19 @@ def data_cb(frame, *args):
469485
assert frames[0].type == WindowUpdateFrame.type
470486
assert frames[0].window_increment == 5535
471487

488+
def test_that_using_proxy_keeps_http_headers_intact(self):
489+
sock = DummySocket()
490+
c = HTTP20Connection('www.google.com', secure=False, proxy_host='localhost')
491+
c._sock = sock
492+
c.request('GET', '/')
493+
s = c.recent_stream
494+
495+
assert s.headers == [
496+
(':method', 'GET'),
497+
(':scheme', 'http'),
498+
(':authority', 'www.google.com'),
499+
(':path', '/'),
500+
]
472501

473502
class TestServerPush(object):
474503
def setup_method(self, method):

0 commit comments

Comments
 (0)