diff --git a/universalClient/api/routes.go b/universalClient/api/routes.go index 75e9ffa7..50c64604 100644 --- a/universalClient/api/routes.go +++ b/universalClient/api/routes.go @@ -6,8 +6,8 @@ import "net/http" func (s *Server) setupRoutes() *http.ServeMux { mux := http.NewServeMux() - // Health check endpoint - mux.HandleFunc("/health", s.handleHealth) + // Health check endpoint — GET only; other methods return 405 Method Not Allowed. + mux.HandleFunc("GET /health", s.handleHealth) return mux } diff --git a/universalClient/api/routes_test.go b/universalClient/api/routes_test.go index 63a4e2e1..6367539a 100644 --- a/universalClient/api/routes_test.go +++ b/universalClient/api/routes_test.go @@ -17,19 +17,45 @@ func TestSetupRoutes(t *testing.T) { mux := server.setupRoutes() - // Test that all routes are registered correctly testCases := []struct { name string + method string path string expectedStatus int }{ { - name: "Health endpoint", + name: "GET /health is allowed", + method: http.MethodGet, path: "/health", expectedStatus: http.StatusOK, }, { - name: "Non-existent endpoint", + name: "POST /health is rejected", + method: http.MethodPost, + path: "/health", + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "PUT /health is rejected", + method: http.MethodPut, + path: "/health", + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "DELETE /health is rejected", + method: http.MethodDelete, + path: "/health", + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "PATCH /health is rejected", + method: http.MethodPatch, + path: "/health", + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "Non-existent endpoint returns 404", + method: http.MethodGet, path: "/api/v1/non-existent", expectedStatus: http.StatusNotFound, }, @@ -37,7 +63,7 @@ func TestSetupRoutes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, tc.path, nil) + req := httptest.NewRequest(tc.method, tc.path, nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) diff --git a/universalClient/api/server.go b/universalClient/api/server.go index 7abe2723..99c554cc 100644 --- a/universalClient/api/server.go +++ b/universalClient/api/server.go @@ -26,8 +26,12 @@ func NewServer(logger zerolog.Logger, port int) *Server { mux := s.setupRoutes() s.server = &http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: mux, + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, // max time to receive request headers + ReadTimeout: 10 * time.Second, // max time to read full request + WriteTimeout: 10 * time.Second, // max time to write response + IdleTimeout: 60 * time.Second, // max keep-alive idle time } return s diff --git a/universalClient/api/server_test.go b/universalClient/api/server_test.go index 10609230..beb77f15 100644 --- a/universalClient/api/server_test.go +++ b/universalClient/api/server_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -105,3 +106,16 @@ func TestServerIntegration(t *testing.T) { assert.Equal(t, "text/plain", resp.Header.Get("Content-Type")) }) } + +// TestServerHasTimeoutsConfigured verifies the http.Server is constructed with +// timeout fields set, defeating Slowloris-style slow-client DoS attacks. +func TestServerHasTimeoutsConfigured(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) + server := NewServer(logger, 0) + + assert.Greater(t, server.server.ReadHeaderTimeout, time.Duration(0), "ReadHeaderTimeout must be set (Slowloris guard)") + assert.Greater(t, server.server.ReadTimeout, time.Duration(0), "ReadTimeout must be set") + assert.Greater(t, server.server.WriteTimeout, time.Duration(0), "WriteTimeout must be set") + assert.Greater(t, server.server.IdleTimeout, time.Duration(0), "IdleTimeout must be set") +} +