From 30be649ae2ffbf02e8b9f6ea872aa9b7170d781b Mon Sep 17 00:00:00 2001 From: s3lph Date: Fri, 3 Aug 2018 18:42:30 +0200 Subject: [PATCH 1/2] Implemented configurable static response headers. --- matemat/webserver/config.py | 9 ++++- matemat/webserver/httpd.py | 33 ++++++++++++------- matemat/webserver/test/abstract_httpd_test.py | 4 +++ matemat/webserver/test/test_config.py | 17 ++++++++++ matemat/webserver/test/test_serve.py | 16 +++++++++ 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/matemat/webserver/config.py b/matemat/webserver/config.py index 1913c4a..5ac6059 100644 --- a/matemat/webserver/config.py +++ b/matemat/webserver/config.py @@ -67,7 +67,9 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]: # Log target: An IO stream (stderr, stdout, ...) or a filename 'log_handler': logging.StreamHandler(), # Variables passed to pagelets - 'pagelet_variables': dict() + 'pagelet_variables': dict(), + # Statically configured headers + 'headers': dict() } # Initialize the config parser @@ -101,4 +103,9 @@ def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]: for k, v in parser['Pagelets'].items(): config['pagelet_variables'][k] = v + # Read all values from the [HttpHeaders] section, if present. These values are set as HTTP response headers + if 'HttpHeaders' in parser.sections(): + for k, v in parser['HttpHeaders'].items(): + config['headers'][k] = v + return config diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index e69c859..20f5bc8 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -105,6 +105,7 @@ class MatematHTTPServer(HTTPServer): staticroot: str, templateroot: str, pagelet_variables: Dict[str, str], + static_headers: Dict[str, str], log_level: int, log_handler: logging.Handler, bind_and_activate: bool = True) -> None: @@ -115,6 +116,8 @@ class MatematHTTPServer(HTTPServer): self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict() # Set up pagelet arguments dict self.pagelet_variables = pagelet_variables + # Set up static HTTP Response headers + self.static_headers = static_headers # Set up the Jinja2 environment self.jinja_env: jinja2.Environment = jinja2.Environment( loader=jinja2.FileSystemLoader(os.path.abspath(templateroot)), @@ -150,6 +153,7 @@ class MatematWebserver(object): staticroot: str, templateroot: str, pagelet_variables: Dict[str, str], + headers: Dict[str, str], log_level: int, log_handler: logging.Handler) -> None: """ @@ -160,6 +164,7 @@ class MatematWebserver(object): :param staticroot: Path to the static webroot directory. :param templateroot: Path to the Jinja2 templates root directory. :param pagelet_variables: Dictionary of variables to pass to pagelet functions. + :param headers: Dictionary of statically configured headers. :param log_level: The log level, as defined in the builtin logging module. :param log_handler: The logging handler. """ @@ -173,6 +178,7 @@ class MatematWebserver(object): staticroot, templateroot, pagelet_variables, + headers, log_level, log_handler) @@ -308,12 +314,13 @@ class HttpHandler(BaseHTTPRequestHandler): return self.session_id: str = session_id + # Set all static headers. These can still be overwritten by a pagelet + headers: Dict[str, str] = dict() + for k, v in self.server.static_headers.items(): + headers[k] = v + # Call a pagelet function, if one is registered for the requested path if path in _PAGELET_PATHS: - # Prepare some headers. Those can still be overwritten by the pagelet - headers: Dict[str, str] = { - 'Cache-Control': 'no-cache' - } # Call the pagelet function pagelet_res = _PAGELET_PATHS[path](method, path, @@ -327,8 +334,9 @@ class HttpHandler(BaseHTTPRequestHandler): self.send_response(hsc) # Format the session cookie timeout string and send the session cookie header expires = timeout.strftime('%a, %d %b %Y %H:%M:%S GMT') - self.send_header('Set-Cookie', - f'matemat_session_id={session_id}; expires={expires}') + headers['Set-Cookie'] = f'matemat_session_id={session_id}; expires={expires}' + # Disable caching + headers['Cache-Control'] = 'no-cache' # Compute the body length and add the appropriate header headers['Content-Length'] = str(len(data)) # If the pagelet did not set its own Content-Type header, use libmagic to guess an appropriate one @@ -394,12 +402,15 @@ class HttpHandler(BaseHTTPRequestHandler): charset = 'binary' # Send content type and length header. Only set the charset if it's not "binary" if charset == 'binary': - self.send_header('Content-Type', mimetype) + headers['Content-Type'] = mimetype else: - self.send_header('Content-Type', f'{mimetype}; charset={charset}') - self.send_header('Content-Length', str(len(data))) - self.send_header('Last-Modified', fileage.strftime('%a, %d %b %Y %H:%M:%S GMT')) - self.send_header('Cache-Control', 'max-age=1') + headers['Content-Type'] = f'{mimetype}; charset={charset}' + headers['Content-Length'] = str(len(data)) + headers['Last-Modified'] = fileage.strftime('%a, %d %b %Y %H:%M:%S GMT') + headers['Cache-Control'] = 'max-age=1' + # Send all headers + for name, value in headers.items(): + self.send_header(name, value) self.end_headers() # Send the requested resource as response body self.wfile.write(data) diff --git a/matemat/webserver/test/abstract_httpd_test.py b/matemat/webserver/test/abstract_httpd_test.py index aadc346..bdec6bc 100644 --- a/matemat/webserver/test/abstract_httpd_test.py +++ b/matemat/webserver/test/abstract_httpd_test.py @@ -112,6 +112,10 @@ class MockServer: self.webroot: str = webroot # Variables to pass to pagelets self.pagelet_variables: Dict[str, str] = dict() + # Static response headers + self.static_headers: Dict[str, str] = { + 'X-Static-Testing-Header': 'helloworld!' + } # Jinja environment with a single, static template self.jinja_env = jinja2.Environment( loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'}) diff --git a/matemat/webserver/test/test_config.py b/matemat/webserver/test/test_config.py index 517bc29..9e13374 100644 --- a/matemat/webserver/test/test_config.py +++ b/matemat/webserver/test/test_config.py @@ -29,6 +29,10 @@ Name=Matemat (Unit Test) UploadDir= /var/test/static/upload DatabaseFile=/var/test/db/test.db + +[HttpHeaders] +Content-Security-Policy = default-src: 'self'; +X-I-Am-A-Header = andthisismyvalue ''' _PARTIAL_CONFIG = ''' @@ -37,6 +41,9 @@ Port=443 [Pagelets] Name=Matemat (Unit Test 2) + +[HttpHeaders] +X-I-Am-A-Header = andthisismyothervalue ''' _LOG_NONE_CONFIG = ''' @@ -113,6 +120,8 @@ class TestConfig(TestCase): self.assertEqual(sys.stderr, config['log_handler'].stream) self.assertIsInstance(config['pagelet_variables'], dict) self.assertEqual(0, len(config['pagelet_variables'])) + self.assertIsInstance(config['headers'], dict) + self.assertEqual(0, len(config['headers'])) def test_parse_config_full(self): """ @@ -144,6 +153,10 @@ class TestConfig(TestCase): self.assertEqual('Matemat\n(Unit Test)', config['pagelet_variables']['Name']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) + self.assertIsInstance(config['headers'], dict) + self.assertEqual(2, len(config['headers'])) + self.assertEqual('default-src: \'self\';', config['headers']['Content-Security-Policy']) + self.assertEqual('andthisismyvalue', config['headers']['X-I-Am-A-Header']) def test_parse_config_precedence(self): """ @@ -172,6 +185,10 @@ class TestConfig(TestCase): self.assertEqual('Matemat (Unit Test 2)', config['pagelet_variables']['Name']) self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir']) self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile']) + self.assertIsInstance(config['headers'], dict) + self.assertEqual(2, len(config['headers'])) + self.assertEqual('default-src: \'self\';', config['headers']['Content-Security-Policy']) + self.assertEqual('andthisismyothervalue', config['headers']['X-I-Am-A-Header']) def test_parse_config_logging_none(self): """ diff --git a/matemat/webserver/test/test_serve.py b/matemat/webserver/test/test_serve.py index b7ec850..bff9a6b 100644 --- a/matemat/webserver/test/test_serve.py +++ b/matemat/webserver/test/test_serve.py @@ -272,3 +272,19 @@ class TestServe(AbstractHttpdTest): packet = self.client_sock.get_response() # No charset should be in the header. Yes, this is a stupid example self.assertEqual('text/plain', packet.headers['Content-Type']) + + def test_serve_static_pagelet_header(self): + # Send a request + self.client_sock.set_request(b'GET /just/testing/serve_pagelet_str HTTP/1.1\r\n\r\n') + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + # The statically set header must be present + self.assertEqual('helloworld!', packet.headers['X-Static-Testing-Header']) + + def test_serve_static_static_header(self): + # Send a request + self.client_sock.set_request(b'GET /testbin.txt HTTP/1.1\r\n\r\n') + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + # The statically set header must be present + self.assertEqual('helloworld!', packet.headers['X-Static-Testing-Header']) From afd5004b096c9b6d0a65451b7ba9ef01a9103235 Mon Sep 17 00:00:00 2001 From: s3lph Date: Fri, 3 Aug 2018 18:59:58 +0200 Subject: [PATCH 2/2] Added documentation for statically configured headers --- doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc b/doc index e52eec0..1a181d3 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit e52eec0831ef72edab816d549d7ee2a85575e292 +Subproject commit 1a181d3c72cf20fe21e93b063dc3c395e1fa8f93