package api import ( "net/http" "net/http/httptest" "net/url" "path/filepath" "strings" "testing" "hamlab/internal/auth" "hamlab/internal/config" "hamlab/internal/store" ) // A KVM's own login is a cookie its page sets in one browser. The hub remembers it from the // requests that carry it, lends it to a browser that lacks it — both on the wire to the // device and back to the browser, since the device's page checks for it client-side — and // forgets it when the device says it is gone or the KVM is removed. func TestKVMCookiesAreRememberedAndLent(t *testing.T) { var seen []string // the Cookie header the fake device received, per request device := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { seen = append(seen, r.Header.Get("Cookie")) if r.URL.Path == "/api/auth/login" { // a device that sets its token by header, with its own domain and Secure http.SetCookie(w, &http.Cookie{Name: "nano-kvm-token", Value: "tok-1", Domain: "10.0.0.217", Secure: true}) } if r.URL.Path == "/api/auth/logout" { http.SetCookie(w, &http.Cookie{Name: "nano-kvm-token", Value: "", MaxAge: -1}) } w.WriteHeader(200) })) defer device.Close() dir := t.TempDir() cfg := config.Default(filepath.Join(dir, "config.json")) if err := cfg.Update(func(c *config.Config) { c.KVMs = []config.KVM{{ID: "abc123", Name: "test", URL: device.URL, Machine: ""}} }); err != nil { t.Fatal(err) } st, err := store.Open(filepath.Join(dir, "state.json")) if err != nil { t.Fatal(err) } s := &Server{cfg: cfg, store: st, sessions: auth.NewSessions(st)} k, _ := s.kvm("abc123") p, err := s.kvmProxy(k) if err != nil { t.Fatal(err) } ask := func(path string, cookies ...*http.Cookie) *httptest.ResponseRecorder { r := httptest.NewRequest("GET", path, nil) r.SetPathValue("id", "abc123") for _, c := range cookies { r.AddCookie(c) } w := httptest.NewRecorder() if strings.HasPrefix(path, kvmPrefix) { s.serveKVM(w, r) } else { p.ServeHTTP(w, r) } return w } // 1. the device sets its token by header: it reaches the browser without the device's // domain or Secure (plain-http console), and the hub remembers it w := ask("/api/auth/login", &http.Cookie{Name: cookieName, Value: "hamlab-session"}) var set *http.Cookie for _, c := range w.Result().Cookies() { if c.Name == "nano-kvm-token" { set = c } } if set == nil || set.Domain != "" || set.Secure || set.Value != "tok-1" || set.Path != "/" { t.Fatalf("rewritten Set-Cookie = %+v, want nano-kvm-token=tok-1 for / without Domain or Secure", set) } if got := s.kvmCookies("abc123"); got["nano-kvm-token"] != "tok-1" { t.Fatalf("remembered = %v, want the token", got) } if strings.Contains(seen[0], "hamlab_session") { t.Fatalf("the device was handed Ham Lab's session: %q", seen[0]) } // 2. a browser that has the cookie teaches a newer value ask("/api/vm/info", &http.Cookie{Name: "nano-kvm-token", Value: "tok-2"}, &http.Cookie{Name: cookieName, Value: "x"}) if got := s.kvmCookies("abc123"); got["nano-kvm-token"] != "tok-2" { t.Fatalf("remembered = %v, want tok-2", got) } if seen[1] != "nano-kvm-token=tok-2" { t.Fatalf("device saw %q, want the browser's own cookie only", seen[1]) } // 3. a browser without it is lent it: on the wire, and back as a Set-Cookie it can read w = ask(kvmPrefix+"abc123/", &http.Cookie{Name: cookieName, Value: "x"}) if seen[2] != "nano-kvm-token=tok-2" { t.Fatalf("device saw %q from a browser with no cookie, want the remembered token", seen[2]) } lent := false for _, c := range w.Result().Cookies() { if c.Name == "nano-kvm-token" && c.Value == "tok-2" && !c.HttpOnly && c.Path == "/" && c.MaxAge > 0 { lent = true } } if !lent { t.Fatalf("browser was not lent the token: %v", w.Result().Cookies()) } // 4. the device logging out forgets it ask("/api/auth/logout", &http.Cookie{Name: "nano-kvm-token", Value: "tok-2"}) if got := s.kvmCookies("abc123"); got != nil { t.Fatalf("after logout remembered = %v, want nothing", got) } // 5. removing the KVM clears whatever is left s.rememberKVMCookies("abc123", []*http.Cookie{{Name: "nano-kvm-token", Value: "tok-3"}}) s.forgetKVMCookies("abc123") if got := s.kvmCookies("abc123"); got != nil { t.Fatalf("after remove remembered = %v, want nothing", got) } _ = url.Parse }