master
go 146 lines 5.36 KB
Raw
1 package cli
2
3 import (
4 "fmt"
5 "net"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "os"
10 "strings"
11 "testing"
12
13 "github.com/ipfs/boxo/routing/http/server"
14 "github.com/ipfs/boxo/routing/http/types"
15 "github.com/ipfs/go-cid"
16 "github.com/ipfs/go-test/random"
17 "github.com/ipfs/kubo/config"
18 "github.com/ipfs/kubo/test/cli/harness"
19 "github.com/ipfs/kubo/test/cli/testutils/httprouting"
20 "github.com/libp2p/go-libp2p/core/peer"
21 "github.com/multiformats/go-multiaddr"
22 "github.com/stretchr/testify/assert"
23 )
24
25 func TestHTTPRetrievalClient(t *testing.T) {
26 t.Parallel()
27
28 // many moving pieces here, show more when debug is needed
29 debug := os.Getenv("DEBUG") == "true"
30
31 // usee local /routing/v1/providers/{cid} and
32 // /ipfs/{cid} HTTP servers to confirm HTTP-only retrieval works end-to-end.
33 t.Run("works end-to-end with an HTTP-only provider", func(t *testing.T) {
34 // setup mocked HTTP Router to handle /routing/v1/providers/cid
35 mockRouter := &httprouting.MockHTTPContentRouter{Debug: debug}
36 delegatedRoutingServer := httptest.NewServer(server.Handler(mockRouter))
37 t.Cleanup(func() { delegatedRoutingServer.Close() })
38
39 // init Kubo repo
40 node := harness.NewT(t).NewNode().Init()
41
42 node.UpdateConfig(func(cfg *config.Config) {
43 // explicitly enable http client
44 cfg.HTTPRetrieval.Enabled = config.True
45 // allow NewMockHTTPProviderServer to use self-signed TLS cert
46 cfg.HTTPRetrieval.TLSInsecureSkipVerify = config.True
47 // setup client-only routing which asks both HTTP + DHT
48 // cfg.Routing.Type = config.NewOptionalString("autoclient")
49 // setup Kubo node to use mocked HTTP Router
50 cfg.Routing.DelegatedRouters = []string{delegatedRoutingServer.URL}
51 })
52
53 // compute a random CID
54 randStr := string(random.Bytes(100))
55 res := node.PipeStrToIPFS(randStr, "add", "-qn", "--cid-version", "1") // -n means dont add to local repo, just produce CID
56 wantCIDStr := res.Stdout.Trimmed()
57 testCid := cid.MustParse(wantCIDStr)
58
59 // setup mock HTTP provider
60 httpProviderServer := NewMockHTTPProviderServer(testCid, randStr, debug)
61 t.Cleanup(func() { httpProviderServer.Close() })
62 httpHost, httpPort, err := splitHostPort(httpProviderServer.URL)
63 assert.NoError(t, err)
64
65 // setup /routing/v1/providers/cid result that points at our mocked HTTP provider
66 mockHTTPProviderPeerID := "12D3KooWCjfPiojcCUmv78Wd1NJzi4Mraj1moxigp7AfQVQvGLwH" // static, it does not matter, we only care about multiaddr
67 mockHTTPMultiaddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%s/tls/http", httpHost, httpPort))
68 mpid, _ := peer.Decode(mockHTTPProviderPeerID)
69 mockRouter.AddProvider(testCid, &types.PeerRecord{
70 Schema: types.SchemaPeer,
71 ID: &mpid,
72 Addrs: []types.Multiaddr{{Multiaddr: mockHTTPMultiaddr}},
73 // no explicit Protocols, ensure multiaddr alone is enough
74 })
75
76 // Start Kubo
77 node.StartDaemon()
78 defer node.StopDaemon()
79
80 if debug {
81 fmt.Printf("delegatedRoutingServer.URL: %s\n", delegatedRoutingServer.URL)
82 fmt.Printf("httpProviderServer.URL: %s\n", httpProviderServer.URL)
83 fmt.Printf("httpProviderServer.Multiaddr: %s\n", mockHTTPMultiaddr)
84 fmt.Printf("testCid: %s\n", testCid)
85 }
86
87 // Now, make Kubo to read testCid. it was not added to local blockstore, so it has only one provider -- a HTTP server.
88
89 // First, confirm delegatedRoutingServer returned HTTP provider
90 findprovsRes := node.IPFS("routing", "findprovs", testCid.String())
91 assert.Equal(t, mockHTTPProviderPeerID, findprovsRes.Stdout.Trimmed())
92
93 // Ok, now attempt retrieval.
94 // If there was no timeout and returned bytes match expected body, HTTP routing and retrieval worked end-to-end.
95 catRes := node.IPFS("cat", testCid.String())
96 assert.Equal(t, randStr, catRes.Stdout.Trimmed())
97 })
98 }
99
100 // NewMockHTTPProviderServer pretends to be http provider that supports
101 // block response https://specs.ipfs.tech/http-gateways/trustless-gateway/#block-responses-application-vnd-ipld-raw
102 func NewMockHTTPProviderServer(c cid.Cid, body string, debug bool) *httptest.Server {
103 expectedPathPrefix := "/ipfs/" + c.String()
104 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
105 if debug {
106 fmt.Printf("NewMockHTTPProviderServer GET %s\n", req.URL.Path)
107 }
108 if strings.HasPrefix(req.URL.Path, expectedPathPrefix) {
109 w.Header().Set("Content-Type", "application/vnd.ipld.raw")
110 w.WriteHeader(http.StatusOK)
111 if req.Method == "GET" {
112 _, err := w.Write([]byte(body))
113 if err != nil {
114 fmt.Fprintf(os.Stderr, "NewMockHTTPProviderServer GET %s error: %v\n", req.URL.Path, err)
115 }
116 }
117 } else if strings.HasPrefix(req.URL.Path, "/ipfs/bafkqaaa") {
118 // This is probe from https://specs.ipfs.tech/http-gateways/trustless-gateway/#dedicated-probe-paths
119 w.Header().Set("Content-Type", "application/vnd.ipld.raw")
120 w.WriteHeader(http.StatusOK)
121 } else {
122 http.Error(w, "Not Found", http.StatusNotFound)
123 }
124 })
125
126 // Make it HTTP/2 with self-signed TLS cert
127 srv := httptest.NewUnstartedServer(handler)
128 srv.EnableHTTP2 = true
129 srv.StartTLS()
130 return srv
131 }
132
133 func splitHostPort(httpUrl string) (ipAddr string, port string, err error) {
134 u, err := url.Parse(httpUrl)
135 if err != nil {
136 return "", "", err
137 }
138 if u.Scheme == "" || u.Host == "" {
139 return "", "", fmt.Errorf("invalid URL format: missing scheme or host")
140 }
141 ipAddr, port, err = net.SplitHostPort(u.Host)
142 if err != nil {
143 return "", "", fmt.Errorf("failed to split host and port from %q: %w", u.Host, err)
144 }
145 return ipAddr, port, nil
146 }