Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions universalClient/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
34 changes: 30 additions & 4 deletions universalClient/api/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,53 @@ 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,
},
}

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)
Expand Down
8 changes: 6 additions & 2 deletions universalClient/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions universalClient/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"testing"
"time"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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")
}

Loading