diff --git a/.env.example b/.env.example
index 7ac8211..c00e6fa 100644
--- a/.env.example
+++ b/.env.example
@@ -24,23 +24,43 @@ HONEYOS_SESSION_SECRET=
HONEYOS_DATA_DIR=/srv/honeyos
# ── Anmeldung ────────────────────────────────────────────────────────────────
-# Leerer HONEYOS_OIDC_ISSUER = KEINE Anmeldung, jeder kommt rein. Das ist zum
-# Entwickeln gedacht und gehört nicht ins offene Netz.
+# Genau einer der beiden Wege:
#
-# In PocketID einen Client anlegen und dort als Rückrufadresse exakt
-# https://os.honeyy.duckdns.org/api/auth/callback
-# eintragen. HoneyOS schickt PKCE mit, ein Client-Geheimnis braucht es
-# trotzdem — also keinen „public client" anlegen.
-HONEYOS_OIDC_ISSUER=https://id.honeyy.duckdns.org
-HONEYOS_OIDC_CLIENT_ID=honeyos
-HONEYOS_OIDC_CLIENT_SECRET=
-# Nur nötig, wenn die Rückrufadresse von PUBLIC_URL abweicht:
-# HONEYOS_OIDC_REDIRECT_URL=
+# A) Ein Proxy davor meldet an (Pangolin, Authelia, …) — HoneyOS fragt nicht
+# noch einmal. Dann NUR das hier setzen:
+HONEYOS_PROXY_AUTH=true
+# Optional: Kopfzeile, in die der Proxy den Nutzernamen schreibt. Ohne sie
+# teilen sich alle einen Satz Einstellungen — bei einer Person völlig ok.
+# HONEYOS_USER_HEADER=Remote-User
+#
+# ⚠ In diesem Betrieb prüft HoneyOS selbst nichts mehr. Der Container darf
+# dann NUR über den Proxy erreichbar sein — nicht zusätzlich auf einem
+# offenen Port im Netz hängen.
+#
+# B) HoneyOS meldet selbst an, mit eigenem Anmeldeschirm. Dann PROXY_AUTH
+# weglassen und stattdessen:
+# HONEYOS_SESSION_SECRET= # openssl rand -hex 32
+# HONEYOS_OIDC_ISSUER=https://id.example.com
+# HONEYOS_OIDC_CLIENT_ID=honeyos
+# HONEYOS_OIDC_CLIENT_SECRET=
+# HONEYOS_OIDC_REDIRECT_URL= # nur wenn abweichend von PUBLIC_URL
+# HONEYOS_BUTTON_LABEL=Anmelden mit PocketID
+# HONEYOS_ALLOWED_USERS=marc # zweiter Riegel neben den IdP-Gruppen
-# Beschriftung des einen Knopfes auf dem Anmeldeschirm.
-HONEYOS_BUTTON_LABEL=Anmelden mit PocketID
+# ── Schlüssel für die App-Liste ──────────────────────────────────────────────
+# Nötig, sobald die Leiste in einer App läuft, die auf einer ANDEREN Site liegt
+# als HoneyOS — etwa honeyos.duckdns.org gegenüber cloud.honeyy.duckdns.org.
+# Der Browser schickt die Sitzung dann nicht mit, und ohne zweiten Ausweis
+# bekäme die Leiste nie eine Liste.
+#
+# Ist der Pfad /api/apps im Proxy von der Anmeldung befreit, ist dieser
+# Schlüssel das Einzige, was davorsteht. Dann setzen.
+# openssl rand -hex 16
+HONEYOS_APPS_KEY=
-# Zweiter Riegel neben den Gruppen in PocketID: nur diese Konten dürfen rein
-# (Benutzername oder sub, kommagetrennt). Leer = jeder, den der Anbieter
-# durchlässt.
-# HONEYOS_ALLOWED_USERS=marc
+# Derselbe Wert gehört in das
+#
+# Zusätzlich lässt HoneyOS die Liste nur an Ursprünge heraus, die selbst als App
+# eingetragen sind. Der Schlüssel allein nützt einer fremden Seite also nichts.
diff --git a/README.md b/README.md
index 75b29d2..8d36457 100644
--- a/README.md
+++ b/README.md
@@ -29,33 +29,73 @@ und die Einstellungen je Nutzer. Sichere sie zusammen mit der `.env` — ohne
außer dass `X-Forwarded-Proto: https` durchkommt — sonst setzt HoneyOS das Sitzungscookie
ohne `Secure`.
-## Anmeldung
+## Anmeldung — zwei Wege, einer davon reicht
-In PocketID einen Client anlegen und dort als Rückrufadresse **exakt** eintragen:
-
-```
-https://os.honeyy.duckdns.org/api/auth/callback
-```
-
-HoneyOS schickt PKCE mit, braucht aber trotzdem ein Client-Geheimnis — also **keinen
-„public client"** anlegen. Danach in die `.env`:
+**A · Ein Proxy davor macht die Tür** (Pangolin, Authelia, …). Dann in der `.env`:
```dotenv
-HONEYOS_PUBLIC_URL=https://os.honeyy.duckdns.org
-HONEYOS_SESSION_SECRET=… # openssl rand -hex 32
-HONEYOS_OIDC_ISSUER=https://id.honeyy.duckdns.org
+HONEYOS_PROXY_AUTH=true
+# HONEYOS_USER_HEADER=Remote-User # optional, trennt die Einstellungen je Nutzer
+```
+
+HoneyOS fragt dann nicht noch einmal — kein zweiter Anmeldeschirm. **Der
+Container darf in diesem Betrieb nur über den Proxy erreichbar sein**, denn er
+prüft selbst nichts mehr.
+
+**B · HoneyOS meldet selbst an**, mit eigenem Anmeldeschirm über deinen
+OIDC-Anbieter. In PocketID einen Client anlegen und als Rückrufadresse **exakt**
+`/api/auth/callback` eintragen. HoneyOS schickt PKCE mit, braucht
+aber trotzdem ein Client-Geheimnis — also **keinen „public client"**.
+
+```dotenv
+HONEYOS_SESSION_SECRET=… # openssl rand -hex 32
+HONEYOS_OIDC_ISSUER=https://id.example.com
HONEYOS_OIDC_CLIENT_ID=honeyos
HONEYOS_OIDC_CLIENT_SECRET=…
HONEYOS_BUTTON_LABEL=Anmelden mit PocketID
-# HONEYOS_ALLOWED_USERS=marc # zweiter Riegel neben den PocketID-Gruppen
```
-Wer nicht angemeldet ist, sieht **nur** den Anmeldeschirm — keine App-Liste, keine Namen.
-Die Sitzung ist ein signiertes Cookie ohne Gegenstück auf dem Server: ein Neustart wirft
-niemanden hinaus, und es gibt nichts aufzuräumen. Sie hält 30 Tage.
+Wer nicht angemeldet ist, sieht nur den Anmeldeschirm — keine App-Liste, keine
+Namen. Die Sitzung ist ein signiertes Cookie ohne Gegenstück im Server: ein
+Neustart wirft niemanden hinaus. Sie hält 30 Tage.
-**Ohne `HONEYOS_OIDC_ISSUER` läuft HoneyOS ganz ohne Anmeldung.** Das ist zum Entwickeln
-gedacht und sagt beim Start deutlich, dass es so nicht ins offene Netz gehört.
+**Weder A noch B** heißt: offen für jeden. Sagt beim Start deutlich, dass das
+nur zum Entwickeln taugt.
+
+## Wenn HoneyOS auf einer anderen Site liegt als die Apps
+
+Der Browser behandelt `honeyos.duckdns.org` und `cloud.honeyy.duckdns.org` als
+**fremde Sites** — `duckdns.org` steht auf der Public-Suffix-Liste, die
+gemeinsame Endung zählt also nicht. Folge: Bei der Anfrage aus einer App heraus
+schickt der Browser die HoneyOS-Sitzung **nicht** mit.
+
+Damit die Leiste trotzdem eine App-Liste bekommt:
+
+```dotenv
+HONEYOS_APPS_KEY=… # openssl rand -hex 16
+```
+
+und derselbe Wert im Skript-Tag der App:
+
+```html
+
+```
+
+Der Schlüssel steht damit in der App, die ohnehin nur Berechtigte sehen. Ohne
+ihn antwortet `/api/apps` mit 401 — auch im Proxy-Betrieb, gerade weil dieser
+Pfad im Proxy von der Anmeldung befreit sein muss.
+
+Zweite Schranke, unabhängig davon: HoneyOS gibt die Liste nur an Ursprünge
+heraus, die **selbst als App eingetragen** sind. Eine fremde Seite scheitert
+also an CORS, selbst mit richtigem Schlüssel.
+
+**Was auf zwei Sites nicht geht:** der Anmeldestand auf den Kacheln des
+Schreibtischs (fragt über die Domaingrenze). Der Name *in der Leiste* geht
+weiterhin — den holt sie bei der App selbst, gleicher Ursprung.
+
+Liegen alle Adressen unter derselben Domain (`os.honeyy…`, `cloud.honeyy…`),
+entfällt der Schlüssel und alles funktioniert ohne Einschränkung.
## Apps anlegen
diff --git a/auth.go b/auth.go
index dfeddef..e7fa27d 100644
--- a/auth.go
+++ b/auth.go
@@ -109,9 +109,18 @@ func (a *Auth) clearSession(w http.ResponseWriter, r *http.Request) {
}
// SubFromRequest liest die Sitzung. Zweiter Rückgabewert sagt, ob sie gilt.
+//
+// Ohne eigene Anmeldung (Proxy davor oder offener Betrieb) gilt jede Anfrage
+// als angemeldet — die Prüfung hat dann jemand anders gemacht, oder es gibt
+// keine. Nennt der Proxy einen Namen, bekommt der seine eigenen Einstellungen.
func (a *Auth) SubFromRequest(r *http.Request) (string, bool) {
if !a.Enabled() {
- return localSub, true // offener Betrieb, siehe Config.Open
+ if a.cfg.UserHeader != "" {
+ if u := strings.TrimSpace(r.Header.Get(a.cfg.UserHeader)); u != "" {
+ return "proxy:" + u, true
+ }
+ }
+ return localSub, true
}
c, err := r.Cookie(sessionCookie)
if err != nil {
diff --git a/auth_test.go b/auth_test.go
index 860da75..fb09f05 100644
--- a/auth_test.go
+++ b/auth_test.go
@@ -386,3 +386,157 @@ func signIn(t *testing.T, h http.Handler) (sess, csrf *http.Cookie) {
}
return sess, csrf
}
+
+// ── Betrieb hinter einem Anmelde-Proxy ─────────────────────────────────────
+// Kein eigener Anmeldeschirm: Pangolin o. ä. hat die Tür schon zugemacht.
+
+func proxyServer(t *testing.T, cfg Config) *Server {
+ t.Helper()
+ store, err := OpenStore(dataPath(t.TempDir()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cfg.SessionSecret = strings.Repeat("k", 40)
+ return &Server{cfg: cfg, auth: NewAuth(cfg), store: store}
+}
+
+func TestProxyBetriebLässtDurch(t *testing.T) {
+ h := proxyServer(t, Config{ProxyAuth: true}).routes()
+
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/me", nil))
+ if rec.Code != 200 {
+ t.Fatalf("/api/me hinter dem Proxy: %d", rec.Code)
+ }
+ // Kein Anmeldeschirm: /api/public darf nicht behaupten, es gäbe OIDC.
+ rec = httptest.NewRecorder()
+ h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/public", nil))
+ var pub struct{ OIDC, SignedIn bool }
+ _ = json.Unmarshal(rec.Body.Bytes(), &pub)
+ if pub.OIDC || !pub.SignedIn {
+ t.Errorf("die Oberfläche würde einen Anmeldeschirm zeigen: %+v", pub)
+ }
+}
+
+// Nennt der Proxy einen Namen, bekommt jeder Name seine eigenen Einstellungen.
+func TestProxyKopfzeileTrenntEinstellungen(t *testing.T) {
+ s := proxyServer(t, Config{ProxyAuth: true, UserHeader: "Remote-User"})
+ h := s.routes()
+
+ put := func(user, body string) {
+ req := httptest.NewRequest("PUT", "/api/settings", strings.NewReader(body))
+ if user != "" {
+ req.Header.Set("Remote-User", user)
+ }
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ if rec.Code != http.StatusNoContent {
+ t.Fatalf("PUT für %q: %d", user, rec.Code)
+ }
+ }
+ put("marc", `{"phosphor":"green"}`)
+ put("gast", `{"phosphor":"ice"}`)
+
+ if got := s.store.User("proxy:marc").Settings["phosphor"]; got != "green" {
+ t.Errorf("marc: %q", got)
+ }
+ if got := s.store.User("proxy:gast").Settings["phosphor"]; got != "ice" {
+ t.Errorf("gast: %q", got)
+ }
+}
+
+// ── Schlüssel für die App-Liste ────────────────────────────────────────────
+
+func TestAppListeBrauchtDenSchlüssel(t *testing.T) {
+ idp := newFakeIdP(t, "honeyos")
+ s := newTestServer(t, idp.URL)
+ s.cfg.AppsKey = "geheim123"
+ s.auth = NewAuth(s.cfg)
+ h := s.routes()
+
+ get := func(mod func(*http.Request)) int {
+ req := httptest.NewRequest("GET", "/api/apps", nil)
+ if mod != nil {
+ mod(req)
+ }
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ return rec.Code
+ }
+
+ if code := get(nil); code != http.StatusUnauthorized {
+ t.Errorf("ohne Schlüssel: %d, erwartet 401", code)
+ }
+ if code := get(func(r *http.Request) { r.Header.Set("X-HoneyOS-Key", "falsch") }); code != http.StatusUnauthorized {
+ t.Errorf("falscher Schlüssel: %d, erwartet 401", code)
+ }
+ if code := get(func(r *http.Request) { r.Header.Set("X-HoneyOS-Key", "geheim123") }); code != 200 {
+ t.Errorf("richtiger Schlüssel: %d, erwartet 200", code)
+ }
+ if code := get(func(r *http.Request) { r.URL.RawQuery = "key=geheim123" }); code != 200 {
+ t.Errorf("Schlüssel als Parameter: %d, erwartet 200", code)
+ }
+}
+
+// Der Schlüssel öffnet nur das Lesen der Liste, nicht das Schreiben.
+func TestSchlüsselDarfNichtSchreiben(t *testing.T) {
+ idp := newFakeIdP(t, "honeyos")
+ s := newTestServer(t, idp.URL)
+ s.cfg.AppsKey = "geheim123"
+ s.auth = NewAuth(s.cfg)
+ h := s.routes()
+
+ req := httptest.NewRequest("PUT", "/api/apps", strings.NewReader(`[{"id":"x","url":"https://a.test"}]`))
+ req.Header.Set("X-HoneyOS-Key", "geheim123")
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ if rec.Code != http.StatusUnauthorized {
+ t.Fatalf("Schreiben mit Schlüssel: %d, erwartet 401", rec.Code)
+ }
+}
+
+// Die Oberfläche muss den Schlüssel mitbekommen, sonst fragt sie sich selbst aus.
+func TestStartseiteTrägtDenSchlüssel(t *testing.T) {
+ s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"})
+ rec := httptest.NewRecorder()
+ s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
+
+ if !strings.Contains(rec.Body.String(), `name="honeyos-key" content="geheim123"`) {
+ t.Fatal("Schlüssel steht nicht in der Startseite")
+ }
+
+ // Ohne Schlüssel darf auch nichts drinstehen.
+ s2 := proxyServer(t, Config{ProxyAuth: true})
+ rec = httptest.NewRecorder()
+ s2.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
+ if strings.Contains(rec.Body.String(), "honeyos-key") {
+ t.Error("ohne Schlüssel steht trotzdem eine Meta-Angabe in der Seite")
+ }
+}
+
+// Der Fall, der beim Durchspielen aufgefallen ist: Ohne eigene Anmeldung gilt
+// jede Anfrage als angemeldet. Der Schlüssel darf dann NICHT auf diese Prüfung
+// zurückfallen, sonst gibt der freigegebene Pfad die Liste an jeden heraus.
+func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) {
+ h := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"}).routes()
+
+ get := func(key string) int {
+ req := httptest.NewRequest("GET", "/api/apps", nil)
+ if key != "" {
+ req.Header.Set("X-HoneyOS-Key", key)
+ }
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ return rec.Code
+ }
+
+ if code := get(""); code != http.StatusUnauthorized {
+ t.Errorf("ohne Schlüssel hinter dem Proxy: %d, erwartet 401", code)
+ }
+ if code := get("falsch"); code != http.StatusUnauthorized {
+ t.Errorf("falscher Schlüssel hinter dem Proxy: %d, erwartet 401", code)
+ }
+ if code := get("geheim123"); code != 200 {
+ t.Errorf("richtiger Schlüssel: %d, erwartet 200", code)
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 7bc90b9..5cb9ac7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -30,3 +30,6 @@ services:
- HONEYOS_OIDC_REDIRECT_URL=${HONEYOS_OIDC_REDIRECT_URL:-}
- HONEYOS_BUTTON_LABEL=${HONEYOS_BUTTON_LABEL:-Anmelden}
- HONEYOS_ALLOWED_USERS=${HONEYOS_ALLOWED_USERS:-}
+ - HONEYOS_PROXY_AUTH=${HONEYOS_PROXY_AUTH:-}
+ - HONEYOS_USER_HEADER=${HONEYOS_USER_HEADER:-}
+ - HONEYOS_APPS_KEY=${HONEYOS_APPS_KEY:-}
diff --git a/main.go b/main.go
index 3dfa2df..91382b4 100644
--- a/main.go
+++ b/main.go
@@ -6,6 +6,8 @@
package main
import (
+ "bytes"
+ "crypto/subtle"
"embed"
"encoding/json"
"io/fs"
@@ -23,16 +25,34 @@ var webFS embed.FS
const localSub = "local"
type Config struct {
- Addr string
- PublicURL string
- DataDir string
- SessionSecret string
+ Addr string
+ PublicURL string
+ DataDir string
+ SessionSecret string
+
+ // Anmeldung — genau einer der beiden Wege:
+ //
+ // OIDCIssuer gesetzt → HoneyOS meldet selbst an, eigener Anmeldeschirm.
+ // ProxyAuth gesetzt → ein vorgelagerter Proxy (Pangolin o. ä.) hat das
+ // schon erledigt. HoneyOS fragt dann nicht nach.
+ //
+ // Keines von beiden heißt: offen. Das ist zum Entwickeln gedacht und sagt
+ // beim Start deutlich, dass es so nicht ins Netz gehört.
OIDCIssuer string
OIDCClientID string
OIDCClientSecret string
OIDCRedirect string
ButtonLabel string
AllowedUsers []string
+
+ ProxyAuth bool
+ // UserHeader nennt die Kopfzeile, in die der Proxy den Nutzernamen schreibt
+ // (z. B. Remote-User). Leer heißt: ein einziger Nutzer für alle.
+ UserHeader string
+
+ // AppsKey schützt die App-Liste, wenn ihr Pfad im Proxy von der Anmeldung
+ // befreit ist. Ohne Schlüssel liest sie jeder, der die Adresse kennt.
+ AppsKey string
}
func (c Config) RedirectURL() string {
@@ -62,6 +82,9 @@ func main() {
OIDCRedirect: env("HONEYOS_OIDC_REDIRECT_URL", ""),
ButtonLabel: env("HONEYOS_BUTTON_LABEL", "Anmelden"),
AllowedUsers: splitList(env("HONEYOS_ALLOWED_USERS", "")),
+ ProxyAuth: env("HONEYOS_PROXY_AUTH", "") == "true",
+ UserHeader: env("HONEYOS_USER_HEADER", ""),
+ AppsKey: env("HONEYOS_APPS_KEY", ""),
}
if cfg.OIDCIssuer != "" {
@@ -75,11 +98,23 @@ func main() {
case cfg.PublicURL == "" && cfg.OIDCRedirect == "":
log.Fatal("HONEYOS_PUBLIC_URL fehlt — daraus entsteht die Rückrufadresse")
}
+ } else if cfg.ProxyAuth {
+ log.Println("Anmeldung: übernimmt der vorgelagerte Proxy (HONEYOS_PROXY_AUTH=true).")
+ if cfg.UserHeader != "" {
+ log.Printf("Nutzername aus der Kopfzeile %s", cfg.UserHeader)
+ } else {
+ log.Println("Kein HONEYOS_USER_HEADER: alle teilen einen Satz Einstellungen.")
+ }
+ log.Println("⚠ HoneyOS prüft dann selbst nichts mehr — es darf nur über den Proxy erreichbar sein.")
} else {
- log.Println("⚠ kein HONEYOS_OIDC_ISSUER: HoneyOS läuft OHNE Anmeldung.")
+ log.Println("⚠ weder HONEYOS_OIDC_ISSUER noch HONEYOS_PROXY_AUTH: HoneyOS läuft OHNE Anmeldung.")
log.Println("⚠ Das ist für die Entwicklung gedacht. Nicht ins offene Netz stellen.")
}
+ if cfg.AppsKey == "" && (cfg.ProxyAuth || cfg.OIDCIssuer == "") {
+ log.Println("Hinweis: ohne HONEYOS_APPS_KEY liest die App-Liste jeder, der ihren Pfad kennt.")
+ }
+
if err := os.MkdirAll(cfg.DataDir, 0o700); err != nil {
log.Fatalf("Datenverzeichnis %s: %v", cfg.DataDir, err)
}
@@ -119,7 +154,7 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("GET /api/me", s.requireAuth(s.handleMe))
mux.HandleFunc("PUT /api/settings", s.requireAuth(s.handleSetSettings))
- mux.HandleFunc("GET /api/apps", s.requireAuth(s.handleApps))
+ mux.HandleFunc("GET /api/apps", s.appsReadable(s.handleApps))
mux.HandleFunc("PUT /api/apps", s.requireAuth(s.handleSetApps))
web, err := fs.Sub(webFS, "web")
@@ -127,11 +162,43 @@ func (s *Server) routes() http.Handler {
log.Fatal(err)
}
files := http.FileServer(http.FS(web))
- mux.Handle("GET /", noCache(files))
+ index := s.indexHTML(web)
+
+ // Die Startseite kommt aus dem Speicher, weil der Schlüssel hineingehört.
+ // Alles andere liefert der Dateiserver unverändert aus.
+ mux.Handle("GET /", noCache(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(index))
+ return
+ }
+ files.ServeHTTP(w, r)
+ })))
return s.cors(mux)
}
+// indexHTML holt die Startseite aus dem Binary und legt den Schlüssel als
+// Meta-Angabe hinein. Die Seite selbst ist im Proxy geschützt; wer sie sieht,
+// darf die Liste ohnehin lesen.
+func (s *Server) indexHTML(web fs.FS) []byte {
+ raw, err := fs.ReadFile(web, "index.html")
+ if err != nil {
+ log.Fatalf("index.html fehlt im Binary: %v", err)
+ }
+ if s.cfg.AppsKey == "" {
+ return raw
+ }
+ meta := ``
+ return bytes.Replace(raw, []byte(""), []byte(" "+meta+"\n "), 1)
+}
+
+// htmlAttr entschärft den Wert für ein Attribut in doppelten Anführungszeichen.
+func htmlAttr(v string) string {
+ r := strings.NewReplacer(`&`, "&", `"`, """, `<`, "<", `>`, ">")
+ return r.Replace(v)
+}
+
/* ── Zwischenschichten ───────────────────────────────────────────────────── */
// cors öffnet die Leiste für die eingetragenen Apps — und nur für die. Die
@@ -144,7 +211,7 @@ func (s *Server) cors(next http.Handler) http.Handler {
h := w.Header()
h.Set("Access-Control-Allow-Origin", origin)
h.Set("Access-Control-Allow-Credentials", "true")
- h.Set("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
+ h.Set("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token, X-HoneyOS-Key")
h.Set("Access-Control-Allow-Methods", "GET, PUT, POST")
h.Add("Vary", "Origin")
if r.Method == http.MethodOptions {
@@ -163,6 +230,36 @@ func noCache(next http.Handler) http.Handler {
})
}
+// appsReadable lässt die App-Liste zusätzlich mit dem Schlüssel durch.
+//
+// Der Grund ist eine Browserregel, keine Bequemlichkeit: Liegt HoneyOS auf
+// einer anderen Site als die Apps, schickt der Browser die Sitzung bei der
+// Anfrage aus einer App heraus nicht mit. Ohne einen zweiten Ausweis bekäme die
+// Leiste dort nie eine Liste. Der Schlüssel steht im Skript-Tag der App und ist
+// damit nur für den lesbar, der ohnehin an die App herankommt.
+func (s *Server) appsReadable(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if key := s.cfg.AppsKey; key != "" {
+ given := r.Header.Get("X-HoneyOS-Key")
+ if given == "" {
+ given = r.URL.Query().Get("key")
+ }
+ if subtle.ConstantTimeCompare([]byte(given), []byte(key)) == 1 {
+ next(w, r, localSub)
+ return
+ }
+ // Ohne eigene Anmeldung gibt es keine Sitzung, die den Schlüssel
+ // ersetzen könnte: dort gilt jede Anfrage als angemeldet, und der
+ // Rückfall würde die Liste an jeden herausgeben. Hier ist Schluss.
+ if !s.auth.Enabled() {
+ writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Schlüssel fehlt oder passt nicht"})
+ return
+ }
+ }
+ s.requireAuth(next)(w, r)
+ }
+}
+
func (s *Server) requireAuth(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sub, ok := s.auth.SubFromRequest(r)
diff --git a/web/honeybar/honeybar.js b/web/honeybar/honeybar.js
index e97855e..dbdf695 100644
--- a/web/honeybar/honeybar.js
+++ b/web/honeybar/honeybar.js
@@ -13,6 +13,10 @@
* data-Attribute am