Skip to content

Commit

Permalink
feat: ttl on generated dir listing, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Sep 7, 2023
1 parent 26b152f commit 4e023b4
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 58 deletions.
63 changes: 24 additions & 39 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,48 +163,33 @@ func TestHeaders(t *testing.T) {

ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car")
backend.namesys["/ipns/example.net"] = newMockNamesysItem(path.NewIPFSPath(root), time.Second*30)
backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.NewIPFSPath(root), time.Second*55)
backend.namesys["/ipns/unknown.com"] = newMockNamesysItem(path.NewIPFSPath(root), 0)

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"))
})
testCases := []struct {
path string
cacheControl string
}{
{"/ipns/example.net/", "public, max-age=30"}, // As generated directory listing
{"/ipns/example.com/", "public, max-age=55"}, // As generated directory listing (different)
{"/ipns/unknown.com/", ""}, // As generated directory listing (unknown)
{"/ipns/example.net/foo/", "public, max-age=30"}, // As index.html directory listing
{"/ipns/example.net/foo/index.html", "public, max-age=30"}, // As deserialized UnixFS file
{"/ipns/example.net/?format=raw", "public, max-age=30"}, // As Raw block
{"/ipns/example.net/?format=dag-json", "public, max-age=30"}, // As DAG-JSON block
{"/ipns/example.net/?format=dag-cbor", "public, max-age=30"}, // As DAG-CBOR block
{"/ipns/example.net/?format=car", "public, max-age=30"}, // As CAR block
}

t.Run("CAR block has Cache-Control", func(t *testing.T) {
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=car", nil)
for _, testCase := range testCases {
req := mustNewRequest(t, http.MethodGet, ts.URL+testCase.path, nil)
res := mustDoWithoutRedirect(t, req)
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
})
if testCase.cacheControl == "" {
assert.Empty(t, res.Header["Cache-Control"])
} else {
assert.Equal(t, testCase.cacheControl, res.Header.Get("Cache-Control"))
}
}
})

t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions gateway/handler_unixfs_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *
dirEtag := getDirListingEtag(resolvedPath.Cid())
w.Header().Set("Etag", dirEtag)

// Add TTL if known.
if ttl > 0 {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(ttl.Seconds())))
}

if r.Method == http.MethodHead {
logger.Debug("return as request's HTTP method is HEAD")
return true
Expand Down
51 changes: 32 additions & 19 deletions namesys/ipns_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import (
"go.opentelemetry.io/otel/trace"
)

// IPNSResolver implements [Resolver] for IPNS Records.
// IPNSResolver implements [Resolver] for IPNS Records. This resolver always returns
// a TTL if the record is still valid. It happens as follows:
//
// 1. Provisory TTL is chosen: record TTL if it exists, otherwise [DefaultIPNSRecordTTL].
// 2. If provisory TTL expires before EOL, then returned TTL is duration between EOL and now.
// 3. If record is expired, 0 is returned as TTL.
type IPNSResolver struct {
routing routing.ValueStore
}
Expand Down Expand Up @@ -102,24 +107,8 @@ func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, nameStr string, opt
return
}

Check warning on line 108 in namesys/ipns_resolver.go

View check run for this annotation

Codecov / codecov/patch

namesys/ipns_resolver.go#L106-L108

Added lines #L106 - L108 were not covered by tests

ttl := DefaultResolverCacheTTL
if recordTTL, err := rec.TTL(); err == nil {
ttl = recordTTL
}

switch eol, err := rec.Validity(); err {
case ipns.ErrUnrecognizedValidity:
// No EOL.
case nil:
ttEol := time.Until(eol)
if ttEol < 0 {
// It *was* valid when we first resolved it.
ttl = 0
} else if ttEol < ttl {
ttl = ttEol
}
default:
log.Errorf("encountered error when parsing EOL: %s", err)
ttl, err := calculateBestTTL(rec)
if err != nil {
emitOnceResult(ctx, out, ResolveResult{Err: err})
return
}

Check warning on line 114 in namesys/ipns_resolver.go

View check run for this annotation

Codecov / codecov/patch

namesys/ipns_resolver.go#L112-L114

Added lines #L112 - L114 were not covered by tests
Expand Down Expand Up @@ -166,3 +155,27 @@ func ResolveIPNS(ctx context.Context, ns NameSystem, p path.Path) (path.Path, ti

return p, ttl, nil
}

func calculateBestTTL(rec *ipns.Record) (time.Duration, error) {
ttl := DefaultResolverCacheTTL
if recordTTL, err := rec.TTL(); err == nil {
ttl = recordTTL
}

switch eol, err := rec.Validity(); err {
case ipns.ErrUnrecognizedValidity:

Check warning on line 166 in namesys/ipns_resolver.go

View check run for this annotation

Codecov / codecov/patch

namesys/ipns_resolver.go#L166

Added line #L166 was not covered by tests
// No EOL.
case nil:
ttEol := time.Until(eol)
if ttEol < 0 {
// It *was* valid when we first resolved it.
ttl = 0

Check warning on line 172 in namesys/ipns_resolver.go

View check run for this annotation

Codecov / codecov/patch

namesys/ipns_resolver.go#L171-L172

Added lines #L171 - L172 were not covered by tests
} else if ttEol < ttl {
ttl = ttEol
}
default:
return 0, err

Check warning on line 177 in namesys/ipns_resolver.go

View check run for this annotation

Codecov / codecov/patch

namesys/ipns_resolver.go#L176-L177

Added lines #L176 - L177 were not covered by tests
}

return ttl, nil
}
1 change: 1 addition & 0 deletions namesys/namesys.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func (ns *namesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path
span.RecordError(err)
return err
}

ttl := DefaultResolverCacheTTL
if publishOpts.TTL >= 0 {
ttl = publishOpts.TTL
Expand Down

0 comments on commit 4e023b4

Please sign in to comment.