Files
julian.kiedaischandClaude Opus 5.5 fb1a467221 feat: F6.1 FILIUS files on F: projects from examples and .fls, copies, export
/svc/examples lists FILIUS' examples, which lie beside F in the image (GPL
like this repository, kept out of the dashboard); /svc/projects makes a
project from one; /svc/projects/<id>/import takes a .fls (up to 64 MiB),
/copy copies a project as it is now, /export gives it back as a .fls with its
warnings. Large files go into the blob store on import and come back from it
on export, which is refused when one is missing.

Found on the way: Node's request body is a Buffer, and Yjs takes no Buffer
into a document, so every .fls with binary files would have failed; and
fflate, now in the bundle, imports createRequire itself, which the build's
banner declared again, so F did not start. The banner names it otherwise and
a test builds and starts the bundle. The proxy's /svc/ takes 70 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:55:11 +00:00

246 lines
11 KiB
Plaintext

# test-filius.hub.mdg-hamburg.de and filius.hub.mdg-hamburg.de → server F
#
# A **site snippet** for the `conf.d` of the external openresty, like the
# dashboard's `proxy.conf` and Scratch's `deploy/proxy/scratch.conf`: it goes
# **into** the existing `http{}` and declares nothing in the main context
# except two `upstream`s and two `map`s.
#
# Two environments, one machine
# -----------------------------
# Test and production are two Docker stacks on the same machine (the LXC
# container of server F), each with its own database, its own secrets and
# its own port:
#
# test-filius.hub.mdg-hamburg.de → :8081 (filius_test)
# filius.hub.mdg-hamburg.de → :8080 (filius_production)
#
# Each stack has **one** entrance, the port of its F process. F delivers
# everything on it: the editor's page and bundle, `/__auth`, `/svc/*`,
# `/blobs`, `/healthz` and the two WebSockets `/sync` and `/sim`. This vhost
# is therefore a pass-through and splits nothing up — except where the
# proxy's own defaults would get in the way (WebSockets, upload size).
#
# The two environments are one `server` block: the `map` below picks the
# stack by the name the browser asked for. Two blocks that only differ in
# one line drift apart; whoever later changes a limit in one forgets the
# other, and the test environment no longer tests what production runs.
#
# Which dashboard talks to which F: `test-dashboard` to `test-filius`,
# `dashboard` to `filius` — each with the ticket secret and service key of
# its own stack. A ticket from the test dashboard is refused in production
# (wrong secret) and that is intended.
#
# ── The one line every installation touches ───────────────────────────
#
# `filius-lxc.invalid` (twice, in the two `upstream`s) is the address of the
# LXC container of server F, which does not exist yet. The placeholder is
# chosen so that it **cannot** work: `.invalid` never resolves, and
# `openresty -t` stops with "host not found in upstream". Whoever puts the
# file in place without replacing it finds out before the reload, not from
# a user.
#
# The Docker stacks have to publish the ports on an address this openresty
# reaches, e.g. in the stack's compose file (F's own port stays 8080 in
# both containers, `FILIUS_PORT`):
#
# test: ports: ["<address>:8081:8080"]
# production: ports: ["<address>:8080:8080"]
#
# Check from the openresty:
#
# curl -s http://<address>:8081/healthz
# curl -s http://<address>:8080/healthz
#
# ── What the stacks need in their environment ─────────────────────────
#
# `FILIUS_ORIGINS` is the origin of the page the editor runs in, and that is
# this host: F refuses a WebSocket and a blob upload from any other origin.
#
# test: FILIUS_ORIGINS=https://test-filius.hub.mdg-hamburg.de
# production: FILIUS_ORIGINS=https://filius.hub.mdg-hamburg.de
#
# No `FILIUS_INSECURE_COOKIE`: TLS ends here, and the browser reaches F only
# via https. The cookie carries `Secure` and `SameSite=Lax`; it reaches F in
# the dashboard's frame because `*.hub.mdg-hamburg.de` is the same site as
# the dashboard.
#
# ── What must **not** stand here ──────────────────────────────────────
#
# No `add_header`, above all no `X-Frame-Options`: the editor lives in a
# frame of the dashboard, and the header would turn it into a white box.
# (An `add_header X-Frame-Options` in the `http{}` of this openresty would do
# the same to this host.)
#
# No CORS headers. F answers CORS for `/blobs` itself, against its
# `FILIUS_ORIGINS`; a second `Access-Control-Allow-Origin` from here makes
# the browser refuse the response.
upstream filius_test {
server filius-lxc.invalid:8081;
}
upstream filius_production {
server filius-lxc.invalid:8080;
}
# Which stack a host goes to. `$host` is the name without port, in lower
# case. The `server_name` below lets only these two names in, so the
# `default` never decides anything; it is there so that no request ends in a
# `proxy_pass` to nothing.
map $host $filius_upstream {
default filius_production;
test-filius.hub.mdg-hamburg.de filius_test;
filius.hub.mdg-hamburg.de filius_production;
}
# Own name instead of `$connection_upgrade`: this `conf.d` holds further
# snippets, the openresty already ships `$connection_upgrade`, and two `map`s
# of the same name are a start-up error in nginx.
map $http_upgrade $filius_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name test-filius.hub.mdg-hamburg.de filius.hub.mdg-hamburg.de;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name test-filius.hub.mdg-hamburg.de filius.hub.mdg-hamburg.de;
http2 on;
# Wildcard for *.hub.mdg-hamburg.de; both names are one label under
# `hub`, as the wildcard requires.
ssl_certificate /etc/ssl/certs/hub.combined;
ssl_certificate_key /etc/ssl/private/hub.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# One file for both environments, without a variable in the path: such a
# path is opened by the worker per request, not by the master at start,
# and the worker may not write in `/var/log/nginx`. Which environment a
# line belongs to is told by each stack's own log (`docker compose logs`).
access_log /var/log/nginx/filius-access.log;
error_log /var/log/nginx/filius-error.log;
# ── The two WebSockets: /sync and /sim ───────────────────────────
#
# `/sync` is the shared document (Hocuspocus), `/sim?project=…` the
# simulation room. Both stay open as long as the project is open.
#
# * `Upgrade`/`Connection`: without them nginx does not pass the
# handshake on.
# * 3600 s: the limit applies between two bytes. F pings every 30 s, so
# an open connection is never silent that long — but a connection
# that is cut after the default 60 s reconnects with a complete
# sign-in and a reload of the document, and everyone sees the others
# blink.
# * `Origin` passes through unchanged (nginx forwards every request
# header); F checks it against `FILIUS_ORIGINS`.
location ~ ^/(sync|sim)$ {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $filius_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_connect_timeout 10s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# ── Large files: /blobs ──────────────────────────────────────────
#
# Pictures and other binary files from 16 KB on go here as one request
# each, up to 16 MiB (`BLOB_MAX_BYTES` in `packages/server/src/blobs.ts`).
# The limit here is **above** F's on purpose: then F answers a file that
# is too large itself, with `413 tooLarge` and its CORS headers, and the
# editor can say so. nginx's own 413 has no CORS headers; the browser
# hands the editor only "network error", and nobody looks for the cause
# in an nginx default on another machine.
location /blobs {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 20m;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
# ── The dashboard's service route: /svc/ ─────────────────────────
#
# Guarded by `FILIUS_SERVICE_KEY`; the dashboard reaches F under this
# name, so if this proxy is down, creating, importing and deleting
# projects is down too. A FILIUS file the dashboard imports comes here
# whole, with its pictures (`/svc/projects/<id>/import`, F takes up to
# 64 MiB, `IMPORT_BYTES`); nginx' default of 1 MB would refuse a class's
# project with a few screenshots in it. F limits everything else under
# `/svc/` to 64 KiB itself. The limit is again above F's, so that F
# answers a file too large itself (see `/blobs`).
location /svc/ {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 70m;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
# ── Everything else ──────────────────────────────────────────────
#
# The editor's page and bundle, `/__auth` (ticket in, cookie out) and
# `/healthz`; nginx's default of 1 MB for a body stays.
location / {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
}
# ── When something does not work ──────────────────────────────────────
#
# | Symptom | Cause |
# |---|---|
# | `openresty -t`: host not found in upstream "filius-lxc.invalid…" | The placeholder is still there, see above. |
# | 502 on one host only | That stack is not running or does not publish its port. `curl http://<address>:8081/healthz` (test) or `:8080` (production) from the openresty. |
# | Test host shows production's projects (or the other way round) | The two ports are swapped in the `upstream`s or in the compose files. |
# | Editor opens, but "connection lost" right away | `/sync` or `/sim` gets no `Upgrade`, or `FILIUS_ORIGINS` does not name this host (F logs `sync.foreignOrigin`). |
# | Sign-in in the dashboard's frame fails silently | The stack runs with `FILIUS_INSECURE_COOKIE=1`, or the dashboard of the other environment issued the ticket. |
# | Large picture: "upload failed" without a reason | `client_max_body_size` in `/blobs` is below 16 MiB. |
# | Importing a FILIUS file fails with 413 (an HTML page, not F's JSON) | `client_max_body_size` in `/svc/` is below 64 MiB. |
# | Editor in the dashboard stays a white box | Something sets `X-Frame-Options` for this host. `curl -sI https://filius.hub.mdg-hamburg.de/` |