Blame SOURCES/00220-pep466-allow-passing-ssl-urrlib-httplib.patch

925e6b
925e6b
# HG changeset patch
925e6b
# User Benjamin Peterson <benjamin@python.org>
925e6b
# Date 1416764565 21600
925e6b
# Node ID 1882157b298a164291d2b3a8b9525eb0902895f6
925e6b
# Parent  588ebc8fd3daf7307961cd614c4da9525bb67313
925e6b
allow passing cert/ssl information to urllib2.urlopen and httplib.HTTPSConnection
925e6b
925e6b
This is basically a backport of issues #9003 and #22366.
925e6b
925e6b
diff --git a/Doc/library/httplib.rst b/Doc/library/httplib.rst
925e6b
--- a/Doc/library/httplib.rst
925e6b
+++ b/Doc/library/httplib.rst
925e6b
@@ -70,12 +70,25 @@ The module provides the following classe
925e6b
       *source_address* was added.
925e6b
 
925e6b
 
925e6b
-.. class:: HTTPSConnection(host[, port[, key_file[, cert_file[, strict[, timeout[, source_address]]]]]])
925e6b
+.. class:: HTTPSConnection(host[, port[, key_file[, cert_file[, strict[, timeout[, source_address, context, check_hostname]]]]]])
925e6b
 
925e6b
    A subclass of :class:`HTTPConnection` that uses SSL for communication with
925e6b
-   secure servers.  Default port is ``443``. *key_file* is the name of a PEM
925e6b
-   formatted file that contains your private key. *cert_file* is a PEM formatted
925e6b
-   certificate chain file.
925e6b
+   secure servers.  Default port is ``443``.  If *context* is specified, it must
925e6b
+   be a :class:`ssl.SSLContext` instance describing the various SSL options.
925e6b
+
925e6b
+   *key_file* and *cert_file* are deprecated, please use
925e6b
+   :meth:`ssl.SSLContext.load_cert_chain` instead, or let
925e6b
+   :func:`ssl.create_default_context` select the system's trusted CA
925e6b
+   certificates for you.
925e6b
+
925e6b
+   Please read :ref:`ssl-security` for more information on best practices.
925e6b
+
925e6b
+   .. note::
925e6b
+      If *context* is specified and has a :attr:`~ssl.SSLContext.verify_mode`
925e6b
+      of either :data:`~ssl.CERT_OPTIONAL` or :data:`~ssl.CERT_REQUIRED`, then
925e6b
+      by default *host* is matched against the host name(s) allowed by the
925e6b
+      server's certificate.  If you want to change that behaviour, you can
925e6b
+      explicitly set *check_hostname* to False.
925e6b
 
925e6b
    .. warning::
925e6b
       This does not do any verification of the server's certificate.
925e6b
@@ -88,6 +101,9 @@ The module provides the following classe
925e6b
    .. versionchanged:: 2.7
925e6b
       *source_address* was added.
925e6b
 
925e6b
+   .. versionchanged:: 2.7.9
925e6b
+      *context* and *check_hostname* was added.
925e6b
+
925e6b
 
925e6b
 .. class:: HTTPResponse(sock, debuglevel=0, strict=0)
925e6b
 
925e6b
diff --git a/Lib/test/keycert2.pem b/Lib/test/keycert2.pem
925e6b
new file mode 100644
925e6b
--- /dev/null
925e6b
+++ b/Lib/test/keycert2.pem
925e6b
@@ -0,0 +1,31 @@
925e6b
+-----BEGIN PRIVATE KEY-----
925e6b
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANcLaMB7T/Wi9DBc
925e6b
+PltGzgt8cxsv55m7PQPHMZvn6Ke8xmNqcmEzib8opRwKGrCV6TltKeFlNSg8dwQK
925e6b
+Tl4ktyTkGCVweRQJ37AkBayvEBml5s+QD4vlhqkJPsL/Nsd+fnqngOGc5+59+C6r
925e6b
+s3XpiLlF5ah/z8q92Mnw54nypw1JAgMBAAECgYBE3t2Mj7GbDLZB6rj5yKJioVfI
925e6b
+BD6bSJEQ7bGgqdQkLFwpKMU7BiN+ekjuwvmrRkesYZ7BFgXBPiQrwhU5J28Tpj5B
925e6b
+EOMYSIOHfzdalhxDGM1q2oK9LDFiCotTaSdEzMYadel5rmKXJ0zcK2Jho0PCuECf
925e6b
+tf/ghRxK+h1Hm0tKgQJBAO6MdGDSmGKYX6/5kPDje7we/lSLorSDkYmV0tmVShsc
925e6b
+JxgaGaapazceA/sHL3Myx7Eenkip+yPYDXEDFvAKNDECQQDmxsT9NOp6mo7ISvky
925e6b
+GFr2vVHsJ745BMWoma4rFjPBVnS8RkgK+b2EpDCdZSrQ9zw2r8sKTgrEyrDiGTEg
925e6b
+wJyZAkA8OOc0flYMJg2aHnYR6kwVjPmGHI5h5gk648EMPx0rROs1sXkiUwkHLCOz
925e6b
+HvhCq+Iv+9vX2lnVjbiu/CmxRdIxAkA1YEfzoKeTD+hyXxTgB04Sv5sRGegfXAEz
925e6b
+i8gC4zG5R/vcCA1lrHmvEiLEZL/QcT6WD3bQvVg0SAU9ZkI8pxARAkA7yqMSvP1l
925e6b
+gJXy44R+rzpLYb1/PtiLkIkaKG3x9TUfPnfD2jY09fPkZlfsRU3/uS09IkhSwimV
925e6b
+d5rWoljEfdou
925e6b
+-----END PRIVATE KEY-----
925e6b
+-----BEGIN CERTIFICATE-----
925e6b
+MIICXTCCAcagAwIBAgIJALVQzebTtrXFMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV
925e6b
+BAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9u
925e6b
+IFNvZnR3YXJlIEZvdW5kYXRpb24xFTATBgNVBAMMDGZha2Vob3N0bmFtZTAeFw0x
925e6b
+NDExMjMxNzAwMDdaFw0yNDExMjAxNzAwMDdaMGIxCzAJBgNVBAYTAlhZMRcwFQYD
925e6b
+VQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZv
925e6b
+dW5kYXRpb24xFTATBgNVBAMMDGZha2Vob3N0bmFtZTCBnzANBgkqhkiG9w0BAQEF
925e6b
+AAOBjQAwgYkCgYEA1wtowHtP9aL0MFw+W0bOC3xzGy/nmbs9A8cxm+fop7zGY2py
925e6b
+YTOJvyilHAoasJXpOW0p4WU1KDx3BApOXiS3JOQYJXB5FAnfsCQFrK8QGaXmz5AP
925e6b
+i+WGqQk+wv82x35+eqeA4Zzn7n34LquzdemIuUXlqH/Pyr3YyfDnifKnDUkCAwEA
925e6b
+AaMbMBkwFwYDVR0RBBAwDoIMZmFrZWhvc3RuYW1lMA0GCSqGSIb3DQEBBQUAA4GB
925e6b
+AKuay3vDKfWzt5+ch/HHBsert84ISot4fUjzXDA/oOgTOEjVcSShHxqNShMOW1oA
925e6b
+QYBpBB/5Kx5RkD/w6imhucxt2WQPRgjX4x4bwMipVH/HvFDp03mG51/Cpi1TyZ74
925e6b
+El7qa/Pd4lHhOLzMKBA6503fpeYSFUIBxZbGLqylqRK7
925e6b
+-----END CERTIFICATE-----
925e6b
diff --git a/Lib/test/selfsigned_pythontestdotnet.pem b/Lib/test/selfsigned_pythontestdotnet.pem
925e6b
new file mode 100644
925e6b
--- /dev/null
925e6b
+++ b/Lib/test/selfsigned_pythontestdotnet.pem
925e6b
@@ -0,0 +1,16 @@
925e6b
+-----BEGIN CERTIFICATE-----
925e6b
+MIIChzCCAfCgAwIBAgIJAKGU95wKR8pSMA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNV
925e6b
+BAYTAlhZMRcwFQYDVQQHDA5DYXN0bGUgQW50aHJheDEjMCEGA1UECgwaUHl0aG9u
925e6b
+IFNvZnR3YXJlIEZvdW5kYXRpb24xIzAhBgNVBAMMGnNlbGYtc2lnbmVkLnB5dGhv
925e6b
+bnRlc3QubmV0MB4XDTE0MTEwMjE4MDkyOVoXDTI0MTAzMDE4MDkyOVowcDELMAkG
925e6b
+A1UEBhMCWFkxFzAVBgNVBAcMDkNhc3RsZSBBbnRocmF4MSMwIQYDVQQKDBpQeXRo
925e6b
+b24gU29mdHdhcmUgRm91bmRhdGlvbjEjMCEGA1UEAwwac2VsZi1zaWduZWQucHl0
925e6b
+aG9udGVzdC5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANDXQXW9tjyZ
925e6b
+Xt0Iv2tLL1+jinr4wGg36ioLDLFkMf+2Y1GL0v0BnKYG4N1OKlAU15LXGeGer8vm
925e6b
+Sv/yIvmdrELvhAbbo3w4a9TMYQA4XkIVLdvu3mvNOAet+8PMJxn26dbDhG809ALv
925e6b
+EHY57lQsBS3G59RZyBPVqAqmImWNJnVzAgMBAAGjKTAnMCUGA1UdEQQeMByCGnNl
925e6b
+bGYtc2lnbmVkLnB5dGhvbnRlc3QubmV0MA0GCSqGSIb3DQEBBQUAA4GBAIOXmdtM
925e6b
+eG9qzP9TiXW/Gc/zI4cBfdCpC+Y4gOfC9bQUC7hefix4iO3+iZjgy3X/FaRxUUoV
925e6b
+HKiXcXIaWqTSUWp45cSh0MbwZXudp6JIAptzdAhvvCrPKeC9i9GvxsPD4LtDAL97
925e6b
+vSaxQBezA7hdxZd90/EeyMgVZgAnTCnvAWX9
925e6b
+-----END CERTIFICATE-----
925e6b
diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py
925e6b
--- a/Lib/test/test_urllib2.py
925e6b
+++ b/Lib/test/test_urllib2.py
925e6b
@@ -8,6 +8,11 @@ import StringIO
925e6b
 import urllib2
925e6b
 from urllib2 import Request, OpenerDirector
925e6b
 
925e6b
+try:
925e6b
+    import ssl
925e6b
+except ImportError:
925e6b
+    ssl = None
925e6b
+
925e6b
 # XXX
925e6b
 # Request
925e6b
 # CacheFTPHandler (hard to write)
925e6b
@@ -47,6 +52,14 @@ class TrivialTests(unittest.TestCase):
925e6b
         for string, list in tests:
925e6b
             self.assertEqual(urllib2.parse_http_list(string), list)
925e6b
 
925e6b
+    @unittest.skipUnless(ssl, "ssl module required")
925e6b
+    def test_cafile_and_context(self):
925e6b
+        context = ssl.create_default_context()
925e6b
+        with self.assertRaises(ValueError):
925e6b
+            urllib2.urlopen(
925e6b
+                "https://localhost", cafile="/nonexistent/path", context=context
925e6b
+            )
925e6b
+
925e6b
 
925e6b
 def test_request_headers_dict():
925e6b
     """
925e6b
diff --git a/Lib/urllib2.py b/Lib/urllib2.py
925e6b
--- a/Lib/urllib2.py
925e6b
+++ b/Lib/urllib2.py
925e6b
@@ -109,6 +109,14 @@ try:
925e6b
 except ImportError:
925e6b
     from StringIO import StringIO
925e6b
 
925e6b
+# check for SSL
925e6b
+try:
925e6b
+    import ssl
925e6b
+except ImportError:
925e6b
+    _have_ssl = False
925e6b
+else:
925e6b
+    _have_ssl = True
925e6b
+
925e6b
 from urllib import (unwrap, unquote, splittype, splithost, quote,
925e6b
      addinfourl, splitport, splittag, toBytes,
925e6b
      splitattr, ftpwrapper, splituser, splitpasswd, splitvalue)
925e6b
@@ -120,11 +128,30 @@ from urllib import localhost, url2pathna
925e6b
 __version__ = sys.version[:3]
925e6b
 
925e6b
 _opener = None
925e6b
-def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
925e6b
+def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
925e6b
+            cafile=None, capath=None, cadefault=False, context=None):
925e6b
     global _opener
925e6b
-    if _opener is None:
925e6b
-        _opener = build_opener()
925e6b
-    return _opener.open(url, data, timeout)
925e6b
+    if cafile or capath or cadefault:
925e6b
+        if context is not None:
925e6b
+            raise ValueError(
925e6b
+                "You can't pass both context and any of cafile, capath, and "
925e6b
+                "cadefault"
925e6b
+            )
925e6b
+        if not _have_ssl:
925e6b
+            raise ValueError('SSL support not available')
925e6b
+        context = ssl._create_stdlib_context(cert_reqs=ssl.CERT_REQUIRED,
925e6b
+                                             cafile=cafile,
925e6b
+                                             capath=capath)
925e6b
+        https_handler = HTTPSHandler(context=context, check_hostname=True)
925e6b
+        opener = build_opener(https_handler)
925e6b
+    elif context:
925e6b
+        https_handler = HTTPSHandler(context=context)
925e6b
+        opener = build_opener(https_handler)
925e6b
+    elif _opener is None:
925e6b
+        _opener = opener = build_opener()
925e6b
+    else:
925e6b
+        opener = _opener
925e6b
+    return opener.open(url, data, timeout)
925e6b
 
925e6b
 def install_opener(opener):
925e6b
     global _opener
925e6b
@@ -1121,7 +1148,7 @@ class AbstractHTTPHandler(BaseHandler):
925e6b
 
925e6b
         return request
925e6b
 
925e6b
-    def do_open(self, http_class, req):
925e6b
+    def do_open(self, http_class, req, **http_conn_args):
925e6b
         """Return an addinfourl object for the request, using http_class.
925e6b
 
925e6b
         http_class must implement the HTTPConnection API from httplib.
925e6b
@@ -1135,7 +1162,8 @@ class AbstractHTTPHandler(BaseHandler):
925e6b
         if not host:
925e6b
             raise URLError('no host given')
925e6b
 
925e6b
-        h = http_class(host, timeout=req.timeout) # will parse host:port
925e6b
+        # will parse host:port
925e6b
+        h = http_class(host, timeout=req.timeout, **http_conn_args)
925e6b
         h.set_debuglevel(self._debuglevel)
925e6b
 
925e6b
         headers = dict(req.unredirected_hdrs)
925e6b
@@ -1203,8 +1231,14 @@ class HTTPHandler(AbstractHTTPHandler):
925e6b
 if hasattr(httplib, 'HTTPS'):
925e6b
     class HTTPSHandler(AbstractHTTPHandler):
925e6b
 
925e6b
+        def __init__(self, debuglevel=0, context=None, check_hostname=None):
925e6b
+            AbstractHTTPHandler.__init__(self, debuglevel)
925e6b
+            self._context = context
925e6b
+            self._check_hostname = check_hostname
925e6b
+
925e6b
         def https_open(self, req):
925e6b
-            return self.do_open(httplib.HTTPSConnection, req)
925e6b
+            return self.do_open(httplib.HTTPSConnection, req,
925e6b
+                context=self._context, check_hostname=self._check_hostname)
925e6b
 
925e6b
         https_request = AbstractHTTPHandler.do_request_
925e6b
 
925e6b
diff -up Python-2.7.5/Lib/test/test_urllib2_localnet.py.ctx Python-2.7.5/Lib/test/test_urllib2_localnet.py
925e6b
--- Python-2.7.5/Lib/test/test_urllib2_localnet.py.ctx	2015-03-30 10:13:48.351310552 +0200
925e6b
+++ Python-2.7.5/Lib/test/test_urllib2_localnet.py	2015-03-30 10:14:54.715713679 +0200
925e6b
@@ -1,5 +1,6 @@
925e6b
 #!/usr/bin/env python
925e6b
 
925e6b
+import os
925e6b
 import urlparse
925e6b
 import urllib2
925e6b
 import BaseHTTPServer
925e6b
@@ -11,6 +12,17 @@ from test import test_support
925e6b
 mimetools = test_support.import_module('mimetools', deprecated=True)
925e6b
 threading = test_support.import_module('threading')
925e6b
 
925e6b
+try:
925e6b
+    import ssl
925e6b
+except ImportError:
925e6b
+    ssl = None
925e6b
+
925e6b
+here = os.path.dirname(__file__)
925e6b
+# Self-signed cert file for 'localhost'
925e6b
+CERT_localhost = os.path.join(here, 'keycert.pem')
925e6b
+# Self-signed cert file for 'fakehostname'
925e6b
+CERT_fakehostname = os.path.join(here, 'keycert2.pem')
925e6b
+
925e6b
 # Loopback http server infrastructure
925e6b
 
925e6b
 class LoopbackHttpServer(BaseHTTPServer.HTTPServer):
925e6b
@@ -25,7 +37,7 @@ class LoopbackHttpServer(BaseHTTPServer.
925e6b
 
925e6b
         # Set the timeout of our listening socket really low so
925e6b
         # that we can stop the server easily.
925e6b
-        self.socket.settimeout(1.0)
925e6b
+        self.socket.settimeout(0.1)
925e6b
 
925e6b
     def get_request(self):
925e6b
         """BaseHTTPServer method, overridden."""
925e6b
@@ -354,6 +366,19 @@ class TestUrlopen(BaseTestCase):
925e6b
         urllib2.install_opener(opener)
925e6b
         super(TestUrlopen, self).setUp()
925e6b
 
925e6b
+    def urlopen(self, url, data=None, **kwargs):
925e6b
+        l = []
925e6b
+        f = urllib2.urlopen(url, data, **kwargs)
925e6b
+        try:
925e6b
+            # Exercise various methods
925e6b
+            l.extend(f.readlines(200))
925e6b
+            l.append(f.readline())
925e6b
+            l.append(f.read(1024))
925e6b
+            l.append(f.read())
925e6b
+        finally:
925e6b
+            f.close()
925e6b
+        return b"".join(l)
925e6b
+
925e6b
     def start_server(self, responses):
925e6b
         handler = GetRequestHandler(responses)
925e6b
 
925e6b
@@ -364,6 +389,16 @@ class TestUrlopen(BaseTestCase):
925e6b
         handler.port = port
925e6b
         return handler
925e6b
 
925e6b
+    def start_https_server(self, responses=None, **kwargs):
925e6b
+        if not hasattr(urllib2, 'HTTPSHandler'):
925e6b
+            self.skipTest('ssl support required')
925e6b
+        from test.ssl_servers import make_https_server
925e6b
+        if responses is None:
925e6b
+            responses = [(200, [], b"we care a bit")]
925e6b
+        handler = GetRequestHandler(responses)
925e6b
+        server = make_https_server(self, handler_class=handler, **kwargs)
925e6b
+        handler.port = server.port
925e6b
+        return handler
925e6b
 
925e6b
     def test_redirection(self):
925e6b
         expected_response = 'We got here...'
925e6b
@@ -434,6 +469,28 @@ class TestUrlopen(BaseTestCase):
925e6b
         finally:
925e6b
             self.server.stop()
925e6b
 
925e6b
+    def test_https(self):
925e6b
+        handler = self.start_https_server()
925e6b
+        context = ssl.create_default_context(cafile=CERT_localhost)
925e6b
+        data = self.urlopen("https://localhost:%s/bizarre" % handler.port, context=context)
925e6b
+        self.assertEqual(data, b"we care a bit")
925e6b
+
925e6b
+    def test_https_with_cafile(self):
925e6b
+        handler = self.start_https_server(certfile=CERT_localhost)
925e6b
+        import ssl
925e6b
+        # Good cert
925e6b
+        data = self.urlopen("https://localhost:%s/bizarre" % handler.port,
925e6b
+                            cafile=CERT_localhost)
925e6b
+        self.assertEqual(data, b"we care a bit")
925e6b
+        # Bad cert
925e6b
+        with self.assertRaises(urllib2.URLError) as cm:
925e6b
+            self.urlopen("https://localhost:%s/bizarre" % handler.port,
925e6b
+                         cafile=CERT_fakehostname)
925e6b
+        # Good cert, but mismatching hostname
925e6b
+        handler = self.start_https_server(certfile=CERT_fakehostname)
925e6b
+        with self.assertRaises(ssl.CertificateError) as cm:
925e6b
+            self.urlopen("https://localhost:%s/bizarre" % handler.port,
925e6b
+                         cafile=CERT_fakehostname)
925e6b
 
925e6b
     def test_sending_headers(self):
925e6b
         handler = self.start_server([(200, [], "we don't care")])
925e6b
diff -up Python-2.7.5/Doc/library/urllib2.rst.ctx Python-2.7.5/Doc/library/urllib2.rst
925e6b
--- Python-2.7.5/Doc/library/urllib2.rst.ctx	2015-03-30 10:20:15.958747076 +0200
925e6b
+++ Python-2.7.5/Doc/library/urllib2.rst	2015-03-30 10:30:46.172779366 +0200
925e6b
@@ -22,13 +22,10 @@ redirections, cookies and more.
925e6b
 The :mod:`urllib2` module defines the following functions:
925e6b
 
925e6b
 
925e6b
-.. function:: urlopen(url[, data][, timeout])
925e6b
+.. function:: urlopen(url[, data[, timeout[, cafile[, capath[, cadefault[, context]]]]])
925e6b
 
925e6b
    Open the URL *url*, which can be either a string or a :class:`Request` object.
925e6b
 
925e6b
-   .. warning::
925e6b
-      HTTPS requests do not do any verification of the server's certificate.
925e6b
-
925e6b
    *data* may be a string specifying additional data to send to the server, or
925e6b
    ``None`` if no such data is needed.  Currently HTTP requests are the only ones
925e6b
    that use *data*; the HTTP request will be a POST instead of a GET when the
925e6b
@@ -41,7 +38,19 @@ The :mod:`urllib2` module defines the fo
925e6b
    The optional *timeout* parameter specifies a timeout in seconds for blocking
925e6b
    operations like the connection attempt (if not specified, the global default
925e6b
    timeout setting will be used).  This actually only works for HTTP, HTTPS and
925e6b
-   FTP connections.
925e6b
+         FTP connections.
925e6b
+
925e6b
+   If *context* is specified, it must be a :class:`ssl.SSLContext` instance
925e6b
+   describing the various SSL options. See :class:`~httplib.HTTPSConnection` for
925e6b
+   more details.
925e6b
+
925e6b
+   The optional *cafile* and *capath* parameters specify a set of trusted CA
925e6b
+   certificates for HTTPS requests.  *cafile* should point to a single file
925e6b
+   containing a bundle of CA certificates, whereas *capath* should point to a
925e6b
+   directory of hashed certificate files.  More information can be found in
925e6b
+   :meth:`ssl.SSLContext.load_verify_locations`.
925e6b
+
925e6b
+   The *cadefault* parameter is ignored.
925e6b
 
925e6b
    This function returns a file-like object with two additional methods:
925e6b
 
925e6b
@@ -66,7 +75,10 @@ The :mod:`urllib2` module defines the fo
925e6b
    handled through the proxy.
925e6b
 
925e6b
    .. versionchanged:: 2.6
925e6b
-      *timeout* was added.
925e6b
+     *timeout* was added.
925e6b
+
925e6b
+   .. versionchanged:: 2.7.9
925e6b
+      *cafile*, *capath*, *cadefault*, and *context* were added.
925e6b
 
925e6b
 
925e6b
 .. function:: install_opener(opener)
925e6b
@@ -280,9 +292,13 @@ The following classes are provided:
925e6b
    A class to handle opening of HTTP URLs.
925e6b
 
925e6b
 
925e6b
-.. class:: HTTPSHandler()
925e6b
+.. class:: HTTPSHandler([debuglevel[, context[, check_hostname]]])
925e6b
+
925e6b
+   A class to handle opening of HTTPS URLs. *context* and *check_hostname* have
925e6b
+   the same meaning as for :class:`httplib.HTTPSConnection`.
925e6b
 
925e6b
-   A class to handle opening of HTTPS URLs.
925e6b
+   .. versionchanged:: 2.7.9
925e6b
+      *context* and *check_hostname* were added.
925e6b
 
925e6b
 
925e6b
 .. class:: FileHandler()
925e6b
diff -up Python-2.7.5/Lib/httplib.py.ctx Python-2.7.5/Lib/httplib.py
925e6b
--- Python-2.7.5/Lib/httplib.py.ctx	2015-03-30 10:19:52.551521393 +0200
925e6b
+++ Python-2.7.5/Lib/httplib.py	2015-03-30 10:30:05.045386751 +0200
925e6b
@@ -1159,21 +1159,44 @@ else:
925e6b
 
925e6b
         def __init__(self, host, port=None, key_file=None, cert_file=None,
925e6b
                      strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
925e6b
-                     source_address=None):
925e6b
+                     source_address=None, context=None, check_hostname=None):
925e6b
             HTTPConnection.__init__(self, host, port, strict, timeout,
925e6b
                                     source_address)
925e6b
             self.key_file = key_file
925e6b
             self.cert_file = cert_file
925e6b
+            if context is None:
925e6b
+                context = ssl.create_default_context()
925e6b
+            will_verify = context.verify_mode != ssl.CERT_NONE
925e6b
+            if check_hostname is None:
925e6b
+                check_hostname = will_verify
925e6b
+            elif check_hostname and not will_verify:
925e6b
+                raise ValueError("check_hostname needs a SSL context with "
925e6b
+                                 "either CERT_OPTIONAL or CERT_REQUIRED")
925e6b
+            if key_file or cert_file:
925e6b
+                context.load_cert_chain(cert_file, key_file)
925e6b
+            self._context = context
925e6b
+            self._check_hostname = check_hostname
925e6b
 
925e6b
         def connect(self):
925e6b
             "Connect to a host on a given (SSL) port."
925e6b
 
925e6b
-            sock = socket.create_connection((self.host, self.port),
925e6b
-                                            self.timeout, self.source_address)
925e6b
+            HTTPConnection.connect(self)
925e6b
+
925e6b
             if self._tunnel_host:
925e6b
-                self.sock = sock
925e6b
-                self._tunnel()
925e6b
-            self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)
925e6b
+                server_hostname = self._tunnel_host
925e6b
+            else:
925e6b
+                server_hostname = self.host
925e6b
+            sni_hostname = server_hostname if ssl.HAS_SNI else None
925e6b
+
925e6b
+            self.sock = self._context.wrap_socket(self.sock,
925e6b
+                                                  server_hostname=sni_hostname)
925e6b
+            if not self._context.check_hostname and self._check_hostname:
925e6b
+                try:
925e6b
+                    ssl.match_hostname(self.sock.getpeercert(), server_hostname)
925e6b
+                except Exception:
925e6b
+                    self.sock.shutdown(socket.SHUT_RDWR)
925e6b
+                    self.sock.close()
925e6b
+                    raise
925e6b
 
925e6b
     __all__.append("HTTPSConnection")
925e6b
 
925e6b
diff -up Python-2.7.5/Lib/test/test_httplib.py.ctx Python-2.7.5/Lib/test/test_httplib.py
925e6b
--- Python-2.7.5/Lib/test/test_httplib.py.ctx	2015-03-30 10:19:12.905139139 +0200
925e6b
+++ Python-2.7.5/Lib/test/test_httplib.py	2015-03-30 10:27:41.822017804 +0200
925e6b
@@ -1,6 +1,7 @@
925e6b
 import httplib
925e6b
 import array
925e6b
 import httplib
925e6b
+import os
925e6b
 import StringIO
925e6b
 import socket
925e6b
 import errno
925e6b
@@ -10,6 +11,14 @@ TestCase = unittest.TestCase
925e6b
 
925e6b
 from test import test_support
925e6b
 
925e6b
+here = os.path.dirname(__file__)
925e6b
+# Self-signed cert file for 'localhost'
925e6b
+CERT_localhost = os.path.join(here, 'keycert.pem')
925e6b
+# Self-signed cert file for 'fakehostname'
925e6b
+CERT_fakehostname = os.path.join(here, 'keycert2.pem')
925e6b
+# Self-signed cert file for self-signed.pythontest.net
925e6b
+CERT_selfsigned_pythontestdotnet = os.path.join(here, 'selfsigned_pythontestdotnet.pem')
925e6b
+
925e6b
 HOST = test_support.HOST
925e6b
 
925e6b
 class FakeSocket:
925e6b
@@ -493,40 +502,147 @@ class TimeoutTest(TestCase):
925e6b
         httpConn.close()
925e6b
 
925e6b
 
925e6b
-class HTTPSTimeoutTest(TestCase):
925e6b
+class HTTPSTest(TestCase):
925e6b
 # XXX Here should be tests for HTTPS, there isn't any right now!
925e6b
+    def setUp(self):
925e6b
+        if not hasattr(httplib, 'HTTPSConnection'):
925e6b
+            self.skipTest('ssl support required')
925e6b
+
925e6b
+    def make_server(self, certfile):
925e6b
+        from test.ssl_servers import make_https_server
925e6b
+        return make_https_server(self, certfile=certfile)
925e6b
 
925e6b
     def test_attributes(self):
925e6b
-        # simple test to check it's storing it
925e6b
-        if hasattr(httplib, 'HTTPSConnection'):
925e6b
-            h = httplib.HTTPSConnection(HOST, TimeoutTest.PORT, timeout=30)
925e6b
-            self.assertEqual(h.timeout, 30)
925e6b
+        # simple test to check it's storing the timeout
925e6b
+        h = httplib.HTTPSConnection(HOST, TimeoutTest.PORT, timeout=30)
925e6b
+        self.assertEqual(h.timeout, 30)
925e6b
+
925e6b
+    def test_networked(self):
925e6b
+        # Default settings: requires a valid cert from a trusted CA
925e6b
+        import ssl
925e6b
+        test_support.requires('network')
925e6b
+        with test_support.transient_internet('self-signed.pythontest.net'):
925e6b
+            h = httplib.HTTPSConnection('self-signed.pythontest.net', 443)
925e6b
+            with self.assertRaises(ssl.SSLError) as exc_info:
925e6b
+                h.request('GET', '/')
925e6b
+            self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
925e6b
+
925e6b
+    def test_networked_noverification(self):
925e6b
+        # Switch off cert verification
925e6b
+        import ssl
925e6b
+        test_support.requires('network')
925e6b
+        with test_support.transient_internet('self-signed.pythontest.net'):
925e6b
+            context = ssl._create_stdlib_context()
925e6b
+            h = httplib.HTTPSConnection('self-signed.pythontest.net', 443,
925e6b
+                                        context=context)
925e6b
+            h.request('GET', '/')
925e6b
+            resp = h.getresponse()
925e6b
+            self.assertIn('nginx', resp.getheader('server'))
925e6b
+
925e6b
+    def test_networked_trusted_by_default_cert(self):
925e6b
+        # Default settings: requires a valid cert from a trusted CA
925e6b
+        test_support.requires('network')
925e6b
+        with test_support.transient_internet('www.python.org'):
925e6b
+            h = httplib.HTTPSConnection('www.python.org', 443)
925e6b
+            h.request('GET', '/')
925e6b
+            resp = h.getresponse()
925e6b
+            content_type = resp.getheader('content-type')
925e6b
+            self.assertIn('text/html', content_type)
925e6b
+
925e6b
+    def test_networked_good_cert(self):
925e6b
+        # We feed the server's cert as a validating cert
925e6b
+        import ssl
925e6b
+        test_support.requires('network')
925e6b
+        with test_support.transient_internet('self-signed.pythontest.net'):
925e6b
+            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
925e6b
+            context.verify_mode = ssl.CERT_REQUIRED
925e6b
+            context.load_verify_locations(CERT_selfsigned_pythontestdotnet)
925e6b
+            h = httplib.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
925e6b
+            h.request('GET', '/')
925e6b
+            resp = h.getresponse()
925e6b
+            server_string = resp.getheader('server')
925e6b
+            self.assertIn('nginx', server_string)
925e6b
+
925e6b
+    def test_networked_bad_cert(self):
925e6b
+        # We feed a "CA" cert that is unrelated to the server's cert
925e6b
+        import ssl
925e6b
+        test_support.requires('network')
925e6b
+        with test_support.transient_internet('self-signed.pythontest.net'):
925e6b
+            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
925e6b
+            context.verify_mode = ssl.CERT_REQUIRED
925e6b
+            context.load_verify_locations(CERT_localhost)
925e6b
+            h = httplib.HTTPSConnection('self-signed.pythontest.net', 443, context=context)
925e6b
+            with self.assertRaises(ssl.SSLError) as exc_info:
925e6b
+                h.request('GET', '/')
925e6b
+            self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
925e6b
+
925e6b
+    def test_local_unknown_cert(self):
925e6b
+        # The custom cert isn't known to the default trust bundle
925e6b
+        import ssl
925e6b
+        server = self.make_server(CERT_localhost)
925e6b
+        h = httplib.HTTPSConnection('localhost', server.port)
925e6b
+        with self.assertRaises(ssl.SSLError) as exc_info:
925e6b
+            h.request('GET', '/')
925e6b
+        self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
925e6b
+
925e6b
+    def test_local_good_hostname(self):
925e6b
+        # The (valid) cert validates the HTTP hostname
925e6b
+        import ssl
925e6b
+        server = self.make_server(CERT_localhost)
925e6b
+        context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
925e6b
+        context.verify_mode = ssl.CERT_REQUIRED
925e6b
+        context.load_verify_locations(CERT_localhost)
925e6b
+        h = httplib.HTTPSConnection('localhost', server.port, context=context)
925e6b
+        h.request('GET', '/nonexistent')
925e6b
+        resp = h.getresponse()
925e6b
+        self.assertEqual(resp.status, 404)
925e6b
+
925e6b
+    def test_local_bad_hostname(self):
925e6b
+        # The (valid) cert doesn't validate the HTTP hostname
925e6b
+        import ssl
925e6b
+        server = self.make_server(CERT_fakehostname)
925e6b
+        context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
925e6b
+        context.verify_mode = ssl.CERT_REQUIRED
925e6b
+        context.load_verify_locations(CERT_fakehostname)
925e6b
+        h = httplib.HTTPSConnection('localhost', server.port, context=context)
925e6b
+        with self.assertRaises(ssl.CertificateError):
925e6b
+            h.request('GET', '/')
925e6b
+        # Same with explicit check_hostname=True
925e6b
+        h = httplib.HTTPSConnection('localhost', server.port, context=context,
925e6b
+                                   check_hostname=True)
925e6b
+        with self.assertRaises(ssl.CertificateError):
925e6b
+            h.request('GET', '/')
925e6b
+        # With check_hostname=False, the mismatching is ignored
925e6b
+        h = httplib.HTTPSConnection('localhost', server.port, context=context,
925e6b
+                                   check_hostname=False)
925e6b
+        h.request('GET', '/nonexistent')
925e6b
+        resp = h.getresponse()
925e6b
+        self.assertEqual(resp.status, 404)
925e6b
 
925e6b
-    @unittest.skipIf(not hasattr(httplib, 'HTTPS'), 'httplib.HTTPS not available')
925e6b
     def test_host_port(self):
925e6b
         # Check invalid host_port
925e6b
 
925e6b
-        # Note that httplib does not accept user:password@ in the host-port.
925e6b
         for hp in ("www.python.org:abc", "user:password@www.python.org"):
925e6b
-            self.assertRaises(httplib.InvalidURL, httplib.HTTP, hp)
925e6b
+            self.assertRaises(httplib.InvalidURL, httplib.HTTPSConnection, hp)
925e6b
 
925e6b
-        for hp, h, p in (("[fe80::207:e9ff:fe9b]:8000", "fe80::207:e9ff:fe9b",
925e6b
-                          8000),
925e6b
-                         ("pypi.python.org:443", "pypi.python.org", 443),
925e6b
-                         ("pypi.python.org", "pypi.python.org", 443),
925e6b
-                         ("pypi.python.org:", "pypi.python.org", 443),
925e6b
-                         ("[fe80::207:e9ff:fe9b]", "fe80::207:e9ff:fe9b", 443)):
925e6b
-            http = httplib.HTTPS(hp)
925e6b
-            c = http._conn
925e6b
-            if h != c.host:
925e6b
-                self.fail("Host incorrectly parsed: %s != %s" % (h, c.host))
925e6b
-            if p != c.port:
925e6b
-                self.fail("Port incorrectly parsed: %s != %s" % (p, c.host))
925e6b
+        for hp, h, p in (("[fe80::207:e9ff:fe9b]:8000",
925e6b
+                          "fe80::207:e9ff:fe9b", 8000),
925e6b
+                         ("www.python.org:443", "www.python.org", 443),
925e6b
+                         ("www.python.org:", "www.python.org", 443),
925e6b
+                         ("www.python.org", "www.python.org", 443),
925e6b
+                         ("[fe80::207:e9ff:fe9b]", "fe80::207:e9ff:fe9b", 443),
925e6b
+                         ("[fe80::207:e9ff:fe9b]:", "fe80::207:e9ff:fe9b",
925e6b
+                             443)):
925e6b
+            c = httplib.HTTPSConnection(hp)
925e6b
+            self.assertEqual(h, c.host)
925e6b
+            self.assertEqual(p, c.port)
925e6b
+ 
925e6b
 
925e6b
 
925e6b
+@test_support.reap_threads
925e6b
 def test_main(verbose=None):
925e6b
     test_support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
925e6b
-                              HTTPSTimeoutTest, SourceAddressTest)
925e6b
+                              HTTPSTest, SourceAddressTest)
925e6b
 
925e6b
 if __name__ == '__main__':
925e6b
     test_main()
925e6b
diff -up Python-2.7.5/Lib/test/test_ssl.py.ctx Python-2.7.5/Lib/test/test_ssl.py
925e6b
--- Python-2.7.5/Lib/test/test_ssl.py.ctx	2015-03-30 10:18:55.677973042 +0200
925e6b
+++ Python-2.7.5/Lib/test/test_ssl.py	2015-03-30 10:22:02.323772604 +0200
925e6b
@@ -14,7 +14,7 @@ import os
925e6b
 import errno
925e6b
 import pprint
925e6b
 import tempfile
925e6b
-import urllib
925e6b
+import urllib2
925e6b
 import traceback
925e6b
 import weakref
925e6b
 import platform
925e6b
@@ -2332,9 +2332,10 @@ else:
925e6b
                 d1 = f.read()
925e6b
             d2 = ''
925e6b
             # now fetch the same data from the HTTPS server
925e6b
-            url = 'https://%s:%d/%s' % (
925e6b
-                HOST, server.port, os.path.split(CERTFILE)[1])
925e6b
-            f = urllib.urlopen(url)
925e6b
+            url = 'https://localhost:%d/%s' % (
925e6b
+                server.port, os.path.split(CERTFILE)[1])
925e6b
+            context = ssl.create_default_context(cafile=CERTFILE)
925e6b
+            f = urllib2.urlopen(url, context=context)
925e6b
             try:
925e6b
                 dlen = f.info().getheader("content-length")
925e6b
                 if dlen and (int(dlen) > 0):