From 4daa75dc323e9ac1961fa665a26d19207aca57f5 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 4 Sep 2023 21:03:51 +0200 Subject: [PATCH] refactor: cleanup types, names, gateway --- CHANGELOG.md | 18 ++++++- gateway/blocks_backend.go | 37 ++++++------- gateway/gateway.go | 5 +- gateway/gateway_test.go | 79 ++++++++++++++++++++++------ gateway/handler.go | 35 ++++++------ gateway/handler_block.go | 2 +- gateway/handler_car.go | 2 +- gateway/handler_codec.go | 2 +- gateway/handler_defaults.go | 4 +- gateway/handler_tar.go | 2 +- gateway/handler_unixfs__redirects.go | 2 +- gateway/handler_unixfs_dir.go | 4 +- gateway/handler_unixfs_dir_test.go | 2 +- gateway/handler_unixfs_file.go | 4 +- gateway/hostname_test.go | 4 +- gateway/metrics.go | 6 +-- gateway/utilities_test.go | 22 +++++--- namesys/interface.go | 8 ++- namesys/ipns_publisher.go | 73 ++++++++++++------------- namesys/ipns_publisher_test.go | 4 +- namesys/ipns_resolver.go | 66 +++++++++++++++++------ namesys/ipns_resolver_test.go | 4 +- namesys/namesys.go | 12 ++--- namesys/namesys_cache.go | 14 ++--- namesys/resolve/resolve.go | 65 ----------------------- 25 files changed, 258 insertions(+), 218 deletions(-) delete mode 100644 namesys/resolve/resolve.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4122902c..02b5eda2a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The following emojis are used to highlight certain changes: ### Added * ✨ The `routing/http` implements Delegated Peer Routing introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417). +* The gateway now sets a `Cache-Control` header for requests under the `/ipns/` namespace + if the TTL for the corresponding IPNS Records or DNSLink entities is known. ### Changed @@ -27,7 +29,19 @@ The following emojis are used to highlight certain changes: * `ReadBitswapProviderRecord` has been renamed to `BitswapRecord` and marked as deprecated. From now on, please use the protocol-agnostic `PeerRecord` for most use cases. The new Peer Schema has been introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417). - +* 🛠 The `namesys` package has been refactored. The following are the largest modifications: + * The options in `coreiface/options/namesys` have been moved to `namesys` and their names + have been made more consistent. + * Many of the exported structs and functions have been renamed in order to be consistent with + the remaining packages. + * `namesys.Resolver.Resolve` now returns a TTL, in addition to the resolved path. If the + TTL is unknown, 0 is returned. `IPNSResolver` is able to resolve a TTL, while `DNSResolver` + is not. + * `namesys/resolver.ResolveIPNS` has been moved to `namesys.ResolveIPNS` and now returns a TTL + in addition to the resolved path. +* 🛠 The `gateway`'s `IPFSBackend.ResolveMutable` is now expected to return a TTL in addition to + the resolved path. If the TTL is unknown, 0 should be returned. + ### Removed * 🛠 The `routing/http` package experienced following removals: @@ -35,6 +49,8 @@ The following emojis are used to highlight certain changes: `ProvideBitswap` is still usable, but marked as deprecated. A protocol-agnostic provide mechanism is being worked on in [IPIP-378](https://github.com/ipfs/specs/pull/378). * Server no longer exports `FindProvidersPath` and `ProvidePath`. +* 🛠 The `coreiface/options/namesys` package has been removed. +* 🛠 The `namesys.StartSpan` function is no longer exported. ### Fixed diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index 8f582e50ff..5625750383 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -9,6 +9,7 @@ import ( "net/http" gopath "path" "strings" + "time" "github.com/ipfs/boxo/blockservice" blockstore "github.com/ipfs/boxo/blockstore" @@ -19,8 +20,8 @@ import ( "github.com/ipfs/boxo/ipld/merkledag" ufile "github.com/ipfs/boxo/ipld/unixfs/file" uio "github.com/ipfs/boxo/ipld/unixfs/io" + "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/namesys" - "github.com/ipfs/boxo/namesys/resolve" ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" blocks "github.com/ipfs/go-block-format" @@ -40,7 +41,6 @@ import ( "github.com/ipld/go-ipld-prime/traversal/selector" selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" - "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" mc "github.com/multiformats/go-multicodec" @@ -556,32 +556,32 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath Immutable return pathRoots, lastPath, nil } -func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p ifacepath.Path) (ImmutablePath, error) { +func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p ifacepath.Path) (ImmutablePath, time.Duration, error) { err := p.IsValid() if err != nil { - return ImmutablePath{}, err + return ImmutablePath{}, 0, err } ipath := ipfspath.Path(p.String()) switch ipath.Segments()[0] { case "ipns": - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) + ipath, ttl, err := namesys.ResolveIPNS(ctx, bb.namesys, ipath) if err != nil { - return ImmutablePath{}, err + return ImmutablePath{}, 0, err } imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) if err != nil { - return ImmutablePath{}, err + return ImmutablePath{}, 0, err } - return imPath, nil + return imPath, ttl, nil case "ipfs": imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) if err != nil { - return ImmutablePath{}, err + return ImmutablePath{}, 0, err } - return imPath, nil + return imPath, 0, nil default: - return ImmutablePath{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) + return ImmutablePath{}, 0, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) } } @@ -590,19 +590,12 @@ func (bb *BlocksBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, return nil, NewErrorStatusCode(errors.New("IPNS Record responses are not supported by this gateway"), http.StatusNotImplemented) } - // Fails fast if the CID is not an encoded Libp2p Key, avoids wasteful - // round trips to the remote routing provider. - if mc.Code(c.Type()) != mc.Libp2pKey { - return nil, NewErrorStatusCode(errors.New("cid codec must be libp2p-key"), http.StatusBadRequest) - } - - // The value store expects the key itself to be encoded as a multihash. - id, err := peer.FromCid(c) + name, err := ipns.NameFromCid(c) if err != nil { - return nil, err + return nil, NewErrorStatusCode(err, http.StatusBadRequest) } - return bb.routing.GetValue(ctx, "/ipns/"+string(id)) + return bb.routing.GetValue(ctx, string(name.RoutingKey())) } func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ifacepath.Path, error) { @@ -651,7 +644,7 @@ func (bb *BlocksBackend) resolvePath(ctx context.Context, p ifacepath.Path) (ifa ipath := ipfspath.Path(p.String()) if ipath.Segments()[0] == "ipns" { - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) + ipath, _, err = namesys.ResolveIPNS(ctx, bb.namesys, ipath) if err != nil { return nil, err } diff --git a/gateway/gateway.go b/gateway/gateway.go index 780691a452..c30eeb1baa 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" @@ -335,11 +336,11 @@ type IPFSBackend interface { GetIPNSRecord(context.Context, cid.Cid) ([]byte, error) // ResolveMutable takes a mutable path and resolves it into an immutable one. This means recursively resolving any - // DNSLink or IPNS records. + // DNSLink or IPNS records. It should also return a TTL. If the TTL is unknown, 0 should be returned. // // For example, given a mapping from `/ipns/dnslink.tld -> /ipns/ipns-id/mydirectory` and `/ipns/ipns-id` to // `/ipfs/some-cid`, the result of passing `/ipns/dnslink.tld/myfile` would be `/ipfs/some-cid/mydirectory/myfile`. - ResolveMutable(context.Context, path.Path) (ImmutablePath, error) + ResolveMutable(context.Context, path.Path) (ImmutablePath, time.Duration, error) // GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN. // Unlike ResolvePath, it does not perform recursive resolution. It only diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 98996acb30..c31c263754 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -29,11 +29,11 @@ func TestGatewayGet(t *testing.T) { k, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "subdir", "fnord")) require.NoError(t, err) - backend.namesys["/ipns/example.com"] = path.FromCid(k.Cid()) - backend.namesys["/ipns/working.example.com"] = path.FromString(k.String()) - backend.namesys["/ipns/double.example.com"] = path.FromString("/ipns/working.example.com") - backend.namesys["/ipns/triple.example.com"] = path.FromString("/ipns/double.example.com") - backend.namesys["/ipns/broken.example.com"] = path.FromString("/ipns/" + k.Cid().String()) + backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.FromCid(k.Cid()), 0) + backend.namesys["/ipns/working.example.com"] = newMockNamesysItem(path.FromString(k.String()), 0) + backend.namesys["/ipns/double.example.com"] = newMockNamesysItem(path.FromString("/ipns/working.example.com"), 0) + backend.namesys["/ipns/triple.example.com"] = newMockNamesysItem(path.FromString("/ipns/double.example.com"), 0) + backend.namesys["/ipns/broken.example.com"] = newMockNamesysItem(path.FromString("/ipns/"+k.Cid().String()), 0) // We picked .man because: // 1. It's a valid TLD. // 2. Go treats it as the file extension for "man" files (even though @@ -41,7 +41,7 @@ func TestGatewayGet(t *testing.T) { // // Unfortunately, this may not work on all platforms as file type // detection is platform dependent. - backend.namesys["/ipns/example.man"] = path.FromString(k.String()) + backend.namesys["/ipns/example.man"] = newMockNamesysItem(path.FromString(k.String()), 0) for _, test := range []struct { host string @@ -90,7 +90,7 @@ func TestPretty404(t *testing.T) { t.Logf("test server url: %s", ts.URL) host := "example.net" - backend.namesys["/ipns/"+host] = path.FromCid(root) + backend.namesys["/ipns/"+host] = newMockNamesysItem(path.FromCid(root), 0) for _, test := range []struct { path string @@ -150,7 +150,56 @@ func TestHeaders(t *testing.T) { dagCborRoots = dirRoots + "," + dagCborCID ) - t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) { + t.Run("Cache-Control uses TTL for /ipns/ when it is known", func(t *testing.T) { + t.Parallel() + + ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car") + backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.FromCid(root), time.Second*30) + + t.Run("UnixFS generated directory listing without index.html has no Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/", nil) + res := mustDoWithoutRedirect(t, req) + require.Empty(t, res.Header["Cache-Control"]) + }) + + t.Run("UnixFS directory with index.html has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + + t.Run("UnixFS file has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/index.html", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + + t.Run("Raw block has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=raw", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + + t.Run("DAG-JSON block has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-json", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + + t.Run("DAG-CBOR block has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-cbor", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + + t.Run("CAR block has Cache-Control", func(t *testing.T) { + req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=car", nil) + res := mustDoWithoutRedirect(t, req) + require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control")) + }) + }) + + t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) { req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipfs/"+rootCID+"/", nil) res := mustDoWithoutRedirect(t, req) @@ -492,7 +541,7 @@ func TestRedirects(t *testing.T) { t.Parallel() ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car") - backend.namesys["/ipns/example.net"] = path.FromCid(root) + backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.FromCid(root), 0) // make request to directory containing index.html req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo", nil) @@ -527,7 +576,7 @@ func TestRedirects(t *testing.T) { t.Parallel() backend, root := newMockBackend(t, "redirects-spa.car") - backend.namesys["/ipns/example.com"] = path.FromCid(root) + backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.FromCid(root), 0) ts := newTestServerWithConfig(t, backend, Config{ Headers: map[string][]string{}, @@ -664,8 +713,8 @@ func TestDeserializedResponses(t *testing.T) { t.Parallel() backend, root := newMockBackend(t, "fixtures.car") - backend.namesys["/ipns/trustless.com"] = path.FromCid(root) - backend.namesys["/ipns/trusted.com"] = path.FromCid(root) + backend.namesys["/ipns/trustless.com"] = newMockNamesysItem(path.FromCid(root), 0) + backend.namesys["/ipns/trusted.com"] = newMockNamesysItem(path.FromCid(root), 0) ts := newTestServerWithConfig(t, backend, Config{ Headers: map[string][]string{}, @@ -727,8 +776,8 @@ func (mb *errorMockBackend) GetCAR(ctx context.Context, path ImmutablePath, para return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) ResolveMutable(ctx context.Context, path ipath.Path) (ImmutablePath, error) { - return ImmutablePath{}, mb.err +func (mb *errorMockBackend) ResolveMutable(ctx context.Context, path ipath.Path) (ImmutablePath, time.Duration, error) { + return ImmutablePath{}, 0, mb.err } func (mb *errorMockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) { @@ -811,7 +860,7 @@ func (mb *panicMockBackend) GetCAR(ctx context.Context, immutablePath ImmutableP panic("i am panicking") } -func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, time.Duration, error) { panic("i am panicking") } diff --git a/gateway/handler.go b/gateway/handler.go index ecf5056173..90a38818e3 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -34,7 +34,7 @@ var log = logging.Logger("boxo/gateway") const ( ipfsPathPrefix = "/ipfs/" - ipnsPathPrefix = "/ipns/" + ipnsPathPrefix = ipns.NamespacePrefix immutableCacheControl = "public, max-age=29030400, immutable" ) @@ -188,6 +188,7 @@ type requestData struct { // Defined for non IPNS Record requests. immutablePath ImmutablePath + ttl time.Duration // Defined if resolution has already happened. pathMetadata *ContentPathMetadata @@ -279,7 +280,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { } if contentPath.Mutable() { - rq.immutablePath, err = i.backend.ResolveMutable(r.Context(), contentPath) + rq.immutablePath, rq.ttl, err = i.backend.ResolveMutable(r.Context(), contentPath) if err != nil { err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusInternalServerError) @@ -409,32 +410,30 @@ func panicHandler(w http.ResponseWriter) { } } -func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, cid cid.Cid, responseFormat string) (modtime time.Time) { +func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, ttl time.Duration, cid cid.Cid, responseFormat string) (modtime time.Time) { // Best effort attempt to set an Etag based on the CID and response format. // Setting an ETag is handled separately for CARs and IPNS records. if etag := getEtag(r, cid, responseFormat); etag != "" { w.Header().Set("Etag", etag) } - // Set Cache-Control and Last-Modified based on contentPath properties + // Set Cache-Control and Last-Modified based on contentPath properties. if contentPath.Mutable() { - // mutable namespaces such as /ipns/ can't be cached forever - - // For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers: - // https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768 - // but we should not set it to fake values and use Cache-Control based on TTL instead - modtime = time.Now() - - // TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/kubo/issues/1818#issuecomment-1015849462 - // TODO: set Last-Modified based on /ipns/ publishing timestamp? + if ttl > 0 { + // When we know the TTL, set the Cache-Control header and disable Last-Modified. + w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(ttl.Seconds()))) + modtime = noModtime + } else { + // Otherwise, we set Last-Modified to the current time to leverage caching heuristics + // built into modern browsers: https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768 + modtime = time.Now() + } } else { - // immutable! CACHE ALL THE THINGS, FOREVER! wolololol w.Header().Set("Cache-Control", immutableCacheControl) + modtime = noModtime // disable Last-Modified - // Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control) - modtime = noModtime - - // TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/kubo/issues/6920? + // TODO: consider setting Last-Modified if UnixFS V1.5 ever gets released + // with metadata: https://github.com/ipfs/kubo/issues/6920 } return modtime diff --git a/gateway/handler_block.go b/gateway/handler_block.go index dbff9a7ad3..83fb9425e3 100644 --- a/gateway/handler_block.go +++ b/gateway/handler_block.go @@ -34,7 +34,7 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h setContentDispositionHeader(w, name, "attachment") // Set remaining headers - modtime := addCacheControlHeaders(w, r, rq.contentPath, blockCid, rawResponseFormat) + modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, blockCid, rawResponseFormat) w.Header().Set("Content-Type", rawResponseFormat) w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) diff --git a/gateway/handler_car.go b/gateway/handler_car.go index 000e0dc9c1..95a7763f33 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -56,7 +56,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R setContentDispositionHeader(w, name, "attachment") // Set Cache-Control (same logic as for a regular files) - addCacheControlHeaders(w, r, rq.contentPath, rootCid, carResponseFormat) + addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, rootCid, carResponseFormat) // Generate the CAR Etag. etag := getCarEtag(rq.immutablePath, params, rootCid) diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go index 007a52fda3..fb3bb7e82a 100644 --- a/gateway/handler_codec.go +++ b/gateway/handler_codec.go @@ -104,7 +104,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt } // Set HTTP headers (for caching, etc). Etag will be replaced if handled by serveCodecHTML. - modtime := addCacheControlHeaders(w, r, rq.contentPath, resolvedPath.Cid(), responseContentType) + modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, resolvedPath.Cid(), responseContentType) name := setCodecContentDisposition(w, r, resolvedPath, responseContentType) w.Header().Set("Content-Type", responseContentType) w.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/gateway/handler_defaults.go b/gateway/handler_defaults.go index de31c1fc1b..edfc0f993c 100644 --- a/gateway/handler_defaults.go +++ b/gateway/handler_defaults.go @@ -128,13 +128,13 @@ func (i *handler) serveDefaults(ctx context.Context, w http.ResponseWriter, r *h // Handling Unixfs file if bytesResponse != nil { rq.logger.Debugw("serving unixfs file", "path", rq.contentPath) - return i.serveFile(ctx, w, r, resolvedPath, rq.contentPath, bytesResponse, pathMetadata.ContentType, rq.begin) + return i.serveFile(ctx, w, r, resolvedPath, rq.contentPath, rq.ttl, bytesResponse, pathMetadata.ContentType, rq.begin) } // Handling Unixfs directory if directoryMetadata != nil || isDirectoryHeadRequest { rq.logger.Debugw("serving unixfs directory", "path", rq.contentPath) - return i.serveDirectory(ctx, w, r, resolvedPath, rq.contentPath, isDirectoryHeadRequest, directoryMetadata, ranges, rq.begin, rq.logger) + return i.serveDirectory(ctx, w, r, resolvedPath, rq.contentPath, rq.ttl, isDirectoryHeadRequest, directoryMetadata, ranges, rq.begin, rq.logger) } i.webError(w, r, fmt.Errorf("unsupported UnixFS type"), http.StatusInternalServerError) diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go index 784e519939..dbd1fde313 100644 --- a/gateway/handler_tar.go +++ b/gateway/handler_tar.go @@ -31,7 +31,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R rootCid := pathMetadata.LastSegment.Cid() // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, rq.contentPath, rootCid, tarResponseFormat) + modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, rootCid, tarResponseFormat) // Set Content-Disposition var name string diff --git a/gateway/handler_unixfs__redirects.go b/gateway/handler_unixfs__redirects.go index a96b87d364..cca25dd1df 100644 --- a/gateway/handler_unixfs__redirects.go +++ b/gateway/handler_unixfs__redirects.go @@ -203,7 +203,7 @@ func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPat logger.Debugf("using _redirects: custom %d file at %q", status, content4xxPath) w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - addCacheControlHeaders(w, r, content4xxPath, content4xxCid, "") + addCacheControlHeaders(w, r, content4xxPath, 0, content4xxCid, "") w.WriteHeader(status) _, err = io.CopyN(w, content4xxFile, size) return err diff --git a/gateway/handler_unixfs_dir.go b/gateway/handler_unixfs_dir.go index 2808cfdc40..7787117f2d 100644 --- a/gateway/handler_unixfs_dir.go +++ b/gateway/handler_unixfs_dir.go @@ -23,7 +23,7 @@ import ( // serveDirectory returns the best representation of UnixFS directory // // It will return index.html if present, or generate directory listing otherwise. -func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, isHeadRequest bool, directoryMetadata *directoryMetadata, ranges []ByteRange, begin time.Time, logger *zap.SugaredLogger) bool { +func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, ttl time.Duration, isHeadRequest bool, directoryMetadata *directoryMetadata, ranges []ByteRange, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "Handler.ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() @@ -94,7 +94,7 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * if err == nil { logger.Debugw("serving index.html file", "path", idxPath) // write to request - success := i.serveFile(ctx, w, r, resolvedPath, idxPath, idxFile, "text/html", begin) + success := i.serveFile(ctx, w, r, resolvedPath, idxPath, ttl, idxFile, "text/html", begin) if success { i.unixfsDirIndexGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) } diff --git a/gateway/handler_unixfs_dir_test.go b/gateway/handler_unixfs_dir_test.go index a8ce04778f..891c1fb6a7 100644 --- a/gateway/handler_unixfs_dir_test.go +++ b/gateway/handler_unixfs_dir_test.go @@ -25,7 +25,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) { k3, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "foo? #<'/bar")) require.NoError(t, err) - backend.namesys["/ipns/example.net"] = path.FromCid(root) + backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.FromCid(root), 0) // make request to directory listing req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo%3F%20%23%3C%27/", nil) diff --git a/gateway/handler_unixfs_file.go b/gateway/handler_unixfs_file.go index cd924e5aa5..6136daf33e 100644 --- a/gateway/handler_unixfs_file.go +++ b/gateway/handler_unixfs_file.go @@ -19,12 +19,12 @@ import ( // serveFile returns data behind a file along with HTTP headers based on // the file itself, its CID and the contentPath used for accessing it. -func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, fileContentType string, begin time.Time) bool { +func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, ttl time.Duration, file files.File, fileContentType string, begin time.Time) bool { _, span := spanTrace(ctx, "Handler.ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid(), "") + modtime := addCacheControlHeaders(w, r, contentPath, ttl, resolvedPath.Cid(), "") // Set Content-Disposition name := addContentDispositionHeader(w, r, contentPath) diff --git a/gateway/hostname_test.go b/gateway/hostname_test.go index a58e0d4040..b2ce7fd7c0 100644 --- a/gateway/hostname_test.go +++ b/gateway/hostname_test.go @@ -20,8 +20,8 @@ func TestToSubdomainURL(t *testing.T) { testCID, err := cid.Decode("bafkqaglimvwgy3zakrsxg5cun5jxkyten5wwc2lokvjeycq") require.NoError(t, err) - backend.namesys["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String()) - backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String()) + backend.namesys["/ipns/dnslink.long-name.example.com"] = newMockNamesysItem(path.FromString(testCID.String()), 0) + backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = newMockNamesysItem(path.FromString(testCID.String()), 0) httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil) httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil) httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil) diff --git a/gateway/metrics.go b/gateway/metrics.go index 69e81425fc..cce2ec57d3 100644 --- a/gateway/metrics.go +++ b/gateway/metrics.go @@ -155,16 +155,16 @@ func (b *ipfsBackendWithMetrics) GetIPNSRecord(ctx context.Context, cid cid.Cid) return r, err } -func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path path.Path) (ImmutablePath, error) { +func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path path.Path) (ImmutablePath, time.Duration, error) { begin := time.Now() name := "IPFSBackend.ResolveMutable" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) defer span.End() - p, err := b.backend.ResolveMutable(ctx, path) + p, ttl, err := b.backend.ResolveMutable(ctx, path) b.updateBackendCallMetric(name, err, begin) - return p, err + return p, ttl, err } func (b *ipfsBackendWithMetrics) GetDNSLinkRecord(ctx context.Context, fqdn string) (path.Path, error) { diff --git a/gateway/utilities_test.go b/gateway/utilities_test.go index 0a06505cc6..3caf04b41f 100644 --- a/gateway/utilities_test.go +++ b/gateway/utilities_test.go @@ -52,7 +52,16 @@ func mustDo(t *testing.T, req *http.Request) *http.Response { return res } -type mockNamesys map[string]path.Path +type mockNamesysItem struct { + path path.Path + ttl time.Duration +} + +func newMockNamesysItem(p path.Path, ttl time.Duration) *mockNamesysItem { + return &mockNamesysItem{path: p, ttl: ttl} +} + +type mockNamesys map[string]*mockNamesysItem func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...namesys.ResolveOption) (value path.Path, ttl time.Duration, err error) { cfg := namesys.DefaultResolveOptions() @@ -70,14 +79,15 @@ func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...namesys.R } depth-- - var ok bool - value, ok = m[name] + v, ok := m[name] if !ok { return "", 0, namesys.ErrResolveFailed } + value = v.path + ttl = v.ttl name = value.String() } - return value, 0, nil + return value, ttl, nil } func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...namesys.ResolveOption) <-chan namesys.ResolveResult { @@ -153,7 +163,7 @@ func (mb *mockBackend) GetCAR(ctx context.Context, immutablePath ImmutablePath, return mb.gw.GetCAR(ctx, immutablePath, params) } -func (mb *mockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (mb *mockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, time.Duration, error) { return mb.gw.ResolveMutable(ctx, p) } @@ -185,7 +195,7 @@ func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip ipath. var imPath ImmutablePath var err error if ip.Mutable() { - imPath, err = mb.ResolveMutable(ctx, ip) + imPath, _, err = mb.ResolveMutable(ctx, ip) if err != nil { return nil, err } diff --git a/namesys/interface.go b/namesys/interface.go index 9133cf9c58..f70eee3ed6 100644 --- a/namesys/interface.go +++ b/namesys/interface.go @@ -18,6 +18,9 @@ var ( // ErrResolveRecursion signals a recursion-depth limit. ErrResolveRecursion = errors.New("could not resolve name (recursion limit exceeded)") + + // ErrNoNamesys is an explicit error for when no [NameSystem] is provided. + ErrNoNamesys = errors.New("no namesys has been provided") ) const ( @@ -63,8 +66,9 @@ type ResolveResult struct { // Resolver is an object capable of resolving names. type Resolver interface { - // Resolve performs a recursive lookup, returning the dereferenced path. For example, - // if example.com has a DNS TXT record pointing to: + // Resolve performs a recursive lookup, returning the dereferenced path and the TTL. + // If the TTL is unknown, then a TTL of 0 is returned. For example, if example.com + // has a DNS TXT record pointing to: // // /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy // diff --git a/namesys/ipns_publisher.go b/namesys/ipns_publisher.go index ef058138ec..6510563229 100644 --- a/namesys/ipns_publisher.go +++ b/namesys/ipns_publisher.go @@ -2,6 +2,7 @@ package namesys import ( "context" + "errors" "strings" "sync" "time" @@ -50,7 +51,7 @@ func (p *IPNSPublisher) Publish(ctx context.Context, priv crypto.PrivKey, value return err } - return PublishRecord(ctx, p.routing, priv.GetPublic(), record) + return PublishIPNSRecord(ctx, p.routing, priv.GetPublic(), record) } // IpnsDsKey returns a datastore key given an IPNS identifier (peer @@ -155,9 +156,9 @@ func (p *IPNSPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu return nil, err } - seqno := uint64(0) + seq := uint64(0) if rec != nil { - seqno, err = rec.Sequence() + seq, err = rec.Sequence() if err != nil { return nil, err } @@ -169,14 +170,14 @@ func (p *IPNSPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu if value != path.Path(p.String()) { // Don't bother incrementing the sequence number unless the // value changes. - seqno++ + seq++ } } opts := ProcessPublishOptions(options) // Create record - r, err := ipns.NewRecord(k, value, seqno, opts.EOL, opts.TTL, ipns.WithV1Compatibility(opts.CompatibleWithV1)) + r, err := ipns.NewRecord(k, value, seq, opts.EOL, opts.TTL, ipns.WithV1Compatibility(opts.CompatibleWithV1)) if err != nil { return nil, err } @@ -194,13 +195,14 @@ func (p *IPNSPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu if err := p.ds.Sync(ctx, key); err != nil { return nil, err } + return r, nil } -// PublishRecord publishes the given entry using the provided ValueStore, -// keyed on the ID associated with the provided public key. The public key is -// also made available to the routing system so that entries can be verified. -func PublishRecord(ctx context.Context, r routing.ValueStore, k crypto.PubKey, rec *ipns.Record) error { +// PublishIPNSRecord publishes the given [ipns.Record] for the provided [crypto.PubKey] in +// the provided [routing.ValueStore]. The public key is also made available to the routing +// system if it cannot be derived from the corresponding [peer.ID]. +func PublishIPNSRecord(ctx context.Context, r routing.ValueStore, pubKey crypto.PubKey, rec *ipns.Record) error { ctx, span := startSpan(ctx, "PutRecordToRouting") defer span.End() @@ -209,25 +211,22 @@ func PublishRecord(ctx context.Context, r routing.ValueStore, k crypto.PubKey, r errs := make(chan error, 2) // At most two errors (IPNS, and public key) - id, err := peer.IDFromPublicKey(k) + pid, err := peer.IDFromPublicKey(pubKey) if err != nil { return err } go func() { - errs <- PublishIPNSRecord(ctx, r, ipns.NameFromPeer(id), rec) + errs <- PutIPNSRecord(ctx, r, ipns.NameFromPeer(pid), rec) }() - // Publish the public key if a public key cannot be extracted from the ID - // TODO: once v0.4.16 is widespread enough, we can stop doing this - // and at that point we can even deprecate the /pk/ namespace in the dht - // - // NOTE: This check actually checks if the public key has been embedded - // in the IPNS entry. This check is sufficient because we embed the - // public key in the IPNS entry if it can't be extracted from the ID. - if _, err := rec.PubKey(); err == nil { + // Publish the public key if the public key cannot be extracted from the peer ID. + // This is most likely not necessary since IPNS Records include, by default, the public + // key in those cases. However, this ensures it's still possible to easily retrieve + // the public key if, for some reason, it is not embedded. + if _, err := pid.ExtractPublicKey(); errors.Is(err, peer.ErrNoPublicKey) { go func() { - errs <- PublishPublicKey(ctx, r, PkKeyForID(id), k) + errs <- PutPublicKey(ctx, r, pid, pubKey) }() if err := waitOnErrChan(ctx, errs); err != nil { @@ -247,39 +246,37 @@ func waitOnErrChan(ctx context.Context, errs chan error) error { } } -// PublishPublicKey stores the given [crypto.PubKey] for the given key in the [routing.ValueStore]. -func PublishPublicKey(ctx context.Context, r routing.ValueStore, key string, pubKey crypto.PubKey) error { - ctx, span := startSpan(ctx, "PublishPublicKey", trace.WithAttributes(attribute.String("Key", key))) +// PutPublicKey puts the given [crypto.PubKey] for the given [peer.ID] in the [routing.ValueStore]. +func PutPublicKey(ctx context.Context, r routing.ValueStore, pid peer.ID, pubKey crypto.PubKey) error { + routingKey := PkRoutingKey(pid) + ctx, span := startSpan(ctx, "PublishPublicKey", trace.WithAttributes(attribute.String("Key", routingKey))) defer span.End() - log.Debugf("Storing pubkey at: %q", key) bytes, err := crypto.MarshalPublicKey(pubKey) if err != nil { return err } - // Store associated public key - return r.PutValue(ctx, key, bytes) + log.Debugf("Storing public key at: %x", routingKey) + return r.PutValue(ctx, routingKey, bytes) } -// PublishIPNSRecord stores the given [ipns.Record] for the given [ipns.Name] in the given [routing.ValueStore]. -func PublishIPNSRecord(ctx context.Context, r routing.ValueStore, name ipns.Name, rec *ipns.Record) error { - routingKey := string(name.RoutingKey()) +// PkRoutingKey returns the public key routing key for the given [peer.ID]. +func PkRoutingKey(id peer.ID) string { + return "/pk/" + string(id) +} +// PutIPNSRecord puts the given [ipns.Record] for the given [ipns.Name] in the [routing.ValueStore]. +func PutIPNSRecord(ctx context.Context, r routing.ValueStore, name ipns.Name, rec *ipns.Record) error { + routingKey := string(name.RoutingKey()) ctx, span := startSpan(ctx, "PublishEntry", trace.WithAttributes(attribute.String("IPNSKey", routingKey))) defer span.End() - data, err := ipns.MarshalRecord(rec) + bytes, err := ipns.MarshalRecord(rec) if err != nil { return err } - log.Debugf("Storing ipns entry at: %x", routingKey) - // Store ipns entry at "/ipns/"+h(pubkey) - return r.PutValue(ctx, routingKey, data) -} - -// PkKeyForID returns the public key routing key for the given [peer.ID]. -func PkKeyForID(id peer.ID) string { - return "/pk/" + string(id) + log.Debugf("Storing ipns record at: %x", routingKey) + return r.PutValue(ctx, routingKey, bytes) } diff --git a/namesys/ipns_publisher_test.go b/namesys/ipns_publisher_test.go index ceb959f122..ae1c92d039 100644 --- a/namesys/ipns_publisher_test.go +++ b/namesys/ipns_publisher_test.go @@ -68,11 +68,11 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected rec, err := ipns.NewRecord(privKey, value, seq, eol, 0) require.NoError(t, err) - err = PublishRecord(ctx, r, pubKey, rec) + err = PublishIPNSRecord(ctx, r, pubKey, rec) require.NoError(t, err) // Check for namekey existence in value store - namekey := PkKeyForID(id) + namekey := PkRoutingKey(id) _, err = r.GetValue(ctx, namekey) require.ErrorIs(t, err, expectedErr) diff --git a/namesys/ipns_resolver.go b/namesys/ipns_resolver.go index be4b0412ba..a81a9c57ff 100644 --- a/namesys/ipns_resolver.go +++ b/namesys/ipns_resolver.go @@ -2,6 +2,7 @@ package namesys import ( "context" + "fmt" "strings" "time" @@ -9,7 +10,6 @@ import ( "github.com/ipfs/boxo/path" dht "github.com/libp2p/go-libp2p-kad-dht" - "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -47,12 +47,12 @@ func (r *IPNSResolver) ResolveAsync(ctx context.Context, name string, options .. return resolveAsync(ctx, r, name, ProcessResolveOptions(options)) } -func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan ResolveResult { - ctx, span := startSpan(ctx, "IpnsResolver.ResolveOnceAsync", trace.WithAttributes(attribute.String("Name", name))) +func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, nameStr string, options ResolveOptions) <-chan ResolveResult { + ctx, span := startSpan(ctx, "IpnsResolver.ResolveOnceAsync", trace.WithAttributes(attribute.String("Name", nameStr))) defer span.End() out := make(chan ResolveResult, 1) - log.Debugf("RoutingResolver resolving %s", name) + log.Debugf("RoutingResolver resolving %s", nameStr) cancel := func() {} if options.DhtTimeout != 0 { @@ -60,25 +60,18 @@ func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, name string, option ctx, cancel = context.WithTimeout(ctx, options.DhtTimeout) } - name = strings.TrimPrefix(name, "/ipns/") - - pid, err := peer.Decode(name) + name, err := ipns.NameFromString(nameStr) if err != nil { - log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", name, err) + log.Debugf("RoutingResolver: could not convert key %q to IPNS name: %s\n", nameStr, err) out <- ResolveResult{Err: err} close(out) cancel() return out } - // Use the routing system to get the name. - // Note that the DHT will call the ipns validator when retrieving - // the value, which in turn verifies the ipns record signature - ipnsKey := string(ipns.NameFromPeer(pid).RoutingKey()) - - vals, err := r.routing.SearchValue(ctx, ipnsKey, dht.Quorum(int(options.DhtRecordCount))) + vals, err := r.routing.SearchValue(ctx, string(name.RoutingKey()), dht.Quorum(int(options.DhtRecordCount))) if err != nil { - log.Debugf("RoutingResolver: dht get for name %s failed: %s", name, err) + log.Debugf("RoutingResolver: dht get for name %s failed: %s", nameStr, err) out <- ResolveResult{Err: err} close(out) cancel() @@ -100,7 +93,7 @@ func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, name string, option rec, err := ipns.UnmarshalRecord(val) if err != nil { - log.Debugf("RoutingResolver: could not unmarshal value for name %s: %s", name, err) + log.Debugf("RoutingResolver: could not unmarshal value for name %s: %s", nameStr, err) emitOnceResult(ctx, out, ResolveResult{Err: err}) return } @@ -142,3 +135,44 @@ func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, name string, option return out } + +// ResolveIPNS is an utility that takes a [NameSystem] and a [path.Path] and resolves the IPNS Path. +func ResolveIPNS(ctx context.Context, ns NameSystem, p path.Path) (path.Path, time.Duration, error) { + ctx, span := startSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String()))) + defer span.End() + + if !strings.HasPrefix(p.String(), ipns.NamespacePrefix) { + return p, 0, nil + } + + // TODO(cryptix): we should be able to query the local cache for the path + if ns == nil { + return "", 0, ErrNoNamesys + } + + seg := p.Segments() + + if len(seg) < 2 || seg[1] == "" { // just "/" without further segments + err := fmt.Errorf("invalid path %q: ipns path missing IPNS ID", p) + return "", 0, err + } + + extensions := seg[2:] + resolvable, err := path.FromSegments("/", seg[0], seg[1]) + if err != nil { + return "", 0, err + } + + resolvedPath, ttl, err := ns.Resolve(ctx, resolvable.String()) + if err != nil { + return "", 0, err + } + + segments := append(resolvedPath.Segments(), extensions...) + p, err = path.FromSegments("/", segments...) + if err != nil { + return "", 0, err + } + + return p, ttl, nil +} diff --git a/namesys/ipns_resolver_test.go b/namesys/ipns_resolver_test.go index 20d9909f19..e2edb1b2e3 100644 --- a/namesys/ipns_resolver_test.go +++ b/namesys/ipns_resolver_test.go @@ -51,7 +51,7 @@ func TestPrexistingExpiredRecord(t *testing.T) { entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) require.NoError(t, err) - err = PublishRecord(context.Background(), d, identity.PublicKey(), entry) + err = PublishIPNSRecord(context.Background(), d, identity.PublicKey(), entry) require.NoError(t, err) // Now, with an old record in the system already, try and publish a new one @@ -77,7 +77,7 @@ func TestPrexistingRecord(t *testing.T) { entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) require.NoError(t, err) - err = PublishRecord(context.Background(), d, identity.PublicKey(), entry) + err = PublishIPNSRecord(context.Background(), d, identity.PublicKey(), entry) require.NoError(t, err) // Now, with an old record in the system already, try and publish a new one diff --git a/namesys/namesys.go b/namesys/namesys.go index 0ba4c68464..baafafcdeb 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -47,7 +47,7 @@ type namesys struct { dnsResolver, ipnsResolver resolver ipnsPublisher Publisher - staticMap map[string]path.Path + staticMap map[string]*cacheEntry cache *lru.Cache[string, any] } @@ -92,19 +92,19 @@ func WithDatastore(ds ds.Datastore) Option { // NewNameSystem constructs an IPFS [NameSystem] based on the given [routing.ValueStore]. func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) { - var staticMap map[string]path.Path + var staticMap map[string]*cacheEntry // Prewarm namesys cache with static records for deterministic tests and debugging. // Useful for testing things like DNSLink without real DNS lookup. // Example: // IPFS_NS_MAP="dnslink-test.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am" if list := os.Getenv("IPFS_NS_MAP"); list != "" { - staticMap = make(map[string]path.Path) + staticMap = make(map[string]*cacheEntry) for _, pair := range strings.Split(list, ",") { mapping := strings.SplitN(pair, ":", 2) key := mapping[0] value := path.FromString(mapping[1]) - staticMap[key] = value + staticMap[key] = &cacheEntry{val: value, ttl: 0} } } @@ -219,7 +219,7 @@ func (ns *namesys) resolveOnceAsync(ctx context.Context, name string, options Re cacheKey = string(ipnsKey) } - if p, ok := ns.cacheGet(cacheKey); ok { + if p, ttl, ok := ns.cacheGet(cacheKey); ok { var err error if len(segments) > 3 { p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) @@ -227,7 +227,7 @@ func (ns *namesys) resolveOnceAsync(ctx context.Context, name string, options Re span.SetAttributes(attribute.Bool("CacheHit", true)) span.RecordError(err) - out <- ResolveResult{Path: p, Err: err} + out <- ResolveResult{Path: p, TTL: ttl, Err: err} close(out) return out } diff --git a/namesys/namesys_cache.go b/namesys/namesys_cache.go index 1b49d63be6..0ea866a4e7 100644 --- a/namesys/namesys_cache.go +++ b/namesys/namesys_cache.go @@ -6,22 +6,22 @@ import ( path "github.com/ipfs/boxo/path" ) -func (ns *namesys) cacheGet(name string) (path.Path, bool) { +func (ns *namesys) cacheGet(name string) (path.Path, time.Duration, bool) { // existence of optional mapping defined via IPFS_NS_MAP is checked first if ns.staticMap != nil { val, ok := ns.staticMap[name] if ok { - return val, true + return val.val, val.ttl, true } } if ns.cache == nil { - return "", false + return "", 0, false } ientry, ok := ns.cache.Get(name) if !ok { - return "", false + return "", 0, false } entry, ok := ientry.(cacheEntry) @@ -31,12 +31,12 @@ func (ns *namesys) cacheGet(name string) (path.Path, bool) { } if time.Now().Before(entry.eol) { - return entry.val, true + return entry.val, entry.ttl, true } ns.cache.Remove(name) - return "", false + return "", 0, false } func (ns *namesys) cacheSet(name string, val path.Path, ttl time.Duration) { @@ -45,6 +45,7 @@ func (ns *namesys) cacheSet(name string, val path.Path, ttl time.Duration) { } ns.cache.Add(name, cacheEntry{ val: val, + ttl: ttl, eol: time.Now().Add(ttl), }) } @@ -58,5 +59,6 @@ func (ns *namesys) cacheInvalidate(name string) { type cacheEntry struct { val path.Path + ttl time.Duration eol time.Time } diff --git a/namesys/resolve/resolve.go b/namesys/resolve/resolve.go deleted file mode 100644 index 2e93ba6fd0..0000000000 --- a/namesys/resolve/resolve.go +++ /dev/null @@ -1,65 +0,0 @@ -package resolve - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/ipfs/boxo/ipns" - "github.com/ipfs/boxo/path" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/ipfs/boxo/namesys" -) - -// ErrNoNamesys is an explicit error for when an IPFS node doesn't -// (yet) have a name system -var ErrNoNamesys = errors.New("core/resolve: no Namesys on IpfsNode - can't resolve ipns entry") - -// ResolveIPNS resolves /ipns paths -func ResolveIPNS(ctx context.Context, ns namesys.NameSystem, p path.Path) (path.Path, error) { - ctx, span := startSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String()))) - defer span.End() - - if strings.HasPrefix(p.String(), ipns.NamespacePrefix) { - // TODO(cryptix): we should be able to query the local cache for the path - if ns == nil { - return "", ErrNoNamesys - } - - seg := p.Segments() - - if len(seg) < 2 || seg[1] == "" { // just "/" without further segments - err := fmt.Errorf("invalid path %q: ipns path missing IPNS ID", p) - return "", err - } - - extensions := seg[2:] - resolvable, err := path.FromSegments("/", seg[0], seg[1]) - if err != nil { - return "", err - } - - resolvedPath, _, err := ns.Resolve(ctx, resolvable.String()) - if err != nil { - return "", err - } - - segments := append(resolvedPath.Segments(), extensions...) - p, err = path.FromSegments("/", segments...) - if err != nil { - return "", err - } - } - - return p, nil -} - -var tracer = otel.Tracer("boxo/namesys/resolve") - -func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return tracer.Start(ctx, fmt.Sprintf("Namesys.%s", name)) -}