package mcpsrv_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"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 unusable origin must leave a loud trail rather than silently unguarding the
// endpoint. It still serves (refusing to start over a config typo is worse), but
// Handler logs a warning; this pins that it does not instead start refusing
// every request, which would look like the guard "working".
func TestHostGuardWithoutOriginStillServes(t *testing.T) {
r, s := newFixture()
h, err := mcpsrv.Handler(mcpsrv.Backend{Docs: r, Index: s}, "test", "")
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 = "anything.example.com"
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code == http.StatusForbidden {
t.Fatalf("unconfigured origin should not 403 every request; got %d", w.Code)
}
}