mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
113 lines
4.1 KiB
Go
113 lines
4.1 KiB
Go
package zip
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// dialRecorder is a Client that records what it was handed and answers 200. It
|
|
// stands in for a plugin child, which is what makes the assertion meaningful:
|
|
// the child's view of the request is the whole question here.
|
|
// It records the host the child would actually READ, which is the host on the
|
|
// SERIALIZED request — not a field of the in-memory object. fasthttp writes the
|
|
// Host header from the URI whenever the URI has been parsed, so reading either
|
|
// field directly can disagree with what goes over the wire. Serializing is the
|
|
// only reading that cannot.
|
|
type dialRecorder struct{ sawHost string }
|
|
|
|
func (d *dialRecorder) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
|
|
var buf bytes.Buffer
|
|
w := bufio.NewWriter(&buf)
|
|
_ = req.Write(w)
|
|
_ = w.Flush()
|
|
for _, line := range strings.Split(buf.String(), "\r\n") {
|
|
if v, ok := strings.CutPrefix(line, "Host: "); ok {
|
|
d.sawHost = v
|
|
break
|
|
}
|
|
}
|
|
resp.SetStatusCode(200)
|
|
return nil
|
|
}
|
|
|
|
// A PROXIED REQUEST REACHES THE CHILD AS THE HOST IT WAS ASKED AS.
|
|
//
|
|
// The child dials its own address, so this header is not routing — it is the
|
|
// only way a child can know which brand was asked, and a multi-brand subsystem
|
|
// cannot answer without it. hanzoai/iam resolves each brand's OIDC issuer from
|
|
// exactly this value: overwritten with the socket path, every brand collapsed
|
|
// onto one issuer and every non-default brand's tokens were rejected by its own
|
|
// relying parties.
|
|
func TestForwardKeepsTheCallersHost(t *testing.T) {
|
|
rec := &dialRecorder{}
|
|
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
|
|
req.Header.SetHost("lux.id")
|
|
req.Header.SetRequestURI("/.well-known/openid-configuration")
|
|
|
|
if err := forward(context.Background(), req, resp, rec, "/var/run/iam.sock", "", "mount /v1/iam", hold{}); err != nil {
|
|
t.Fatalf("forward: %v", err)
|
|
}
|
|
if rec.sawHost != "lux.id" {
|
|
t.Fatalf("child saw Host %q, want %q — a brand-resolving child cannot answer as the brand it was asked as", rec.sawHost, "lux.id")
|
|
}
|
|
}
|
|
|
|
// A request with no Host of its own still gets one, because HTTP/1.1 requires
|
|
// the header. That is the MCP hop: a freshly built request rather than a
|
|
// proxied inbound, and the dial address is the honest answer there.
|
|
func TestForwardFallsBackToTheDialAddress(t *testing.T) {
|
|
rec := &dialRecorder{}
|
|
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
|
|
req.Header.SetRequestURI("/mcp")
|
|
|
|
if err := forward(context.Background(), req, resp, rec, "plugin.sock", "", "mcp iam", hold{}); err != nil {
|
|
t.Fatalf("forward: %v", err)
|
|
}
|
|
if rec.sawHost != "plugin.sock" {
|
|
t.Fatalf("a hostless request reached the child as %q, want the dial address", rec.sawHost)
|
|
}
|
|
}
|
|
|
|
// The path rewrite is independent of the host, and both still apply together.
|
|
func TestForwardStillRewritesThePath(t *testing.T) {
|
|
rec := &dialRecorder{}
|
|
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
|
|
req.Header.SetHost("hanzo.ai")
|
|
req.Header.SetRequestURI("/v1/iam/mcp")
|
|
|
|
if err := forward(context.Background(), req, resp, rec, "iam.sock", "/mcp", "mcp iam", hold{}); err != nil {
|
|
t.Fatalf("forward: %v", err)
|
|
}
|
|
if got := string(req.URI().Path()); got != "/mcp" {
|
|
t.Fatalf("path = %q, want /mcp", got)
|
|
}
|
|
if rec.sawHost != "hanzo.ai" {
|
|
t.Fatalf("host = %q, want hanzo.ai", rec.sawHost)
|
|
}
|
|
}
|
|
|
|
// A dead target is 503 and names itself, unchanged by any of the above.
|
|
func TestForwardWithNoClientIs503(t *testing.T) {
|
|
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
err := forward(context.Background(), req, resp, nil, "iam.sock", "", "mount /v1/iam", hold{})
|
|
if err == nil || !strings.Contains(err.Error(), "no instance running") {
|
|
t.Fatalf("err = %v, want a 503 naming the mount", err)
|
|
}
|
|
}
|