Files
talk/internal/api/cors.go
T
m3ta-chiron 156c20dbb3 feat(api): CORS-Tür für POST /speak — OPTIONS-Preflight und ACAO aus Config
OPTIONS /speak beantwortet Preflights mit 204 + Allow-Headers/Methods/Max-Age,
wenn die Origin in server.allowed_origins steht; ungeliste Origins und leere
Liste bekommen keine CORS-Header (Browser blockt). Nur /speak, kein anderer
Endpunkt. /status ergänzt additiv cors:[origins] (immer vorhanden, [] wenn zu).
2026-10-18 20:06:05 +02:00

79 lines
2.9 KiB
Go

package api
import (
"net/http"
)
// CORS-Tür für POST /speak (Task 06) — und NUR für diesen Endpunkt: Ein
// Browser (z. B. eine spätere LibreChat-Integration) darf genau die Ansage-
// Schnittstelle cross-origin rufen, keine Steuer-Endpunkte. Die Erlaubnis
// kommt ausschließlich aus der Config-Liste server.allowed_origins
// (Env TALK_ALLOWED_ORIGINS): exakte Origins wie
// "https://chat.az-gruppe.com"; der Eintrag "*" erlaubt jede Origin
// ausdrücklich. Bewusster Default: LEER — dann sendet der Daemon gar keine
// CORS-Header und der Browser blockt (Tür zu, bis sie konfiguriert wird).
//
// Ungelistete Origins bekommen bewusst trotzdem 204/202 ohne CORS-Header:
// HTTP-seitig ist die Anfrage sauber, die Blockade macht der Browser (so
// verrät der Daemon nichts über seine Erlaubnisliste).
// CORS-Antwort-Header (Namen zentral, damit Preflight und echter POST
// dieselben setzen).
const (
headerAllowOrigin = "Access-Control-Allow-Origin"
headerAllowMethods = "Access-Control-Allow-Methods"
headerAllowHeaders = "Access-Control-Allow-Headers"
headerMaxAge = "Access-Control-Max-Age"
headerVary = "Vary"
)
// preflightAge ist die Preflight-Cache-Dauer: 10 Minuten reichen für eine
// Chat-Session und halten den OPTIONS-Chatter klein.
const preflightAge = "600"
// corsACAOLiefert den Wert für Access-Control-Allow-Origin: bei Wildcard-
// Konfiguration das literale "*", sonst die exakt gematchte Origin (die
// identisch mit dem Config-Eintrag ist). Leer heißt: nicht erlaubt.
func (h *handler) corsACAO(r *http.Request) string {
origin := r.Header.Get("Origin")
if origin == "" {
return ""
}
for _, allowed := range h.allowedOrigins {
if allowed == "*" {
return "*"
}
if allowed == origin {
return allowed
}
}
return ""
}
// setSpeakCors setzt die CORS-Header auf echten POST-/speak-Antworten,
// wenn die Anfrage-Origin erlaubt ist. Vary: Origin steht immer (auch bei
// geschlossener Tür), damit Caches die Antwort nie fälschlich origin-
// übergreifend wiederverwenden.
func (h *handler) setSpeakCors(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerVary, "Origin")
if acao := h.corsACAO(r); acao != "" {
w.Header().Set(headerAllowOrigin, acao)
}
}
// speakPreflight behandelt OPTIONS /speak (CORS-Preflight): 204 mit den
// Freigabe-Headern bei gelisteter Origin, 204 ohne jegliche CORS-Header
// sonst (ungelistet, fehlend oder Tür komplett zu) — der Browser verwirft
// den eigentlichen Request, während der Daemon für Nicht-Browser-Clients
// erreichbar bleibt.
func (h *handler) speakPreflight(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerVary, "Origin")
if acao := h.corsACAO(r); acao != "" {
w.Header().Set(headerAllowOrigin, acao)
w.Header().Set(headerAllowMethods, "POST, OPTIONS")
w.Header().Set(headerAllowHeaders, "Content-Type")
w.Header().Set(headerMaxAge, preflightAge)
}
w.WriteHeader(http.StatusNoContent)
}