package web import ( "net/http" "net/http/httptest" "strings" "testing" ) // The __Host- prefix is only valid on a Secure cookie: getting this condition // backwards would make the development instance fail to log in at all, and // silently — the browser discards the Set-Cookie and the panel just shows the // login form again. func TestSessionCookieNameFollowsCookieSecure(t *testing.T) { secure := &Server{cfg: Config{CookieSecure: true}} if got := secure.sessionCookie(); got != "__Host-selfpost_session" { t.Errorf("with TLS the cookie is named %q, want the __Host- prefixed name", got) } plain := &Server{cfg: Config{CookieSecure: false}} if got := plain.sessionCookie(); got != "selfpost_session" { t.Errorf("without TLS the cookie is named %q, want the bare name", got) } } // A neighbouring host on the same registrable domain can set a cookie by the // same name; the browser then sends both, oldest first. Picking one at random // would leave the administrator in a login loop with no explanation, so the // request counts as signed out instead. func TestSessionTokenRejectsDuplicates(t *testing.T) { s := &Server{cfg: Config{CookieSecure: false}} r := httptest.NewRequest(http.MethodGet, "http://panel.example.com/domains", nil) r.AddCookie(&http.Cookie{Name: "selfpost_session", Value: "planted-by-a-neighbour"}) r.AddCookie(&http.Cookie{Name: "selfpost_session", Value: "the-real-session"}) if token, ok := s.sessionToken(r); ok { t.Fatalf("duplicate cookies accepted, token = %q", token) } } func TestSessionTokenReadsOneCookie(t *testing.T) { s := &Server{cfg: Config{CookieSecure: true}} r := httptest.NewRequest(http.MethodGet, "http://panel.example.com/domains", nil) r.AddCookie(&http.Cookie{Name: "__Host-selfpost_session", Value: "the-real-session"}) token, ok := s.sessionToken(r) if !ok || token != "the-real-session" { t.Fatalf("sessionToken = %q, %t; want the cookie's value", token, ok) } } // A cookie under the other deployment's name is not this deployment's session: // after an upgrade the pre-14 cookie must not be honoured as if it were the // prefixed one. func TestSessionTokenIgnoresTheOtherName(t *testing.T) { s := &Server{cfg: Config{CookieSecure: true}} r := httptest.NewRequest(http.MethodGet, "http://panel.example.com/domains", nil) r.AddCookie(&http.Cookie{Name: "selfpost_session", Value: "left-over-from-an-older-build"}) if _, ok := s.sessionToken(r); ok { t.Fatal("the unprefixed cookie was accepted on a TLS deployment") } } func TestRequireAuthRejectsDuplicateCookies(t *testing.T) { s := &Server{cfg: Config{CookieSecure: false}, sessions: newTestSessionStore(t)} token := s.sessions.Create("admin") reached := false h := s.requireAuth(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true })) r := httptest.NewRequest(http.MethodGet, "http://panel.example.com/domains", nil) r.AddCookie(&http.Cookie{Name: "selfpost_session", Value: "planted-by-a-neighbour"}) r.AddCookie(&http.Cookie{Name: "selfpost_session", Value: token}) rec := httptest.NewRecorder() h.ServeHTTP(rec, r) if reached { t.Fatal("the handler ran even though the session cookie was shadowed") } if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/login" { t.Fatalf("status = %d, Location = %q; want a redirect to /login", rec.Code, rec.Header().Get("Location")) } } // Signing out has to expire the cookie under both names, or the cookie left // over from a pre-__Host- build stays in the browser for the rest of its life. func TestLogoutClearsBothCookieNames(t *testing.T) { s := &Server{cfg: Config{CookieSecure: true}, sessions: newTestSessionStore(t)} token := s.sessions.Create("admin") r := httptest.NewRequest(http.MethodPost, "http://panel.example.com/logout", nil) r.Host = "panel.example.com" r.AddCookie(&http.Cookie{Name: "__Host-selfpost_session", Value: token}) rec := httptest.NewRecorder() s.handleLogout(rec, r) if _, ok := s.sessions.Lookup(token); ok { t.Error("the session survived sign-out") } set := rec.Header().Values("Set-Cookie") for _, name := range []string{"selfpost_session=", "__Host-selfpost_session="} { var found bool for _, c := range set { if strings.HasPrefix(c, name) && strings.Contains(c, "Max-Age=0") { found = true } } if !found { t.Errorf("sign-out does not expire a cookie named %q: %v", strings.TrimSuffix(name, "="), set) } } }