diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 5829a511f..5f24e9d9e 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -70,20 +70,10 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods class FakeConnection(object): """Fake OpenSSL.SSL.Connection.""" - MAKEFILE_SUPPORT = hasattr(socket, "_fileobject") - """Is `makefile` supported on your platform? - - .. warning:: `makefile`, as currently implemented, is supported - on select platforms only, as it uses CPython's internal API. - You've been warned! - - """ - # pylint: disable=missing-docstring def __init__(self, connection): self._wrapped = connection - self._makefile_refs = 0 def __getattr__(self, name): return getattr(self._wrapped, name) @@ -92,27 +82,6 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods # OpenSSL.SSL.Connection.shutdown doesn't accept any args return self._wrapped.shutdown() - # stuff below ripped off from - # https://hg.python.org/cpython/file/2.7/Lib/ssl.py - - def makefile(self, mode='r', bufsize=-1): - assert self.MAKEFILE_SUPPORT, ( - "You need compatible version for makefile support") - self._makefile_refs += 1 - # SocketServer.StreamRequesthandler.finish will try to - # close the wfile/rfile. close=True causes curl: (56) - # GnuTLS recv error (-110): The TLS connection was - # non-properly terminated. - # TODO: doesn't work in Python3 - # pylint: disable=protected-access - return socket._fileobject(self._wrapped, mode, bufsize, close=False) - - def close(self): - if self._makefile_refs < 1: - self._wrapped.close() - else: - self._makefile_refs -= 1 - def accept(self): # pylint: disable=missing-docstring sock, addr = self.sock.accept() diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index d4d6f0ea4..089c2ff18 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -44,15 +44,7 @@ class TLSServer(socketserver.TCPServer): return socketserver.TCPServer.server_bind(self) -class HTTPSServer(TLSServer, BaseHTTPServer.HTTPServer): - """HTTPS Server.""" - - def server_bind(self): - self._wrap_sock() - BaseHTTPServer.HTTPServer.server_bind(self) - - -class ACMEServerMixin: # pylint: disable=old-style-class,no-init +class ACMEServerMixin: # pylint: disable=old-style-class """ACME server common settings mixin.""" server_version = "ACME standalone client" allow_reuse_address = True @@ -81,27 +73,23 @@ class ACMEServerMixin: # pylint: disable=old-style-class,no-init self.server_close() -class ACMETLSServer(HTTPSServer, ACMEServerMixin): - """ACME TLS Server.""" +class DVSNIServer(TLSServer, ACMEServerMixin): + """DVSNI Server.""" - SIMPLE_HTTP_SUPPORT = crypto_util.SSLSocket.FakeConnection.MAKEFILE_SUPPORT - """Is SimpleHTTP supported on your platform. - - Please see a warning for `acme.crypto_util.SSLSocket.FakeConnection`. - - """ - - def __init__(self, *args, **kwargs): + def __init__(self, server_address, certs): ACMEServerMixin.__init__(self) - HTTPSServer.__init__(self, *args, **kwargs) + TLSServer.__init__( + self, server_address, socketserver.BaseRequestHandler, certs=certs) -class ACMEServer(BaseHTTPServer.HTTPServer, ACMEServerMixin): - """ACME Server (non-TLS).""" +class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin): + """SimpleHTTP Server.""" - def __init__(self, *args, **kwargs): + def __init__(self, server_address, resources): ACMEServerMixin.__init__(self) - BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + BaseHTTPServer.HTTPServer.__init__( + self, server_address, SimpleHTTPRequestHandler.partial_init( + simple_http_resources=resources)) class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): @@ -133,14 +121,14 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() - self.wfile.write(self.server.server_version) + self.wfile.write(self.server.server_version.encode()) def handle_404(self): """Handler 404 Not Found errors.""" self.send_response(http_client.NOT_FOUND, message="Not Found") self.send_header("Content-type", "text/html") self.end_headers() - self.wfile.write("404") + self.wfile.write(b"404") def handle_simple_http_resource(self): """Handle SimpleHTTP provisioned resources.""" @@ -171,24 +159,8 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): cls, simple_http_resources=simple_http_resources) -class ACMERequestHandler(SimpleHTTPRequestHandler): - """ACME request handler.""" - - def handle_one_request(self): - """Handle single request. - - Makes sure that DVSNI probers are ignored. - - """ - try: - return SimpleHTTPRequestHandler.handle_one_request(self) - except OpenSSL.SSL.ZeroReturnError: - logger.debug("Client prematurely closed connection (prober?). " - "Ignoring request.") - - -def simple_server(cli_args, forever=True): - """Run simple standalone client server.""" +def simple_dvsni_server(cli_args, forever=True): + """Run simple standalone DVSNI server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() @@ -198,7 +170,6 @@ def simple_server(cli_args, forever=True): args = parser.parse_args(cli_args[1:]) certs = {} - resources = {} _, hosts, _ = next(os.walk('.')) for host in hosts: @@ -206,15 +177,13 @@ def simple_server(cli_args, forever=True): cert_contents = cert_file.read() with open(os.path.join(host, "key.pem")) as key_file: key_contents = key_file.read() - certs[host] = ( + certs[host.encode()] = ( OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, key_contents), OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) - handler = ACMERequestHandler.partial_init( - simple_http_resources=resources) - server = ACMETLSServer(('', int(args.port)), handler, certs=certs) + server = DVSNIServer(('', int(args.port)), certs=certs) six.print_("Serving at https://localhost:{0}...".format( server.socket.getsockname()[1])) if forever: # pragma: no cover @@ -224,4 +193,4 @@ def simple_server(cli_args, forever=True): if __name__ == "__main__": - sys.exit(simple_server(sys.argv)) # pragma: no cover + sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index a6f7502b6..9eb192c74 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -13,6 +13,7 @@ import requests from acme import challenges from acme import crypto_util +from acme import errors from acme import jose from acme import test_util @@ -30,25 +31,27 @@ class TLSServerTest(unittest.TestCase): class ACMEServerMixinTest(unittest.TestCase): """Tests for acme.standalone.ACMEServerMixin.""" + def setUp(self): + from acme.standalone import ACMEServerMixin + + class _MockServer(socketserver.TCPServer, ACMEServerMixin): + def __init__(self, *args, **kwargs): + socketserver.TCPServer.__init__(self, *args, **kwargs) + ACMEServerMixin.__init__(self) + self.server = _MockServer(("", 0), socketserver.BaseRequestHandler) + + def test_serve_shutdown(self): + thread = threading.Thread(target=self.server.serve_forever2) + thread.start() + self.server.shutdown2() + def test_shutdown2_not_running(self): - from acme.standalone import ACMEServer - server = ACMEServer(("", 0), socketserver.BaseRequestHandler) - server.shutdown2() - server.shutdown2() + self.server.shutdown2() + self.server.shutdown2() -class ACMEServerTest(unittest.TestCase): - """Test for acme.standalone.ACMEServer.""" - - def test_init(self): - from acme.standalone import ACMEServer - server = ACMEServer(("", 0), socketserver.BaseRequestHandler) - # pylint: disable=protected-access - self.assertFalse(server._stopped) - - -class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase): - """End-to-end test for ACME TLS server with SimpleHTTP.""" +class DVSNIServerTest(unittest.TestCase): + """Test for acme.standalone.DVSNIServer.""" def setUp(self): self.certs = { @@ -56,36 +59,19 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase): # pylint: disable=protected-access test_util.load_cert('cert.pem')._wrapped), } - self.account_key = jose.JWK.load( - test_util.load_vector('rsa1024_key.pem')) - - from acme.standalone import ACMETLSServer - from acme.standalone import ACMERequestHandler - self.resources = set() - handler = ACMERequestHandler.partial_init( - simple_http_resources=self.resources) - self.server = ACMETLSServer(('', 0), handler, certs=self.certs) - self.server_thread = threading.Thread( - # pylint: disable=no-member - target=self.server.serve_forever2) - self.server_thread.start() - - self.port = self.server.socket.getsockname()[1] + from acme.standalone import DVSNIServer + self.server = DVSNIServer(("", 0), certs=self.certs) + # pylint: disable=no-member + self.thread = threading.Thread(target=self.server.handle_request) + self.thread.start() def tearDown(self): self.server.shutdown2() - self.server_thread.join() + self.thread.join() - def test_index(self): - response = requests.get( - 'https://localhost:{0}'.format(self.port), verify=False) - self.assertEqual(response.text, 'ACME standalone client') - self.assertTrue(response.ok) - - def test_404(self): - response = requests.get( - 'https://localhost:{0}/foo'.format(self.port), verify=False) - self.assertEqual(response.status_code, http_client.NOT_FOUND) + def test_init(self): + # pylint: disable=protected-access + self.assertFalse(self.server._stopped) def test_dvsni(self): cert = crypto_util.probe_sni( @@ -93,9 +79,41 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase): self.assertEqual(jose.ComparableX509(cert), jose.ComparableX509(self.certs[b'localhost'][1])) + +class SimpleHTTPServerTest(unittest.TestCase): + """Tests for acme.standalone.SimpleHTTPServer.""" + + def setUp(self): + self.account_key = jose.JWK.load( + test_util.load_vector('rsa1024_key.pem')) + self.resources = set() + + from acme.standalone import SimpleHTTPServer + self.server = SimpleHTTPServer(('', 0), resources=self.resources) + + # pylint: disable=no-member + self.port = self.server.socket.getsockname()[1] + self.thread = threading.Thread(target=self.server.handle_request) + self.thread.start() + + def tearDown(self): + self.server.shutdown2() + self.thread.join() + + def test_index(self): + response = requests.get( + 'http://localhost:{0}'.format(self.port), verify=False) + self.assertEqual(response.text, 'ACME standalone client') + 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_simple_http(self, add): chall = challenges.SimpleHTTP(token=(b'x' * 16)) - response = challenges.SimpleHTTPResponse(tls=True) + response = challenges.SimpleHTTPResponse(tls=False) from acme.standalone import SimpleHTTPRequestHandler resource = SimpleHTTPRequestHandler.SimpleHTTPResource( @@ -114,8 +132,8 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase): self.assertFalse(self._test_simple_http(add=False)) -class TestSimpleServer(unittest.TestCase): - """Tests for acme.standalone.simple_server.""" +class TestSimpleDVSNIServer(unittest.TestCase): + """Tests for acme.standalone.simple_dvsni_server.""" def setUp(self): # mirror ../examples/standalone @@ -126,9 +144,10 @@ class TestSimpleServer(unittest.TestCase): shutil.copy(test_util.vector_path('rsa512_key.pem'), os.path.join(localhost_dir, 'key.pem')) - from acme.standalone import simple_server - self.thread = threading.Thread(target=simple_server, kwargs={ - 'cli_args': ('xxx', '--port', '1234'), + from acme.standalone import simple_dvsni_server + self.port = 1234 + self.thread = threading.Thread(target=simple_dvsni_server, kwargs={ + 'cli_args': ('xxx', '--port', str(self.port)), 'forever': False, }) self.old_cwd = os.getcwd() @@ -145,12 +164,13 @@ class TestSimpleServer(unittest.TestCase): while max_attempts: max_attempts -= 1 try: - response = requests.get('https://localhost:1234', verify=False) - except requests.ConnectionError: + cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', self.port) + except errors.Error: self.assertTrue(max_attempts > 0, "Timeout!") time.sleep(1) # wait until thread starts else: - self.assertEqual(response.text, 'ACME standalone client') + self.assertEqual(jose.ComparableX509(cert), + test_util.load_cert('cert.pem')) break diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 97964e8cc..cb95ec408 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -1,7 +1,6 @@ """Standalone Authenticator.""" import argparse import collections -import functools import logging import random import socket @@ -61,25 +60,19 @@ class ServerManager(object): if port in self._instances: return self._instances[port].server - logger.debug("Starting new server at %s (tls=%s)", port, tls) - handler = acme_standalone.ACMERequestHandler.partial_init( - self.simple_http_resources) - - if tls: - cls = functools.partial( - acme_standalone.ACMETLSServer, certs=self.certs) - else: - cls = acme_standalone.ACMEServer - + address = ("", port) try: - server = cls(("", port), handler) + if tls: + server = acme_standalone.DVSNIServer(address, self.certs) + else: + server = acme_standalone.SimpleHTTPServer( + address, self.simple_http_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) # if port == 0, then random free port on OS is taken # pylint: disable=no-member host, real_port = server.socket.getsockname() - thread = threading.Thread(target=server.serve_forever2) logger.debug("Starting server at %s:%d", host, real_port) thread.start()