Skip to content

Commit fb409c2

Browse files
committed
refactor: cleanup types, names, gateway
1 parent 4da80c7 commit fb409c2

25 files changed

+260
-214
lines changed

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The following emojis are used to highlight certain changes:
1717
### Added
1818

1919
* ✨ The `routing/http` implements Delegated Peer Routing introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417).
20+
* The gateway now sets a `Cache-Control` header for requests under the `/ipns/` namespace
21+
if the TTL for the corresponding IPNS Records or DNSLink entities is known.
2022

2123
### Changed
2224

@@ -27,14 +29,28 @@ The following emojis are used to highlight certain changes:
2729
* `ReadBitswapProviderRecord` has been renamed to `BitswapRecord` and marked as deprecated.
2830
From now on, please use the protocol-agnostic `PeerRecord` for most use cases. The new
2931
Peer Schema has been introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417).
30-
32+
* 🛠 The `namesys` package has been refactored. The following are the largest modifications:
33+
* The options in `coreiface/options/namesys` have been moved to `namesys` and their names
34+
have been made more consistent.
35+
* Many of the exported structs and functions have been renamed in order to be consistent with
36+
the remaining packages.
37+
* `namesys.Resolver.Resolve` now returns a TTL, in addition to the resolved path. If the
38+
TTL is unknown, 0 is returned. `IPNSResolver` is able to resolve a TTL, while `DNSResolver`
39+
is not.
40+
* `namesys/resolver.ResolveIPNS` has been moved to `namesys.ResolveIPNS` and now returns a TTL
41+
in addition to the resolved path.
42+
* 🛠 The `gateway`'s `IPFSBackend.ResolveMutable` is now expected to return a TTL in addition to
43+
the resolved path. If the TTL is unknown, 0 should be returned.
44+
3145
### Removed
3246

3347
* 🛠 The `routing/http` package experienced following removals:
3448
* Server and client no longer support the experimental `Provide` method.
3549
`ProvideBitswap` is still usable, but marked as deprecated. A protocol-agnostic
3650
provide mechanism is being worked on in [IPIP-378](https://github.com/ipfs/specs/pull/378).
3751
* Server no longer exports `FindProvidersPath` and `ProvidePath`.
52+
* 🛠 The `coreiface/options/namesys` package has been removed.
53+
* 🛠 The `namesys.StartSpan` function is no longer exported.
3854

3955
### Fixed
4056

gateway/blocks_backend.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"net/http"
1010
"strings"
11+
"time"
1112

1213
"github.com/ipfs/boxo/blockservice"
1314
blockstore "github.com/ipfs/boxo/blockstore"
@@ -17,8 +18,8 @@ import (
1718
"github.com/ipfs/boxo/ipld/merkledag"
1819
ufile "github.com/ipfs/boxo/ipld/unixfs/file"
1920
uio "github.com/ipfs/boxo/ipld/unixfs/io"
21+
"github.com/ipfs/boxo/ipns"
2022
"github.com/ipfs/boxo/namesys"
21-
"github.com/ipfs/boxo/namesys/resolve"
2223
"github.com/ipfs/boxo/path"
2324
"github.com/ipfs/boxo/path/resolver"
2425
blocks "github.com/ipfs/go-block-format"
@@ -38,7 +39,6 @@ import (
3839
"github.com/ipld/go-ipld-prime/traversal/selector"
3940
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
4041
routinghelpers "github.com/libp2p/go-libp2p-routing-helpers"
41-
"github.com/libp2p/go-libp2p/core/peer"
4242
"github.com/libp2p/go-libp2p/core/routing"
4343
mc "github.com/multiformats/go-multicodec"
4444

@@ -556,18 +556,23 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
556556
return pathRoots, lastPath, nil
557557
}
558558

559-
func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
559+
func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, error) {
560560
switch p.Namespace() {
561561
case path.IPNSNamespace:
562-
p, err := resolve.ResolveIPNS(ctx, bb.namesys, p)
562+
p, ttl, err := namesys.ResolveIPNS(ctx, bb.namesys, p)
563563
if err != nil {
564-
return nil, err
564+
return nil, 0, err
565+
}
566+
ip, err := path.NewImmutablePath(p)
567+
if err != nil {
568+
return nil, 0, err
565569
}
566-
return path.NewImmutablePath(p)
570+
return ip, ttl, nil
567571
case path.IPFSNamespace:
568-
return path.NewImmutablePath(p)
572+
ip, err := path.NewImmutablePath(p)
573+
return ip, 0, err
569574
default:
570-
return nil, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
575+
return nil, 0, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
571576
}
572577
}
573578

@@ -576,19 +581,12 @@ func (bb *BlocksBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte,
576581
return nil, NewErrorStatusCode(errors.New("IPNS Record responses are not supported by this gateway"), http.StatusNotImplemented)
577582
}
578583

579-
// Fails fast if the CID is not an encoded Libp2p Key, avoids wasteful
580-
// round trips to the remote routing provider.
581-
if mc.Code(c.Type()) != mc.Libp2pKey {
582-
return nil, NewErrorStatusCode(errors.New("cid codec must be libp2p-key"), http.StatusBadRequest)
583-
}
584-
585-
// The value store expects the key itself to be encoded as a multihash.
586-
id, err := peer.FromCid(c)
584+
name, err := ipns.NameFromCid(c)
587585
if err != nil {
588-
return nil, err
586+
return nil, NewErrorStatusCode(err, http.StatusBadRequest)
589587
}
590588

591-
return bb.routing.GetValue(ctx, "/ipns/"+string(id))
589+
return bb.routing.GetValue(ctx, string(name.RoutingKey()))
592590
}
593591

594592
func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) {
@@ -628,7 +626,7 @@ func (bb *BlocksBackend) ResolvePath(ctx context.Context, path path.ImmutablePat
628626
func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
629627
var err error
630628
if p.Namespace() == path.IPNSNamespace {
631-
p, err = resolve.ResolveIPNS(ctx, bb.namesys, p)
629+
p, _, err = namesys.ResolveIPNS(ctx, bb.namesys, p)
632630
if err != nil {
633631
return nil, err
634632
}

gateway/gateway.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sort"
99
"strconv"
1010
"strings"
11+
"time"
1112

1213
"github.com/ipfs/boxo/files"
1314
"github.com/ipfs/boxo/gateway/assets"
@@ -303,11 +304,11 @@ type IPFSBackend interface {
303304
GetIPNSRecord(context.Context, cid.Cid) ([]byte, error)
304305

305306
// ResolveMutable takes a mutable path and resolves it into an immutable one. This means recursively resolving any
306-
// DNSLink or IPNS records.
307+
// DNSLink or IPNS records. It should also return a TTL. If the TTL is unknown, 0 should be returned.
307308
//
308309
// For example, given a mapping from `/ipns/dnslink.tld -> /ipns/ipns-id/mydirectory` and `/ipns/ipns-id` to
309310
// `/ipfs/some-cid`, the result of passing `/ipns/dnslink.tld/myfile` would be `/ipfs/some-cid/mydirectory/myfile`.
310-
ResolveMutable(context.Context, path.Path) (path.ImmutablePath, error)
311+
ResolveMutable(context.Context, path.Path) (path.ImmutablePath, time.Duration, error)
311312

312313
// GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN.
313314
// Unlike ResolvePath, it does not perform recursive resolution. It only

gateway/gateway_test.go

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,19 @@ func TestGatewayGet(t *testing.T) {
3737
return p
3838
}
3939

40-
backend.namesys["/ipns/example.com"] = path.NewIPFSPath(k.Cid())
41-
backend.namesys["/ipns/working.example.com"] = k
42-
backend.namesys["/ipns/double.example.com"] = mustMakeDNSLinkPath("working.example.com")
43-
backend.namesys["/ipns/triple.example.com"] = mustMakeDNSLinkPath("double.example.com")
44-
backend.namesys["/ipns/broken.example.com"] = mustMakeDNSLinkPath(k.Cid().String())
40+
backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.NewIPFSPath(k.Cid()), 0)
41+
backend.namesys["/ipns/working.example.com"] = newMockNamesysItem(k, 0)
42+
backend.namesys["/ipns/double.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath("working.example.com"), 0)
43+
backend.namesys["/ipns/triple.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath("double.example.com"), 0)
44+
backend.namesys["/ipns/broken.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath(k.Cid().String()), 0)
4545
// We picked .man because:
4646
// 1. It's a valid TLD.
4747
// 2. Go treats it as the file extension for "man" files (even though
4848
// nobody actually *uses* this extension, AFAIK).
4949
//
5050
// Unfortunately, this may not work on all platforms as file type
5151
// detection is platform dependent.
52-
backend.namesys["/ipns/example.man"] = k
52+
backend.namesys["/ipns/example.man"] = newMockNamesysItem(k, 0)
5353

5454
for _, test := range []struct {
5555
host string
@@ -98,7 +98,7 @@ func TestPretty404(t *testing.T) {
9898
t.Logf("test server url: %s", ts.URL)
9999

100100
host := "example.net"
101-
backend.namesys["/ipns/"+host] = path.NewIPFSPath(root)
101+
backend.namesys["/ipns/"+host] = newMockNamesysItem(path.NewIPFSPath(root), 0)
102102

103103
for _, test := range []struct {
104104
path string
@@ -158,7 +158,56 @@ func TestHeaders(t *testing.T) {
158158
dagCborRoots = dirRoots + "," + dagCborCID
159159
)
160160

161-
t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) {
161+
t.Run("Cache-Control uses TTL for /ipns/ when it is known", func(t *testing.T) {
162+
t.Parallel()
163+
164+
ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
165+
backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.NewIPFSPath(root), time.Second*30)
166+
167+
t.Run("UnixFS generated directory listing without index.html has no Cache-Control", func(t *testing.T) {
168+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/", nil)
169+
res := mustDoWithoutRedirect(t, req)
170+
require.Empty(t, res.Header["Cache-Control"])
171+
})
172+
173+
t.Run("UnixFS directory with index.html has Cache-Control", func(t *testing.T) {
174+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/", nil)
175+
res := mustDoWithoutRedirect(t, req)
176+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
177+
})
178+
179+
t.Run("UnixFS file has Cache-Control", func(t *testing.T) {
180+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/index.html", nil)
181+
res := mustDoWithoutRedirect(t, req)
182+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
183+
})
184+
185+
t.Run("Raw block has Cache-Control", func(t *testing.T) {
186+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=raw", nil)
187+
res := mustDoWithoutRedirect(t, req)
188+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
189+
})
190+
191+
t.Run("DAG-JSON block has Cache-Control", func(t *testing.T) {
192+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-json", nil)
193+
res := mustDoWithoutRedirect(t, req)
194+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
195+
})
196+
197+
t.Run("DAG-CBOR block has Cache-Control", func(t *testing.T) {
198+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-cbor", nil)
199+
res := mustDoWithoutRedirect(t, req)
200+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
201+
})
202+
203+
t.Run("CAR block has Cache-Control", func(t *testing.T) {
204+
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=car", nil)
205+
res := mustDoWithoutRedirect(t, req)
206+
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
207+
})
208+
})
209+
210+
t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) {
162211
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipfs/"+rootCID+"/", nil)
163212
res := mustDoWithoutRedirect(t, req)
164213

@@ -500,7 +549,7 @@ func TestRedirects(t *testing.T) {
500549
t.Parallel()
501550

502551
ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
503-
backend.namesys["/ipns/example.net"] = path.NewIPFSPath(root)
552+
backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
504553

505554
// make request to directory containing index.html
506555
req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo", nil)
@@ -535,7 +584,7 @@ func TestRedirects(t *testing.T) {
535584
t.Parallel()
536585

537586
backend, root := newMockBackend(t, "redirects-spa.car")
538-
backend.namesys["/ipns/example.com"] = path.NewIPFSPath(root)
587+
backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
539588

540589
ts := newTestServerWithConfig(t, backend, Config{
541590
Headers: map[string][]string{},
@@ -672,8 +721,8 @@ func TestDeserializedResponses(t *testing.T) {
672721
t.Parallel()
673722

674723
backend, root := newMockBackend(t, "fixtures.car")
675-
backend.namesys["/ipns/trustless.com"] = path.NewIPFSPath(root)
676-
backend.namesys["/ipns/trusted.com"] = path.NewIPFSPath(root)
724+
backend.namesys["/ipns/trustless.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
725+
backend.namesys["/ipns/trusted.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
677726

678727
ts := newTestServerWithConfig(t, backend, Config{
679728
Headers: map[string][]string{},
@@ -735,8 +784,8 @@ func (mb *errorMockBackend) GetCAR(ctx context.Context, path path.ImmutablePath,
735784
return ContentPathMetadata{}, nil, mb.err
736785
}
737786

738-
func (mb *errorMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
739-
return nil, mb.err
787+
func (mb *errorMockBackend) ResolveMutable(ctx context.Context, path path.Path) (path.ImmutablePath, time.Duration, error) {
788+
return nil, 0, mb.err
740789
}
741790

742791
func (mb *errorMockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
@@ -819,7 +868,7 @@ func (mb *panicMockBackend) GetCAR(ctx context.Context, immutablePath path.Immut
819868
panic("i am panicking")
820869
}
821870

822-
func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
871+
func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, error) {
823872
panic("i am panicking")
824873
}
825874

gateway/handler.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var log = logging.Logger("boxo/gateway")
3434

3535
const (
3636
ipfsPathPrefix = "/ipfs/"
37-
ipnsPathPrefix = "/ipns/"
37+
ipnsPathPrefix = ipns.NamespacePrefix
3838
immutableCacheControl = "public, max-age=29030400, immutable"
3939
)
4040

@@ -188,6 +188,7 @@ type requestData struct {
188188

189189
// Defined for non IPNS Record requests.
190190
immutablePath path.ImmutablePath
191+
ttl time.Duration
191192

192193
// Defined if resolution has already happened.
193194
pathMetadata *ContentPathMetadata
@@ -279,7 +280,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
279280
}
280281

281282
if contentPath.Namespace().Mutable() {
282-
rq.immutablePath, err = i.backend.ResolveMutable(r.Context(), contentPath)
283+
rq.immutablePath, rq.ttl, err = i.backend.ResolveMutable(r.Context(), contentPath)
283284
if err != nil {
284285
err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err)
285286
i.webError(w, r, err, http.StatusInternalServerError)
@@ -409,32 +410,30 @@ func panicHandler(w http.ResponseWriter) {
409410
}
410411
}
411412

412-
func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath path.Path, cid cid.Cid, responseFormat string) (modtime time.Time) {
413+
func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath path.Path, ttl time.Duration, cid cid.Cid, responseFormat string) (modtime time.Time) {
413414
// Best effort attempt to set an Etag based on the CID and response format.
414415
// Setting an ETag is handled separately for CARs and IPNS records.
415416
if etag := getEtag(r, cid, responseFormat); etag != "" {
416417
w.Header().Set("Etag", etag)
417418
}
418419

419-
// Set Cache-Control and Last-Modified based on contentPath properties
420+
// Set Cache-Control and Last-Modified based on contentPath properties.
420421
if contentPath.Namespace().Mutable() {
421-
// mutable namespaces such as /ipns/ can't be cached forever
422-
423-
// For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers:
424-
// https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768
425-
// but we should not set it to fake values and use Cache-Control based on TTL instead
426-
modtime = time.Now()
427-
428-
// TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/kubo/issues/1818#issuecomment-1015849462
429-
// TODO: set Last-Modified based on /ipns/ publishing timestamp?
422+
if ttl > 0 {
423+
// When we know the TTL, set the Cache-Control header and disable Last-Modified.
424+
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(ttl.Seconds())))
425+
modtime = noModtime
426+
} else {
427+
// Otherwise, we set Last-Modified to the current time to leverage caching heuristics
428+
// built into modern browsers: https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768
429+
modtime = time.Now()
430+
}
430431
} else {
431-
// immutable! CACHE ALL THE THINGS, FOREVER! wolololol
432432
w.Header().Set("Cache-Control", immutableCacheControl)
433+
modtime = noModtime // disable Last-Modified
433434

434-
// Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control)
435-
modtime = noModtime
436-
437-
// TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/kubo/issues/6920?
435+
// TODO: consider setting Last-Modified if UnixFS V1.5 ever gets released
436+
// with metadata: https://github.com/ipfs/kubo/issues/6920
438437
}
439438

440439
return modtime

gateway/handler_block.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h
3434
setContentDispositionHeader(w, name, "attachment")
3535

3636
// Set remaining headers
37-
modtime := addCacheControlHeaders(w, r, rq.contentPath, blockCid, rawResponseFormat)
37+
modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, blockCid, rawResponseFormat)
3838
w.Header().Set("Content-Type", rawResponseFormat)
3939
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
4040

gateway/handler_car.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
5757
setContentDispositionHeader(w, name, "attachment")
5858

5959
// Set Cache-Control (same logic as for a regular files)
60-
addCacheControlHeaders(w, r, rq.contentPath, rootCid, carResponseFormat)
60+
addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, rootCid, carResponseFormat)
6161

6262
// Generate the CAR Etag.
6363
etag := getCarEtag(rq.immutablePath, params, rootCid)

gateway/handler_codec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt
104104
}
105105

106106
// Set HTTP headers (for caching, etc). Etag will be replaced if handled by serveCodecHTML.
107-
modtime := addCacheControlHeaders(w, r, rq.contentPath, resolvedPath.Cid(), responseContentType)
107+
modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, resolvedPath.Cid(), responseContentType)
108108
name := setCodecContentDisposition(w, r, resolvedPath, responseContentType)
109109
w.Header().Set("Content-Type", responseContentType)
110110
w.Header().Set("X-Content-Type-Options", "nosniff")

0 commit comments

Comments
 (0)