Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(526)

Side by Side Diff: third_party/fancy_urllib/__init__.py

Issue 12317103: Added gsutil to depot tools (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/fancy_urllib/README ('k') | third_party/gsutil/CHECKSUM » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software
4 # Foundation; All Rights Reserved
5
6 """A HTTPSConnection/Handler with additional proxy and cert validation features.
7
8 In particular, monkey patches in Python r74203 to provide support for CONNECT
9 proxies and adds SSL cert validation if the ssl module is present.
10 """
11
12 __author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)"
13
14 import base64
15 import httplib
16 import logging
17 import re
18 import socket
19 import urllib2
20
21 from urllib import splittype
22 from urllib import splituser
23 from urllib import splitpasswd
24
25 class InvalidCertificateException(httplib.HTTPException):
26 """Raised when a certificate is provided with an invalid hostname."""
27
28 def __init__(self, host, cert, reason):
29 """Constructor.
30
31 Args:
32 host: The hostname the connection was made to.
33 cert: The SSL certificate (as a dictionary) the host returned.
34 """
35 httplib.HTTPException.__init__(self)
36 self.host = host
37 self.cert = cert
38 self.reason = reason
39
40 def __str__(self):
41 return ('Host %s returned an invalid certificate (%s): %s\n'
42 'To learn more, see '
43 'http://code.google.com/appengine/kb/general.html#rpcssl' %
44 (self.host, self.reason, self.cert))
45
46 def can_validate_certs():
47 """Return True if we have the SSL package and can validate certificates."""
48 try:
49 import ssl
50 return True
51 except ImportError:
52 return False
53
54 def _create_fancy_connection(tunnel_host=None, key_file=None,
55 cert_file=None, ca_certs=None):
56 # This abomination brought to you by the fact that
57 # the HTTPHandler creates the connection instance in the middle
58 # of do_open so we need to add the tunnel host to the class.
59
60 class PresetProxyHTTPSConnection(httplib.HTTPSConnection):
61 """An HTTPS connection that uses a proxy defined by the enclosing scope."""
62
63 def __init__(self, *args, **kwargs):
64 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
65
66 self._tunnel_host = tunnel_host
67 if tunnel_host:
68 logging.debug("Creating preset proxy https conn: %s", tunnel_host)
69
70 self.key_file = key_file
71 self.cert_file = cert_file
72 self.ca_certs = ca_certs
73 try:
74 import ssl
75 if self.ca_certs:
76 self.cert_reqs = ssl.CERT_REQUIRED
77 else:
78 self.cert_reqs = ssl.CERT_NONE
79 except ImportError:
80 pass
81
82 def _tunnel(self):
83 self._set_hostport(self._tunnel_host, None)
84 logging.info("Connecting through tunnel to: %s:%d",
85 self.host, self.port)
86 self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self.host, self.port))
87 response = self.response_class(self.sock, strict=self.strict,
88 method=self._method)
89 (_, code, message) = response._read_status()
90
91 if code != 200:
92 self.close()
93 raise socket.error, "Tunnel connection failed: %d %s" % (
94 code, message.strip())
95
96 while True:
97 line = response.fp.readline()
98 if line == "\r\n":
99 break
100
101 def _get_valid_hosts_for_cert(self, cert):
102 """Returns a list of valid host globs for an SSL certificate.
103
104 Args:
105 cert: A dictionary representing an SSL certificate.
106 Returns:
107 list: A list of valid host globs.
108 """
109 if 'subjectAltName' in cert:
110 return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns']
111 else:
112 # Return a list of commonName fields
113 return [x[0][1] for x in cert['subject']
114 if x[0][0].lower() == 'commonname']
115
116 def _validate_certificate_hostname(self, cert, hostname):
117 """Validates that a given hostname is valid for an SSL certificate.
118
119 Args:
120 cert: A dictionary representing an SSL certificate.
121 hostname: The hostname to test.
122 Returns:
123 bool: Whether or not the hostname is valid for this certificate.
124 """
125 hosts = self._get_valid_hosts_for_cert(cert)
126 for host in hosts:
127 # Convert the glob-style hostname expression (eg, '*.google.com') into a
128 # valid regular expression.
129 host_re = host.replace('.', '\.').replace('*', '[^.]*')
130 if re.search('^%s$' % (host_re,), hostname, re.I):
131 return True
132 return False
133
134
135 def connect(self):
136 # TODO(frew): When we drop support for <2.6 (in the far distant future),
137 # change this to socket.create_connection.
138 self.sock = _create_connection((self.host, self.port))
139
140 if self._tunnel_host:
141 self._tunnel()
142
143 # ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl,
144 # with fallback.
145 try:
146 import ssl
147 self.sock = ssl.wrap_socket(self.sock,
148 keyfile=self.key_file,
149 certfile=self.cert_file,
150 ca_certs=self.ca_certs,
151 cert_reqs=self.cert_reqs)
152
153 if self.cert_reqs & ssl.CERT_REQUIRED:
154 cert = self.sock.getpeercert()
155 hostname = self.host.split(':', 0)[0]
156 if not self._validate_certificate_hostname(cert, hostname):
157 raise InvalidCertificateException(hostname, cert,
158 'hostname mismatch')
159 except ImportError:
160 ssl = socket.ssl(self.sock,
161 keyfile=self.key_file,
162 certfile=self.cert_file)
163 self.sock = httplib.FakeSocket(self.sock, ssl)
164
165 return PresetProxyHTTPSConnection
166
167
168 # Here to end of _create_connection copied wholesale from Python 2.6"s socket.py
169 _GLOBAL_DEFAULT_TIMEOUT = object()
170
171
172 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT):
173 """Connect to *address* and return the socket object.
174
175 Convenience function. Connect to *address* (a 2-tuple ``(host,
176 port)``) and return the socket object. Passing the optional
177 *timeout* parameter will set the timeout on the socket instance
178 before attempting to connect. If no *timeout* is supplied, the
179 global default timeout setting returned by :func:`getdefaulttimeout`
180 is used.
181 """
182
183 msg = "getaddrinfo returns an empty list"
184 host, port = address
185 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
186 af, socktype, proto, canonname, sa = res
187 sock = None
188 try:
189 sock = socket.socket(af, socktype, proto)
190 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
191 sock.settimeout(timeout)
192 sock.connect(sa)
193 return sock
194
195 except socket.error, msg:
196 if sock is not None:
197 sock.close()
198
199 raise socket.error, msg
200
201
202 class FancyRequest(urllib2.Request):
203 """A request that allows the use of a CONNECT proxy."""
204
205 def __init__(self, *args, **kwargs):
206 urllib2.Request.__init__(self, *args, **kwargs)
207 self._tunnel_host = None
208 self._key_file = None
209 self._cert_file = None
210 self._ca_certs = None
211
212 def set_proxy(self, host, type):
213 saved_type = None
214
215 if self.get_type() == "https" and not self._tunnel_host:
216 self._tunnel_host = self.get_host()
217 saved_type = self.get_type()
218 urllib2.Request.set_proxy(self, host, type)
219
220 if saved_type:
221 # Don't set self.type, we want to preserve the
222 # type for tunneling.
223 self.type = saved_type
224
225 def set_ssl_info(self, key_file=None, cert_file=None, ca_certs=None):
226 self._key_file = key_file
227 self._cert_file = cert_file
228 self._ca_certs = ca_certs
229
230
231 class FancyProxyHandler(urllib2.ProxyHandler):
232 """A ProxyHandler that works with CONNECT-enabled proxies."""
233
234 # Taken verbatim from /usr/lib/python2.5/urllib2.py
235 def _parse_proxy(self, proxy):
236 """Return (scheme, user, password, host/port) given a URL or an authority.
237
238 If a URL is supplied, it must have an authority (host:port) component.
239 According to RFC 3986, having an authority component means the URL must
240 have two slashes after the scheme:
241
242 >>> _parse_proxy('file:/ftp.example.com/')
243 Traceback (most recent call last):
244 ValueError: proxy URL with no authority: 'file:/ftp.example.com/'
245
246 The first three items of the returned tuple may be None.
247
248 Examples of authority parsing:
249
250 >>> _parse_proxy('proxy.example.com')
251 (None, None, None, 'proxy.example.com')
252 >>> _parse_proxy('proxy.example.com:3128')
253 (None, None, None, 'proxy.example.com:3128')
254
255 The authority component may optionally include userinfo (assumed to be
256 username:password):
257
258 >>> _parse_proxy('joe:password@proxy.example.com')
259 (None, 'joe', 'password', 'proxy.example.com')
260 >>> _parse_proxy('joe:password@proxy.example.com:3128')
261 (None, 'joe', 'password', 'proxy.example.com:3128')
262
263 Same examples, but with URLs instead:
264
265 >>> _parse_proxy('http://proxy.example.com/')
266 ('http', None, None, 'proxy.example.com')
267 >>> _parse_proxy('http://proxy.example.com:3128/')
268 ('http', None, None, 'proxy.example.com:3128')
269 >>> _parse_proxy('http://joe:password@proxy.example.com/')
270 ('http', 'joe', 'password', 'proxy.example.com')
271 >>> _parse_proxy('http://joe:password@proxy.example.com:3128')
272 ('http', 'joe', 'password', 'proxy.example.com:3128')
273
274 Everything after the authority is ignored:
275
276 >>> _parse_proxy('ftp://joe:password@proxy.example.com/rubbish:3128')
277 ('ftp', 'joe', 'password', 'proxy.example.com')
278
279 Test for no trailing '/' case:
280
281 >>> _parse_proxy('http://joe:password@proxy.example.com')
282 ('http', 'joe', 'password', 'proxy.example.com')
283
284 """
285 scheme, r_scheme = splittype(proxy)
286 if not r_scheme.startswith("/"):
287 # authority
288 scheme = None
289 authority = proxy
290 else:
291 # URL
292 if not r_scheme.startswith("//"):
293 raise ValueError("proxy URL with no authority: %r" % proxy)
294 # We have an authority, so for RFC 3986-compliant URLs (by ss 3.
295 # and 3.3.), path is empty or starts with '/'
296 end = r_scheme.find("/", 2)
297 if end == -1:
298 end = None
299 authority = r_scheme[2:end]
300 userinfo, hostport = splituser(authority)
301 if userinfo is not None:
302 user, password = splitpasswd(userinfo)
303 else:
304 user = password = None
305 return scheme, user, password, hostport
306
307 def proxy_open(self, req, proxy, type):
308 # This block is copied wholesale from Python2.6 urllib2.
309 # It is idempotent, so the superclass method call executes as normal
310 # if invoked.
311 orig_type = req.get_type()
312 proxy_type, user, password, hostport = self._parse_proxy(proxy)
313 if proxy_type is None:
314 proxy_type = orig_type
315 if user and password:
316 user_pass = "%s:%s" % (urllib2.unquote(user), urllib2.unquote(password))
317 creds = base64.b64encode(user_pass).strip()
318 # Later calls overwrite earlier calls for the same header
319 req.add_header("Proxy-authorization", "Basic " + creds)
320 hostport = urllib2.unquote(hostport)
321 req.set_proxy(hostport, proxy_type)
322 # This condition is the change
323 if orig_type == "https":
324 return None
325
326 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type)
327
328
329 class FancyHTTPSHandler(urllib2.HTTPSHandler):
330 """An HTTPSHandler that works with CONNECT-enabled proxies."""
331
332 def do_open(self, http_class, req):
333 # Intentionally very specific so as to opt for false negatives
334 # rather than false positives.
335 try:
336 return urllib2.HTTPSHandler.do_open(
337 self,
338 _create_fancy_connection(req._tunnel_host,
339 req._key_file,
340 req._cert_file,
341 req._ca_certs),
342 req)
343 except urllib2.URLError, url_error:
344 try:
345 import ssl
346 if (type(url_error.reason) == ssl.SSLError and
347 url_error.reason.args[0] == 1):
348 # Display the reason to the user. Need to use args for python2.5
349 # compat.
350 raise InvalidCertificateException(req.host, '',
351 url_error.reason.args[1])
352 except ImportError:
353 pass
354
355 raise url_error
356
357
358 # We have to implement this so that we persist the tunneling behavior
359 # through redirects.
360 class FancyRedirectHandler(urllib2.HTTPRedirectHandler):
361 """A redirect handler that persists CONNECT-enabled proxy information."""
362
363 def redirect_request(self, req, *args, **kwargs):
364 new_req = urllib2.HTTPRedirectHandler.redirect_request(
365 self, req, *args, **kwargs)
366 # Same thing as in our set_proxy implementation, but in this case
367 # we"ve only got a Request to work with, so it was this or copy
368 # everything over piecemeal.
369 #
370 # Note that we do not persist tunneling behavior from an http request
371 # to an https request, because an http request does not set _tunnel_host.
372 #
373 # Also note that in Python < 2.6, you will get an error in
374 # FancyHTTPSHandler.do_open() on an https urllib2.Request that uses an http
375 # proxy, since the proxy type will be set to http instead of https.
376 # (FancyRequest, and urllib2.Request in Python >= 2.6 set the proxy type to
377 # https.) Such an urllib2.Request could result from this redirect
378 # if you are redirecting from an http request (since an an http request
379 # does not have _tunnel_host set, and thus you will not set the proxy
380 # in the code below), and if you have defined a proxy for https in, say,
381 # FancyProxyHandler, and that proxy has type http.
382 if hasattr(req, "_tunnel_host") and isinstance(new_req, urllib2.Request):
383 if new_req.get_type() == "https":
384 if req._tunnel_host:
385 # req is proxied, so copy the proxy info.
386 new_req._tunnel_host = new_req.get_host()
387 new_req.set_proxy(req.host, "https")
388 else:
389 # req is not proxied, so just make sure _tunnel_host is defined.
390 new_req._tunnel_host = None
391 new_req.type = "https"
392 if hasattr(req, "_key_file") and isinstance(new_req, urllib2.Request):
393 # Copy the auxiliary data in case this or any further redirect is https
394 new_req._key_file = req._key_file
395 new_req._cert_file = req._cert_file
396 new_req._ca_certs = req._ca_certs
397
398 return new_req
OLDNEW
« no previous file with comments | « third_party/fancy_urllib/README ('k') | third_party/gsutil/CHECKSUM » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698