| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "io" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "strings" |
| 9 | "sync" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/ipfs/kubo/config" |
| 14 | "github.com/ipfs/kubo/test/cli/harness" |
| 15 | . "github.com/ipfs/kubo/test/cli/testutils" |
| 16 | "github.com/stretchr/testify/assert" |
| 17 | "github.com/stretchr/testify/require" |
| 18 | ) |
| 19 | |
| 20 | func TestHTTPDelegatedRouting(t *testing.T) { |
| 21 | t.Parallel() |
| 22 | node := harness.NewT(t).NewNode().Init().StartDaemon() |
| 23 | |
| 24 | fakeServer := func(contentType string, resp ...string) *httptest.Server { |
| 25 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 26 | w.Header().Set("Content-Type", contentType) |
| 27 | for _, r := range resp { |
| 28 | _, err := w.Write([]byte(r)) |
| 29 | if err != nil { |
| 30 | panic(err) |
| 31 | } |
| 32 | } |
| 33 | })) |
| 34 | } |
| 35 | |
| 36 | findProvsCID := "baeabep4vu3ceru7nerjjbk37sxb7wmftteve4hcosmyolsbsiubw2vr6pqzj6mw7kv6tbn6nqkkldnklbjgm5tzbi4hkpkled4xlcr7xz4bq" |
| 37 | provs := []string{"12D3KooWAobjw92XDcnQ1rRmRJDA3zAQpdPYUpZKrJxH6yccSpje", "12D3KooWARYacCc6eoCqvsS9RW9MA2vo51CV75deoiqssx3YgyYJ"} |
| 38 | |
| 39 | t.Run("default routing config has no routers defined", func(t *testing.T) { |
| 40 | assert.Nil(t, node.ReadConfig().Routing.Routers) |
| 41 | }) |
| 42 | |
| 43 | t.Run("no routers means findprovs returns no results", func(t *testing.T) { |
| 44 | res := node.IPFS("routing", "findprovs", findProvsCID).Stdout.String() |
| 45 | assert.Empty(t, res) |
| 46 | }) |
| 47 | |
| 48 | t.Run("no routers means findprovs returns no results", func(t *testing.T) { |
| 49 | res := node.IPFS("routing", "findprovs", findProvsCID).Stdout.String() |
| 50 | assert.Empty(t, res) |
| 51 | }) |
| 52 | |
| 53 | node.StopDaemon() |
| 54 | |
| 55 | t.Run("missing method params make the daemon fail", func(t *testing.T) { |
| 56 | node.UpdateConfig(func(cfg *config.Config) { |
| 57 | cfg.Routing.Type = config.NewOptionalString("custom") |
| 58 | cfg.Routing.Methods = config.Methods{ |
| 59 | "find-peers": {RouterName: "TestDelegatedRouter"}, |
| 60 | "find-providers": {RouterName: "TestDelegatedRouter"}, |
| 61 | "get-ipns": {RouterName: "TestDelegatedRouter"}, |
| 62 | "provide": {RouterName: "TestDelegatedRouter"}, |
| 63 | } |
| 64 | }) |
| 65 | res := node.RunIPFS("daemon") |
| 66 | assert.Equal(t, 1, res.ExitErr.ProcessState.ExitCode()) |
| 67 | assert.Contains( |
| 68 | t, |
| 69 | res.Stderr.String(), |
| 70 | `method name "put-ipns" is missing from Routing.Methods config param`, |
| 71 | ) |
| 72 | }) |
| 73 | |
| 74 | t.Run("having wrong methods makes daemon fail", func(t *testing.T) { |
| 75 | node.UpdateConfig(func(cfg *config.Config) { |
| 76 | cfg.Routing.Type = config.NewOptionalString("custom") |
| 77 | cfg.Routing.Methods = config.Methods{ |
| 78 | "find-peers": {RouterName: "TestDelegatedRouter"}, |
| 79 | "find-providers": {RouterName: "TestDelegatedRouter"}, |
| 80 | "get-ipns": {RouterName: "TestDelegatedRouter"}, |
| 81 | "provide": {RouterName: "TestDelegatedRouter"}, |
| 82 | "put-ipns": {RouterName: "TestDelegatedRouter"}, |
| 83 | "NOT_SUPPORTED": {RouterName: "TestDelegatedRouter"}, |
| 84 | } |
| 85 | }) |
| 86 | res := node.RunIPFS("daemon") |
| 87 | assert.Equal(t, 1, res.ExitErr.ProcessState.ExitCode()) |
| 88 | assert.Contains( |
| 89 | t, |
| 90 | res.Stderr.String(), |
| 91 | `method name "NOT_SUPPORTED" is not a supported method on Routing.Methods config param`, |
| 92 | ) |
| 93 | }) |
| 94 | |
| 95 | t.Run("adding HTTP delegated routing endpoint to Routing.Routers config works", func(t *testing.T) { |
| 96 | server := fakeServer("application/json", ToJSONStr(JSONObj{ |
| 97 | "Providers": []JSONObj{ |
| 98 | { |
| 99 | "Schema": "bitswap", // Legacy bitswap schema. |
| 100 | "Protocol": "transport-bitswap", |
| 101 | "ID": provs[1], |
| 102 | "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, |
| 103 | }, |
| 104 | { |
| 105 | "Schema": "peer", |
| 106 | "Protocols": []string{"transport-bitswap"}, |
| 107 | "ID": provs[0], |
| 108 | "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, |
| 109 | }, |
| 110 | }, |
| 111 | })) |
| 112 | t.Cleanup(server.Close) |
| 113 | |
| 114 | node.IPFS("config", "Routing.Type", "custom") |
| 115 | node.IPFS("config", "Routing.Routers.TestDelegatedRouter", "--json", ToJSONStr(JSONObj{ |
| 116 | "Type": "http", |
| 117 | "Parameters": JSONObj{ |
| 118 | "Endpoint": server.URL, |
| 119 | }, |
| 120 | })) |
| 121 | node.IPFS("config", "Routing.Methods", "--json", ToJSONStr(JSONObj{ |
| 122 | "find-peers": JSONObj{"RouterName": "TestDelegatedRouter"}, |
| 123 | "find-providers": JSONObj{"RouterName": "TestDelegatedRouter"}, |
| 124 | "get-ipns": JSONObj{"RouterName": "TestDelegatedRouter"}, |
| 125 | "provide": JSONObj{"RouterName": "TestDelegatedRouter"}, |
| 126 | "put-ipns": JSONObj{"RouterName": "TestDelegatedRouter"}, |
| 127 | })) |
| 128 | |
| 129 | res := node.IPFS("config", "Routing.Routers.TestDelegatedRouter.Parameters.Endpoint") |
| 130 | assert.Equal(t, res.Stdout.Trimmed(), server.URL) |
| 131 | |
| 132 | node.StartDaemon() |
| 133 | res = node.IPFS("routing", "findprovs", findProvsCID) |
| 134 | assert.Equal(t, provs[1]+"\n"+provs[0], res.Stdout.Trimmed()) |
| 135 | }) |
| 136 | |
| 137 | node.StopDaemon() |
| 138 | |
| 139 | t.Run("adding HTTP delegated routing endpoint to Routing.Routers config works (streaming)", func(t *testing.T) { |
| 140 | server := fakeServer("application/x-ndjson", ToJSONStr(JSONObj{ |
| 141 | "Schema": "peer", |
| 142 | "Protocols": []string{"transport-bitswap"}, |
| 143 | "ID": provs[0], |
| 144 | "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, |
| 145 | }), ToJSONStr(JSONObj{ |
| 146 | "Schema": "bitswap", // Legacy bitswap schema. |
| 147 | "Protocol": "transport-bitswap", |
| 148 | "ID": provs[1], |
| 149 | "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, |
| 150 | })) |
| 151 | t.Cleanup(server.Close) |
| 152 | |
| 153 | node.IPFS("config", "Routing.Routers.TestDelegatedRouter", "--json", ToJSONStr(JSONObj{ |
| 154 | "Type": "http", |
| 155 | "Parameters": JSONObj{ |
| 156 | "Endpoint": server.URL, |
| 157 | }, |
| 158 | })) |
| 159 | |
| 160 | res := node.IPFS("config", "Routing.Routers.TestDelegatedRouter.Parameters.Endpoint") |
| 161 | assert.Equal(t, res.Stdout.Trimmed(), server.URL) |
| 162 | |
| 163 | node.StartDaemon() |
| 164 | res = node.IPFS("routing", "findprovs", findProvsCID) |
| 165 | assert.Equal(t, provs[0]+"\n"+provs[1], res.Stdout.Trimmed()) |
| 166 | }) |
| 167 | |
| 168 | t.Run("HTTP client should emit OpenCensus metrics", func(t *testing.T) { |
| 169 | resp := node.APIClient().Get("/debug/metrics/prometheus") |
| 170 | assert.Contains(t, resp.Body, "routing_http_client_length_count") |
| 171 | }) |
| 172 | } |
| 173 | |
| 174 | // TestHTTPDelegatedRoutingProviderAddrs verifies that provider records sent to |
| 175 | // HTTP routers contain the expected addresses based on Addresses configuration. |
| 176 | // See https://github.com/ipfs/kubo/issues/11213 |
| 177 | func TestHTTPDelegatedRoutingProviderAddrs(t *testing.T) { |
| 178 | t.Parallel() |
| 179 | |
| 180 | // captureProviderAddrs returns a mock server and a function to retrieve captured addresses. |
| 181 | captureProviderAddrs := func(t *testing.T) (*httptest.Server, func() []string) { |
| 182 | t.Helper() |
| 183 | var mu sync.Mutex |
| 184 | var capturedAddrs []string |
| 185 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 186 | if (r.Method == http.MethodPut || r.Method == http.MethodPost) && |
| 187 | strings.HasPrefix(r.URL.Path, "/routing/v1/providers") { |
| 188 | body, _ := io.ReadAll(r.Body) |
| 189 | var envelope struct { |
| 190 | Providers []struct { |
| 191 | Payload json.RawMessage `json:"Payload"` |
| 192 | } `json:"Providers"` |
| 193 | } |
| 194 | if json.Unmarshal(body, &envelope) == nil { |
| 195 | for _, prov := range envelope.Providers { |
| 196 | var payload struct { |
| 197 | Addrs []string `json:"Addrs"` |
| 198 | } |
| 199 | if json.Unmarshal(prov.Payload, &payload) == nil && len(payload.Addrs) > 0 { |
| 200 | mu.Lock() |
| 201 | capturedAddrs = payload.Addrs |
| 202 | mu.Unlock() |
| 203 | } |
| 204 | } |
| 205 | } |
| 206 | w.WriteHeader(http.StatusOK) |
| 207 | return |
| 208 | } |
| 209 | if strings.HasPrefix(r.URL.Path, "/routing/v1/") { |
| 210 | w.WriteHeader(http.StatusOK) |
| 211 | return |
| 212 | } |
| 213 | w.WriteHeader(http.StatusNotFound) |
| 214 | })) |
| 215 | t.Cleanup(srv.Close) |
| 216 | return srv, func() []string { |
| 217 | mu.Lock() |
| 218 | defer mu.Unlock() |
| 219 | return capturedAddrs |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | customRoutingConf := func(endpoint string) map[string]any { |
| 224 | return map[string]any{ |
| 225 | "Type": "custom", |
| 226 | "Methods": map[string]any{ |
| 227 | "provide": map[string]any{"RouterName": "TestRouter"}, |
| 228 | "find-providers": map[string]any{"RouterName": "TestRouter"}, |
| 229 | "find-peers": map[string]any{"RouterName": "TestRouter"}, |
| 230 | "get-ipns": map[string]any{"RouterName": "TestRouter"}, |
| 231 | "put-ipns": map[string]any{"RouterName": "TestRouter"}, |
| 232 | }, |
| 233 | "Routers": map[string]any{ |
| 234 | "TestRouter": map[string]any{ |
| 235 | "Type": "http", |
| 236 | "Parameters": map[string]any{"Endpoint": endpoint}, |
| 237 | }, |
| 238 | }, |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | t.Run("provider records respect user-provided Addresses.Announce override", func(t *testing.T) { |
| 243 | t.Parallel() |
| 244 | srv, getAddrs := captureProviderAddrs(t) |
| 245 | |
| 246 | node := harness.NewT(t).NewNode().Init() |
| 247 | node.SetIPFSConfig("Addresses.Announce", []string{"/ip4/1.2.3.4/tcp/4001"}) |
| 248 | node.SetIPFSConfig("Routing", customRoutingConf(srv.URL)) |
| 249 | node.StartDaemon() |
| 250 | defer node.StopDaemon() |
| 251 | |
| 252 | cidStr := node.IPFSAddStr(time.Now().String()) |
| 253 | node.IPFS("routing", "provide", cidStr) |
| 254 | |
| 255 | addrs := getAddrs() |
| 256 | require.NotEmpty(t, addrs, "provider record should contain addresses") |
| 257 | assert.Equal(t, []string{"/ip4/1.2.3.4/tcp/4001"}, addrs) |
| 258 | }) |
| 259 | |
| 260 | t.Run("provider records respect user-provided Addresses.AppendAnnounce", func(t *testing.T) { |
| 261 | t.Parallel() |
| 262 | srv, getAddrs := captureProviderAddrs(t) |
| 263 | |
| 264 | node := harness.NewT(t).NewNode().Init() |
| 265 | node.SetIPFSConfig("Addresses.AppendAnnounce", []string{"/ip4/5.6.7.8/tcp/4001"}) |
| 266 | node.SetIPFSConfig("Routing", customRoutingConf(srv.URL)) |
| 267 | node.StartDaemon() |
| 268 | defer node.StopDaemon() |
| 269 | |
| 270 | cidStr := node.IPFSAddStr(time.Now().String()) |
| 271 | node.IPFS("routing", "provide", cidStr) |
| 272 | |
| 273 | addrs := getAddrs() |
| 274 | require.NotEmpty(t, addrs, "provider record should contain addresses") |
| 275 | assert.Contains(t, addrs, "/ip4/5.6.7.8/tcp/4001", "AppendAnnounce address should be present") |
| 276 | }) |
| 277 | |
| 278 | t.Run("provider records resolve 0.0.0.0 Swarm bind to interface addresses", func(t *testing.T) { |
| 279 | t.Parallel() |
| 280 | srv, getAddrs := captureProviderAddrs(t) |
| 281 | |
| 282 | // Default Addresses.Swarm binds to /ip4/0.0.0.0/... If httpRouterAddrFunc |
| 283 | // forwards those verbatim, HTTP routers receive useless unroutable entries. |
| 284 | // See https://github.com/ipfs/kubo/issues/11213. |
| 285 | node := harness.NewT(t).NewNode().Init() |
| 286 | node.SetIPFSConfig("Routing", customRoutingConf(srv.URL)) |
| 287 | node.StartDaemon() |
| 288 | defer node.StopDaemon() |
| 289 | |
| 290 | cidStr := node.IPFSAddStr(time.Now().String()) |
| 291 | node.IPFS("routing", "provide", cidStr) |
| 292 | |
| 293 | addrs := getAddrs() |
| 294 | require.NotEmpty(t, addrs, "provider record should contain addresses") |
| 295 | for _, a := range addrs { |
| 296 | assert.NotContains(t, a, "/ip4/0.0.0.0/", "unresolved 0.0.0.0 in provider record: %s", a) |
| 297 | assert.NotContains(t, a, "/ip6/::/", "unresolved :: in provider record: %s", a) |
| 298 | } |
| 299 | }) |
| 300 | } |