1818from .response import HTTP11Response
1919from ..tls import wrap_socket , H2C_PROTOCOL
2020from ..common .bufsocket import BufferedSocket
21- from ..common .exceptions import TLSUpgrade , HTTPUpgrade
21+ from ..common .exceptions import TLSUpgrade , HTTPUpgrade , ProxyError
2222from ..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+ )
2426from ..compat import bytes
2527
2628# We prefer pycohttpparser to the pure-Python interpretation
3638BODY_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+
3978class 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