diff --git a/README.md b/README.md index b4a5ef5..83a4402 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,9 @@ matrix/ - Monitor state: `/var/lib/cinny-monitor/last-upstream-commit` - Monitor log: `/var/log/cinny-monitor.log` - Build log: `/var/log/cinny-build.log` -- Nginx site config: `/etc/nginx/sites-available/cinny` +- Nginx site config: `/etc/nginx/sites-available/cinny` (copy in this repo: `cinny/nginx.conf`, synced with live on 2026-09-23) +- Nginx security headers: `/etc/nginx/snippets/cinny-security-headers.conf` (`cinny/nginx-security-headers.conf`). Included at server level **and** in every `location` that sets its own `add_header`; nginx drops inherited `add_header`s in such blocks, which left static assets without `nosniff` and `/sw.js` without its CSP (cinny #210). Edit the CSP here, not in the site config. +- ⚠️ The `matrix-deploy` hook on 106 has been unreachable since May (webhook bound to `127.0.0.1:9000`), so edits to these files in the repo are **not** auto-deployed there. Apply them by hand (`nginx -t` then `nginx -s reload`) and keep the repo copy in sync. --- @@ -535,7 +537,7 @@ All custom code lives in `src/app/` on the `lotus` branch of `code.lotusguild.or | **PiP position persistence + snap** | `src/app/components/CallEmbedProvider.tsx` | PiP position saved to `localStorage` on drag end; restored on next PiP enter (clamped to viewport). Double-click snaps to nearest corner with 180ms CSS transition | | **Threads (P3-8 + P4-1)** | `src/app/features/room/thread/`, `state/room/thread.ts`, `utils/threadNotifications.ts`, `hooks/useRoomsListener.ts` | Full m.thread support: side panel (own composer, per-thread drafts), "N replies" unread chips, threaded receipts; SDK `threadSupport` on, markAsRead unthreaded; replies no longer render inline. **Slack-style notifications**: default = participating-only, per-thread All/Mentions/Mute in `io.lotus.thread_notifications` account data; muted threads subtracted from room badges client-side | | **KaTeX math + encrypted-search cache + session hardening + crypto diagnostics** | `utils/{mathParse,searchCache,cryptoDiagLog}.ts`, `state/sessions.ts`, `LOTUS_E2EE_INVESTIGATION.md` | July 2026 batch: `$…$`/`$$…$$` + `data-mx-maths` via lazy KaTeX; opt-in IndexedDB search index for E2EE rooms (wiped on logout); atomic `cinny_session_v1` blob + cross-tab logout sync; KE-1→4 diagnostics capture card in Developer Tools | -| **Inline media embeds** | `src/app/utils/videoEmbed.ts`, `src/app/components/url-preview/{UrlPreviewCard,UrlPreview.css}.tsx` | Media links play/render **in place** behind a click-to-play **facade** (homeserver `og:image` thumbnail; third-party iframe mounts only on Play). 16 providers: video (YouTube/Shorts, Vimeo, Dailymotion, Streamable, Twitch, Loom, Kick), audio (Spotify, SoundCloud, Apple Music, Tidal), self-resizing posts (X/Twitter, Instagram, Reddit, Bluesky). TikTok short links resolve via CORS `oEmbed`; posts self-size via origin-scoped `postMessage`. Sandbox omits `allow-top-navigation`; previews capped at 6/msg. Setting `inlineMediaEmbeds`. **CSP:** every embed host is enumerated in the desktop Tauri `frame-src` (`cinny-desktop/tauri.conf.json`) **and** the deployed web nginx `frame-src` allowlist on LXC 106 (`/etc/nginx/sites-available/cinny` — hand-maintained; the wildcard `frame-src 'self' https:` in this repo's `cinny/nginx.conf` is the looser fallback) | +| **Inline media embeds** | `src/app/utils/videoEmbed.ts`, `src/app/components/url-preview/{UrlPreviewCard,UrlPreview.css}.tsx` | Media links play/render **in place** behind a click-to-play **facade** (homeserver `og:image` thumbnail; third-party iframe mounts only on Play). 16 providers: video (YouTube/Shorts, Vimeo, Dailymotion, Streamable, Twitch, Loom, Kick), audio (Spotify, SoundCloud, Apple Music, Tidal), self-resizing posts (X/Twitter, Instagram, Reddit, Bluesky). TikTok short links resolve via CORS `oEmbed`; posts self-size via origin-scoped `postMessage`. Sandbox omits `allow-top-navigation`; previews capped at 6/msg. Setting `inlineMediaEmbeds`. **CSP:** every embed host is enumerated in the desktop Tauri `frame-src` (`cinny-desktop/tauri.conf.json`) **and** the deployed web nginx `frame-src` allowlist on LXC 106 (`/etc/nginx/snippets/cinny-security-headers.conf` on LXC 106; repo copy `cinny/nginx-security-headers.conf`) | | **On-device message translation** | `src/app/features/room/message/`, `src/app/hooks/useMessageTranslation.ts`, `src/app/state/settings.ts` | "Translate" button on foreign-language messages renders the translation inline with a "Translated from <lang> · Show original" toggle + optional auto-translate mode. Runs **100% on-device** via the browser's built-in Translator API (Chromium: Chrome/Edge + Lotus desktop app) — message text never leaves the device and never reaches any cloud translation service (Google/DeepL/Microsoft), so it works in **E2EE rooms** without weakening encryption. Target language in Settings → General (default English). Feature-detected — gracefully hidden where unsupported (Firefox/Safari/mobile) | | **Desktop app (Tauri)** | `cinny-desktop` → `src-tauri/src/native/*.rs`, `src-tauri/src/lib.rs`; cinny `src/app/hooks/useTauri*.ts`, `src/app/components/TauriDesktopFeatures.tsx` | Tauri v2 native shell: rich WinRT toast notifications (click → open room, inline quick reply), Windows Focus Assist → DND sync, taskbar Jump List of recent rooms, taskbar thumbnail + volume-flyout call controls (mute/deafen/end), no-sleep during calls, network-change awareness (`mx.retryImmediately`), opt-in TDS window chrome, recursive folder drag-drop, auto-update toast. Windows-native pieces compile in CI (Gitea `windows` runner + GitHub `windows-latest`); detail in cinny `LOTUS_FEATURES.md` → Desktop App Features | | **LiveKit codec config** | `/etc/livekit/config.yaml` (LXC 151) | `enabled_codecs`: VP8, H264, VP9, Opus, RED for better quality and redundancy | diff --git a/cinny/nginx-security-headers.conf b/cinny/nginx-security-headers.conf new file mode 100644 index 0000000..05577f7 --- /dev/null +++ b/cinny/nginx-security-headers.conf @@ -0,0 +1,12 @@ +# Security headers for chat.lotusguild.org (matrix repo: cinny/nginx-security-headers.conf). +# Included at server level AND in every location that sets its own add_header: +# nginx drops inherited add_header directives in any block that defines one, +# so without the include, static assets lost nosniff and /sw.js lost its CSP +# (a service worker's CSP comes from its own script response). cinny #210. +add_header X-Frame-Options SAMEORIGIN always; +add_header X-Content-Type-Options nosniff always; +add_header X-XSS-Protection "1; mode=block" always; +add_header Referrer-Policy strict-origin-when-cross-origin always; +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; +add_header Permissions-Policy "accelerometer=(), autoplay=(self), camera=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(), magnetometer=(), microphone=(self), midi=(), payment=(), usb=()" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://drive.lotusguild.org https://media.giphy.com https://media0.giphy.com https://media1.giphy.com https://media2.giphy.com https://media3.giphy.com https://media4.giphy.com https://www.openstreetmap.org https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' https://matrix.lotusguild.org wss://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im https://api.giphy.com https://*.giphy.com wss:; media-src 'self' https: blob:; frame-src 'self' https:; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always; diff --git a/cinny/nginx.conf b/cinny/nginx.conf index 1419a4c..fb311b5 100644 --- a/cinny/nginx.conf +++ b/cinny/nginx.conf @@ -18,21 +18,16 @@ server { limit_conn chat_conn 25; index index.html; - # Security headers - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy strict-origin-when-cross-origin always; + # 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. - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # 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. - add_header Permissions-Policy "accelerometer=(), autoplay=(self), camera=(self), display-capture=(self), encrypted-media=(self), fullscreen=(self), geolocation=(self), gyroscope=(), magnetometer=(), microphone=(self), midi=(), payment=(), usb=()" always; # Block all source map files and dotfiles from public access location ~* \.(js|css)\.map$ { @@ -48,27 +43,36 @@ server { return 404; } - # Content Security Policy - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://drive.lotusguild.org https://media.giphy.com https://media0.giphy.com https://media1.giphy.com https://media2.giphy.com https://media3.giphy.com https://media4.giphy.com https://www.openstreetmap.org https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' https://matrix.lotusguild.org wss://matrix.lotusguild.org https://matrix.org https://*.matrix.org https://mozilla.org https://mozilla.modular.im https://chat.mozilla.org https://vector.im https://api.giphy.com https://*.giphy.com wss:; media-src 'self' https: blob:; frame-src 'self' https:; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always; # 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; } # 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; @@ -77,13 +81,6 @@ server { proxy_connect_timeout 5; } - # [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 { - return 303 /share; - } - location / { rewrite ^/config\.json$ /config.json break; rewrite ^/manifest\.json$ /manifest.json break;