diff --git a/pylast/__init__.py b/pylast/__init__.py index 0909bd9..fb8fd41 100644 --- a/pylast/__init__.py +++ b/pylast/__init__.py @@ -42,8 +42,23 @@ __email__ = 'amr.hassan@gmail.com' def _deprecation_warning(message): 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: - 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 from urllib.parse import splithost as url_split_host from urllib.parse import quote_plus as url_quote_plus @@ -51,7 +66,10 @@ if sys.version_info[0] == 3: unichr = chr 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 from urllib import splithost as url_split_host 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) +# 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): """ @@ -1098,9 +1169,15 @@ class _Request(object): (HOST_NAME, HOST_SUBDIR) = self.network.ws_server if self.network.is_proxy_enabled(): - conn = HTTPSConnection( - host=self.network._get_proxy()[0], - port=self.network._get_proxy()[1]) + if _can_use_ssl_securely(): + conn = HTTPSConnection( + 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: conn.request( @@ -1110,7 +1187,15 @@ class _Request(object): raise NetworkError(self.network, e) 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: conn.request( @@ -4291,7 +4376,15 @@ class _ScrobblerRequest(object): def execute(self): """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 = [] for name in self.params.keys(): diff --git a/setup.py b/setup.py index 1e99522..171fb43 100755 --- a/setup.py +++ b/setup.py @@ -7,6 +7,12 @@ setup( version="1.6.0", author="Amr Hassan ", 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'], description=("A Python interface to Last.fm and Libre.fm"), author_email="amr.hassan@gmail.com",