18
18
from .response import HTTP11Response
19
19
from ..tls import wrap_socket , H2C_PROTOCOL
20
20
from ..common .bufsocket import BufferedSocket
21
- from ..common .exceptions import TLSUpgrade , HTTPUpgrade
21
+ from ..common .exceptions import TLSUpgrade , HTTPUpgrade , ProxyError
22
22
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
+ )
24
26
from ..compat import bytes
25
27
26
28
# We prefer pycohttpparser to the pure-Python interpretation
36
38
BODY_FLAT = 2
37
39
38
40
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
+
39
78
class HTTP11Connection (object ):
40
79
"""
41
80
An object representing a single HTTP/1.1 connection to a server.
@@ -53,14 +92,16 @@ class HTTP11Connection(object):
53
92
:param proxy_host: (optional) The proxy to connect to. This can be an IP
54
93
address or a host name and may include a port.
55
94
: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,
57
96
defaults to 8080.
97
+ :param proxy_headers: (optional) The headers to send to a proxy.
58
98
"""
59
99
60
100
version = HTTPVersion .http11
61
101
62
102
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 ):
64
105
if port is None :
65
106
self .host , self .port = to_host_port_tuple (host , default_port = 80 )
66
107
else :
@@ -83,17 +124,21 @@ def __init__(self, host, port=None, secure=None, ssl_context=None,
83
124
self .ssl_context = ssl_context
84
125
self ._sock = None
85
126
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
+
86
131
# 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
94
138
else :
95
139
self .proxy_host = None
96
140
self .proxy_port = None
141
+ self .proxy_headers = proxy_headers
97
142
98
143
#: The size of the in-memory buffer used to store data from the
99
144
#: network. This is used as a performance optimisation. Increase buffer
@@ -113,19 +158,28 @@ def connect(self):
113
158
:returns: Nothing.
114
159
"""
115
160
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
122
161
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 )
124
179
proto = None
125
180
126
181
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 )
129
183
130
184
log .debug ("Selected protocol: %s" , proto )
131
185
sock = BufferedSocket (sock , self .network_buffer_size )
@@ -154,33 +208,37 @@ def request(self, method, url, body=None, headers=None):
154
208
:returns: Nothing.
155
209
"""
156
210
157
- headers = headers or {}
158
-
159
211
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 )
160
220
url = to_bytestring (url )
161
221
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
+ )
171
229
172
230
if self ._sock is None :
173
231
self .connect ()
174
232
175
- if self ._send_http_upgrade :
233
+ if not is_connect_method and self ._send_http_upgrade :
176
234
self ._add_upgrade_headers (headers )
177
235
self ._send_http_upgrade = False
178
236
179
237
# We may need extra headers.
180
238
if body :
181
239
body_type = self ._add_body_headers (headers , body )
182
240
183
- if b'host' not in headers :
241
+ if not is_connect_method and b'host' not in headers :
184
242
headers [b'host' ] = self .host
185
243
186
244
# Begin by emitting the header block.
@@ -192,13 +250,20 @@ def request(self, method, url, body=None, headers=None):
192
250
193
251
return
194
252
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
+
195
257
def get_response (self ):
196
258
"""
197
259
Returns a response object.
198
260
199
261
This is an early beta, so the response object is pretty stupid. That's
200
262
ok, we'll fix it later.
201
263
"""
264
+ method = self ._current_request_method
265
+ self ._current_request_method = None
266
+
202
267
headers = HTTPHeaderMap ()
203
268
204
269
response = None
@@ -228,7 +293,8 @@ def get_response(self):
228
293
response .msg .tobytes (),
229
294
headers ,
230
295
self ._sock ,
231
- self
296
+ self ,
297
+ method
232
298
)
233
299
234
300
def _send_headers (self , method , url , headers ):
0 commit comments