diff --git a/SENSORS.md b/SENSORS.md index 2634379..b404c8c 100644 --- a/SENSORS.md +++ b/SENSORS.md @@ -202,3 +202,10 @@ closed while the page is hidden, refilled when it returns. `web/src/sampleAdapte the seam that hands the stock modules the object they were written against. The old `/api/metrics` and `/api/metrics/history` are gone. `hamlab.sample_interval` is the sampler's own rate, as a sensor. + +## From another origin + +The sensor endpoints (and only they) answer any origin: `Access-Control-Allow-Origin: *`, +preflight included. `*` is the one value browsers won't combine with cookies, so a foreign +page can reach the API with a token and can never ride a logged-in console's session. +`examples/external-demo/` is a complete outside consumer in one HTML file. diff --git a/examples/external-demo/README.md b/examples/external-demo/README.md new file mode 100644 index 0000000..52f4890 --- /dev/null +++ b/examples/external-demo/README.md @@ -0,0 +1,17 @@ +# External demo + +`index.html` is a stranger's page: one file, no build, no framework, served from a +different origin than Ham Lab. It knows a base URL and a read-only sensor token +(`config.js`, not in git) and nothing else. Each tab is one shaped query POSTed to +`/api/sensors/query?full=1` on an interval; sparklines are seeded from +`/api/sensors/history`; the Fleet tab is one call to `/api/fleet/sensors/query`. + +```js +// config.js +window.HAMLAB = { base: 'http://10.0.0.234:8100', token: '' } +``` + +Anyone who can load the page can read the token, so serve it only where that's acceptable +(it can read sensors and nothing else). Deployed on `core` as the `hamlab-demo` stack — +see Hamlet `docs/docker.md`. Update it with +`cp examples/external-demo/index.html /home/core/apps/hamlab-demo/site/`. diff --git a/examples/external-demo/index.html b/examples/external-demo/index.html new file mode 100644 index 0000000..d3e7910 --- /dev/null +++ b/examples/external-demo/index.html @@ -0,0 +1,179 @@ + + + + + + +Ham Lab sensors — external demo + + + + +
+

Ham Lab sensors

external demo · + + + +
+ +
+
+
What this tab asked, and what came back
+
+ + + + diff --git a/internal/api/server.go b/internal/api/server.go index 2b5aa0a..2380216 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -186,6 +186,19 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me return s } +// sensorPath reports whether a path belongs to the read-only sensor API. +func sensorPath(p string) bool { + if p == "/metrics" || p == "/api/sensors" || strings.HasPrefix(p, "/api/sensors/") || strings.HasPrefix(p, "/api/fleet/sensors/") { + return true + } + if rest, ok := strings.CutPrefix(p, "/api/nodes/"); ok { + if _, after, ok := strings.Cut(rest, "/"); ok { + return after == "sensors" || strings.HasPrefix(after, "sensors/") + } + } + return false +} + // Handler wraps the mux with headers a root-capable console should always send. func (s *Server) Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -197,6 +210,19 @@ func (s *Server) Handler() http.Handler { h.Set("Content-Security-Policy", "frame-ancestors 'none'") } h.Set("X-Content-Type-Options", "nosniff") + // The sensor API is for other people's pages too. Any origin may read it — with a + // token, never the session: "*" is the one value browsers refuse to pair with + // cookies, so a foreign page can't ride a logged-in console. Nothing else is opened. + if sensorPath(r.URL.Path) { + h.Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + h.Set("Access-Control-Allow-Methods", "GET, POST") + h.Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + h.Set("Access-Control-Max-Age", "86400") + w.WriteHeader(http.StatusNoContent) + return + } + } h.Set("Referrer-Policy", "no-referrer") s.mux.ServeHTTP(w, r) })