From 54b47c4a3eae6385a04e230c2df0e631c2c1670a Mon Sep 17 00:00:00 2001 From: Unrud Date: Fri, 10 Jun 2016 14:30:58 +0200 Subject: [PATCH 1/4] Refactor: Move response code into function --- radicale/__init__.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/radicale/__init__.py b/radicale/__init__.py index 51bb34c..8d36ee7 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -218,6 +218,15 @@ class Application: def __call__(self, environ, start_response): """Manage a request.""" + def response(status, headers={}, answer=None): + # Start response + status = "%i %s" % (status, + client.responses.get(status, "Unknown")) + self.logger.debug("Answer status: %s" % status) + start_response(status, list(headers.items())) + # Return response content + return [answer] if answer else [] + self.logger.info("%s request at %s received" % ( environ["REQUEST_METHOD"], environ["PATH_INFO"])) headers = pprint.pformat(self.headers_log(environ)) @@ -234,9 +243,7 @@ class Application: # Request path not starting with base_prefix, not allowed self.logger.debug( "Path not starting with prefix: %s", environ["PATH_INFO"]) - status, headers, _ = NOT_ALLOWED - start_response(status, list(headers.items())) - return [] + return response(*NOT_ALLOWED) # Sanitize request URI environ["PATH_INFO"] = storage.sanitize_path( @@ -275,10 +282,7 @@ class Application: status = client.SEE_OTHER self.logger.info("/.well-known/ redirection to: %s" % redirect) headers = {"Location": redirect} - status = "%i %s" % ( - status, client.responses.get(status, "Unknown")) - start_response(status, list(headers.items())) - return [] + return response(status, headers) is_authenticated = self.is_authenticated(user, password) is_valid_user = is_authenticated or not user @@ -342,13 +346,7 @@ class Application: for key in self.configuration.options("headers"): headers[key] = self.configuration.get("headers", key) - # Start response - status = "%i %s" % (status, client.responses.get(status, "Unknown")) - self.logger.debug("Answer status: %s" % status) - start_response(status, list(headers.items())) - - # Return response content - return [answer] if answer else [] + return response(status, headers, answer) # All these functions must have the same parameters, some are useless # pylint: disable=W0612,W0613,R0201 From b55d2181eda8e5ae3bf562d6bd7889035a25aa41 Mon Sep 17 00:00:00 2001 From: Unrud Date: Fri, 10 Jun 2016 14:33:25 +0200 Subject: [PATCH 2/4] Add timeout to integrated sever --- config | 3 +++ radicale/__init__.py | 18 ++++++++++++++++-- radicale/__main__.py | 1 + radicale/config.py | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/config b/config index f3a3a46..ee28711 100644 --- a/config +++ b/config @@ -24,6 +24,9 @@ # File storing the PID in daemon mode #pid = +# Socket timeout (seconds) +#timeout = 10 + # SSL flag, enable HTTPS protocol #ssl = False diff --git a/radicale/__init__.py b/radicale/__init__.py index 8d36ee7..ba1994d 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -54,6 +54,10 @@ WELL_KNOWN_RE = re.compile(r"/\.well-known/(carddav|caldav)/?$") class HTTPServer(wsgiref.simple_server.WSGIServer): """HTTP server.""" + + # These class attributes must be set before creating instance + client_timeout = None + def __init__(self, address, handler, bind_and_activate=True): """Create server.""" ipv6 = ":" in address[0] @@ -72,6 +76,13 @@ class HTTPServer(wsgiref.simple_server.WSGIServer): self.server_bind() self.server_activate() + def get_request(self): + # Set timeout for client + _socket, address = super().get_request() + if self.client_timeout: + _socket.settimeout(self.client_timeout) + return _socket, address + class HTTPSServer(HTTPServer): """HTTPS server.""" @@ -290,8 +301,11 @@ class Application: # Get content content_length = int(environ.get("CONTENT_LENGTH") or 0) if content_length: - content = self.decode( - environ["wsgi.input"].read(content_length), environ) + try: + content = self.decode( + environ["wsgi.input"].read(content_length), environ) + except socket.timeout: + return response(client.REQUEST_TIMEOUT) self.logger.debug("Request content:\n%s" % content) else: content = None diff --git a/radicale/__main__.py b/radicale/__main__.py index cda628b..dc17905 100644 --- a/radicale/__main__.py +++ b/radicale/__main__.py @@ -170,6 +170,7 @@ def run(): name, filename, exception)) else: server_class = ThreadedHTTPServer + server_class.client_timeout = configuration.getint("server", "timeout") if not configuration.getboolean("server", "dns_lookup"): RequestHandler.address_string = lambda self: self.client_address[0] diff --git a/radicale/config.py b/radicale/config.py index 71a315b..7ec3d11 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -34,6 +34,7 @@ INITIAL_CONFIG = { "hosts": "0.0.0.0:5232", "daemon": "False", "pid": "", + "timeout": "10", "ssl": "False", "certificate": "/etc/apache2/ssl/server.crt", "key": "/etc/apache2/ssl/server.key", From e438d9fd4bad74e204b200b4faac136c2a08a332 Mon Sep 17 00:00:00 2001 From: Unrud Date: Fri, 10 Jun 2016 14:34:52 +0200 Subject: [PATCH 3/4] Limit size of request body --- config | 3 +++ radicale/__init__.py | 6 ++++++ radicale/config.py | 1 + 3 files changed, 10 insertions(+) diff --git a/config b/config index ee28711..4228a5e 100644 --- a/config +++ b/config @@ -24,6 +24,9 @@ # File storing the PID in daemon mode #pid = +# Max size of request body (bytes) +#max_content_length = 10000000 + # Socket timeout (seconds) #timeout = 10 diff --git a/radicale/__init__.py b/radicale/__init__.py index ba1994d..b1cdc52 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -301,6 +301,12 @@ class Application: # Get content content_length = int(environ.get("CONTENT_LENGTH") or 0) if content_length: + max_content_length = self.configuration.getint( + "server", "max_content_length") + if max_content_length and content_length > max_content_length: + self.logger.debug( + "Request body too large: %d", content_length) + return response(client.REQUEST_ENTITY_TOO_LARGE) try: content = self.decode( environ["wsgi.input"].read(content_length), environ) diff --git a/radicale/config.py b/radicale/config.py index 7ec3d11..c639211 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -34,6 +34,7 @@ INITIAL_CONFIG = { "hosts": "0.0.0.0:5232", "daemon": "False", "pid": "", + "max_content_length": "10000000", "timeout": "10", "ssl": "False", "certificate": "/etc/apache2/ssl/server.crt", From 83ea9da2b413cfe87dc8965b8ce0a184c7f5ca53 Mon Sep 17 00:00:00 2001 From: Unrud Date: Fri, 10 Jun 2016 14:36:44 +0200 Subject: [PATCH 4/4] Limit number of parallel connections --- config | 3 +++ radicale/__init__.py | 18 ++++++++++++++++-- radicale/__main__.py | 2 ++ radicale/config.py | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/config b/config index 4228a5e..313f29a 100644 --- a/config +++ b/config @@ -24,6 +24,9 @@ # File storing the PID in daemon mode #pid = +# Max parallel connections +#max_connections = 20 + # Max size of request body (bytes) #max_content_length = 10000000 diff --git a/radicale/__init__.py b/radicale/__init__.py index b1cdc52..03727f5 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -29,9 +29,11 @@ should have been included in this package. import os import pprint import base64 +import contextlib import socket import socketserver import ssl +import threading import wsgiref.simple_server import re import zlib @@ -57,6 +59,7 @@ class HTTPServer(wsgiref.simple_server.WSGIServer): # These class attributes must be set before creating instance client_timeout = None + max_connections = None def __init__(self, address, handler, bind_and_activate=True): """Create server.""" @@ -76,6 +79,13 @@ class HTTPServer(wsgiref.simple_server.WSGIServer): self.server_bind() self.server_activate() + if self.max_connections: + self.connections_guard = threading.BoundedSemaphore( + self.max_connections) + else: + # use dummy context manager + self.connections_guard = contextlib.suppress() + def get_request(self): # Set timeout for client _socket, address = super().get_request() @@ -106,11 +116,15 @@ class HTTPSServer(HTTPServer): class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer): - pass + def process_request_thread(self, request, client_address): + with self.connections_guard: + return super().process_request_thread(request, client_address) class ThreadedHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer): - pass + def process_request_thread(self, request, client_address): + with self.connections_guard: + return super().process_request_thread(request, client_address) class RequestHandler(wsgiref.simple_server.WSGIRequestHandler): diff --git a/radicale/__main__.py b/radicale/__main__.py index dc17905..4e8c73d 100644 --- a/radicale/__main__.py +++ b/radicale/__main__.py @@ -171,6 +171,8 @@ def run(): else: server_class = ThreadedHTTPServer server_class.client_timeout = configuration.getint("server", "timeout") + server_class.max_connections = configuration.getint("server", + "max_connections") if not configuration.getboolean("server", "dns_lookup"): RequestHandler.address_string = lambda self: self.client_address[0] diff --git a/radicale/config.py b/radicale/config.py index c639211..27f799b 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -34,6 +34,7 @@ INITIAL_CONFIG = { "hosts": "0.0.0.0:5232", "daemon": "False", "pid": "", + "max_connections": "20", "max_content_length": "10000000", "timeout": "10", "ssl": "False",