From 87b66719e345f6addcf89acf87f391c179597ec6 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 19:00:37 +0200 Subject: [PATCH 1/3] Implemented (probably quite shaky) static content cache headers. --- matemat/webserver/httpd.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index 1aae08e..aa282e1 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -10,6 +10,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from http.cookies import SimpleCookie from uuid import uuid4 from datetime import datetime, timedelta +from time import localtime import jinja2 @@ -323,7 +324,7 @@ class HttpHandler(BaseHTTPRequestHandler): # Send the HTTP status code 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") + expires = timeout.strftime('%a, %d %b %Y %H:%M:%S GMT') self.send_header('Set-Cookie', f'matemat_session_id={session_id}; expires={expires}') # Compute the body length and add the appropriate header @@ -345,6 +346,21 @@ class HttpHandler(BaseHTTPRequestHandler): filepath: str = os.path.abspath(os.path.join(self.server.webroot, path[1:])) # Make sure the file is actually inside the webroot directory and that it exists if os.path.commonpath([filepath, self.server.webroot]) == self.server.webroot and os.path.exists(filepath): + # Parse the If-Modified-Since header to check whether the browser can reuse cached content + datestr: str = self.headers.get('If-Modified-Since', 'Thu, 01 Jan 1970 00:00:00 GMT') + maxage: datetime = datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S %Z') + # File modification time in LOCAL time + filestat: int = int(os.path.getmtime(filepath)) + # Create timezone-unaware datetime object and subtract UTC offset + fileage: datetime = datetime.fromtimestamp(filestat) - timedelta(seconds=localtime().tm_gmtoff) + + # If the file has not been replaced by a newer version than requested by the client, send a 304 response + if fileage <= maxage: + self.send_response(304, 'Not Modified') + self.send_header('Content-Length', '0') + self.end_headers() + return + # Open and read the file with open(filepath, 'rb') as f: data = f.read() @@ -358,6 +374,8 @@ class HttpHandler(BaseHTTPRequestHandler): # Send content type and length header self.send_header('Content-Type', mimetype) 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') self.end_headers() # Send the requested resource as response body self.wfile.write(data) From a9fc6f451b45ba5da0e66fbc261b8f0c0aeec761 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 22:55:14 +0200 Subject: [PATCH 2/3] Cleaner datetime API usage. --- matemat/webserver/httpd.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/matemat/webserver/httpd.py b/matemat/webserver/httpd.py index aa282e1..e962a8c 100644 --- a/matemat/webserver/httpd.py +++ b/matemat/webserver/httpd.py @@ -10,7 +10,6 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from http.cookies import SimpleCookie from uuid import uuid4 from datetime import datetime, timedelta -from time import localtime import jinja2 @@ -349,10 +348,10 @@ class HttpHandler(BaseHTTPRequestHandler): # Parse the If-Modified-Since header to check whether the browser can reuse cached content datestr: str = self.headers.get('If-Modified-Since', 'Thu, 01 Jan 1970 00:00:00 GMT') maxage: datetime = datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S %Z') - # File modification time in LOCAL time + # Get file modification time filestat: int = int(os.path.getmtime(filepath)) - # Create timezone-unaware datetime object and subtract UTC offset - fileage: datetime = datetime.fromtimestamp(filestat) - timedelta(seconds=localtime().tm_gmtoff) + # Create UTC datetime object from mtime + fileage: datetime = datetime.utcfromtimestamp(filestat) # If the file has not been replaced by a newer version than requested by the client, send a 304 response if fileage <= maxage: From 96eaa2c4c0d5dcd36c088ae56267fb77a76d9e56 Mon Sep 17 00:00:00 2001 From: s3lph Date: Sat, 14 Jul 2018 23:05:50 +0200 Subject: [PATCH 3/3] Unit tests for 304 Not Modified testing. --- matemat/webserver/test/abstract_httpd_test.py | 2 +- matemat/webserver/test/test_serve.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/matemat/webserver/test/abstract_httpd_test.py b/matemat/webserver/test/abstract_httpd_test.py index 0817d41..5680836 100644 --- a/matemat/webserver/test/abstract_httpd_test.py +++ b/matemat/webserver/test/abstract_httpd_test.py @@ -118,7 +118,7 @@ class MockServer: ) # Set up logger self.logger: logging.Logger = logging.getLogger('matemat unit test') - self.logger.setLevel(0) + self.logger.setLevel(logging.DEBUG) # Initalize a log handler to stderr and set the log format sh: logging.StreamHandler = logging.StreamHandler() sh.setFormatter(logging.Formatter('%(asctime)s %(name)s [%(levelname)s]: %(message)s')) diff --git a/matemat/webserver/test/test_serve.py b/matemat/webserver/test/test_serve.py index 5db53b3..c67e06e 100644 --- a/matemat/webserver/test/test_serve.py +++ b/matemat/webserver/test/test_serve.py @@ -3,6 +3,8 @@ from typing import Any, Dict, Union import os import os.path +from datetime import datetime, timedelta + from matemat.exceptions import HttpException from matemat.webserver import HttpHandler, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet @@ -163,6 +165,34 @@ class TestServe(AbstractHttpdTest): self.assertEqual(403, packet.statuscode) self.assertNotEqual(b'This should not be readable', packet.body) + def test_serve_static_cache(self): + # Request a static resource + timeout: datetime = datetime.utcnow() + timedelta(hours=1) + timeoutstr = timeout.strftime('%a, %d %b %Y %H:%M:%S GMT') + self.client_sock.set_request( + f'GET /static_resource.txt HTTP/1.1\r\nIf-Modified-Since: {timeoutstr}\r\n\r\n'.encode('utf-8')) + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + + # Make sure that no pagelet was called + self.assertIsNone(packet.pagelet) + # Make sure a 304 Not Modified is sent and the body is empty + self.assertEqual(304, packet.statuscode) + self.assertEqual(0, len(packet.body)) + + def test_serve_static_cache_renew(self): + # Request a static resource + self.client_sock.set_request( + b'GET /static_resource.txt HTTP/1.1\r\nIf-Modified-Since: Mon, 01 Jan 2018 13:37:42 GMT\r\n\r\n') + HttpHandler(self.client_sock, ('::1', 45678), self.server) + packet = self.client_sock.get_response() + + # Make sure that no pagelet was called + self.assertIsNone(packet.pagelet) + # Make sure the expected content is served + self.assertEqual(200, packet.statuscode) + self.assertEqual(b'static resource test', packet.body) + def test_serve_not_found(self): # Request a nonexistent resource self.client_sock.set_request(b'GET /nonexistent HTTP/1.1\r\n\r\n')