Commit 8f35ca92 authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #9003: http.client.HTTPSConnection, urllib.request.HTTPSHandler and

urllib.request.urlopen now take optional arguments to allow for
server certificate checking, as recommended in public uses of HTTPS.
parent d84e495b
...@@ -50,19 +50,31 @@ The module provides the following classes: ...@@ -50,19 +50,31 @@ The module provides the following classes:
*source_address* was added. *source_address* was added.
.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]]) .. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]], *, context=None, check_hostname=None)
A subclass of :class:`HTTPConnection` that uses SSL for communication with 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 secure servers. Default port is ``443``. If *context* is specified, it
formatted file that contains your private key, and *cert_file* is a PEM must be a :class:`ssl.SSLContext` instance describing the various SSL
formatted certificate chain file; both can be used for authenticating options. If *context* is specified and has a :attr:`~ssl.SSLContext.verify_mode`
yourself against the server. 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
.. warning:: server's certificate. If you want to change that behaviour, you can
This does not do any verification of the server's certificate. explicitly set *check_hostname* to False.
*key_file* and *cert_file* are deprecated, please use
:meth:`ssl.SSLContext.load_cert_chain` instead.
If you access arbitrary hosts on the Internet, it is recommended to
require certificate checking and feed the *context* with a set of
trusted CA certificates::
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('/etc/pki/tls/certs/ca-bundle.crt')
h = client.HTTPSConnection('svn.python.org', 443, context=context)
.. versionchanged:: 3.2 .. versionchanged:: 3.2
*source_address* was added. *source_address*, *context* and *check_hostname* were added.
.. class:: HTTPResponse(sock, debuglevel=0, strict=0, method=None, url=None) .. class:: HTTPResponse(sock, debuglevel=0, strict=0, method=None, url=None)
......
...@@ -15,14 +15,11 @@ authentication, redirections, cookies and more. ...@@ -15,14 +15,11 @@ authentication, redirections, cookies and more.
The :mod:`urllib.request` module defines the following functions: The :mod:`urllib.request` module defines the following functions:
.. function:: urlopen(url, data=None[, timeout]) .. function:: urlopen(url, data=None[, timeout], *, cafile=None, capath=None)
Open the URL *url*, which can be either a string or a Open the URL *url*, which can be either a string or a
:class:`Request` object. :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 *data* may be a string specifying additional data to send to the
server, or ``None`` if no such data is needed. Currently HTTP server, or ``None`` if no such data is needed. Currently HTTP
requests are the only ones that use *data*; the HTTP request will requests are the only ones that use *data*; the HTTP request will
...@@ -38,6 +35,16 @@ The :mod:`urllib.request` module defines the following functions: ...@@ -38,6 +35,16 @@ The :mod:`urllib.request` module defines the following functions:
the global default timeout setting will be used). This actually the global default timeout setting will be used). This actually
only works for HTTP, HTTPS and FTP connections. only works for HTTP, HTTPS and FTP connections.
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`.
.. warning::
If neither *cafile* nor *capath* is specified, an HTTPS request
will not do any verification of the server's certificate.
This function returns a file-like object with two additional methods from This function returns a file-like object with two additional methods from
the :mod:`urllib.response` module the :mod:`urllib.response` module
...@@ -62,6 +69,9 @@ The :mod:`urllib.request` module defines the following functions: ...@@ -62,6 +69,9 @@ The :mod:`urllib.request` module defines the following functions:
Proxy handling, which was done by passing a dictionary parameter to Proxy handling, which was done by passing a dictionary parameter to
``urllib.urlopen``, can be obtained by using :class:`ProxyHandler` objects. ``urllib.urlopen``, can be obtained by using :class:`ProxyHandler` objects.
.. versionchanged:: 3.2
*cafile* and *capath* were added.
.. function:: install_opener(opener) .. function:: install_opener(opener)
Install an :class:`OpenerDirector` instance as the default global opener. Install an :class:`OpenerDirector` instance as the default global opener.
...@@ -421,9 +431,13 @@ The following classes are provided: ...@@ -421,9 +431,13 @@ The following classes are provided:
A class to handle opening of HTTP URLs. A class to handle opening of HTTP URLs.
.. class:: HTTPSHandler() .. class:: HTTPSHandler(debuglevel=0, context=None, check_hostname=None)
A class to handle opening of HTTPS URLs. *context* and *check_hostname*
have the same meaning as in :class:`http.client.HTTPSConnection`.
A class to handle opening of HTTPS URLs. .. versionchanged:: 3.2
*context* and *check_hostname* were added.
.. class:: FileHandler() .. class:: FileHandler()
......
...@@ -1047,13 +1047,29 @@ else: ...@@ -1047,13 +1047,29 @@ else:
default_port = HTTPS_PORT default_port = HTTPS_PORT
# XXX Should key_file and cert_file be deprecated in favour of context?
def __init__(self, host, port=None, key_file=None, cert_file=None, def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None): source_address=None, *, context=None, check_hostname=None):
super(HTTPSConnection, self).__init__(host, port, strict, timeout, super(HTTPSConnection, self).__init__(host, port, strict, timeout,
source_address) source_address)
self.key_file = key_file self.key_file = key_file
self.cert_file = cert_file self.cert_file = cert_file
if context is None:
# Some reasonable defaults
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
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(certfile, keyfile)
self._context = context
self._check_hostname = check_hostname
def connect(self): def connect(self):
"Connect to a host on a given (SSL) port." "Connect to a host on a given (SSL) port."
...@@ -1065,7 +1081,14 @@ else: ...@@ -1065,7 +1081,14 @@ else:
self.sock = sock self.sock = sock
self._tunnel() self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) self.sock = self._context.wrap_socket(sock)
try:
if self._check_hostname:
ssl.match_hostname(self.sock.getpeercert(), self.host)
except Exception:
self.sock.shutdown(socket.SHUT_RDWR)
self.sock.close()
raise
__all__.append("HTTPSConnection") __all__.append("HTTPSConnection")
......
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJnsJZVrppL+W5I9
zGQrrawWwE5QJpBK9nWw17mXrZ03R1cD9BamLGivVISbPlRlAVnZBEyh1ATpsB7d
CUQ+WHEvALquvx4+Yw5l+fXeiYRjrLRBYZuVy8yNtXzU3iWcGObcYRkUdiXdOyP7
sLF2YZHRvQZpzgDBKkrraeQ81w21AgMBAAECgYBEm7n07FMHWlE+0kT0sXNsLYfy
YE+QKZnJw9WkaDN+zFEEPELkhZVt5BjsMraJr6v2fIEqF0gGGJPkbenffVq2B5dC
lWUOxvJHufMK4sM3Cp6s/gOp3LP+QkzVnvJSfAyZU6l+4PGX5pLdUsXYjPxgzjzL
S36tF7/2Uv1WePyLUQJBAMsPhYzUXOPRgmbhcJiqi9A9c3GO8kvSDYTCKt3VMnqz
HBn6MQ4VQasCD1F+7jWTI0FU/3vdw8non/Fj8hhYqZcCQQDCDRdvmZqDiZnpMqDq
L6ZSrLTVtMvZXZbgwForaAD9uHj51TME7+eYT7EG2YCgJTXJ4YvRJEnPNyskwdKt
vTSTAkEAtaaN/vyemEJ82BIGStwONNw0ILsSr5cZ9tBHzqiA/tipY+e36HRFiXhP
QcU9zXlxyWkDH8iz9DSAmE2jbfoqwwJANlMJ65E543cjIlitGcKLMnvtCCLcKpb7
xSG0XJB6Lo11OKPJ66jp0gcFTSCY1Lx2CXVd+gfJrfwI1Pp562+bhwJBAJ9IfDPU
R8OpO9v1SGd8x33Owm7uXOpB9d63/T70AD1QOXjKUC4eXYbt0WWfWuny/RNPRuyh
w7DXSfUF+kPKolU=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICXTCCAcagAwIBAgIJAIO3upAG445fMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV
BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u
IFNvZnR3YXJlIEZvdW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTAeFw0x
MDEwMDkxNTAxMDBaFw0yMDEwMDYxNTAxMDBaMGIxCzAJBgNVBAYTAlhZMRcwFQYD
VQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZv
dW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTCBnzANBgkqhkiG9w0BAQEF
AAOBjQAwgYkCgYEAmewllWumkv5bkj3MZCutrBbATlAmkEr2dbDXuZetnTdHVwP0
FqYsaK9UhJs+VGUBWdkETKHUBOmwHt0JRD5YcS8Auq6/Hj5jDmX59d6JhGOstEFh
m5XLzI21fNTeJZwY5txhGRR2Jd07I/uwsXZhkdG9BmnOAMEqSutp5DzXDbUCAwEA
AaMbMBkwFwYDVR0RBBAwDoIMZmFrZWhvc3RuYW1lMA0GCSqGSIb3DQEBBQUAA4GB
AH+iMClLLGSaKWgwXsmdVo4FhTZZHo8Uprrtg3N9FxEeE50btpDVQysgRt5ias3K
m+bME9zbKwvbVWD5zZdjus4pDgzwF/iHyccL8JyYhxOvS/9zmvAtFXj/APIIbZFp
IT75d9f88ScIGEtknZQejnrdhB64tYki/EqluiuKBqKD
-----END CERTIFICATE-----
...@@ -57,3 +57,8 @@ if __name__ == '__main__': ...@@ -57,3 +57,8 @@ if __name__ == '__main__':
with open('keycert.pem', 'w') as f: with open('keycert.pem', 'w') as f:
f.write(key) f.write(key)
f.write(cert) f.write(cert)
# For certificate matching tests
cert, key = make_cert_key('fakehostname')
with open('keycert2.pem', 'w') as f:
f.write(key)
f.write(cert)
import os
import sys
import ssl
import threading
import urllib.parse
# Rename HTTPServer to _HTTPServer so as to avoid confusion with HTTPSServer.
from http.server import HTTPServer as _HTTPServer, SimpleHTTPRequestHandler
from test import support
here = os.path.dirname(__file__)
HOST = support.HOST
CERTFILE = os.path.join(here, 'keycert.pem')
# This one's based on HTTPServer, which is based on SocketServer
class HTTPSServer(_HTTPServer):
def __init__(self, server_address, handler_class, context):
_HTTPServer.__init__(self, server_address, handler_class)
self.context = context
def __str__(self):
return ('<%s %s:%s>' %
(self.__class__.__name__,
self.server_name,
self.server_port))
def get_request(self):
# override this to wrap socket with SSL
sock, addr = self.socket.accept()
sslconn = self.context.wrap_socket(sock, server_side=True)
return sslconn, addr
class RootedHTTPRequestHandler(SimpleHTTPRequestHandler):
# need to override translate_path to get a known root,
# instead of using os.curdir, since the test could be
# run from anywhere
server_version = "TestHTTPS/1.0"
root = here
# Avoid hanging when a request gets interrupted by the client
timeout = 5
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
"""
# abandon query parameters
path = urllib.parse.urlparse(path)[2]
path = os.path.normpath(urllib.parse.unquote(path))
words = path.split('/')
words = filter(None, words)
path = self.root
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
path = os.path.join(path, word)
return path
def log_message(self, format, *args):
# we override this to suppress logging unless "verbose"
if support.verbose:
sys.stdout.write(" server (%s:%d %s):\n [%s] %s\n" %
(self.server.server_address,
self.server.server_port,
self.request.cipher(),
self.log_date_time_string(),
format%args))
class HTTPSServerThread(threading.Thread):
def __init__(self, context, host=HOST, handler_class=None):
self.flag = None
self.server = HTTPSServer((host, 0),
handler_class or RootedHTTPRequestHandler,
context)
self.port = self.server.server_port
threading.Thread.__init__(self)
self.daemon = True
def __str__(self):
return "<%s %s>" % (self.__class__.__name__, self.server)
def start(self, flag=None):
self.flag = flag
threading.Thread.start(self)
def run(self):
if self.flag:
self.flag.set()
self.server.serve_forever(0.05)
def stop(self):
self.server.shutdown()
def make_https_server(case, certfile=CERTFILE, host=HOST, handler_class=None):
# we assume the certfile contains both private key and certificate
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.load_cert_chain(certfile)
server = HTTPSServerThread(context, host, handler_class)
flag = threading.Event()
server.start(flag)
flag.wait()
def cleanup():
if support.verbose:
sys.stdout.write('stopping HTTPS server\n')
server.stop()
if support.verbose:
sys.stdout.write('joining HTTPS thread\n')
server.join()
case.addCleanup(cleanup)
return server
import errno import errno
from http import client from http import client
import io import io
import os
import array import array
import socket import socket
...@@ -9,6 +10,14 @@ TestCase = unittest.TestCase ...@@ -9,6 +10,14 @@ TestCase = unittest.TestCase
from test import support from test import 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')
# Root cert file (CA) for svn.python.org's cert
CACERT_svn_python_org = os.path.join(here, 'https_svn_python_org_root.pem')
HOST = support.HOST HOST = support.HOST
class FakeSocket: class FakeSocket:
...@@ -370,15 +379,98 @@ class TimeoutTest(TestCase): ...@@ -370,15 +379,98 @@ class TimeoutTest(TestCase):
self.assertEqual(httpConn.sock.gettimeout(), 30) self.assertEqual(httpConn.sock.gettimeout(), 30)
httpConn.close() httpConn.close()
class HTTPSTimeoutTest(TestCase):
# XXX Here should be tests for HTTPS, there isn't any right now! class HTTPSTest(TestCase):
def setUp(self):
if not hasattr(client, '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)
def test_attributes(self): def test_attributes(self):
# simple test to check it's storing it # simple test to check it's storing the timeout
if hasattr(client, 'HTTPSConnection'):
h = client.HTTPSConnection(HOST, TimeoutTest.PORT, timeout=30) h = client.HTTPSConnection(HOST, TimeoutTest.PORT, timeout=30)
self.assertEqual(h.timeout, 30) self.assertEqual(h.timeout, 30)
def _check_svn_python_org(self, resp):
# Just a simple check that everything went fine
server_string = resp.getheader('server')
self.assertIn('Apache', server_string)
def test_networked(self):
# Default settings: no cert verification is done
support.requires('network')
with support.transient_internet('svn.python.org'):
h = client.HTTPSConnection('svn.python.org', 443)
h.request('GET', '/')
resp = h.getresponse()
self._check_svn_python_org(resp)
def test_networked_good_cert(self):
# We feed a CA cert that validates the server's cert
import ssl
support.requires('network')
with support.transient_internet('svn.python.org'):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CACERT_svn_python_org)
h = client.HTTPSConnection('svn.python.org', 443, context=context)
h.request('GET', '/')
resp = h.getresponse()
self._check_svn_python_org(resp)
def test_networked_bad_cert(self):
# We feed a "CA" cert that is unrelated to the server's cert
import ssl
support.requires('network')
with support.transient_internet('svn.python.org'):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT_localhost)
h = client.HTTPSConnection('svn.python.org', 443, context=context)
with self.assertRaises(ssl.SSLError):
h.request('GET', '/')
def test_local_good_hostname(self):
# The (valid) cert validates the HTTP hostname
import ssl
from test.ssl_servers import make_https_server
server = make_https_server(self, CERT_localhost)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT_localhost)
h = client.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
from test.ssl_servers import make_https_server
server = make_https_server(self, CERT_fakehostname)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT_fakehostname)
h = client.HTTPSConnection('localhost', server.port, context=context)
with self.assertRaises(ssl.CertificateError):
h.request('GET', '/')
# Same with explicit check_hostname=True
h = client.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 = client.HTTPSConnection('localhost', server.port, context=context,
check_hostname=False)
h.request('GET', '/nonexistent')
resp = h.getresponse()
self.assertEqual(resp.status, 404)
class RequestBodyTest(TestCase): class RequestBodyTest(TestCase):
"""Test cases where a request includes a message body.""" """Test cases where a request includes a message body."""
...@@ -488,7 +580,7 @@ class HTTPResponseTest(TestCase): ...@@ -488,7 +580,7 @@ class HTTPResponseTest(TestCase):
def test_main(verbose=None): def test_main(verbose=None):
support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
HTTPSTimeoutTest, RequestBodyTest, SourceAddressTest, HTTPSTest, RequestBodyTest, SourceAddressTest,
HTTPResponseTest) HTTPResponseTest)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -11,15 +11,13 @@ import os ...@@ -11,15 +11,13 @@ import os
import errno import errno
import pprint import pprint
import tempfile import tempfile
import urllib.parse, urllib.request import urllib.request
import traceback import traceback
import asyncore import asyncore
import weakref import weakref
import platform import platform
import functools import functools
from http.server import HTTPServer, SimpleHTTPRequestHandler
# Optionally test SSL support, if we have it in the tested platform # Optionally test SSL support, if we have it in the tested platform
skip_expected = False skip_expected = False
try: try:
...@@ -605,6 +603,8 @@ except ImportError: ...@@ -605,6 +603,8 @@ except ImportError:
else: else:
_have_threads = True _have_threads = True
from test.ssl_servers import make_https_server
class ThreadedEchoServer(threading.Thread): class ThreadedEchoServer(threading.Thread):
class ConnectionHandler(threading.Thread): class ConnectionHandler(threading.Thread):
...@@ -774,98 +774,6 @@ else: ...@@ -774,98 +774,6 @@ else:
def stop(self): def stop(self):
self.active = False self.active = False
class OurHTTPSServer(threading.Thread):
# This one's based on HTTPServer, which is based on SocketServer
class HTTPSServer(HTTPServer):
def __init__(self, server_address, RequestHandlerClass, certfile):
HTTPServer.__init__(self, server_address, RequestHandlerClass)
# we assume the certfile contains both private key and certificate
self.certfile = certfile
self.allow_reuse_address = True
def __str__(self):
return ('<%s %s:%s>' %
(self.__class__.__name__,
self.server_name,
self.server_port))
def get_request(self):
# override this to wrap socket with SSL
sock, addr = self.socket.accept()
sslconn = ssl.wrap_socket(sock, server_side=True,
certfile=self.certfile)
return sslconn, addr
class RootedHTTPRequestHandler(SimpleHTTPRequestHandler):
# need to override translate_path to get a known root,
# instead of using os.curdir, since the test could be
# run from anywhere
server_version = "TestHTTPS/1.0"
root = None
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
"""
# abandon query parameters
path = urllib.parse.urlparse(path)[2]
path = os.path.normpath(urllib.parse.unquote(path))
words = path.split('/')
words = filter(None, words)
path = self.root
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in self.root: continue
path = os.path.join(path, word)
return path
def log_message(self, format, *args):
# we override this to suppress logging unless "verbose"
if support.verbose:
sys.stdout.write(" server (%s:%d %s):\n [%s] %s\n" %
(self.server.server_address,
self.server.server_port,
self.request.cipher(),
self.log_date_time_string(),
format%args))
def __init__(self, certfile):
self.flag = None
self.RootedHTTPRequestHandler.root = os.path.split(CERTFILE)[0]
self.server = self.HTTPSServer(
(HOST, 0), self.RootedHTTPRequestHandler, certfile)
self.port = self.server.server_port
threading.Thread.__init__(self)
self.daemon = True
def __str__(self):
return "<%s %s>" % (self.__class__.__name__, self.server)
def start(self, flag=None):
self.flag = flag
threading.Thread.start(self)
def run(self):
if self.flag:
self.flag.set()
self.server.serve_forever(0.05)
def stop(self):
self.server.shutdown()
class AsyncoreEchoServer(threading.Thread): class AsyncoreEchoServer(threading.Thread):
# this one's based on asyncore.dispatcher # this one's based on asyncore.dispatcher
...@@ -1349,13 +1257,8 @@ else: ...@@ -1349,13 +1257,8 @@ else:
def test_socketserver(self): def test_socketserver(self):
"""Using a SocketServer to create and manage SSL connections.""" """Using a SocketServer to create and manage SSL connections."""
server = OurHTTPSServer(CERTFILE) server = make_https_server(self, CERTFILE)
flag = threading.Event()
server.start(flag)
# wait for it to start
flag.wait()
# try to connect # try to connect
try:
if support.verbose: if support.verbose:
sys.stdout.write('\n') sys.stdout.write('\n')
with open(CERTFILE, 'rb') as f: with open(CERTFILE, 'rb') as f:
...@@ -1365,6 +1268,7 @@ else: ...@@ -1365,6 +1268,7 @@ else:
url = 'https://%s:%d/%s' % ( url = 'https://%s:%d/%s' % (
HOST, server.port, os.path.split(CERTFILE)[1]) HOST, server.port, os.path.split(CERTFILE)[1])
f = urllib.request.urlopen(url) f = urllib.request.urlopen(url)
try:
dlen = f.info().get("content-length") dlen = f.info().get("content-length")
if dlen and (int(dlen) > 0): if dlen and (int(dlen) > 0):
d2 = f.read(int(dlen)) d2 = f.read(int(dlen))
...@@ -1372,15 +1276,9 @@ else: ...@@ -1372,15 +1276,9 @@ else:
sys.stdout.write( sys.stdout.write(
" client: read %d bytes from remote server '%s'\n" " client: read %d bytes from remote server '%s'\n"
% (len(d2), server)) % (len(d2), server))
finally:
f.close() f.close()
self.assertEqual(d1, d2) self.assertEqual(d1, d2)
finally:
if support.verbose:
sys.stdout.write('stopping server\n')
server.stop()
if support.verbose:
sys.stdout.write('joining thread\n')
server.join()
def test_asyncore_server(self): def test_asyncore_server(self):
"""Check the example asyncore integration.""" """Check the example asyncore integration."""
......
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import email import email
import urllib.parse import urllib.parse
import urllib.request import urllib.request
...@@ -9,6 +10,13 @@ import hashlib ...@@ -9,6 +10,13 @@ import hashlib
from test import support from test import support
threading = support.import_module('threading') threading = support.import_module('threading')
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 # Loopback http server infrastructure
class LoopbackHttpServer(http.server.HTTPServer): class LoopbackHttpServer(http.server.HTTPServer):
...@@ -23,7 +31,7 @@ class LoopbackHttpServer(http.server.HTTPServer): ...@@ -23,7 +31,7 @@ class LoopbackHttpServer(http.server.HTTPServer):
# Set the timeout of our listening socket really low so # Set the timeout of our listening socket really low so
# that we can stop the server easily. # that we can stop the server easily.
self.socket.settimeout(1.0) self.socket.settimeout(0.1)
def get_request(self): def get_request(self):
"""HTTPServer method, overridden.""" """HTTPServer method, overridden."""
...@@ -221,15 +229,7 @@ class FakeProxyHandler(http.server.BaseHTTPRequestHandler): ...@@ -221,15 +229,7 @@ class FakeProxyHandler(http.server.BaseHTTPRequestHandler):
# Test cases # Test cases
class BaseTestCase(unittest.TestCase): class ProxyAuthTests(unittest.TestCase):
def setUp(self):
self._threads = support.threading_setup()
def tearDown(self):
support.threading_cleanup(*self._threads)
class ProxyAuthTests(BaseTestCase):
URL = "http://localhost" URL = "http://localhost"
USER = "tester" USER = "tester"
...@@ -340,7 +340,7 @@ def GetRequestHandler(responses): ...@@ -340,7 +340,7 @@ def GetRequestHandler(responses):
return FakeHTTPRequestHandler return FakeHTTPRequestHandler
class TestUrlopen(BaseTestCase): class TestUrlopen(unittest.TestCase):
"""Tests urllib.request.urlopen using the network. """Tests urllib.request.urlopen using the network.
These tests are not exhaustive. Assuming that testing using files does a These tests are not exhaustive. Assuming that testing using files does a
...@@ -358,9 +358,9 @@ class TestUrlopen(BaseTestCase): ...@@ -358,9 +358,9 @@ class TestUrlopen(BaseTestCase):
self.server.stop() self.server.stop()
super(TestUrlopen, self).tearDown() super(TestUrlopen, self).tearDown()
def urlopen(self, url, data=None): def urlopen(self, url, data=None, **kwargs):
l = [] l = []
f = urllib.request.urlopen(url, data) f = urllib.request.urlopen(url, data, **kwargs)
try: try:
# Exercise various methods # Exercise various methods
l.extend(f.readlines(200)) l.extend(f.readlines(200))
...@@ -383,6 +383,17 @@ class TestUrlopen(BaseTestCase): ...@@ -383,6 +383,17 @@ class TestUrlopen(BaseTestCase):
handler.port = port handler.port = port
return handler return handler
def start_https_server(self, responses=None, certfile=CERT_localhost):
if not hasattr(urllib.request, '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, certfile=certfile, handler_class=handler)
handler.port = server.port
return handler
def test_redirection(self): def test_redirection(self):
expected_response = b"We got here..." expected_response = b"We got here..."
responses = [ responses = [
...@@ -440,6 +451,28 @@ class TestUrlopen(BaseTestCase): ...@@ -440,6 +451,28 @@ class TestUrlopen(BaseTestCase):
self.assertEqual(data, expected_response) self.assertEqual(data, expected_response)
self.assertEqual(handler.requests, ["/bizarre", b"get=with_feeling"]) self.assertEqual(handler.requests, ["/bizarre", b"get=with_feeling"])
def test_https(self):
handler = self.start_https_server()
data = self.urlopen("https://localhost:%s/bizarre" % handler.port)
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(urllib.error.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): def test_sending_headers(self):
handler = self.start_server() handler = self.start_server()
req = urllib.request.Request("http://localhost:%s/" % handler.port, req = urllib.request.Request("http://localhost:%s/" % handler.port,
...@@ -521,6 +554,8 @@ class TestUrlopen(BaseTestCase): ...@@ -521,6 +554,8 @@ class TestUrlopen(BaseTestCase):
(index, len(lines[index]), len(line))) (index, len(lines[index]), len(line)))
self.assertEqual(index + 1, len(lines)) self.assertEqual(index + 1, len(lines))
@support.reap_threads
def test_main(): def test_main():
support.run_unittest(ProxyAuthTests, TestUrlopen) support.run_unittest(ProxyAuthTests, TestUrlopen)
......
...@@ -114,11 +114,27 @@ else: ...@@ -114,11 +114,27 @@ else:
__version__ = sys.version[:3] __version__ = sys.version[:3]
_opener = None _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):
global _opener global _opener
if _opener is None: if cafile or capath:
_opener = build_opener() if not _have_ssl:
return _opener.open(url, data, timeout) raise ValueError('SSL support not available')
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
if cafile or capath:
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(cafile, capath)
check_hostname = True
else:
check_hostname = False
https_handler = HTTPSHandler(context=context, check_hostname=check_hostname)
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): def install_opener(opener):
global _opener global _opener
...@@ -1053,7 +1069,7 @@ class AbstractHTTPHandler(BaseHandler): ...@@ -1053,7 +1069,7 @@ class AbstractHTTPHandler(BaseHandler):
return request return request
def do_open(self, http_class, req): def do_open(self, http_class, req, **http_conn_args):
"""Return an HTTPResponse object for the request, using http_class. """Return an HTTPResponse object for the request, using http_class.
http_class must implement the HTTPConnection API from http.client. http_class must implement the HTTPConnection API from http.client.
...@@ -1062,7 +1078,8 @@ class AbstractHTTPHandler(BaseHandler): ...@@ -1062,7 +1078,8 @@ class AbstractHTTPHandler(BaseHandler):
if not host: if not host:
raise URLError('no host given') 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)
headers = dict(req.unredirected_hdrs) headers = dict(req.unredirected_hdrs)
headers.update(dict((k, v) for k, v in req.headers.items() headers.update(dict((k, v) for k, v in req.headers.items()
...@@ -1114,10 +1131,18 @@ class HTTPHandler(AbstractHTTPHandler): ...@@ -1114,10 +1131,18 @@ class HTTPHandler(AbstractHTTPHandler):
http_request = AbstractHTTPHandler.do_request_ http_request = AbstractHTTPHandler.do_request_
if hasattr(http.client, 'HTTPSConnection'): if hasattr(http.client, 'HTTPSConnection'):
import ssl
class HTTPSHandler(AbstractHTTPHandler): 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): def https_open(self, req):
return self.do_open(http.client.HTTPSConnection, req) return self.do_open(http.client.HTTPSConnection, req,
context=self._context, check_hostname=self._check_hostname)
https_request = AbstractHTTPHandler.do_request_ https_request = AbstractHTTPHandler.do_request_
......
...@@ -13,6 +13,10 @@ Core and Builtins ...@@ -13,6 +13,10 @@ Core and Builtins
Library Library
------- -------
- Issue #9003: http.client.HTTPSConnection, urllib.request.HTTPSHandler and
urllib.request.urlopen now take optional arguments to allow for
server certificate checking, as recommended in public uses of HTTPS.
- Issue #6612: Fix site and sysconfig to catch os.getcwd() error, eg. if the - Issue #6612: Fix site and sysconfig to catch os.getcwd() error, eg. if the
current directory was deleted. Patch written by W. Trevor King. current directory was deleted. Patch written by W. Trevor King.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment