"""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 and a stand-in for WonderInk's own start page and sign-in, built the way the real site's is (read from its code, and measured with a real account): the sign-in is cookies its JavaScript writes, and rrf-auth is "Basic " and the base64 of the email and password — so a check can prove that nothing Wonder Ink writes down holds one. Sign out calls /rrframework/dataStore/signout and then sets them to "". /app "Signed in as …" if rrf-auth is set, else a sign-in form /app?busy=1 the same, plus what a site does by itself: a request every second and an animation, and no input at all /links every kind of link a lesson has, for following them 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 re 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", ".pdf": "application/pdf"} fetched_once = set() lock = threading.Lock() LESSON = """Picture lesson

Noah's Ark

Noah built an ark, and the animals came two by two. Colour the pictures below.

More links for this lesson

""" APP = """App
""" LINKS = """Links

Links in a lesson

Saul Welcome Question Slide

The lesson, no target

A picture on another site

The worksheet (PDF)

Go to the bottom

The bottom

""" 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 == "/app": return self.answer(200, APP.encode(), "text/html", [("Cache-Control", "no-store")]) if path == "/whoami": token = re.search(r"(?:^|; )rrf-auth=([^;]+)", self.headers.get("Cookie", "")) return self.answer(200 if token else 403, b"{}", "application/json") if path in ("/rrframework/dataStore/signout", "/beacon"): return self.answer(200, b"{}", "application/json") 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 == "/links": return self.answer(200, LINKS.replace("OTHER", str(OTHER)).encode(), "text/html") 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()