Repository: certbot/certbot

Issue:
Standalone server waits forever to drain clients during shutdown
Originally discovered in [a help thread on the forums](https://community.letsencrypt.org/t/certbot-hangs-on-stopping-server/102450?u=_az).

## My operating system is (include version):
Ubuntu 19.04

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
certbot-auto

## I ran this command and it produced this output:
    sudo certbot-auto certonly -d certbot.eff.org --standalone --dry-run

and simultaneously in a second terminal:

    nc -vvv localhost 80

Causes an infinite hang until the `nc` client disconnects:

    2019-09-20 19:29:28,932:DEBUG:certbot.error_handler:Calling registered functions
    2019-09-20 19:29:28,932:INFO:certbot.auth_handler:Cleaning up challenges
    2019-09-20 19:29:28,932:DEBUG:certbot.plugins.standalone:Stopping server at :::80...

## Certbot's behavior differed from what I expected because:
The standalone servers should not wait forever for clients to drain. I don't think they should wait at all and instead should just close the server socket. There is no reason to expect further legitimate traffic to the server. 

If a very patient internet crawler happens to hit the port, we need to dispatch of it quickly.

It may be appropriate to also add read and write timeouts to the socket, since it is practically always going to be exposed to the internet.


Discussion:
Thanks for opening the issue. I labeled it as both; bug and a feature request, as it falls between the two.

Resolving patch:
diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py
--- a/acme/acme/standalone.py
+++ b/acme/acme/standalone.py
@@ -176,10 +176,10 @@ def __init__(self, *args, **kwargs):
 class HTTP01Server(HTTPServer, ACMEServerMixin):
     """HTTP01 Server."""
 
-    def __init__(self, server_address, resources, ipv6=False):
+    def __init__(self, server_address, resources, ipv6=False, timeout=30):
         HTTPServer.__init__(
             self, server_address, HTTP01RequestHandler.partial_init(
-                simple_http_resources=resources), ipv6=ipv6)
+                simple_http_resources=resources, timeout=timeout), ipv6=ipv6)
 
 
 class HTTP01DualNetworkedServers(BaseDualNetworkedServers):
@@ -204,6 +204,7 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
     def __init__(self, *args, **kwargs):
         self.simple_http_resources = kwargs.pop("simple_http_resources", set())
+        self.timeout = kwargs.pop('timeout', 30)
         BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
 
     def log_message(self, format, *args):  # pylint: disable=redefined-builtin
@@ -253,7 +254,7 @@ def handle_simple_http_resource(self):
                          self.path)
 
     @classmethod
-    def partial_init(cls, simple_http_resources):
+    def partial_init(cls, simple_http_resources, timeout):
         """Partially initialize this handler.
 
         This is useful because `socketserver.BaseServer` takes
@@ -262,7 +263,8 @@ def partial_init(cls, simple_http_resources):
 
         """
         return functools.partial(
-            cls, simple_http_resources=simple_http_resources)
+            cls, simple_http_resources=simple_http_resources,
+            timeout=timeout)
 
 
 class _BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):


Test patch:
diff --git a/acme/tests/standalone_test.py b/acme/tests/standalone_test.py
--- a/acme/tests/standalone_test.py
+++ b/acme/tests/standalone_test.py
@@ -85,6 +85,28 @@ def test_http01_found(self):
     def test_http01_not_found(self):
         self.assertFalse(self._test_http01(add=False))
 
+    def test_timely_shutdown(self):
+        from acme.standalone import HTTP01Server
+        server = HTTP01Server(('', 0), resources=set(), timeout=0.05)
+        server_thread = threading.Thread(target=server.serve_forever)
+        server_thread.start()
+
+        client = socket.socket()
+        client.connect(('localhost', server.socket.getsockname()[1]))
+
+        stop_thread = threading.Thread(target=server.shutdown)
+        stop_thread.start()
+        server_thread.join(5.)
+
+        is_hung = server_thread.is_alive()
+        try:
+            client.shutdown(socket.SHUT_RDWR)
+        except: # pragma: no cover, pylint: disable=bare-except
+            # may raise error because socket could already be closed
+            pass
+
+        self.assertFalse(is_hung, msg='Server shutdown should not be hung')
+
 
 @unittest.skipIf(not challenges.TLSALPN01.is_supported(), "pyOpenSSL too old")
 class TLSALPN01ServerTest(unittest.TestCase):