diff --git a/CHANGELOG.md b/CHANGELOG.md index 3837e52a8..3b0b09656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ The following emojis are used to highlight certain changes: ### Added +- `routing/http`: ✨ Support for HTTP(S) URLs alongside multiaddrs in Delegated Routing API ([IPIP-518](https://github.com/ipfs/specs/pull/518)) + - The `Addrs` field in the Peer schema now accepts both multiaddr strings (starting with `/`) and HTTP(S) URLs + - Addresses are parsed with defensive programming: unsupported addresses are skipped, and processing continues with remaining addresses + - Special protocol filtering logic: `tls` filter matches both `/tls` multiaddrs and `https://` URLs, `http` filter matches both multiaddrs and URLs with HTTP semantics + - Schema-agnostic implementation allows any URI scheme (not just http/https) for future extensibility + - Includes `ToMultiaddr()` method for backward compatibility during transition period: converts HTTP(S) URLs to multiaddrs using `/https` (matching IPNI convention) and `/dns` (for dual-stack support) + ### Changed - upgrade to `go-libp2p` [v0.44.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.44.0) diff --git a/routing/http/client/client.go b/routing/http/client/client.go index 291793622..8a00ccba7 100644 --- a/routing/http/client/client.go +++ b/routing/http/client/client.go @@ -72,7 +72,7 @@ type Client struct { accepts string peerID peer.ID - addrs []types.Multiaddr + addrs types.Addresses identity crypto.PrivKey // Called immediately after signing a provide request. It is used @@ -182,8 +182,9 @@ func WithUserAgent(ua string) Option { func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) Option { return func(c *Client) error { c.peerID = peerID + c.addrs = make(types.Addresses, 0, len(addrs)) for _, a := range addrs { - c.addrs = append(c.addrs, types.Multiaddr{Multiaddr: a}) + c.addrs = append(c.addrs, *types.NewAddressFromMultiaddr(a)) } return nil } diff --git a/routing/http/client/client_test.go b/routing/http/client/client_test.go index 6171fe85f..b6d48331b 100644 --- a/routing/http/client/client_test.go +++ b/routing/http/client/client_test.go @@ -146,18 +146,21 @@ func makeCID() cid.Cid { return c } -func drAddrsToAddrs(drmas []types.Multiaddr) (addrs []multiaddr.Multiaddr) { - for _, a := range drmas { - addrs = append(addrs, a.Multiaddr) +func drAddrsToAddrs(draddrs types.Addresses) (addrs []multiaddr.Multiaddr) { + for _, a := range draddrs { + if a.IsMultiaddr() { + addrs = append(addrs, a.Multiaddr()) + } } return } -func addrsToDRAddrs(addrs []multiaddr.Multiaddr) (drmas []types.Multiaddr) { +func addrsToDRAddrs(addrs []multiaddr.Multiaddr) types.Addresses { + draddrs := make(types.Addresses, 0, len(addrs)) for _, a := range addrs { - drmas = append(drmas, types.Multiaddr{Multiaddr: a}) + draddrs = append(draddrs, *types.NewAddressFromMultiaddr(a)) } - return + return draddrs } func makePeerRecord(protocols []string) types.PeerRecord { diff --git a/routing/http/contentrouter/contentrouter.go b/routing/http/contentrouter/contentrouter.go index 8a8cfb8ae..831ae505b 100644 --- a/routing/http/contentrouter/contentrouter.go +++ b/routing/http/contentrouter/contentrouter.go @@ -139,7 +139,11 @@ func readProviderResponses(ctx context.Context, iter iter.ResultIter[types.Recor var addrs []multiaddr.Multiaddr for _, a := range result.Addrs { - addrs = append(addrs, a.Multiaddr) + // Try to convert to multiaddr for backward compatibility + if ma := a.ToMultiaddr(); ma != nil { + addrs = append(addrs, ma) + } + // Note: Non-HTTP URLs are skipped as they can't be represented as multiaddrs } select { @@ -167,7 +171,11 @@ func readProviderResponses(ctx context.Context, iter iter.ResultIter[types.Recor var addrs []multiaddr.Multiaddr for _, a := range result.Addrs { - addrs = append(addrs, a.Multiaddr) + // Try to convert to multiaddr for backward compatibility + if ma := a.ToMultiaddr(); ma != nil { + addrs = append(addrs, ma) + } + // Note: Non-HTTP URLs are skipped as they can't be represented as multiaddrs } select { @@ -216,7 +224,11 @@ func (c *contentRouter) FindPeer(ctx context.Context, pid peer.ID) (peer.AddrInf var addrs []multiaddr.Multiaddr for _, a := range res.Val.Addrs { - addrs = append(addrs, a.Multiaddr) + // Try to convert to multiaddr for backward compatibility + if ma := a.ToMultiaddr(); ma != nil { + addrs = append(addrs, ma) + } + // Note: Non-HTTP URLs are skipped as they can't be represented as multiaddrs } // If there are no addresses there's nothing of value to return diff --git a/routing/http/contentrouter/contentrouter_test.go b/routing/http/contentrouter/contentrouter_test.go index 839293617..4b313c05e 100644 --- a/routing/http/contentrouter/contentrouter_test.go +++ b/routing/http/contentrouter/contentrouter_test.go @@ -189,7 +189,7 @@ func TestFindPeer(t *testing.T) { { Schema: types.SchemaPeer, ID: &p1, - Addrs: []types.Multiaddr{{Multiaddr: multiaddr.StringCast("/ip4/1.2.3.4/tcp/1234")}}, + Addrs: types.Addresses{*types.NewAddressFromMultiaddr(multiaddr.StringCast("/ip4/1.2.3.4/tcp/1234"))}, Protocols: []string{"transport-bitswap"}, }, } diff --git a/routing/http/filters/filters.go b/routing/http/filters/filters.go index 9af7bf8b9..273156003 100644 --- a/routing/http/filters/filters.go +++ b/routing/http/filters/filters.go @@ -9,7 +9,6 @@ import ( "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" logging "github.com/ipfs/go-log/v2" - "github.com/multiformats/go-multiaddr" ) var logger = logging.Logger("routing/http/filters") @@ -159,15 +158,16 @@ func applyFilters(provider *types.PeerRecord, filterAddrs, filterProtocols []str return provider } -// applyAddrFilter filters a list of multiaddresses based on the provided filter query. +// applyAddrFilter filters a list of addresses based on the provided filter query. // // Parameters: -// - addrs: A slice of types.Multiaddr to be filtered. +// - addrs: A slice of types.Address to be filtered. // - filterAddrsQuery: A slice of strings representing the filter criteria. // // The function supports both positive and negative filters: -// - Positive filters (e.g., "tcp", "udp") include addresses that match the specified protocols. +// - Positive filters (e.g., "tcp", "udp", "http") include addresses that match the specified protocols. // - Negative filters (e.g., "!tcp", "!udp") exclude addresses that match the specified protocols. +// - "unknown" can be passed to include providers whose addresses are unknown or cannot be parsed. // // If no filters are provided, the original list of addresses is returned unchanged. // If only negative filters are provided, addresses not matching any negative filter are included. @@ -175,60 +175,64 @@ func applyFilters(provider *types.PeerRecord, filterAddrs, filterProtocols []str // If both positive and negative filters are provided, the address must match at least one positive filter and no negative filters to be included. // // Returns: -// A new slice of types.Multiaddr containing only the addresses that pass the filter criteria. -func applyAddrFilter(addrs []types.Multiaddr, filterAddrsQuery []string) []types.Multiaddr { +// A new slice of types.Address containing only the addresses that pass the filter criteria. +func applyAddrFilter(addrs types.Addresses, filterAddrsQuery []string) types.Addresses { if len(filterAddrsQuery) == 0 { return addrs } - var filteredAddrs []types.Multiaddr - var positiveFilters, negativeFilters []multiaddr.Protocol + var filteredAddrs types.Addresses + var positiveFilters, negativeFilters []string + var includeUnknown bool // Separate positive and negative filters for _, filter := range filterAddrsQuery { - if strings.HasPrefix(filter, "!") { - negativeFilters = append(negativeFilters, multiaddr.ProtocolWithName(filter[1:])) + if filter == "unknown" { + includeUnknown = true + } else if strings.HasPrefix(filter, "!") { + negativeFilters = append(negativeFilters, filter[1:]) } else { - positiveFilters = append(positiveFilters, multiaddr.ProtocolWithName(filter)) + positiveFilters = append(positiveFilters, filter) } } for _, addr := range addrs { - protocols := addr.Protocols() + // Handle unknown (unparseable) addresses + if !addr.IsValid() { + if includeUnknown { + filteredAddrs = append(filteredAddrs, addr) + } + continue + } // Check negative filters - if containsAny(protocols, negativeFilters) { + shouldExclude := false + for _, filter := range negativeFilters { + if addr.HasProtocol(filter) { + shouldExclude = true + break + } + } + if shouldExclude { continue } // If no positive filters or matches a positive filter, include the address - if len(positiveFilters) == 0 || containsAny(protocols, positiveFilters) { + if len(positiveFilters) == 0 { filteredAddrs = append(filteredAddrs, addr) + } else { + for _, filter := range positiveFilters { + if addr.HasProtocol(filter) { + filteredAddrs = append(filteredAddrs, addr) + break + } + } } } return filteredAddrs } -// Helper function to check if protocols contain any of the filters -func containsAny(protocols []multiaddr.Protocol, filters []multiaddr.Protocol) bool { - for _, filter := range filters { - if containsProtocol(protocols, filter) { - return true - } - } - return false -} - -func containsProtocol(protos []multiaddr.Protocol, proto multiaddr.Protocol) bool { - for _, p := range protos { - if p.Code == proto.Code { - return true - } - } - return false -} - // protocolsAllowed returns true if the peerProtocols are allowed by the filter protocols. func protocolsAllowed(peerProtocols []string, filterProtocols []string) bool { if len(filterProtocols) == 0 { diff --git a/routing/http/filters/filters_test.go b/routing/http/filters/filters_test.go index d86316045..5fde888af 100644 --- a/routing/http/filters/filters_test.go +++ b/routing/http/filters/filters_test.go @@ -74,21 +74,21 @@ func TestApplyAddrFilter(t *testing.T) { addr7, _ := multiaddr.NewMultiaddr("/dns4/ny5.bootstrap.libp2p.io/tcp/443/wss/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt") addr8, _ := multiaddr.NewMultiaddr("/ip4/127.0.0.1/udp/4001/quic-v1/webtransport/certhash/uEiAMrMcVWFNiqtSeRXZTwHTac4p9WcGh5hg8kVBzTC1JTA/certhash/uEiA4dfvbbbnBIYalhp1OpW1Bk-nuWIKSy21ol6vPea67Cw/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt") - addrs := []types.Multiaddr{ - {Multiaddr: addr1}, - {Multiaddr: addr2}, - {Multiaddr: addr3}, - {Multiaddr: addr4}, - {Multiaddr: addr5}, - {Multiaddr: addr6}, - {Multiaddr: addr7}, - {Multiaddr: addr8}, + addrs := types.Addresses{ + *types.NewAddressFromMultiaddr(addr1), + *types.NewAddressFromMultiaddr(addr2), + *types.NewAddressFromMultiaddr(addr3), + *types.NewAddressFromMultiaddr(addr4), + *types.NewAddressFromMultiaddr(addr5), + *types.NewAddressFromMultiaddr(addr6), + *types.NewAddressFromMultiaddr(addr7), + *types.NewAddressFromMultiaddr(addr8), } testCases := []struct { name string filterAddrs []string - expectedAddrs []types.Multiaddr + expectedAddrs types.Addresses }{ { name: "No filter", @@ -98,52 +98,52 @@ func TestApplyAddrFilter(t *testing.T) { { name: "Filter TCP", filterAddrs: []string{"tcp"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr1}, {Multiaddr: addr3}, {Multiaddr: addr4}, {Multiaddr: addr7}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr1), *types.NewAddressFromMultiaddr(addr3), *types.NewAddressFromMultiaddr(addr4), *types.NewAddressFromMultiaddr(addr7)}, }, { name: "Filter UDP", filterAddrs: []string{"udp"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr2}, {Multiaddr: addr5}, {Multiaddr: addr6}, {Multiaddr: addr8}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr2), *types.NewAddressFromMultiaddr(addr5), *types.NewAddressFromMultiaddr(addr6), *types.NewAddressFromMultiaddr(addr8)}, }, { name: "Filter WebSocket", filterAddrs: []string{"ws"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr3}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr3)}, }, { name: "Exclude TCP", filterAddrs: []string{"!tcp"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr2}, {Multiaddr: addr5}, {Multiaddr: addr6}, {Multiaddr: addr8}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr2), *types.NewAddressFromMultiaddr(addr5), *types.NewAddressFromMultiaddr(addr6), *types.NewAddressFromMultiaddr(addr8)}, }, { name: "Filter TCP addresses that don't have WebSocket and p2p-circuit", filterAddrs: []string{"tcp", "!ws", "!wss", "!p2p-circuit"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr1}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr1)}, }, { name: "Include WebTransport and exclude p2p-circuit", filterAddrs: []string{"webtransport", "!p2p-circuit"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr8}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr8)}, }, { name: "empty for unknown protocol nae", filterAddrs: []string{"fakeproto"}, - expectedAddrs: []types.Multiaddr{}, + expectedAddrs: types.Addresses{}, }, { name: "Include WebTransport but ignore unknown protocol name", filterAddrs: []string{"webtransport", "fakeproto"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr6}, {Multiaddr: addr8}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr6), *types.NewAddressFromMultiaddr(addr8)}, }, { name: "Multiple filters", filterAddrs: []string{"tcp", "ws"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr1}, {Multiaddr: addr3}, {Multiaddr: addr4}, {Multiaddr: addr7}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr1), *types.NewAddressFromMultiaddr(addr3), *types.NewAddressFromMultiaddr(addr4), *types.NewAddressFromMultiaddr(addr7)}, }, { name: "Multiple negative filters", filterAddrs: []string{"!tcp", "!ws"}, - expectedAddrs: []types.Multiaddr{{Multiaddr: addr2}, {Multiaddr: addr5}, {Multiaddr: addr6}, {Multiaddr: addr8}}, + expectedAddrs: types.Addresses{*types.NewAddressFromMultiaddr(addr2), *types.NewAddressFromMultiaddr(addr5), *types.NewAddressFromMultiaddr(addr6), *types.NewAddressFromMultiaddr(addr8)}, }, } @@ -156,24 +156,24 @@ func TestApplyAddrFilter(t *testing.T) { for _, expectedAddr := range tc.expectedAddrs { found := false for _, resultAddr := range result { - if expectedAddr.Multiaddr.Equal(resultAddr.Multiaddr) { + if expectedAddr.IsMultiaddr() && resultAddr.IsMultiaddr() && expectedAddr.Multiaddr().Equal(resultAddr.Multiaddr()) { found = true break } } - assert.True(t, found, "Expected address not found in test %s result: %s", tc.name, expectedAddr.Multiaddr) + assert.True(t, found, "Expected address not found in test %s result: %s", tc.name, expectedAddr.String()) } // Check that each result address is in the expected list for _, resultAddr := range result { found := false for _, expectedAddr := range tc.expectedAddrs { - if resultAddr.Multiaddr.Equal(expectedAddr.Multiaddr) { + if resultAddr.IsMultiaddr() && expectedAddr.IsMultiaddr() && resultAddr.Multiaddr().Equal(expectedAddr.Multiaddr()) { found = true break } } - assert.True(t, found, "Unexpected address found in test %s result: %s", tc.name, resultAddr.Multiaddr) + assert.True(t, found, "Unexpected address found in test %s result: %s", tc.name, resultAddr.String()) } }) } @@ -271,9 +271,9 @@ func TestApplyFilters(t *testing.T) { name: "No filters", provider: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -281,9 +281,9 @@ func TestApplyFilters(t *testing.T) { filterProtocols: []string{}, expected: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -292,13 +292,13 @@ func TestApplyFilters(t *testing.T) { name: "Protocol filter", provider: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001"), - mustMultiaddr(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001/ws"), - mustMultiaddr(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/127.0.0.1/tcp/4001"), + mustAddress(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), + mustAddress(t, "/ip4/127.0.0.1/tcp/4001/ws"), + mustAddress(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -306,13 +306,13 @@ func TestApplyFilters(t *testing.T) { filterProtocols: []string{"transport-ipfs-gateway-http", "transport-bitswap"}, expected: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001"), - mustMultiaddr(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001/ws"), - mustMultiaddr(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/127.0.0.1/tcp/4001"), + mustAddress(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), + mustAddress(t, "/ip4/127.0.0.1/tcp/4001/ws"), + mustAddress(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -321,14 +321,14 @@ func TestApplyFilters(t *testing.T) { name: "Address filter", provider: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001"), - mustMultiaddr(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), - mustMultiaddr(t, "/ip4/127.0.0.1/tcp/4001/ws"), - mustMultiaddr(t, "/ip4/127.0.0.1/udp/4001/webrtc-direct/certhash/uEiCZqN653gMqxrWNmYuNg7Emwb-wvtsuzGE3XD6rypViZA"), - mustMultiaddr(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/127.0.0.1/tcp/4001"), + mustAddress(t, "/ip4/127.0.0.1/udp/4001/quic-v1"), + mustAddress(t, "/ip4/127.0.0.1/tcp/4001/ws"), + mustAddress(t, "/ip4/127.0.0.1/udp/4001/webrtc-direct/certhash/uEiCZqN653gMqxrWNmYuNg7Emwb-wvtsuzGE3XD6rypViZA"), + mustAddress(t, "/ip4/102.101.1.1/tcp/4001/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/102.101.1.1/udp/4001/quic-v1/webtransport/p2p/12D3KooWEjsGPUQJ4Ej3d1Jcg4VckWhFbhc6mkGunMm1faeSzZMu/p2p-circuit"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -336,9 +336,9 @@ func TestApplyFilters(t *testing.T) { filterProtocols: []string{"transport-ipfs-gateway-http", "transport-bitswap"}, expected: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/127.0.0.1/udp/4001/webrtc-direct/certhash/uEiCZqN653gMqxrWNmYuNg7Emwb-wvtsuzGE3XD6rypViZA"), - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/127.0.0.1/udp/4001/webrtc-direct/certhash/uEiCZqN653gMqxrWNmYuNg7Emwb-wvtsuzGE3XD6rypViZA"), + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, Protocols: []string{"transport-ipfs-gateway-http"}, }, @@ -347,16 +347,16 @@ func TestApplyFilters(t *testing.T) { name: "Unknown protocol filter", provider: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, }, filterAddrs: []string{}, filterProtocols: []string{"unknown"}, expected: &types.PeerRecord{ ID: &pid, - Addrs: []types.Multiaddr{ - mustMultiaddr(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), + Addrs: types.Addresses{ + mustAddress(t, "/ip4/8.8.8.8/udp/4001/quic-v1/webtransport"), }, }, }, @@ -370,10 +370,10 @@ func TestApplyFilters(t *testing.T) { } } -func mustMultiaddr(t *testing.T, s string) types.Multiaddr { +func mustAddress(t *testing.T, s string) types.Address { addr, err := multiaddr.NewMultiaddr(s) if err != nil { t.Fatalf("Failed to create multiaddr: %v", err) } - return types.Multiaddr{Multiaddr: addr} + return *types.NewAddressFromMultiaddr(addr) } diff --git a/routing/http/server/server.go b/routing/http/server/server.go index 7e96d58f2..87af039ea 100644 --- a/routing/http/server/server.go +++ b/routing/http/server/server.go @@ -414,9 +414,17 @@ func (s *server) provide(w http.ResponseWriter, httpReq *http.Request) { for i, k := range v.Payload.Keys { keys[i] = k.Cid } - addrs := make([]multiaddr.Multiaddr, len(v.Payload.Addrs)) - for i, a := range v.Payload.Addrs { - addrs[i] = a.Multiaddr + // Convert addresses to multiaddrs for ProvideBitswap + // TODO: Similar to contentrouter, we may need to handle URLs specially here + addrs := make([]multiaddr.Multiaddr, 0, len(v.Payload.Addrs)) + for _, a := range v.Payload.Addrs { + if a.IsMultiaddr() { + addrs = append(addrs, a.Multiaddr()) + } + // TODO: if URL is https:// or http://, special-case it and convert + // to multiaddr (e.g., /dns4/example.com/tcp/443/tls/http) to ensure + // smooth transition during the period when existing software expects + // /ip.../http multiaddrs as a way of signaling HTTP retrieval is supported } advisoryTTL, err := s.svc.ProvideBitswap(httpReq.Context(), &BitswapWriteProvideRequest{ Keys: keys, diff --git a/routing/http/server/server_test.go b/routing/http/server/server_test.go index 29e259925..4782d6cef 100644 --- a/routing/http/server/server_test.go +++ b/routing/http/server/server_test.go @@ -124,20 +124,20 @@ func TestProviders(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap"}, - Addrs: []types.Multiaddr{ - {Multiaddr: addr1}, - {Multiaddr: addr2}, - {Multiaddr: addr3}, - {Multiaddr: addr4}, - {Multiaddr: addr5}, - {Multiaddr: addr6}, + Addrs: types.Addresses{ + *types.NewAddressFromMultiaddr(addr1), + *types.NewAddressFromMultiaddr(addr2), + *types.NewAddressFromMultiaddr(addr3), + *types.NewAddressFromMultiaddr(addr4), + *types.NewAddressFromMultiaddr(addr5), + *types.NewAddressFromMultiaddr(addr6), }, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid2, Protocols: []string{"transport-ipfs-gateway-http"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, }, ) @@ -388,13 +388,13 @@ func TestPeers(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap", "transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, }) @@ -433,19 +433,19 @@ func TestPeers(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap", "transport-foo"}, - Addrs: []types.Multiaddr{ - {Multiaddr: addr1}, - {Multiaddr: addr2}, - {Multiaddr: addr3}, - {Multiaddr: addr4}, + Addrs: types.Addresses{ + *types.NewAddressFromMultiaddr(addr1), + *types.NewAddressFromMultiaddr(addr2), + *types.NewAddressFromMultiaddr(addr3), + *types.NewAddressFromMultiaddr(addr4), }, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid2, Protocols: []string{"transport-foo"}, - Addrs: []types.Multiaddr{ - {Multiaddr: addr5}, + Addrs: types.Addresses{ + *types.NewAddressFromMultiaddr(addr5), }, }}, }) @@ -485,19 +485,19 @@ func TestPeers(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap", "transport-foo"}, - Addrs: []types.Multiaddr{ - {Multiaddr: addr1}, - {Multiaddr: addr2}, - {Multiaddr: addr3}, - {Multiaddr: addr4}, + Addrs: types.Addresses{ + *types.NewAddressFromMultiaddr(addr1), + *types.NewAddressFromMultiaddr(addr2), + *types.NewAddressFromMultiaddr(addr3), + *types.NewAddressFromMultiaddr(addr4), }, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid2, Protocols: []string{"transport-foo"}, - Addrs: []types.Multiaddr{ - {Multiaddr: addr5}, + Addrs: types.Addresses{ + *types.NewAddressFromMultiaddr(addr5), }, }}, }) @@ -556,13 +556,13 @@ func TestPeers(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap", "transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, }) @@ -616,13 +616,13 @@ func TestPeers(t *testing.T) { Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-bitswap", "transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, {Val: &types.PeerRecord{ Schema: types.SchemaPeer, ID: &pid, Protocols: []string{"transport-foo"}, - Addrs: []types.Multiaddr{}, + Addrs: types.Addresses{}, }}, } diff --git a/routing/http/types/address.go b/routing/http/types/address.go new file mode 100644 index 000000000..1829937cc --- /dev/null +++ b/routing/http/types/address.go @@ -0,0 +1,319 @@ +package types + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + "strings" + + "github.com/ipfs/boxo/routing/http/internal/drjson" + "github.com/multiformats/go-multiaddr" +) + +// Address represents an address that can be either a multiaddr or a URI. +// It implements the parsing logic from IPIP-518: strings starting with '/' +// are parsed as multiaddrs, others are parsed as URIs. +// This type is schema-agnostic and will accept any valid URI scheme. +type Address struct { + raw string + multiaddr multiaddr.Multiaddr + url *url.URL +} + +// NewAddress creates a new Address from a string. +// It accepts any valid multiaddr or URI, following IPIP-518 parsing rules. +func NewAddress(s string) (*Address, error) { + addr := &Address{raw: s} + + // IPIP-518 parsing logic + if strings.HasPrefix(s, "/") { + // Parse as multiaddr + ma, err := multiaddr.NewMultiaddr(s) + if err != nil { + return nil, fmt.Errorf("invalid multiaddr: %w", err) + } + addr.multiaddr = ma + } else { + // Parse as URI - accept any valid URI scheme + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("invalid URI: %w", err) + } + // Must be absolute URL + if !u.IsAbs() { + return nil, fmt.Errorf("URI must be absolute") + } + addr.url = u + } + + return addr, nil +} + +// NewAddressFromMultiaddr creates a new Address from a multiaddr. +func NewAddressFromMultiaddr(ma multiaddr.Multiaddr) *Address { + return &Address{ + raw: ma.String(), + multiaddr: ma, + } +} + +// String returns the original string representation of the address. +func (a *Address) String() string { + return a.raw +} + +// Multiaddr returns the multiaddr if this is a multiaddr, nil otherwise. +func (a *Address) Multiaddr() multiaddr.Multiaddr { + return a.multiaddr +} + +// URL returns the URL if this is a URL, nil otherwise. +func (a *Address) URL() *url.URL { + return a.url +} + +// IsMultiaddr returns true if this address is a multiaddr. +func (a *Address) IsMultiaddr() bool { + return a.multiaddr != nil +} + +// IsURL returns true if this address is a URL. +func (a *Address) IsURL() bool { + return a.url != nil +} + +// IsValid returns true if the address was successfully parsed as either +// a multiaddr or a URI. Returns false for unparseable addresses. +func (a *Address) IsValid() bool { + return a.multiaddr != nil || a.url != nil +} + +// MarshalJSON implements json.Marshaler. +func (a *Address) MarshalJSON() ([]byte, error) { + return drjson.MarshalJSONBytes(a.raw) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (a *Address) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + addr, err := NewAddress(s) + if err != nil { + // Per IPIP-518: implementations MUST skip addresses they cannot parse + // We'll store the raw string but mark it as invalid + a.raw = s + a.multiaddr = nil + a.url = nil + return nil // Don't return error, just skip + } + + *a = *addr + return nil +} + +// Protocols returns the protocols in this address. +// For multiaddrs, it returns the multiaddr protocols. +// For URLs, it returns the scheme. +func (a *Address) Protocols() []string { + if a.url != nil { + return []string{a.url.Scheme} + } else if a.multiaddr != nil { + protos := a.multiaddr.Protocols() + result := make([]string, len(protos)) + for i, p := range protos { + result[i] = p.Name + } + return result + } + return nil +} + +// HasProtocol checks if the address contains the given protocol. +// For URLs, it checks the scheme. For multiaddrs, it checks the protocols. +// Special handling for http/https/tls as per IPIP-518: +// - "http" matches http://, https:// URLs and /http, /tls/http multiaddrs +// - "https" matches https:// URLs, /tls/http, /https multiaddrs +// - "tls" matches /tls multiaddrs AND https:// URLs +func (a *Address) HasProtocol(proto string) bool { + proto = strings.ToLower(proto) + + if a.url != nil { + scheme := strings.ToLower(a.url.Scheme) + + switch proto { + case "http": + // "http" matches both http and https URLs + return scheme == "http" || scheme == "https" + case "https": + return scheme == "https" + case "tls": + // TLS matches https URLs + return scheme == "https" + default: + return scheme == proto + } + } else if a.multiaddr != nil { + protocols := a.Protocols() + + switch proto { + case "http": + // "http" matches /http, including /tls/http + for _, p := range protocols { + if p == "http" { + return true + } + } + case "https": + // "https" matches /https or the combination /tls/http + hasTLS := false + hasHTTP := false + for _, p := range protocols { + if p == "https" { + return true + } + if p == "tls" { + hasTLS = true + } + if p == "http" { + hasHTTP = true + } + } + return hasTLS && hasHTTP + case "tls": + // "tls" matches any multiaddr with /tls + for _, p := range protocols { + if p == "tls" { + return true + } + } + default: + for _, p := range protocols { + if p == proto { + return true + } + } + } + } + + return false +} + +// ToMultiaddr attempts to convert an HTTP(S) URL to a multiaddr for backward compatibility. +// Returns nil if the address cannot be converted (e.g., non-HTTP schemes or invalid addresses). +// This is a temporary compatibility layer for the transition period while existing software +// expects multiaddrs with /http protocol to signal HTTP retrieval support. +func (a *Address) ToMultiaddr() multiaddr.Multiaddr { + // If already a multiaddr, return it as-is + if a.IsMultiaddr() { + return a.multiaddr + } + + // If not a URL, cannot convert + if !a.IsURL() || a.url == nil { + return nil + } + + // Only convert http/https URLs + scheme := strings.ToLower(a.url.Scheme) + if scheme != "http" && scheme != "https" { + return nil + } + + // Parse hostname and port + host := a.url.Hostname() + port := a.url.Port() + + // Set default ports if not specified + if port == "" { + if scheme == "https" { + port = "443" + } else { + port = "80" + } + } + + // Determine address type + var addrProto string + if ip := net.ParseIP(host); ip != nil { + // Use IP-specific protocols for IP addresses + if ip.To4() != nil { + addrProto = "ip4" + } else { + addrProto = "ip6" + } + } else { + // Use generic /dns for domain names (resolves to both IPv4 and IPv6) + addrProto = "dns" + } + + // Build multiaddr string + var maStr string + if scheme == "https" { + // For HTTPS, use /https as this is what existing HTTP providers + // announce on IPNI, so we follow same convention for backward-compatibility + maStr = fmt.Sprintf("/%s/%s/tcp/%s/https", addrProto, host, port) + } else { + // For HTTP, use /http + maStr = fmt.Sprintf("/%s/%s/tcp/%s/http", addrProto, host, port) + } + + // Create and return multiaddr + ma, err := multiaddr.NewMultiaddr(maStr) + if err != nil { + // Log the error for debugging but return nil + // This can happen with invalid hostnames or other edge cases + return nil + } + return ma +} + +// Addresses is a slice of Address that can be marshaled/unmarshaled from/to JSON. +type Addresses []Address + +// String returns a string representation of the addresses for printing. +func (addrs Addresses) String() string { + if len(addrs) == 0 { + return "[]" + } + + strs := make([]string, len(addrs)) + for i, addr := range addrs { + strs[i] = addr.String() + } + return fmt.Sprintf("[%s]", strings.Join(strs, " ")) +} + +// MarshalJSON implements json.Marshaler for Addresses. +func (addrs Addresses) MarshalJSON() ([]byte, error) { + strs := make([]string, len(addrs)) + for i, addr := range addrs { + strs[i] = addr.String() + } + return json.Marshal(strs) +} + +// UnmarshalJSON implements json.Unmarshaler for Addresses. +// Per IPIP-518, it MUST skip addresses that cannot be parsed. +func (addrs *Addresses) UnmarshalJSON(b []byte) error { + var strs []string + if err := json.Unmarshal(b, &strs); err != nil { + return err + } + + result := make(Addresses, 0, len(strs)) + for _, s := range strs { + addr := Address{raw: s} + if a, err := NewAddress(s); err == nil { + addr = *a + } + // Always add the address, even if invalid (will be skipped during filtering) + result = append(result, addr) + } + + *addrs = result + return nil +} diff --git a/routing/http/types/address_test.go b/routing/http/types/address_test.go new file mode 100644 index 000000000..1f07a07e1 --- /dev/null +++ b/routing/http/types/address_test.go @@ -0,0 +1,444 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewAddress(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + isURL bool + isMultiaddr bool + protocols []string + }{ + { + name: "valid multiaddr", + input: "/ip4/127.0.0.1/tcp/4001", + wantErr: false, + isURL: false, + isMultiaddr: true, + protocols: []string{"ip4", "tcp"}, + }, + { + name: "valid https URL", + input: "https://example.com", + wantErr: false, + isURL: true, + isMultiaddr: false, + protocols: []string{"https"}, + }, + { + name: "valid http URL", + input: "http://example.com:8080", + wantErr: false, + isURL: true, + isMultiaddr: false, + protocols: []string{"http"}, + }, + { + name: "valid http URL with path", + input: "http://example.com:8080/path", + wantErr: false, + isURL: true, + isMultiaddr: false, + protocols: []string{"http"}, + }, + { + name: "other URI scheme foo", + input: "foo://example.com/path", + wantErr: false, + isURL: true, + isMultiaddr: false, + protocols: []string{"foo"}, + }, + { + name: "other URI scheme bar", + input: "bar://something", + wantErr: false, + isURL: true, + isMultiaddr: false, + protocols: []string{"bar"}, + }, + { + name: "relative URL", + input: "example.com", + wantErr: true, + isURL: false, + isMultiaddr: false, + }, + { + name: "invalid multiaddr", + input: "/invalid", + wantErr: true, + isURL: false, + isMultiaddr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := NewAddress(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.input, addr.String()) + assert.Equal(t, tt.isURL, addr.IsURL()) + assert.Equal(t, tt.isMultiaddr, addr.IsMultiaddr()) + assert.Equal(t, tt.protocols, addr.Protocols()) + }) + } +} + +func TestAddressHasProtocol(t *testing.T) { + tests := []struct { + name string + address string + protocol string + expected bool + }{ + // Multiaddr tests + { + name: "multiaddr has tcp", + address: "/ip4/127.0.0.1/tcp/4001", + protocol: "tcp", + expected: true, + }, + { + name: "multiaddr doesn't have udp", + address: "/ip4/127.0.0.1/tcp/4001", + protocol: "udp", + expected: false, + }, + { + name: "multiaddr with /http", + address: "/dns4/example.com/tcp/80/http", + protocol: "http", + expected: true, + }, + { + name: "multiaddr with /tls/http matches https", + address: "/dns4/example.com/tcp/443/tls/http", + protocol: "https", + expected: true, + }, + { + name: "multiaddr with /tls/http matches http", + address: "/dns4/example.com/tcp/443/tls/http", + protocol: "http", + expected: true, + }, + { + name: "multiaddr with /tls/http matches tls", + address: "/dns4/example.com/tcp/443/tls/http", + protocol: "tls", + expected: true, + }, + // URL tests + { + name: "https URL matches https", + address: "https://example.com", + protocol: "https", + expected: true, + }, + { + name: "https URL matches http", + address: "https://example.com", + protocol: "http", + expected: true, + }, + { + name: "https URL matches tls", + address: "https://example.com", + protocol: "tls", + expected: true, + }, + { + name: "http URL matches http", + address: "http://example.com", + protocol: "http", + expected: true, + }, + { + name: "http URL doesn't match https", + address: "http://example.com", + protocol: "https", + expected: false, + }, + { + name: "http URL doesn't match tls", + address: "http://example.com", + protocol: "tls", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := NewAddress(tt.address) + require.NoError(t, err) + assert.Equal(t, tt.expected, addr.HasProtocol(tt.protocol)) + }) + } +} + +func TestAddressJSON(t *testing.T) { + tests := []struct { + name string + address string + }{ + { + name: "multiaddr", + address: "/ip4/127.0.0.1/tcp/4001", + }, + { + name: "https URL", + address: "https://example.com", + }, + { + name: "http URL with port", + address: "http://example.com:8080", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := NewAddress(tt.address) + require.NoError(t, err) + + // Marshal to JSON + data, err := json.Marshal(addr) + require.NoError(t, err) + + // Should be a JSON string + var str string + err = json.Unmarshal(data, &str) + require.NoError(t, err) + assert.Equal(t, tt.address, str) + + // Unmarshal back + var addr2 Address + err = json.Unmarshal(data, &addr2) + require.NoError(t, err) + assert.Equal(t, addr.String(), addr2.String()) + assert.Equal(t, addr.IsURL(), addr2.IsURL()) + assert.Equal(t, addr.IsMultiaddr(), addr2.IsMultiaddr()) + }) + } +} + +func TestAddressesJSON(t *testing.T) { + input := []string{ + "/ip4/127.0.0.1/tcp/4001", + "https://example.com", + "http://localhost:8080", + "/invalid/addr", // This should be included but marked as invalid + } + + // Create Addresses from strings + var addrs Addresses + data, err := json.Marshal(input) + require.NoError(t, err) + + err = json.Unmarshal(data, &addrs) + require.NoError(t, err) + + // Should have all 4 addresses + assert.Len(t, addrs, 4) + + // First three should be valid + assert.True(t, addrs[0].IsValid()) + assert.True(t, addrs[0].IsMultiaddr()) + + assert.True(t, addrs[1].IsValid()) + assert.True(t, addrs[1].IsURL()) + + assert.True(t, addrs[2].IsValid()) + assert.True(t, addrs[2].IsURL()) + + // Last one should be invalid but present + assert.False(t, addrs[3].IsValid()) + assert.Equal(t, "/invalid/addr", addrs[3].String()) + + // Marshal back should give the same strings + data2, err := json.Marshal(addrs) + require.NoError(t, err) + + var output []string + err = json.Unmarshal(data2, &output) + require.NoError(t, err) + + assert.Equal(t, input, output) +} + +func TestNewAddressFromMultiaddr(t *testing.T) { + ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/4001") + require.NoError(t, err) + + addr := NewAddressFromMultiaddr(ma) + assert.True(t, addr.IsMultiaddr()) + assert.False(t, addr.IsURL()) + assert.Equal(t, ma.String(), addr.String()) + assert.Equal(t, ma, addr.Multiaddr()) + assert.Nil(t, addr.URL()) +} + +func TestPeerRecordWithMixedAddresses(t *testing.T) { + // Test that PeerRecord can handle mixed addresses + jsonData := `{ + "Schema": "peer", + "ID": "12D3KooWM8sovaEGU1bmiWGWAzvs47DEcXKZZTuJnpQyVTkRs2Vn", + "Addrs": [ + "/ip4/192.168.1.1/tcp/4001", + "https://trustless-gateway.example.com", + "http://example.org:8080", + "/dns4/libp2p.example.com/tcp/443/wss" + ], + "Protocols": ["transport-bitswap", "transport-ipfs-gateway-http"] + }` + + var pr PeerRecord + err := json.Unmarshal([]byte(jsonData), &pr) + require.NoError(t, err) + + assert.Equal(t, "peer", pr.Schema) + assert.Len(t, pr.Addrs, 4) + + // Check each address + assert.True(t, pr.Addrs[0].IsMultiaddr()) + assert.True(t, pr.Addrs[1].IsURL()) + assert.True(t, pr.Addrs[2].IsURL()) + assert.True(t, pr.Addrs[3].IsMultiaddr()) + + // Marshal back + data, err := json.Marshal(pr) + require.NoError(t, err) + + // Check it's valid JSON + var check map[string]interface{} + err = json.Unmarshal(data, &check) + require.NoError(t, err) + + addrs, ok := check["Addrs"].([]interface{}) + require.True(t, ok) + assert.Len(t, addrs, 4) +} + +func TestAddressToMultiaddr(t *testing.T) { + tests := []struct { + name string + input string + expected string // Expected multiaddr string, empty if conversion not possible + }{ + { + name: "https URL with default port", + input: "https://example.com", + expected: "/dns/example.com/tcp/443/https", + }, + { + name: "http URL with default port", + input: "http://example.com", + expected: "/dns/example.com/tcp/80/http", + }, + { + name: "http URL with custom port", + input: "http://example.com:8080", + expected: "/dns/example.com/tcp/8080/http", + }, + { + name: "https URL with custom port", + input: "https://example.com:8443", + expected: "/dns/example.com/tcp/8443/https", + }, + { + name: "http URL with IPv4", + input: "http://192.168.1.1:8080", + expected: "/ip4/192.168.1.1/tcp/8080/http", + }, + { + name: "https URL with IPv4", + input: "https://192.168.1.1", + expected: "/ip4/192.168.1.1/tcp/443/https", + }, + { + name: "http URL with IPv6", + input: "http://[::1]:8080", + expected: "/ip6/::1/tcp/8080/http", + }, + { + name: "https URL with IPv6", + input: "https://[::1]", + expected: "/ip6/::1/tcp/443/https", + }, + { + name: "http URL with path - path portion ignored", + input: "http://example.com/path/to/resource", + expected: "/dns/example.com/tcp/80/http", + }, + { + name: "https URL with query - path and query portions ignored", + input: "https://example.com:8443/path?query=value", + expected: "/dns/example.com/tcp/8443/https", + }, + { + name: "non-HTTP scheme not converted", + input: "ftp://example.com", + expected: "", + }, + { + name: "websocket scheme not converted", + input: "ws://example.com", + expected: "", + }, + { + name: "existing multiaddr returned as-is", + input: "/ip4/127.0.0.1/tcp/4001", + expected: "/ip4/127.0.0.1/tcp/4001", + }, + { + name: "existing http multiaddr returned as-is", + input: "/dns/example.com/tcp/443/https", + expected: "/dns/example.com/tcp/443/https", + }, + { + name: "localhost http", + input: "http://localhost:8080", + expected: "/dns/localhost/tcp/8080/http", + }, + { + name: "localhost https", + input: "https://localhost", + expected: "/dns/localhost/tcp/443/https", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := NewAddress(tt.input) + require.NoError(t, err) + + ma := addr.ToMultiaddr() + if tt.expected == "" { + assert.Nil(t, ma, "Expected nil multiaddr for %s", tt.input) + } else { + require.NotNil(t, ma, "Expected non-nil multiaddr for %s", tt.input) + assert.Equal(t, tt.expected, ma.String()) + } + }) + } + + // Test with invalid address + t.Run("invalid address returns nil", func(t *testing.T) { + addr := &Address{raw: "invalid"} + ma := addr.ToMultiaddr() + assert.Nil(t, ma) + }) +} diff --git a/routing/http/types/record_bitswap.go b/routing/http/types/record_bitswap.go index bf749e515..d3f1b17f9 100644 --- a/routing/http/types/record_bitswap.go +++ b/routing/http/types/record_bitswap.go @@ -26,7 +26,7 @@ type BitswapRecord struct { Schema string Protocol string ID *peer.ID - Addrs []Multiaddr `json:",omitempty"` + Addrs Addresses `json:",omitempty"` } func (br *BitswapRecord) GetSchema() string { @@ -53,7 +53,7 @@ type BitswapPayload struct { Timestamp *Time AdvisoryTTL *Duration ID *peer.ID - Addrs []Multiaddr + Addrs Addresses } func (wr *WriteBitswapRecord) GetSchema() string { diff --git a/routing/http/types/record_peer.go b/routing/http/types/record_peer.go index cb4a04fca..00b41093e 100644 --- a/routing/http/types/record_peer.go +++ b/routing/http/types/record_peer.go @@ -14,7 +14,7 @@ var _ Record = &PeerRecord{} type PeerRecord struct { Schema string ID *peer.ID - Addrs []Multiaddr + Addrs Addresses Protocols []string // Extra contains extra fields that were included in the original JSON raw @@ -31,7 +31,7 @@ func (pr *PeerRecord) UnmarshalJSON(b []byte) error { v := struct { Schema string ID *peer.ID - Addrs []Multiaddr + Addrs Addresses Protocols []string }{} err := json.Unmarshal(b, &v)