Files
magpie/serve_cli_test.go
yetone 83c963c466 gateway: a gateway MAGPIE_ADDR puts on the network asks callers from other machines, a container's host among them, for an enabled gateway key, as sharing does
What changed
- lanGuard: a request whose peer isn't loopback (or that carries a
  forwarding header) is remote. While the gateway is shared, or
  MAGPIE_ADDR names a host other than loopback/localhost (OnNetwork:
  0.0.0.0 in the Docker image, a server's address), a remote request is
  answered only with an enabled gateway key, on every route behind the
  guard; otherwise it is refused (403) as before. Before, a MAGPIE_ADDR on
  the network with sharing off took remote requests with any key.
  A remote request let in with a key is marked as shared-mode ones are,
  so quotas, quota cards, refresh and prices answer it.
  callerGuard follows the same condition. OpenToAnyone is replaced by
  OnNetwork.
- Loopback is unchanged: any key still works on the gateway's own
  machine and inside the container (HEALTHCHECK included).
- CLI keyNote and the console's Connect note no longer describe an open
  gateway; on the network they say other machines need a gateway key
  (`magpie gateway-key add <name>`). The console shows the Gateway keys
  block and network addresses when OnNetwork (gatewayJSON.onNetwork
  replaces open); the "Open to the network" strings are removed.
- Docs: docker.html (en/zh/ja) and remote.html (en/zh/ja) say callers
  outside the container need a gateway key, how to make one, that
  sharing isn't needed for it, and what an upgrade means for agents that
  sent `magpie`; docs/reference.md and providers-accounts.md likewise.
  A MAGPIE_ADDR pinned to one interface's address now asks a key of this
  computer's agents calling that address too (documented).

Tests
- New TestNetworkGatewayGuardsEveryRoute: from 172.17.0.1 and
  192.168.1.9 every registered route (model APIs, count_tokens,
  systemone, the Responses websocket GET, MCP, Codex backend and
  catalog, /v1/magpie/*, images, embeddings, rerank, videos, Gemini)
  gets 401 without a valid key; with one, models and quotas answer 200.
- TestNetworkGatewayNeedsAKey (was TestExplicitOpenGatewayAccepts...),
  TestLANGuard, TestProxiedLoopbackNeedsTheKey, TestQuotasOverLAN,
  TestMCPProxyStaysHere, session route and remote prices tests, TestOnNetwork,
  serve_cli and gui public_url tests updated. With the old lanGuard
  restored, TestNetworkGatewayNeedsAKey, TestNetworkGatewayGuardsEveryRoute,
  TestLANGuard and TestProxiedLoopbackNeedsTheKey fail.

Run
- go test -tags nogui -count=1 ./internal/gateway ./internal/gui
  ./internal/provider . under a temp HOME: ok.
- gofmt -l clean; GOOS=linux and GOOS=windows go vet -tags nogui on
  those packages: clean.
- GUI: public-url and gateway-caller-keys tests, Chromium and WebKit: 22/22.
- Locally built image (OrbStack), -p 127.0.0.1:3481:3425: from the host,
  /v1/models with no key, `magpie` and another made-up key -> 401;
  count_tokens and /mcp/x -> 401; a key from `docker exec ... magpie
  gateway-key add Host` -> models 200, quotas 200; inside the container
  wget /v1/models -> 200; healthcheck exits 0. `web --addr 0.0.0.0:3430`:
  /api/state 401 without the web key, 200 with its cookie.
2026-10-10 09:26:23 +08:00

84 lines
3.0 KiB
Go

package main
import (
"os"
"strings"
"testing"
"github.com/yetone/magpie/internal/settings"
)
// The serve banner says the gateway takes any key from this machine alone,
// and, when MAGPIE_ADDR puts it on the network (a Docker image) or it is
// shared from Settings, a gateway key from others: never from anyone.
func TestKeyNote(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", os.Getenv("HOME"))
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
for _, c := range []struct{ addr, want string }{
{"", "only listens on localhost"},
{"127.0.0.1:3426", "only listens on localhost"},
{"localhost:3426", "only listens on localhost"},
{"[::1]:3426", "only listens on localhost"},
{"0.0.0.0:3425", "from others, an enabled gateway key"},
{":3425", "from others, an enabled gateway key"},
{"192.168.1.5:3425", "from others, an enabled gateway key"},
} {
t.Setenv("MAGPIE_ADDR", c.addr)
if n := keyNote(); !strings.Contains(n, c.want) || strings.Contains(n, "anyone") {
t.Errorf("MAGPIE_ADDR=%q: %q", c.addr, n)
}
}
for _, key := range []string{"", "sk-magpie-k"} {
if err := settings.Save(settings.Settings{LAN: true, LANKey: key}); err != nil {
t.Fatal(err)
}
for _, addr := range []string{"", "0.0.0.0:3425"} {
t.Setenv("MAGPIE_ADDR", addr)
if n := keyNote(); !strings.Contains(n, "gateway key") || strings.Contains(n, "only listens on localhost") {
t.Error("shared:", n)
}
}
}
}
// The CLIs print the address other machines are told to reach the gateway
// at: a valid MAGPIE_PUBLIC_URL, otherwise the one reached from this
// machine, so `magpie models` and the serve banner never print a bad one.
func TestAdvertisedURL(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("MAGPIE_ADDR", "127.0.0.1:3425")
for _, c := range []struct{ public, want string }{
{"", "http://127.0.0.1:3425"},
{"https://magpie.example.com", "https://magpie.example.com"},
{"nas.lan:3425/", "http://nas.lan:3425"},
{"ftp://magpie.example.com", "http://127.0.0.1:3425"},
{"https://magpie.example.com/magpie?x=1", "http://127.0.0.1:3425"},
} {
t.Setenv("MAGPIE_PUBLIC_URL", c.public)
if got := advertisedURL(); got != c.want {
t.Errorf("MAGPIE_PUBLIC_URL=%q: %q, want %q", c.public, got, c.want)
}
}
}
// Shared, the serve banner (docker logs) says where other machines reach
// the gateway: MAGPIE_PUBLIC_URL when a container sets it.
func TestShareLines(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", os.Getenv("HOME"))
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("MAGPIE_ADDR", "0.0.0.0:3425")
t.Setenv("MAGPIE_PUBLIC_URL", "http://192.168.1.20:3425/")
if l := shareLines(); len(l) != 0 {
t.Error("unshared:", l)
}
if err := settings.Save(settings.Settings{LAN: true, LANKey: "sk-magpie-k"}); err != nil {
t.Fatal(err)
}
if l := strings.Join(shareLines(), "\n"); !strings.Contains(l, "http://192.168.1.20:3425") || strings.Contains(l, "container") {
t.Error("shared:", l)
}
}