mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-11 12:37:33 +00:00
## What this does Server changes so the UI can be served from another origin, such as a Cloudflare Pages project and its previews, against an existing backend. It ships before the frontend, which is parked on [`backup/static-frontend-1a`](https://github.com/ppicazo/CoreScope/tree/backup/static-frontend-1a) and will come as its own PR. - **Exposed headers:** allowed origins get `Access-Control-Expose-Headers: Retry-After, X-CoreScope-Load-Status`. The UI reads both, and cross-origin they are `null` without this. - **Wildcard origins:** an entry may be `scheme://*.host[:port]`, such as `https://*.myproject.pages.dev`. It matches exactly one leftmost label (each deploy and branch preview) for both CORS and `/ws`, but not `myproject.pages.dev` itself. The part after `*.` needs at least three labels, so `*.pages.dev` is refused. Refused or malformed wildcards are dropped at startup with a log line. That floor is a sanity check, not a Public Suffix List lookup: `https://*.s3.amazonaws.com` passes and would admit every S3 website endpoint, so a wildcard belongs only over a host the operator controls. - **Case:** exact entries now match case-insensitively for CORS (was `==`), as they already did for `/ws`. - **Test only:** pins that the three anonymous POSTs (`/api/decode`, `/api/packets/observations`, `/api/paths/inspect`) accept a `text/plain` JSON body, which keeps them free of a preflight. Credentials stay off, so nothing a listed origin sends rides on cookies. Beyond GET, a listed origin, and so every deployment under a wildcard entry, can: - call those three anonymous POSTs as simple requests. That predates this PR; they compute an answer and write nothing. `Access-Control-Allow-Methods: GET, HEAD, OPTIONS` only governs preflighted requests. - with `userManagement.enabled`, use the existing bearer tier for companion linking (`/api/auth/device-token`, `/api/auth/me`, `/api/auth/logout`, `/api/account/settings`, `/api/account/companions*`) with POST, PUT and DELETE. Those routes need a device token in `Authorization`, not a cookie, but a preview deployment gets them too. On such a server, list a wildcard only if previews may use them. ## Configuring ```yaml - CORS_ALLOWED_ORIGINS=https://existing-embed.example,https://myproject.pages.dev,https://*.myproject.pages.dev ``` The env var replaces `corsAllowedOrigins`, so keep the origins already listed there. ## Testing - `go test ./...`, `go vet` and `gofmt` are clean in `cmd/server`. The new tests were written first and failed first. - In Chromium, the static build from the backup branch, served on `pr-1.proj.preview.localhost`, passed its split E2E (9/9, WebSocket included) against this server. - From the same setup, the project origin connected, a two-level subdomain was blocked, and `X-CoreScope-Load-Status` read `ready` where master gives `null`. ## Performance These run per request in the CORS middleware and the `/ws` handshake, and they allocate nothing. Same-origin requests skip them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.4 KiB
Go
59 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// A UI served from another origin (a static host listed in
|
|
// CORS_ALLOWED_ORIGINS) sends its three anonymous JSON POSTs with Content-Type
|
|
// text/plain. That keeps each one a CORS simple request: no preflight, which
|
|
// the embed CORS tier (Allow-Methods GET, HEAD, OPTIONS) would
|
|
// refuse. It works because these handlers decode the body as JSON whatever its
|
|
// Content-Type. If a handler starts insisting on application/json, this fails
|
|
// here instead of on the static host.
|
|
func TestAnonymousPostsAcceptTextPlainJSON(t *testing.T) {
|
|
_, router := setupTestServerWithAPIKey(t, "")
|
|
// The full test server is still warming the inspect graph (503), and the
|
|
// bare server path_inspect_test.go uses has no node data to search. A
|
|
// prefix the handler rejects after decoding still proves the point: a body
|
|
// it could not decode would answer "invalid JSON" instead.
|
|
inspect := newTestServerForInspect(t)
|
|
|
|
cases := []struct {
|
|
path, body string
|
|
handler http.Handler
|
|
want int
|
|
wantBody string
|
|
}{
|
|
{"/api/decode", `{"hex":"0200"}`, router, http.StatusOK, `"decoded"`},
|
|
{"/api/packets/observations", `{"hashes":["abc123"]}`, router, http.StatusOK, `"results"`},
|
|
{"/api/paths/inspect", `{"prefixes":["abc"]}`, http.HandlerFunc(inspect.handlePathInspect), http.StatusBadRequest, "even-length hex"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.path, func(t *testing.T) {
|
|
post := func(contentType string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest("POST", c.path, bytes.NewBufferString(c.body))
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.Header.Set("Origin", "https://beta.corescope.example")
|
|
w := httptest.NewRecorder()
|
|
c.handler.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
asJSON := post("application/json")
|
|
asText := post("text/plain;charset=UTF-8")
|
|
if asJSON.Code != c.want || !bytes.Contains(asJSON.Body.Bytes(), []byte(c.wantBody)) {
|
|
t.Fatalf("application/json: expected %d with %s, got %d (body: %s)", c.want, c.wantBody, asJSON.Code, asJSON.Body.String())
|
|
}
|
|
if asText.Code != asJSON.Code {
|
|
t.Fatalf("text/plain: got %d, application/json got %d (body: %s)", asText.Code, asJSON.Code, asText.Body.String())
|
|
}
|
|
if asText.Body.String() != asJSON.Body.String() {
|
|
t.Fatalf("text/plain answered differently:\n text/plain: %s\n json: %s", asText.Body.String(), asJSON.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|