ipx-web: the page in its own container, behind Caddy (#203)
web/build.mjs builds a static site, web/dist, with the icons and typeface beside the pages; Dockerfile.web serves it with Caddy, which passes /api and /media to ipx on the same origin, so cookies, live updates and Cloudflare Access work as before. ipx serves no page any more: build.rs and every include_str! of the web files are gone, and its image is built without node. What ipx did to the page as it served it is kept another way. Caddy asks ipx before serving a page (forward_auth): /api/page-check sends a browser not signed in to /login and a non-admin away from /admin, and refuses them /admin.js; /api/login-check sends someone Access vouches for past the form; a page asked for with ?token= goes to /api/signin, which sets the cookie. The theme and the admin link, once written into the page per person, are set by web/boot.js in <head> from the theme cookie and the last visit, before the first frame, and /api/me brings the version and the account's theme from before. Caddy strips the Cf-Access-* headers from every peer but TUNNEL_PEER, the address cloudflared's requests come from, which is the boundary trusted_proxies drew; ipx then trusts only Caddy's address. The browser suite runs Caddy in front of its daemon, so it tests what production would run. Not deployed: production's cutover is on #203. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
75
web/Caddyfile
Normal file
75
web/Caddyfile
Normal file
@@ -0,0 +1,75 @@
|
||||
# ipx-web (#203): the frontend as static files, and everything else passed to ipx, on one origin,
|
||||
# so the session cookie, live updates and Cloudflare Access work as they did when ipx served it.
|
||||
# IPX_UPSTREAM where ipx is, host:port (ipx:8099)
|
||||
# WEB_BIND where to listen (:8080)
|
||||
# TUNNEL_PEER the one address whose Cf-Access-* headers are passed on: where cloudflared's
|
||||
# requests come from. Nobody else's are, so nobody else can claim to be signed in
|
||||
# by Cloudflare. Unset passes none.
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
http://{$WEB_BIND::8080} {
|
||||
root * {$WEB_ROOT:/srv}
|
||||
|
||||
# Before anything else, so no page or API call sees them: only ipx sets these.
|
||||
@forged not remote_ip {$TUNNEL_PEER:255.255.255.255}
|
||||
request_header @forged -Cf-Access-Authenticated-User-Email
|
||||
request_header @forged -Cf-Access-Jwt-Assertion
|
||||
|
||||
@api path /api/* /media/*
|
||||
handle @api {
|
||||
# Live updates are a stream: flushed as they come, never buffered.
|
||||
reverse_proxy {$IPX_UPSTREAM:ipx:8099} {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
# Signing in with the shared token in the address (?token=): ipx sets the session cookie and
|
||||
# sends the browser on to the page without it.
|
||||
@tokened {
|
||||
path / /admin
|
||||
query token=*
|
||||
}
|
||||
handle @tokened {
|
||||
rewrite * /api/signin?{query}
|
||||
reverse_proxy {$IPX_UPSTREAM:ipx:8099}
|
||||
}
|
||||
|
||||
# The app and the admin page go to whoever ipx says may have them: a browser not signed in is
|
||||
# sent to /login, anyone but an admin asking for /admin to the app.
|
||||
@page path / /index.html /admin /admin.html /admin.js
|
||||
handle @page {
|
||||
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
||||
uri /api/page-check
|
||||
}
|
||||
rewrite / /index.html
|
||||
rewrite /admin /admin.html
|
||||
header Cache-Control no-cache
|
||||
file_server
|
||||
}
|
||||
|
||||
# The sign-in form, except for someone Cloudflare Access vouches for, who is sent to the app.
|
||||
@login path /login /login.html
|
||||
handle @login {
|
||||
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
||||
uri /api/login-check
|
||||
}
|
||||
rewrite * /login.html
|
||||
header Cache-Control no-cache
|
||||
file_server
|
||||
}
|
||||
|
||||
# Scripts and the stylesheet are named by a hash of their contents (web/build.mjs), so a browser
|
||||
# keeps them a year; the icons a day, under names that do not change.
|
||||
@hashed path /app.js /admin.js /login.js /app.css /hls.js /boot.js
|
||||
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
||||
# The same PNG as /favicon.png, for a browser that asks for /favicon.ico on its own.
|
||||
header /favicon.ico Content-Type image/png
|
||||
@icons path *.png *.svg *.ico *.woff2
|
||||
header @icons Cache-Control "public, max-age=86400"
|
||||
handle {
|
||||
file_server
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user