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
14 changes: 4 additions & 10 deletions internal/server/hmac.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package server

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"sync"
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions internal/server/hmac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions internal/server/http_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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.
Expand Down
Loading