Use default SSL context when possible

https://docs.python.org/2/library/ssl.html#best-defaults

Deal with older Pythons which didn't do certificate validation, have
sane defaults or even provided a cipher string.
This commit is contained in:
Simon Lundström 2016-10-05 14:18:49 +02:00
parent 44c592df45
commit 99d567492f
2 changed files with 106 additions and 7 deletions

View file

@ -42,8 +42,23 @@ __email__ = 'amr.hassan@gmail.com'
def _deprecation_warning(message): def _deprecation_warning(message):
warnings.warn(message, DeprecationWarning) warnings.warn(message, DeprecationWarning)
def _can_use_ssl_securely():
# Python 3.3 doesn't support create_default_context() but can be made to
# work sanely.
# <2.7.9 and <3.2 never did any SSL verification so don't do SSL there.
# >3.4 and >2.7.9 has sane defaults so use SSL there.
v = sys.version_info
return v > (3, 3) or ((2, 7, 9) < v < (3, 0))
if _can_use_ssl_securely():
import ssl
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
from http.client import HTTPSConnection if _can_use_ssl_securely():
from http.client import HTTPSConnection
else:
from http.client import HTTPConnection
import html.entities as htmlentitydefs import html.entities as htmlentitydefs
from urllib.parse import splithost as url_split_host from urllib.parse import splithost as url_split_host
from urllib.parse import quote_plus as url_quote_plus from urllib.parse import quote_plus as url_quote_plus
@ -51,7 +66,10 @@ if sys.version_info[0] == 3:
unichr = chr unichr = chr
elif sys.version_info[0] == 2: elif sys.version_info[0] == 2:
from httplib import HTTPSConnection if _can_use_ssl_securely():
from httplib import HTTPSConnection
else:
from httplib import HTTPConnection
import htmlentitydefs import htmlentitydefs
from urllib import splithost as url_split_host from urllib import splithost as url_split_host
from urllib import quote_plus as url_quote_plus from urllib import quote_plus as url_quote_plus
@ -131,6 +149,59 @@ RE_XML_ILLEGAL = (u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' +
XML_ILLEGAL = re.compile(RE_XML_ILLEGAL) XML_ILLEGAL = re.compile(RE_XML_ILLEGAL)
# Python <=3.3 doesn't support create_default_context()
# <2.7.9 and <3.2 never did any SSL verification
# FIXME This can be removed after 2017-09 when 3.3 is no longer supported and
# pypy3 uses 3.4 or later, see
# https://en.wikipedia.org/wiki/CPython#Version_history
if sys.version_info[0] == 3 and sys.version_info[1] == 3:
import certifi
SSL_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
SSL_CONTEXT.verify_mode = ssl.CERT_REQUIRED
SSL_CONTEXT.options |= ssl.OP_NO_COMPRESSION
# Intermediate from https://wiki.mozilla.org/Security/Server_Side_TLS
# Create the cipher string
cipher_string = """
ECDHE-ECDSA-CHACHA20-POLY1305
ECDHE-RSA-CHACHA20-POLY1305
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
DHE-RSA-AES128-GCM-SHA256
DHE-RSA-AES256-GCM-SHA384
ECDHE-ECDSA-AES128-SHA256
ECDHE-RSA-AES128-SHA256
ECDHE-ECDSA-AES128-SHA
ECDHE-RSA-AES256-SHA384
ECDHE-RSA-AES128-SHA
ECDHE-ECDSA-AES256-SHA384
ECDHE-ECDSA-AES256-SHA
ECDHE-RSA-AES256-SHA
DHE-RSA-AES128-SHA256
DHE-RSA-AES128-SHA
DHE-RSA-AES256-SHA256
DHE-RSA-AES256-SHA
ECDHE-ECDSA-DES-CBC3-SHA
ECDHE-RSA-DES-CBC3-SHA
EDH-RSA-DES-CBC3-SHA
AES128-GCM-SHA256
AES256-GCM-SHA384
AES128-SHA256
AES256-SHA256
AES128-SHA
AES256-SHA
DES-CBC3-SHA
!DSS
"""
cipher_string = ' '.join(cipher_string.split())
SSL_CONTEXT.set_ciphers(cipher_string)
SSL_CONTEXT.load_verify_locations(certifi.where())
# Python >3.4 and >2.7.9 has sane defaults
elif sys.version_info > (3, 4) or ((2, 7, 9) < sys.version_info < (3, 0)):
SSL_CONTEXT = ssl.create_default_context()
class _Network(object): class _Network(object):
""" """
@ -1098,9 +1169,15 @@ class _Request(object):
(HOST_NAME, HOST_SUBDIR) = self.network.ws_server (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
if self.network.is_proxy_enabled(): if self.network.is_proxy_enabled():
conn = HTTPSConnection( if _can_use_ssl_securely():
host=self.network._get_proxy()[0], conn = HTTPSConnection(
port=self.network._get_proxy()[1]) context=SSL_CONTEXT,
host=self.network._get_proxy()[0],
port=self.network._get_proxy()[1])
else:
conn = HTTPConnection(
host=self.network._get_proxy()[0],
port=self.network._get_proxy()[1])
try: try:
conn.request( conn.request(
@ -1110,7 +1187,15 @@ class _Request(object):
raise NetworkError(self.network, e) raise NetworkError(self.network, e)
else: else:
conn = HTTPSConnection(host=HOST_NAME) if _can_use_ssl_securely():
conn = HTTPSConnection(
context=SSL_CONTEXT,
host=HOST_NAME
)
else:
conn = HTTPConnection(
host=HOST_NAME
)
try: try:
conn.request( conn.request(
@ -4291,7 +4376,15 @@ class _ScrobblerRequest(object):
def execute(self): def execute(self):
"""Returns a string response of this request.""" """Returns a string response of this request."""
connection = HTTPSConnection(self.hostname) if _can_use_ssl_securely():
connection = HTTPSConnection(
context=SSL_CONTEXT,
host=self.hostname
)
else:
connection = HTTPConnection(
host=self.hostname
)
data = [] data = []
for name in self.params.keys(): for name in self.params.keys():

View file

@ -7,6 +7,12 @@ setup(
version="1.6.0", version="1.6.0",
author="Amr Hassan <amr.hassan@gmail.com>", author="Amr Hassan <amr.hassan@gmail.com>",
install_requires=['six'], install_requires=['six'],
# FIXME This can be removed after 2017-09 when 3.3 is no longer supported
# and pypy3 uses 3.4 or later, see
# https://en.wikipedia.org/wiki/CPython#Version_history
extras_require={
':python_version=="3.3"': ["certifi"],
},
tests_require=['mock', 'pytest', 'coverage', 'pep8', 'pyyaml', 'pyflakes'], tests_require=['mock', 'pytest', 'coverage', 'pep8', 'pyyaml', 'pyflakes'],
description=("A Python interface to Last.fm and Libre.fm"), description=("A Python interface to Last.fm and Libre.fm"),
author_email="amr.hassan@gmail.com", author_email="amr.hassan@gmail.com",