Skip to content

Commit

Permalink
Gateway: Add ETag to IPNS requests
Browse files Browse the repository at this point in the history
Closes ipfs/go-ipfs#1818.

License: MIT
Signed-off-by: Johan Kiviniemi <devel@johan.kiviniemi.name>
  • Loading branch information
ion1 committed Nov 5, 2015
1 parent 0a0ea41 commit c0b09e0
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 10 deletions.
16 changes: 12 additions & 4 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,15 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

etag := gopath.Base(urlPath)
ndHash, err := res.Node.Key()
if err != nil {
internalWebError(w, err)
return
}

// ETag requires the quote marks:
// https://tools.ietf.org/html/rfc7232#section-2.3
etag := "\"" + ndHash.String() + "\""
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
Expand Down Expand Up @@ -148,11 +156,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request

// Remember to unset the Cache-Control/ETag headers before error
// responses! The webError functions unset them automatically.
setSuccessHeaders(w, res)
setSuccessHeaders(w, res, etag)

modtime := time.Now()
if strings.HasPrefix(urlPath, ipfsPathPrefix) {
w.Header().Set("Etag", etag)
// set modtime to a really long time ago, since files are immutable and should stay cached
modtime = time.Unix(1, 0)
}
Expand Down Expand Up @@ -455,8 +462,9 @@ func internalWebError(w http.ResponseWriter, err error) {
webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError)
}

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

func unsetSuccessHeaders(w http.ResponseWriter) {
Expand Down
42 changes: 36 additions & 6 deletions core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,15 @@ func TestGatewayGet(t *testing.T) {
path string
status int
ttl []time.Duration // an approximation for a Maybe/Option sum type
etag string
text string
}{
{"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"},
{"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}, k, "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}, k, "fnord"},
{"example.com", "/", http.StatusOK, []time.Duration{kTTL}, k, "fnord"},
} {
var c http.Client
r, err := http.NewRequest("GET", ts.URL+test.path, nil)
Expand All @@ -170,6 +171,7 @@ func TestGatewayGet(t *testing.T) {
continue
}
checkCacheControl(t, urlstr, resp, test.ttl)
checkETag(t, urlstr, resp, test.etag)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("error reading response from %s: %s", urlstr, err)
Expand Down Expand Up @@ -210,6 +212,10 @@ func TestIPNSHostnameRedirect(t *testing.T) {
if err != nil {
t.Fatal(err)
}
kFoo, err := dagn2.Key()
if err != nil {
t.Fatal(err)
}
t.Logf("k: %s\n", k)
kTTL := 10 * time.Minute
ns["/ipns/example.net"] = mockEntry{
Expand Down Expand Up @@ -239,7 +245,9 @@ 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})
checkETag(t, "http://example.net/foo", res, kFoo.String())
}

func TestIPNSHostnameBacklinks(t *testing.T) {
Expand Down Expand Up @@ -276,6 +284,14 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
if err != nil {
t.Fatal(err)
}
kFoo, err := dagn2.Key()
if err != nil {
t.Fatal(err)
}
kBar, err := dagn3.Key()
if err != nil {
t.Fatal(err)
}
t.Logf("k: %s\n", k)
kTTL := 10 * time.Minute
ns["/ipns/example.net"] = mockEntry{
Expand Down Expand Up @@ -314,6 +330,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
}

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

// make request to directory listing
req, err = http.NewRequest("GET", ts.URL, nil)
Expand Down Expand Up @@ -346,6 +363,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
}

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

// make request to directory listing
req, err = http.NewRequest("GET", ts.URL+"/foo/bar/", nil)
Expand Down Expand Up @@ -378,6 +396,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
}

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

func checkCacheControl(t *testing.T, urlstr string, resp *http.Response, ttlMaybe []time.Duration) {
Expand All @@ -390,3 +409,14 @@ func checkCacheControl(t *testing.T, urlstr string, resp *http.Response, ttlMayb
t.Errorf("unexpected Cache-Control header from %s: expected %q; got %q", urlstr, expCacheControl, cacheControl)
}
}

func checkETag(t *testing.T, urlstr string, resp *http.Response, expETagSansQuotes string) {
expETag := ""
if expETagSansQuotes != "" {
expETag = "\"" + expETagSansQuotes + "\""
}
etag := resp.Header.Get("ETag")
if etag != expETag {
t.Errorf("unexpected ETag header from %s: expected %q; got %q", urlstr, expETag, etag)
}
}
10 changes: 10 additions & 0 deletions test/sharness/lib/test-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,16 @@ test_should_contain() {
fi
}

test_should_not_contain() {
test "$#" = 2 || error "bug in the test script: not 2 parameters to test_should_not_contain"
if grep -q "$1" "$2"
then
echo "'$2' should not contain '$1', but it does:"
cat "$2"
return 1
fi
}

test_str_contains() {
find=$1
shift
Expand Down
58 changes: 58 additions & 0 deletions test/sharness/t0110-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ test_expect_success "$ITEM: has long Cache-Control max-age" '
test_expect_max_age actual.headers "$immutable_ttl"
'

# The go http package replaces ETag with Etag. Accept both. (HTTP header
# names are case-insensitive but ETag is the official name.)
test_expect_success "$ITEM: has ETag" '
test_should_contain "^E[Tt]ag: \"$HASH1\".\$" actual.headers
'

ITEM="GET IPFS path on API"

test_expect_success "$ITEM: forbidden (403)" '
Expand All @@ -95,6 +101,10 @@ test_expect_success "$ITEM: has no Cache-Control max-age" '
test_expect_no_max_age actual.headers
'

test_expect_success "$ITEM: has no ETag" '
test_should_not_contain "^ETag: " actual.headers
'

ITEM="GET IPFS directory path"

test_expect_success "$ITEM: succeeds" '
Expand All @@ -111,6 +121,10 @@ test_expect_success "$ITEM: has long Cache-Control max-age" '
test_expect_max_age actual.headers "$immutable_ttl"
'

test_expect_success "$ITEM: has ETag" '
test_should_contain "^E[Tt]ag: \"$HASH2\".\$" actual.headers
'

ITEM="GET IPFS directory file"

test_expect_success "$ITEM: succeeds" '
Expand All @@ -127,6 +141,11 @@ test_expect_success "$ITEM: has long Cache-Control max-age" '
test_expect_max_age actual.headers "$immutable_ttl"
'

test_expect_success "$ITEM: has ETag" '
hash=$(ipfs add -q "$DATA2"/test) &&
test_should_contain "^E[Tt]ag: \"$hash\".\$" actual.headers
'

ITEM="GET IPFS directory path with index.html"

test_expect_success "$ITEM: succeeds" '
Expand All @@ -143,6 +162,13 @@ test_expect_success "$ITEM: has long Cache-Control max-age" '
test_expect_max_age actual.headers "$immutable_ttl"
'

# The ETag is based on the hash of the path, the index.html magic does not
# affect it.
test_expect_success "$ITEM: has ETag" '
hash=$(ipfs add -r -q "$DATA2"/has-index-html | tail -n 1) &&
test_should_contain "^E[Tt]ag: \"$hash\".\$" actual.headers
'

ITEM="GET IPFS directory path/ with index.html"

test_expect_success "$ITEM: succeeds" '
Expand All @@ -159,6 +185,13 @@ test_expect_success "$ITEM: has long Cache-Control max-age" '
test_expect_max_age actual.headers "$immutable_ttl"
'

# The ETag is based on the hash of the path, the index.html magic does not
# affect it.
test_expect_success "$ITEM: has ETag" '
hash=$(ipfs add -r -q "$DATA2"/has-index-html | tail -n 1) &&
test_should_contain "^E[Tt]ag: \"$hash\".\$" actual.headers
'

ITEM="GET IPFS non-existent file"

test_expect_success "$ITEM: not found (404)" '
Expand All @@ -173,6 +206,10 @@ test_expect_success "$ITEM: has no Cache-Control max-age" '
test_expect_no_max_age actual.headers
'

test_expect_success "$ITEM: has no ETag" '
test_should_not_contain "^ETag: " actual.headers
'

ITEM="GET IPNS path"

ttl=10
Expand Down Expand Up @@ -200,6 +237,10 @@ test_expect_failure "$ITEM: has Cache-Control max-age=$ttl" '
test_expect_max_age actual.headers "$ttl"
'

test_expect_failure "$ITEM: has ETag" '
test_should_contain "^E[Tt]ag: \"$HASH1\".\$" actual.headers
'

ITEM="GET IPNS path again before cache expiry"

test_expect_failure "$ITEM: succeeds" '
Expand All @@ -220,6 +261,10 @@ test_expect_failure "$ITEM: has Cache-Control max-age between 0 and $ttl" '
test_fsh cat actual.headers
'

test_expect_failure "$ITEM: has ETag" '
test_should_contain "^E[Tt]ag: \"$HASH1\".\$" actual.headers
'

ITEM="GET IPNS path again after cache expiry"

test_expect_failure "$ITEM: IPNS publish with default TTL succeeds" '
Expand All @@ -242,6 +287,11 @@ test_expect_failure "$ITEM: has default Cache-Control max-age" '
test_expect_max_age actual.headers "$unknown_ttl"
'

test_expect_failure "$ITEM: has ETag" '
hash=$(ipfs add -q "$DATA2"/test) &&
test_should_contain "^E[Tt]ag: \"$hash\".\$" actual.headers
'

ITEM="GET invalid IPFS path"

test_expect_success "$ITEM: bad request (400)" '
Expand All @@ -254,6 +304,10 @@ test_expect_success "$ITEM: has no Cache-Control max-age" '
test_expect_no_max_age actual.headers
'

test_expect_success "$ITEM: has no ETag" '
test_should_not_contain "^ETag: " actual.headers
'

ITEM="GET invalid root path"

test_expect_success "$ITEM: not found (404)" '
Expand All @@ -266,6 +320,10 @@ test_expect_success "$ITEM: has no Cache-Control max-age" '
test_expect_no_max_age actual.headers
'

test_expect_success "$ITEM: has no ETag" '
test_should_not_contain "^ETag: " actual.headers
'

test_expect_success "GET /webui returns code expected" '
test_curl_resp_http_code "http://127.0.0.1:$apiport/webui" "HTTP/1.1 302 Found" "HTTP/1.1 301 Moved Permanently"
'
Expand Down

0 comments on commit c0b09e0

Please sign in to comment.