diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7dffd929..ed1bb6137 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 3a673dad6..15437e75d 100644
--- a/gateway/blocks_backend.go
+++ b/gateway/blocks_backend.go
@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"strings"
+ "time"
"github.com/ipfs/boxo/blockservice"
blockstore "github.com/ipfs/boxo/blockstore"
@@ -17,8 +18,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"
"github.com/ipfs/boxo/path"
"github.com/ipfs/boxo/path/resolver"
blocks "github.com/ipfs/go-block-format"
@@ -38,7 +39,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,18 +556,23 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
return pathRoots, lastPath, nil
}
-func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
+func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, error) {
switch p.Namespace() {
case path.IPNSNamespace:
- p, err := resolve.ResolveIPNS(ctx, bb.namesys, p)
+ p, ttl, err := namesys.ResolveIPNS(ctx, bb.namesys, p)
if err != nil {
- return nil, err
+ return nil, 0, err
+ }
+ ip, err := path.NewImmutablePath(p)
+ if err != nil {
+ return nil, 0, err
}
- return path.NewImmutablePath(p)
+ return ip, ttl, nil
case path.IPFSNamespace:
- return path.NewImmutablePath(p)
+ ip, err := path.NewImmutablePath(p)
+ return ip, 0, err
default:
- return nil, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
+ return nil, 0, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented)
}
}
@@ -576,19 +581,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) (path.Path, error) {
@@ -628,7 +626,7 @@ func (bb *BlocksBackend) ResolvePath(ctx context.Context, path path.ImmutablePat
func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
var err error
if p.Namespace() == path.IPNSNamespace {
- p, err = resolve.ResolveIPNS(ctx, bb.namesys, p)
+ p, _, err = namesys.ResolveIPNS(ctx, bb.namesys, p)
if err != nil {
return nil, err
}
diff --git a/gateway/gateway.go b/gateway/gateway.go
index 3be6c10f3..1664fa1e7 100644
--- a/gateway/gateway.go
+++ b/gateway/gateway.go
@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
"strings"
+ "time"
"github.com/ipfs/boxo/files"
"github.com/ipfs/boxo/gateway/assets"
@@ -303,11 +304,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) (path.ImmutablePath, error)
+ ResolveMutable(context.Context, path.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 785c338ca..4e112f509 100644
--- a/gateway/gateway_test.go
+++ b/gateway/gateway_test.go
@@ -37,11 +37,11 @@ func TestGatewayGet(t *testing.T) {
return p
}
- backend.namesys["/ipns/example.com"] = path.NewIPFSPath(k.Cid())
- backend.namesys["/ipns/working.example.com"] = k
- backend.namesys["/ipns/double.example.com"] = mustMakeDNSLinkPath("working.example.com")
- backend.namesys["/ipns/triple.example.com"] = mustMakeDNSLinkPath("double.example.com")
- backend.namesys["/ipns/broken.example.com"] = mustMakeDNSLinkPath(k.Cid().String())
+ backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.NewIPFSPath(k.Cid()), 0)
+ backend.namesys["/ipns/working.example.com"] = newMockNamesysItem(k, 0)
+ backend.namesys["/ipns/double.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath("working.example.com"), 0)
+ backend.namesys["/ipns/triple.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath("double.example.com"), 0)
+ backend.namesys["/ipns/broken.example.com"] = newMockNamesysItem(mustMakeDNSLinkPath(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
@@ -49,7 +49,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"] = k
+ backend.namesys["/ipns/example.man"] = newMockNamesysItem(k, 0)
for _, test := range []struct {
host string
@@ -98,7 +98,7 @@ func TestPretty404(t *testing.T) {
t.Logf("test server url: %s", ts.URL)
host := "example.net"
- backend.namesys["/ipns/"+host] = path.NewIPFSPath(root)
+ backend.namesys["/ipns/"+host] = newMockNamesysItem(path.NewIPFSPath(root), 0)
for _, test := range []struct {
path string
@@ -158,7 +158,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.NewIPFSPath(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)
@@ -500,7 +549,7 @@ func TestRedirects(t *testing.T) {
t.Parallel()
ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
- backend.namesys["/ipns/example.net"] = path.NewIPFSPath(root)
+ backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
// make request to directory containing index.html
req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo", nil)
@@ -535,7 +584,7 @@ func TestRedirects(t *testing.T) {
t.Parallel()
backend, root := newMockBackend(t, "redirects-spa.car")
- backend.namesys["/ipns/example.com"] = path.NewIPFSPath(root)
+ backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
ts := newTestServerWithConfig(t, backend, Config{
Headers: map[string][]string{},
@@ -672,8 +721,8 @@ func TestDeserializedResponses(t *testing.T) {
t.Parallel()
backend, root := newMockBackend(t, "fixtures.car")
- backend.namesys["/ipns/trustless.com"] = path.NewIPFSPath(root)
- backend.namesys["/ipns/trusted.com"] = path.NewIPFSPath(root)
+ backend.namesys["/ipns/trustless.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
+ backend.namesys["/ipns/trusted.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)
ts := newTestServerWithConfig(t, backend, Config{
Headers: map[string][]string{},
@@ -735,8 +784,8 @@ func (mb *errorMockBackend) GetCAR(ctx context.Context, path path.ImmutablePath,
return ContentPathMetadata{}, nil, mb.err
}
-func (mb *errorMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
- return nil, mb.err
+func (mb *errorMockBackend) ResolveMutable(ctx context.Context, path path.Path) (path.ImmutablePath, time.Duration, error) {
+ return nil, 0, mb.err
}
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
panic("i am panicking")
}
-func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
+func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, error) {
panic("i am panicking")
}
diff --git a/gateway/handler.go b/gateway/handler.go
index c3c19250a..82796daf9 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 path.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.Namespace().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 path.Path, cid cid.Cid, responseFormat string) (modtime time.Time) {
+func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath path.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.Namespace().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 487ddf8b6..d7f59bcd1 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 56aa42e9d..ae27140f4 100644
--- a/gateway/handler_car.go
+++ b/gateway/handler_car.go
@@ -57,7 +57,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 df222a7c2..62fdb6934 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 de31c1fc1..edfc0f993 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 529e463da..d2f6fa88d 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 5641d39f2..f9ba210a0 100644
--- a/gateway/handler_unixfs__redirects.go
+++ b/gateway/handler_unixfs__redirects.go
@@ -229,7 +229,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 86329b61c..86c59850c 100644
--- a/gateway/handler_unixfs_dir.go
+++ b/gateway/handler_unixfs_dir.go
@@ -22,7 +22,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 path.ImmutablePath, contentPath path.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 path.ImmutablePath, contentPath path.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()
@@ -104,7 +104,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().String()).Observe(time.Since(begin).Seconds())
}
diff --git a/gateway/handler_unixfs_dir_test.go b/gateway/handler_unixfs_dir_test.go
index fef101315..c3596fd40 100644
--- a/gateway/handler_unixfs_dir_test.go
+++ b/gateway/handler_unixfs_dir_test.go
@@ -28,7 +28,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
k3, err := backend.resolvePathNoRootsReturned(ctx, p3)
require.NoError(t, err)
- backend.namesys["/ipns/example.net"] = path.NewIPFSPath(root)
+ backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.NewIPFSPath(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 5f5b1c52c..6d4109ea5 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 path.ImmutablePath, contentPath path.Path, file files.File, fileContentType string, begin time.Time) bool {
+func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath path.ImmutablePath, contentPath path.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 7030c88dc..6f77ae73a 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.NewIPFSPath(testCID)
- backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.NewIPFSPath(testCID)
+ backend.namesys["/ipns/dnslink.long-name.example.com"] = newMockNamesysItem(path.NewIPFSPath(testCID), 0)
+ backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = newMockNamesysItem(path.NewIPFSPath(testCID), 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 0597d56a0..c48f1f3c9 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) (path.ImmutablePath, error) {
+func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path 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 369c6cf2a..2e8318886 100644
--- a/gateway/utilities_test.go
+++ b/gateway/utilities_test.go
@@ -51,7 +51,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()
@@ -69,14 +78,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 nil, 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 {
@@ -152,7 +162,7 @@ func (mb *mockBackend) GetCAR(ctx context.Context, immutablePath path.ImmutableP
return mb.gw.GetCAR(ctx, immutablePath, params)
}
-func (mb *mockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) {
+func (mb *mockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, time.Duration, error) {
return mb.gw.ResolveMutable(ctx, p)
}
@@ -184,7 +194,7 @@ func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip path.P
var imPath path.ImmutablePath
var err error
if ip.Namespace().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 9133cf9c5..f70eee3ed 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 48ec2edba..7d7b6f205 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.String() != 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 927d005fa..54450d7cb 100644
--- a/namesys/ipns_publisher_test.go
+++ b/namesys/ipns_publisher_test.go
@@ -69,11 +69,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 e0dc757ee..d48e658a6 100644
--- a/namesys/ipns_resolver.go
+++ b/namesys/ipns_resolver.go
@@ -2,14 +2,13 @@ package namesys
import (
"context"
- "strings"
+ "fmt"
"time"
"github.com/ipfs/boxo/ipns"
"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 +46,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 +59,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 +92,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 +134,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 p.Namespace() != path.IPNSNamespace {
+ return p, 0, nil
+ }
+
+ // TODO(cryptix): we should be able to query the local cache for the path
+ if ns == nil {
+ return nil, 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 nil, 0, err
+ }
+
+ extensions := seg[2:]
+ resolvable, err := path.NewPathFromSegments(seg[0], seg[1])
+ if err != nil {
+ return nil, 0, err
+ }
+
+ resolvedPath, ttl, err := ns.Resolve(ctx, resolvable.String())
+ if err != nil {
+ return nil, 0, err
+ }
+
+ segments := append(resolvedPath.Segments(), extensions...)
+ p, err = path.NewPathFromSegments(segments...)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return p, ttl, nil
+}
diff --git a/namesys/ipns_resolver_test.go b/namesys/ipns_resolver_test.go
index c4cee5b00..13984a6a7 100644
--- a/namesys/ipns_resolver_test.go
+++ b/namesys/ipns_resolver_test.go
@@ -56,7 +56,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
@@ -85,7 +85,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 7014303e1..415782323 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,14 +92,14 @@ 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]
@@ -107,7 +107,7 @@ func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) {
if err != nil {
return nil, err
}
- staticMap[key] = value
+ staticMap[key] = &cacheEntry{val: value, ttl: 0}
}
}
@@ -222,7 +222,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.Join(p, segments[3])
@@ -230,7 +230,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 d761971ee..34be88955 100644
--- a/namesys/namesys_cache.go
+++ b/namesys/namesys_cache.go
@@ -6,22 +6,22 @@ import (
"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 nil, false
+ return nil, 0, false
}
ientry, ok := ns.cache.Get(name)
if !ok {
- return nil, false
+ return nil, 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 nil, false
+ return nil, 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 b2d85e9e8..000000000
--- a/namesys/resolve/resolve.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package resolve
-
-import (
- "context"
- "errors"
- "fmt"
-
- "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 p.Namespace() == path.IPNSNamespace {
- // TODO(cryptix): we should be able to query the local cache for the path
- if ns == nil {
- return nil, 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 nil, err
- }
-
- extensions := seg[2:]
- resolvable, err := path.NewPathFromSegments(seg[0], seg[1])
- if err != nil {
- return nil, err
- }
-
- resolvedPath, _, err := ns.Resolve(ctx, resolvable.String())
- if err != nil {
- return nil, err
- }
-
- segments := append(resolvedPath.Segments(), extensions...)
- p, err = path.NewPathFromSegments(segments...)
- if err != nil {
- return nil, 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))
-}