# 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) # WEB_TRACING on: a span for each request to OpenTelemetry (OTEL_EXPORTER_OTLP_ENDPOINT, # OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME), continuing the page's traceparent # and passing it to ipx, so one trace runs browser, Caddy, ipx (#213). Off: none, # and no export failing every few seconds where there is nothing to send to. # 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 # Everything else Caddy logs, its errors among them, which carry the request too: the same # filter as the access log's below. log default { output stderr format filter { wrap json fields { request>uri query { delete token } request>headers delete } } } } (tracing-on) { tracing { span ipx-web } } (tracing-off) { } http://{$WEB_BIND::8080} { import tracing-{$WEB_TRACING:off} root * {$WEB_ROOT:/srv} # One JSON line a request on stdout, for Loki and the dashboard's Page row (#210). Not the # shared token a ?token= link carries, nor the request's headers, which hold the session # cookie and Cloudflare Access's token. log { output stdout format filter { wrap json fields { request>uri query { delete token } request>headers delete } } } # Compressed, zstd or gzip as the browser takes them: the feed list and a page of items went # out at over 100 KB each (#239). Not the live updates, which a compressor would hold back # until it had enough to send, nor /media, which is audio and video already compressed. @compress not path /api/events /media/* encode @compress zstd gzip # The copies web/build.mjs compressed are served in place of their file, never asked for by # name: /admin.js.br would be the admin page's script without the check below that keeps it to # admins (#253). # A handle of its own: a bare respond runs after the handles, and the last one serves any file. @packed path *.br *.gz handle @packed { respond 404 } # 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 { precompressed br gzip } } # 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 { precompressed br gzip } } # 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 { precompressed br gzip } } }