fix(honeybar): Ursprung zur Laufzeit statt beim Bauen

Die Adresse im <script>-Tag stand fest in der index.html, waehrend die CSP
aus HONEYBAR_ORIGIN kam. Wer die Leiste woanders betreibt, bekam genau den
Widerspruch: die CSP erlaubte den eigenen Ursprung, geladen wurde der alte.

Der erste Versuch, das ueber ein Bau-Argument zu loesen, half beim fertigen
Abbild nicht — dort ist der Bau vorbei, und ein Umzug haette einen Neubau
verlangt. Ursprung und Schluessel stehen jetzt in /api/v1/public/config, die
Oberflaeche haengt das Skript selbst ein. Beides speist sich aus derselben
Variablen wie die CSP, sie koennen also nicht auseinanderlaufen, und eine
Aenderung wirkt beim Neustart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-24 23:07:35 +02:00
1 parent 6c7eb348b0
commit 57156a6394
11 files changed
+74 -67

No files matched your search

+12
View File
@@ -374,6 +374,12 @@ func main() {
faviconURL = admin.AppIconURL(s["app_icon_storage_key"], "64") faviconURL = admin.AppIconURL(s["app_icon_storage_key"], "64")
appleTouchURL = admin.AppIconURL(s["app_icon_storage_key"], "192") appleTouchURL = admin.AppIconURL(s["app_icon_storage_key"], "192")
} }
// Ohne Ursprung gibt es keine Leiste — dann hat auch der Schluessel
// nichts auf der Seite zu suchen.
honeybarKey := ""
if cfg.HoneybarOrigin != "" {
honeybarKey = cfg.HoneybarKey
}
apps := cfg.ExternalApps apps := cfg.ExternalApps
if apps == nil { if apps == nil {
apps = []config.ExternalApp{} // nie null – das Frontend erwartet eine Liste apps = []config.ExternalApp{} // nie null – das Frontend erwartet eine Liste
@@ -398,6 +404,12 @@ func main() {
// ist – sonst gibt der Endpunkt eine Einstellung preis, die // ist – sonst gibt der Endpunkt eine Einstellung preis, die
// niemanden etwas angeht. // niemanden etwas angeht.
"oidc_button_label": oidcLabel, "oidc_button_label": oidcLabel,
// Die HoneyOS-Leiste laedt die Oberflaeche selbst nach. Zur
// Laufzeit statt beim Bauen, damit ein fertiges Abbild ueber die
// .env umgezogen werden kann — und damit derselbe Ursprung
// gilt, den die CSP erlaubt.
"honeybar_origin": cfg.HoneybarOrigin,
"honeybar_key": honeybarKey,
}) })
}) })
+15 -2
View File
@@ -83,9 +83,21 @@ type Config struct {
// (z. B. https://os.example.com). Er wird in die CSP aufgenommen — ohne das // (z. B. https://os.example.com). Er wird in die CSP aufgenommen — ohne das
// blockt der Browser das Skript und dessen Anfragen. // blockt der Browser das Skript und dessen Anfragen.
// //
// Muss zur Adresse im <script>-Tag der index.html passen. Leer heißt: keine // Derselbe Wert geht über /api/v1/public/config an die Oberfläche, die das
// Leiste, und die CSP bleibt so eng wie bisher. // Skript daraufhin selbst nachlädt. Beides aus einer Quelle: so kann die
// CSP nicht einen anderen Ursprung erlauben als den geladenen.
//
// Leer heißt: keine Leiste, und die CSP bleibt so eng wie bisher.
HoneybarOrigin string HoneybarOrigin string
// HoneybarKey ist der Ausweis, mit dem sich die Leiste bei HoneyOS die
// App-Liste holt (dort HONEYOS_APPS_KEY). Nötig, wenn HoneyOS auf einer
// anderen Site liegt als Aether: dann schickt der Browser die
// HoneyOS-Sitzung nicht mit.
//
// Er steht anschließend im Quelltext der Seite — das ist unvermeidlich,
// die Leiste läuft im Browser. Er gibt nur die App-Liste her, nichts
// Schreibendes.
HoneybarKey string
// OfficeWopiURL ist die Adresse, unter der der **Office-Server** Aether // OfficeWopiURL ist die Adresse, unter der der **Office-Server** Aether
// erreicht. // erreicht.
// //
@@ -189,6 +201,7 @@ func Load() (*Config, error) {
OfficeURL: strings.TrimRight(getEnv("OFFICE_URL", ""), "/"), OfficeURL: strings.TrimRight(getEnv("OFFICE_URL", ""), "/"),
OfficePublicURL: strings.TrimRight(getEnv("OFFICE_PUBLIC_URL", ""), "/"), OfficePublicURL: strings.TrimRight(getEnv("OFFICE_PUBLIC_URL", ""), "/"),
HoneybarOrigin: strings.TrimRight(getEnv("HONEYBAR_ORIGIN", ""), "/"), HoneybarOrigin: strings.TrimRight(getEnv("HONEYBAR_ORIGIN", ""), "/"),
HoneybarKey: getEnv("HONEYBAR_KEY", ""),
OfficeWopiURL: strings.TrimRight(getEnv("OFFICE_WOPI_URL", ""), "/"), OfficeWopiURL: strings.TrimRight(getEnv("OFFICE_WOPI_URL", ""), "/"),
OfficeAllowedHosts: splitList(getEnv("OFFICE_ALLOWED_HOSTS", "")), OfficeAllowedHosts: splitList(getEnv("OFFICE_ALLOWED_HOSTS", "")),
OfficeInsecureTLS: getEnv("OFFICE_INSECURE_TLS", "false") == "true", OfficeInsecureTLS: getEnv("OFFICE_INSECURE_TLS", "false") == "true",
+11 -11
View File
@@ -65,19 +65,19 @@ ADMIN_PASSWORD=changeme_strong_admin_password
# OIDC_BUTTON_LABEL=Anmelden mit PocketID # OIDC_BUTTON_LABEL=Anmelden mit PocketID
# ── HoneyOS-Leiste ──────────────────────────────────────────────────────────── # ── HoneyOS-Leiste ────────────────────────────────────────────────────────────
# Ursprung, von dem die Leiste geladen wird. Landet an zwei Stellen: beim # Ursprung, von dem die Leiste geladen wird. Wird an zwei Stellen gebraucht und
# Bauen im <script>-Tag der index.html, zur Laufzeit in der CSP # kommt deshalb aus einer Quelle: die CSP erlaubt ihn (script-src +
# (script-src + connect-src). Leer = keine Leiste, CSP so eng wie ohne sie. # connect-src), und die Oberflaeche laedt das Skript von dort nach.
# #
# Wird beim **Bauen** eingesetzt — nach einer Änderung also # Wirkt beim **Neustart** — kein Neubau noetig.
# `docker compose build app`, ein Neustart genügt nicht. # Leer = keine Leiste, und die CSP bleibt so eng wie ohne sie.
HONEYBAR_ORIGIN=https://honeyos.example.com HONEYBAR_ORIGIN=https://os.example.com
# Schlüssel, mit dem sich die Leiste die App-Liste holt. Nötig, weil HoneyOS auf # Ausweis, mit dem sich die Leiste die App-Liste holt. Muss zu HONEYOS_APPS_KEY
# einer anderen Site liegt als Aether und der Browser die HoneyOS-Sitzung dann # drueben passen. Nur noetig, wenn HoneyOS auf einer anderen Site liegt als
# nicht mitschickt. Muss zu HONEYOS_APPS_KEY drüben passen. # Aether — dann schickt der Browser die HoneyOS-Sitzung nicht mit.
# #
# Wird beim **Bauen** in die index.html eingesetzt — nach einer Änderung also # Gleiche Site (os.beispiel.duckdns.org + cloud.beispiel.duckdns.org): kann leer
# `docker compose build app`, ein Neustart genügt nicht. # bleiben, die Sitzung reicht.
HONEYBAR_KEY= HONEYBAR_KEY=
# ── Volltextsuche (OCR) ─────────────────────────────────────────────────────── # ── Volltextsuche (OCR) ───────────────────────────────────────────────────────
-7
View File
@@ -4,13 +4,6 @@ WORKDIR /app/frontend
COPY frontend/package*.json ./ COPY frontend/package*.json ./
RUN npm ci RUN npm ci
COPY frontend/ ./ COPY frontend/ ./
# Ursprung und Schlüssel für die HoneyOS-Leiste. Vite setzt beides in die
# index.html ein. Ohne Ursprung faellt der Tag weg (keine Leiste), ohne
# Schlüssel bekommt die Leiste keine App-Liste — Aether bleibt unberührt.
ARG HONEYBAR_ORIGIN=""
ARG HONEYBAR_KEY=""
ENV VITE_HONEYBAR_ORIGIN=$HONEYBAR_ORIGIN
ENV VITE_HONEYBAR_KEY=$HONEYBAR_KEY
RUN npm run build RUN npm run build
# ── Stage 2: Backend build ──────────────────────────────────────────────────── # ── Stage 2: Backend build ────────────────────────────────────────────────────
-4
View File
@@ -16,10 +16,6 @@ services:
# frontend/, nicht nur diesen Ordner. # frontend/, nicht nur diesen Ordner.
context: .. context: ..
dockerfile: deploy/Dockerfile dockerfile: deploy/Dockerfile
args:
# Wird in die index.html eingebacken, siehe dort.
HONEYBAR_ORIGIN: ${HONEYBAR_ORIGIN:-}
HONEYBAR_KEY: ${HONEYBAR_KEY:-}
image: gitea.honeyy.duckdns.org/docker/aether:latest image: gitea.honeyy.duckdns.org/docker/aether:latest
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
-22
View File
@@ -19,27 +19,5 @@
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<!-- HoneyOS-Leiste: der Umschalter zwischen den eigenen Apps. Sie rendert
in einem Shadow DOM, Aethers Stile berührt sie nicht.
Sie steht hier statt serverseitig eingespritzt, weil der Service Worker
die index.html in seinen Vorrat legt (navigateFallback) — was zur
Bauzeit nicht drinsteht, käme beim Browser nie an.
`defer`: ohne wartet der Seitenaufbau auf das Skript. Ist HoneyOS
langsam oder aus, darf das Aether nicht ausbremsen.
Ursprung und Schlüssel kommen beide aus der .env (HONEYBAR_ORIGIN,
HONEYBAR_KEY) und werden beim Bauen eingesetzt. Derselbe Ursprung
landet zur Laufzeit in der CSP — so können die beiden nicht
auseinanderlaufen. Ist er leer, entfernt vite.config.ts diesen Tag
ganz; Aether läuft dann ohne Leiste weiter. HoneyOS liegt auf einer anderen Site als Aether
(duckdns.org ist ein public suffix), deshalb schickt der Browser die
HoneyOS-Sitzung nicht mit — ohne diesen zweiten Ausweis bekäme die
Leiste nie eine App-Liste. Der Schlüssel steht bewusst nicht im
Quelltext: er käme sonst ins Repo und liesse sich nur mit einer
Code-Änderung wechseln. -->
<script defer src="%VITE_HONEYBAR_ORIGIN%/honeybar/honeybar.js"
data-app="aether" data-key="%VITE_HONEYBAR_KEY%"></script>
</body> </body>
</html> </html>
+5
View File
@@ -231,6 +231,11 @@ export interface PublicConfig {
oidc_button_label?: string oidc_button_label?: string
primary_color?: string primary_color?: string
accent_color?: string accent_color?: string
/** Ursprung der HoneyOS-Leiste (HONEYBAR_ORIGIN). Leer = keine Leiste. */
honeybar_origin?: string
/** Ausweis fuer deren App-Liste (HONEYBAR_KEY). Nur gesetzt, wenn es einen
* Ursprung gibt. */
honeybar_key?: string
external_apps: ExternalApp[] external_apps: ExternalApp[]
} }
+28
View File
@@ -0,0 +1,28 @@
/**
* Die HoneyOS-Leiste nachladen.
*
* Zur Laufzeit statt beim Bauen: Der Ursprung steht in der .env des Servers,
* ein fertiges Abbild laesst sich damit umziehen, ohne es neu zu bauen. Frueher
* stand die Adresse fest in der index.html — wer die Leiste woanders betrieb,
* bekam einen CSP-Verstoss, weil der erlaubte und der geladene Ursprung
* auseinanderliefen. Beides kommt jetzt aus HONEYBAR_ORIGIN.
*
* Faellt der Server aus oder ist kein Ursprung gesetzt, passiert schlicht
* nichts: Aether ist nicht auf die Leiste angewiesen.
*/
export function mountHoneybar(origin?: string, key?: string) {
if (!origin) return
// Der Aufruf haengt an der Konfiguration, und die wird auch neu geladen,
// wenn der Admin etwas aendert. Zweimal einbinden hiesse zwei Leisten.
if (document.querySelector('script[data-honeybar]')) return
const el = document.createElement('script')
el.dataset.honeybar = ''
el.defer = true
el.src = origin.replace(/\/+$/, '') + '/honeybar/honeybar.js'
// honeybar.js liest seine Einstellungen ueber document.currentScript –
// das gilt auch fuer ein nachtraeglich eingehaengtes Skript.
el.dataset.app = 'aether'
if (key) el.dataset.key = key
document.head.appendChild(el)
}
+2
View File
@@ -1,3 +1,4 @@
import { mountHoneybar } from '../honeybar'
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react' import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'
import { publicApi } from '../api/client' import { publicApi } from '../api/client'
import { applyTheme } from '../theme' import { applyTheme } from '../theme'
@@ -103,6 +104,7 @@ export function AppSettingsProvider({ children }: { children: ReactNode }) {
if (typeof cfg.max_versions_kept === 'number' && cfg.max_versions_kept > 0) { if (typeof cfg.max_versions_kept === 'number' && cfg.max_versions_kept > 0) {
setMaxVersionsKept(cfg.max_versions_kept) setMaxVersionsKept(cfg.max_versions_kept)
} }
mountHoneybar(cfg.honeybar_origin, cfg.honeybar_key)
applyTheme(cfg.primary_color, cfg.accent_color) applyTheme(cfg.primary_color, cfg.accent_color)
applyThemeColor(cfg.primary_color) applyThemeColor(cfg.primary_color)
document.title = cfg.cloud_name || 'Aether' document.title = cfg.cloud_name || 'Aether'
+1
View File
@@ -0,0 +1 @@
{"root":["./src/App.tsx","./src/honeybar.ts","./src/main.tsx","./src/test-setup.ts","./src/theme.ts","./src/api/client.ts","./src/components/AppPasswords.tsx","./src/components/AppSwitcher.tsx","./src/components/Breadcrumb.tsx","./src/components/ConfirmDialog.tsx","./src/components/ContextMenu.tsx","./src/components/Dialog.tsx","./src/components/FileCover.tsx","./src/components/FileInfoDialog.tsx","./src/components/FilePreview.tsx","./src/components/FileViews.tsx","./src/components/MoveTo.tsx","./src/components/OfficeEditor.tsx","./src/components/PageHeader.tsx","./src/components/PdfViewer.tsx","./src/components/SearchBox.tsx","./src/components/ShareDialog.tsx","./src/components/ShareWithDialog.tsx","./src/components/Shell.tsx","./src/components/Sidebar.tsx","./src/components/Tabs.tsx","./src/components/TagChip.tsx","./src/components/TagDialog.tsx","./src/components/TagEditDialog.tsx","./src/components/UploadQueue.tsx","./src/components/UserMenu.tsx","./src/components/VersionsDialog.tsx","./src/components/WebDavGuide.tsx","./src/components/filetypes.ts","./src/components/icons.tsx","./src/hooks/useAppSettings.tsx","./src/hooks/useAuth.ts","./src/hooks/useInternalDrag.ts","./src/hooks/useNavHistory.ts","./src/hooks/useOverlayDismiss.ts","./src/pages/AdminPage.tsx","./src/pages/FileBrowser.tsx","./src/pages/LoginPage.tsx","./src/pages/ProfilePage.tsx","./src/pages/SharePage.tsx","./src/pages/SharedPage.tsx","./src/pages/SharedWithMePage.tsx","./src/pages/TagsPage.tsx","./src/pages/TrashPage.tsx","./src/pages/admin/AuditTab.tsx","./src/pages/admin/GroupsTab.tsx","./src/pages/admin/ThemingTab.tsx","./src/pages/admin/UsersTab.tsx","./src/pages/admin/ui.tsx"],"version":"6.0.3"}
-21
View File
@@ -32,30 +32,9 @@ function pdfjsStandardFonts(): Plugin {
} }
} }
/**
* Den <script>-Tag der HoneyOS-Leiste entfernen, wenn kein Ursprung gesetzt ist.
*
* Ohne das bliebe nach der Ersetzung src="/honeybar/honeybar.js" stehen: der
* Service Worker beantwortet die Navigation mit index.html, der Browser meldet
* einen MIME-Fehler. Kein Ursprung heisst schlicht keine Leiste.
*/
function honeybar(): Plugin {
return {
name: 'aether-honeybar',
transformIndexHtml: {
order: 'pre',
handler(html) {
if (process.env.VITE_HONEYBAR_ORIGIN?.trim()) return html
return html.replace(/\s*<script[^>]*honeybar\.js[^>]*><\/script>/, '')
},
},
}
}
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
pdfjsStandardFonts(), pdfjsStandardFonts(),
honeybar(),
react(), react(),
tailwindcss(), tailwindcss(),
VitePWA({ VitePWA({