@cryptotaxi247 / kubo / commits / f9567a0a0

fix(gateway): curl without redirect on localhost

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: https://github.com/ipfs/go-ipfs/pull/6982 License: MIT Signed-off-by: Marcin Rataj <lidel@lidel.org>

Marcin Rataj committed Mar 11, 2020 at 18:07 UTC f9567a0a0fb12b2d27735708e85b9e16a84f528c
3 files changed +60 -3
core/corehttp/gateway_handler.go
+20
@@ -38,6 +38,25 @@ type gatewayHandler struct {
38 api coreiface.CoreAPI
39 }
40
41 +// StatusResponseWriter enables us to override HTTP Status Code passed to
42 +// WriteHeader function inside of http.ServeContent. Decision is based on
43 +// presence of HTTP Headers such as Location.
44 +type statusResponseWriter struct {
45 + http.ResponseWriter
46 +}
47 +
48 +func (sw *statusResponseWriter) WriteHeader(code int) {
49 + // Check if we need to adjust Status Code to account for scheduled redirect
50 + // This enables us to return payload along with HTTP 301
51 + // for subdomain redirect in web browsers while also returning body for cli
52 + // tools which do not follow redirects by default (curl, wget).
53 + redirect := sw.ResponseWriter.Header().Get("Location")
54 + if redirect != "" && code == http.StatusOK {
55 + code = http.StatusMovedPermanently
56 + }
57 + sw.ResponseWriter.WriteHeader(code)
58 +}
59 +
60 func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
61 i := &gatewayHandler{
62 config: c,
@@ -366,6 +385,7 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam
385 }
386 w.Header().Set("Content-Type", ctype)
387
388 + w = &statusResponseWriter{w}
389 http.ServeContent(w, req, name, modtime, content)
390 }
391
core/corehttp/hostname.go
+18 -2
@@ -95,8 +95,24 @@ func HostnameOption() ServeOption {
95 // Yes, redirect if applicable
96 // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
97 if newURL, ok := toSubdomainURL(r.Host, r.URL.Path, r); ok {
98 - http.Redirect(w, r, newURL, http.StatusMovedPermanently)
99 - return
98 + // Just to be sure single Origin can't be abused in
99 + // web browsers that ignored the redirect for some
100 + // reason, Clear-Site-Data header clears browsing
101 + // data (cookies, storage etc) associated with
102 + // hostname's root Origin
103 + // Note: we can't use "*" due to bug in Chromium:
104 + // https://bugs.chromium.org/p/chromium/issues/detail?id=898503
105 + w.Header().Set("Clear-Site-Data", "\"cookies\", \"storage\"")
106 +
107 + // Set "Location" header with redirect destination.
108 + // It is ignored by curl in default mode, but will
109 + // be respected by user agents that follow
110 + // redirects by default, namely web browsers
111 + w.Header().Set("Location", newURL)
112 +
113 + // Note: we continue regular gateway processing:
114 + // HTTP Status Code http.StatusMovedPermanently
115 + // will be set later, in statusResponseWriter
116 }
117 }
118
test/sharness/t0114-gateway-subdomains.sh
+22 -1
@@ -145,10 +145,31 @@ test_localhost_gateway_response_should_contain \
145 # payload directly, but redirect to URL with proper origin isolation
146
147 test_localhost_gateway_response_should_contain \
148 - "request for localhost/ipfs/{CIDv1} redirects to subdomain" \
148 + "request for localhost/ipfs/{CIDv1} returns status code HTTP 301" \
149 + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \
150 + "301 Moved Permanently"
151 +
152 +test_localhost_gateway_response_should_contain \
153 + "request for localhost/ipfs/{CIDv1} returns Location HTTP header for subdomain redirect in browsers" \
154 "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \
155 "Location: http://$CIDv1.ipfs.localhost:$GWAY_PORT/"
156
157 +# Responses to the root domain of subdomain gateway hostname should Clear-Site-Data
158 +# https://github.com/ipfs/go-ipfs/issues/6975#issuecomment-597472477
159 +test_localhost_gateway_response_should_contain \
160 + "request for localhost/ipfs/{CIDv1} returns Clear-Site-Data header to purge Origin cookies and storage" \
161 + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \
162 + 'Clear-Site-Data: \"cookies\", \"storage\"'
163 +
164 +# We return body with HTTP 301 so existing cli scripts that use path-based
165 +# gateway do not break (curl doesn't auto-redirect without passing -L; wget
166 +# does not span across hostnames by default)
167 +# Context: https://github.com/ipfs/go-ipfs/issues/6975
168 +test_localhost_gateway_response_should_contain \
169 + "request for localhost/ipfs/{CIDv1} includes valid payload in body for CLI tools like curl" \
170 + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \
171 + "$CID_VAL"
172 +
173 test_localhost_gateway_response_should_contain \
174 "request for localhost/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain" \
175 "http://localhost:$GWAY_PORT/ipfs/$CIDv0" \