# [matrix #23] SPA routes (/, /home/…, /login/…) are rewritten to /index.html # inside `location /`, where no cache headers were set: browsers then reused a # heuristically fresh shell after a deploy and requested deleted assets. Only # the rewritten shell gets no-cache; other files there keep their defaults. map $uri $lotus_shell_cache_control { /index.html "no-cache, no-store, must-revalidate"; default ""; } # All traffic arrives through Nginx Proxy Manager (10.10.10.27), so without # realip every visitor shared ONE chat_limit bucket (20 r/s, keyed by # $binary_remote_addr = the proxy): a scanner burst got real users 503s. # Each server block below trusts X-Real-IP from the proxy only (NPM sets it # to the client address, overwriting anything the client sent). # # [matrix #22] Decorations get their own, larger budget: opening the profile # decoration picker can fetch hundreds of thumbnails, which would otherwise use # up chat_limit and 503 the app's own files. limit_req_zone $binary_remote_addr zone=decorations_limit:10m rate=100r/s; server { listen 80; listen [::]:80; server_name chat.lotusguild.org; # Brotli compression (better than gzip for modern browsers) brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/plain text/css application/javascript application/json image/svg+xml application/wasm font/woff2; root /var/www/html; server_tokens off; client_max_body_size 50m; set_real_ip_from 10.10.10.27; real_ip_header X-Real-IP; limit_req zone=chat_limit burst=60 nodelay; limit_conn chat_conn 25; index index.html; # Security headers (incl. CSP) — see the snippet include snippets/cinny-security-headers.conf; # HSTS: TLS terminates upstream (this server is listen 80), so this reaches # the browser only if the front proxy passes upstream response headers # through; otherwise set it at the TLS terminator. includeSubDomains covers # all *.lotusguild.org (all HTTPS); `preload` is inert until submitted to # hstspreload.org. # Permissions-Policy: allow only what the app uses (self) — calls # (camera/microphone/display-capture), location share (geolocation), sounds # (autoplay), Element Call (fullscreen/encrypted-media) — and deny the rest. # Block all source map files and dotfiles from public access location ~* \.(js|css)\.map$ { deny all; return 404; } location ~ /\. { deny all; return 404; } location = /netlify.toml { deny all; return 404; } # Service worker must never be cached so updates are picked up immediately location = /sw.js { include snippets/cinny-security-headers.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } # Bundled Element Call: framed by the app itself, so it must not carry the # app CSP (frame-ancestors 'none' blocked every web call). Same caching as # below: HTML never cached, hashed assets for a year. location ^~ /public/element-call/ { include snippets/cinny-security-headers-framed.conf; location ~* \.html$ { include snippets/cinny-security-headers-framed.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } location ~* \.(?:js|css|woff2?|png|svg|ico|webp|wasm)$ { include snippets/cinny-security-headers-framed.conf; expires 1y; add_header Cache-Control "public, immutable" always; } } # Cache content-addressed static assets aggressively location ~* \.(?:js|css|woff2?|png|svg|ico|webp)$ { include snippets/cinny-security-headers.conf; expires 1y; add_header Cache-Control "public, immutable" always; } # Never cache HTML or JSON (index.html, config.json, manifest.json) location ~* \.(json|html)$ { include snippets/cinny-security-headers.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } # [Gitea #155] PWA share target. The service worker normally answers this # POST itself; if it isn't controlling the page yet, land on /share # (the shared files are lost, but nothing 405s). location = /share-target { absolute_redirect off; return 303 /share; } # Auto-deploy webhook — proxied to local webhook service location = /hooks/lotus-deploy { proxy_pass http://127.0.0.1:9001/hooks/lotus-deploy; proxy_set_header Host $host; proxy_read_timeout 300; proxy_connect_timeout 5; } # [matrix #22] Avatar decorations, installed by deploy/lxc106-cinny.sh from # the Gitea generic package (outside the web root, so app deploys' # rsync --delete never touches them). ^~ beats the image regex below: these # can be replaced in place under the same name, so a week, not immutable. location ^~ /decorations/ { alias /var/www/decorations/; # Replaces the server-level chat_limit here (nginx doesn't inherit # limit_req into a location that sets its own). limit_req zone=decorations_limit burst=700 nodelay; include snippets/cinny-security-headers.conf; add_header Cache-Control "public, max-age=604800" always; location ~ /MANIFEST\.sha256$ { return 404; } } location / { # add_header here drops the server-level headers, so include them again. include snippets/cinny-security-headers.conf; add_header Cache-Control $lotus_shell_cache_control always; rewrite ^/config\.json$ /config.json break; rewrite ^/manifest\.json$ /manifest.json break; rewrite ^/sw\.js$ /sw.js break; rewrite ^/pdf\.worker\.min\.js$ /pdf.worker.min.js break; rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/assets/(.*)$ /assets/$1 break; rewrite ^(.+)$ /index.html break; } } # [cinny #43] Element Call on its own origin. The web app frames # https://call.chat.lotusguild.org/public/element-call/index.html (config.json # `elementCallUrl`), so the call page can no longer read the app's storage # (login token, crypto store) or use its service worker. Serves ONLY the call # page — the same files as chat.lotusguild.org/public/element-call/ — and 404s # everything else, so this hostname exposes nothing new. server { listen 80; listen [::]:80; server_name call.chat.lotusguild.org; brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/plain text/css application/javascript application/json image/svg+xml application/wasm font/woff2; root /var/www/html; server_tokens off; set_real_ip_from 10.10.10.27; real_ip_header X-Real-IP; limit_req zone=chat_limit burst=60 nodelay; limit_conn chat_conn 25; include snippets/cinny-security-headers-call.conf; location ^~ /public/element-call/ { include snippets/cinny-security-headers-call.conf; location ~* \.map$ { return 404; } location ~ /\. { return 404; } location ~* \.html$ { include snippets/cinny-security-headers-call.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } location ~* \.(?:js|css|woff2?|png|svg|ico|webp|wasm)$ { include snippets/cinny-security-headers-call.conf; expires 1y; add_header Cache-Control "public, immutable" always; } try_files $uri =404; } location / { return 404; } }