All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m41s
- Implement storage backend deletion, which automatically resets default storage settings and user-specific overrides when a backend is removed. - Add unit tests covering the delete action and its cleanup side effects. - Improve admin UI responsiveness, fixing table scrolling, flex wrapping, and text truncation for long storage backend names. - Update security documentation to clarify trusted proxy configurations and explain how trusted proxies are protected from automatic bans.
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"warpbox.dev/backend/libs/services"
|
|
)
|
|
|
|
func TestBansMiddlewareBlocksActiveBan(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
if _, err := bans.CreateManualBan("203.0.113.20", "test", "admin", time.Now().UTC().Add(time.Hour)); err != nil {
|
|
t.Fatalf("CreateManualBan returned error: %v", err)
|
|
}
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Fatal("next handler should not be called")
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, nil))
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
request.RemoteAddr = "127.0.0.1:6070"
|
|
request.Header.Set("X-Forwarded-For", "203.0.113.20")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusForbidden || response.Body.String() != "forbidden\n" {
|
|
t.Fatalf("blocked response = %d %q", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestBansMiddlewareAllowsNonBannedIP(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
called := false
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
_, _ = io.WriteString(w, "ok")
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, nil))
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
request.RemoteAddr = "203.0.113.21:6070"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
|
|
if !called || response.Code != http.StatusOK {
|
|
t.Fatalf("allowed response = called %v code %d", called, response.Code)
|
|
}
|
|
}
|
|
|
|
func TestBansMiddlewareAutoBansMaliciousPaths(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
settings, err := bans.Settings()
|
|
if err != nil {
|
|
t.Fatalf("Settings returned error: %v", err)
|
|
}
|
|
settings.AutoBanEnabled = true
|
|
settings.MaliciousPathThreshold = 3
|
|
if err := bans.UpdateSettings(settings); err != nil {
|
|
t.Fatalf("UpdateSettings returned error: %v", err)
|
|
}
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.NotFound(w, r)
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, nil))
|
|
|
|
for i := 0; i < 3; i++ {
|
|
request := httptest.NewRequest(http.MethodGet, "/.env", nil)
|
|
request.RemoteAddr = "203.0.113.22:6070"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if i < 2 && response.Code == http.StatusForbidden {
|
|
t.Fatalf("request %d blocked before threshold", i+1)
|
|
}
|
|
if i == 2 && response.Code != http.StatusForbidden {
|
|
t.Fatalf("request 3 status = %d, want forbidden", response.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBansMiddlewareSkipsAutoBanWhenDisabled(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.NotFound(w, r)
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, nil))
|
|
|
|
for i := 0; i < 5; i++ {
|
|
request := httptest.NewRequest(http.MethodGet, "/.env", nil)
|
|
request.RemoteAddr = "203.0.113.23:6070"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code == http.StatusForbidden {
|
|
t.Fatalf("request %d was blocked while auto-ban disabled", i+1)
|
|
}
|
|
}
|
|
if _, ok, err := bans.Match("203.0.113.23", time.Now().UTC()); err != nil || ok {
|
|
t.Fatalf("disabled auto-ban Match = %v, %v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestBansMiddlewareDoesNotBlockProtectedProxyIP(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
if _, err := bans.CreateManualBan("127.0.0.1", "bad historical ban", "admin", time.Now().UTC().Add(time.Hour)); err != nil {
|
|
t.Fatalf("CreateManualBan returned error: %v", err)
|
|
}
|
|
called := false
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
_, _ = io.WriteString(w, "ok")
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, []string{"127.0.0.1"}))
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
request.RemoteAddr = "127.0.0.1:6070"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
|
|
if !called || response.Code != http.StatusOK {
|
|
t.Fatalf("protected proxy response = called %v code %d", called, response.Code)
|
|
}
|
|
}
|
|
|
|
func TestBansMiddlewareDoesNotAutoBanProtectedProxyIP(t *testing.T) {
|
|
bans := newMiddlewareBanService(t)
|
|
settings, err := bans.Settings()
|
|
if err != nil {
|
|
t.Fatalf("Settings returned error: %v", err)
|
|
}
|
|
settings.AutoBanEnabled = true
|
|
settings.MaliciousPathThreshold = 1
|
|
if err := bans.UpdateSettings(settings); err != nil {
|
|
t.Fatalf("UpdateSettings returned error: %v", err)
|
|
}
|
|
handler := Chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.NotFound(w, r)
|
|
}), Bans(slog.New(slog.NewTextHandler(io.Discard, nil)), bans, []string{"127.0.0.1"}))
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/.env", nil)
|
|
request.RemoteAddr = "127.0.0.1:6070"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
|
|
if response.Code == http.StatusForbidden {
|
|
t.Fatalf("protected proxy was auto-banned")
|
|
}
|
|
if _, ok, err := bans.Match("127.0.0.1", time.Now().UTC()); err != nil || ok {
|
|
t.Fatalf("protected proxy Match = %v, %v", ok, err)
|
|
}
|
|
}
|
|
|
|
func newMiddlewareBanService(t *testing.T) *services.BanService {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
upload, err := services.NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default())
|
|
if err != nil {
|
|
t.Fatalf("NewUploadService returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := upload.Close(); err != nil {
|
|
t.Fatalf("Close returned error: %v", err)
|
|
}
|
|
})
|
|
bans, err := services.NewBanService(upload.DB())
|
|
if err != nil {
|
|
t.Fatalf("NewBanService returned error: %v", err)
|
|
}
|
|
return bans
|
|
}
|