A KVM tab: paste an address, get the device's own page
Ham Lab proxies each KVM from its own origin, which is what makes the simple version possible. A KVM that speaks only plain HTTP would otherwise be blocked outright as mixed content in a TLS console; served from here it loads, it is behind this login rather than a second one, and the iframe is same-origin. Nothing in it knows anything about any particular device. The prefix /kvm/<id>/ is only the entry point — a device UI asks for its assets by absolute path, which no prefix can rewrite away, so any request Ham Lab does not serve itself is routed back to whichever KVM the asking page belongs to, worked out from the Referer and from a cookie when there is no Referer, as on a WebSocket upgrade. Ham Lab's own routes always match first, so a device can never shadow the console, and the fallback is gated on a live session. The list is hub-side on purpose: a KVM earns its keep when the machine is down, which is when that machine's own Ham Lab cannot answer. On the keyboard, a page cannot forward keys into an iframe and does not need to — one with focus receives them directly. Fullscreen plus navigator.keyboard.lock() is what takes Escape, Tab and Ctrl-W back from the browser; that is Chromium-only and the tab says so rather than pretending otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e6625a540c
commit
fd88c4ff78
+32
-1
@@ -69,6 +69,7 @@ type Server struct {
|
||||
storage *storage.Manager
|
||||
space *space.Manager
|
||||
update updateState
|
||||
kvms kvmProxies
|
||||
fsLocal files.FS
|
||||
fsSudo files.FS // nil without passwordless sudo
|
||||
nodes *nodes.Manager
|
||||
@@ -284,7 +285,20 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
m.HandleFunc("PUT /api/prefs/sampler", s.auth(s.putSampler))
|
||||
m.HandleFunc("GET /api/prefs/history", s.auth(s.getHistoryWindow))
|
||||
m.HandleFunc("PUT /api/prefs/history", s.auth(s.putHistoryWindow))
|
||||
m.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { jsonErr(w, 404, "not found") })
|
||||
m.HandleFunc("GET /api/kvms", s.auth(s.listKVMs))
|
||||
m.HandleFunc("POST /api/kvms", s.auth(s.addKVM))
|
||||
m.HandleFunc("DELETE /api/kvms/{id}", s.auth(s.removeKVM))
|
||||
m.HandleFunc("/kvm/{id}/{rest...}", s.auth(s.serveKVM))
|
||||
m.HandleFunc("/kvm/{id}", s.auth(s.serveKVM))
|
||||
// A path Ham Lab does not serve may still belong to a KVM's page, which asks for its
|
||||
// own assets by absolute path. Ham Lab's own routes matched first, so this can only
|
||||
// catch what would otherwise have been a 404.
|
||||
m.HandleFunc("/api/", s.auth(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.kvmFallback(w, r) {
|
||||
return
|
||||
}
|
||||
jsonErr(w, 404, "not found")
|
||||
}))
|
||||
m.HandleFunc("/", s.static)
|
||||
s.mux = m
|
||||
return s
|
||||
@@ -355,6 +369,17 @@ func (s *Server) setCookie(w http.ResponseWriter, r *http.Request, tok string, m
|
||||
})
|
||||
}
|
||||
|
||||
// signedIn reports whether a request carries a live browser session, without the
|
||||
// side effects of the auth middleware.
|
||||
func (s *Server) signedIn(r *http.Request) bool {
|
||||
c, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, ok := s.sessions.Lookup(c.Value, clientKey(r))
|
||||
return ok
|
||||
}
|
||||
|
||||
// auth accepts the browser session cookie or a bearer API token issued to a hub.
|
||||
func (s *Server) auth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2445,6 +2470,12 @@ func (s *Server) static(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// A path with no file of ours behind it may belong to a KVM's page, which asks for its
|
||||
// own assets by absolute path. Checked here rather than in the router because this
|
||||
// handler is the catch-all — and gated on a session, since this handler is public.
|
||||
if s.signedIn(r) && s.kvmFallback(w, r) {
|
||||
return
|
||||
}
|
||||
// SPA fallback
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
http.ServeFileFS(w, r, s.ui, "index.html")
|
||||
|
||||
Reference in New Issue
Block a user