diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index cf74202434a..d3c4d26392d 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -38,6 +38,25 @@ type gatewayHandler struct { api coreiface.CoreAPI } +// StatusResponseWriter enables us to override HTTP Status Code passed to +// WriteHeader function inside of http.ServeContent. Decision is based on +// presence of HTTP Headers such as Location. +type statusResponseWriter struct { + http.ResponseWriter +} + +func (sw *statusResponseWriter) WriteHeader(code int) { + // Check if we need to adjust Status Code to account for scheduled redirect + // This enables us to return payload along with HTTP 301 + // for subdomain redirect in web browsers while also returning body for cli + // tools which do not follow redirects by default (curl, wget). + redirect := sw.ResponseWriter.Header().Get("Location") + if redirect != "" && code == http.StatusOK { + code = http.StatusMovedPermanently + } + sw.ResponseWriter.WriteHeader(code) +} + func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler { i := &gatewayHandler{ config: c, @@ -366,6 +385,7 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam } w.Header().Set("Content-Type", ctype) + w = &statusResponseWriter{w} http.ServeContent(w, req, name, modtime, content) } diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index c2977f5174c..2bb508707f6 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -95,8 +95,24 @@ func HostnameOption() ServeOption { // Yes, redirect if applicable // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link if newURL, ok := toSubdomainURL(r.Host, r.URL.Path, r); ok { - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - return + // Just to be sure single Origin can't be abused in + // web browsers that ignored the redirect for some + // reason, Clear-Site-Data header clears browsing + // data (cookies, storage etc) associated with + // hostname's root Origin + // Note: we can't use "*" due to bug in Chromium: + // https://bugs.chromium.org/p/chromium/issues/detail?id=898503 + w.Header().Set("Clear-Site-Data", "\"cookies\", \"storage\"") + + // Set "Location" header with redirect destination. + // It is ignored by curl in default mode, but will + // be respected by user agents that follow + // redirects by default, namely web browsers + w.Header().Set("Location", newURL) + + // Note: we continue regular gateway processing: + // HTTP Status Code http.StatusMovedPermanently + // will be set later, in statusResponseWriter } } diff --git a/test/sharness/t0114-gateway-subdomains.sh b/test/sharness/t0114-gateway-subdomains.sh index 38aa4cc9710..d079f56e6c7 100755 --- a/test/sharness/t0114-gateway-subdomains.sh +++ b/test/sharness/t0114-gateway-subdomains.sh @@ -145,10 +145,31 @@ test_localhost_gateway_response_should_contain \ # payload directly, but redirect to URL with proper origin isolation test_localhost_gateway_response_should_contain \ - "request for localhost/ipfs/{CIDv1} redirects to subdomain" \ + "request for localhost/ipfs/{CIDv1} returns status code HTTP 301" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "301 Moved Permanently" + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} returns Location HTTP header for subdomain redirect in browsers" \ "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ "Location: http://$CIDv1.ipfs.localhost:$GWAY_PORT/" +# Responses to the root domain of subdomain gateway hostname should Clear-Site-Data +# https://github.com/ipfs/go-ipfs/issues/6975#issuecomment-597472477 +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} returns Clear-Site-Data header to purge Origin cookies and storage" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + 'Clear-Site-Data: \"cookies\", \"storage\"' + +# We return body with HTTP 301 so existing cli scripts that use path-based +# gateway do not break (curl doesn't auto-redirect without passing -L; wget +# does not span across hostnames by default) +# Context: https://github.com/ipfs/go-ipfs/issues/6975 +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} includes valid payload in body for CLI tools like curl" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "$CID_VAL" + test_localhost_gateway_response_should_contain \ "request for localhost/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain" \ "http://localhost:$GWAY_PORT/ipfs/$CIDv0" \