diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 7eada3a1a..ebb1349fe 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -546,3 +546,30 @@ func TestGoGetSupport(t *testing.T) { assert.Nil(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) } + +func TestIpnsBase58MultihashRedirect(t *testing.T) { + ts, _, _ := newTestServerAndNode(t, nil) + t.Logf("test server url: %s", ts.URL) + + t.Run("ED25519 Base58-encoded key", func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipns/12D3KooWRBy97UB99e3J6hiPesre1MZeuNQvfan4gBziswrRJsNK?keep=query", nil) + assert.Nil(t, err) + + res, err := doWithoutRedirect(req) + assert.Nil(t, err) + assert.Equal(t, "/ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8?keep=query", res.Header.Get("Location")) + }) + + t.Run("RSA Base58-encoded key", func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipns/QmcJM7PRfkSbcM5cf1QugM5R37TLRKyJGgBEhXjLTB8uA2?keep=query", nil) + assert.Nil(t, err) + + res, err := doWithoutRedirect(req) + assert.Nil(t, err) + assert.Equal(t, "/ipns/k2k4r8ol4m8kkcqz509c1rcjwunebj02gcnm5excpx842u736nja8ger?keep=query", res.Header.Get("Location")) + }) +} diff --git a/gateway/handler.go b/gateway/handler.go index 4da858c10..8ce4ff7eb 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -19,6 +19,8 @@ import ( ipath "github.com/ipfs/boxo/coreiface/path" cid "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multibase" prometheus "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -204,6 +206,10 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } + if handleIpnsB58mhToCidRedirection(w, r) { + return + } + contentPath := ipath.New(r.URL.Path) ctx := context.WithValue(r.Context(), ContentPathKey, contentPath) r = r.WithContext(ctx) @@ -728,6 +734,57 @@ func handleServiceWorkerRegistration(r *http.Request) (err *ErrorResponse) { return nil } +// handleIpnsB58mhToCidRedirection redirects from /ipns/b58mh to /ipns/cid in +// the most cost-effective way. +func handleIpnsB58mhToCidRedirection(w http.ResponseWriter, r *http.Request) bool { + if _, dnslink := r.Context().Value(DNSLinkHostnameKey).(string); dnslink { + // For DNSLink hostnames, do not perform redirection in order to not break + // website. For example, if `example.net` is backed by `/ipns/base58`, we + // must NOT redirect to `example.net/ipns/base36-id`. + return false + } + + if w.Header().Get("Location") != "" { + // Ignore this if there is already a redirection in place. This happens + // if there is a subdomain redirection. In that case, the path is already + // converted to CIDv1. + return false + } + + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) < 3 { + return false + } + + if pathParts[1] != "ipns" { + return false + } + + id, err := peer.Decode(pathParts[2]) + if err != nil { + return false + } + + // Convert the peer ID to a CIDv1. + cid := peer.ToCid(id) + + // Encode CID in base36 to match the subdomain URLs. + encodedCID, err := cid.StringOfBase(multibase.Base36) + if err != nil { + return false + } + + // If the CID was already encoded, do not redirect. + if encodedCID == pathParts[2] { + return false + } + + pathParts[2] = encodedCID + r.URL.Path = strings.Join(pathParts, "/") + http.Redirect(w, r, r.URL.String(), http.StatusFound) + return true +} + // Attempt to fix redundant /ipfs/ namespace as long as resulting // 'intended' path is valid. This is in case gremlins were tickled // wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id}