From 373109c0d7a6627cfccc78056e5dc9bc4e9a942f Mon Sep 17 00:00:00 2001 From: Alejandro Angulo Date: Sun, 2 Oct 2016 15:25:22 -0700 Subject: [PATCH 1/3] don't want user creds in the repo --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 292d1ca..4535a42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# User Credentials +test_pylast.yaml + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From 44c592df45c7eabb517fe3b7e748b23c0ecf64de Mon Sep 17 00:00:00 2001 From: Alejandro Angulo Date: Mon, 3 Oct 2016 20:45:28 -0700 Subject: [PATCH 2/3] switch from HTTPConnection to HTTPSConnection --- pylast/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pylast/__init__.py b/pylast/__init__.py index 88039dd..0909bd9 100644 --- a/pylast/__init__.py +++ b/pylast/__init__.py @@ -43,7 +43,7 @@ def _deprecation_warning(message): warnings.warn(message, DeprecationWarning) if sys.version_info[0] == 3: - from http.client import HTTPConnection + from http.client import HTTPSConnection 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 +51,7 @@ if sys.version_info[0] == 3: unichr = chr elif sys.version_info[0] == 2: - from httplib import HTTPConnection + from httplib import HTTPSConnection import htmlentitydefs from urllib import splithost as url_split_host from urllib import quote_plus as url_quote_plus @@ -1098,7 +1098,7 @@ class _Request(object): (HOST_NAME, HOST_SUBDIR) = self.network.ws_server if self.network.is_proxy_enabled(): - conn = HTTPConnection( + conn = HTTPSConnection( host=self.network._get_proxy()[0], port=self.network._get_proxy()[1]) @@ -1110,7 +1110,7 @@ class _Request(object): raise NetworkError(self.network, e) else: - conn = HTTPConnection(host=HOST_NAME) + conn = HTTPSConnection(host=HOST_NAME) try: conn.request( @@ -4291,7 +4291,7 @@ class _ScrobblerRequest(object): def execute(self): """Returns a string response of this request.""" - connection = HTTPConnection(self.hostname) + connection = HTTPSConnection(self.hostname) data = [] for name in self.params.keys(): From 99d567492fea17834cd66cfc066e6ebffd041a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lundstr=C3=B6m?= Date: Wed, 5 Oct 2016 14:18:49 +0200 Subject: [PATCH 3/3] 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. --- pylast/__init__.py | 107 ++++++++++++++++++++++++++++++++++++++++++--- setup.py | 6 +++ 2 files changed, 106 insertions(+), 7 deletions(-) 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",