"""A signed-in lesson with pictures in it, for right-click printing. wonderink-lesson-site.py PICTURES PORT OTHER_PORT LOG Run inside the lab by tools/lab/wonderink-print-check. Stands in for Wonder Ink's own site, which the lab has no account for, with every kind of picture a lesson might put under a teacher's pointer: /login sets a session cookie (HttpOnly, SameSite=Lax), as a sign-in does, and goes on to /lesson /lesson the page; its pictures are all behind that cookie /private/NAME a picture, 403 without the cookie /once/N/NAME a picture that can be fetched exactly once — so a print that fetches it again gets a 404, not a picture OTHER_PORT the same pictures from another origin, with no CORS Every request is logged, one JSON line each, so a check can say what was fetched while it printed. What was measured with this, and why Print image fetches nothing, is in classroom-wonderink's _take_picture. """ import http.server import json import sys import threading import time import uuid from pathlib import Path from urllib.parse import urlparse PICTURES = Path(sys.argv[1]) PORT, OTHER = int(sys.argv[2]), int(sys.argv[3]) LOG = Path(sys.argv[4]) TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp"} fetched_once = set() lock = threading.Lock() LESSON = """
Noah built an ark, and the animals came two by two. Colour the pictures below.
"""
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def answer(self, code, body=b"", kind="text/plain", headers=()):
self.send_response(code)
self.send_header("Content-Type", kind)
self.send_header("Content-Length", str(len(body)))
for name, value in headers:
self.send_header(name, value)
self.end_headers()
self.wfile.write(body)
def picture(self, name):
path = PICTURES / Path(name).name
if not path.is_file() or path.suffix not in TYPES:
return self.answer(404, b"no such picture")
return self.answer(200, path.read_bytes(), TYPES[path.suffix],
[("Cache-Control", "private, max-age=600")])
def do_GET(self):
path = urlparse(self.path).path
signed_in = "session=ok" in self.headers.get("Cookie", "")
with lock, open(LOG, "a") as log:
log.write(json.dumps({"t": time.time(), "port": self.server.server_port,
"path": path, "signed_in": signed_in}) + "\n")
if self.server.server_port == OTHER:
if path.startswith("/img/"):
return self.picture(path[5:])
return self.answer(404)
if path == "/login":
return self.answer(302, headers=[
("Set-Cookie", "session=ok; Path=/; HttpOnly; SameSite=Lax"),
("Location", "/lesson")])
if not signed_in:
return self.answer(403, b"sign in first")
if path == "/lesson":
page = LESSON.replace("NONCE", uuid.uuid4().hex).replace("OTHER", str(OTHER))
return self.answer(200, page.encode(), "text/html")
if path.startswith("/private/"):
return self.picture(path[9:])
if path.startswith("/once/"):
with lock:
if path in fetched_once:
return self.answer(404, b"this address has been used")
fetched_once.add(path)
return self.picture(path.rsplit("/", 1)[1])
return self.answer(404)
for port in (OTHER, PORT):
server = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
threading.Event().wait()