Hello community, here is the log from the commit of package python-acme for openSUSE:Factory checked in at 2017-06-12 15:34:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-acme (Old) and /work/SRC/openSUSE:Factory/.python-acme.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-acme" Mon Jun 12 15:34:41 2017 rev:8 rq:502854 version:0.15.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-acme/python-acme.changes 2017-06-02 10:34:24.270815408 +0200 +++ /work/SRC/openSUSE:Factory/.python-acme.new/python-acme.changes 2017-06-12 15:34:46.972464474 +0200 @@ -1,0 +2,6 @@ +Sun Jun 11 08:48:15 UTC 2017 - [email protected] + +- update to 0.15.0 + - No changelog from upstream + +------------------------------------------------------------------- Old: ---- acme-0.14.2.tar.gz acme-0.14.2.tar.gz.asc New: ---- acme-0.15.0.tar.gz acme-0.15.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-acme.spec ++++++ --- /var/tmp/diff_new_pack.sBsTRS/_old 2017-06-12 15:34:48.816204429 +0200 +++ /var/tmp/diff_new_pack.sBsTRS/_new 2017-06-12 15:34:48.816204429 +0200 @@ -18,7 +18,7 @@ %define libname acme Name: python-%{libname} -Version: 0.14.2 +Version: 0.15.0 Release: 0 Summary: Python library for the ACME protocol License: Apache-2.0 ++++++ acme-0.14.2.tar.gz -> acme-0.15.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/PKG-INFO new/acme-0.15.0/PKG-INFO --- old/acme-0.14.2/PKG-INFO 2017-05-25 23:12:54.000000000 +0200 +++ new/acme-0.15.0/PKG-INFO 2017-06-08 18:26:22.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: acme -Version: 0.14.2 +Version: 0.15.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/client.py new/acme-0.15.0/acme/client.py --- old/acme-0.14.2/acme/client.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/client.py 2017-06-08 18:26:04.000000000 +0200 @@ -564,6 +564,9 @@ except ValueError: jobj = None + if response.status_code == 409: + raise errors.ConflictError(response.headers.get('Location')) + if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/client_test.py new/acme-0.15.0/acme/client_test.py --- old/acme-0.14.2/acme/client_test.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/client_test.py 2017-06-08 18:26:04.000000000 +0200 @@ -513,6 +513,12 @@ self.assertEqual( self.response, self.net._check_response(self.response)) + def test_check_response_conflict(self): + self.response.ok = False + self.response.status_code = 409 + # pylint: disable=protected-access + self.assertRaises(errors.ConflictError, self.net._check_response, self.response) + def test_check_response_jobj(self): self.response.json.return_value = {} for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/crypto_util.py new/acme-0.15.0/acme/crypto_util.py --- old/acme-0.14.2/acme/crypto_util.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/crypto_util.py 2017-06-08 18:26:04.000000000 +0200 @@ -107,7 +107,7 @@ def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the @@ -132,9 +132,14 @@ socket_kwargs = {} if sys.version_info < (2, 7) else { 'source_address': source_address} + host_protocol_agnostic = None if host == '::' or host == '0' else host + try: # pylint: disable=star-args - sock = socket.create_connection((host, port), **socket_kwargs) + logger.debug("Attempting to connect to %s:%d%s.", host_protocol_agnostic, port, + " from {0}:{1}".format(source_address[0], source_address[1]) if \ + socket_kwargs else "") + sock = socket.create_connection((host_protocol_agnostic, port), **socket_kwargs) except socket.error as error: raise errors.Error(error) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/errors.py new/acme-0.15.0/acme/errors.py --- old/acme-0.14.2/acme/errors.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/errors.py 2017-06-08 18:26:04.000000000 +0200 @@ -82,3 +82,14 @@ def __repr__(self): return '{0}(exhausted={1!r}, updated={2!r})'.format( self.__class__.__name__, self.exhausted, self.updated) + +class ConflictError(ClientError): + """Error for when the server returns a 409 (Conflict) HTTP status. + + In the version of ACME implemented by Boulder, this is used to find an + account if you only have the private key, but don't know the account URL. + """ + def __init__(self, location): + self.location = location + super(ConflictError, self).__init__() + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/jose/jws.py new/acme-0.15.0/acme/jose/jws.py --- old/acme-0.14.2/acme/jose/jws.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/jose/jws.py 2017-06-08 18:26:04.000000000 +0200 @@ -222,7 +222,8 @@ protected_params = {} for header in protect: - protected_params[header] = header_params.pop(header) + if header in header_params: + protected_params[header] = header_params.pop(header) if protected_params: # pylint: disable=star-args protected = cls.header_cls(**protected_params).json_dumps() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/jws.py new/acme-0.15.0/acme/jws.py --- old/acme-0.14.2/acme/jws.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/jws.py 2017-06-08 18:26:04.000000000 +0200 @@ -49,6 +49,6 @@ # jwk field if kid is not provided. include_jwk = kid is None return super(JWS, cls).sign(payload, key=key, alg=alg, - protect=frozenset(['nonce', 'url', 'kid']), + protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']), nonce=nonce, url=url, kid=kid, include_jwk=include_jwk) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/standalone.py new/acme-0.15.0/acme/standalone.py --- old/acme-0.14.2/acme/standalone.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/standalone.py 2017-06-08 18:26:04.000000000 +0200 @@ -4,7 +4,9 @@ import functools import logging import os +import socket import sys +import threading from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error @@ -26,6 +28,11 @@ """Generic TLS Server.""" def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access @@ -49,12 +56,81 @@ allow_reuse_address = True +class BaseDualNetworkedServers(object): + """Base class for a pair of IPv6 and IPv4 servers that tries to do everything + it's asked for both servers, but where failures in one server don't + affect the other. + + If two servers are instantiated, they will serve on the same port. + """ + + def __init__(self, ServerClass, server_address, *remaining_args, **kwargs): + port = server_address[1] + self.threads = [] + self.servers = [] + + # Must try True first. + # Ubuntu, for example, will fail to bind to IPv4 if we've already bound + # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 + # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the + # same port, which means that server will accept the IPv4 connections. + # If Python is compiled without IPv6, we'll error out but (probably) successfully + # create the IPv4 server. + for ip_version in [True, False]: + try: + kwargs["ipv6"] = ip_version + new_address = (server_address[0],) + (port,) + server_address[2:] + new_args = (new_address,) + remaining_args + server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args + except socket.error: + logger.debug("Failed to bind to %s:%s using %s", new_address[0], + new_address[1], "IPv6" if ip_version else "IPv4") + else: + self.servers.append(server) + # If two servers are set up and port 0 was passed in, ensure we always + # bind to the same port for both servers. + port = server.socket.getsockname()[1] + if len(self.servers) == 0: + raise socket.error("Could not bind to IPv4 or IPv6.") + + def serve_forever(self): + """Wraps socketserver.TCPServer.serve_forever""" + for server in self.servers: + thread = threading.Thread( + # pylint: disable=no-member + target=server.serve_forever) + thread.start() + self.threads.append(thread) + + def getsocknames(self): + """Wraps socketserver.TCPServer.socket.getsockname""" + return [server.socket.getsockname() for server in self.servers] + + def shutdown_and_server_close(self): + """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and + threading.Thread.join""" + for server in self.servers: + server.shutdown() + server.server_close() + for thread in self.threads: + thread.join() + self.threads = [] + + class TLSSNI01Server(TLSServer, ACMEServerMixin): """TLSSNI01 Server.""" - def __init__(self, server_address, certs): + def __init__(self, server_address, certs, ipv6=False): TLSServer.__init__( - self, server_address, BaseRequestHandlerWithLogging, certs=certs) + self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6) + + +class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers): + """TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs) class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): @@ -70,13 +146,33 @@ socketserver.BaseRequestHandler.handle(self) -class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): +class HTTPServer(BaseHTTPServer.HTTPServer): + """Generic HTTP Server.""" + + def __init__(self, *args, **kwargs): + self.ipv6 = kwargs.pop("ipv6", False) + if self.ipv6: + self.address_family = socket.AF_INET6 + else: + self.address_family = socket.AF_INET + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + + +class HTTP01Server(HTTPServer, ACMEServerMixin): """HTTP01 Server.""" - def __init__(self, server_address, resources): - BaseHTTPServer.HTTPServer.__init__( + def __init__(self, server_address, resources, ipv6=False): + HTTPServer.__init__( self, server_address, HTTP01RequestHandler.partial_init( - simple_http_resources=resources)) + simple_http_resources=resources), ipv6=ipv6) + + +class HTTP01DualNetworkedServers(BaseDualNetworkedServers): + """HTTP01Server Wrapper. Tries everything for both. Failures for one don't + affect the other.""" + + def __init__(self, *args, **kwargs): + BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme/standalone_test.py new/acme-0.15.0/acme/standalone_test.py --- old/acme-0.14.2/acme/standalone_test.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/acme/standalone_test.py 2017-06-08 18:26:04.000000000 +0200 @@ -1,6 +1,7 @@ """Tests for acme.standalone.""" import os import shutil +import socket import threading import tempfile import time @@ -9,6 +10,7 @@ from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # type: ignore # pylint: disable=import-error +import mock import requests from acme import challenges @@ -29,6 +31,13 @@ ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() # pylint: disable=no-member + def test_ipv6(self): + if socket.has_ipv6: + from acme.standalone import TLSServer + server = TLSServer( + ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) + server.server_close() # pylint: disable=no-member + class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" @@ -112,6 +121,136 @@ self.assertFalse(self._test_http01(add=False)) +class BaseDualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.BaseDualNetworkedServers.""" + + _multiprocess_can_split_ = True + + class SingleProtocolServer(socketserver.TCPServer): + """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" + def __init__(self, *args, **kwargs): + ipv6 = kwargs.pop("ipv6", False) + if ipv6: + self.address_family = socket.AF_INET6 + kwargs["bind_and_activate"] = False + else: + self.address_family = socket.AF_INET + socketserver.TCPServer.__init__(self, *args, **kwargs) + if ipv6: + # pylint: disable=no-member + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + try: + self.server_bind() + self.server_activate() + except: + self.server_close() + raise + + @mock.patch("socket.socket.bind") + def test_fail_to_bind(self, mock_bind): + mock_bind.side_effect = socket.error + from acme.standalone import BaseDualNetworkedServers + self.assertRaises(socket.error, BaseDualNetworkedServers, + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + + def test_ports_equal(self): + from acme.standalone import BaseDualNetworkedServers + servers = BaseDualNetworkedServers( + BaseDualNetworkedServersTest.SingleProtocolServer, + ("", 0), + socketserver.BaseRequestHandler) + socknames = servers.getsocknames() + prev_port = None + # assert ports are equal + for sockname in socknames: + port = sockname[1] + if prev_port: + self.assertEqual(prev_port, port) + prev_port = port + + +class TLSSNI01DualNetworkedServersTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.certs = {b'localhost': ( + test_util.load_pyopenssl_private_key('rsa2048_key.pem'), + test_util.load_cert('rsa2048_cert.pem'), + )} + from acme.standalone import TLSSNI01DualNetworkedServers + self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs) + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_connect(self): + socknames = self.servers.getsocknames() + # connect to all addresses + for sockname in socknames: + host, port = sockname[:2] + cert = crypto_util.probe_sni( + b'localhost', host=host, port=port, timeout=1) + self.assertEqual(jose.ComparableX509(cert), + jose.ComparableX509(self.certs[b'localhost'][1])) + + +class HTTP01DualNetworkedServersTest(unittest.TestCase): + """Tests for acme.standalone.HTTP01DualNetworkedServers.""" + + _multiprocess_can_split_ = True + + def setUp(self): + self.account_key = jose.JWK.load( + test_util.load_vector('rsa1024_key.pem')) + self.resources = set() + + from acme.standalone import HTTP01DualNetworkedServers + self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) + + # pylint: disable=no-member + self.port = self.servers.getsocknames()[0][1] + self.servers.serve_forever() + + def tearDown(self): + self.servers.shutdown_and_server_close() + + def test_index(self): + response = requests.get( + 'http://localhost:{0}'.format(self.port), verify=False) + self.assertEqual( + response.text, 'ACME client standalone challenge solver') + self.assertTrue(response.ok) + + def test_404(self): + response = requests.get( + 'http://localhost:{0}/foo'.format(self.port), verify=False) + self.assertEqual(response.status_code, http_client.NOT_FOUND) + + def _test_http01(self, add): + chall = challenges.HTTP01(token=(b'x' * 16)) + response, validation = chall.response_and_validation(self.account_key) + + from acme.standalone import HTTP01RequestHandler + resource = HTTP01RequestHandler.HTTP01Resource( + chall=chall, response=response, validation=validation) + if add: + self.resources.add(resource) + return resource.response.simple_verify( + resource.chall, 'localhost', self.account_key.public_key(), + port=self.port) + + def test_http01_found(self): + self.assertTrue(self._test_http01(add=True)) + + def test_http01_not_found(self): + self.assertFalse(self._test_http01(add=False)) + + class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" @@ -137,7 +276,6 @@ ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) - self.thread.start() def tearDown(self): os.chdir(self.old_cwd) @@ -146,13 +284,12 @@ def test_it(self): max_attempts = 5 - while max_attempts: - max_attempts -= 1 + for attempt in range(max_attempts): try: cert = crypto_util.probe_sni( b'localhost', b'0.0.0.0', self.port) except errors.Error: - self.assertTrue(max_attempts > 0, "Timeout!") + self.assertTrue(attempt + 1 < max_attempts, "Timeout!") time.sleep(1) # wait until thread starts else: self.assertEqual(jose.ComparableX509(cert), @@ -160,6 +297,11 @@ 'rsa2048_cert.pem')) break + if attempt == 0: + # the first attempt is always meant to fail, so we can test + # the socket failure code-path for probe_sni, as well + self.thread.start() + if __name__ == "__main__": unittest.main() # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/acme.egg-info/PKG-INFO new/acme-0.15.0/acme.egg-info/PKG-INFO --- old/acme-0.14.2/acme.egg-info/PKG-INFO 2017-05-25 23:12:54.000000000 +0200 +++ new/acme-0.15.0/acme.egg-info/PKG-INFO 2017-06-08 18:26:22.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: acme -Version: 0.14.2 +Version: 0.15.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.14.2/setup.py new/acme-0.15.0/setup.py --- old/acme-0.14.2/setup.py 2017-05-25 23:12:46.000000000 +0200 +++ new/acme-0.15.0/setup.py 2017-06-08 18:26:04.000000000 +0200 @@ -4,7 +4,7 @@ from setuptools import find_packages -version = '0.14.2' +version = '0.15.0' # Please update tox.ini when modifying dependency version requirements install_requires = [
