Skip to content

Commit

Permalink
fix(gateway): curl without redirect on localhost
Browse files Browse the repository at this point in the history
When request is sent to http://localhost:8080/ipfs/$cid response has
HTTP 301 status code and "Location" header with redirect destination at
$cid.ipfs.localhost:8080

Redirect is followed by browsersi, but not by commandline tools.
Status 301 is ignored by curl in default mode: it will print response
and won't follow redirect, user needs to add -L for that.

To fix curl, we return correct payload in body of HTTP 301 response,
but set Clear-Site-Data header to ensure Origin sandbox can't be abused.

This requires a surgical workaround:
If Location header is present in ResponseWriter's Header map,
we ensure http.ServeContent() returns HTTP 301

Context: #6982

License: MIT
Signed-off-by: Marcin Rataj <lidel@lidel.org>
  • Loading branch information
lidel authored and Stebalien committed Mar 18, 2020
1 parent e5a38ea commit b805817
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
20 changes: 20 additions & 0 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

Expand Down
20 changes: 18 additions & 2 deletions core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
23 changes: 22 additions & 1 deletion test/sharness/t0114-gateway-subdomains.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down

0 comments on commit b805817

Please sign in to comment.