From 543be29796eee1e72280d22ddde12b57ef5c7ad7 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 4 Jan 2019 11:18:24 -0800 Subject: [PATCH 1/4] api: let the CORs library handle CORs headers License: MIT Signed-off-by: Steven Allen --- core/corehttp/commands.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index 99d3b3eff70..aa2f51065fa 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -67,12 +67,17 @@ func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { } } - c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)) + c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)+1) // Copy these because the config is shared and this function is called // in multiple places concurrently. Updating these in-place *is* racy. for h, v := range nc.API.HTTPHeaders { - c.Headers[h] = v + switch h { + case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials: + // these are handled by the CORs library. + default: + c.Headers[h] = v + } } c.Headers["Server"] = []string{"go-ipfs/" + version.CurrentVersionNumber} } From 82629c00ec12e376e26b28d3551395d5e2dfb85c Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 4 Jan 2019 12:08:57 -0800 Subject: [PATCH 2/4] gateway: fix CORs headers fixes #5138 -- always add user-agent to access-control-allow-headers. fixes #5888 -- same with content-type. fixes #5892 -- extend user-provided headers instead of overriding them. License: MIT Signed-off-by: Steven Allen --- core/corehttp/gateway.go | 58 +++++++++++++++++++++++++++++++- core/corehttp/gateway_handler.go | 13 ------- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 52560750a6e..ae3e035d87e 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/http" + "sort" version "github.com/ipfs/go-ipfs" core "github.com/ipfs/go-ipfs/core" @@ -18,6 +19,26 @@ type GatewayConfig struct { PathPrefixes []string } +// A helper function to clean up a set of headers: +// 1. Canonicalizes. +// 2. Deduplicates. +// 3. Sorts. +func cleanHeaderSet(headers []string) []string { + // Deduplicate and canonicalize. + m := make(map[string]struct{}, len(headers)) + for _, h := range headers { + m[http.CanonicalHeaderKey(h)] = struct{}{} + } + result := make([]string, 0, len(m)) + for k := range m { + result = append(result, k) + } + + // Sort + sort.Strings(result) + return result +} + func GatewayOption(writable bool, paths ...string) ServeOption { return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { cfg, err := n.Repo.Config() @@ -30,8 +51,43 @@ func GatewayOption(writable bool, paths ...string) ServeOption { return nil, err } + headers := make(map[string][]string, len(cfg.Gateway.HTTPHeaders)) + for h, v := range cfg.Gateway.HTTPHeaders { + headers[h] = v + } + + // Hard-coded headers. + const ACAHeadersName = "Access-Control-Allow-Headers" + const ACEHeadersName = "Access-Control-Expose-Headers" + const ACAOriginName = "Access-Control-Allow-Origin" + const ACAMethodsName = "Access-Control-Allow-Methods" + + if _, ok := headers[ACAOriginName]; !ok { + // Default to *all* + headers[ACAOriginName] = []string{"*"} + } + if _, ok := headers[ACAMethodsName]; !ok { + // Default to GET + headers[ACAMethodsName] = []string{"GET"} + } + + headers[ACAHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Type", + "User-Agent", + "Range", + "X-Requested-With", + }, headers[ACAHeadersName]...)) + + headers[ACEHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Range", + "X-Chunked-Output", + "X-Stream-Output", + }, headers[ACEHeadersName]...)) + gateway := newGatewayHandler(n, GatewayConfig{ - Headers: cfg.Gateway.HTTPHeaders, + Headers: headers, Writable: writable, PathPrefixes: cfg.Gateway.PathPrefixes, }, api) diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index 553ebdcf8f5..bb6ff51c4c7 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -184,19 +184,6 @@ func (i *gatewayHandler) getOrHeadHandler(ctx context.Context, w http.ResponseWr w.Header().Set("X-IPFS-Path", urlPath) w.Header().Set("Etag", etag) - // set 'allowed' headers - // & expose those headers - var allowedHeadersArr = []string{ - "Content-Range", - "X-Chunked-Output", - "X-Stream-Output", - } - - var allowedHeaders = strings.Join(allowedHeadersArr, ", ") - - w.Header().Set("Access-Control-Allow-Headers", allowedHeaders) - w.Header().Set("Access-Control-Expose-Headers", allowedHeaders) - // Suborigin header, sandboxes apps from each other in the browser (even // though they are served from the same gateway domain). // From 4bbf4cc9a08a55f1e0f69447a06179f2106e48c0 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 4 Jan 2019 13:00:31 -0800 Subject: [PATCH 3/4] gateway, api: canonicalize headers from user config License: MIT Signed-off-by: Steven Allen --- core/corehttp/commands.go | 1 + core/corehttp/gateway.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index aa2f51065fa..3b2a971b9de 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -72,6 +72,7 @@ func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { // Copy these because the config is shared and this function is called // in multiple places concurrently. Updating these in-place *is* racy. for h, v := range nc.API.HTTPHeaders { + h = http.CanonicalHeaderKey(h) switch h { case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials: // these are handled by the CORs library. diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index ae3e035d87e..044144a7694 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -53,7 +53,7 @@ func GatewayOption(writable bool, paths ...string) ServeOption { headers := make(map[string][]string, len(cfg.Gateway.HTTPHeaders)) for h, v := range cfg.Gateway.HTTPHeaders { - headers[h] = v + headers[http.CanonicalHeaderKey(h)] = v } // Hard-coded headers. From b15cf0198f177be7c46813b81c4b31eb74001aaa Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 4 Jan 2019 13:18:30 -0800 Subject: [PATCH 4/4] gateway: fix cors headers tests License: MIT Signed-off-by: Steven Allen --- test/sharness/t0112-gateway-cors.sh | 55 ++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/test/sharness/t0112-gateway-cors.sh b/test/sharness/t0112-gateway-cors.sh index 2cd3fe89c9f..c7c5c3e40c6 100755 --- a/test/sharness/t0112-gateway-cors.sh +++ b/test/sharness/t0112-gateway-cors.sh @@ -18,41 +18,70 @@ test_init_ipfs test_config_ipfs_cors_headers test_launch_ipfs_daemon -gwport=$GWAY_PORT -apiport=$API_PORT thash='QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn' # Gateway # HTTP GET Request test_expect_success "GET to Gateway succeeds" ' - curl -svX GET "http://127.0.0.1:$gwport/ipfs/$thash" 2>curl_output + curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$thash" >/dev/null 2>curl_output && + cat curl_output ' -cat curl_output # GET Response from Gateway should contain CORS headers test_expect_success "GET response for Gateway resource looks good" ' - grep "Access-Control-Allow-Origin:" curl_output | grep "\*" && - grep "Access-Control-Allow-Methods:" curl_output | grep " GET\b" && - grep "Access-Control-Allow-Headers:" curl_output + grep "< Access-Control-Allow-Origin: \*" curl_output && + grep "< Access-Control-Allow-Methods: GET" curl_output && + grep "< Access-Control-Allow-Headers: Range" curl_output && + grep "< Access-Control-Expose-Headers: Content-Range" curl_output ' # HTTP OPTIONS Request test_expect_success "OPTIONS to Gateway succeeds" ' - curl -svX OPTIONS "http://127.0.0.1:$gwport/ipfs/$thash" 2>curl_output + curl -svX OPTIONS "http://127.0.0.1:$GWAY_PORT/ipfs/$thash" 2>curl_output && + cat curl_output ' + # OPTION Response from Gateway should contain CORS headers test_expect_success "OPTIONS response for Gateway resource looks good" ' - grep "Access-Control-Allow-Origin:" curl_output | grep "\*" && - grep "Access-Control-Allow-Methods:" curl_output | grep " GET\b" && - grep "Access-Control-Allow-Headers:" curl_output + grep "< Access-Control-Allow-Origin: \*" curl_output && + grep "< Access-Control-Allow-Methods: GET" curl_output && + grep "< Access-Control-Allow-Headers: Range" curl_output && + grep "< Access-Control-Expose-Headers: Content-Range" curl_output +' + +test_kill_ipfs_daemon + +# Change headers +test_expect_success "Can configure gateway headers" ' + ipfs config --json Gateway.HTTPHeaders.Access-Control-Allow-Headers "[\"X-Custom1\"]" && + ipfs config --json Gateway.HTTPHeaders.Access-Control-Expose-Headers "[\"X-Custom2\"]" && + ipfs config --json Gateway.HTTPHeaders.Access-Control-Allow-Origin "[\"localhost\"]" +' + +test_launch_ipfs_daemon + +test_expect_success "OPTIONS to Gateway succeeds" ' + curl -svX OPTIONS "http://127.0.0.1:$GWAY_PORT/ipfs/$thash" 2>curl_output && + cat curl_output +' + +test_expect_success "Access-Control-Allow-Headers extends" ' + grep "< Access-Control-Allow-Headers: Range" curl_output && + grep "< Access-Control-Allow-Headers: X-Custom1" curl_output && + grep "< Access-Control-Expose-Headers: Content-Range" curl_output && + grep "< Access-Control-Expose-Headers: X-Custom2" curl_output +' + +test_expect_success "Access-Control-Allow-Origin replaces" ' + grep "< Access-Control-Allow-Origin: localhost" curl_output ' # Read-Only API (at the Gateway Port) # HTTP GET Request test_expect_success "GET to API succeeds" ' - curl -svX GET "http://127.0.0.1:$gwport/api/v0/cat?arg=$thash" 2>curl_output + curl -svX GET "http://127.0.0.1:$GWAY_PORT/api/v0/cat?arg=$thash" >/dev/null 2>curl_output ' # GET Response from the API should NOT contain CORS headers # Blacklisting: https://git.io/vzaj2 @@ -63,7 +92,7 @@ test_expect_success "OPTIONS response for API looks good" ' # HTTP OPTIONS Request test_expect_success "OPTIONS to API succeeds" ' - curl -svX OPTIONS "http://127.0.0.1:$gwport/api/v0/cat?arg=$thash" 2>curl_output + curl -svX OPTIONS "http://127.0.0.1:$GWAY_PORT/api/v0/cat?arg=$thash" 2>curl_output ' # OPTIONS Response from the API should NOT contain CORS headers test_expect_success "OPTIONS response for API looks good" '