From 9faa9709eea99faea4414ae16a4bc6e339f4b17a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:28:48 +0000 Subject: [PATCH 1/2] Initial plan From 3e68a2de00a7a314adb58f6265dc186ad03d291e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:33:25 +0000 Subject: [PATCH 2/2] refactor(server): reuse shared request body restore logic in HMAC middleware --- internal/server/hmac.go | 14 ++++---------- internal/server/hmac_test.go | 23 +++++++++++++++++++++++ internal/server/http_helpers.go | 19 +++++++++++++++---- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/internal/server/hmac.go b/internal/server/hmac.go index 92b6875c2..b5646e73a 100644 --- a/internal/server/hmac.go +++ b/internal/server/hmac.go @@ -1,11 +1,9 @@ package server import ( - "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" - "io" "net/http" "strconv" "sync" @@ -167,14 +165,10 @@ func hmacMiddleware(secret string, next http.HandlerFunc) http.HandlerFunc { } // Read and restore body for downstream handlers - var body []byte - if r.Body != nil && r.Body != http.NoBody { - body, err = io.ReadAll(r.Body) - if err != nil { - rejectRequest(w, r, http.StatusBadRequest, "bad_request", "failed to read request body", "auth", "hmac_validation_failed", "body_read_error") - return - } - r.Body = io.NopCloser(bytes.NewReader(body)) + body, err := readAndRestoreRequestBody(r) + if err != nil { + rejectRequest(w, r, http.StatusBadRequest, "bad_request", "failed to read request body", "auth", "hmac_validation_failed", "body_read_error") + return } expected := computeHMAC(secret, timestamp, nonce, r.URL.Path, body) diff --git a/internal/server/hmac_test.go b/internal/server/hmac_test.go index 6069922aa..d66fc0f5f 100644 --- a/internal/server/hmac_test.go +++ b/internal/server/hmac_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "io" "net/http" "net/http/httptest" "strconv" @@ -86,6 +87,28 @@ func TestHMACMiddleware_ValidSignature(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) } +func TestHMACMiddleware_ValidSignature_GETWithBody(t *testing.T) { + called := false + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, `{"method":"test"}`, string(body)) + w.WriteHeader(http.StatusOK) + }) + wrapped := hmacMiddleware(testHMACSecret, handler) + + body := []byte(`{"method":"test"}`) + req := httptest.NewRequest("GET", "/mcp", bytes.NewReader(body)) + signRequest(t, req, testHMACSecret, body, time.Now(), "nonce-get-body-001") + + w := httptest.NewRecorder() + wrapped(w, req) + + assert.True(t, called, "next handler should be called on valid signature") + assert.Equal(t, http.StatusOK, w.Code) +} + func TestHMACMiddleware_MissingHeaders(t *testing.T) { tests := []struct { name string diff --git a/internal/server/http_helpers.go b/internal/server/http_helpers.go index 6d4419f22..5cc70a45d 100644 --- a/internal/server/http_helpers.go +++ b/internal/server/http_helpers.go @@ -54,11 +54,11 @@ func rejectRequest(w http.ResponseWriter, r *http.Request, status int, code, msg httputil.RejectRequest(w, status, code, msg) } -// peekRequestBody reads all bytes from a POST request body and restores it +// readAndRestoreRequestBody reads all bytes from request body and restores it // so downstream handlers can read it again. -// Returns nil, nil for non-POST requests or requests with no body. -func peekRequestBody(r *http.Request) ([]byte, error) { - if r.Method != http.MethodPost || r.Body == nil || r.Body == http.NoBody { +// Returns nil, nil for requests with no body. +func readAndRestoreRequestBody(r *http.Request) ([]byte, error) { + if r.Body == nil || r.Body == http.NoBody { return nil, nil } @@ -81,6 +81,17 @@ func peekRequestBody(r *http.Request) ([]byte, error) { return b, nil } +// peekRequestBody reads all bytes from a POST request body and restores it +// so downstream handlers can read it again. +// Returns nil, nil for non-POST requests or requests with no body. +func peekRequestBody(r *http.Request) ([]byte, error) { + if r.Method != http.MethodPost { + return nil, nil + } + + return readAndRestoreRequestBody(r) +} + // logHTTPRequestBody logs the request body for debugging purposes. // It reads the body, logs it, and restores it so it can be read again. // The backendID parameter is optional and can be empty for unified mode.