From 49ad09efe76bf4f4f0b4f09f270b2d8124ee062f Mon Sep 17 00:00:00 2001 From: vsecoder Date: Wed, 22 Apr 2026 17:22:39 +0300 Subject: [PATCH] fix(node): proper CORS middleware + preflight handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop Electron renderer runs at http://127.0.0.1:5173 (dev) or file:// (prod); the node HTTP API is at a different origin by design. Browsers enforce CORS, and our per-handler `Access-Control-Allow-Origin: *` header only covered the happy path — preflight OPTIONS requests, which browsers send before any POST with a JSON body or Authorization header, fell through to the 404 handler without CORS headers and the subsequent real request was blocked. Added node/cors.go — a single middleware that: * Sets Access-Control-Allow-Origin / -Methods / -Headers / -Expose-Headers / -Max-Age on every response. * Short-circuits OPTIONS with 204, never invoking the mux. Wired into stats.go:ListenAndServe so the wrapping is unconditional (the node's security model gates writes by token + Ed25519 signature, not by origin, so wide CORS is the correct default). Cleaned up the now-redundant per-jsonOK/jsonErr Allow-Origin setters in api_common.go — the middleware sets a single consistent header instead of two collisions from handlers that both write one. Symptom before: `net::ERR_FAILED` / "CORS policy blocked" errors in the Electron devtools console when hitting /api/* or /relay/*. Symptom after: clean GET/POST, preflight answers in ~1ms. --- node/api_common.go | 2 -- node/cors.go | 32 ++++++++++++++++++++++++++++++++ node/stats.go | 5 ++++- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 node/cors.go diff --git a/node/api_common.go b/node/api_common.go index c574815..73a52e5 100644 --- a/node/api_common.go +++ b/node/api_common.go @@ -15,13 +15,11 @@ import ( func jsonOK(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") _ = json.NewEncoder(w).Encode(v) } func jsonErr(w http.ResponseWriter, err error, code int) { w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } diff --git a/node/cors.go b/node/cors.go new file mode 100644 index 0000000..1641439 --- /dev/null +++ b/node/cors.go @@ -0,0 +1,32 @@ +package node + +import "net/http" + +// withCORS wraps any http.Handler so every response carries the CORS +// headers browser-based clients (Electron renderer, web explorer from a +// different origin, mobile webview) need. Also short-circuits OPTIONS +// preflight requests with a 204 — without this, POST /api/tx with a +// JSON body triggers a preflight that the regular handler answers as +// 404/405 and the browser refuses the follow-up. +// +// The allow-list is wide on purpose. The node's security model doesn't +// rely on same-origin — API tokens (DCHAIN_API_TOKEN + DCHAIN_API_PRIVATE) +// and Ed25519 tx signatures are what gate writes. Cross-origin access is +// a first-class feature here, not an attack vector. +func withCORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + h.Set("Access-Control-Allow-Origin", "*") + h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH") + h.Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With") + h.Set("Access-Control-Expose-Headers", "Content-Length, Content-Type") + h.Set("Access-Control-Max-Age", "86400") // cache preflight for a day + + if r.Method == http.MethodOptions { + // Preflight. Don't hand to the mux — just answer. + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/node/stats.go b/node/stats.go index b0d29b7..9a39078 100644 --- a/node/stats.go +++ b/node/stats.go @@ -310,7 +310,10 @@ func (t *Tracker) ServeHTTP(q QueryFunc, fns ...func(*http.ServeMux)) http.Handl } // ListenAndServe starts the HTTP stats server on addr (e.g. ":8080"). +// All responses pass through withCORS so browser + Electron clients +// get correct Access-Control-* headers and preflight OPTIONS requests +// are answered with 204 instead of falling through to the 404 handler. func (t *Tracker) ListenAndServe(addr string, q QueryFunc, fns ...func(*http.ServeMux)) error { - handler := t.ServeHTTP(q, fns...) + handler := withCORS(t.ServeHTTP(q, fns...)) return http.ListenAndServe(addr, handler) }