diff --git a/.env.example b/.env.example index ec0c9ee..ec11456 100644 --- a/.env.example +++ b/.env.example @@ -117,14 +117,21 @@ OIDC_BUTTON_LABEL=Sign in with SSO / OIDC # sonst verweigert der Browser den Rahmen. OFFICE_PROVIDER=onlyoffice # collabora | onlyoffice (auch Euro-Office) OFFICE_URL=http://office # Backend → Office (Collabora: :9980) -# Browser → Office +# Browser → Office. **Muss eine Adresse sein, die der Browser auflösen kann** +# – deine Domain oder die LAN-IP, niemals "localhost" und nie der interne +# Dienstname. Sonst lädt der Editor am Rechner mit Docker und bleibt auf jedem +# anderen Gerät schwarz, ohne Fehlermeldung. Leer = wie OFFICE_URL, und das +# ist fast immer falsch. OFFICE_PUBLIC_URL= OFFICE_WOPI_URL=http://app:8080 # Office → Aether OFFICE_ALLOWED_HOSTS=office # gegen SSRF # Nur für ein Office mit selbstsigniertem Zertifikat im eigenen Netz. Schaltet # die Zertifikatsprüfung **ab** – im Internet nichts zu suchen. OFFICE_INSECURE_TLS=false -# nur OnlyOffice, openssl rand -hex 32 +# Liest **der Office-Container**, nicht Aether: Es schützt seine eigene API. +# Der WOPI-Weg braucht es nicht, der hat eigene Token. Bei OnlyOffice und +# Euro-Office mit JWT_ENABLED=true gehört es gesetzt, sonst steht deren API +# offen. Collabora kennt es nicht. openssl rand -hex 32 OFFICE_JWT_SECRET= # ══ Volltextsuche (OCR) ═══════════════════════════════════════════════════════ diff --git a/README.md b/README.md index 9c948e7..783ebb7 100644 --- a/README.md +++ b/README.md @@ -665,6 +665,13 @@ Container heraus er selbst. Der Port unterscheidet sich je nach Anbieter: **Collabora lauscht auf 9980, OnlyOffice und Euro-Office auf 80.** +> **Zur Portbindung in den Schnipseln:** Dort steht `${APP_BIND:-0.0.0.0}` – +> also standardmäßig **offen im Netz**, anders als bei Aether selbst +> (`127.0.0.1`). Das ist Absicht: Der Browser muss den Office-Server direkt +> erreichen, und über Loopback kann das kein zweites Gerät. Wer einen Reverse +> Proxy davor setzt – der saubere Weg –, trägt hier `127.0.0.1` ein und lässt +> den Proxy die öffentliche Adresse bedienen. + Die Schnipsel unten starten sofort mit `docker compose up -d`. Zum Abschalten `docker compose stop office`. Wer den Container lieber ganz aus der normalen Startfolge nimmt, hängt `profiles: ["office"]` an den Dienst – dann braucht es @@ -787,17 +794,28 @@ heißen gleich. Für uns ist es deshalb **derselbe Anbieter** – Die `.env` ist dieselbe wie bei OnlyOffice. Rechne mit **7 GB** allein für das Abbild. -> **Teilweise geprüft.** Collabora ist hier gemessen worden, den Kreis von -> Tippen bis Speichern eingeschlossen. OnlyOffice lief inzwischen auf dieser -> Anlage – gemessen sind sein Start, die Umwandlung nach PDF und die -> JWT-Signatur (ohne Signatur antwortet es mit `{"error":-8}`); im Leerlauf -> braucht es 396 MB, nach jedem Start belegt es für einige Minuten einen Kern, -> weil es seine eigenen Web-Dateien packt. **Nicht** gemessen ist dort der -> WOPI-Kreis vom Tippen bis zum Speichern. Euro-Office steht nach der -> Dokumentation und dem gemeinsamen Weg, ist hier nie gelaufen. +> **Stand der Prüfung (16.09.2026).** Collabora ist hier gemessen worden, den +> Kreis von Tippen bis Speichern eingeschlossen. **Euro-Office lief inzwischen +> auf dieser Anlage** – geprüft sind: Abbild ziehen (6,99 GB), Start, +> `/hosting/discovery` (antwortet mit 196 KB und einer breiten Formatliste bis +> zu iWork und EPUB), `CheckFileInfo`, das Ausliefern der Datei (der Inhalt +> stand im Editor) und die Handy-Fassung über `mobileEdit`. **Nicht** geprüft +> ist dort das Zurückschreiben, also `PutFile`. OnlyOffice selbst kam hier nie +> über die Ermittlung hinaus. > -> Wenn der Editor dort leer bleibt, sind das die zwei Stellen, an denen es -> nach Aktenlage hakt: +> Genau dieser Schnipsel lief – mit zwei Abweichungen, die nur diese Anlage +> betreffen: Dienstname `euro` statt `office` und Port 9981 statt 9980, weil +> Collabora daneben weiterlief. +> +> **Die Falle, die uns einen Tag gekostet hat**, steht nicht in dieser +> Aufzählung, sondern eine Ebene höher: `OFFICE_PUBLIC_URL`. Stand dort +> `http://localhost:9980`, lief alles am Rechner mit Docker – und am Telefon +> blieb der Rahmen **schwarz**, denn dort ist „localhost" das Telefon. Diese +> Adresse muss der Browser auflösen können: deine Domain oder die LAN-IP. Der +> Fehler meldet sich nicht, er zeigt nur nichts. +> +> Bleibt der Editor trotz richtiger Adresse leer, sind das die zwei Stellen, +> an denen es nach Aktenlage hakt: > > 1. **Der IP-Filter.** OnlyOffice nimmt WOPI-Anfragen nur von einem > „vertrauenswürdigen Integrator" an; dessen Adresse muss in seiner @@ -807,45 +825,14 @@ Abbild. > Signatur **nicht** – erlaubt, aber falls es dort eine Pflicht gibt, wäre > das der Ort. > -> `JWT_SECRET` betrifft die eigene API des Dokumentenservers, nicht den -> WOPI-Weg – unser Adapter signiert damit nichts. Nachgemessen: `POST -> /ConvertService.ashx` ohne Signatur antwortet `{"error":-8}`, mit Signatur -> (HS256, entweder als Feld `token` im Rumpf oder als `Authorization: Bearer` -> über `{"payload": …}`) kommt das Ergebnis. Der Schlüssel gehört also -> gesetzt – sonst läuft die API offen. - -### Am Telefon: die Handy-Fassung des Editors - -Ein Office-Dokument öffnet **überall im Editor** – am Schreibtisch wie am -Telefon. Am Telefon fordert Aether dabei die Handy-Fassung an, wo der -Office-Server eine anbietet: - -| Anbieter | am Telefon | -|---|---| -| **Collabora** | seine eigene Handy-Ansicht (kennt keine getrennte Aktion) | -| **OnlyOffice / Euro-Office** | die WOPI-Aktion `mobileEdit` | - -Die Ermittlungsliste nennt zu jeder Endung mehrere Aktionen. OnlyOffice und -Euro-Office führen neben `edit`/`view` auch `mobileEdit`/`mobileView`, und -dahinter steckt eine eigene, für kleine Schirme gebaute Oberfläche – dieselbe -Adresse mit `?mobile=1`. Aether hat bis zum 16.09.2026 immer die -Schreibtisch-Fassung angefordert; am Telefon blieb zwischen Menüband, -Formatleisten und Eigenschaftenbereich vom Dokument ein Rechteck übrig. - -Entschieden wird am **Zeigegerät**, nicht an der Fensterbreite: Ein schmales -Fenster auf dem Schreibtisch ist immer noch eine Maus und eine Tastatur. Die -Oberfläche schickt dazu `?mobile=1` an `/api/v1/office/session/{id}`. - -> **Entfallen am 16.09.2026: die PDF-Ansicht.** Ein Office-Dokument wurde am -> Telefon zuvor als gewandeltes PDF gezeigt, mit einem „Edit"-Knopf hinein. -> Das war eine Notlösung für eine Oberfläche, die Aether selbst falsch -> angefordert hatte – mit `mobileEdit` ist der Anlass weg. Mit ihr sind -> `internal/office/preview.go`, `convert.go` und die Spalte -> `files.preview_version` gefallen (Migration 000026); die Dateien unter -> `previews/` sammelt der GC ein. Wer ein PDF braucht, exportiert es im -> Editor. Die Umwandlung im Hintergrund (`/cool/convert-to/pdf` bzw. -> `/ConvertService.ashx`) hatte keinen anderen Aufrufer und ist damit -> ebenfalls entfallen. +> **`JWT_SECRET` liest Aether nicht.** Es schützt die *eigene* API des +> Dokumentenservers, nicht den WOPI-Weg – der hat eigene Token. Bis zum +> 16.09.2026 signierte Aether damit seine Umwandlungsaufrufe; mit der +> PDF-Ansicht ist auch das entfallen, und `OFFICE_JWT_SECRET` wird von der +> Anwendung nicht mehr gelesen. In der `.env` gehört es trotzdem gesetzt: +> **Der Office-Container liest es**, und ohne steht seine API offen, wenn +> `JWT_ENABLED` an ist. Geprüft mit Euro-Office: `JWT_ENABLED=true` und +> Aether ohne jedes Signieren – der WOPI-Weg läuft. ### Welche Dateien, und wie gut diff --git a/backend/cmd/aether/main.go b/backend/cmd/aether/main.go index da22416..f1779d0 100644 --- a/backend/cmd/aether/main.go +++ b/backend/cmd/aether/main.go @@ -267,7 +267,7 @@ func main() { var officeHandler *office.Handler if cfg.OfficeEnabled() { client, err := office.NewClient(cfg.OfficeProvider, cfg.OfficeURL, cfg.OfficePublicURL, - cfg.OfficeAllowedHosts, cfg.OfficeInsecureTLS, cfg.OfficeJWTSecret) + cfg.OfficeAllowedHosts, cfg.OfficeInsecureTLS) if err != nil { // Kein Fatal: Eine falsche Office-Adresse soll die Cloud nicht am // Starten hindern. Dateien bleiben erreichbar, nur das Bearbeiten diff --git a/backend/internal/office/discovery.go b/backend/internal/office/discovery.go index 7be41f9..1ca4ab2 100644 --- a/backend/internal/office/discovery.go +++ b/backend/internal/office/discovery.go @@ -48,10 +48,7 @@ type Client struct { // selbst sieht – im Compose-Netz also `collabora:9980`. Die schickt man // keinem Browser. publicBase string - // jwtSecret signiert Aufrufe an die **eigene** API von OnlyOffice (die - // Umwandlung). Der WOPI-Weg braucht ihn nicht, der hat eigene Token. - jwtSecret string - http *http.Client + http *http.Client mu sync.Mutex actions map[string]string // ".docx|edit" -> urlsrc @@ -65,7 +62,7 @@ type Client struct { // auf – ohne Grenze wird daraus ein Weg ins interne Netz, sobald jemand die // Konfiguration beeinflussen kann (SSRF). Die Adresse aus OFFICE_URL zählt // immer dazu, sonst wäre die Voreinstellung unbrauchbar. -func NewClient(provider, baseURL, publicURL string, allowedHosts []string, insecureTLS bool, jwtSecret string) (*Client, error) { +func NewClient(provider, baseURL, publicURL string, allowedHosts []string, insecureTLS bool) (*Client, error) { u, err := url.Parse(baseURL) if err != nil || u.Host == "" { return nil, fmt.Errorf("OFFICE_URL is not a valid address: %q", baseURL) @@ -99,7 +96,6 @@ func NewClient(provider, baseURL, publicURL string, allowedHosts []string, insec provider: provider, baseURL: strings.TrimRight(baseURL, "/"), publicBase: strings.TrimRight(publicURL, "/"), - jwtSecret: jwtSecret, http: &http.Client{Timeout: 20 * time.Second, Transport: transport}, }, nil } diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index c6faace..4ffe05a 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -100,11 +100,6 @@ type Config struct { // OfficeInsecureTLS erlaubt ein selbstsigniertes Zertifikat am // Office-Server. Nur für den Testaufbau auf demselben Rechner. OfficeInsecureTLS bool - // OfficeJWTSecret signiert Aufrufe an die eigene API von OnlyOffice und - // Euro-Office – gebraucht wird das für die Umwandlung nach PDF. Collabora - // kennt es nicht. Fehlt es, während der Server `JWT_ENABLED` hat, - // antwortet er auf jede Umwandlung mit `{"error":-8}`. - OfficeJWTSecret string } // splitList zerlegt eine kommagetrennte Liste und wirft Leeres weg. @@ -150,7 +145,6 @@ func Load() (*Config, error) { OfficeProvider: strings.ToLower(strings.TrimSpace(getEnv("OFFICE_PROVIDER", ""))), OfficeURL: strings.TrimRight(getEnv("OFFICE_URL", ""), "/"), - OfficeJWTSecret: getEnv("OFFICE_JWT_SECRET", ""), OfficePublicURL: strings.TrimRight(getEnv("OFFICE_PUBLIC_URL", ""), "/"), HoneybarOrigin: strings.TrimRight(getEnv("HONEYBAR_ORIGIN", ""), "/"), OfficeWopiURL: strings.TrimRight(getEnv("OFFICE_WOPI_URL", ""), "/"),