134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
hyper/tls
|
||
~~~~~~~~~
|
||
|
||
Contains the TLS/SSL logic for use in hyper.
|
||
"""
|
||
import os.path as path
|
||
from .common.exceptions import MissingCertFile
|
||
from .compat import ignore_missing, ssl
|
||
|
||
|
||
NPN_PROTOCOL = 'h2'
|
||
H2_NPN_PROTOCOLS = [NPN_PROTOCOL, 'h2-16', 'h2-15', 'h2-14']
|
||
SUPPORTED_NPN_PROTOCOLS = H2_NPN_PROTOCOLS + ['http/1.1']
|
||
|
||
H2C_PROTOCOL = 'h2c'
|
||
|
||
# We have a singleton SSLContext object. There's no reason to be creating one
|
||
# per connection.
|
||
_context = None
|
||
|
||
# Work out where our certificates are.
|
||
cert_loc = path.join(path.dirname(__file__), 'certs.pem')
|
||
|
||
|
||
def wrap_socket(sock, server_hostname, ssl_context=None, force_proto=None):
|
||
"""
|
||
A vastly simplified SSL wrapping function. We'll probably extend this to
|
||
do more things later.
|
||
"""
|
||
|
||
global _context
|
||
|
||
if ssl_context:
|
||
# if an SSLContext is provided then use it instead of default context
|
||
_ssl_context = ssl_context
|
||
else:
|
||
# create the singleton SSLContext we use
|
||
if _context is None: # pragma: no cover
|
||
_context = init_context()
|
||
_ssl_context = _context
|
||
|
||
# the spec requires SNI support
|
||
ssl_sock = _ssl_context.wrap_socket(sock, server_hostname=server_hostname)
|
||
# Setting SSLContext.check_hostname to True only verifies that the
|
||
# post-handshake servername matches that of the certificate. We also need
|
||
# to check that it matches the requested one.
|
||
if _ssl_context.check_hostname: # pragma: no cover
|
||
try:
|
||
ssl.match_hostname(ssl_sock.getpeercert(), server_hostname)
|
||
except AttributeError:
|
||
ssl.verify_hostname(ssl_sock, server_hostname) # pyopenssl
|
||
|
||
# Allow for the protocol to be forced externally.
|
||
proto = force_proto
|
||
|
||
# ALPN is newer, so we prefer it over NPN. The odds of us getting
|
||
# different answers is pretty low, but let's be sure.
|
||
with ignore_missing():
|
||
if proto is None:
|
||
proto = ssl_sock.selected_alpn_protocol()
|
||
|
||
with ignore_missing():
|
||
if proto is None:
|
||
proto = ssl_sock.selected_npn_protocol()
|
||
|
||
return (ssl_sock, proto)
|
||
|
||
|
||
def init_context(cert_path=None, cert=None, cert_password=None):
|
||
"""
|
||
Create a new ``SSLContext`` that is correctly set up for an HTTP/2
|
||
connection. This SSL context object can be customized and passed as a
|
||
parameter to the :class:`HTTPConnection <hyper.HTTPConnection>` class.
|
||
Provide your own certificate file in case you don’t want to use hyper’s
|
||
default certificate. The path to the certificate can be absolute or
|
||
relative to your working directory.
|
||
|
||
:param cert_path: (optional) The path to the certificate file of
|
||
“certification authority” (CA) certificates
|
||
:param cert: (optional) if string, path to ssl client cert file (.pem).
|
||
If tuple, ('cert', 'key') pair.
|
||
The certfile string must be the path to a single file in PEM format
|
||
containing the certificate as well as any number of CA certificates
|
||
needed to establish the certificate’s authenticity. The keyfile string,
|
||
if present, must point to a file containing the private key in.
|
||
Otherwise the private key will be taken from certfile as well.
|
||
:param cert_password: (optional) The password argument may be a function to
|
||
call to get the password for decrypting the private key. It will only
|
||
be called if the private key is encrypted and a password is necessary.
|
||
It will be called with no arguments, and it should return a string,
|
||
bytes, or bytearray. If the return value is a string it will be
|
||
encoded as UTF-8 before using it to decrypt the key. Alternatively a
|
||
string, bytes, or bytearray value may be supplied directly as the
|
||
password argument. It will be ignored if the private key is not
|
||
encrypted and no password is needed.
|
||
:returns: An ``SSLContext`` correctly set up for HTTP/2.
|
||
"""
|
||
cafile = cert_path or cert_loc
|
||
if not cafile or not path.exists(cafile):
|
||
err_msg = ("No certificate found at " + str(cafile) + ". Either " +
|
||
"ensure the default cert.pem file is included in the " +
|
||
"distribution or provide a custom certificate when " +
|
||
"creating the connection.")
|
||
raise MissingCertFile(err_msg)
|
||
|
||
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||
context.set_default_verify_paths()
|
||
context.load_verify_locations(cafile=cafile)
|
||
context.verify_mode = ssl.CERT_REQUIRED
|
||
context.check_hostname = True
|
||
|
||
with ignore_missing():
|
||
context.set_npn_protocols(SUPPORTED_NPN_PROTOCOLS)
|
||
|
||
with ignore_missing():
|
||
context.set_alpn_protocols(SUPPORTED_NPN_PROTOCOLS)
|
||
|
||
# required by the spec
|
||
context.options |= ssl.OP_NO_COMPRESSION
|
||
|
||
if cert is not None:
|
||
try:
|
||
basestring
|
||
except NameError:
|
||
basestring = (str, bytes)
|
||
if not isinstance(cert, basestring):
|
||
context.load_cert_chain(cert[0], cert[1], cert_password)
|
||
else:
|
||
context.load_cert_chain(cert, password=cert_password)
|
||
|
||
return context
|