diff --git a/main.go b/badger.go similarity index 100% rename from main.go rename to badger.go diff --git a/badger_test.go b/badger_test.go new file mode 100644 index 0000000..f8ab045 --- /dev/null +++ b/badger_test.go @@ -0,0 +1,187 @@ +package badger_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + badger "github.com/fosrl/badger" +) + +// newTestHandler builds a Badger handler with the given config, failing the test +// if construction returns an unexpected error. +func newTestHandler(t *testing.T, cfg *badger.Config, next http.Handler) http.Handler { + t.Helper() + h, err := badger.New(context.Background(), next, cfg, "test") + if err != nil { + t.Fatalf("New() returned unexpected error: %v", err) + } + return h +} + +func TestCreateConfig(t *testing.T) { + if badger.CreateConfig() == nil { + t.Fatal("CreateConfig() returned nil") + } +} + +func TestNewRequiresFieldsWhenForwardAuthEnabled(t *testing.T) { + cases := map[string]*badger.Config{ + "missing apiBaseUrl": { + UserSessionCookieName: "p_session_token", + ResourceSessionRequestParam: "p_session_request", + }, + "missing userSessionCookieName": { + APIBaseUrl: "http://localhost:3001", + ResourceSessionRequestParam: "p_session_request", + }, + "missing resourceSessionRequestParam": { + APIBaseUrl: "http://localhost:3001", + UserSessionCookieName: "p_session_token", + }, + } + + for name, cfg := range cases { + t.Run(name, func(t *testing.T) { + cfg.DisableDefaultCFIPs = true + if _, err := badger.New(context.Background(), nil, cfg, "test"); err == nil { + t.Fatalf("expected error for %q, got nil", name) + } + }) + } +} + +func TestNewSucceedsWhenForwardAuthDisabled(t *testing.T) { + cfg := &badger.Config{ + DisableForwardAuth: true, + DisableDefaultCFIPs: true, + } + h, err := badger.New(context.Background(), http.NotFoundHandler(), cfg, "test") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if h == nil { + t.Fatal("expected non-nil handler") + } +} + +func TestNewRejectsInvalidCIDR(t *testing.T) { + cfg := &badger.Config{ + DisableForwardAuth: true, + DisableDefaultCFIPs: true, + TrustIP: []string{"not-a-cidr"}, + } + if _, err := badger.New(context.Background(), http.NotFoundHandler(), cfg, "test"); err == nil { + t.Fatal("expected error for invalid CIDR, got nil") + } +} + +func TestServeHTTPDisableForwardAuthCallsNext(t *testing.T) { + called := false + next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + called = true + rw.WriteHeader(http.StatusOK) + }) + cfg := &badger.Config{DisableForwardAuth: true, DisableDefaultCFIPs: true} + h := newTestHandler(t, cfg, next) + + rw := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + h.ServeHTTP(rw, req) + + if !called { + t.Fatal("expected next handler to be called") + } + if rw.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, rw.Code) + } +} + +func TestRealIPUntrustedUsesDirectIPAndStripsCFHeaders(t *testing.T) { + var forwarded *http.Request + next := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + forwarded = req + }) + cfg := &badger.Config{DisableForwardAuth: true, DisableDefaultCFIPs: true} + h := newTestHandler(t, cfg, next) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.RemoteAddr = "198.51.100.7:9999" + req.Header.Set("CF-Connecting-IP", "1.2.3.4") + h.ServeHTTP(httptest.NewRecorder(), req) + + if got := forwarded.Header.Get("X-Real-Ip"); got != "198.51.100.7" { + t.Fatalf("expected X-Real-Ip from direct remote addr, got %q", got) + } + if got := forwarded.Header.Get("CF-Connecting-IP"); got != "" { + t.Fatalf("expected CF-Connecting-IP to be stripped for untrusted source, got %q", got) + } +} + +func TestRealIPTrustedProxyUsesCustomHeader(t *testing.T) { + var forwarded *http.Request + next := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + forwarded = req + }) + cfg := &badger.Config{ + DisableForwardAuth: true, + DisableDefaultCFIPs: true, + TrustIP: []string{"192.0.2.0/24"}, + CustomIPHeader: "X-Custom-IP", + } + h := newTestHandler(t, cfg, next) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.RemoteAddr = "192.0.2.10:12345" + req.Header.Set("X-Custom-IP", "203.0.113.5") + h.ServeHTTP(httptest.NewRecorder(), req) + + if got := forwarded.Header.Get("X-Forwarded-For"); got != "203.0.113.5" { + t.Fatalf("expected X-Forwarded-For from custom header, got %q", got) + } + if got := forwarded.Header.Get("X-Real-Ip"); got != "203.0.113.5" { + t.Fatalf("expected X-Real-Ip from custom header, got %q", got) + } +} + +func TestStripSessionCookiesPreservesUnrelated(t *testing.T) { + verify := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/badger/verify-session" { + http.Error(rw, "unexpected path", http.StatusNotFound) + return + } + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte(`{"data":{"valid":true}}`)) + })) + defer verify.Close() + + var forwarded *http.Request + next := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + forwarded = req + }) + cfg := &badger.Config{ + APIBaseUrl: verify.URL, + UserSessionCookieName: "p_session_token", + ResourceSessionRequestParam: "p_session_request", + DisableDefaultCFIPs: true, + } + h := newTestHandler(t, cfg, next) + + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.Header.Set("Cookie", "p_session_token=secret; other=keep") + h.ServeHTTP(httptest.NewRecorder(), req) + + if forwarded == nil { + t.Fatal("expected next handler to be called for valid session") + } + cookie := forwarded.Header.Get("Cookie") + if strings.Contains(cookie, "p_session_token") { + t.Fatalf("expected session cookie to be stripped, got %q", cookie) + } + if !strings.Contains(cookie, "other=keep") { + t.Fatalf("expected unrelated cookie to be preserved, got %q", cookie) + } +} diff --git a/ips/ips_test.go b/ips/ips_test.go new file mode 100644 index 0000000..251399b --- /dev/null +++ b/ips/ips_test.go @@ -0,0 +1,18 @@ +package ips + +import ( + "net" + "testing" +) + +func TestCFIPs(t *testing.T) { + cidrs := CFIPs() + if len(cidrs) == 0 { + t.Fatal("CFIPs() returned an empty list") + } + for _, cidr := range cidrs { + if _, _, err := net.ParseCIDR(cidr); err != nil { + t.Errorf("CFIPs() returned invalid CIDR %q: %v", cidr, err) + } + } +}