Skip to content

Commit 4e023b4

Browse files
committed
feat: ttl on generated dir listing, tests
1 parent 26b152f commit 4e023b4

File tree

4 files changed

+62
-58
lines changed

4 files changed

+62
-58
lines changed

gateway/gateway_test.go

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -163,48 +163,33 @@ func TestHeaders(t *testing.T) {
163163

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

167-
t.Run("UnixFS generated directory listing without index.html has no Cache-Control", func(t *testing.T) {
168-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/", nil)
169-
res := mustDoWithoutRedirect(t, req)
170-
require.Empty(t, res.Header["Cache-Control"])
171-
})
172-
173-
t.Run("UnixFS directory with index.html has Cache-Control", func(t *testing.T) {
174-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/", nil)
175-
res := mustDoWithoutRedirect(t, req)
176-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
177-
})
178-
179-
t.Run("UnixFS file has Cache-Control", func(t *testing.T) {
180-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net/foo/index.html", nil)
181-
res := mustDoWithoutRedirect(t, req)
182-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
183-
})
184-
185-
t.Run("Raw block has Cache-Control", func(t *testing.T) {
186-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=raw", nil)
187-
res := mustDoWithoutRedirect(t, req)
188-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
189-
})
190-
191-
t.Run("DAG-JSON block has Cache-Control", func(t *testing.T) {
192-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-json", nil)
193-
res := mustDoWithoutRedirect(t, req)
194-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
195-
})
196-
197-
t.Run("DAG-CBOR block has Cache-Control", func(t *testing.T) {
198-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=dag-cbor", nil)
199-
res := mustDoWithoutRedirect(t, req)
200-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
201-
})
169+
testCases := []struct {
170+
path string
171+
cacheControl string
172+
}{
173+
{"/ipns/example.net/", "public, max-age=30"}, // As generated directory listing
174+
{"/ipns/example.com/", "public, max-age=55"}, // As generated directory listing (different)
175+
{"/ipns/unknown.com/", ""}, // As generated directory listing (unknown)
176+
{"/ipns/example.net/foo/", "public, max-age=30"}, // As index.html directory listing
177+
{"/ipns/example.net/foo/index.html", "public, max-age=30"}, // As deserialized UnixFS file
178+
{"/ipns/example.net/?format=raw", "public, max-age=30"}, // As Raw block
179+
{"/ipns/example.net/?format=dag-json", "public, max-age=30"}, // As DAG-JSON block
180+
{"/ipns/example.net/?format=dag-cbor", "public, max-age=30"}, // As DAG-CBOR block
181+
{"/ipns/example.net/?format=car", "public, max-age=30"}, // As CAR block
182+
}
202183

203-
t.Run("CAR block has Cache-Control", func(t *testing.T) {
204-
req := mustNewRequest(t, http.MethodGet, ts.URL+"/ipns/example.net?format=car", nil)
184+
for _, testCase := range testCases {
185+
req := mustNewRequest(t, http.MethodGet, ts.URL+testCase.path, nil)
205186
res := mustDoWithoutRedirect(t, req)
206-
require.Equal(t, "public, max-age=30", res.Header.Get("Cache-Control"))
207-
})
187+
if testCase.cacheControl == "" {
188+
assert.Empty(t, res.Header["Cache-Control"])
189+
} else {
190+
assert.Equal(t, testCase.cacheControl, res.Header.Get("Cache-Control"))
191+
}
192+
}
208193
})
209194

210195
t.Run("Cache-Control is not immutable on generated /ipfs/ HTML dir listings", func(t *testing.T) {

gateway/handler_unixfs_dir.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *
126126
dirEtag := getDirListingEtag(resolvedPath.Cid())
127127
w.Header().Set("Etag", dirEtag)
128128

129+
// Add TTL if known.
130+
if ttl > 0 {
131+
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(ttl.Seconds())))
132+
}
133+
129134
if r.Method == http.MethodHead {
130135
logger.Debug("return as request's HTTP method is HEAD")
131136
return true

namesys/ipns_resolver.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import (
1313
"go.opentelemetry.io/otel/trace"
1414
)
1515

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

105-
ttl := DefaultResolverCacheTTL
106-
if recordTTL, err := rec.TTL(); err == nil {
107-
ttl = recordTTL
108-
}
109-
110-
switch eol, err := rec.Validity(); err {
111-
case ipns.ErrUnrecognizedValidity:
112-
// No EOL.
113-
case nil:
114-
ttEol := time.Until(eol)
115-
if ttEol < 0 {
116-
// It *was* valid when we first resolved it.
117-
ttl = 0
118-
} else if ttEol < ttl {
119-
ttl = ttEol
120-
}
121-
default:
122-
log.Errorf("encountered error when parsing EOL: %s", err)
110+
ttl, err := calculateBestTTL(rec)
111+
if err != nil {
123112
emitOnceResult(ctx, out, ResolveResult{Err: err})
124113
return
125114
}
@@ -166,3 +155,27 @@ func ResolveIPNS(ctx context.Context, ns NameSystem, p path.Path) (path.Path, ti
166155

167156
return p, ttl, nil
168157
}
158+
159+
func calculateBestTTL(rec *ipns.Record) (time.Duration, error) {
160+
ttl := DefaultResolverCacheTTL
161+
if recordTTL, err := rec.TTL(); err == nil {
162+
ttl = recordTTL
163+
}
164+
165+
switch eol, err := rec.Validity(); err {
166+
case ipns.ErrUnrecognizedValidity:
167+
// No EOL.
168+
case nil:
169+
ttEol := time.Until(eol)
170+
if ttEol < 0 {
171+
// It *was* valid when we first resolved it.
172+
ttl = 0
173+
} else if ttEol < ttl {
174+
ttl = ttEol
175+
}
176+
default:
177+
return 0, err
178+
}
179+
180+
return ttl, nil
181+
}

namesys/namesys.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ func (ns *namesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path
283283
span.RecordError(err)
284284
return err
285285
}
286+
286287
ttl := DefaultResolverCacheTTL
287288
if publishOpts.TTL >= 0 {
288289
ttl = publishOpts.TTL

0 commit comments

Comments
 (0)