diff --git a/daemon.go b/daemon.go index ea54b73..668c0f7 100644 --- a/daemon.go +++ b/daemon.go @@ -12,10 +12,13 @@ import ( "github.com/ipfs/boxo/bitswap/message/pb" "github.com/ipfs/boxo/bitswap/network" "github.com/ipfs/boxo/bitswap/network/httpnet" + "github.com/ipfs/boxo/gateway" + "github.com/ipfs/boxo/namesys" "github.com/ipfs/boxo/routing/http/client" "github.com/ipfs/boxo/routing/http/contentrouter" "github.com/ipfs/go-cid" vole "github.com/ipshipyard/vole/lib" + doh "github.com/libp2p/go-doh-resolver" "github.com/libp2p/go-libp2p" dhtpb "github.com/libp2p/go-libp2p-kad-dht/pb" "github.com/libp2p/go-libp2p/core/host" @@ -37,6 +40,7 @@ type daemon struct { h host.Host dht kademlia dhtMessenger *dhtpb.ProtocolMessenger + ns namesys.NameSystem createTestHost func() (host.Host, error) promRegistry *prometheus.Registry httpSkipVerify bool @@ -90,10 +94,34 @@ func newDaemon(ctx context.Context, acceleratedDHT bool) (*daemon, error) { return nil, err } + // Create DNS resolver with delegated-ipfs.dev DoH endpoint to match IPFS Mainnet behavior. + // This endpoint is used by the Helia ecosystem in browsers and ensures consistent + // DNSLink resolution without requiring local DNS resolver configuration. + // DNS caching is disabled (doh.WithCacheDisabled) because this is a diagnostic tool + // that should always query current DNS state rather than serve potentially stale cached results. + dnsResolver, err := gateway.NewDNSResolver( + map[string]string{ + ".": "https://delegated-ipfs.dev/dns-query", + }, + doh.WithCacheDisabled(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create DNS resolver: %w", err) + } + + // Create namesys without caching (no WithCache option) to ensure fresh IPNS resolution. + // Diagnostic tools should not cache IPNS records as users expect to see current network state + // when running checks, not cached data from previous resolutions. + ns, err := namesys.NewNameSystem(d, namesys.WithDNSResolver(dnsResolver)) + if err != nil { + return nil, err + } + return &daemon{ h: h, dht: d, dhtMessenger: pm, + ns: ns, promRegistry: promRegistry, createTestHost: func() (host.Host, error) { // TODO: when behind NAT, this will fail to determine its own public addresses which will block it from running dctur and hole punching @@ -106,6 +134,14 @@ func newDaemon(ctx context.Context, acceleratedDHT bool) (*daemon, error) { }}, nil } +type MutableResolution struct { + InputPath string `json:"InputPath,omitempty"` + ResolvedPath string `json:"ResolvedPath,omitempty"` + DiagnosticURL string `json:"DiagnosticURL,omitempty"` + Error string `json:"Error,omitempty"` + IsMutableInput bool `json:"IsMutableInput,omitempty"` +} + type cidCheckOutput *[]providerOutput type providerOutput struct { diff --git a/go.mod b/go.mod index 86650e2..51920a9 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/ipfs/go-datastore v0.9.0 github.com/ipfs/go-ipld-format v0.6.3 github.com/ipshipyard/vole v0.0.0-20251030190237-1448fb4935ac + github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.43.0 github.com/libp2p/go-libp2p-kad-dht v0.35.1 github.com/libp2p/go-libp2p-record v0.3.1 @@ -26,21 +27,26 @@ require ( github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/ajg/form v1.5.1 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheggaaa/pb/v3 v3.1.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect github.com/cskr/pubsub v1.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gammazero/chanqueue v1.1.1 // indirect github.com/gammazero/deque v1.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -56,13 +62,18 @@ require ( github.com/huin/goupnp v1.3.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect + github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-cidutil v0.1.0 // indirect github.com/ipfs/go-dsqueue v0.1.0 // indirect github.com/ipfs/go-ipfs-pq v0.0.3 // indirect + github.com/ipfs/go-ipfs-redirects-file v0.1.2 // indirect + github.com/ipfs/go-ipld-cbor v0.2.1 // indirect github.com/ipfs/go-ipld-legacy v0.2.2 // indirect github.com/ipfs/go-log/v2 v2.8.2 // indirect github.com/ipfs/go-metrics-interface v0.3.0 // indirect github.com/ipfs/go-peertaskqueue v0.8.2 // indirect + github.com/ipfs/go-unixfsnode v1.10.2 // indirect + github.com/ipld/go-car/v2 v2.16.0 // indirect github.com/ipld/go-codec-dagpb v1.7.0 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect @@ -102,6 +113,7 @@ require ( github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/gomega v1.36.3 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect @@ -135,8 +147,12 @@ require ( github.com/sergi/go-diff v1.3.1 // indirect github.com/smartystreets/assertions v1.13.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect + github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -148,6 +164,7 @@ require ( github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect @@ -167,6 +184,7 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d6c4a4f..364c008 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAU github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -54,6 +56,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf h1:dwGgBWn84wUS1pVikGiruW+x5XM4amhjaZO20vCjay4= +github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -70,6 +74,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjY github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -78,6 +84,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -90,6 +98,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/chanqueue v1.1.1 h1:n9Y+zbBxw2f7uUE9wpgs0rOSkP/I/yhDLiNuhyVjojQ= github.com/gammazero/chanqueue v1.1.1/go.mod h1:fMwpwEiuUgpab0sH4VHiVcEoji1pSi+EIzeG4TPeKPc= github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo= @@ -207,7 +217,11 @@ github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1I github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4= +github.com/ipfs/go-ipfs-redirects-file v0.1.2 h1:QCK7VtL91FH17KROVVy5KrzDx2hu68QvB2FTWk08ZQk= +github.com/ipfs/go-ipfs-redirects-file v0.1.2/go.mod h1:yIiTlLcDEM/8lS6T3FlCEXZktPPqSOyuY6dEzVqw7Fw= github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= +github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= +github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8= github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk= github.com/ipfs/go-ipld-legacy v0.2.2 h1:DThbqCPVLpWBcGtU23KDLiY2YRZZnTkXQyfz8aOfBkQ= @@ -223,10 +237,14 @@ github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= github.com/ipfs/go-unixfsnode v1.10.2 h1:TREegX1J4X+k1w4AhoDuxxFvVcS9SegMRvrmxF6Tca8= github.com/ipfs/go-unixfsnode v1.10.2/go.mod h1:ImDPTSiKZ+2h4UVdkSDITJHk87bUAp7kX/lgifjRicg= +github.com/ipld/go-car/v2 v2.16.0 h1:LWe0vmN/QcQmUU4tr34W5Nv5mNraW+G6jfN2s+ndBco= +github.com/ipld/go-car/v2 v2.16.0/go.mod h1:RqFGWN9ifcXVmCrTAVnfnxiWZk1+jIx67SYhenlmL34= github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0= github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM= github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20250821084354-a425e60cd714 h1:cqNk8PEwHnK0vqWln+U/YZhQc9h2NB3KjUjDPZo5Q2s= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20250821084354-a425e60cd714/go.mod h1:ZEUdra3CoqRVRYgAX/jAJO9aZGz6SKtKEG628fHHktY= github.com/ipshipyard/vole v0.0.0-20251030190237-1448fb4935ac h1:2+Pn7dUwzYM/BA+BFjxC5u6JRiF+9bha89DlimGzbIQ= github.com/ipshipyard/vole v0.0.0-20251030190237-1448fb4935ac/go.mod h1:SR1N3uIxIHyGl2DglxA/t3VGmqJgIG/wG4oWpg1AUeY= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= @@ -273,6 +291,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE= +github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8= github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= @@ -402,6 +422,8 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= +github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= @@ -543,11 +565,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ= +github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= @@ -563,6 +588,14 @@ github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsX github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc h1:BCPnHtcboadS0DvysUuJXZ4lWVv5Bh5i7+tbIyi+ck4= +github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc/go.mod h1:r45hJU7yEoA81k6MWNhpMj/kms0n14dkzkxYHoB96UM= +github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= +github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= @@ -597,10 +630,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= @@ -765,6 +804,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= diff --git a/main.go b/main.go index 4eaa3aa..537d6b1 100644 --- a/main.go +++ b/main.go @@ -5,16 +5,19 @@ import ( "crypto/subtle" "embed" "encoding/json" + "fmt" "log" "net" "net/http" "os" + "strings" "time" + "github.com/ipfs/boxo/namesys" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/peer" "github.com/multiformats/go-multiaddr" - "github.com/multiformats/go-multihash" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -74,6 +77,7 @@ func main() { const ( defaultCheckTimeout = 60 * time.Second defaultIndexerURL = "https://cid.contact" + libp2pKeyCodec = 0x72 // multicodec for libp2p-key (PeerID in CIDv1 format) ) func startServer(ctx context.Context, d *daemon, tcpListener, metricsUsername, metricPassword string) error { @@ -107,21 +111,10 @@ func startServer(ctx context.Context, d *daemon, tcpListener, metricsUsername, m http.Error(w, "missing 'cid' query parameter", http.StatusBadRequest) return } - cidKey, err := cid.Decode(cidStr) - if err != nil { - mh, mhErr := multihash.FromB58String(cidStr) - if mhErr != nil { - mh, mhErr = multihash.FromHexString(cidStr) - if mhErr != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } - cidKey = cid.NewCidV1(cid.Raw, mh) - } checkTimeout := defaultCheckTimeout if timeoutStr != "" { + var err error checkTimeout, err = time.ParseDuration(timeoutStr + "s") if err != nil { http.Error(w, "Invalid timeout value (in seconds)", http.StatusBadRequest) @@ -137,20 +130,57 @@ func startServer(ctx context.Context, d *daemon, tcpListener, metricsUsername, m withTimeout, cancel := context.WithTimeout(r.Context(), checkTimeout) defer cancel() + // Resolve input (CID, IPNS name, or DNSLink) + cidKey, mutableRes, err := resolveInput(withTimeout, d.ns, cidStr) + if err != nil { + if mutableRes != nil && mutableRes.Error != "" { + // Resolution attempted but failed, return resolution info with error + w.Header().Add("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "MutableResolution": mutableRes, + }) + return + } + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var data interface{} if maStr == "" { - data, err = d.runCidCheck(withTimeout, cidKey, ipniURL, httpRetrieval) + cidOutput, err := d.runCidCheck(withTimeout, cidKey, ipniURL, httpRetrieval) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // Wrap response with resolution info at top level + if mutableRes != nil { + data = map[string]interface{}{ + "MutableResolution": mutableRes, + "Providers": cidOutput, + } + } else { + data = cidOutput + } } else { ma, ai, err400 := parseMultiaddr(maStr) if err400 != nil { http.Error(w, err400.Error(), http.StatusBadRequest) return } - data, err = d.runPeerCheck(withTimeout, ma, ai, cidKey, ipniURL, httpRetrieval) - } - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + peerOutput, err := d.runPeerCheck(withTimeout, ma, ai, cidKey, ipniURL, httpRetrieval) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + // Wrap response with resolution info at top level + if mutableRes != nil { + data = map[string]interface{}{ + "MutableResolution": mutableRes, + "Result": peerOutput, + } + } else { + data = peerOutput + } } w.Header().Add("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(data) @@ -267,6 +297,101 @@ func getWebAddress(l net.Listener) string { } } +// buildDiagnosticURL returns the appropriate diagnostic URL for a given IPNS name or DNSLink. +func buildDiagnosticURL(name string) string { + if strings.Contains(name, ".") { + return "https://dnslink.dev/#" + name + } + return "https://ipns.ipfs.network/#" + name +} + +// resolveInput attempts to parse input as a CID, +// and if that fails, tries to resolve it as an IPNS name or DNSLink. +// Returns the resolved CID and optional resolution info. +func resolveInput(ctx context.Context, ns namesys.NameSystem, input string) (cid.Cid, *MutableResolution, error) { + // Strip ipfs:// prefix if present + input = strings.TrimPrefix(input, "ipfs://") + + // Try to decode as CID + if c, err := cid.Decode(input); err == nil { + if c.Type() == libp2pKeyCodec { + // PeerID in CIDv1 format - resolve as IPNS + return resolveMutablePath(ctx, ns, "/ipns/"+input, true) + } + // Regular content CID - return immediately + return c, nil, nil + } + + // Try to decode as legacy PeerID (base58btc multihash) + if _, err := peer.Decode(input); err == nil { + // Legacy PeerID - resolve as IPNS + return resolveMutablePath(ctx, ns, "/ipns/"+input, true) + } + + // Must be DNSLink or IPNS path - resolve with warning + return resolveMutablePath(ctx, ns, input, true) +} + +// resolveMutablePath resolves an IPNS or DNSLink path to a CID and returns resolution metadata. +func resolveMutablePath(ctx context.Context, ns namesys.NameSystem, input string, isMutableInput bool) (cid.Cid, *MutableResolution, error) { + mutableRes := &MutableResolution{ + IsMutableInput: isMutableInput, + } + + // Normalize input to an IPNS path + var p path.Path + var err error + + if strings.HasPrefix(input, "/ipns/") || strings.HasPrefix(input, "/ipfs/") { + mutableRes.InputPath = input + p, err = path.NewPath(input) + } else { + // Bare domain or IPNS name - prefix with /ipns/ + mutableRes.InputPath = "/ipns/" + input + p, err = path.NewPath(mutableRes.InputPath) + } + + if err != nil { + return cid.Cid{}, nil, fmt.Errorf("not a valid CID, IPNS name, or DNSLink: %w", err) + } + + // If it's an /ipfs/ path, extract the CID directly + if !p.Mutable() { + segments := p.Segments() + c, err := cid.Decode(segments[1]) + if err != nil { + return cid.Cid{}, nil, fmt.Errorf("invalid CID in path: %w", err) + } + return c, nil, nil + } + + // Attempt IPNS/DNSLink resolution + result, err := ns.Resolve(ctx, p) + name := p.Segments()[1] + + if err != nil { + mutableRes.Error = err.Error() + mutableRes.DiagnosticURL = buildDiagnosticURL(name) + return cid.Cid{}, mutableRes, fmt.Errorf("resolution failed: %w", err) + } + + mutableRes.ResolvedPath = result.Path.String() + mutableRes.DiagnosticURL = buildDiagnosticURL(name) + + // Extract CID from resolved path + segments := result.Path.Segments() + if len(segments) < 2 { + return cid.Cid{}, mutableRes, fmt.Errorf("resolved path has insufficient components: %s", result.Path.String()) + } + + c, err := cid.Decode(segments[1]) + if err != nil { + return cid.Cid{}, mutableRes, fmt.Errorf("invalid CID in resolved path: %w", err) + } + + return c, mutableRes, nil +} + func parseMultiaddr(maStr string) (multiaddr.Multiaddr, peer.AddrInfo, error) { ma, err := multiaddr.NewMultiaddr(maStr) if err != nil { diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..b8db846 --- /dev/null +++ b/main_test.go @@ -0,0 +1,320 @@ +package main + +import ( + "context" + "errors" + "testing" + + "github.com/ipfs/boxo/namesys" + "github.com/ipfs/boxo/path" + "github.com/ipfs/go-cid" + ci "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" +) + +// mockNameSystem implements namesys.NameSystem for testing +type mockNameSystem struct { + resolveFunc func(context.Context, path.Path) (namesys.Result, error) +} + +func (m *mockNameSystem) Resolve(ctx context.Context, p path.Path, opts ...namesys.ResolveOption) (namesys.Result, error) { + if m.resolveFunc != nil { + return m.resolveFunc(ctx, p) + } + return namesys.Result{}, errors.New("not implemented") +} + +func (m *mockNameSystem) ResolveAsync(ctx context.Context, p path.Path, opts ...namesys.ResolveOption) <-chan namesys.AsyncResult { + ch := make(chan namesys.AsyncResult, 1) + close(ch) + return ch +} + +func (m *mockNameSystem) Publish(ctx context.Context, sk ci.PrivKey, value path.Path, opts ...namesys.PublishOption) error { + return errors.New("not implemented") +} + +func TestBuildDiagnosticURL(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "domain with dot", + input: "example.com", + expected: "https://dnslink.dev/#example.com", + }, + { + name: "subdomain", + input: "subdomain.example.com", + expected: "https://dnslink.dev/#subdomain.example.com", + }, + { + name: "IPNS key without dot", + input: "k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", + expected: "https://ipns.ipfs.network/#k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", + }, + { + name: "legacy PeerID", + input: "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + expected: "https://ipns.ipfs.network/#QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildDiagnosticURL(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestResolveInput_RegularCID(t *testing.T) { + ctx := context.Background() + mockNS := &mockNameSystem{} + + // Regular content CID should be returned immediately without resolution + input := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + expectedCID, err := cid.Decode(input) + require.NoError(t, err) + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.Nil(t, mutableRes) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_PeerIDv1(t *testing.T) { + ctx := context.Background() + + // PeerID in CIDv1 format (libp2p-key codec 0x72) + // Reference: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation + // Example from spec: bafzbeie5745rpv2m6tjyuugywy4d5ewrqgqqhfnf445he3omzpjbx5xqxe + input := "bafzaajaiaejcbzdibmxyzdjbbehgvizh6g5cjyzhqlidscv64ubjeeeqk4w2nlj2" + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + require.True(t, p.Mutable()) + require.Equal(t, "/ipns/"+input, p.String()) + + resolvedPath, err := path.NewPath("/ipfs/" + resolvedCID) + require.NoError(t, err) + return namesys.Result{Path: resolvedPath}, nil + }, + } + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.NotNil(t, mutableRes) + require.True(t, mutableRes.IsMutableInput, "PeerID v1 should be marked as mutable input") + require.Equal(t, "/ipns/"+input, mutableRes.InputPath) + require.Equal(t, "/ipfs/"+resolvedCID, mutableRes.ResolvedPath) + require.Contains(t, mutableRes.DiagnosticURL, "ipns.ipfs.network") + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_LegacyPeerIDAsContentCID(t *testing.T) { + ctx := context.Background() + mockNS := &mockNameSystem{} + + // Bare legacy PeerID (base58 multihash starting with Qm) is ambiguous + // Reference: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation + // Example from spec: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N + // UX decision: treat as content CID (immutable) by default, not IPNS name + input := "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" + expectedCID, err := cid.Decode(input) + require.NoError(t, err) + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.Nil(t, mutableRes, "Bare Qm... should be treated as content CID, not IPNS") + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_ExplicitIPNSPath(t *testing.T) { + ctx := context.Background() + + // Explicit /ipns/ prefix makes intent clear - resolve as IPNS + legacyPeerID := "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" + input := "/ipns/" + legacyPeerID + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + require.True(t, p.Mutable()) + require.Equal(t, input, p.String()) + + resolvedPath, err := path.NewPath("/ipfs/" + resolvedCID) + require.NoError(t, err) + return namesys.Result{Path: resolvedPath}, nil + }, + } + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.NotNil(t, mutableRes) + require.True(t, mutableRes.IsMutableInput, "Explicit /ipns/ path should be marked as mutable") + require.Equal(t, input, mutableRes.InputPath) + require.Equal(t, "/ipfs/"+resolvedCID, mutableRes.ResolvedPath) + require.Contains(t, mutableRes.DiagnosticURL, "ipns.ipfs.network") + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_Ed25519PeerID(t *testing.T) { + ctx := context.Background() + + // Ed25519 identity multihash format from libp2p spec + // Reference: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation + // Example from spec: 12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA + // This format does NOT decode as CID, only as PeerID via peer.Decode() + input := "12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA" + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + require.True(t, p.Mutable()) + require.Equal(t, "/ipns/"+input, p.String()) + + resolvedPath, err := path.NewPath("/ipfs/" + resolvedCID) + require.NoError(t, err) + return namesys.Result{Path: resolvedPath}, nil + }, + } + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.NotNil(t, mutableRes) + require.True(t, mutableRes.IsMutableInput, "Ed25519 PeerID should be marked as mutable input") + require.Equal(t, "/ipns/"+input, mutableRes.InputPath) + require.Equal(t, "/ipfs/"+resolvedCID, mutableRes.ResolvedPath) + require.Contains(t, mutableRes.DiagnosticURL, "ipns.ipfs.network") + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_DNSLink(t *testing.T) { + ctx := context.Background() + + input := "example.com" + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + require.True(t, p.Mutable()) + require.Equal(t, "/ipns/"+input, p.String()) + + resolvedPath, err := path.NewPath("/ipfs/" + resolvedCID) + require.NoError(t, err) + return namesys.Result{Path: resolvedPath}, nil + }, + } + + resultCID, mutableRes, err := resolveInput(ctx, mockNS, input) + require.NoError(t, err) + require.NotNil(t, mutableRes) + require.True(t, mutableRes.IsMutableInput, "DNSLink should be marked as mutable input") + require.Equal(t, "/ipns/"+input, mutableRes.InputPath) + require.Equal(t, "/ipfs/"+resolvedCID, mutableRes.ResolvedPath) + require.Contains(t, mutableRes.DiagnosticURL, "dnslink.dev") + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveInput_InvalidInput(t *testing.T) { + ctx := context.Background() + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + return namesys.Result{}, errors.New("invalid path") + }, + } + + input := "not-a-valid-cid-or-name" + _, mutableRes, err := resolveInput(ctx, mockNS, input) + require.Error(t, err) + + // Should attempt resolution and fail + if mutableRes != nil { + require.NotEmpty(t, mutableRes.Error) + } +} + +func TestResolveMutablePath_Success(t *testing.T) { + ctx := context.Background() + + input := "/ipns/example.com" + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + resolvedPath, err := path.NewPath("/ipfs/" + resolvedCID) + require.NoError(t, err) + return namesys.Result{Path: resolvedPath}, nil + }, + } + + resultCID, mutableRes, err := resolveMutablePath(ctx, mockNS, input, true) + require.NoError(t, err) + require.NotNil(t, mutableRes) + require.True(t, mutableRes.IsMutableInput) + require.Equal(t, input, mutableRes.InputPath) + require.Equal(t, "/ipfs/"+resolvedCID, mutableRes.ResolvedPath) + require.NotEmpty(t, mutableRes.DiagnosticURL) + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} + +func TestResolveMutablePath_Failed(t *testing.T) { + ctx := context.Background() + + input := "/ipns/nonexistent.example.com" + expectedError := errors.New("no DNSLink record found") + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + return namesys.Result{}, expectedError + }, + } + + _, mutableRes, err := resolveMutablePath(ctx, mockNS, input, true) + require.Error(t, err) + require.NotNil(t, mutableRes) + require.Contains(t, mutableRes.Error, expectedError.Error()) + require.NotEmpty(t, mutableRes.DiagnosticURL) +} + +func TestResolveMutablePath_DirectIPFSPath(t *testing.T) { + ctx := context.Background() + + // Direct /ipfs/ path should extract CID without calling namesys + resolvedCID := "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + input := "/ipfs/" + resolvedCID + + mockNS := &mockNameSystem{ + resolveFunc: func(ctx context.Context, p path.Path) (namesys.Result, error) { + t.Fatal("Should not call Resolve for /ipfs/ path") + return namesys.Result{}, nil + }, + } + + resultCID, mutableRes, err := resolveMutablePath(ctx, mockNS, input, false) + require.NoError(t, err) + require.Nil(t, mutableRes) + + expectedCID, err := cid.Decode(resolvedCID) + require.NoError(t, err) + require.Equal(t, expectedCID, resultCID) +} diff --git a/web/index.html b/web/index.html index 361459b..7cedc2a 100644 --- a/web/index.html +++ b/web/index.html @@ -48,8 +48,8 @@

- - + +
diff --git a/web/script.js b/web/script.js index a669520..57e8160 100644 --- a/web/script.js +++ b/web/script.js @@ -202,42 +202,96 @@ function toggleSubmitButton() { } } +function formatMutableResolution(mutableRes) { + if (!mutableRes) return '' + + // Extract domain from diagnostic URL for display (without hash) + let diagnosticSite = '' + if (mutableRes.DiagnosticURL) { + try { + const url = new URL(mutableRes.DiagnosticURL) + diagnosticSite = url.hostname + } catch (e) { + diagnosticSite = 'diagnostic tool' + } + } + + let html = '
' + + // Show warning if input is mutable or legacy format + if (mutableRes.IsMutableInput) { + html += `
⚠️ Input is not an immutable CID, assuming mutable pointer
` + } + + if (mutableRes.Error) { + // Resolution failed + html += `${iconCross} Mutable pointer resolution failed` + html += ` for ${mutableRes.InputPath || 'unknown'}` + html += `
Error: ${mutableRes.Error}
` + if (mutableRes.DiagnosticURL) { + html += `` + } + } else { + // Resolution succeeded + html += `${iconCheck} Mutable pointer resolved successfully` + html += '
' + // Add details box similar to provider results + html += '
' + html += '
Input: ' + (mutableRes.InputPath || 'unknown') + '
' + html += '
Resolved to: ' + (mutableRes.ResolvedPath || 'unknown') + '
' + if (mutableRes.DiagnosticURL) { + html += '' + } + html += '
' + html += '
' + } + + html += '
' + return html +} + function formatMaddrOutput (multiaddr, respObj) { const peerIDStartIndex = multiaddr.lastIndexOf("/p2p/") const peerID = multiaddr.slice(peerIDStartIndex + 5); const addrPart = multiaddr.slice(0, peerIDStartIndex); let outHtml = `
` + // Show resolution info if present (at top level) + outHtml += formatMutableResolution(respObj.MutableResolution) + + // Extract actual peer check result (might be wrapped) + const peerResult = respObj.Result || respObj + // Connection status - if (respObj.ConnectionError !== "") { - outHtml += `
${iconCross}Could not connect to multiaddr: ${respObj.ConnectionError}
` + if (peerResult.ConnectionError !== "") { + outHtml += `
${iconCross}Could not connect to multiaddr: ${peerResult.ConnectionError}
` } else { - const madrs = respObj?.ConnectionMaddrs + const madrs = peerResult?.ConnectionMaddrs outHtml += `
${iconCheck}Successfully connected to multiaddr${madrs?.length > 1 ? 's' : '' }:
${madrs.join('
')}
` } // DHT status if (multiaddr.indexOf("/p2p/") === 0 && multiaddr.lastIndexOf("/") === 4) { // only peer id passed with /p2p/PeerID - if (Object.keys(respObj.PeerFoundInDHT).length === 0) { + if (Object.keys(peerResult.PeerFoundInDHT).length === 0) { outHtml += `
${iconCross}Could not find any multiaddrs in the DHT
` } else { - outHtml += `
${iconCheck}Found multiaddrs advertised in the DHT:
${Object.keys(respObj.PeerFoundInDHT).join('
')}
` + outHtml += `
${iconCheck}Found multiaddrs advertised in the DHT:
${Object.keys(peerResult.PeerFoundInDHT).join('
')}
` } } else { // a proper maddr with an IP was passed let foundAddr = false - for (const key in respObj.PeerFoundInDHT) { + for (const key in peerResult.PeerFoundInDHT) { if (key === addrPart) { foundAddr = true - outHtml += `
${iconCheck}Found multiaddr with ${respObj.PeerFoundInDHT[key]} DHT peers
` + outHtml += `
${iconCheck}Found multiaddr with ${peerResult.PeerFoundInDHT[key]} DHT peers
` break } } if (!foundAddr) { let alt = '' - if (Object.keys(respObj.PeerFoundInDHT).length > 0) { - alt = `
Instead found:
${Object.keys(respObj.PeerFoundInDHT).join('
')}
` + if (Object.keys(peerResult.PeerFoundInDHT).length > 0) { + alt = `
Instead found:
${Object.keys(peerResult.PeerFoundInDHT).join('
')}
` } else { alt = '
No other addresses were found.' } @@ -246,19 +300,19 @@ function formatMaddrOutput (multiaddr, respObj) { } // Provider record - if (respObj.ProviderRecordFromPeerInDHT === true || respObj.ProviderRecordFromPeerInIPNI === true) { - outHtml += `
${iconCheck}Found multihash advertised in ${respObj.ProviderRecordFromPeerInDHT ? 'DHT' : 'IPNI'}
` + if (peerResult.ProviderRecordFromPeerInDHT === true || peerResult.ProviderRecordFromPeerInIPNI === true) { + outHtml += `
${iconCheck}Found multihash advertised in ${peerResult.ProviderRecordFromPeerInDHT ? 'DHT' : 'IPNI'}
` } else { outHtml += `
${iconCross}Could not find the multihash in DHT or IPNI
` } // Bitswap - if (respObj.DataAvailableOverBitswap?.Enabled === true) { - if (respObj.DataAvailableOverBitswap?.Error !== "") { - outHtml += `
${iconCross}There was an error downloading the data for the CID from the peer via Bitswap: ${respObj.DataAvailableOverBitswap.Error}
` - } else if (respObj.DataAvailableOverBitswap?.Responded !== true) { + if (peerResult.DataAvailableOverBitswap?.Enabled === true) { + if (peerResult.DataAvailableOverBitswap?.Error !== "") { + outHtml += `
${iconCross}There was an error downloading the data for the CID from the peer via Bitswap: ${peerResult.DataAvailableOverBitswap.Error}
` + } else if (peerResult.DataAvailableOverBitswap?.Responded !== true) { outHtml += `
${iconCross}The peer did not quickly respond if it had the data for the CID over Bitswap
` - } else if (respObj.DataAvailableOverBitswap?.Found === true) { + } else if (peerResult.DataAvailableOverBitswap?.Found === true) { outHtml += `
${iconCheck}The peer responded that it has the data for the CID over Bitswap
` } else { outHtml += `
${iconCross}The peer responded that it does not have the data for the CID over Bitswap
` @@ -266,14 +320,14 @@ function formatMaddrOutput (multiaddr, respObj) { } // HTTP - if (respObj.DataAvailableOverHTTP?.Enabled === true) { - if (respObj.DataAvailableOverHTTP?.Error !== "") { - outHtml += `
${iconCross}There was an error downloading the data for the CID via HTTP: ${respObj.DataAvailableOverHTTP.Error}
` + if (peerResult.DataAvailableOverHTTP?.Enabled === true) { + if (peerResult.DataAvailableOverHTTP?.Error !== "") { + outHtml += `
${iconCross}There was an error downloading the data for the CID via HTTP: ${peerResult.DataAvailableOverHTTP.Error}
` } - if (respObj.DataAvailableOverHTTP?.Connected !== true) { + if (peerResult.DataAvailableOverHTTP?.Connected !== true) { outHtml += `
${iconCross}HTTP connection was unsuccessful to the HTTP endpoint
` - } else if (respObj.DataAvailableOverHTTP?.Found === true) { + } else if (peerResult.DataAvailableOverHTTP?.Found === true) { outHtml += `
${iconCheck}The HTTP endpoint responded that it has the data for the CID
` } else { outHtml += `
${iconCross}The HTTP endpoint responded that it does not have the data for the CID
` @@ -284,11 +338,25 @@ function formatMaddrOutput (multiaddr, respObj) { } function formatJustCidOutput (resp) { - if (resp.length === 0) { - return `
${iconCross}No providers found for the given CID
` + let outHtml = '' + + // Show resolution info if present (at top level) + if (resp.MutableResolution) { + outHtml += formatMutableResolution(resp.MutableResolution) + // Handle resolution-only response (resolution failed, no providers) + if (!resp.Providers) { + return outHtml + } + } + + // Extract providers array (might be at top level or under Providers key) + const providers = resp.Providers || resp + + if (!Array.isArray(providers) || providers.length === 0) { + return outHtml + `
${iconCross}No providers found for the given CID
` } - const successfulProviders = resp.reduce((acc, provider) => { + const successfulProviders = providers.reduce((acc, provider) => { if(provider.ConnectionError === '' && (provider.DataAvailableOverBitswap?.Found === true || provider.DataAvailableOverHTTP?.Found === true)) { acc++ } @@ -296,7 +364,7 @@ function formatJustCidOutput (resp) { }, 0) // Show providers with the data first, followed by reachable providers, then by those with addresses - resp.sort((a, b) => { + providers.sort((a, b) => { const aHasData = a.DataAvailableOverBitswap?.Found || a.DataAvailableOverHTTP?.Found const bHasData = b.DataAvailableOverBitswap?.Found || b.DataAvailableOverHTTP?.Found @@ -336,9 +404,9 @@ function formatJustCidOutput (resp) { return 0 }) - let outHtml = `
${successfulProviders > 0 ? iconCheck : iconCross} Found ${successfulProviders} working providers (out of ${resp.length} provider records sampled from Amino DHT and IPNI) that could be connected to and had the CID available over Bitswap:
` + outHtml += `
${successfulProviders > 0 ? iconCheck : iconCross} Found ${successfulProviders} working providers (out of ${providers.length} provider records sampled from Amino DHT and IPNI) that could be connected to and had the CID available over Bitswap:
` outHtml += `
` - for (const provider of resp) { + for (const provider of providers) { const couldConnect = provider.ConnectionError === '' const hasBitswap = provider.DataAvailableOverBitswap?.Enabled === true const hasHTTP = provider.DataAvailableOverHTTP?.Enabled === true