Repository navigation
Server and Security
ask.py serve runs a small HTTP server from the Python standard library. This page says what it serves, what it checks and what it refuses, with the responses from the wiki's run. SECURITY.md says how to report a problem.
Always 127.0.0.1, never another interface. Port 0 (the default) lets the system pick a free port. On Windows the socket is bound with SO_EXCLUSIVEADDRUSE and without SO_REUSEADDR, so a second server cannot take a port that is in use. Each server serves one set.
| Path | What | Needs |
|---|---|---|
/ |
the page, with this run's token written into it | the Host header |
/app.js, /app.css
|
the page's script and styles | the Host header |
/m/<root>/<path> |
a media file from the set folder (0) or a media root (1, 2, ...) |
the Host header |
/api/set |
the set as the page sees it: media turned into URLs, the answers so far, the last visit | the token |
/api/ping |
{ok, pid, set_dir}, which status and stop use |
the token |
POST /api/answer |
saves one answer | the token and the Origin rule |
POST /api/attachment |
saves one pasted or dropped image | the token and the Origin rule |
POST /api/event |
appends a visit, pause or done mark |
the token and the Origin rule |
POST /api/submit |
study sets: scores the current attempt and returns the review | the token and the Origin rule |
POST /api/retake |
study sets: starts a new attempt once the current one is submitted | the token and the Origin rule |
The Host header must be 127.0.0.1:<port> or localhost:<port>, which stops DNS-rebinding pages. The token is a fresh random string for each server, sent as X-Ask-Token; another web page cannot read it out of the served page. On a POST, an Origin header, when present, must be http://127.0.0.1:<port> or http://localhost:<port>; a request without one is accepted, since only the token proves it came from the page.
The page is served with a Content Security Policy that allows nothing from the network (default-src 'none', scripts, styles and media from the server itself only), Referrer-Policy: no-referrer and Cache-Control: no-store. Media is served with a sandboxing policy of its own and nosniff.
In a study set, /api/set leaves out the answer key until the attempt is submitted: no correct, no option why, no rubric or model_answer, and the option ids are replaced by o1, o2, ... in the order shown, so the page source cannot give the answers away. Study mode shows what the page gets. A drawn figure arrives as a data: URL, which the Content Security Policy allows for images.
Media is looked up by extension, as on Question set format, and served with Accept-Ranges: bytes so a browser can seek. One range per request; a request with several gets the whole file (200), which HTTP allows. In the run, for the 14,077-byte test clip:
GET /m/1/arrow_pair.mp4 -> 206
Content-Range: bytes 0-1/14077
A range that starts past the end gets 416 with Content-Range: bytes */<size> (from the source; not run for the wiki). Paths are resolved, symbolic links included, and must stay inside their root and end in a media extension. A file that is not media, and a path that climbs out of its root, both get 403:
GET /m/0/questions.json -> 403
{
"ok": false,
"error": "not a media file in this set"
}
GET /m/1/..%2F..%2Fswatch.png -> 403
{
"ok": false,
"error": "not a media file in this set"
}
A pasted or dropped image is posted to /api/attachment with the question id in X-Ask-Qid. The server checks the first bytes, not the file name or the Content-Type, and keeps only PNG, JPEG, GIF and WebP, up to 10 MB. It saves them as attachments/<qid>-<n>.<ext>, numbered from 1:
POST /api/attachment -> 200
{
"ok": true,
"path": "attachments/arrow-1.png",
"url": "/m/0/attachments/arrow-1.png"
}
An answer can only name attachments that exist under attachments/. JSON bodies are capped at 1 MB.
From the run, in the order the requests were made:
GET /api/set -> 403
{
"ok": false,
"error": "missing or wrong token"
}
GET /api/set -> 403
{
"ok": false,
"error": "wrong Host header"
}
POST /api/answer -> 403
{
"ok": false,
"error": "missing or wrong token or origin"
}
POST /api/answer -> 400
{
"ok": false,
"error": "choice 'maybe' is not an option of render"
}
The third request had the right token and Origin: http://evil.example. Uploads that are not images, or are too large:
POST /api/attachment -> 400
{
"ok": false,
"error": "only PNG, JPEG, GIF and WebP images can be attached"
}
POST /api/attachment -> 413
{
"ok": false,
"error": "too large (limit 10 MB)"
}
A study set answers 409 to an answer or a submit after the attempt is submitted, and to a retake before it (Study mode). A decision set answers 400 "this set is not a study set" to both routes (from the source). Other refusals, from the source: 411 "Content-Length is required", 400 "bad JSON: ..." and "unknown question", 400 "unknown event type", and 404 "not found" for any other path.
Each answer is written to answers.json at once: a temporary file, then a replace, retried for about ten seconds on Windows while another program holds the file open. Every answer raises the set's rev by one, and an answer event goes into events.jsonl. When questions.json cannot be read while the server runs, /api/set keeps serving the last good copy with a note saying so, so a half-saved edit does not empty the page.
stop reads server.json, pings /api/ping with the token, and stops the process only when the PID the server reports is the PID in the file. It never looks a process up by name, so it cannot stop another program that happens to run Python. A dead server's server.json is removed and nothing is killed (Commands).
There is no hosted service and nothing is sent anywhere. Questions, answers, notes and images stay in the set folder.
This wiki describes askwright 0.2.0 and was last updated on 2026-10-25. The skill is MIT licensed. Report problems in the issues. Download: askwright 0.2.0.
Using it
- Getting started
- Commands
- Question set format
- Lint rules and errors
- Server and security
- Study mode
- Figures
- Recipes
- FAQ
The releases
Contributing
Elsewhere