From d56bdba3ff35e8203131cfd2a43c0499e5d3e2ce Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 15 Apr 2026 21:19:03 -0300 Subject: [PATCH] khatru: WithServiceURL() subhandlers. --- khatru/handlers.go | 4 ++-- khatru/relay.go | 19 ++++++++++++++-- khatru/relay_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ khatru/utils.go | 1 + 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/khatru/handlers.go b/khatru/handlers.go index df093eb..e954413 100644 --- a/khatru/handlers.go +++ b/khatru/handlers.go @@ -43,8 +43,8 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) relayPathMatches := true - if rl.ServiceURL != "" { - p, err := url.Parse(rl.ServiceURL) + if serviceURL := rl.getServiceURL(r); serviceURL != "" { + p, err := url.Parse(serviceURL) if err == nil { relayPathMatches = strings.TrimSuffix(r.URL.Path, "/") == strings.TrimSuffix(p.Path, "/") } diff --git a/khatru/relay.go b/khatru/relay.go index 23a337c..9e22b0b 100644 --- a/khatru/relay.go +++ b/khatru/relay.go @@ -166,8 +166,8 @@ func (rl *Relay) UseEventstore(store eventstore.Store, maxQueryLimit int) { } func (rl *Relay) getBaseURL(r *http.Request) string { - if rl.ServiceURL != "" { - return rl.ServiceURL + if serviceURL := rl.getServiceURL(r); serviceURL != "" { + return serviceURL } host := r.Header.Get("X-Forwarded-Host") @@ -192,6 +192,14 @@ func (rl *Relay) getBaseURL(r *http.Request) string { return proto + "://" + host + r.URL.Path } +func (rl *Relay) getServiceURL(r *http.Request) string { + if serviceURL, ok := r.Context().Value(serviceURLOverrideKey).(string); ok { + return serviceURL + } + + return rl.ServiceURL +} + // Stats returns the current number of connected clients and open listeners. func (rl *Relay) Stats() (clients, listeners int) { rl.clientsMutex.Lock() @@ -211,3 +219,10 @@ func (rl *Relay) Router() *http.ServeMux { func (rl *Relay) SetRouter(mux *http.ServeMux) { rl.serveMux = mux } + +func (rl *Relay) WithServiceURL(serviceURL string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), serviceURLOverrideKey, serviceURL) + rl.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/khatru/relay_test.go b/khatru/relay_test.go index baceb4b..bcaed80 100644 --- a/khatru/relay_test.go +++ b/khatru/relay_test.go @@ -2,6 +2,9 @@ package khatru import ( "context" + "encoding/json" + "io" + "net/http" "net/http/httptest" "strconv" "testing" @@ -9,9 +12,60 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore/slicestore" + "fiatjaf.com/nostr/nip11" "github.com/stretchr/testify/require" ) +func TestWithServiceURL(t *testing.T) { + relay := NewRelay() + relay.Info.Icon = "icon.png" + relay.Info.Banner = "banner.png" + relay.Router().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + io.WriteString(w, "fallback") + }) + + handlerA := relay.WithServiceURL("https://a.example/relay") + handlerB := relay.WithServiceURL("https://b.example/relay") + + t.Run("uses override for nip11 base url", func(t *testing.T) { + for _, tc := range []struct { + name string + handler http.Handler + expectedBase string + }{ + {name: "first interface", handler: handlerA, expectedBase: "https://a.example/relay"}, + {name: "second interface", handler: handlerB, expectedBase: "https://b.example/relay"}, + } { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://internal/relay", nil) + req.Header.Set("Accept", "application/nostr+json") + rr := httptest.NewRecorder() + + tc.handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var info nip11.RelayInformationDocument + require.NoError(t, json.NewDecoder(rr.Body).Decode(&info)) + require.Equal(t, tc.expectedBase+"/icon.png", info.Icon) + require.Equal(t, tc.expectedBase+"/banner.png", info.Banner) + }) + } + }) + + t.Run("uses override for relay path matching", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://internal/not-relay", nil) + req.Header.Set("Accept", "application/nostr+json") + rr := httptest.NewRecorder() + + handlerA.ServeHTTP(rr, req) + + require.Equal(t, http.StatusAccepted, rr.Code) + require.Equal(t, "fallback", rr.Body.String()) + }) +} + func TestBasicRelayFunctionality(t *testing.T) { // setup relay with in-memory store relay := NewRelay() diff --git a/khatru/utils.go b/khatru/utils.go index 800d0c0..b11f138 100644 --- a/khatru/utils.go +++ b/khatru/utils.go @@ -11,6 +11,7 @@ const ( subscriptionIdKey nip86HeaderAuthKey internalCallKey + serviceURLOverrideKey ) func RequestAuth(ctx context.Context) {