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")
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
if apps == nil {
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
// niemanden etwas angeht.
"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
// blockt der Browser das Skript und dessen Anfragen.
//
// Muss zur Adresse im <script>-Tag der index.html passen. Leer heißt: keine
// Leiste, und die CSP bleibt so eng wie bisher.
// Derselbe Wert geht über /api/v1/public/config an die Oberfläche, die das
// 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
// 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
// erreicht.
//
@@ -189,6 +201,7 @@ func Load() (*Config, error) {
OfficeURL: strings.TrimRight(getEnv("OFFICE_URL", ""), "/"),
OfficePublicURL: strings.TrimRight(getEnv("OFFICE_PUBLIC_URL", ""), "/"),
HoneybarOrigin: strings.TrimRight(getEnv("HONEYBAR_ORIGIN", ""), "/"),
HoneybarKey: getEnv("HONEYBAR_KEY", ""),
OfficeWopiURL: strings.TrimRight(getEnv("OFFICE_WOPI_URL", ""), "/"),
OfficeAllowedHosts: splitList(getEnv("OFFICE_ALLOWED_HOSTS", "")),
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
# ── HoneyOS-Leiste ────────────────────────────────────────────────────────────
# Ursprung, von dem die Leiste geladen wird. Landet an zwei Stellen: beim
# Bauen im <script>-Tag der index.html, zur Laufzeit in der CSP
# (script-src + connect-src). Leer = keine Leiste, CSP so eng wie ohne sie.
# Ursprung, von dem die Leiste geladen wird. Wird an zwei Stellen gebraucht und
# kommt deshalb aus einer Quelle: die CSP erlaubt ihn (script-src +
# connect-src), und die Oberflaeche laedt das Skript von dort nach.
#
# Wird beim **Bauen** eingesetzt — nach einer Änderung also
# `docker compose build app`, ein Neustart genügt nicht.
HONEYBAR_ORIGIN=https://honeyos.example.com
# Schlüssel, mit dem sich die Leiste die App-Liste holt. Nötig, weil HoneyOS auf
# einer anderen Site liegt als Aether und der Browser die HoneyOS-Sitzung dann
# nicht mitschickt. Muss zu HONEYOS_APPS_KEY drüben passen.
# Wirkt beim **Neustart** — kein Neubau noetig.
# Leer = keine Leiste, und die CSP bleibt so eng wie ohne sie.
HONEYBAR_ORIGIN=https://os.example.com
# Ausweis, mit dem sich die Leiste die App-Liste holt. Muss zu HONEYOS_APPS_KEY
# drueben passen. Nur noetig, wenn HoneyOS auf einer anderen Site liegt als
# Aether — dann schickt der Browser die HoneyOS-Sitzung nicht mit.
#
# Wird beim **Bauen** in die index.html eingesetzt — nach einer Änderung also
# `docker compose build app`, ein Neustart genügt nicht.
# Gleiche Site (os.beispiel.duckdns.org + cloud.beispiel.duckdns.org): kann leer
# bleiben, die Sitzung reicht.
HONEYBAR_KEY=
# ── Volltextsuche (OCR) ───────────────────────────────────────────────────────
-7
View File
@@ -4,13 +4,6 @@ WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
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
# ── Stage 2: Backend build ────────────────────────────────────────────────────
-4
View File
@@ -16,10 +16,6 @@ services:
# frontend/, nicht nur diesen Ordner.
context: ..
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
restart: unless-stopped
env_file: .env
-22
View File
@@ -19,27 +19,5 @@
<div id="root"></div>
<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>
</html>
+5
View File
@@ -231,6 +231,11 @@ export interface PublicConfig {
oidc_button_label?: string
primary_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[]
}
+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 { publicApi } from '../api/client'
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) {
setMaxVersionsKept(cfg.max_versions_kept)
}
mountHoneybar(cfg.honeybar_origin, cfg.honeybar_key)
applyTheme(cfg.primary_color, cfg.accent_color)
applyThemeColor(cfg.primary_color)
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({
plugins: [
pdfjsStandardFonts(),
honeybar(),
react(),
tailwindcss(),
VitePWA({