Skip to content

Commit

Permalink
Gateway: Add Cache-Control/max-age to IPNS requests
Browse files Browse the repository at this point in the history
License: MIT
Signed-off-by: Johan Kiviniemi <[email protected]>
  • Loading branch information
ion1 committed Nov 5, 2015
1 parent 0534038 commit c013392
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 54 deletions.
26 changes: 17 additions & 9 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

nd, err := core.Resolve(ctx, i.node, path.Path(urlPath))
res, err := core.ResolveFull(ctx, i.node, path.Path(urlPath))
if err != nil {
webError(w, "Path Resolve error", err, http.StatusBadRequest)
return
Expand Down Expand Up @@ -139,22 +139,20 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
w.Header().Set("Suborigin", pathRoot)
}

dr, err := uio.NewDagReader(ctx, nd, i.node.DAG)
dr, err := uio.NewDagReader(ctx, res.Node, i.node.DAG)
if err != nil && err != uio.ErrIsDir {
// not a directory and still an error
internalWebError(w, err)
return
}

// set these headers _after_ the error, for we may just not have it
// and dont want the client to cache a 500 response...
// and only if it's /ipfs!
// TODO: break this out when we split /ipfs /ipns routes.
// Remember to unset the Cache-Control/ETag headers before error
// responses! The webError functions unset them automatically.
setSuccessHeaders(w, res)

modtime := time.Now()
if strings.HasPrefix(urlPath, ipfsPathPrefix) {
w.Header().Set("Etag", etag)
w.Header().Set("Cache-Control", "public, max-age=29030400")

// set modtime to a really long time ago, since files are immutable and should stay cached
modtime = time.Unix(1, 0)
}
Expand All @@ -170,7 +168,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
var dirListing []directoryItem
// loop through files
foundIndex := false
for _, link := range nd.Links {
for _, link := range res.Node.Links {
if link.Name == "index.html" {
log.Debugf("found index.html link for %s", urlPath)
foundIndex = true
Expand Down Expand Up @@ -446,6 +444,7 @@ func webError(w http.ResponseWriter, message string, err error, defaultCode int)
}

func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) {
unsetSuccessHeaders(w)
w.WriteHeader(code)
log.Errorf("%s: %s", message, err) // TODO(cryptix): log errors until we have a better way to expose these (counter metrics maybe)
fmt.Fprintf(w, "%s: %s", message, err)
Expand All @@ -455,3 +454,12 @@ func webErrorWithCode(w http.ResponseWriter, message string, err error, code int
func internalWebError(w http.ResponseWriter, err error) {
webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError)
}

func setSuccessHeaders(w http.ResponseWriter, res core.ResolveResult) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(res.TTL.Seconds())))
}

func unsetSuccessHeaders(w http.ResponseWriter) {
w.Header().Del("Cache-Control")
w.Header().Del("ETag")
}
62 changes: 50 additions & 12 deletions core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package corehttp

import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand All @@ -20,7 +21,12 @@ import (
testutil "github.com/ipfs/go-ipfs/util/testutil"
)

type mockNamesys map[string]path.Path
type mockNamesys map[string]mockEntry

type mockEntry struct {
path path.Path
ttl time.Duration
}

func (m mockNamesys) Resolve(ctx context.Context, name string) (path.Path, error) {
res, err := m.ResolveFull(ctx, name)
Expand All @@ -37,11 +43,11 @@ func (m mockNamesys) ResolveFull(ctx context.Context, name string) (namesys.Reso
}

func (m mockNamesys) ResolveNFull(ctx context.Context, name string, depth int) (namesys.ResolveResult, error) {
p, ok := m[name]
entry, ok := m[name]
if !ok {
return namesys.ResolveResult{}, namesys.ErrResolveFailed
}
return namesys.ResolveResult{p, 0}, nil
return namesys.ResolveResult{entry.path, entry.ttl}, nil
}

func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
Expand Down Expand Up @@ -124,21 +130,26 @@ func TestGatewayGet(t *testing.T) {
if err != nil {
t.Fatal(err)
}
ns["/ipns/example.com"] = path.FromString("/ipfs/" + k)
kTTL := 10 * time.Minute
ns["/ipns/example.com"] = mockEntry{
path: path.FromString("/ipfs/" + k),
ttl: kTTL,
}

t.Log(ts.URL)
for _, test := range []struct {
host string
path string
status int
ttl []time.Duration // an approximation for a Maybe/Option sum type
text string
}{
{"localhost:5001", "/", http.StatusNotFound, "404 page not found\n"},
{"localhost:5001", "/" + k, http.StatusNotFound, "404 page not found\n"},
{"localhost:5001", "/ipfs/" + k, http.StatusOK, "fnord"},
{"localhost:5001", "/ipns/nxdomain.example.com", http.StatusBadRequest, "Path Resolve error: " + namesys.ErrResolveFailed.Error()},
{"localhost:5001", "/ipns/example.com", http.StatusOK, "fnord"},
{"example.com", "/", http.StatusOK, "fnord"},
{"localhost:5001", "/", http.StatusNotFound, nil, "404 page not found\n"},
{"localhost:5001", "/" + k, http.StatusNotFound, nil, "404 page not found\n"},
{"localhost:5001", "/ipfs/" + k, http.StatusOK, []time.Duration{namesys.ImmutableTTL}, "fnord"},
{"localhost:5001", "/ipns/nxdomain.example.com", http.StatusBadRequest, nil, "Path Resolve error: " + namesys.ErrResolveFailed.Error()},
{"localhost:5001", "/ipns/example.com", http.StatusOK, []time.Duration{kTTL}, "fnord"},
{"example.com", "/", http.StatusOK, []time.Duration{kTTL}, "fnord"},
} {
var c http.Client
r, err := http.NewRequest("GET", ts.URL+test.path, nil)
Expand All @@ -158,6 +169,7 @@ func TestGatewayGet(t *testing.T) {
t.Errorf("got %d, expected %d from %s", resp.StatusCode, test.status, urlstr)
continue
}
checkCacheControl(t, urlstr, resp, test.ttl)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("error reading response from %s: %s", urlstr, err)
Expand Down Expand Up @@ -199,7 +211,11 @@ func TestIPNSHostnameRedirect(t *testing.T) {
t.Fatal(err)
}
t.Logf("k: %s\n", k)
ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String())
kTTL := 10 * time.Minute
ns["/ipns/example.net"] = mockEntry{
path: path.FromString("/ipfs/" + k.String()),
ttl: kTTL,
}

// make request to directory containing index.html
req, err := http.NewRequest("GET", ts.URL+"/foo", nil)
Expand All @@ -223,6 +239,7 @@ func TestIPNSHostnameRedirect(t *testing.T) {
} else if hdr[0] != "/foo/" {
t.Errorf("location header is %v, expected /foo/", hdr[0])
}
checkCacheControl(t, "http://example.net/foo", res, []time.Duration{kTTL})
}

func TestIPNSHostnameBacklinks(t *testing.T) {
Expand Down Expand Up @@ -260,7 +277,11 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
t.Fatal(err)
}
t.Logf("k: %s\n", k)
ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String())
kTTL := 10 * time.Minute
ns["/ipns/example.net"] = mockEntry{
path: path.FromString("/ipfs/" + k.String()),
ttl: kTTL,
}

// make request to directory listing
req, err := http.NewRequest("GET", ts.URL+"/foo/", nil)
Expand Down Expand Up @@ -292,6 +313,8 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
t.Fatalf("expected file in directory listing")
}

checkCacheControl(t, "http://example.net/foo/", res, []time.Duration{kTTL})

// make request to directory listing
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
Expand Down Expand Up @@ -322,6 +345,8 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
t.Fatalf("expected file in directory listing")
}

checkCacheControl(t, "http://example.net/", res, []time.Duration{kTTL})

// make request to directory listing
req, err = http.NewRequest("GET", ts.URL+"/foo/bar/", nil)
if err != nil {
Expand Down Expand Up @@ -351,4 +376,17 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
if !strings.Contains(s, "<a href=\"/foo/bar/file.txt\">") {
t.Fatalf("expected file in directory listing")
}

checkCacheControl(t, "http://example.net/foo/bar/", res, []time.Duration{kTTL})
}

func checkCacheControl(t *testing.T, urlstr string, resp *http.Response, ttlMaybe []time.Duration) {
expCacheControl := ""
for _, ttl := range ttlMaybe {
expCacheControl = fmt.Sprintf("public, max-age=%d", int(ttl.Seconds()))
}
cacheControl := resp.Header.Get("Cache-Control")
if cacheControl != expCacheControl {
t.Errorf("unexpected Cache-Control header from %s: expected %q; got %q", urlstr, expCacheControl, cacheControl)
}
}
Loading

0 comments on commit c013392

Please sign in to comment.