From 58aaee00f80cf913174cf47f8c40367b70b8a5df Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Tue, 5 Jul 2022 15:39:44 +0200 Subject: [PATCH] feat(gw): Cache-Control: only-if-cached This implements the only-if-cached behavior documented in specs: https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#only-if-cached-head-behavior --- core/corehttp/gateway.go | 6 +++- core/corehttp/gateway_handler.go | 46 ++++++++++++++++++++++++---- test/sharness/t0116-gateway-cache.sh | 22 +++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index dcaf92f29bf..2d300183ae8 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -92,12 +92,16 @@ func GatewayOption(writable bool, paths ...string) ServeOption { "X-Ipfs-Roots", }, headers[ACEHeadersName]...)) - var gateway http.Handler = newGatewayHandler(GatewayConfig{ + var gateway http.Handler + gateway, err = newGatewayHandler(GatewayConfig{ Headers: headers, Writable: writable, PathPrefixes: cfg.Gateway.PathPrefixes, FastDirIndexThreshold: int(cfg.Gateway.FastDirIndexThreshold.WithDefault(100)), }, api) + if err != nil { + return nil, err + } gateway = otelhttp.NewHandler(gateway, "Gateway.Request") diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index e7c6df4a66e..238473bbbac 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -24,6 +24,7 @@ import ( path "github.com/ipfs/go-path" "github.com/ipfs/go-path/resolver" coreiface "github.com/ipfs/interface-go-ipfs-core" + options "github.com/ipfs/interface-go-ipfs-core/options" ipath "github.com/ipfs/interface-go-ipfs-core/path" routing "github.com/libp2p/go-libp2p-core/routing" prometheus "github.com/prometheus/client_golang/prometheus" @@ -66,8 +67,9 @@ type redirectTemplateData struct { // gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) type gatewayHandler struct { - config GatewayConfig - api coreiface.CoreAPI + config GatewayConfig + api coreiface.CoreAPI + offlineApi coreiface.CoreAPI // generic metrics firstContentBlockGetMetric *prometheus.HistogramVec @@ -211,10 +213,15 @@ func newGatewayHistogramMetric(name string, help string) *prometheus.HistogramVe return histogramMetric } -func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler { +func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) (*gatewayHandler, error) { + offlineApi, err := api.WithOptions(options.Api.Offline(true)) + if err != nil { + return nil, err + } i := &gatewayHandler{ - config: c, - api: api, + config: c, + api: api, + offlineApi: offlineApi, // Improved Metrics // ---------------------------- // Time till the first content block (bar in /ipfs/cid/foo/bar) @@ -255,7 +262,7 @@ func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler { "The time to receive the first UnixFS node on a GET from the gateway.", ), } - return i + return i, nil } func parseIpfsPath(p string) (cid.Cid, string, error) { @@ -360,6 +367,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } contentPath := ipath.New(r.URL.Path) + + if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled { + return + } + if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled { return } @@ -956,6 +968,28 @@ func debugStr(path string) string { return q } +// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore. +// https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header +func (i *gatewayHandler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) { + if r.Header.Get("Cache-Control") == "only-if-cached" { + _, err := i.offlineApi.Block().Stat(r.Context(), contentPath) + if err != nil { + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusPreconditionFailed) + return true + } + errMsg := fmt.Sprintf("%q not in local datastore", contentPath.String()) + http.Error(w, errMsg, http.StatusPreconditionFailed) + return true + } + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return true + } + } + return false +} + func handleUnsupportedHeaders(r *http.Request) (err *requestError) { // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/kubo/issues/7702) // TODO: remove this after go-ipfs 0.13 ships diff --git a/test/sharness/t0116-gateway-cache.sh b/test/sharness/t0116-gateway-cache.sh index e5471088e2d..0b6998d2a1e 100755 --- a/test/sharness/t0116-gateway-cache.sh +++ b/test/sharness/t0116-gateway-cache.sh @@ -67,6 +67,28 @@ test_expect_success "Prepare IPNS unixfs content path for testing" ' cat curl_ipns_file_output ' +# Cache-Control: only-if-cached + test_expect_success "HEAD for /ipfs/ with only-if-cached succeeds when in local datastore" ' + curl -sv -I -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" > curl_onlyifcached_postitive_head 2>&1 && + cat curl_onlyifcached_postitive_head && + grep "< HTTP/1.1 200 OK" curl_onlyifcached_postitive_head + ' + test_expect_success "HEAD for /ipfs/ with only-if-cached fails when not in local datastore" ' + curl -sv -I -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$(date | ipfs add --only-hash -Q)" > curl_onlyifcached_negative_head 2>&1 && + cat curl_onlyifcached_negative_head && + grep "< HTTP/1.1 412 Precondition Failed" curl_onlyifcached_negative_head + ' + test_expect_success "GET for /ipfs/ with only-if-cached succeeds when in local datastore" ' + curl -svX GET -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" >/dev/null 2>curl_onlyifcached_postitive_out && + cat curl_onlyifcached_postitive_out && + grep "< HTTP/1.1 200 OK" curl_onlyifcached_postitive_out + ' + test_expect_success "GET for /ipfs/ with only-if-cached fails when not in local datastore" ' + curl -svX GET -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$(date | ipfs add --only-hash -Q)" >/dev/null 2>curl_onlyifcached_negative_out && + cat curl_onlyifcached_negative_out && + grep "< HTTP/1.1 412 Precondition Failed" curl_onlyifcached_negative_out + ' + # X-Ipfs-Path ## dir generated listing