Index: third_party/fancy_urllib/__init__.py |
diff --git a/third_party/fancy_urllib/__init__.py b/third_party/fancy_urllib/__init__.py |
index d4da0dd192005b7bb0eae318850b3053976f04d4..945d83bc7d9b94d61e3d71bf97652e2e050efc1a 100644 |
--- a/third_party/fancy_urllib/__init__.py |
+++ b/third_party/fancy_urllib/__init__.py |
@@ -1,5 +1,3 @@ |
-#!/usr/bin/env python |
-# |
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software |
# Foundation; All Rights Reserved |
@@ -14,13 +12,12 @@ __author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)" |
import base64 |
import httplib |
import logging |
-import re |
import socket |
-import urllib2 |
- |
+from urllib import splitpasswd |
from urllib import splittype |
from urllib import splituser |
-from urllib import splitpasswd |
+import urllib2 |
+ |
class InvalidCertificateException(httplib.HTTPException): |
"""Raised when a certificate is provided with an invalid hostname.""" |
@@ -31,6 +28,7 @@ class InvalidCertificateException(httplib.HTTPException): |
Args: |
host: The hostname the connection was made to. |
cert: The SSL certificate (as a dictionary) the host returned. |
+ reason: user readable error reason. |
""" |
httplib.HTTPException.__init__(self) |
self.host = host |
@@ -38,21 +36,35 @@ class InvalidCertificateException(httplib.HTTPException): |
self.reason = reason |
def __str__(self): |
- return ('Host %s returned an invalid certificate (%s): %s\n' |
- 'To learn more, see ' |
- 'http://code.google.com/appengine/kb/general.html#rpcssl' % |
+ return ("Host %s returned an invalid certificate (%s): %s\n" |
+ "To learn more, see " |
+ "http://code.google.com/appengine/kb/general.html#rpcssl" % |
(self.host, self.reason, self.cert)) |
+ |
+try: |
+ import ssl |
+ _CAN_VALIDATE_CERTS = True |
+except ImportError: |
+ _CAN_VALIDATE_CERTS = False |
+ |
+ |
def can_validate_certs(): |
"""Return True if we have the SSL package and can validate certificates.""" |
- try: |
- import ssl |
- return True |
- except ImportError: |
- return False |
- |
-def _create_fancy_connection(tunnel_host=None, key_file=None, |
- cert_file=None, ca_certs=None): |
+ return _CAN_VALIDATE_CERTS |
+ |
+ |
+# Reexport SSLError so clients don't have to to do their own checking for ssl's |
+# existence. |
+if can_validate_certs(): |
+ SSLError = ssl.SSLError |
+else: |
+ SSLError = None |
+ |
+ |
+def create_fancy_connection(tunnel_host=None, key_file=None, |
+ cert_file=None, ca_certs=None, |
+ proxy_authorization=None): |
# This abomination brought to you by the fact that |
# the HTTPHandler creates the connection instance in the middle |
# of do_open so we need to add the tunnel host to the class. |
@@ -70,28 +82,64 @@ def _create_fancy_connection(tunnel_host=None, key_file=None, |
self.key_file = key_file |
self.cert_file = cert_file |
self.ca_certs = ca_certs |
- try: |
- import ssl |
+ if can_validate_certs(): |
if self.ca_certs: |
self.cert_reqs = ssl.CERT_REQUIRED |
else: |
self.cert_reqs = ssl.CERT_NONE |
- except ImportError: |
- pass |
+ |
+ def _get_hostport(self, host, port): |
+ # Python 2.7.7rc1 (hg r90728:568041fd8090), 3.4.1 and 3.5 rename |
+ # _set_hostport to _get_hostport and changes it's functionality. The |
+ # Python 2.7.7rc1 version of this method is included here for |
+ # compatibility with earlier versions of Python. Without this, HTTPS over |
+ # HTTP CONNECT proxies cannot be used. |
+ |
+ # This method may be removed if compatibility with Python <2.7.7rc1 is not |
+ # required. |
+ |
+ # Python bug: http://bugs.python.org/issue7776 |
+ if port is None: |
+ i = host.rfind(":") |
+ j = host.rfind("]") # ipv6 addresses have [...] |
+ if i > j: |
+ try: |
+ port = int(host[i+1:]) |
+ except ValueError: |
+ if host[i+1:] == "": # http://foo.com:/ == http://foo.com/ |
+ port = self.default_port |
+ else: |
+ raise httplib.InvalidURL("nonnumeric port: '%s'" % host[i+1:]) |
+ host = host[:i] |
+ else: |
+ port = self.default_port |
+ if host and host[0] == "[" and host[-1] == "]": |
+ host = host[1:-1] |
+ |
+ return (host, port) |
def _tunnel(self): |
- self._set_hostport(self._tunnel_host, None) |
+ self.host, self.port = self._get_hostport(self._tunnel_host, None) |
logging.info("Connecting through tunnel to: %s:%d", |
self.host, self.port) |
- self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self.host, self.port)) |
+ |
+ self.send("CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port)) |
+ |
+ if proxy_authorization: |
+ self.send("Proxy-Authorization: %s\r\n" % proxy_authorization) |
+ |
+ # blank line |
+ self.send("\r\n") |
+ |
response = self.response_class(self.sock, strict=self.strict, |
method=self._method) |
+ # pylint: disable=protected-access |
(_, code, message) = response._read_status() |
if code != 200: |
self.close() |
- raise socket.error, "Tunnel connection failed: %d %s" % ( |
- code, message.strip()) |
+ raise socket.error("Tunnel connection failed: %d %s" % |
+ (code, message.strip())) |
while True: |
line = response.fp.readline() |
@@ -106,15 +154,15 @@ def _create_fancy_connection(tunnel_host=None, key_file=None, |
Returns: |
list: A list of valid host globs. |
""" |
- if 'subjectAltName' in cert: |
- return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] |
+ if "subjectAltName" in cert: |
+ return [x[1] for x in cert["subjectAltName"] if x[0].lower() == "dns"] |
else: |
# Return a list of commonName fields |
- return [x[0][1] for x in cert['subject'] |
- if x[0][0].lower() == 'commonname'] |
+ return [x[0][1] for x in cert["subject"] |
+ if x[0][0].lower() == "commonname"] |
def _validate_certificate_hostname(self, cert, hostname): |
- """Validates that a given hostname is valid for an SSL certificate. |
+ """Perform RFC2818/6125 validation against a cert and hostname. |
Args: |
cert: A dictionary representing an SSL certificate. |
@@ -124,14 +172,19 @@ def _create_fancy_connection(tunnel_host=None, key_file=None, |
""" |
hosts = self._get_valid_hosts_for_cert(cert) |
for host in hosts: |
- # Convert the glob-style hostname expression (eg, '*.google.com') into a |
- # valid regular expression. |
- host_re = host.replace('.', '\.').replace('*', '[^.]*') |
- if re.search('^%s$' % (host_re,), hostname, re.I): |
+ # Wildcards are only valid when the * exists at the end of the last |
+ # (left-most) label, and there are at least 3 labels in the expression. |
+ if ("*." in host and host.count("*") == 1 and |
+ host.count(".") > 1 and "." in hostname): |
+ left_expected, right_expected = host.split("*.") |
+ left_hostname, right_hostname = hostname.split(".", 1) |
+ if (left_hostname.startswith(left_expected) and |
+ right_expected == right_hostname): |
+ return True |
+ elif host == hostname: |
return True |
return False |
- |
def connect(self): |
# TODO(frew): When we drop support for <2.6 (in the far distant future), |
# change this to socket.create_connection. |
@@ -141,9 +194,11 @@ def _create_fancy_connection(tunnel_host=None, key_file=None, |
self._tunnel() |
# ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl, |
- # with fallback. |
- try: |
- import ssl |
+ # with fallback. Note: Since can_validate_certs() just checks for the |
+ # ssl module, it's equivalent to attempting to import ssl from |
+ # the function, but doesn't require a dynamic import, which doesn't |
+ # play nicely with dev_appserver. |
+ if can_validate_certs(): |
self.sock = ssl.wrap_socket(self.sock, |
keyfile=self.key_file, |
certfile=self.cert_file, |
@@ -152,15 +207,15 @@ def _create_fancy_connection(tunnel_host=None, key_file=None, |
if self.cert_reqs & ssl.CERT_REQUIRED: |
cert = self.sock.getpeercert() |
- hostname = self.host.split(':', 0)[0] |
+ hostname = self.host.split(":", 0)[0] |
if not self._validate_certificate_hostname(cert, hostname): |
raise InvalidCertificateException(hostname, cert, |
- 'hostname mismatch') |
- except ImportError: |
- ssl = socket.ssl(self.sock, |
- keyfile=self.key_file, |
- certfile=self.cert_file) |
- self.sock = httplib.FakeSocket(self.sock, ssl) |
+ "hostname mismatch") |
+ else: |
+ ssl_socket = socket.ssl(self.sock, |
+ keyfile=self.key_file, |
+ certfile=self.cert_file) |
+ self.sock = httplib.FakeSocket(self.sock, ssl_socket) |
return PresetProxyHTTPSConnection |
@@ -329,17 +384,24 @@ class FancyProxyHandler(urllib2.ProxyHandler): |
class FancyHTTPSHandler(urllib2.HTTPSHandler): |
"""An HTTPSHandler that works with CONNECT-enabled proxies.""" |
- def do_open(self, http_class, req): |
+ def do_open(self, http_class, req, *args, **kwargs): |
+ proxy_authorization = None |
+ for header in req.headers: |
+ if header.lower() == "proxy-authorization": |
+ proxy_authorization = req.headers[header] |
+ break |
+ |
# Intentionally very specific so as to opt for false negatives |
# rather than false positives. |
try: |
return urllib2.HTTPSHandler.do_open( |
self, |
- _create_fancy_connection(req._tunnel_host, |
- req._key_file, |
- req._cert_file, |
- req._ca_certs), |
- req) |
+ create_fancy_connection(req._tunnel_host, |
+ req._key_file, |
+ req._cert_file, |
+ req._ca_certs, |
+ proxy_authorization), |
+ req, *args, **kwargs) |
except urllib2.URLError, url_error: |
try: |
import ssl |
@@ -347,7 +409,7 @@ class FancyHTTPSHandler(urllib2.HTTPSHandler): |
url_error.reason.args[0] == 1): |
# Display the reason to the user. Need to use args for python2.5 |
# compat. |
- raise InvalidCertificateException(req.host, '', |
+ raise InvalidCertificateException(req.host, "", |
url_error.reason.args[1]) |
except ImportError: |
pass |