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