From 7837f90d00508e2741aef553ae4c2d3f0300e2c3 Mon Sep 17 00:00:00 2001 From: yetone Date: Wed, 7 Oct 2026 03:48:11 +0800 Subject: [PATCH] gateway: a listed web page's call with a gateway key is answered by the running gateway, not refused 401 On the real server lanGuard sits in front of Handler and, for a gateway key sent from this computer, records the caller and puts magpie's own token in the key's place, so corsGuard never saw the key and refused every listed page's call with "a web page calls magpie with a gateway key". corsGuard now also takes the caller lanGuard recorded. Verified: TestCORSKeyThroughTheServer (lanGuard(Handler), a listed origin with and without a key) fails without the change (401 for the keyed call) and passes with it; TestCORSOrigins passes. --- internal/gateway/cors.go | 6 ++++- internal/gateway/cors_test.go | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/gateway/cors.go b/internal/gateway/cors.go index b4982d85..654e9800 100644 --- a/internal/gateway/cors.go +++ b/internal/gateway/cors.go @@ -42,7 +42,11 @@ func corsGuard(next http.Handler) http.Handler { w.WriteHeader(http.StatusNoContent) return } - if !slices.ContainsFunc(callerKeys(r), func(k string) bool { _, ok := access.Authenticate(k); return ok }) { + // lanGuard, in front of Handler on the real server, has already + // taken the key and put magpie's own token in its place + keyed := access.Caller(r.Context()).KeyID != "" || + slices.ContainsFunc(callerKeys(r), func(k string) bool { _, ok := access.Authenticate(k); return ok }) + if !keyed { writeError(w, provider.Chat, http.StatusUnauthorized, "a web page calls magpie with a gateway key: create one in magpie's Gateway page and send it as Authorization: Bearer (or x-api-key)") return } diff --git a/internal/gateway/cors_test.go b/internal/gateway/cors_test.go index 82f26ee0..ab01fddc 100644 --- a/internal/gateway/cors_test.go +++ b/internal/gateway/cors_test.go @@ -117,3 +117,46 @@ func TestCORSOrigins(t *testing.T) { t.Fatalf("agent: %d %v", w.Code, w.Header()) } } + +// On the real server lanGuard sits in front of Handler and, for a gateway +// key sent from this computer, records who called and puts magpie's own +// token in the key's place; a listed page's keyed call is still answered. +func TestCORSKeyThroughTheServer(t *testing.T) { + fresh(t) + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, `{"choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}`) + })) + defer up.Close() + if err := provider.Save(provider.Provider{ID: "plan", Name: "Plan", Key: "upstream-secret", Chat: up.URL + "/v1", Models: []string{"m1"}}); err != nil { + t.Fatal(err) + } + secret, err := access.Update("add-key", access.Change{Name: "Web app"}) + if err != nil { + t.Fatal(err) + } + s := settings.Load() + s.CORSOrigins = []string{"http://localhost:3000"} + if err := settings.Save(s); err != nil { + t.Fatal(err) + } + srv := lanGuard(New().Handler()) + for _, key := range []string{secret, ""} { + r := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(chatReq)) + r.RemoteAddr = "127.0.0.1:50123" + r.Header.Set("Origin", "http://localhost:3000") + r.Header.Set("Content-Type", "application/json") + if key != "" { + r.Header.Set("Authorization", "Bearer "+key) + } + w := httptest.NewRecorder() + srv.ServeHTTP(w, r) + want := http.StatusOK + if key == "" { + want = http.StatusUnauthorized + } + if w.Code != want || w.Header().Get("Access-Control-Allow-Origin") != "http://localhost:3000" { + t.Errorf("key %q: %d %v %s", key, w.Code, w.Header(), w.Body) + } + } +}