mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Two of the eight CodeQL alerts on PR #313 had clean, low-cost fixes: - go/clear-text-logging (#141, #142): the SiriusXM stub logged the raw Authorization header value at INFO. The header carries a long-lived bearer token (margeAuthToken) — capturing service logs would yield replayable credentials. Switch to logging only the boolean presence (`authPresent=%t`). - go/bad-redirect-check (#138): the Stockholm handler's bare-path redirect uses cfg.BasePath verbatim. basePath is operator-provided (CLI flag / STOCKHOLM_BASE_PATH env), not request input — but a value like "//evil.com" would still produce a scheme-relative redirect to an external host. Reject any leading-double-slash or embedded backslash at construction time so the redirect target can only ever be an absolute local path. The remaining CodeQL alerts are out of scope here: - go/request-forgery on proxy.go (#139, #140): the /api/http-proxy endpoint takes a user-provided url= parameter and fetches it by design — that's the whole point of the proxy. Mitigations already in place: isProxyLoop rejects self-references; the proxy is only reachable under a LAN trust model. - go/path-injection on static.go (#143, #144, #145): the path-traversal guard in resolveStaticFile (string-prefix check on absolute paths) is sound, but CodeQL doesn't trace it across the function boundary. A clearer refactor to filepath.Rel might silence the alert; deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
5.4 KiB
Go
154 lines
5.4 KiB
Go
package stockholm
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Handler is the main entry point for the Stockholm frontend backend.
|
|
type Handler struct {
|
|
cfg *Config
|
|
backendCfg *BackendConfig
|
|
state *NativeState
|
|
bridge *Bridge
|
|
stockholmDir string
|
|
}
|
|
|
|
// New initialises and returns a Stockholm Handler.
|
|
//
|
|
// stockholmDir is the path to the extracted Stockholm frontend (contains index.html).
|
|
// workspaceRoot is used to locate backend/state and backend/config directories.
|
|
// backendURL is the external URL of this service (used for config URL rewriting).
|
|
// basePath is the URL prefix at which the Stockholm UI is mounted (e.g. "/stockholm");
|
|
// pass "" to serve at the root.
|
|
func New(stockholmDir, workspaceRoot, backendURL, basePath string) (*Handler, error) {
|
|
if _, err := os.Stat(stockholmDir); err != nil {
|
|
return nil, fmt.Errorf("stockholm dir not found at %q: %w", stockholmDir, err)
|
|
}
|
|
|
|
cfg, err := LoadConfig(stockholmDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load stockholm config: %w", err)
|
|
}
|
|
|
|
if backendURL != "" {
|
|
margeURL := firstNonEmpty(os.Getenv("MARGE_URL"), backendURL)
|
|
|
|
authServiceURL := firstNonEmpty(os.Getenv("AUTH_SERVICE_URL"), backendURL)
|
|
if err := RewriteConfigURLs(stockholmDir, backendURL, margeURL, authServiceURL); err != nil {
|
|
log.Printf("[Stockholm] Warning: failed to rewrite config URLs: %v", err)
|
|
}
|
|
}
|
|
|
|
backendCfg := LoadBackendConfig(workspaceRoot)
|
|
|
|
stateDir := filepath.Join(workspaceRoot, "backend", "state")
|
|
if err := os.MkdirAll(stateDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("create state dir: %w", err)
|
|
}
|
|
|
|
state := NewNativeState(stateDir)
|
|
if err := state.Load(); err != nil {
|
|
log.Printf("[Stockholm] Warning: failed to load native state: %v", err)
|
|
}
|
|
|
|
// Normalise basePath: no trailing slash, must start with "/" or be empty.
|
|
if basePath != "" && !strings.HasPrefix(basePath, "/") {
|
|
basePath = "/" + basePath
|
|
}
|
|
|
|
basePath = strings.TrimRight(basePath, "/")
|
|
|
|
// Defence in depth: basePath is operator-provided (CLI flag /
|
|
// STOCKHOLM_BASE_PATH env var), not request input — but if it
|
|
// were ever set to "//evil.com" (typo or hostile env injection)
|
|
// the bare-path redirect below would go scheme-relative to
|
|
// evil.com. Reject any leading-double-slash and any backslash
|
|
// so the redirect target can only ever be an absolute local
|
|
// path. CodeQL go/bad-redirect-check raised the original
|
|
// concern.
|
|
if strings.HasPrefix(basePath, "//") || strings.HasPrefix(basePath, "/\\") || strings.ContainsAny(basePath, "\\") {
|
|
return nil, fmt.Errorf("invalid stockholm base path %q: must be an absolute path starting with a single '/'", basePath)
|
|
}
|
|
|
|
cfg.BasePath = basePath
|
|
|
|
state.SeedFromEnv(cfg)
|
|
|
|
bridge := newBridge(cfg, state)
|
|
|
|
return &Handler{
|
|
cfg: cfg,
|
|
backendCfg: backendCfg,
|
|
state: state,
|
|
bridge: bridge,
|
|
stockholmDir: stockholmDir,
|
|
}, nil
|
|
}
|
|
|
|
// Mount registers all Stockholm routes on the given chi router.
|
|
// API routes (/api/native/*, /api/http-proxy) are registered under cfg.BasePath
|
|
// because the patched JS uses window.__stockholmBase as a prefix for all API calls.
|
|
// Static content is served under cfg.BasePath (e.g. /stockholm) if set,
|
|
// otherwise at the root.
|
|
func (h *Handler) Mount(r chi.Router) {
|
|
apiBase := h.cfg.BasePath
|
|
r.Post(apiBase+"/api/native/appSend", h.bridge.HandleAppSend)
|
|
r.Get(apiBase+"/api/native/runQueue", h.bridge.HandleRunQueue)
|
|
r.HandleFunc(apiBase+"/api/http-proxy", h.handleProxy)
|
|
|
|
if h.cfg.BasePath != "" {
|
|
// Redirect bare /stockholm to /stockholm/ so the browser sets the correct
|
|
// base URL for relative asset references.
|
|
r.Get(h.cfg.BasePath, func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, h.cfg.BasePath+"/", http.StatusMovedPermanently)
|
|
})
|
|
// Strip the base path prefix before passing to handleStatic so that
|
|
// resolveStaticFile sees paths like "/" or "/index.html", not "/stockholm/".
|
|
// r.Route does NOT strip r.URL.Path, so we must use http.StripPrefix explicitly.
|
|
stripped := http.StripPrefix(h.cfg.BasePath, http.HandlerFunc(h.handleStatic))
|
|
r.Get(h.cfg.BasePath+"/", stripped.ServeHTTP)
|
|
r.Head(h.cfg.BasePath+"/", stripped.ServeHTTP)
|
|
r.Get(h.cfg.BasePath+"/*", stripped.ServeHTTP)
|
|
r.Head(h.cfg.BasePath+"/*", stripped.ServeHTTP)
|
|
} else {
|
|
// Serve static content at the root (catch-all at the end).
|
|
r.Get("/*", h.handleStatic)
|
|
r.Head("/*", h.handleStatic)
|
|
r.Get("/", h.handleStatic)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleProxy(w http.ResponseWriter, r *http.Request) {
|
|
HandleProxy(w, r, h.cfg, h.state)
|
|
}
|
|
|
|
func (h *Handler) handleStatic(w http.ResponseWriter, r *http.Request) {
|
|
ServeStatic(w, r, h.stockholmDir, h.backendCfg, h.state, h.cfg)
|
|
}
|
|
|
|
// HandleStatic is the exported form of handleStatic, needed when mounting the
|
|
// Stockholm static handler inside sub-routers (e.g. to resolve the /setup/ path
|
|
// collision between the management API and the Stockholm setup wizard pages).
|
|
func (h *Handler) HandleStatic(w http.ResponseWriter, r *http.Request) {
|
|
ServeStatic(w, r, h.stockholmDir, h.backendCfg, h.state, h.cfg)
|
|
}
|
|
|
|
// Config returns the loaded Stockholm config (for integration with the proxy handler
|
|
// that may need to inject BMX/marge headers).
|
|
func (h *Handler) Config() *Config {
|
|
return h.cfg
|
|
}
|
|
|
|
// State returns the NativeState (for integration with handlers that need to read
|
|
// auth tokens or account IDs).
|
|
func (h *Handler) State() *NativeState {
|
|
return h.state
|
|
}
|