Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 803e6d6

Browse files
committed
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.
1 parent bd4dacb commit 803e6d6

11 files changed

Lines changed: 419 additions & 161 deletions

File tree

Doc/library/http.client.rst

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,31 @@ The module provides the following classes:
5050
*source_address* was added.
5151

5252

53-
.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]])
53+
.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]], *, context=None, check_hostname=None)
5454

5555
A subclass of :class:`HTTPConnection` that uses SSL for communication with
56-
secure servers. Default port is ``443``. *key_file* is the name of a PEM
57-
formatted file that contains your private key, and *cert_file* is a PEM
58-
formatted certificate chain file; both can be used for authenticating
59-
yourself against the server.
60-
61-
.. warning::
62-
This does not do any verification of the server's certificate.
56+
secure servers. Default port is ``443``. If *context* is specified, it
57+
must be a :class:`ssl.SSLContext` instance describing the various SSL
58+
options. If *context* is specified and has a :attr:`~ssl.SSLContext.verify_mode`
59+
of either :data:`~ssl.CERT_OPTIONAL` or :data:`~ssl.CERT_REQUIRED`, then
60+
by default *host* is matched against the host name(s) allowed by the
61+
server's certificate. If you want to change that behaviour, you can
62+
explicitly set *check_hostname* to False.
63+
64+
*key_file* and *cert_file* are deprecated, please use
65+
:meth:`ssl.SSLContext.load_cert_chain` instead.
66+
67+
If you access arbitrary hosts on the Internet, it is recommended to
68+
require certificate checking and feed the *context* with a set of
69+
trusted CA certificates::
70+
71+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
72+
context.verify_mode = ssl.CERT_REQUIRED
73+
context.load_verify_locations('/etc/pki/tls/certs/ca-bundle.crt')
74+
h = client.HTTPSConnection('svn.python.org', 443, context=context)
6375

6476
.. versionchanged:: 3.2
65-
*source_address* was added.
77+
*source_address*, *context* and *check_hostname* were added.
6678

6779

6880
.. class:: HTTPResponse(sock, debuglevel=0, strict=0, method=None, url=None)

Doc/library/urllib.request.rst

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,11 @@ authentication, redirections, cookies and more.
1515
The :mod:`urllib.request` module defines the following functions:
1616

1717

18-
.. function:: urlopen(url, data=None[, timeout])
18+
.. function:: urlopen(url, data=None[, timeout], *, cafile=None, capath=None)
1919

2020
Open the URL *url*, which can be either a string or a
2121
:class:`Request` object.
2222

23-
.. warning::
24-
HTTPS requests do not do any verification of the server's certificate.
25-
2623
*data* may be a string specifying additional data to send to the
2724
server, or ``None`` if no such data is needed. Currently HTTP
2825
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:
3835
the global default timeout setting will be used). This actually
3936
only works for HTTP, HTTPS and FTP connections.
4037

38+
The optional *cafile* and *capath* parameters specify a set of trusted
39+
CA certificates for HTTPS requests. *cafile* should point to a single
40+
file containing a bundle of CA certificates, whereas *capath* should
41+
point to a directory of hashed certificate files. More information can
42+
be found in :meth:`ssl.SSLContext.load_verify_locations`.
43+
44+
.. warning::
45+
If neither *cafile* nor *capath* is specified, an HTTPS request
46+
will not do any verification of the server's certificate.
47+
4148
This function returns a file-like object with two additional methods from
4249
the :mod:`urllib.response` module
4350

@@ -62,6 +69,9 @@ The :mod:`urllib.request` module defines the following functions:
6269
Proxy handling, which was done by passing a dictionary parameter to
6370
``urllib.urlopen``, can be obtained by using :class:`ProxyHandler` objects.
6471

72+
.. versionchanged:: 3.2
73+
*cafile* and *capath* were added.
74+
6575
.. function:: install_opener(opener)
6676

6777
Install an :class:`OpenerDirector` instance as the default global opener.
@@ -421,9 +431,13 @@ The following classes are provided:
421431
A class to handle opening of HTTP URLs.
422432

423433

424-
.. class:: HTTPSHandler()
434+
.. class:: HTTPSHandler(debuglevel=0, context=None, check_hostname=None)
435+
436+
A class to handle opening of HTTPS URLs. *context* and *check_hostname*
437+
have the same meaning as in :class:`http.client.HTTPSConnection`.
425438

426-
A class to handle opening of HTTPS URLs.
439+
.. versionchanged:: 3.2
440+
*context* and *check_hostname* were added.
427441

428442

429443
.. class:: FileHandler()

Lib/http/client.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,13 +1047,29 @@ class HTTPSConnection(HTTPConnection):
10471047

10481048
default_port = HTTPS_PORT
10491049

1050+
# XXX Should key_file and cert_file be deprecated in favour of context?
1051+
10501052
def __init__(self, host, port=None, key_file=None, cert_file=None,
10511053
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
1052-
source_address=None):
1054+
source_address=None, *, context=None, check_hostname=None):
10531055
super(HTTPSConnection, self).__init__(host, port, strict, timeout,
10541056
source_address)
10551057
self.key_file = key_file
10561058
self.cert_file = cert_file
1059+
if context is None:
1060+
# Some reasonable defaults
1061+
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1062+
context.options |= ssl.OP_NO_SSLv2
1063+
will_verify = context.verify_mode != ssl.CERT_NONE
1064+
if check_hostname is None:
1065+
check_hostname = will_verify
1066+
elif check_hostname and not will_verify:
1067+
raise ValueError("check_hostname needs a SSL context with "
1068+
"either CERT_OPTIONAL or CERT_REQUIRED")
1069+
if key_file or cert_file:
1070+
context.load_cert_chain(certfile, keyfile)
1071+
self._context = context
1072+
self._check_hostname = check_hostname
10571073

10581074
def connect(self):
10591075
"Connect to a host on a given (SSL) port."
@@ -1065,7 +1081,14 @@ def connect(self):
10651081
self.sock = sock
10661082
self._tunnel()
10671083

1068-
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)
1084+
self.sock = self._context.wrap_socket(sock)
1085+
try:
1086+
if self._check_hostname:
1087+
ssl.match_hostname(self.sock.getpeercert(), self.host)
1088+
except Exception:
1089+
self.sock.shutdown(socket.SHUT_RDWR)
1090+
self.sock.close()
1091+
raise
10691092

10701093
__all__.append("HTTPSConnection")
10711094

Lib/test/keycert2.pem

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJnsJZVrppL+W5I9
3+
zGQrrawWwE5QJpBK9nWw17mXrZ03R1cD9BamLGivVISbPlRlAVnZBEyh1ATpsB7d
4+
CUQ+WHEvALquvx4+Yw5l+fXeiYRjrLRBYZuVy8yNtXzU3iWcGObcYRkUdiXdOyP7
5+
sLF2YZHRvQZpzgDBKkrraeQ81w21AgMBAAECgYBEm7n07FMHWlE+0kT0sXNsLYfy
6+
YE+QKZnJw9WkaDN+zFEEPELkhZVt5BjsMraJr6v2fIEqF0gGGJPkbenffVq2B5dC
7+
lWUOxvJHufMK4sM3Cp6s/gOp3LP+QkzVnvJSfAyZU6l+4PGX5pLdUsXYjPxgzjzL
8+
S36tF7/2Uv1WePyLUQJBAMsPhYzUXOPRgmbhcJiqi9A9c3GO8kvSDYTCKt3VMnqz
9+
HBn6MQ4VQasCD1F+7jWTI0FU/3vdw8non/Fj8hhYqZcCQQDCDRdvmZqDiZnpMqDq
10+
L6ZSrLTVtMvZXZbgwForaAD9uHj51TME7+eYT7EG2YCgJTXJ4YvRJEnPNyskwdKt
11+
vTSTAkEAtaaN/vyemEJ82BIGStwONNw0ILsSr5cZ9tBHzqiA/tipY+e36HRFiXhP
12+
QcU9zXlxyWkDH8iz9DSAmE2jbfoqwwJANlMJ65E543cjIlitGcKLMnvtCCLcKpb7
13+
xSG0XJB6Lo11OKPJ66jp0gcFTSCY1Lx2CXVd+gfJrfwI1Pp562+bhwJBAJ9IfDPU
14+
R8OpO9v1SGd8x33Owm7uXOpB9d63/T70AD1QOXjKUC4eXYbt0WWfWuny/RNPRuyh
15+
w7DXSfUF+kPKolU=
16+
-----END PRIVATE KEY-----
17+
-----BEGIN CERTIFICATE-----
18+
MIICXTCCAcagAwIBAgIJAIO3upAG445fMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV
19+
BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u
20+
IFNvZnR3YXJlIEZvdW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTAeFw0x
21+
MDEwMDkxNTAxMDBaFw0yMDEwMDYxNTAxMDBaMGIxCzAJBgNVBAYTAlhZMRcwFQYD
22+
VQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZv
23+
dW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTCBnzANBgkqhkiG9w0BAQEF
24+
AAOBjQAwgYkCgYEAmewllWumkv5bkj3MZCutrBbATlAmkEr2dbDXuZetnTdHVwP0
25+
FqYsaK9UhJs+VGUBWdkETKHUBOmwHt0JRD5YcS8Auq6/Hj5jDmX59d6JhGOstEFh
26+
m5XLzI21fNTeJZwY5txhGRR2Jd07I/uwsXZhkdG9BmnOAMEqSutp5DzXDbUCAwEA
27+
AaMbMBkwFwYDVR0RBBAwDoIMZmFrZWhvc3RuYW1lMA0GCSqGSIb3DQEBBQUAA4GB
28+
AH+iMClLLGSaKWgwXsmdVo4FhTZZHo8Uprrtg3N9FxEeE50btpDVQysgRt5ias3K
29+
m+bME9zbKwvbVWD5zZdjus4pDgzwF/iHyccL8JyYhxOvS/9zmvAtFXj/APIIbZFp
30+
IT75d9f88ScIGEtknZQejnrdhB64tYki/EqluiuKBqKD
31+
-----END CERTIFICATE-----

Lib/test/make_ssl_certs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@ def make_cert_key(hostname):
5757
with open('keycert.pem', 'w') as f:
5858
f.write(key)
5959
f.write(cert)
60+
# For certificate matching tests
61+
cert, key = make_cert_key('fakehostname')
62+
with open('keycert2.pem', 'w') as f:
63+
f.write(key)
64+
f.write(cert)

Lib/test/ssl_servers.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import os
2+
import sys
3+
import ssl
4+
import threading
5+
import urllib.parse
6+
# Rename HTTPServer to _HTTPServer so as to avoid confusion with HTTPSServer.
7+
from http.server import HTTPServer as _HTTPServer, SimpleHTTPRequestHandler
8+
9+
from test import support
10+
11+
here = os.path.dirname(__file__)
12+
13+
HOST = support.HOST
14+
CERTFILE = os.path.join(here, 'keycert.pem')
15+
16+
# This one's based on HTTPServer, which is based on SocketServer
17+
18+
class HTTPSServer(_HTTPServer):
19+
20+
def __init__(self, server_address, handler_class, context):
21+
_HTTPServer.__init__(self, server_address, handler_class)
22+
self.context = context
23+
24+
def __str__(self):
25+
return ('<%s %s:%s>' %
26+
(self.__class__.__name__,
27+
self.server_name,
28+
self.server_port))
29+
30+
def get_request(self):
31+
# override this to wrap socket with SSL
32+
sock, addr = self.socket.accept()
33+
sslconn = self.context.wrap_socket(sock, server_side=True)
34+
return sslconn, addr
35+
36+
class RootedHTTPRequestHandler(SimpleHTTPRequestHandler):
37+
# need to override translate_path to get a known root,
38+
# instead of using os.curdir, since the test could be
39+
# run from anywhere
40+
41+
server_version = "TestHTTPS/1.0"
42+
root = here
43+
# Avoid hanging when a request gets interrupted by the client
44+
timeout = 5
45+
46+
def translate_path(self, path):
47+
"""Translate a /-separated PATH to the local filename syntax.
48+
49+
Components that mean special things to the local file system
50+
(e.g. drive or directory names) are ignored. (XXX They should
51+
probably be diagnosed.)
52+
53+
"""
54+
# abandon query parameters
55+
path = urllib.parse.urlparse(path)[2]
56+
path = os.path.normpath(urllib.parse.unquote(path))
57+
words = path.split('/')
58+
words = filter(None, words)
59+
path = self.root
60+
for word in words:
61+
drive, word = os.path.splitdrive(word)
62+
head, word = os.path.split(word)
63+
path = os.path.join(path, word)
64+
return path
65+
66+
def log_message(self, format, *args):
67+
# we override this to suppress logging unless "verbose"
68+
if support.verbose:
69+
sys.stdout.write(" server (%s:%d %s):\n [%s] %s\n" %
70+
(self.server.server_address,
71+
self.server.server_port,
72+
self.request.cipher(),
73+
self.log_date_time_string(),
74+
format%args))
75+
76+
class HTTPSServerThread(threading.Thread):
77+
78+
def __init__(self, context, host=HOST, handler_class=None):
79+
self.flag = None
80+
self.server = HTTPSServer((host, 0),
81+
handler_class or RootedHTTPRequestHandler,
82+
context)
83+
self.port = self.server.server_port
84+
threading.Thread.__init__(self)
85+
self.daemon = True
86+
87+
def __str__(self):
88+
return "<%s %s>" % (self.__class__.__name__, self.server)
89+
90+
def start(self, flag=None):
91+
self.flag = flag
92+
threading.Thread.start(self)
93+
94+
def run(self):
95+
if self.flag:
96+
self.flag.set()
97+
self.server.serve_forever(0.05)
98+
99+
def stop(self):
100+
self.server.shutdown()
101+
102+
103+
def make_https_server(case, certfile=CERTFILE, host=HOST, handler_class=None):
104+
# we assume the certfile contains both private key and certificate
105+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
106+
context.load_cert_chain(certfile)
107+
server = HTTPSServerThread(context, host, handler_class)
108+
flag = threading.Event()
109+
server.start(flag)
110+
flag.wait()
111+
def cleanup():
112+
if support.verbose:
113+
sys.stdout.write('stopping HTTPS server\n')
114+
server.stop()
115+
if support.verbose:
116+
sys.stdout.write('joining HTTPS thread\n')
117+
server.join()
118+
case.addCleanup(cleanup)
119+
return server

0 commit comments

Comments
 (0)