mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
144 lines
4.8 KiB
Go
144 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
func TestDecodeSecurityRequestBodyEnforcesWholeDocumentLimit(t *testing.T) {
|
|
prefix := `{"value":"`
|
|
suffix := `"}`
|
|
exact := prefix + strings.Repeat("a", int(maxSecurityRequestBodyBytes)-len(prefix)-len(suffix)) + suffix
|
|
|
|
var decoded struct {
|
|
Value string `json:"value"`
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(exact))
|
|
if err := decodeSecurityRequestBody(httptest.NewRecorder(), req, &decoded); err != nil {
|
|
t.Fatalf("exact-limit body rejected: %v", err)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(exact+" "))
|
|
err := decodeSecurityRequestBody(httptest.NewRecorder(), req, &decoded)
|
|
var maxBytesErr *http.MaxBytesError
|
|
if !errors.As(err, &maxBytesErr) {
|
|
t.Fatalf("over-limit trailing input error = %v, want *http.MaxBytesError", err)
|
|
}
|
|
}
|
|
|
|
func TestLoginRejectsOversizedAndConcatenatedJSON(t *testing.T) {
|
|
router := newLoginRouter(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "oversized trailing input",
|
|
body: `{"username":"admin","password":"wrong"}` + strings.Repeat(" ", int(maxSecurityRequestBodyBytes)),
|
|
wantStatus: http.StatusRequestEntityTooLarge,
|
|
},
|
|
{
|
|
name: "concatenated objects",
|
|
body: `{"username":"admin","password":"wrong"}{"username":"admin","password":"Password!1"}`,
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(tt.body))
|
|
req.RemoteAddr = "192.0.2.80:1234"
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleLogin(rec, req)
|
|
|
|
if rec.Code != tt.wantStatus {
|
|
t.Fatalf("status = %d, want %d (%s)", rec.Code, tt.wantStatus, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicBootstrapHandlersRejectOversizedJSONBeforeCredentialUse(t *testing.T) {
|
|
t.Run("validation", func(t *testing.T) {
|
|
router := &Router{
|
|
bootstrapTokenHash: "configured",
|
|
bootstrapTokenValidationLimiter: NewRateLimiter(10, 5*time.Minute),
|
|
}
|
|
t.Cleanup(router.bootstrapTokenValidationLimiter.Stop)
|
|
|
|
body := `{"token":"` + strings.Repeat("a", int(maxSecurityRequestBodyBytes)) + `"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/security/validate-bootstrap-token", strings.NewReader(body))
|
|
req.RemoteAddr = "192.0.2.81:1234"
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleValidateBootstrapToken(rec, req)
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want %d (%s)", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("quick setup", func(t *testing.T) {
|
|
cfg := &config.Config{DataPath: t.TempDir(), ConfigPath: t.TempDir()}
|
|
router := &Router{config: cfg}
|
|
clientIP := "192.0.2.82"
|
|
authLimiter.Reset(clientIP)
|
|
t.Cleanup(func() { authLimiter.Reset(clientIP) })
|
|
|
|
body := `{"username":"` + strings.Repeat("a", int(maxSecurityRequestBodyBytes)) + `"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body))
|
|
req.RemoteAddr = clientIP + ":1234"
|
|
rec := httptest.NewRecorder()
|
|
|
|
handleQuickSecuritySetupFixed(router)(rec, req)
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want %d (%s)", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
|
|
}
|
|
if cfg.AuthUser != "" || cfg.AuthPass != "" || cfg.HasAPITokens() {
|
|
t.Fatalf("oversized request changed auth state: user=%q pass_set=%v tokens=%d", cfg.AuthUser, cfg.AuthPass != "", len(cfg.APITokens))
|
|
}
|
|
})
|
|
|
|
t.Run("recovery", func(t *testing.T) {
|
|
router := &Router{mux: http.NewServeMux(), config: &config.Config{}}
|
|
router.registerAuthSecurityInstallRoutes()
|
|
|
|
body := `{"action":"` + strings.Repeat("a", int(maxSecurityRequestBodyBytes)) + `"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/api/security/recovery", strings.NewReader(body))
|
|
req.RemoteAddr = "127.0.0.1:1234"
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.mux.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want %d (%s)", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func FuzzDecodeSecurityRequestBody(f *testing.F) {
|
|
f.Add(`{"username":"admin","password":"Password!1"}`)
|
|
f.Add(`{"username":"admin"}{"password":"Password!1"}`)
|
|
f.Add("{")
|
|
f.Add("")
|
|
|
|
f.Fuzz(func(t *testing.T, body string) {
|
|
var decoded map[string]any
|
|
req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(body))
|
|
err := decodeSecurityRequestBody(httptest.NewRecorder(), req, &decoded)
|
|
if err == nil && int64(len(body)) > maxSecurityRequestBodyBytes {
|
|
t.Fatalf("accepted %d-byte body above %d-byte limit", len(body), maxSecurityRequestBodyBytes)
|
|
}
|
|
})
|
|
}
|