| OLD | NEW |
| 1 #!/usr/bin/env python | |
| 2 # | |
| 3 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software | 1 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software |
| 4 # Foundation; All Rights Reserved | 2 # Foundation; All Rights Reserved |
| 5 | 3 |
| 6 """A HTTPSConnection/Handler with additional proxy and cert validation features. | 4 """A HTTPSConnection/Handler with additional proxy and cert validation features. |
| 7 | 5 |
| 8 In particular, monkey patches in Python r74203 to provide support for CONNECT | 6 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. | 7 proxies and adds SSL cert validation if the ssl module is present. |
| 10 """ | 8 """ |
| 11 | 9 |
| 12 __author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)" | 10 __author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)" |
| 13 | 11 |
| 14 import base64 | 12 import base64 |
| 15 import httplib | 13 import httplib |
| 16 import logging | 14 import logging |
| 17 import re | |
| 18 import socket | 15 import socket |
| 16 from urllib import splitpasswd |
| 17 from urllib import splittype |
| 18 from urllib import splituser |
| 19 import urllib2 | 19 import urllib2 |
| 20 | 20 |
| 21 from urllib import splittype | |
| 22 from urllib import splituser | |
| 23 from urllib import splitpasswd | |
| 24 | 21 |
| 25 class InvalidCertificateException(httplib.HTTPException): | 22 class InvalidCertificateException(httplib.HTTPException): |
| 26 """Raised when a certificate is provided with an invalid hostname.""" | 23 """Raised when a certificate is provided with an invalid hostname.""" |
| 27 | 24 |
| 28 def __init__(self, host, cert, reason): | 25 def __init__(self, host, cert, reason): |
| 29 """Constructor. | 26 """Constructor. |
| 30 | 27 |
| 31 Args: | 28 Args: |
| 32 host: The hostname the connection was made to. | 29 host: The hostname the connection was made to. |
| 33 cert: The SSL certificate (as a dictionary) the host returned. | 30 cert: The SSL certificate (as a dictionary) the host returned. |
| 31 reason: user readable error reason. |
| 34 """ | 32 """ |
| 35 httplib.HTTPException.__init__(self) | 33 httplib.HTTPException.__init__(self) |
| 36 self.host = host | 34 self.host = host |
| 37 self.cert = cert | 35 self.cert = cert |
| 38 self.reason = reason | 36 self.reason = reason |
| 39 | 37 |
| 40 def __str__(self): | 38 def __str__(self): |
| 41 return ('Host %s returned an invalid certificate (%s): %s\n' | 39 return ("Host %s returned an invalid certificate (%s): %s\n" |
| 42 'To learn more, see ' | 40 "To learn more, see " |
| 43 'http://code.google.com/appengine/kb/general.html#rpcssl' % | 41 "http://code.google.com/appengine/kb/general.html#rpcssl" % |
| 44 (self.host, self.reason, self.cert)) | 42 (self.host, self.reason, self.cert)) |
| 45 | 43 |
| 44 |
| 45 try: |
| 46 import ssl |
| 47 _CAN_VALIDATE_CERTS = True |
| 48 except ImportError: |
| 49 _CAN_VALIDATE_CERTS = False |
| 50 |
| 51 |
| 46 def can_validate_certs(): | 52 def can_validate_certs(): |
| 47 """Return True if we have the SSL package and can validate certificates.""" | 53 """Return True if we have the SSL package and can validate certificates.""" |
| 48 try: | 54 return _CAN_VALIDATE_CERTS |
| 49 import ssl | |
| 50 return True | |
| 51 except ImportError: | |
| 52 return False | |
| 53 | 55 |
| 54 def _create_fancy_connection(tunnel_host=None, key_file=None, | 56 |
| 55 cert_file=None, ca_certs=None): | 57 # Reexport SSLError so clients don't have to to do their own checking for ssl's |
| 58 # existence. |
| 59 if can_validate_certs(): |
| 60 SSLError = ssl.SSLError |
| 61 else: |
| 62 SSLError = None |
| 63 |
| 64 |
| 65 def create_fancy_connection(tunnel_host=None, key_file=None, |
| 66 cert_file=None, ca_certs=None, |
| 67 proxy_authorization=None): |
| 56 # This abomination brought to you by the fact that | 68 # This abomination brought to you by the fact that |
| 57 # the HTTPHandler creates the connection instance in the middle | 69 # the HTTPHandler creates the connection instance in the middle |
| 58 # of do_open so we need to add the tunnel host to the class. | 70 # of do_open so we need to add the tunnel host to the class. |
| 59 | 71 |
| 60 class PresetProxyHTTPSConnection(httplib.HTTPSConnection): | 72 class PresetProxyHTTPSConnection(httplib.HTTPSConnection): |
| 61 """An HTTPS connection that uses a proxy defined by the enclosing scope.""" | 73 """An HTTPS connection that uses a proxy defined by the enclosing scope.""" |
| 62 | 74 |
| 63 def __init__(self, *args, **kwargs): | 75 def __init__(self, *args, **kwargs): |
| 64 httplib.HTTPSConnection.__init__(self, *args, **kwargs) | 76 httplib.HTTPSConnection.__init__(self, *args, **kwargs) |
| 65 | 77 |
| 66 self._tunnel_host = tunnel_host | 78 self._tunnel_host = tunnel_host |
| 67 if tunnel_host: | 79 if tunnel_host: |
| 68 logging.debug("Creating preset proxy https conn: %s", tunnel_host) | 80 logging.debug("Creating preset proxy https conn: %s", tunnel_host) |
| 69 | 81 |
| 70 self.key_file = key_file | 82 self.key_file = key_file |
| 71 self.cert_file = cert_file | 83 self.cert_file = cert_file |
| 72 self.ca_certs = ca_certs | 84 self.ca_certs = ca_certs |
| 73 try: | 85 if can_validate_certs(): |
| 74 import ssl | |
| 75 if self.ca_certs: | 86 if self.ca_certs: |
| 76 self.cert_reqs = ssl.CERT_REQUIRED | 87 self.cert_reqs = ssl.CERT_REQUIRED |
| 77 else: | 88 else: |
| 78 self.cert_reqs = ssl.CERT_NONE | 89 self.cert_reqs = ssl.CERT_NONE |
| 79 except ImportError: | 90 |
| 80 pass | 91 def _get_hostport(self, host, port): |
| 92 # Python 2.7.7rc1 (hg r90728:568041fd8090), 3.4.1 and 3.5 rename |
| 93 # _set_hostport to _get_hostport and changes it's functionality. The |
| 94 # Python 2.7.7rc1 version of this method is included here for |
| 95 # compatibility with earlier versions of Python. Without this, HTTPS over |
| 96 # HTTP CONNECT proxies cannot be used. |
| 97 |
| 98 # This method may be removed if compatibility with Python <2.7.7rc1 is not |
| 99 # required. |
| 100 |
| 101 # Python bug: http://bugs.python.org/issue7776 |
| 102 if port is None: |
| 103 i = host.rfind(":") |
| 104 j = host.rfind("]") # ipv6 addresses have [...] |
| 105 if i > j: |
| 106 try: |
| 107 port = int(host[i+1:]) |
| 108 except ValueError: |
| 109 if host[i+1:] == "": # http://foo.com:/ == http://foo.com/ |
| 110 port = self.default_port |
| 111 else: |
| 112 raise httplib.InvalidURL("nonnumeric port: '%s'" % host[i+1:]) |
| 113 host = host[:i] |
| 114 else: |
| 115 port = self.default_port |
| 116 if host and host[0] == "[" and host[-1] == "]": |
| 117 host = host[1:-1] |
| 118 |
| 119 return (host, port) |
| 81 | 120 |
| 82 def _tunnel(self): | 121 def _tunnel(self): |
| 83 self._set_hostport(self._tunnel_host, None) | 122 self.host, self.port = self._get_hostport(self._tunnel_host, None) |
| 84 logging.info("Connecting through tunnel to: %s:%d", | 123 logging.info("Connecting through tunnel to: %s:%d", |
| 85 self.host, self.port) | 124 self.host, self.port) |
| 86 self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self.host, self.port)) | 125 |
| 126 self.send("CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port)) |
| 127 |
| 128 if proxy_authorization: |
| 129 self.send("Proxy-Authorization: %s\r\n" % proxy_authorization) |
| 130 |
| 131 # blank line |
| 132 self.send("\r\n") |
| 133 |
| 87 response = self.response_class(self.sock, strict=self.strict, | 134 response = self.response_class(self.sock, strict=self.strict, |
| 88 method=self._method) | 135 method=self._method) |
| 136 # pylint: disable=protected-access |
| 89 (_, code, message) = response._read_status() | 137 (_, code, message) = response._read_status() |
| 90 | 138 |
| 91 if code != 200: | 139 if code != 200: |
| 92 self.close() | 140 self.close() |
| 93 raise socket.error, "Tunnel connection failed: %d %s" % ( | 141 raise socket.error("Tunnel connection failed: %d %s" % |
| 94 code, message.strip()) | 142 (code, message.strip())) |
| 95 | 143 |
| 96 while True: | 144 while True: |
| 97 line = response.fp.readline() | 145 line = response.fp.readline() |
| 98 if line == "\r\n": | 146 if line == "\r\n": |
| 99 break | 147 break |
| 100 | 148 |
| 101 def _get_valid_hosts_for_cert(self, cert): | 149 def _get_valid_hosts_for_cert(self, cert): |
| 102 """Returns a list of valid host globs for an SSL certificate. | 150 """Returns a list of valid host globs for an SSL certificate. |
| 103 | 151 |
| 104 Args: | 152 Args: |
| 105 cert: A dictionary representing an SSL certificate. | 153 cert: A dictionary representing an SSL certificate. |
| 106 Returns: | 154 Returns: |
| 107 list: A list of valid host globs. | 155 list: A list of valid host globs. |
| 108 """ | 156 """ |
| 109 if 'subjectAltName' in cert: | 157 if "subjectAltName" in cert: |
| 110 return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] | 158 return [x[1] for x in cert["subjectAltName"] if x[0].lower() == "dns"] |
| 111 else: | 159 else: |
| 112 # Return a list of commonName fields | 160 # Return a list of commonName fields |
| 113 return [x[0][1] for x in cert['subject'] | 161 return [x[0][1] for x in cert["subject"] |
| 114 if x[0][0].lower() == 'commonname'] | 162 if x[0][0].lower() == "commonname"] |
| 115 | 163 |
| 116 def _validate_certificate_hostname(self, cert, hostname): | 164 def _validate_certificate_hostname(self, cert, hostname): |
| 117 """Validates that a given hostname is valid for an SSL certificate. | 165 """Perform RFC2818/6125 validation against a cert and hostname. |
| 118 | 166 |
| 119 Args: | 167 Args: |
| 120 cert: A dictionary representing an SSL certificate. | 168 cert: A dictionary representing an SSL certificate. |
| 121 hostname: The hostname to test. | 169 hostname: The hostname to test. |
| 122 Returns: | 170 Returns: |
| 123 bool: Whether or not the hostname is valid for this certificate. | 171 bool: Whether or not the hostname is valid for this certificate. |
| 124 """ | 172 """ |
| 125 hosts = self._get_valid_hosts_for_cert(cert) | 173 hosts = self._get_valid_hosts_for_cert(cert) |
| 126 for host in hosts: | 174 for host in hosts: |
| 127 # Convert the glob-style hostname expression (eg, '*.google.com') into a | 175 # Wildcards are only valid when the * exists at the end of the last |
| 128 # valid regular expression. | 176 # (left-most) label, and there are at least 3 labels in the expression. |
| 129 host_re = host.replace('.', '\.').replace('*', '[^.]*') | 177 if ("*." in host and host.count("*") == 1 and |
| 130 if re.search('^%s$' % (host_re,), hostname, re.I): | 178 host.count(".") > 1 and "." in hostname): |
| 179 left_expected, right_expected = host.split("*.") |
| 180 left_hostname, right_hostname = hostname.split(".", 1) |
| 181 if (left_hostname.startswith(left_expected) and |
| 182 right_expected == right_hostname): |
| 183 return True |
| 184 elif host == hostname: |
| 131 return True | 185 return True |
| 132 return False | 186 return False |
| 133 | 187 |
| 134 | |
| 135 def connect(self): | 188 def connect(self): |
| 136 # TODO(frew): When we drop support for <2.6 (in the far distant future), | 189 # TODO(frew): When we drop support for <2.6 (in the far distant future), |
| 137 # change this to socket.create_connection. | 190 # change this to socket.create_connection. |
| 138 self.sock = _create_connection((self.host, self.port)) | 191 self.sock = _create_connection((self.host, self.port)) |
| 139 | 192 |
| 140 if self._tunnel_host: | 193 if self._tunnel_host: |
| 141 self._tunnel() | 194 self._tunnel() |
| 142 | 195 |
| 143 # ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl, | 196 # ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl, |
| 144 # with fallback. | 197 # with fallback. Note: Since can_validate_certs() just checks for the |
| 145 try: | 198 # ssl module, it's equivalent to attempting to import ssl from |
| 146 import ssl | 199 # the function, but doesn't require a dynamic import, which doesn't |
| 200 # play nicely with dev_appserver. |
| 201 if can_validate_certs(): |
| 147 self.sock = ssl.wrap_socket(self.sock, | 202 self.sock = ssl.wrap_socket(self.sock, |
| 148 keyfile=self.key_file, | 203 keyfile=self.key_file, |
| 149 certfile=self.cert_file, | 204 certfile=self.cert_file, |
| 150 ca_certs=self.ca_certs, | 205 ca_certs=self.ca_certs, |
| 151 cert_reqs=self.cert_reqs) | 206 cert_reqs=self.cert_reqs) |
| 152 | 207 |
| 153 if self.cert_reqs & ssl.CERT_REQUIRED: | 208 if self.cert_reqs & ssl.CERT_REQUIRED: |
| 154 cert = self.sock.getpeercert() | 209 cert = self.sock.getpeercert() |
| 155 hostname = self.host.split(':', 0)[0] | 210 hostname = self.host.split(":", 0)[0] |
| 156 if not self._validate_certificate_hostname(cert, hostname): | 211 if not self._validate_certificate_hostname(cert, hostname): |
| 157 raise InvalidCertificateException(hostname, cert, | 212 raise InvalidCertificateException(hostname, cert, |
| 158 'hostname mismatch') | 213 "hostname mismatch") |
| 159 except ImportError: | 214 else: |
| 160 ssl = socket.ssl(self.sock, | 215 ssl_socket = socket.ssl(self.sock, |
| 161 keyfile=self.key_file, | 216 keyfile=self.key_file, |
| 162 certfile=self.cert_file) | 217 certfile=self.cert_file) |
| 163 self.sock = httplib.FakeSocket(self.sock, ssl) | 218 self.sock = httplib.FakeSocket(self.sock, ssl_socket) |
| 164 | 219 |
| 165 return PresetProxyHTTPSConnection | 220 return PresetProxyHTTPSConnection |
| 166 | 221 |
| 167 | 222 |
| 168 # Here to end of _create_connection copied wholesale from Python 2.6"s socket.py | 223 # Here to end of _create_connection copied wholesale from Python 2.6"s socket.py |
| 169 _GLOBAL_DEFAULT_TIMEOUT = object() | 224 _GLOBAL_DEFAULT_TIMEOUT = object() |
| 170 | 225 |
| 171 | 226 |
| 172 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT): | 227 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT): |
| 173 """Connect to *address* and return the socket object. | 228 """Connect to *address* and return the socket object. |
| (...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 322 # This condition is the change | 377 # This condition is the change |
| 323 if orig_type == "https": | 378 if orig_type == "https": |
| 324 return None | 379 return None |
| 325 | 380 |
| 326 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type) | 381 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type) |
| 327 | 382 |
| 328 | 383 |
| 329 class FancyHTTPSHandler(urllib2.HTTPSHandler): | 384 class FancyHTTPSHandler(urllib2.HTTPSHandler): |
| 330 """An HTTPSHandler that works with CONNECT-enabled proxies.""" | 385 """An HTTPSHandler that works with CONNECT-enabled proxies.""" |
| 331 | 386 |
| 332 def do_open(self, http_class, req): | 387 def do_open(self, http_class, req, *args, **kwargs): |
| 388 proxy_authorization = None |
| 389 for header in req.headers: |
| 390 if header.lower() == "proxy-authorization": |
| 391 proxy_authorization = req.headers[header] |
| 392 break |
| 393 |
| 333 # Intentionally very specific so as to opt for false negatives | 394 # Intentionally very specific so as to opt for false negatives |
| 334 # rather than false positives. | 395 # rather than false positives. |
| 335 try: | 396 try: |
| 336 return urllib2.HTTPSHandler.do_open( | 397 return urllib2.HTTPSHandler.do_open( |
| 337 self, | 398 self, |
| 338 _create_fancy_connection(req._tunnel_host, | 399 create_fancy_connection(req._tunnel_host, |
| 339 req._key_file, | 400 req._key_file, |
| 340 req._cert_file, | 401 req._cert_file, |
| 341 req._ca_certs), | 402 req._ca_certs, |
| 342 req) | 403 proxy_authorization), |
| 404 req, *args, **kwargs) |
| 343 except urllib2.URLError, url_error: | 405 except urllib2.URLError, url_error: |
| 344 try: | 406 try: |
| 345 import ssl | 407 import ssl |
| 346 if (type(url_error.reason) == ssl.SSLError and | 408 if (type(url_error.reason) == ssl.SSLError and |
| 347 url_error.reason.args[0] == 1): | 409 url_error.reason.args[0] == 1): |
| 348 # Display the reason to the user. Need to use args for python2.5 | 410 # Display the reason to the user. Need to use args for python2.5 |
| 349 # compat. | 411 # compat. |
| 350 raise InvalidCertificateException(req.host, '', | 412 raise InvalidCertificateException(req.host, "", |
| 351 url_error.reason.args[1]) | 413 url_error.reason.args[1]) |
| 352 except ImportError: | 414 except ImportError: |
| 353 pass | 415 pass |
| 354 | 416 |
| 355 raise url_error | 417 raise url_error |
| 356 | 418 |
| 357 | 419 |
| 358 # We have to implement this so that we persist the tunneling behavior | 420 # We have to implement this so that we persist the tunneling behavior |
| 359 # through redirects. | 421 # through redirects. |
| 360 class FancyRedirectHandler(urllib2.HTTPRedirectHandler): | 422 class FancyRedirectHandler(urllib2.HTTPRedirectHandler): |
| (...skipping 28 matching lines...) Expand all Loading... |
| 389 # req is not proxied, so just make sure _tunnel_host is defined. | 451 # req is not proxied, so just make sure _tunnel_host is defined. |
| 390 new_req._tunnel_host = None | 452 new_req._tunnel_host = None |
| 391 new_req.type = "https" | 453 new_req.type = "https" |
| 392 if hasattr(req, "_key_file") and isinstance(new_req, urllib2.Request): | 454 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 | 455 # Copy the auxiliary data in case this or any further redirect is https |
| 394 new_req._key_file = req._key_file | 456 new_req._key_file = req._key_file |
| 395 new_req._cert_file = req._cert_file | 457 new_req._cert_file = req._cert_file |
| 396 new_req._ca_certs = req._ca_certs | 458 new_req._ca_certs = req._ca_certs |
| 397 | 459 |
| 398 return new_req | 460 return new_req |
| OLD | NEW |