Files
traefik-botfilter/botfilter_test.go
T

184 lines
6.3 KiB
Go

package traefik_botfilter
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestRequiredHeaderCreatesTemporaryBan(t *testing.T) {
cfg := CreateConfig()
cfg.RequireUserAgent = true
cfg.RequireAccept = true
cfg.RequireHost = true
nextCalls := 0
handler, err := New(context.Background(), http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
nextCalls++
rw.WriteHeader(http.StatusOK)
}), cfg, "test")
if err != nil {
t.Fatalf("New() error = %v", err)
}
first := httptest.NewRequest(http.MethodGet, "http://wiki.example/content/article", nil)
first.RemoteAddr = "203.0.113.11:54321"
first.Header.Set("Accept", "text/html")
first.Header.Del("User-Agent")
firstResponse := httptest.NewRecorder()
handler.ServeHTTP(firstResponse, first)
if firstResponse.Code != http.StatusForbidden {
t.Fatalf("first response status = %d, want %d", firstResponse.Code, http.StatusForbidden)
}
if firstResponse.Header().Get("Retry-After") == "" {
t.Fatal("first response did not include Retry-After")
}
second := httptest.NewRequest(http.MethodGet, "http://wiki.example/", nil)
second.RemoteAddr = "203.0.113.11:54321"
second.Header.Set("Accept", "text/html")
second.Header.Set("User-Agent", "Mozilla/5.0 Chrome/120.0 AppleWebKit/537.36 Safari/537.36")
secondResponse := httptest.NewRecorder()
handler.ServeHTTP(secondResponse, second)
if secondResponse.Code != http.StatusForbidden {
t.Fatalf("second response status = %d, want cached ban", secondResponse.Code)
}
if nextCalls != 0 {
t.Fatalf("next handler calls = %d, want 0", nextCalls)
}
}
func TestWhitelistBypassesFilter(t *testing.T) {
cfg := CreateConfig()
cfg.RequireUserAgent = true
cfg.WhitelistCIDRs = []string{"192.168.0.0/16"}
handler, err := New(context.Background(), http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}), cfg, "test")
if err != nil {
t.Fatalf("New() error = %v", err)
}
request := httptest.NewRequest(http.MethodGet, "http://wiki.example/", nil)
request.RemoteAddr = "192.168.30.25:1234"
request.Header.Del("User-Agent")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusNoContent {
t.Fatalf("response status = %d, want whitelist to reach next handler", response.Code)
}
}
func TestEncodedScanPathBansBeforeUpstream(t *testing.T) {
cfg := CreateConfig()
cfg.RequireUserAgent = false
cfg.RequireAccept = false
nextCalls := 0
handler, err := New(context.Background(), http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
nextCalls++
rw.WriteHeader(http.StatusOK)
}), cfg, "test")
if err != nil {
t.Fatalf("New() error = %v", err)
}
request := httptest.NewRequest(http.MethodGet, "http://wiki.example/foo/..%2F.env", nil)
request.RemoteAddr = "198.51.100.22:1234"
request.Header.Set("User-Agent", "Mozilla/5.0 Chrome/120.0 AppleWebKit/537.36 Safari/537.36")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("response status = %d, want %d", response.Code, http.StatusForbidden)
}
if nextCalls != 0 {
t.Fatalf("next handler calls = %d, want 0", nextCalls)
}
}
func Test404ScoreBansOnSubsequentRequest(t *testing.T) {
cfg := CreateConfig()
cfg.RequireUserAgent = false
cfg.RequireAccept = false
cfg.RandomArticlePatterns = nil
cfg.EmptyUserAgentScore = 0
cfg.MissingAcceptScore = 0
cfg.ScoreThreshold = 40
nextCalls := 0
handler, err := New(context.Background(), http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
nextCalls++
rw.WriteHeader(http.StatusNotFound)
}), cfg, "test")
if err != nil {
t.Fatalf("New() error = %v", err)
}
first := httptest.NewRequest(http.MethodGet, "http://wiki.example/not-found", nil)
first.RemoteAddr = "198.51.100.23:1234"
first.Header.Set("User-Agent", "Mozilla/5.0 Chrome/120.0 AppleWebKit/537.36 Safari/537.36")
first.Header.Set("Accept", "text/html")
firstResponse := httptest.NewRecorder()
handler.ServeHTTP(firstResponse, first)
if firstResponse.Code != http.StatusNotFound {
t.Fatalf("first response status = %d, want 404", firstResponse.Code)
}
second := httptest.NewRequest(http.MethodGet, "http://wiki.example/another-miss", nil)
second.RemoteAddr = "198.51.100.23:1234"
second.Header.Set("User-Agent", "Mozilla/5.0 Chrome/120.0 AppleWebKit/537.36 Safari/537.36")
second.Header.Set("Accept", "text/html")
secondResponse := httptest.NewRecorder()
handler.ServeHTTP(secondResponse, second)
if secondResponse.Code != http.StatusForbidden {
t.Fatalf("second response status = %d, want cached ban after two 404s", secondResponse.Code)
}
if nextCalls != 1 {
t.Fatalf("next handler calls = %d, want 1", nextCalls)
}
}
func TestTrustedProxyHeaderSeparatesClients(t *testing.T) {
cfg := CreateConfig()
cfg.RequireUserAgent = true
cfg.ClientIPHeader = "X-Forwarded-For"
cfg.TrustedProxyCIDRs = []string{"127.0.0.0/8"}
nextCalls := 0
handler, err := New(context.Background(), http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
nextCalls++
rw.WriteHeader(http.StatusNoContent)
}), cfg, "test")
if err != nil {
t.Fatalf("New() error = %v", err)
}
bad := httptest.NewRequest(http.MethodGet, "http://wiki.example/", nil)
bad.RemoteAddr = "127.0.0.1:1234"
bad.Header.Set("X-Forwarded-For", "198.51.100.61")
bad.Header.Del("User-Agent")
badResponse := httptest.NewRecorder()
handler.ServeHTTP(badResponse, bad)
if badResponse.Code != http.StatusForbidden {
t.Fatalf("bad client status = %d, want 403", badResponse.Code)
}
good := httptest.NewRequest(http.MethodGet, "http://wiki.example/", nil)
good.RemoteAddr = "127.0.0.1:1234"
good.Header.Set("X-Forwarded-For", "198.51.100.62")
good.Header.Set("User-Agent", "Mozilla/5.0 Chrome/120.0 AppleWebKit/537.36 Safari/537.36")
goodResponse := httptest.NewRecorder()
handler.ServeHTTP(goodResponse, good)
if goodResponse.Code != http.StatusNoContent {
t.Fatalf("good client status = %d, want 204", goodResponse.Code)
}
if nextCalls != 1 {
t.Fatalf("next handler calls = %d, want 1", nextCalls)
}
}
func TestCompileConfigRejectsInvalidCIDR(t *testing.T) {
cfg := CreateConfig()
cfg.WhitelistCIDRs = []string{"not-a-cidr"}
if _, err := compileConfig(cfg); err == nil {
t.Fatal("compileConfig() error = nil, want invalid CIDR error")
}
}