package mcpsrv_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"sourcecraft.dev/bigbes/sr-ht-spec/mcpsrv"
)
// The SDK's own DNS-rebinding guard is disabled (it cannot tell nginx from an
// attacker), so this replacement is the only thing protecting /mcp. These cases
// are the reason the bypass is defensible: a hostile Host is still refused, and
// the legitimate proxied Host that the SDK guard would have rejected is let
// through.
func TestHostGuard(t *testing.T) {
const origin = "https://spec.srht.bigb.es"
for _, tc := range []struct {
name string
host string
want int
}{
{"proxied real hostname", "spec.srht.bigb.es", http.StatusOK},
{"proxied with port", "spec.srht.bigb.es:443", http.StatusOK},
{"case-insensitive", "SPEC.SRHT.BIGB.ES", http.StatusOK},
{"loopback for dev", "127.0.0.1:5091", http.StatusOK},
{"localhost for dev", "localhost:5091", http.StatusOK},
{"rebinding attacker domain", "evil.example.com", http.StatusForbidden},
{"attacker subdomain of us", "spec.srht.bigb.es.evil.com", http.StatusForbidden},
{"another srht service", "git.srht.bigb.es", http.StatusForbidden},
} {
t.Run(tc.name, func(t *testing.T) {
r, s := newFixture()
h, err := mcpsrv.Handler(mcpsrv.Backend{Docs: r, Index: s}, "test", origin)
if err != nil {
t.Fatalf("Handler: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/mcp",
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize"}`))
req.Host = tc.host
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if tc.want == http.StatusForbidden && w.Code != http.StatusForbidden {
t.Fatalf("Host %q got %d, want 403 — the guard did not block it", tc.host, w.Code)
}
if tc.want == http.StatusOK && w.Code == http.StatusForbidden {
t.Fatalf("Host %q got 403 — the guard blocked a legitimate request", tc.host)
}
})
}
}
// An origin with no host in it is a construction error, not a warning.
//
// This used to be the opposite: Handler logged "Host validation on /mcp is
// DISABLED" and returned the bare handler, on the reasoning that refusing to
// start over a config typo is worse than running unguarded. It is not. The
// guard is the only thing protecting /mcp once the SDK's own is disabled, so
// that path turned one unparseable value into a silently open endpoint that
// passes every functional test — the class of failure nobody discovers. The
// daemon cannot reach it in any case: service.Config.Validate already refuses
// to start unless the origin parses and carries a host.
func TestHandlerRefusesAnOriginItCannotGuardWith(t *testing.T) {
for _, origin := range []string{
"",
" ",
"not a url at all",
"https://", // parses, but carries no host
"/just/path", // relative, no host
} {
t.Run(origin, func(t *testing.T) {
r, s := newFixture()
h, err := mcpsrv.Handler(mcpsrv.Backend{Docs: r, Index: s}, "test", origin)
require.Error(t, err, "an origin with no host must not yield a handler")
require.Nil(t, h, "an unguarded handler must never escape the constructor")
require.Contains(t, err.Error(), "no host to guard /mcp with")
})
}
}
// A usable origin still builds, which is what keeps the test above from passing
// for the wrong reason.
func TestHandlerAcceptsAUsableOrigin(t *testing.T) {
r, s := newFixture()
h, err := mcpsrv.Handler(mcpsrv.Backend{Docs: r, Index: s}, "test", "https://spec.srht.bigb.es")
require.NoError(t, err)
require.NotNil(t, h)
}