release: 0.1.0: Phases 0-11: panel, Postfix/DKIM, milter, backup, Docker image
Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`). Added: - Panel (Go, single static binary) with SQLite persistence, one-time - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and - Application (sender identity) management: SASL credentials via `sasldb2`, - Full Postfix relay config generated from env at container start: SMTPS 465, - Journal milter (pure Go, `go-milter`) recording every send to `send_log`; … (6 more in CHANGELOG) Full detail: CHANGELOG.md (0.1.0).
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user