package session import ( "crypto/tls" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestNewID(t *testing.T) { a := NewID() b := NewID() if a == b { t.Fatal("NewID returned the same value twice") } if len(a) != 64 { // 32 random bytes, hex t.Fatalf("NewID() length = %d, want 64", len(a)) } for _, r := range a { if !strings.ContainsRune("0123456789abcdef", r) { t.Fatalf("NewID() = %q, want hex", a) } } } func TestSetSessionCookieAttributes(t *testing.T) { cases := []struct { name string tls bool forwarded string wantSecure bool }{ {"plain http dev", false, "", false}, {"direct tls", true, "", true}, {"behind https proxy", false, "https", true}, {"behind http proxy", false, "http", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodPost, "/", nil) if tc.tls { r.TLS = &tls.ConnectionState{} } if tc.forwarded != "" { r.Header.Set("X-Forwarded-Proto", tc.forwarded) } rr := httptest.NewRecorder() SetCookie(rr, r, "abc123") cookies := rr.Result().Cookies() if len(cookies) != 1 { t.Fatalf("got %d cookies, want 1", len(cookies)) } c := cookies[0] if c.Name != CookieName { t.Fatalf("cookie name = %q, want %q", c.Name, CookieName) } if c.Value != "abc123" { t.Fatalf("cookie value = %q, want the session id verbatim", c.Value) } if !c.HttpOnly { t.Fatal("cookie HttpOnly = false, want true") } if c.SameSite != http.SameSiteLaxMode { t.Fatalf("cookie SameSite = %v, want Lax", c.SameSite) } if c.Path != "/" { t.Fatalf("cookie Path = %q, want /", c.Path) } if c.Secure != tc.wantSecure { t.Fatalf("cookie Secure = %v, want %v", c.Secure, tc.wantSecure) } if c.MaxAge != int(SessionTTL/time.Second) { t.Fatalf("cookie MaxAge = %d, want %d", c.MaxAge, int(SessionTTL/time.Second)) } }) } } func TestClearSessionCookie(t *testing.T) { r := httptest.NewRequest(http.MethodPost, "/logout", nil) rr := httptest.NewRecorder() ClearCookie(rr, r) cookies := rr.Result().Cookies() if len(cookies) != 1 { t.Fatalf("got %d cookies, want 1", len(cookies)) } if cookies[0].MaxAge >= 0 { t.Fatalf("cleared cookie MaxAge = %d, want negative", cookies[0].MaxAge) } } func TestClientIP(t *testing.T) { cases := []struct { name string remoteAddr string xff []string want string }{ {"no header falls back to remote addr", "203.0.113.9:5555", nil, "203.0.113.9"}, {"single proxy hop", "10.0.0.1:5555", []string{"203.0.113.9"}, "203.0.113.9"}, { // The client sent "1.2.3.4" itself; Traefik appended the address it // actually saw. Only the rightmost entry is trustworthy. name: "spoofed left entry is ignored", remoteAddr: "10.0.0.1:5555", xff: []string{"1.2.3.4, 203.0.113.9"}, want: "203.0.113.9", }, { name: "spoofed separate header line is ignored", remoteAddr: "10.0.0.1:5555", xff: []string{"1.2.3.4", "203.0.113.9"}, want: "203.0.113.9", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodPost, "/", nil) r.RemoteAddr = tc.remoteAddr for _, v := range tc.xff { r.Header.Add("X-Forwarded-For", v) } if got := ClientIP(r); got != tc.want { t.Fatalf("ClientIP() = %q, want %q", got, tc.want) } }) } } func TestLoginLimiterBlocksAfterMaxFailures(t *testing.T) { l := NewLoginLimiter() now := time.Now() for i := 0; i < MaxFailures; i++ { if wait := l.RetryAfter("1.2.3.4", now); wait != 0 { t.Fatalf("blocked after %d failures, want block only after %d", i, MaxFailures) } l.Fail("1.2.3.4", now) } wait := l.RetryAfter("1.2.3.4", now) if wait <= 0 { t.Fatalf("retryAfter = %v after %d failures, want > 0", wait, MaxFailures) } if wait > Window { t.Fatalf("retryAfter = %v, want <= %v", wait, Window) } } func TestLoginLimiterWindowExpires(t *testing.T) { l := NewLoginLimiter() start := time.Now() for i := 0; i < MaxFailures; i++ { l.Fail("1.2.3.4", start) } if l.RetryAfter("1.2.3.4", start) == 0 { t.Fatal("expected block immediately after the failures") } later := start.Add(Window + time.Second) if wait := l.RetryAfter("1.2.3.4", later); wait != 0 { t.Fatalf("retryAfter = %v once the window passed, want 0", wait) } } func TestLoginLimiterResetClearsCounter(t *testing.T) { l := NewLoginLimiter() now := time.Now() for i := 0; i < MaxFailures; i++ { l.Fail("1.2.3.4", now) } l.Reset("1.2.3.4") if wait := l.RetryAfter("1.2.3.4", now); wait != 0 { t.Fatalf("retryAfter = %v after reset, want 0", wait) } } func TestLoginLimiterIsPerIP(t *testing.T) { l := NewLoginLimiter() now := time.Now() for i := 0; i < MaxFailures; i++ { l.Fail("1.2.3.4", now) } if wait := l.RetryAfter("5.6.7.8", now); wait != 0 { t.Fatalf("retryAfter for a different IP = %v, want 0", wait) } }