Implemented configurable static response headers.
This commit is contained in:
parent
5b771312d1
commit
30be649ae2
5 changed files with 67 additions and 12 deletions
|
@ -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 target: An IO stream (stderr, stdout, ...) or a filename
|
||||||
'log_handler': logging.StreamHandler(),
|
'log_handler': logging.StreamHandler(),
|
||||||
# Variables passed to pagelets
|
# Variables passed to pagelets
|
||||||
'pagelet_variables': dict()
|
'pagelet_variables': dict(),
|
||||||
|
# Statically configured headers
|
||||||
|
'headers': dict()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize the config parser
|
# 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():
|
for k, v in parser['Pagelets'].items():
|
||||||
config['pagelet_variables'][k] = v
|
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
|
return config
|
||||||
|
|
|
@ -105,6 +105,7 @@ class MatematHTTPServer(HTTPServer):
|
||||||
staticroot: str,
|
staticroot: str,
|
||||||
templateroot: str,
|
templateroot: str,
|
||||||
pagelet_variables: Dict[str, str],
|
pagelet_variables: Dict[str, str],
|
||||||
|
static_headers: Dict[str, str],
|
||||||
log_level: int,
|
log_level: int,
|
||||||
log_handler: logging.Handler,
|
log_handler: logging.Handler,
|
||||||
bind_and_activate: bool = True) -> None:
|
bind_and_activate: bool = True) -> None:
|
||||||
|
@ -115,6 +116,8 @@ class MatematHTTPServer(HTTPServer):
|
||||||
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict()
|
self.session_vars: Dict[str, Tuple[datetime, Dict[str, Any]]] = dict()
|
||||||
# Set up pagelet arguments dict
|
# Set up pagelet arguments dict
|
||||||
self.pagelet_variables = pagelet_variables
|
self.pagelet_variables = pagelet_variables
|
||||||
|
# Set up static HTTP Response headers
|
||||||
|
self.static_headers = static_headers
|
||||||
# Set up the Jinja2 environment
|
# Set up the Jinja2 environment
|
||||||
self.jinja_env: jinja2.Environment = jinja2.Environment(
|
self.jinja_env: jinja2.Environment = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader(os.path.abspath(templateroot)),
|
loader=jinja2.FileSystemLoader(os.path.abspath(templateroot)),
|
||||||
|
@ -150,6 +153,7 @@ class MatematWebserver(object):
|
||||||
staticroot: str,
|
staticroot: str,
|
||||||
templateroot: str,
|
templateroot: str,
|
||||||
pagelet_variables: Dict[str, str],
|
pagelet_variables: Dict[str, str],
|
||||||
|
headers: Dict[str, str],
|
||||||
log_level: int,
|
log_level: int,
|
||||||
log_handler: logging.Handler) -> None:
|
log_handler: logging.Handler) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -160,6 +164,7 @@ class MatematWebserver(object):
|
||||||
:param staticroot: Path to the static webroot directory.
|
:param staticroot: Path to the static webroot directory.
|
||||||
:param templateroot: Path to the Jinja2 templates root directory.
|
:param templateroot: Path to the Jinja2 templates root directory.
|
||||||
:param pagelet_variables: Dictionary of variables to pass to pagelet functions.
|
: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_level: The log level, as defined in the builtin logging module.
|
||||||
:param log_handler: The logging handler.
|
:param log_handler: The logging handler.
|
||||||
"""
|
"""
|
||||||
|
@ -173,6 +178,7 @@ class MatematWebserver(object):
|
||||||
staticroot,
|
staticroot,
|
||||||
templateroot,
|
templateroot,
|
||||||
pagelet_variables,
|
pagelet_variables,
|
||||||
|
headers,
|
||||||
log_level,
|
log_level,
|
||||||
log_handler)
|
log_handler)
|
||||||
|
|
||||||
|
@ -308,12 +314,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
self.session_id: str = session_id
|
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
|
# Call a pagelet function, if one is registered for the requested path
|
||||||
if path in _PAGELET_PATHS:
|
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
|
# Call the pagelet function
|
||||||
pagelet_res = _PAGELET_PATHS[path](method,
|
pagelet_res = _PAGELET_PATHS[path](method,
|
||||||
path,
|
path,
|
||||||
|
@ -327,8 +334,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||||
self.send_response(hsc)
|
self.send_response(hsc)
|
||||||
# Format the session cookie timeout string and send the session cookie header
|
# Format the session cookie timeout string and send the session cookie header
|
||||||
expires = timeout.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
expires = timeout.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||||
self.send_header('Set-Cookie',
|
headers['Set-Cookie'] = f'matemat_session_id={session_id}; expires={expires}'
|
||||||
f'matemat_session_id={session_id}; expires={expires}')
|
# Disable caching
|
||||||
|
headers['Cache-Control'] = 'no-cache'
|
||||||
# Compute the body length and add the appropriate header
|
# Compute the body length and add the appropriate header
|
||||||
headers['Content-Length'] = str(len(data))
|
headers['Content-Length'] = str(len(data))
|
||||||
# If the pagelet did not set its own Content-Type header, use libmagic to guess an appropriate one
|
# 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'
|
charset = 'binary'
|
||||||
# Send content type and length header. Only set the charset if it's not "binary"
|
# Send content type and length header. Only set the charset if it's not "binary"
|
||||||
if charset == 'binary':
|
if charset == 'binary':
|
||||||
self.send_header('Content-Type', mimetype)
|
headers['Content-Type'] = mimetype
|
||||||
else:
|
else:
|
||||||
self.send_header('Content-Type', f'{mimetype}; charset={charset}')
|
headers['Content-Type'] = f'{mimetype}; charset={charset}'
|
||||||
self.send_header('Content-Length', str(len(data)))
|
headers['Content-Length'] = str(len(data))
|
||||||
self.send_header('Last-Modified', fileage.strftime('%a, %d %b %Y %H:%M:%S GMT'))
|
headers['Last-Modified'] = fileage.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||||
self.send_header('Cache-Control', 'max-age=1')
|
headers['Cache-Control'] = 'max-age=1'
|
||||||
|
# Send all headers
|
||||||
|
for name, value in headers.items():
|
||||||
|
self.send_header(name, value)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
# Send the requested resource as response body
|
# Send the requested resource as response body
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
|
|
|
@ -112,6 +112,10 @@ class MockServer:
|
||||||
self.webroot: str = webroot
|
self.webroot: str = webroot
|
||||||
# Variables to pass to pagelets
|
# Variables to pass to pagelets
|
||||||
self.pagelet_variables: Dict[str, str] = dict()
|
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
|
# Jinja environment with a single, static template
|
||||||
self.jinja_env = jinja2.Environment(
|
self.jinja_env = jinja2.Environment(
|
||||||
loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'})
|
loader=jinja2.DictLoader({'test.txt': 'Hello, {{ what }}!'})
|
||||||
|
|
|
@ -29,6 +29,10 @@ Name=Matemat
|
||||||
(Unit Test)
|
(Unit Test)
|
||||||
UploadDir= /var/test/static/upload
|
UploadDir= /var/test/static/upload
|
||||||
DatabaseFile=/var/test/db/test.db
|
DatabaseFile=/var/test/db/test.db
|
||||||
|
|
||||||
|
[HttpHeaders]
|
||||||
|
Content-Security-Policy = default-src: 'self';
|
||||||
|
X-I-Am-A-Header = andthisismyvalue
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_PARTIAL_CONFIG = '''
|
_PARTIAL_CONFIG = '''
|
||||||
|
@ -37,6 +41,9 @@ Port=443
|
||||||
|
|
||||||
[Pagelets]
|
[Pagelets]
|
||||||
Name=Matemat (Unit Test 2)
|
Name=Matemat (Unit Test 2)
|
||||||
|
|
||||||
|
[HttpHeaders]
|
||||||
|
X-I-Am-A-Header = andthisismyothervalue
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_LOG_NONE_CONFIG = '''
|
_LOG_NONE_CONFIG = '''
|
||||||
|
@ -113,6 +120,8 @@ class TestConfig(TestCase):
|
||||||
self.assertEqual(sys.stderr, config['log_handler'].stream)
|
self.assertEqual(sys.stderr, config['log_handler'].stream)
|
||||||
self.assertIsInstance(config['pagelet_variables'], dict)
|
self.assertIsInstance(config['pagelet_variables'], dict)
|
||||||
self.assertEqual(0, len(config['pagelet_variables']))
|
self.assertEqual(0, len(config['pagelet_variables']))
|
||||||
|
self.assertIsInstance(config['headers'], dict)
|
||||||
|
self.assertEqual(0, len(config['headers']))
|
||||||
|
|
||||||
def test_parse_config_full(self):
|
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('Matemat\n(Unit Test)', config['pagelet_variables']['Name'])
|
||||||
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
||||||
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile'])
|
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):
|
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('Matemat (Unit Test 2)', config['pagelet_variables']['Name'])
|
||||||
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
||||||
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile'])
|
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):
|
def test_parse_config_logging_none(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -272,3 +272,19 @@ class TestServe(AbstractHttpdTest):
|
||||||
packet = self.client_sock.get_response()
|
packet = self.client_sock.get_response()
|
||||||
# No charset should be in the header. Yes, this is a stupid example
|
# No charset should be in the header. Yes, this is a stupid example
|
||||||
self.assertEqual('text/plain', packet.headers['Content-Type'])
|
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'])
|
||||||
|
|
Loading…
Reference in a new issue