kubo/test/cli/http_retrieval_client_test.go
Hector Sanjuan b5d73695ba
feat: opt-in http retrieval client (#10772)
* Feat: http retrieval as experimental feature

This introduces the http-retrieval capability as an experimental feature.

It can be enabled in the configuration `Experimental.HTTPRetrieval.Enabled = true`.

Documentation and changelog to be added later.

* refactor: HTTPRetrieval.Enabled as Flag

* docs(config): HTTPRetrieval section

* refactor: reusable MockHTTPContentRouter

* feat: HTTPRetrieval.TLSInsecureSkipVerify

allows self-signed certificates in tests

* feat(config): HTTPRetrieval.MaxBlockSize

* test: end-to-end HTTPRetrieval.Enabled

this spawns two http services on localhost:
1. HTTP router that returns HTTP provider when /routing/v1/providers/cid  i queried
2. HTTP provider that returns a block when /ipfs/cid is queried
3. Configures Kubo to use (1) instead of cid.contact

this seems to work (running test with DEBUG=true shows (1) was queried
for the test CID and returned multiaddr of (2), but Kubo never requested
test CID block from (2) – needs investigation

* fix: enable /routing/v1/peers for non-cid.contact

we artificially limited every delegated routing endpoint because of
cid.contact being limited to one endpoint

* feat: Routing.DelegatedRouters

make it easy to override the hardcoded implicit HTTP routeur URL
without having to set the entire custom Router.Routers and
Router.Methods

(http_retrieval_client_test.go still needs to be fixed in future commit)

* test: flag remaining work

* docs: review feedback

* refactor: providerQueryMgr with bitswapNetworks

this fixes two regressions:

(1) introduced in https://github.com/ipfs/kubo/issues/10717
    where we only used bitswapLib2p query manager
    (this is why E2E did not act on http provider)

(2) introduced in https://github.com/ipfs/kubo/pull/10765
    where it was not possible to set binary peerID in IgnoreProviders
    (we changed to []string)

* refactor: Bitswap.Libp2pEnabled

replaces Bitswap.Enabled with Bitswap.Libp2pEnabled
adds tests that confirm it is possible to disable libp2p bitswap fully
and only keep http in client mode

also, removes the need for passing empty blockstore in client-only mode

* docs: changelog

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
2025-05-06 19:06:40 +02:00

146 lines
5.4 KiB
Go

package cli
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/ipfs/boxo/routing/http/server"
"github.com/ipfs/boxo/routing/http/types"
"github.com/ipfs/go-cid"
"github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/test/cli/harness"
"github.com/ipfs/kubo/test/cli/testutils"
"github.com/ipfs/kubo/test/cli/testutils/httprouting"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/assert"
)
func TestHTTPRetrievalClient(t *testing.T) {
t.Parallel()
// many moving pieces here, show more when debug is needed
debug := os.Getenv("DEBUG") == "true"
// usee local /routing/v1/providers/{cid} and
// /ipfs/{cid} HTTP servers to confirm HTTP-only retrieval works end-to-end.
t.Run("works end-to-end with an HTTP-only provider", func(t *testing.T) {
// setup mocked HTTP Router to handle /routing/v1/providers/cid
mockRouter := &httprouting.MockHTTPContentRouter{Debug: debug}
delegatedRoutingServer := httptest.NewServer(server.Handler(mockRouter))
t.Cleanup(func() { delegatedRoutingServer.Close() })
// init Kubo repo
node := harness.NewT(t).NewNode().Init()
node.UpdateConfig(func(cfg *config.Config) {
// explicitly enable http client
cfg.HTTPRetrieval.Enabled = config.True
// allow NewMockHTTPProviderServer to use self-signed TLS cert
cfg.HTTPRetrieval.TLSInsecureSkipVerify = config.True
// setup client-only routing which asks both HTTP + DHT
// cfg.Routing.Type = config.NewOptionalString("autoclient")
// setup Kubo node to use mocked HTTP Router
cfg.Routing.DelegatedRouters = []string{delegatedRoutingServer.URL}
})
// compute a random CID
randStr := string(testutils.RandomBytes(100))
res := node.PipeStrToIPFS(randStr, "add", "-qn", "--cid-version", "1") // -n means dont add to local repo, just produce CID
wantCIDStr := res.Stdout.Trimmed()
testCid := cid.MustParse(wantCIDStr)
// setup mock HTTP provider
httpProviderServer := NewMockHTTPProviderServer(testCid, randStr, debug)
t.Cleanup(func() { httpProviderServer.Close() })
httpHost, httpPort, err := splitHostPort(httpProviderServer.URL)
assert.NoError(t, err)
// setup /routing/v1/providers/cid result that points at our mocked HTTP provider
mockHTTPProviderPeerID := "12D3KooWCjfPiojcCUmv78Wd1NJzi4Mraj1moxigp7AfQVQvGLwH" // static, it does not matter, we only care about multiaddr
mockHTTPMultiaddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%s/tls/http", httpHost, httpPort))
mpid, _ := peer.Decode(mockHTTPProviderPeerID)
mockRouter.AddProvider(testCid, &types.PeerRecord{
Schema: types.SchemaPeer,
ID: &mpid,
Addrs: []types.Multiaddr{{Multiaddr: mockHTTPMultiaddr}},
// no explicit Protocols, ensure multiaddr alone is enough
})
// Start Kubo
node.StartDaemon()
if debug {
fmt.Printf("delegatedRoutingServer.URL: %s\n", delegatedRoutingServer.URL)
fmt.Printf("httpProviderServer.URL: %s\n", httpProviderServer.URL)
fmt.Printf("httpProviderServer.Multiaddr: %s\n", mockHTTPMultiaddr)
fmt.Printf("testCid: %s\n", testCid)
}
// Now, make Kubo to read testCid. it was not added to local blockstore, so it has only one provider -- a HTTP server.
// First, confirm delegatedRoutingServer returned HTTP provider
findprovsRes := node.IPFS("routing", "findprovs", testCid.String())
assert.Equal(t, mockHTTPProviderPeerID, findprovsRes.Stdout.Trimmed())
// Ok, now attempt retrieval.
// If there was no timeout and returned bytes match expected body, HTTP routing and retrieval worked end-to-end.
catRes := node.IPFS("cat", testCid.String())
assert.Equal(t, randStr, catRes.Stdout.Trimmed())
})
}
// NewMockHTTPProviderServer pretends to be http provider that supports
// block response https://specs.ipfs.tech/http-gateways/trustless-gateway/#block-responses-application-vnd-ipld-raw
func NewMockHTTPProviderServer(c cid.Cid, body string, debug bool) *httptest.Server {
expectedPathPrefix := "/ipfs/" + c.String()
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if debug {
fmt.Printf("NewMockHTTPProviderServer GET %s\n", req.URL.Path)
}
if strings.HasPrefix(req.URL.Path, expectedPathPrefix) {
w.Header().Set("Content-Type", "application/vnd.ipld.raw")
w.WriteHeader(http.StatusOK)
if req.Method == "GET" {
_, err := w.Write([]byte(body))
if err != nil {
fmt.Fprintf(os.Stderr, "NewMockHTTPProviderServer GET %s error: %v\n", req.URL.Path, err)
}
}
} else if strings.HasPrefix(req.URL.Path, "/ipfs/bafkqaaa") {
// This is probe from https://specs.ipfs.tech/http-gateways/trustless-gateway/#dedicated-probe-paths
w.Header().Set("Content-Type", "application/vnd.ipld.raw")
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Not Found", http.StatusNotFound)
}
})
// Make it HTTP/2 with self-signed TLS cert
srv := httptest.NewUnstartedServer(handler)
srv.EnableHTTP2 = true
srv.StartTLS()
return srv
}
func splitHostPort(httpUrl string) (ipAddr string, port string, err error) {
u, err := url.Parse(httpUrl)
if err != nil {
return "", "", err
}
if u.Scheme == "" || u.Host == "" {
return "", "", fmt.Errorf("invalid URL format: missing scheme or host")
}
ipAddr, port, err = net.SplitHostPort(u.Host)
if err != nil {
return "", "", fmt.Errorf("failed to split host and port from %q: %w", u.Host, err)
}
return ipAddr, port, nil
}