Files
mailgoserver/internal/webui/ratelimit_test.go
T

96 lines
3.6 KiB
Go

package webui
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// TestWebmailLoginAccountLockout confirms repeated wrong-password attempts against
// one account eventually get refused with a generic lockout message rather than
// checking the password at all, and that the lockout doesn't touch a different
// account from the same IP (the per-IP throttle, not this per-account layer, would
// apply there).
func TestWebmailLoginAccountLockout(t *testing.T) {
app := newTestApp(t)
app.Cfg.Section("Auth").Key("login_attempt_limit").SetValue("3")
mux := app.Mux()
domains, _ := app.DB.ListDomains()
victimID := createTestMailboxWithPassword(t, app, "lockout-victim@example.com", domains[0].ID, "the-real-password-1!")
otherID := createTestMailboxWithPassword(t, app, "lockout-other@example.com", domains[0].ID, "another-password-1!")
attempt := func(email, password string) *httptest.ResponseRecorder {
form := url.Values{"email": {email}, "password": {password}}
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.RemoteAddr = "203.0.113.9:12345"
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
return rec
}
for i := 0; i < 3; i++ {
rec := attempt("lockout-victim@example.com", "wrong password")
if !strings.Contains(rec.Body.String(), "Incorrect email or password") {
t.Fatalf("attempt %d: expected a normal wrong-password error, got: %s", i, rec.Body.String())
}
}
locked := attempt("lockout-victim@example.com", "the-real-password-1!") // even the CORRECT password now
if !strings.Contains(locked.Body.String(), "Too many failed attempts") {
t.Fatalf("expected the account locked out after repeated failures, got: %s", locked.Body.String())
}
if locked.Result().Cookies() != nil {
for _, c := range locked.Result().Cookies() {
if c.Name == mailboxSessionCookieName && c.Value != "" {
t.Fatal("expected no session granted while locked out, even with the correct password")
}
}
}
// A different account from the same IP is unaffected by the per-account lockout.
rec := attempt("lockout-other@example.com", "another-password-1!")
found := false
for _, c := range rec.Result().Cookies() {
if c.Name == mailboxSessionCookieName {
found = true
}
}
if !found {
t.Fatal("expected a different account from the same IP to log in normally")
}
_ = victimID
_ = otherID
}
// TestLoginRateLimitPerIP confirms the per-IP throttle kicks in independent of which
// account is being tried, once enough requests arrive from one source IP.
func TestLoginRateLimitPerIP(t *testing.T) {
app := newTestApp(t)
app.loginLimiter = newIPRateLimiter(3, 1<<62) // tiny limit, effectively-infinite window for a deterministic test
mux := app.Mux()
domains, _ := app.DB.ListDomains()
createTestMailboxWithPassword(t, app, "ratelimit@example.com", domains[0].ID, "correct-password-1!")
attempt := func() *httptest.ResponseRecorder {
form := url.Values{"email": {"ratelimit@example.com"}, "password": {"correct-password-1!"}}
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.RemoteAddr = "203.0.113.10:12345"
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
return rec
}
for i := 0; i < 3; i++ {
if rec := attempt(); rec.Code == http.StatusTooManyRequests {
t.Fatalf("attempt %d: unexpectedly rate-limited early", i)
}
}
if rec := attempt(); rec.Code != http.StatusTooManyRequests {
t.Fatalf("expected 429 past the per-IP limit, got %d", rec.Code)
}
}