Skip to content

Commit a26b503

Browse files
authoredApr 13, 2024··
gateway: remote car gateway backend (#587)
1 parent 63c3ec6 commit a26b503

33 files changed

+3956
-267
lines changed
 
+158-16
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
name: Gateway Conformance
2+
# This workflow runs https://github.com/ipfs/gateway-conformance
3+
# against different backend implementations of boxo/gateway
24

35
on:
46
push:
57
branches:
68
- main
79
pull_request:
10+
workflow_dispatch:
811

912
concurrency:
1013
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }}
1114
cancel-in-progress: true
1215

1316
jobs:
14-
gateway-conformance:
17+
# This test uses a static CAR file as a local blockstore,
18+
# allowing us to test conformance against BlocksBackend (gateway/backend_blocks.go)
19+
# which is used by implementations like Kubo
20+
local-block-backend:
1521
runs-on: ubuntu-latest
1622
steps:
1723
# 1. Download the gateway-conformance fixtures
@@ -21,49 +27,185 @@ jobs:
2127
output: fixtures
2228
merged: true
2329

24-
# 2. Build the car-gateway
30+
# 2. Build the gateway binary
31+
- name: Checkout boxo
32+
uses: actions/checkout@v4
33+
with:
34+
path: boxo
35+
- name: Setup Go
36+
uses: actions/setup-go@v5
37+
with:
38+
go-version-file: 'boxo/examples/go.mod'
39+
cache-dependency-path: "boxo/**/*.sum"
40+
- name: Build test-gateway
41+
run: go build -o test-gateway
42+
working-directory: boxo/examples/gateway/car-file
43+
44+
# 3. Start the gateway binary
45+
- name: Start test-gateway
46+
run: boxo/examples/gateway/car-file/test-gateway -c fixtures/fixtures.car -p 8040 &
47+
48+
# 4. Run the gateway-conformance tests
49+
- name: Run gateway-conformance tests
50+
uses: ipfs/gateway-conformance/.github/actions/test@v0.5
51+
with:
52+
gateway-url: http://127.0.0.1:8040
53+
json: output.json
54+
xml: output.xml
55+
html: output.html
56+
markdown: output.md
57+
subdomain-url: http://example.net
58+
specs: -trustless-ipns-gateway,-path-ipns-gateway,-subdomain-ipns-gateway,-dnslink-gateway
59+
60+
# 5. Upload the results
61+
- name: Upload MD summary
62+
if: failure() || success()
63+
run: cat output.md >> $GITHUB_STEP_SUMMARY
64+
- name: Upload HTML report
65+
if: failure() || success()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: gateway-conformance_local-block-backend.html
69+
path: output.html
70+
- name: Upload JSON report
71+
if: failure() || success()
72+
uses: actions/upload-artifact@v4
73+
with:
74+
name: gateway-conformance_local-block-backend.json
75+
path: output.json
76+
77+
# This test uses remote block gateway (?format=raw) as a remote blockstore,
78+
# allowing us to test conformance against RemoteBlocksBackend
79+
# (gateway/backend_blocks.go) which is used by implementations like
80+
# rainbow configured to use with remote block backend
81+
# Ref. https://specs.ipfs.tech/http-gateways/trustless-gateway/#block-responses-application-vnd-ipld-raw
82+
remote-block-backend:
83+
runs-on: ubuntu-latest
84+
steps:
85+
# 1. Download the gateway-conformance fixtures
86+
- name: Download gateway-conformance fixtures
87+
uses: ipfs/gateway-conformance/.github/actions/extract-fixtures@v0.5
88+
with:
89+
output: fixtures
90+
merged: true
91+
92+
# 2. Build the gateway binaries
93+
- name: Checkout boxo
94+
uses: actions/checkout@v4
95+
with:
96+
path: boxo
2597
- name: Setup Go
26-
uses: actions/setup-go@v4
98+
uses: actions/setup-go@v5
99+
with:
100+
go-version-file: 'boxo/examples/go.mod'
101+
cache-dependency-path: "boxo/**/*.sum"
102+
- name: Build remote-block-backend # it will act as a trustless CAR gateway
103+
run: go build -o remote-block-backend
104+
working-directory: boxo/examples/gateway/car-file
105+
- name: Build test-gateway # this one will be used for tests, it will use previous one as its remote block backend
106+
run: go build -o test-gateway
107+
working-directory: boxo/examples/gateway/proxy-blocks
108+
109+
# 3. Start the gateway binaries
110+
- name: Start remote HTTP backend that serves application/vnd.ipld.raw
111+
run: boxo/examples/gateway/car-file/remote-block-backend -c fixtures/fixtures.car -p 8030 & # this endpoint will respond to application/vnd.ipld.car requests
112+
- name: Start gateway that uses the remote block backend
113+
run: boxo/examples/gateway/proxy-blocks/test-gateway -g http://127.0.0.1:8030 -p 8040 &
114+
115+
# 4. Run the gateway-conformance tests
116+
- name: Run gateway-conformance tests
117+
uses: ipfs/gateway-conformance/.github/actions/test@v0.5
118+
with:
119+
gateway-url: http://127.0.0.1:8040 # we test gateway that is backed by a remote block gateway
120+
json: output.json
121+
xml: output.xml
122+
html: output.html
123+
markdown: output.md
124+
subdomain-url: http://example.net
125+
specs: -trustless-ipns-gateway,-path-ipns-gateway,-subdomain-ipns-gateway,-dnslink-gateway
126+
args: -skip 'TestGatewayCache/.*_for_%2Fipfs%2F_with_only-if-cached_succeeds_when_in_local_datastore'
127+
128+
# 5. Upload the results
129+
- name: Upload MD summary
130+
if: failure() || success()
131+
run: cat output.md >> $GITHUB_STEP_SUMMARY
132+
- name: Upload HTML report
133+
if: failure() || success()
134+
uses: actions/upload-artifact@v4
27135
with:
28-
go-version: 1.21.x
136+
name: gateway-conformance_remote-block-backend.html
137+
path: output.html
138+
- name: Upload JSON report
139+
if: failure() || success()
140+
uses: actions/upload-artifact@v4
141+
with:
142+
name: gateway-conformance_remote-block-backend.json
143+
path: output.json
144+
145+
# This test uses remote CAR gateway (?format=car, IPIP-402)
146+
# allowing us to test conformance against remote CarFetcher backend.
147+
# (gateway/backend_car_fetcher.go) which is used by implementations like
148+
# rainbow configured to use with remote car backend
149+
# Ref. https://specs.ipfs.tech/http-gateways/trustless-gateway/#car-responses-application-vnd-ipld-car
150+
remote-car-backend:
151+
runs-on: ubuntu-latest
152+
steps:
153+
# 1. Download the gateway-conformance fixtures
154+
- name: Download gateway-conformance fixtures
155+
uses: ipfs/gateway-conformance/.github/actions/extract-fixtures@v0.5
156+
with:
157+
output: fixtures
158+
merged: true
159+
160+
# 2. Build the gateway binaries
29161
- name: Checkout boxo
30162
uses: actions/checkout@v4
31163
with:
32164
path: boxo
33-
- name: Build car-gateway
34-
run: go build -o car-gateway
35-
working-directory: boxo/examples/gateway/car
165+
- name: Setup Go
166+
uses: actions/setup-go@v5
167+
with:
168+
go-version-file: 'boxo/examples/go.mod'
169+
cache-dependency-path: "boxo/**/*.sum"
170+
- name: Build remote-car-backend # it will act as a trustless CAR gateway
171+
run: go build -o remote-car-backend
172+
working-directory: boxo/examples/gateway/car-file
173+
- name: Build test-gateway # this one will be used for tests, it will use previous one as its remote CAR backend
174+
run: go build -o test-gateway
175+
working-directory: boxo/examples/gateway/proxy-car
36176

37-
# 3. Start the car-gateway
38-
- name: Start car-gateway
39-
run: boxo/examples/gateway/car/car-gateway -c fixtures/fixtures.car -p 8040 &
177+
# 3. Start the gateway binaries
178+
- name: Start remote HTTP backend that serves application/vnd.ipld.car (IPIP-402)
179+
run: boxo/examples/gateway/car-file/remote-car-backend -c fixtures/fixtures.car -p 8030 & # this endpoint will respond to application/vnd.ipld.raw requests
180+
- name: Start gateway that uses the remote CAR backend
181+
run: boxo/examples/gateway/proxy-car/test-gateway -g http://127.0.0.1:8030 -p 8040 &
40182

41183
# 4. Run the gateway-conformance tests
42184
- name: Run gateway-conformance tests
43185
uses: ipfs/gateway-conformance/.github/actions/test@v0.5
44186
with:
45-
gateway-url: http://127.0.0.1:8040
187+
gateway-url: http://127.0.0.1:8040 # we test gateway that is backed by a remote car gateway
46188
json: output.json
47189
xml: output.xml
48190
html: output.html
49191
markdown: output.md
50192
subdomain-url: http://example.net
51193
specs: -trustless-ipns-gateway,-path-ipns-gateway,-subdomain-ipns-gateway,-dnslink-gateway
52-
args: -skip 'TestGatewayCar/GET_response_for_application/vnd.ipld.car/Header_Content-Length'
194+
args: -skip 'TestGatewayCache/.*_for_%2Fipfs%2F_with_only-if-cached_succeeds_when_in_local_datastore'
53195

54196
# 5. Upload the results
55197
- name: Upload MD summary
56198
if: failure() || success()
57199
run: cat output.md >> $GITHUB_STEP_SUMMARY
58200
- name: Upload HTML report
59201
if: failure() || success()
60-
uses: actions/upload-artifact@v3
202+
uses: actions/upload-artifact@v4
61203
with:
62-
name: gateway-conformance.html
204+
name: gateway-conformance_remote-car-backend.html
63205
path: output.html
64206
- name: Upload JSON report
65207
if: failure() || success()
66-
uses: actions/upload-artifact@v3
208+
uses: actions/upload-artifact@v4
67209
with:
68-
name: gateway-conformance.json
210+
name: gateway-conformance_remote-car-backend.json
69211
path: output.json

‎CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ The following emojis are used to highlight certain changes:
1616

1717
### Added
1818

19-
* `gateway` now includes `NewRemoteBlocksBackend` which allows you to create a gateway backend that uses one or multiple other gateways as backend. These gateways must support RAW block requests (`application/vnd.ipld.raw`), as well as IPNS Record requests (`application/vnd.ipfs.ipns-record`). With this, we also introduced a `NewCacheBlockStore`, `NewRemoteBlockstore` and `NewRemoteValueStore`.
19+
*`gateway` has new backend possibilities:
20+
* `NewRemoteBlocksBackend` allows you to create a gateway backend that uses one or multiple other gateways as backend. These gateways must support RAW block requests (`application/vnd.ipld.raw`), as well as IPNS Record requests (`application/vnd.ipfs.ipns-record`). With this, we also introduced `NewCacheBlockStore`, `NewRemoteBlockstore` and `NewRemoteValueStore`.
21+
* `NewRemoteCarBackend` allows you to create a gateway backend that uses one or multiple Trustless Gateways as backend. These gateways must support CAR requests (`application/vnd.ipld.car`), as well as the extensions describe in [IPIP-402](https://specs.ipfs.tech/ipips/ipip-0402/). With this, we also introduced `NewCarBackend`, `NewRemoteCarFetcher` and `NewRetryCarFetcher`.
2022

2123
### Changed
2224

‎examples/README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Once you have your example finished, do not forget to run `go mod tidy` and addi
2727
## Examples and Tutorials
2828

2929
- [Fetching a UnixFS file by CID](./unixfs-file-cid)
30-
- [Gateway backed by a CAR file](./gateway/car)
31-
- [Gateway backed by a remote blockstore and IPNS resolver](./gateway/proxy)
30+
- [Gateway backed by a local blockstore in form of a CAR file](./gateway/car-file)
31+
- [Gateway backed by a remote (HTTP) blockstore and IPNS resolver](./gateway/proxy-blocks)
32+
- [Gateway backed by a remote (HTTP) CAR Gateway](./gateway/proxy-car)
3233
- [Delegated Routing V1 Command Line Client](./routing/delegated-routing-client/)

‎examples/gateway/car/README.md ‎examples/gateway/car-file/README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
# HTTP Gateway backed by a CAR File
1+
# HTTP Gateway backed by a CAR File as BlocksBackend
22

33
This is an example that shows how to build a Gateway backed by the contents of
44
a CAR file. A [CAR file](https://ipld.io/specs/transport/car/) is a Content
55
Addressable aRchive that contains blocks.
66

7+
The `main.go` sets up a `blockService` backed by a static CAR file,
8+
and then uses it to initialize `gateway.NewBlocksBackend(blockService)`.
9+
710
## Build
811

912
```bash
10-
> go build -o car-gateway
13+
> go build -o gateway
1114
```
1215

1316
## Usage
@@ -23,7 +26,7 @@ Then, you can start the gateway with:
2326

2427

2528
```
26-
./car-gateway -c data.car -p 8040
29+
./gateway -c data.car -p 8040
2730
```
2831

2932
### Subdomain gateway
File renamed without changes.
File renamed without changes.
File renamed without changes.

‎examples/gateway/proxy/README.md ‎examples/gateway/proxy-blocks/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ gateway using `?format=ipns-record`. In addition, DNSLink lookups are done local
1818
## Build
1919

2020
```bash
21-
> go build -o verifying-proxy
21+
> go build -o gateway
2222
```
2323

2424
## Usage
@@ -28,7 +28,7 @@ types. Once you have it, run the proxy gateway with its address as the host para
2828

2929

3030
```
31-
./verifying-proxy -g https://ipfs.io -p 8040
31+
./gateway -g https://trustless-gateway.link -p 8040
3232
```
3333

3434
### Subdomain gateway

‎examples/gateway/proxy/main.go ‎examples/gateway/proxy-blocks/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828
defer (func() { _ = tp.Shutdown(ctx) })()
2929

3030
// Creates the gateway with the remote block store backend.
31-
backend, err := gateway.NewRemoteBlocksBackend([]string{*gatewayUrlPtr})
31+
backend, err := gateway.NewRemoteBlocksBackend([]string{*gatewayUrlPtr}, nil)
3232
if err != nil {
3333
log.Fatal(err)
3434
}

‎examples/gateway/proxy/main_test.go ‎examples/gateway/proxy-blocks/main_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
)
2222

2323
func newProxyGateway(t *testing.T, rs *httptest.Server) *httptest.Server {
24-
backend, err := gateway.NewRemoteBlocksBackend([]string{rs.URL})
24+
backend, err := gateway.NewRemoteBlocksBackend([]string{rs.URL}, nil)
2525
require.NoError(t, err)
2626
handler := common.NewHandler(backend)
2727
ts := httptest.NewServer(handler)

‎examples/gateway/proxy-car/README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Gateway as Proxy for Trustless CAR Remote Backend
2+
3+
This is an example of building a "verifying proxy" Gateway that has no
4+
local on-disk blockstore, but instead, uses `application/vnd.ipld.car` and
5+
`application/vnd.ipfs.ipns-record` responses from a remote HTTP server that
6+
implements CAR support from [Trustless Gateway
7+
Specification](https://specs.ipfs.tech/http-gateways/trustless-gateway/).
8+
9+
**NOTE:** the remote CAR backend MUST implement [IPIP-0402: Partial CAR Support on Trustless Gateways](https://specs.ipfs.tech/ipips/ipip-0402/)
10+
11+
## Build
12+
13+
```bash
14+
> go build -o gateway
15+
```
16+
17+
## Usage
18+
19+
First, you need a compliant gateway that supports both [CAR requests](https://www.iana.org/assignments/media-types/application/vnd.ipld.car) and IPNS Record response
20+
types. Once you have it, run the proxy gateway with its address as the host parameter:
21+
22+
```
23+
./gateway -g https://trustless-gateway.link -p 8040
24+
```
25+
26+
### Subdomain gateway
27+
28+
Now you can access the gateway in [`localhost:8040`](http://localhost:8040/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze). It will
29+
behave like a regular [subdomain gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#subdomain-gateway),
30+
except for the fact that it runs no libp2p, and has no local blockstore.
31+
All data is provided by a remote trustless gateway, fetched as CAR files and IPNS Records, and verified locally.
32+
33+
### Path gateway
34+
35+
If you don't need Origin isolation and only care about hosting flat files,
36+
a plain [path gateway](https://docs.ipfs.tech/how-to/address-ipfs-on-web/#path-gateway) at
37+
[`127.0.0.1:8040`](http://127.0.0.1:8040/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi)
38+
may suffice.
39+
40+
### DNSLink gateway
41+
42+
Gateway supports hosting of [DNSLink](https://dnslink.dev/) websites. All you need is to pass `Host` header with FQDN that has DNSLink set up:
43+
44+
```console
45+
$ curl -sH 'Host: en.wikipedia-on-ipfs.org' 'http://127.0.0.1:8080/wiki/' | head -3
46+
<!DOCTYPE html><html class="client-js"><head>
47+
<meta charset="UTF-8">
48+
<title>Wikipedia, the free encyclopedia</title>
49+
```
50+
51+
Put it behind a reverse proxy terminating TLS (like Nginx) and voila!

‎examples/gateway/proxy-car/main.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"log"
7+
"net/http"
8+
"strconv"
9+
10+
"github.com/ipfs/boxo/examples/gateway/common"
11+
"github.com/ipfs/boxo/gateway"
12+
)
13+
14+
func main() {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
defer cancel()
17+
18+
gatewayUrlPtr := flag.String("g", "", "gateway to proxy to")
19+
port := flag.Int("p", 8040, "port to run this gateway from")
20+
flag.Parse()
21+
22+
// Setups up tracing. This is optional and only required if the implementer
23+
// wants to be able to enable tracing.
24+
tp, err := common.SetupTracing(ctx, "CAR Gateway Example")
25+
if err != nil {
26+
log.Fatal(err)
27+
}
28+
defer (func() { _ = tp.Shutdown(ctx) })()
29+
30+
// Creates the gateway with the remote car (IPIP-402) backend.
31+
backend, err := gateway.NewRemoteCarBackend([]string{*gatewayUrlPtr}, nil)
32+
if err != nil {
33+
log.Fatal(err)
34+
}
35+
36+
handler := common.NewHandler(backend)
37+
38+
log.Printf("Listening on http://localhost:%d", *port)
39+
log.Printf("Try loading an image: http://localhost:%d/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", *port)
40+
log.Printf("Try browsing Wikipedia snapshot: http://localhost:%d/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", *port)
41+
log.Printf("Metrics available at http://127.0.0.1:%d/debug/metrics/prometheus", *port)
42+
if err := http.ListenAndServe(":"+strconv.Itoa(*port), handler); err != nil {
43+
log.Fatal(err)
44+
}
45+
}

‎examples/go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ require (
6060
github.com/huin/goupnp v1.3.0 // indirect
6161
github.com/ipfs/bbloom v0.0.4 // indirect
6262
github.com/ipfs/go-bitfield v1.1.0 // indirect
63+
github.com/ipfs/go-blockservice v0.5.0 // indirect
64+
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
6365
github.com/ipfs/go-ipfs-delay v0.0.1 // indirect
66+
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
67+
github.com/ipfs/go-ipfs-exchange-interface v0.2.0 // indirect
6468
github.com/ipfs/go-ipfs-pq v0.0.3 // indirect
6569
github.com/ipfs/go-ipfs-redirects-file v0.1.1 // indirect
6670
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
@@ -69,9 +73,12 @@ require (
6973
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
7074
github.com/ipfs/go-log v1.0.5 // indirect
7175
github.com/ipfs/go-log/v2 v2.5.1 // indirect
76+
github.com/ipfs/go-merkledag v0.11.0 // indirect
7277
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
7378
github.com/ipfs/go-peertaskqueue v0.8.1 // indirect
7479
github.com/ipfs/go-unixfsnode v1.9.0 // indirect
80+
github.com/ipfs/go-verifcid v0.0.2 // indirect
81+
github.com/ipld/go-car v0.6.2 // indirect
7582
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
7683
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
7784
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect

‎examples/go.sum

+13
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
136136
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
137137
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
138138
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
139+
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
139140
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140141
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
141142
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -167,13 +168,17 @@ github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
167168
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
168169
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
169170
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
171+
github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
172+
github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk=
170173
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
171174
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
172175
github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY=
173176
github.com/ipfs/go-blockservice v0.5.0/go.mod h1:W6brZ5k20AehbmERplmERn8o2Ni3ZZubvAxaIUeaT6w=
177+
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
174178
github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
175179
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
176180
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
181+
github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
177182
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
178183
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
179184
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
@@ -184,6 +189,7 @@ github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IW
184189
github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
185190
github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8=
186191
github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
192+
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
187193
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
188194
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
189195
github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q=
@@ -196,6 +202,8 @@ github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE
196202
github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4=
197203
github.com/ipfs/go-ipfs-redirects-file v0.1.1 h1:Io++k0Vf/wK+tfnhEh63Yte1oQK5VGT2hIEYpD0Rzx8=
198204
github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
205+
github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc=
206+
github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo=
199207
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
200208
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
201209
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
@@ -221,6 +229,8 @@ github.com/ipfs/go-unixfsnode v1.9.0 h1:ubEhQhr22sPAKO2DNsyVBW7YB/zA8Zkif25aBvz8
221229
github.com/ipfs/go-unixfsnode v1.9.0/go.mod h1:HxRu9HYHOjK6HUqFBAi++7DVoWAHn0o4v/nZ/VA+0g8=
222230
github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs=
223231
github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU=
232+
github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc=
233+
github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8=
224234
github.com/ipld/go-car/v2 v2.13.1 h1:KnlrKvEPEzr5IZHKTXLAEub+tPrzeAFQVRlSQvuxBO4=
225235
github.com/ipld/go-car/v2 v2.13.1/go.mod h1:QkdjjFNGit2GIkpQ953KBwowuoukoM75nP/JI1iDJdo=
226236
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
@@ -251,6 +261,7 @@ github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY
251261
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
252262
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
253263
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
264+
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
254265
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
255266
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
256267
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -337,6 +348,7 @@ github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2
337348
github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk=
338349
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
339350
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
351+
github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
340352
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
341353
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
342354
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
@@ -700,6 +712,7 @@ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7
700712
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
701713
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
702714
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
715+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
703716
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
704717
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
705718
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=

‎gateway/backend.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package gateway
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
10+
"github.com/ipfs/boxo/ipns"
11+
"github.com/ipfs/boxo/namesys"
12+
"github.com/ipfs/boxo/path"
13+
"github.com/ipfs/boxo/path/resolver"
14+
"github.com/ipfs/go-cid"
15+
routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
16+
"github.com/libp2p/go-libp2p/core/routing"
17+
"github.com/prometheus/client_golang/prometheus"
18+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
19+
)
20+
21+
type backendOptions struct {
22+
ns namesys.NameSystem
23+
vs routing.ValueStore
24+
r resolver.Resolver
25+
26+
// Only used by [CarBackend]:
27+
promRegistry prometheus.Registerer
28+
getBlockTimeout time.Duration
29+
}
30+
31+
// WithNameSystem sets the name system to use with the different backends. If not set
32+
// it will use the default DNSLink resolver generated by [NewDNSResolver] along
33+
// with any configured [routing.ValueStore].
34+
func WithNameSystem(ns namesys.NameSystem) BackendOption {
35+
return func(opts *backendOptions) error {
36+
opts.ns = ns
37+
return nil
38+
}
39+
}
40+
41+
// WithValueStore sets the [routing.ValueStore] to use with the different backends.
42+
func WithValueStore(vs routing.ValueStore) BackendOption {
43+
return func(opts *backendOptions) error {
44+
opts.vs = vs
45+
return nil
46+
}
47+
}
48+
49+
// WithResolver sets the [resolver.Resolver] to use with the different backends.
50+
func WithResolver(r resolver.Resolver) BackendOption {
51+
return func(opts *backendOptions) error {
52+
opts.r = r
53+
return nil
54+
}
55+
}
56+
57+
// WithPrometheusRegistry sets the registry to use with [CarBackend].
58+
func WithPrometheusRegistry(reg prometheus.Registerer) BackendOption {
59+
return func(opts *backendOptions) error {
60+
opts.promRegistry = reg
61+
return nil
62+
}
63+
}
64+
65+
const DefaultGetBlockTimeout = time.Second * 60
66+
67+
// WithGetBlockTimeout sets a custom timeout when getting blocks from the
68+
// [CarFetcher] to use with [CarBackend]. By default, [DefaultGetBlockTimeout]
69+
// is used.
70+
func WithGetBlockTimeout(dur time.Duration) BackendOption {
71+
return func(opts *backendOptions) error {
72+
opts.getBlockTimeout = dur
73+
return nil
74+
}
75+
}
76+
77+
type BackendOption func(options *backendOptions) error
78+
79+
// baseBackend contains some common backend functionalities that are shared by
80+
// different backend implementations.
81+
type baseBackend struct {
82+
routing routing.ValueStore
83+
namesys namesys.NameSystem
84+
}
85+
86+
func newBaseBackend(vs routing.ValueStore, ns namesys.NameSystem) (baseBackend, error) {
87+
if vs == nil {
88+
vs = routinghelpers.Null{}
89+
}
90+
91+
if ns == nil {
92+
dns, err := NewDNSResolver(nil, nil)
93+
if err != nil {
94+
return baseBackend{}, err
95+
}
96+
97+
ns, err = namesys.NewNameSystem(vs, namesys.WithDNSResolver(dns))
98+
if err != nil {
99+
return baseBackend{}, err
100+
}
101+
}
102+
103+
return baseBackend{
104+
routing: vs,
105+
namesys: ns,
106+
}, nil
107+
}
108+
109+
func (bb *baseBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, time.Time, error) {
110+
switch p.Namespace() {
111+
case path.IPNSNamespace:
112+
res, err := namesys.Resolve(ctx, bb.namesys, p)
113+
if err != nil {
114+
return path.ImmutablePath{}, 0, time.Time{}, err
115+
}
116+
ip, err := path.NewImmutablePath(res.Path)
117+
if err != nil {
118+
return path.ImmutablePath{}, 0, time.Time{}, err
119+
}
120+
return ip, res.TTL, res.LastMod, nil
121+
case path.IPFSNamespace:
122+
ip, err := path.NewImmutablePath(p)
123+
return ip, 0, time.Time{}, err
124+
default:
125+
return path.ImmutablePath{}, 0, time.Time{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
126+
}
127+
}
128+
129+
func (bb *baseBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
130+
if bb.routing == nil {
131+
return nil, NewErrorStatusCode(errors.New("IPNS Record responses are not supported by this gateway"), http.StatusNotImplemented)
132+
}
133+
134+
name, err := ipns.NameFromCid(c)
135+
if err != nil {
136+
return nil, NewErrorStatusCode(err, http.StatusBadRequest)
137+
}
138+
139+
return bb.routing.GetValue(ctx, string(name.RoutingKey()))
140+
}
141+
142+
func (bb *baseBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) {
143+
if bb.namesys != nil {
144+
p, err := path.NewPath("/ipns/" + hostname)
145+
if err != nil {
146+
return nil, err
147+
}
148+
res, err := bb.namesys.Resolve(ctx, p, namesys.ResolveWithDepth(1))
149+
if err == namesys.ErrResolveRecursion {
150+
err = nil
151+
}
152+
return res.Path, err
153+
}
154+
155+
return nil, NewErrorStatusCode(errors.New("not implemented"), http.StatusNotImplemented)
156+
}
157+
158+
// newRemoteHTTPClient creates a new [http.Client] that is optimized for retrieving
159+
// multiple blocks from a single gateway concurrently.
160+
func newRemoteHTTPClient() *http.Client {
161+
transport := &http.Transport{
162+
MaxIdleConns: 1000,
163+
MaxConnsPerHost: 100,
164+
MaxIdleConnsPerHost: 100,
165+
IdleConnTimeout: 90 * time.Second,
166+
ForceAttemptHTTP2: true,
167+
}
168+
169+
return &http.Client{
170+
Timeout: DefaultGetBlockTimeout,
171+
Transport: otelhttp.NewTransport(transport),
172+
}
173+
}

‎gateway/blocks_backend.go ‎gateway/backend_blocks.go

+76-154
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@ import (
88
"io"
99
"net/http"
1010
"strings"
11-
"time"
1211

1312
"github.com/ipfs/boxo/blockservice"
1413
blockstore "github.com/ipfs/boxo/blockstore"
14+
"github.com/ipfs/boxo/exchange/offline"
1515
"github.com/ipfs/boxo/fetcher"
1616
bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice"
1717
"github.com/ipfs/boxo/files"
1818
"github.com/ipfs/boxo/ipld/merkledag"
1919
ufile "github.com/ipfs/boxo/ipld/unixfs/file"
2020
uio "github.com/ipfs/boxo/ipld/unixfs/io"
21-
"github.com/ipfs/boxo/ipns"
22-
"github.com/ipfs/boxo/namesys"
2321
"github.com/ipfs/boxo/path"
2422
"github.com/ipfs/boxo/path/resolver"
2523
blocks "github.com/ipfs/go-block-format"
@@ -38,8 +36,6 @@ import (
3836
"github.com/ipld/go-ipld-prime/traversal"
3937
"github.com/ipld/go-ipld-prime/traversal/selector"
4038
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
41-
routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
42-
"github.com/libp2p/go-libp2p/core/routing"
4339
mc "github.com/multiformats/go-multicodec"
4440

4541
// Ensure basic codecs are registered.
@@ -51,54 +47,18 @@ import (
5147

5248
// BlocksBackend is an [IPFSBackend] implementation based on a [blockservice.BlockService].
5349
type BlocksBackend struct {
50+
baseBackend
5451
blockStore blockstore.Blockstore
5552
blockService blockservice.BlockService
5653
dagService format.DAGService
5754
resolver resolver.Resolver
58-
59-
// Optional routing system to handle /ipns addresses.
60-
namesys namesys.NameSystem
61-
routing routing.ValueStore
6255
}
6356

6457
var _ IPFSBackend = (*BlocksBackend)(nil)
6558

66-
type blocksBackendOptions struct {
67-
ns namesys.NameSystem
68-
vs routing.ValueStore
69-
r resolver.Resolver
70-
}
71-
72-
// WithNameSystem sets the name system to use with the [BlocksBackend]. If not set
73-
// it will use the default DNSLink resolver generated by [NewDNSResolver] along
74-
// with any configured [routing.ValueStore].
75-
func WithNameSystem(ns namesys.NameSystem) BlocksBackendOption {
76-
return func(opts *blocksBackendOptions) error {
77-
opts.ns = ns
78-
return nil
79-
}
80-
}
81-
82-
// WithValueStore sets the [routing.ValueStore] to use with the [BlocksBackend].
83-
func WithValueStore(vs routing.ValueStore) BlocksBackendOption {
84-
return func(opts *blocksBackendOptions) error {
85-
opts.vs = vs
86-
return nil
87-
}
88-
}
89-
90-
// WithResolver sets the [resolver.Resolver] to use with the [BlocksBackend].
91-
func WithResolver(r resolver.Resolver) BlocksBackendOption {
92-
return func(opts *blocksBackendOptions) error {
93-
opts.r = r
94-
return nil
95-
}
96-
}
97-
98-
type BlocksBackendOption func(options *blocksBackendOptions) error
99-
100-
func NewBlocksBackend(blockService blockservice.BlockService, opts ...BlocksBackendOption) (*BlocksBackend, error) {
101-
var compiledOptions blocksBackendOptions
59+
// NewBlocksBackend creates a new [BlocksBackend] backed by a [blockservice.BlockService].
60+
func NewBlocksBackend(blockService blockservice.BlockService, opts ...BackendOption) (*BlocksBackend, error) {
61+
var compiledOptions backendOptions
10262
for _, o := range opts {
10363
if err := o(&compiledOptions); err != nil {
10464
return nil, err
@@ -108,50 +68,51 @@ func NewBlocksBackend(blockService blockservice.BlockService, opts ...BlocksBack
10868
// Setup the DAG services, which use the CAR block store.
10969
dagService := merkledag.NewDAGService(blockService)
11070

111-
// Setup a name system so that we are able to resolve /ipns links.
112-
var (
113-
ns namesys.NameSystem
114-
vs routing.ValueStore
115-
r resolver.Resolver
116-
)
117-
118-
vs = compiledOptions.vs
119-
if vs == nil {
120-
vs = routinghelpers.Null{}
121-
}
122-
123-
ns = compiledOptions.ns
124-
if ns == nil {
125-
dns, err := NewDNSResolver(nil, nil)
126-
if err != nil {
127-
return nil, err
128-
}
129-
130-
ns, err = namesys.NewNameSystem(vs, namesys.WithDNSResolver(dns))
131-
if err != nil {
132-
return nil, err
133-
}
134-
}
135-
136-
r = compiledOptions.r
71+
// Setup the [resolver.Resolver] if not provided.
72+
r := compiledOptions.r
13773
if r == nil {
138-
// Setup the UnixFS resolver.
13974
fetcherCfg := bsfetcher.NewFetcherConfig(blockService)
14075
fetcherCfg.PrototypeChooser = dagpb.AddSupportToChooser(bsfetcher.DefaultPrototypeChooser)
14176
fetcher := fetcherCfg.WithReifier(unixfsnode.Reify)
14277
r = resolver.NewBasicResolver(fetcher)
14378
}
14479

80+
// Setup the [baseBackend] which takes care of some shared functionality, such
81+
// as resolving /ipns links.
82+
baseBackend, err := newBaseBackend(compiledOptions.vs, compiledOptions.ns)
83+
if err != nil {
84+
return nil, err
85+
}
86+
14587
return &BlocksBackend{
88+
baseBackend: baseBackend,
14689
blockStore: blockService.Blockstore(),
14790
blockService: blockService,
14891
dagService: dagService,
14992
resolver: r,
150-
routing: vs,
151-
namesys: ns,
15293
}, nil
15394
}
15495

96+
// NewRemoteBlocksBackend creates a new [BlocksBackend] backed by one or more
97+
// gateways. These gateways must support RAW block requests and IPNS Record
98+
// requests. See [NewRemoteBlockstore] and [NewRemoteValueStore] for more details.
99+
//
100+
// To create a more custom [BlocksBackend], please use [NewBlocksBackend] directly.
101+
func NewRemoteBlocksBackend(gatewayURL []string, httpClient *http.Client, opts ...BackendOption) (*BlocksBackend, error) {
102+
blockStore, err := NewRemoteBlockstore(gatewayURL, httpClient)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
valueStore, err := NewRemoteValueStore(gatewayURL, httpClient)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
blockService := blockservice.New(blockStore, offline.Exchange(blockStore))
113+
return NewBlocksBackend(blockService, append(opts, WithValueStore(valueStore))...)
114+
}
115+
155116
func (bb *BlocksBackend) Get(ctx context.Context, path path.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) {
156117
md, nd, err := bb.getNode(ctx, path)
157118
if err != nil {
@@ -367,9 +328,17 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p path.ImmutablePath, param
367328
unixfsnode.AddUnixFSReificationToLinkSystem(&lsys)
368329
lsys.StorageReadOpener = blockOpener(ctx, blockGetter)
369330

331+
// First resolve the path since we always need to.
332+
lastCid, remainder, err := pathResolver.ResolveToLastNode(ctx, p)
333+
if err != nil {
334+
// io.PipeWriter.CloseWithError always returns nil.
335+
_ = w.CloseWithError(err)
336+
return
337+
}
338+
370339
// TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769
371340
// TODO: this is very slow if blocks are remote due to linear traversal. Do we need deterministic traversals here?
372-
carWriteErr := walkGatewaySimpleSelector(ctx, p, params, &lsys, pathResolver)
341+
carWriteErr := walkGatewaySimpleSelector(ctx, lastCid, nil, remainder, params, &lsys)
373342

374343
// io.PipeWriter.CloseWithError always returns nil.
375344
_ = w.CloseWithError(carWriteErr)
@@ -379,29 +348,49 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p path.ImmutablePath, param
379348
}
380349

381350
// walkGatewaySimpleSelector walks the subgraph described by the path and terminal element parameters
382-
func walkGatewaySimpleSelector(ctx context.Context, p path.ImmutablePath, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error {
383-
// First resolve the path since we always need to.
384-
lastCid, remainder, err := pathResolver.ResolveToLastNode(ctx, p)
385-
if err != nil {
386-
return err
387-
}
388-
351+
func walkGatewaySimpleSelector(ctx context.Context, lastCid cid.Cid, terminalBlk blocks.Block, remainder []string, params CarParams, lsys *ipld.LinkSystem) error {
389352
lctx := ipld.LinkContext{Ctx: ctx}
390353
pathTerminalCidLink := cidlink.Link{Cid: lastCid}
391354

392355
// If the scope is the block, now we only need to retrieve the root block of the last element of the path.
393356
if params.Scope == DagScopeBlock {
394-
_, err = lsys.LoadRaw(lctx, pathTerminalCidLink)
357+
_, err := lsys.LoadRaw(lctx, pathTerminalCidLink)
395358
return err
396359
}
397360

398-
// If we're asking for everything then give it
399-
if params.Scope == DagScopeAll {
400-
lastCidNode, err := lsys.Load(lctx, pathTerminalCidLink, basicnode.Prototype.Any)
361+
pc := dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
362+
if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
363+
return tlnkNd.LinkTargetNodePrototype(), nil
364+
}
365+
return basicnode.Prototype.Any, nil
366+
})
367+
368+
np, err := pc(pathTerminalCidLink, lctx)
369+
if err != nil {
370+
return err
371+
}
372+
373+
var lastCidNode datamodel.Node
374+
if terminalBlk != nil {
375+
decoder, err := lsys.DecoderChooser(pathTerminalCidLink)
401376
if err != nil {
402377
return err
403378
}
379+
nb := np.NewBuilder()
380+
blockData := terminalBlk.RawData()
381+
if err := decoder(nb, bytes.NewReader(blockData)); err != nil {
382+
return err
383+
}
384+
lastCidNode = nb.Build()
385+
} else {
386+
lastCidNode, err = lsys.Load(lctx, pathTerminalCidLink, np)
387+
if err != nil {
388+
return err
389+
}
390+
}
404391

392+
// If we're asking for everything then give it
393+
if params.Scope == DagScopeAll {
405394
sel, err := selector.ParseSelector(selectorparse.CommonSelector_ExploreAllRecursively)
406395
if err != nil {
407396
return err
@@ -427,23 +416,6 @@ func walkGatewaySimpleSelector(ctx context.Context, p path.ImmutablePath, params
427416
// From now on, dag-scope=entity!
428417
// Since we need more of the graph load it to figure out what we have
429418
// This includes determining if the terminal node is UnixFS or not
430-
pc := dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
431-
if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
432-
return tlnkNd.LinkTargetNodePrototype(), nil
433-
}
434-
return basicnode.Prototype.Any, nil
435-
})
436-
437-
np, err := pc(pathTerminalCidLink, lctx)
438-
if err != nil {
439-
return err
440-
}
441-
442-
lastCidNode, err := lsys.Load(lctx, pathTerminalCidLink, np)
443-
if err != nil {
444-
return err
445-
}
446-
447419
if pbn, ok := lastCidNode.(dagpb.PBNode); !ok {
448420
// If it's not valid dag-pb then we're done
449421
return nil
@@ -630,55 +602,6 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
630602
return pathRoots, lastPath, remainder, nil
631603
}
632604

633-
func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, time.Time, error) {
634-
switch p.Namespace() {
635-
case path.IPNSNamespace:
636-
res, err := namesys.Resolve(ctx, bb.namesys, p)
637-
if err != nil {
638-
return path.ImmutablePath{}, 0, time.Time{}, err
639-
}
640-
ip, err := path.NewImmutablePath(res.Path)
641-
if err != nil {
642-
return path.ImmutablePath{}, 0, time.Time{}, err
643-
}
644-
return ip, res.TTL, res.LastMod, nil
645-
case path.IPFSNamespace:
646-
ip, err := path.NewImmutablePath(p)
647-
return ip, 0, time.Time{}, err
648-
default:
649-
return path.ImmutablePath{}, 0, time.Time{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
650-
}
651-
}
652-
653-
func (bb *BlocksBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
654-
if bb.routing == nil {
655-
return nil, NewErrorStatusCode(errors.New("IPNS Record responses are not supported by this gateway"), http.StatusNotImplemented)
656-
}
657-
658-
name, err := ipns.NameFromCid(c)
659-
if err != nil {
660-
return nil, NewErrorStatusCode(err, http.StatusBadRequest)
661-
}
662-
663-
return bb.routing.GetValue(ctx, string(name.RoutingKey()))
664-
}
665-
666-
func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) {
667-
if bb.namesys != nil {
668-
p, err := path.NewPath("/ipns/" + hostname)
669-
if err != nil {
670-
return nil, err
671-
}
672-
res, err := bb.namesys.Resolve(ctx, p, namesys.ResolveWithDepth(1))
673-
if err == namesys.ErrResolveRecursion {
674-
err = nil
675-
}
676-
return res.Path, err
677-
}
678-
679-
return nil, NewErrorStatusCode(errors.New("not implemented"), http.StatusNotImplemented)
680-
}
681-
682605
func (bb *BlocksBackend) IsCached(ctx context.Context, p path.Path) bool {
683606
rp, _, err := bb.resolvePath(ctx, p)
684607
if err != nil {
@@ -711,11 +634,10 @@ func (bb *BlocksBackend) ResolvePath(ctx context.Context, path path.ImmutablePat
711634
func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.ImmutablePath, []string, error) {
712635
var err error
713636
if p.Namespace() == path.IPNSNamespace {
714-
res, err := namesys.Resolve(ctx, bb.namesys, p)
637+
p, _, _, err = bb.baseBackend.ResolveMutable(ctx, p)
715638
if err != nil {
716639
return path.ImmutablePath{}, nil, err
717640
}
718-
p = res.Path
719641
}
720642

721643
if p.Namespace() != path.IPFSNamespace {

‎gateway/backend_car.go

+1,147
Large diffs are not rendered by default.

‎gateway/backend_car_fetcher.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package gateway
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"math/rand"
9+
"net/http"
10+
"net/url"
11+
"strconv"
12+
"strings"
13+
"time"
14+
15+
"github.com/ipfs/boxo/path"
16+
)
17+
18+
type DataCallback func(p path.ImmutablePath, reader io.Reader) error
19+
20+
// CarFetcher powers a [CarBackend].
21+
type CarFetcher interface {
22+
Fetch(ctx context.Context, path path.ImmutablePath, params CarParams, cb DataCallback) error
23+
}
24+
25+
type remoteCarFetcher struct {
26+
httpClient *http.Client
27+
gatewayURL []string
28+
rand *rand.Rand
29+
}
30+
31+
// NewRemoteCarFetcher returns a [CarFetcher] that is backed by one or more gateways
32+
// that support partial [CAR requests], as described in [IPIP-402]. You can optionally
33+
// pass your own [http.Client].
34+
//
35+
// [CAR requests]: https://www.iana.org/assignments/media-types/application/vnd.ipld.car
36+
// [IPIP-402]: https://specs.ipfs.tech/ipips/ipip-0402
37+
func NewRemoteCarFetcher(gatewayURL []string, httpClient *http.Client) (CarFetcher, error) {
38+
if len(gatewayURL) == 0 {
39+
return nil, errors.New("missing gateway URLs to which to proxy")
40+
}
41+
42+
if httpClient == nil {
43+
httpClient = newRemoteHTTPClient()
44+
}
45+
46+
return &remoteCarFetcher{
47+
gatewayURL: gatewayURL,
48+
httpClient: httpClient,
49+
rand: rand.New(rand.NewSource(time.Now().Unix())),
50+
}, nil
51+
}
52+
53+
func (ps *remoteCarFetcher) Fetch(ctx context.Context, path path.ImmutablePath, params CarParams, cb DataCallback) error {
54+
url := contentPathToCarUrl(path, params)
55+
56+
urlStr := fmt.Sprintf("%s%s", ps.getRandomGatewayURL(), url.String())
57+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
58+
if err != nil {
59+
return err
60+
}
61+
log.Debugw("car fetch", "url", req.URL)
62+
req.Header.Set("Accept", "application/vnd.ipld.car;order=dfs;dups=y")
63+
resp, err := ps.httpClient.Do(req)
64+
if err != nil {
65+
return err
66+
}
67+
68+
if resp.StatusCode != http.StatusOK {
69+
errData, err := io.ReadAll(resp.Body)
70+
if err != nil {
71+
err = fmt.Errorf("could not read error message: %w", err)
72+
} else {
73+
err = fmt.Errorf("%q", string(errData))
74+
}
75+
return fmt.Errorf("http error from car gateway: %s: %w", resp.Status, err)
76+
}
77+
78+
err = cb(path, resp.Body)
79+
if err != nil {
80+
resp.Body.Close()
81+
return err
82+
}
83+
return resp.Body.Close()
84+
}
85+
86+
func (ps *remoteCarFetcher) getRandomGatewayURL() string {
87+
return ps.gatewayURL[ps.rand.Intn(len(ps.gatewayURL))]
88+
}
89+
90+
// contentPathToCarUrl returns an URL that allows retrieval of specified resource
91+
// from a trustless gateway that implements IPIP-402
92+
func contentPathToCarUrl(path path.ImmutablePath, params CarParams) *url.URL {
93+
return &url.URL{
94+
Path: path.String(),
95+
RawQuery: carParamsToString(params),
96+
}
97+
}
98+
99+
// carParamsToString converts CarParams to URL parameters compatible with IPIP-402
100+
func carParamsToString(params CarParams) string {
101+
paramsBuilder := strings.Builder{}
102+
paramsBuilder.WriteString("format=car") // always send explicit format in URL, this makes debugging easier, even when Accept header was set
103+
if params.Scope != "" {
104+
paramsBuilder.WriteString("&dag-scope=")
105+
paramsBuilder.WriteString(string(params.Scope))
106+
}
107+
if params.Range != nil {
108+
paramsBuilder.WriteString("&entity-bytes=")
109+
paramsBuilder.WriteString(strconv.FormatInt(params.Range.From, 10))
110+
paramsBuilder.WriteString(":")
111+
if params.Range.To != nil {
112+
paramsBuilder.WriteString(strconv.FormatInt(*params.Range.To, 10))
113+
} else {
114+
paramsBuilder.WriteString("*")
115+
}
116+
}
117+
return paramsBuilder.String()
118+
}
119+
120+
type retryCarFetcher struct {
121+
inner CarFetcher
122+
retries int
123+
}
124+
125+
// NewRetryCarFetcher returns a [CarFetcher] that retries to fetch up to the given
126+
// [allowedRetries] using the [inner] [CarFetcher]. If the inner fetcher returns
127+
// an [ErrPartialResponse] error, then the number of retries is reset to the initial
128+
// maximum allowed retries.
129+
func NewRetryCarFetcher(inner CarFetcher, allowedRetries int) (CarFetcher, error) {
130+
if allowedRetries <= 0 {
131+
return nil, errors.New("number of retries must be a number larger than 0")
132+
}
133+
134+
return &retryCarFetcher{
135+
inner: inner,
136+
retries: allowedRetries,
137+
}, nil
138+
}
139+
140+
func (r *retryCarFetcher) Fetch(ctx context.Context, path path.ImmutablePath, params CarParams, cb DataCallback) error {
141+
return r.fetch(ctx, path, params, cb, r.retries)
142+
}
143+
144+
func (r *retryCarFetcher) fetch(ctx context.Context, path path.ImmutablePath, params CarParams, cb DataCallback, retriesLeft int) error {
145+
err := r.inner.Fetch(ctx, path, params, cb)
146+
if err == nil {
147+
return nil
148+
}
149+
150+
if retriesLeft > 0 {
151+
retriesLeft--
152+
} else {
153+
return fmt.Errorf("retry fetcher out of retries: %w", err)
154+
}
155+
156+
switch t := err.(type) {
157+
case ErrPartialResponse:
158+
if len(t.StillNeed) > 1 {
159+
return errors.New("only a single request at a time is supported")
160+
}
161+
162+
// Resets the number of retries for partials, mimicking Caboose logic.
163+
retriesLeft = r.retries
164+
165+
return r.fetch(ctx, t.StillNeed[0].Path, t.StillNeed[0].Params, cb, retriesLeft)
166+
default:
167+
return r.fetch(ctx, path, params, cb, retriesLeft)
168+
}
169+
}

‎gateway/backend_car_fetcher_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package gateway
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/ipfs/boxo/path"
9+
)
10+
11+
func TestContentPathToCarUrl(t *testing.T) {
12+
negativeOffset := int64(-42)
13+
testCases := []struct {
14+
contentPath string // to be turned into ImmutablePath
15+
carParams CarParams
16+
expectedUrl string // url.URL.String()
17+
}{
18+
{
19+
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
20+
carParams: CarParams{},
21+
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car",
22+
},
23+
{
24+
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
25+
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 0, To: nil}},
26+
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=entity&entity-bytes=0:*",
27+
},
28+
{
29+
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
30+
carParams: CarParams{Scope: "block"},
31+
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=block",
32+
},
33+
{
34+
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
35+
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 4, To: &negativeOffset}},
36+
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=entity&entity-bytes=4:-42",
37+
},
38+
{
39+
// a regression test for case described in https://github.com/ipfs/gateway-conformance/issues/115
40+
contentPath: "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze/I/Auditorio_de_Tenerife%2C_Santa_Cruz_de_Tenerife%2C_España%2C_2012-12-15%2C_DD_02.jpg.webp",
41+
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 0, To: nil}},
42+
expectedUrl: "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze/I/Auditorio_de_Tenerife%252C_Santa_Cruz_de_Tenerife%252C_Espa%C3%B1a%252C_2012-12-15%252C_DD_02.jpg.webp?format=car&dag-scope=entity&entity-bytes=0:*",
43+
},
44+
}
45+
46+
for _, tc := range testCases {
47+
t.Run("TestContentPathToCarUrl", func(t *testing.T) {
48+
p, err := path.NewPath(tc.contentPath)
49+
require.NoError(t, err)
50+
51+
contentPath, err := path.NewImmutablePath(p)
52+
require.NoError(t, err)
53+
54+
result := contentPathToCarUrl(contentPath, tc.carParams).String()
55+
if result != tc.expectedUrl {
56+
t.Errorf("Expected %q, but got %q", tc.expectedUrl, result)
57+
}
58+
})
59+
}
60+
}

‎gateway/backend_car_files.go

+697
Large diffs are not rendered by default.

‎gateway/backend_car_test.go

+1,100
Large diffs are not rendered by default.

‎gateway/backend_car_traversal.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package gateway
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"sync"
10+
"time"
11+
12+
"github.com/ipfs/boxo/verifcid"
13+
blocks "github.com/ipfs/go-block-format"
14+
"github.com/ipfs/go-cid"
15+
"github.com/ipfs/go-unixfsnode"
16+
"github.com/ipld/go-car"
17+
"github.com/ipld/go-ipld-prime"
18+
"github.com/ipld/go-ipld-prime/datamodel"
19+
"github.com/ipld/go-ipld-prime/linking"
20+
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
21+
"github.com/multiformats/go-multihash"
22+
)
23+
24+
type getBlock func(ctx context.Context, cid cid.Cid) (blocks.Block, error)
25+
26+
var errNilBlock = ErrInvalidResponse{Message: "received a nil block with no error"}
27+
28+
func carToLinearBlockGetter(ctx context.Context, reader io.Reader, timeout time.Duration, metrics *CarBackendMetrics) (getBlock, error) {
29+
cr, err := car.NewCarReaderWithOptions(reader, car.WithErrorOnEmptyRoots(false))
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
cbCtx, cncl := context.WithCancel(ctx)
35+
36+
type blockRead struct {
37+
block blocks.Block
38+
err error
39+
}
40+
41+
blkCh := make(chan blockRead, 1)
42+
go func() {
43+
defer cncl()
44+
defer close(blkCh)
45+
for {
46+
blk, rdErr := cr.Next()
47+
select {
48+
case blkCh <- blockRead{blk, rdErr}:
49+
if rdErr != nil {
50+
cncl()
51+
}
52+
case <-cbCtx.Done():
53+
return
54+
}
55+
}
56+
}()
57+
58+
isFirstBlock := true
59+
mx := sync.Mutex{}
60+
61+
return func(ctx context.Context, c cid.Cid) (blocks.Block, error) {
62+
mx.Lock()
63+
defer mx.Unlock()
64+
if err := verifcid.ValidateCid(verifcid.DefaultAllowlist, c); err != nil {
65+
return nil, err
66+
}
67+
68+
isId, bdata := extractIdentityMultihashCIDContents(c)
69+
if isId {
70+
return blocks.NewBlockWithCid(bdata, c)
71+
}
72+
73+
// initially set a higher timeout here so that if there's an initial timeout error we get it from the car reader.
74+
var t *time.Timer
75+
if isFirstBlock {
76+
t = time.NewTimer(timeout * 2)
77+
} else {
78+
t = time.NewTimer(timeout)
79+
}
80+
var blkRead blockRead
81+
var ok bool
82+
select {
83+
case blkRead, ok = <-blkCh:
84+
if !t.Stop() {
85+
<-t.C
86+
}
87+
t.Reset(timeout)
88+
case <-t.C:
89+
return nil, ErrGatewayTimeout
90+
}
91+
if !ok || blkRead.err != nil {
92+
if !ok || errors.Is(blkRead.err, io.EOF) {
93+
return nil, io.ErrUnexpectedEOF
94+
}
95+
return nil, blockstoreErrToGatewayErr(blkRead.err)
96+
}
97+
if blkRead.block != nil {
98+
metrics.carBlocksFetchedMetric.Inc()
99+
if !blkRead.block.Cid().Equals(c) {
100+
return nil, ErrInvalidResponse{Message: fmt.Sprintf("received block with cid %s, expected %s", blkRead.block.Cid(), c)}
101+
}
102+
return blkRead.block, nil
103+
}
104+
return nil, errNilBlock
105+
}, nil
106+
}
107+
108+
// extractIdentityMultihashCIDContents will check if a given CID has an identity multihash and if so return true and
109+
// the bytes encoded in the digest, otherwise will return false.
110+
// Taken from https://github.com/ipfs/boxo/blob/b96767cc0971ca279feb36e7844e527a774309ab/blockstore/idstore.go#L30
111+
func extractIdentityMultihashCIDContents(k cid.Cid) (bool, []byte) {
112+
// Pre-check by calling Prefix(), this much faster than extracting the hash.
113+
if k.Prefix().MhType != multihash.IDENTITY {
114+
return false, nil
115+
}
116+
117+
dmh, err := multihash.Decode(k.Hash())
118+
if err != nil || dmh.Code != multihash.IDENTITY {
119+
return false, nil
120+
}
121+
return true, dmh.Digest
122+
}
123+
124+
func getCarLinksystem(fn getBlock) *ipld.LinkSystem {
125+
lsys := cidlink.DefaultLinkSystem()
126+
lsys.StorageReadOpener = func(linkContext linking.LinkContext, link datamodel.Link) (io.Reader, error) {
127+
c := link.(cidlink.Link).Cid
128+
blk, err := fn(linkContext.Ctx, c)
129+
if err != nil {
130+
return nil, err
131+
}
132+
return bytes.NewReader(blk.RawData()), nil
133+
}
134+
lsys.TrustedStorage = true
135+
unixfsnode.AddUnixFSReificationToLinkSystem(&lsys)
136+
return &lsys
137+
}

‎gateway/blockstore.go

+21-9
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ var _ blockstore.Blockstore = (*cacheBlockStore)(nil)
3434
// NewCacheBlockStore creates a new [blockstore.Blockstore] that caches blocks
3535
// in memory using a two queue cache. It can be useful, for example, when paired
3636
// with a proxy blockstore (see [NewRemoteBlockstore]).
37-
func NewCacheBlockStore(size int) (blockstore.Blockstore, error) {
37+
//
38+
// If the given [prometheus.Registerer] is nil, a new one will be created using
39+
// [prometheus.NewRegistry].
40+
func NewCacheBlockStore(size int, reg prometheus.Registerer) (blockstore.Blockstore, error) {
3841
c, err := lru.New2Q[string, []byte](size)
3942
if err != nil {
4043
return nil, err
4144
}
4245

46+
if reg == nil {
47+
reg = prometheus.NewRegistry()
48+
}
49+
4350
cacheHitsMetric := prometheus.NewCounter(prometheus.CounterOpts{
4451
Namespace: "ipfs",
4552
Subsystem: "http",
@@ -54,12 +61,12 @@ func NewCacheBlockStore(size int) (blockstore.Blockstore, error) {
5461
Help: "The number of global block cache requests.",
5562
})
5663

57-
err = prometheus.Register(cacheHitsMetric)
64+
err = reg.Register(cacheHitsMetric)
5865
if err != nil {
5966
return nil, err
6067
}
6168

62-
err = prometheus.Register(cacheRequestsMetric)
69+
err = reg.Register(cacheRequestsMetric)
6370
if err != nil {
6471
return nil, err
6572
}
@@ -151,18 +158,23 @@ type remoteBlockstore struct {
151158
}
152159

153160
// NewRemoteBlockstore creates a new [blockstore.Blockstore] that is backed by one
154-
// or more gateways that support RAW block requests. See the [Trustless Gateway]
155-
// specification for more details.
161+
// or more gateways that support [RAW block] requests. See the [Trustless Gateway]
162+
// specification for more details. You can optionally pass your own [http.Client].
156163
//
157164
// [Trustless Gateway]: https://specs.ipfs.tech/http-gateways/trustless-gateway/
158-
func NewRemoteBlockstore(gatewayURL []string) (blockstore.Blockstore, error) {
165+
// [RAW block]: https://www.iana.org/assignments/media-types/application/vnd.ipld.raw
166+
func NewRemoteBlockstore(gatewayURL []string, httpClient *http.Client) (blockstore.Blockstore, error) {
159167
if len(gatewayURL) == 0 {
160-
return nil, errors.New("missing gateway URLs to which to proxy")
168+
return nil, errors.New("missing remote block backend URL")
169+
}
170+
171+
if httpClient == nil {
172+
httpClient = newRemoteHTTPClient()
161173
}
162174

163175
return &remoteBlockstore{
164176
gatewayURL: gatewayURL,
165-
httpClient: newRemoteHTTPClient(),
177+
httpClient: httpClient,
166178
rand: rand.New(rand.NewSource(time.Now().Unix())),
167179
// Enables block validation by default. Important since we are
168180
// proxying block requests to untrusted gateways.
@@ -185,7 +197,7 @@ func (ps *remoteBlockstore) fetch(ctx context.Context, c cid.Cid) (blocks.Block,
185197
defer resp.Body.Close()
186198

187199
if resp.StatusCode != http.StatusOK {
188-
return nil, fmt.Errorf("http error from block gateway: %s", resp.Status)
200+
return nil, fmt.Errorf("http error from remote block backend: %s", resp.Status)
189201
}
190202

191203
rb, err := io.ReadAll(resp.Body)

‎gateway/errors.go

+39-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"time"
1111

1212
"github.com/ipfs/boxo/gateway/assets"
13+
"github.com/ipfs/boxo/path"
1314
"github.com/ipfs/boxo/path/resolver"
1415
"github.com/ipfs/go-cid"
1516
"github.com/ipld/go-ipld-prime/datamodel"
17+
"github.com/ipld/go-ipld-prime/schema"
1618
)
1719

1820
var (
@@ -127,6 +129,42 @@ func (e *ErrorStatusCode) Unwrap() error {
127129
return e.Err
128130
}
129131

132+
// ErrInvalidResponse can be returned from a [DataCallback] to indicate that
133+
// the data provided for the requested resource was explicitly 'incorrect',
134+
// for example, when received blocks did not belong to the requested dag,
135+
// or non-car-conforming data was returned.
136+
type ErrInvalidResponse struct {
137+
Message string
138+
}
139+
140+
func (e ErrInvalidResponse) Error() string {
141+
return e.Message
142+
}
143+
144+
// ErrPartialResponse can be returned from a [DataCallback] to indicate that some of the requested resource
145+
// was successfully fetched, and that instead of retrying the full resource, that there are
146+
// one or more more specific resources that should be fetched (via StillNeed) to complete the request.
147+
//
148+
// This primitive allows for resume mechanism that is useful when a big CAR
149+
// stream gets truncated due to network error, HTTP middleware timeout, etc,
150+
// but some useful blocks were received and should not be fetched again.
151+
type ErrPartialResponse struct {
152+
error
153+
StillNeed []CarResource
154+
}
155+
156+
type CarResource struct {
157+
Path path.ImmutablePath
158+
Params CarParams
159+
}
160+
161+
func (epr ErrPartialResponse) Error() string {
162+
if epr.error != nil {
163+
return fmt.Sprintf("partial response: %s", epr.error.Error())
164+
}
165+
return "received a partial CAR response from the backend"
166+
}
167+
130168
func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defaultCode int) {
131169
code := defaultCode
132170

@@ -184,7 +222,7 @@ func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defa
184222
// isErrNotFound returns true for IPLD errors that should return 4xx errors (e.g. the path doesn't exist, the data is
185223
// the wrong type, etc.), rather than issues with just finding and retrieving the data.
186224
func isErrNotFound(err error) bool {
187-
if errors.Is(err, &resolver.ErrNoLink{}) {
225+
if errors.Is(err, &resolver.ErrNoLink{}) || errors.Is(err, schema.ErrNoSuchField{}) {
188226
return true
189227
}
190228

‎gateway/gateway_test.go

+13-11
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
)
2121

2222
func TestGatewayGet(t *testing.T) {
23-
ts, backend, root := newTestServerAndNode(t, nil, "fixtures.car")
23+
ts, backend, root := newTestServerAndNode(t, "fixtures.car")
2424

2525
ctx, cancel := context.WithCancel(context.Background())
2626
defer cancel()
@@ -96,7 +96,7 @@ func TestGatewayGet(t *testing.T) {
9696
func TestHeaders(t *testing.T) {
9797
t.Parallel()
9898

99-
ts, backend, root := newTestServerAndNode(t, nil, "headers-test.car")
99+
ts, backend, root := newTestServerAndNode(t, "headers-test.car")
100100

101101
var (
102102
rootCID = "bafybeidbcy4u6y55gsemlubd64zk53xoxs73ifd6rieejxcr7xy46mjvky"
@@ -121,7 +121,7 @@ func TestHeaders(t *testing.T) {
121121
t.Run("Cache-Control uses TTL for /ipns/ when it is known", func(t *testing.T) {
122122
t.Parallel()
123123

124-
ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
124+
ts, backend, root := newTestServerAndNode(t, "ipns-hostname-redirects.car")
125125
backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.FromCid(root), time.Second*30)
126126
backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.FromCid(root), time.Second*55)
127127
backend.namesys["/ipns/unknown.com"] = newMockNamesysItem(path.FromCid(root), 0)
@@ -420,7 +420,7 @@ func TestHeaders(t *testing.T) {
420420
}
421421

422422
func TestGoGetSupport(t *testing.T) {
423-
ts, _, root := newTestServerAndNode(t, nil, "fixtures.car")
423+
ts, _, root := newTestServerAndNode(t, "fixtures.car")
424424

425425
// mimic go-get
426426
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipfs/"+root.String()+"?go-get=1", nil)
@@ -432,7 +432,7 @@ func TestRedirects(t *testing.T) {
432432
t.Parallel()
433433

434434
t.Run("IPNS Base58 Multihash Redirect", func(t *testing.T) {
435-
ts, _, _ := newTestServerAndNode(t, nil, "fixtures.car")
435+
ts, _, _ := newTestServerAndNode(t, "fixtures.car")
436436

437437
t.Run("ED25519 Base58-encoded key", func(t *testing.T) {
438438
t.Parallel()
@@ -453,7 +453,7 @@ func TestRedirects(t *testing.T) {
453453

454454
t.Run("URI Query Redirects", func(t *testing.T) {
455455
t.Parallel()
456-
ts, _, _ := newTestServerAndNode(t, mockNamesys{}, "fixtures.car")
456+
ts, _, _ := newTestServerAndNode(t, "fixtures.car")
457457

458458
cid := "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
459459
for _, test := range []struct {
@@ -492,7 +492,7 @@ func TestRedirects(t *testing.T) {
492492
t.Run("IPNS Hostname Redirects", func(t *testing.T) {
493493
t.Parallel()
494494

495-
ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
495+
ts, backend, root := newTestServerAndNode(t, "ipns-hostname-redirects.car")
496496
backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.FromCid(root), 0)
497497

498498
// make request to directory containing index.html
@@ -555,9 +555,11 @@ func TestRedirects(t *testing.T) {
555555

556556
// Check statuses and body.
557557
require.Equal(t, http.StatusOK, res.StatusCode)
558-
body, err := io.ReadAll(res.Body)
559-
require.NoError(t, err)
560-
require.Equal(t, "hello world\n", string(body))
558+
if method != http.MethodHead {
559+
body, err := io.ReadAll(res.Body)
560+
require.NoError(t, err)
561+
require.Equal(t, "hello world\n", string(body))
562+
}
561563

562564
// Check Etag.
563565
etag := res.Header.Get("Etag")
@@ -948,7 +950,7 @@ func TestPanicStatusCode(t *testing.T) {
948950

949951
func TestBrowserErrorHTML(t *testing.T) {
950952
t.Parallel()
951-
ts, _, root := newTestServerAndNode(t, nil, "fixtures.car")
953+
ts, _, root := newTestServerAndNode(t, "fixtures.car")
952954

953955
t.Run("plain error if request does not have Accept: text/html", func(t *testing.T) {
954956
t.Parallel()

‎gateway/handler_unixfs_dir.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,9 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *
121121
i.unixfsDirIndexGetMetric.WithLabelValues(originalContentPath.Namespace()).Observe(time.Since(rq.begin).Seconds())
122122
}
123123
return success
124-
}
125-
126-
if isErrNotFound(err) {
124+
} else if isErrNotFound(err) {
127125
rq.logger.Debugw("no index.html; noop", "path", idxPath)
128-
} else if err != nil {
126+
} else {
129127
i.webError(w, r, err, http.StatusInternalServerError)
130128
return false
131129
}

‎gateway/handler_unixfs_dir_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func TestIPNSHostnameBacklinks(t *testing.T) {
1414
// Test if directory listing on DNSLink Websites have correct backlinks.
15-
ts, backend, root := newTestServerAndNode(t, nil, "dir-special-chars.car")
15+
ts, backend, root := newTestServerAndNode(t, "dir-special-chars.car")
1616

1717
ctx, cancel := context.WithCancel(context.Background())
1818
defer cancel()

‎gateway/remote_blocks_backend.go

-53
This file was deleted.
Binary file not shown.

‎gateway/utilities_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
)
2828

2929
func mustNewRequest(t *testing.T, method string, path string, body io.Reader) *http.Request {
30-
r, err := http.NewRequest(http.MethodGet, path, body)
30+
r, err := http.NewRequest(method, path, body)
3131
require.NoError(t, err)
3232
return r
3333
}
@@ -224,7 +224,7 @@ func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip path.P
224224
return md.LastSegment, nil
225225
}
226226

227-
func newTestServerAndNode(t *testing.T, ns mockNamesys, fixturesFile string) (*httptest.Server, *mockBackend, cid.Cid) {
227+
func newTestServerAndNode(t *testing.T, fixturesFile string) (*httptest.Server, *mockBackend, cid.Cid) {
228228
backend, root := newMockBackend(t, fixturesFile)
229229
ts := newTestServer(t, backend)
230230
return ts, backend, root

‎gateway/value_store.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@ type remoteValueStore struct {
2020
rand *rand.Rand
2121
}
2222

23-
// NewRemoteValueStore creates a new [routing.ValueStore] that is backed by one
24-
// or more gateways that support IPNS Record requests. See the [Trustless Gateway]
25-
// specification for more details.
23+
// NewRemoteValueStore creates a new [routing.ValueStore] backed by one or more
24+
// gateways that support IPNS Record requests. See the [Trustless Gateway]
25+
// specification for more details. You can optionally pass your own [http.Client].
2626
//
2727
// [Trustless Gateway]: https://specs.ipfs.tech/http-gateways/trustless-gateway/
28-
func NewRemoteValueStore(gatewayURL []string) (routing.ValueStore, error) {
28+
func NewRemoteValueStore(gatewayURL []string, httpClient *http.Client) (routing.ValueStore, error) {
2929
if len(gatewayURL) == 0 {
3030
return nil, errors.New("missing gateway URLs to which to proxy")
3131
}
3232

33+
if httpClient == nil {
34+
httpClient = newRemoteHTTPClient()
35+
}
36+
3337
return &remoteValueStore{
3438
gatewayURL: gatewayURL,
35-
httpClient: newRemoteHTTPClient(),
39+
httpClient: httpClient,
3640
rand: rand.New(rand.NewSource(time.Now().Unix())),
3741
}, nil
3842
}

‎go.mod

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/gogo/protobuf v1.3.2
1414
github.com/google/uuid v1.5.0
1515
github.com/gorilla/mux v1.8.1
16+
github.com/hashicorp/go-multierror v1.1.1
1617
github.com/hashicorp/golang-lru/v2 v2.0.7
1718
github.com/ipfs/bbloom v0.0.4
1819
github.com/ipfs/go-bitfield v1.1.0
@@ -30,6 +31,7 @@ require (
3031
github.com/ipfs/go-metrics-interface v0.0.1
3132
github.com/ipfs/go-peertaskqueue v0.8.1
3233
github.com/ipfs/go-unixfsnode v1.9.0
34+
github.com/ipld/go-car v0.6.2
3335
github.com/ipld/go-car/v2 v2.13.1
3436
github.com/ipld/go-codec-dagpb v1.6.0
3537
github.com/ipld/go-ipld-prime v0.21.0
@@ -103,14 +105,19 @@ require (
103105
github.com/gorilla/websocket v1.5.0 // indirect
104106
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
105107
github.com/hashicorp/errwrap v1.1.0 // indirect
106-
github.com/hashicorp/go-multierror v1.1.1 // indirect
107108
github.com/hashicorp/golang-lru v1.0.2 // indirect
108109
github.com/huin/goupnp v1.3.0 // indirect
110+
github.com/ipfs/go-blockservice v0.5.0 // indirect
111+
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
112+
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
113+
github.com/ipfs/go-ipfs-exchange-interface v0.2.0 // indirect
109114
github.com/ipfs/go-ipfs-pq v0.0.3 // indirect
110115
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
111116
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
112117
github.com/ipfs/go-log v1.0.5 // indirect
118+
github.com/ipfs/go-merkledag v0.11.0 // indirect
113119
github.com/ipfs/go-unixfs v0.4.5 // indirect
120+
github.com/ipfs/go-verifcid v0.0.2 // indirect
114121
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
115122
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
116123
github.com/klauspost/compress v1.17.4 // indirect

‎go.sum

+12
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
137137
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
138138
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
139139
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
140+
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140141
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
141142
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
142143
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -170,17 +171,21 @@ github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
170171
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
171172
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
172173
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
174+
github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
175+
github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk=
173176
github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
174177
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
175178
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
176179
github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY=
177180
github.com/ipfs/go-blockservice v0.5.0/go.mod h1:W6brZ5k20AehbmERplmERn8o2Ni3ZZubvAxaIUeaT6w=
178181
github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
182+
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
179183
github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
180184
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
181185
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
182186
github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q=
183187
github.com/ipfs/go-cidutil v0.1.0/go.mod h1:e7OEVBMIv9JaOxt9zaGEmAoSlXW9jdFZ5lP/0PwcfpA=
188+
github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
184189
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
185190
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
186191
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
@@ -191,6 +196,7 @@ github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IW
191196
github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
192197
github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8=
193198
github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
199+
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
194200
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
195201
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
196202
github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q=
@@ -203,6 +209,8 @@ github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE
203209
github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4=
204210
github.com/ipfs/go-ipfs-redirects-file v0.1.1 h1:Io++k0Vf/wK+tfnhEh63Yte1oQK5VGT2hIEYpD0Rzx8=
205211
github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
212+
github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc=
213+
github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo=
206214
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
207215
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
208216
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
@@ -229,6 +237,8 @@ github.com/ipfs/go-unixfsnode v1.9.0 h1:ubEhQhr22sPAKO2DNsyVBW7YB/zA8Zkif25aBvz8
229237
github.com/ipfs/go-unixfsnode v1.9.0/go.mod h1:HxRu9HYHOjK6HUqFBAi++7DVoWAHn0o4v/nZ/VA+0g8=
230238
github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs=
231239
github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU=
240+
github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc=
241+
github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8=
232242
github.com/ipld/go-car/v2 v2.13.1 h1:KnlrKvEPEzr5IZHKTXLAEub+tPrzeAFQVRlSQvuxBO4=
233243
github.com/ipld/go-car/v2 v2.13.1/go.mod h1:QkdjjFNGit2GIkpQ953KBwowuoukoM75nP/JI1iDJdo=
234244
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
@@ -260,6 +270,7 @@ github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY
260270
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
261271
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
262272
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
273+
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
263274
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
264275
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
265276
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -720,6 +731,7 @@ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7
720731
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
721732
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
722733
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
734+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
723735
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
724736
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
725737
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=

0 commit comments

Comments
 (0)
Please sign in to comment.