From 7cf5e87cfe881fd09486ea038fa7870f60afe54f Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Thu, 23 Jul 2015 18:44:46 -0700 Subject: [PATCH 1/6] Added API + Gateway support for arbitrary HTTP headers This commit fixes + improves CORS support License: MIT Signed-off-by: Juan Batiz-Benet --- cmd/ipfs/daemon.go | 45 ++++++++++++++++++++------ commands/http/handler.go | 58 ++++++++++++++++++++++----------- commands/http/handler_test.go | 16 +++++++-- core/corehttp/commands.go | 61 +++++++++++++++++++++++++++++++---- repo/config/api.go | 5 +++ repo/config/config.go | 1 + repo/config/gateway.go | 1 + 7 files changed, 151 insertions(+), 36 deletions(-) create mode 100644 repo/config/api.go diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 0c31ce01ae3..d14770ebf8c 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -61,20 +61,47 @@ The API address can be changed the same way: Make sure to restart the daemon after changing addresses. -By default, the gateway is only accessible locally. To expose it to other computers -in the network, use 0.0.0.0 as the ip address: +By default, the gateway is only accessible locally. To expose it to +other computers in the network, use 0.0.0.0 as the ip address: ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080 -Be careful if you expose the API. It is a security risk, as anyone could control -your node remotely. If you need to control the node remotely, make sure to protect -the port as you would other services or database (firewall, authenticated proxy, etc). +Be careful if you expose the API. It is a security risk, as anyone could +control your node remotely. If you need to control the node remotely, +make sure to protect the port as you would other services or database +(firewall, authenticated proxy, etc). -In order to explicitly allow Cross-Origin requests, export the root url as -environment variable API_ORIGIN. For example, to allow a local server at port 8888, -run this then restart the daemon: +HTTP Headers - export API_ORIGIN="http://localhost:8888/`, +IPFS supports passing arbitrary headers to the API and Gateway. You can +do this by setting headers on the API.HTTPHeaders and Gateway.HTTPHeaders +keys: + + ipfs config --json API.HTTPHeaders.X-Special-Header '["so special :)"]' + ipfs config --json Gateway.HTTPHeaders.X-Special-Header '["so special :)"]' + +Note that the value of the keys is an _array_ of strings. This is because +headers can have more than one value, and it is convenient to pass through +to other libraries. + +CORS Headers (for API) + +You can setup CORS headers the same way: + + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]' + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' + + +DEPRECATION NOTICE + +Previously, IPFS used an environment variable as seen below: + + export API_ORIGIN="http://localhost:8888/" + +This is deprecated. It is still honored in this version, but will be removed in a +future version, along with this notice. Please move to setting the HTTP Headers. +`, }, Options: []cmds.Option{ diff --git a/commands/http/handler.go b/commands/http/handler.go index 7fa7f45521c..763da51b779 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" cmds "github.com/ipfs/go-ipfs/commands" u "github.com/ipfs/go-ipfs/util" @@ -46,33 +46,51 @@ const ( plainText = "text/plain" ) +var localhostOrigins = []string{ + "http://127.0.0.1", + "https://127.0.0.1", + "http://localhost", + "https://localhost", +} + var mimeTypes = map[string]string{ cmds.JSON: "application/json", cmds.XML: "application/xml", cmds.Text: "text/plain", } -func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Handler { - // allow whitelisted origins (so we can make API requests from the browser) - if len(allowedOrigin) > 0 { - log.Info("Allowing API requests from origin: " + allowedOrigin) +type ServerConfig struct { + // AddHeaders is an optional function that gets to write additional + // headers to HTTP responses to the API requests. + AddHeaders func(http.Header) + + // CORSOpts is a set of options for CORS headers. + CORSOpts *cors.Options +} + +func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler { + if cfg == nil { + cfg = &ServerConfig{} } - // Create a handler for the API. - internal := internalHandler{ctx, root} + if cfg.CORSOpts == nil { + cfg.CORSOpts = new(cors.Options) + } - // Create a CORS object for wrapping the internal handler. - c := cors.New(cors.Options{ - AllowedMethods: []string{"GET", "POST", "PUT"}, + // by default, use GET, PUT, POST + if cfg.CORSOpts.AllowedMethods == nil { + cfg.CORSOpts.AllowedMethods = []string{"GET", "POST", "PUT"} + } - // use AllowOriginFunc instead of AllowedOrigins because we want to be - // restrictive by default. - AllowOriginFunc: func(origin string) bool { - return (allowedOrigin == "*") || (origin == allowedOrigin) - }, - }) + // by default, only let 127.0.0.1 through. + if cfg.CORSOpts.AllowedOrigins == nil { + cfg.CORSOpts.AllowedOrigins = localhostOrigins + } // Wrap the internal handler with CORS handling-middleware. + // Create a handler for the API. + internal := internalHandler{ctx, root} + c := cors.New(*cfg.CORSOpts) return &Handler{internal, c.Handler(internal)} } @@ -129,7 +147,7 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { res := i.root.Call(req) // now handle responding to the client properly - sendResponse(w, req, res) + sendResponse(w, r, req, res) } func guessMimeType(res cmds.Response) (string, error) { @@ -145,7 +163,7 @@ func guessMimeType(res cmds.Response) (string, error) { return mimeTypes[enc], nil } -func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) { +func sendResponse(w http.ResponseWriter, r *http.Request, req cmds.Request, res cmds.Response) { mime, err := guessMimeType(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -203,6 +221,10 @@ func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) { } h.Set(transferEncodingHeader, "chunked") + if r.Method == "HEAD" { // after all the headers. + return + } + if err := writeResponse(status, w, out); err != nil { log.Error("error while writing stream", err) } diff --git a/commands/http/handler_test.go b/commands/http/handler_test.go index 1d622e048c4..17a2ba36dcd 100644 --- a/commands/http/handler_test.go +++ b/commands/http/handler_test.go @@ -5,6 +5,8 @@ import ( "net/http/httptest" "testing" + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + "github.com/ipfs/go-ipfs/commands" ) @@ -16,12 +18,20 @@ func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]s } } +func originCfg(origin string) *ServerConfig { + return &ServerConfig{ + CORSOpts: &cors.Options{ + AllowedOrigins: []string{origin}, + }, + } +} + func TestDisallowedOrigin(t *testing.T) { res := httptest.NewRecorder() req, _ := http.NewRequest("GET", "http://example.com/foo", nil) req.Header.Add("Origin", "http://barbaz.com") - handler := NewHandler(commands.Context{}, nil, "") + handler := NewHandler(commands.Context{}, nil, originCfg("")) handler.ServeHTTP(res, req) assertHeaders(t, res.Header(), map[string]string{ @@ -38,7 +48,7 @@ func TestWildcardOrigin(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com/foo", nil) req.Header.Add("Origin", "http://foobar.com") - handler := NewHandler(commands.Context{}, nil, "*") + handler := NewHandler(commands.Context{}, nil, originCfg("*")) handler.ServeHTTP(res, req) assertHeaders(t, res.Header(), map[string]string{ @@ -57,7 +67,7 @@ func TestAllowedMethod(t *testing.T) { req.Header.Add("Origin", "http://www.foobar.com") req.Header.Add("Access-Control-Request-Method", "PUT") - handler := NewHandler(commands.Context{}, nil, "http://www.foobar.com") + handler := NewHandler(commands.Context{}, nil, originCfg("http://www.foobar.com")) handler.ServeHTTP(res, req) assertHeaders(t, res.Header(), map[string]string{ diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index f3e5c8a456f..f8e676600d3 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -3,22 +3,71 @@ package corehttp import ( "net/http" "os" + "strings" + + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" commands "github.com/ipfs/go-ipfs/commands" cmdsHttp "github.com/ipfs/go-ipfs/commands/http" core "github.com/ipfs/go-ipfs/core" corecommands "github.com/ipfs/go-ipfs/core/commands" + config "github.com/ipfs/go-ipfs/repo/config" ) -const ( - // TODO rename - originEnvKey = "API_ORIGIN" -) +const originEnvKey = "API_ORIGIN" +const originEnvKeyDeprecate = `You are using the ` + originEnvKey + `ENV Variable. +This functionality is deprecated, and will be removed in future versions. +Instead, try either adding headers to the config, or passing them via +cli arguments: + + ipfs config API.HTTPHeaders 'Access-Control-Allow-Origin' '*' + ipfs daemon + +or + + ipfs daemon --api-http-header 'Access-Control-Allow-Origin: *' +` + +func addCORSFromEnv(c *cmdsHttp.ServerConfig) { + origin := os.Getenv(originEnvKey) + if origin != "" { + log.Warning(originEnvKeyDeprecate) + if c.CORSOpts == nil { + c.CORSOpts.AllowedOrigins = []string{origin} + } + c.CORSOpts.AllowedOrigins = append(c.CORSOpts.AllowedOrigins, origin) + } +} + +func addCORSFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { + log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders) + + if acao := nc.API.HTTPHeaders["Access-Control-Allow-Origin"]; acao != nil { + c.CORSOpts.AllowedOrigins = acao + } + if acam := nc.API.HTTPHeaders["Access-Control-Allow-Methods"]; acam != nil { + c.CORSOpts.AllowedMethods = acam + } + if acac := nc.API.HTTPHeaders["Access-Control-Allow-Credentials"]; acac != nil { + for _, v := range acac { + c.CORSOpts.AllowCredentials = (strings.ToLower(v) == "true") + } + } +} func CommandsOption(cctx commands.Context) ServeOption { return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { - origin := os.Getenv(originEnvKey) - cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, origin) + + cfg := &cmdsHttp.ServerConfig{ + CORSOpts: &cors.Options{ + AllowedMethods: []string{"GET", "POST", "PUT"}, + }, + } + + addCORSFromConfig(cfg, n.Repo.Config()) + addCORSFromEnv(cfg) + + cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, cfg) mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler) return mux, nil } diff --git a/repo/config/api.go b/repo/config/api.go new file mode 100644 index 00000000000..b36b1080304 --- /dev/null +++ b/repo/config/api.go @@ -0,0 +1,5 @@ +package config + +type API struct { + HTTPHeaders map[string][]string // HTTP headers to return with the API. +} diff --git a/repo/config/config.go b/repo/config/config.go index ad493a18995..42b56550c9e 100644 --- a/repo/config/config.go +++ b/repo/config/config.go @@ -26,6 +26,7 @@ type Config struct { Tour Tour // local node's tour position Gateway Gateway // local node's gateway server options SupernodeRouting SupernodeClientConfig // local node's routing servers (if SupernodeRouting enabled) + API API // local node's API settings Swarm SwarmConfig Log Log } diff --git a/repo/config/gateway.go b/repo/config/gateway.go index dfb72880c60..07bc9aad2cb 100644 --- a/repo/config/gateway.go +++ b/repo/config/gateway.go @@ -2,6 +2,7 @@ package config // Gateway contains options for the HTTP gateway server. type Gateway struct { + HTTPHeaders map[string][]string // HTTP headers to return with the gateway RootRedirect string Writable bool } From c633e2b575e9a27c52139d5c6a717dae2f73b8fe Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 07:20:05 -0700 Subject: [PATCH 2/6] cmds/http: remove referrer check it used to be here for a CSRF check. but we now have CORS checks. License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/commands/http/handler.go b/commands/http/handler.go index 763da51b779..03300bf5026 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -102,20 +102,6 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Debug("Incoming API request: ", r.URL) - // error on external referers (to prevent CSRF attacks) - referer := r.Referer() - scheme := r.URL.Scheme - if len(scheme) == 0 { - scheme = "http" - } - host := fmt.Sprintf("%s://%s/", scheme, r.Host) - // empty string means the user isn't following a link (they are directly typing in the url) - if referer != "" && !strings.HasPrefix(referer, host) { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte("403 - Forbidden")) - return - } - req, err := Parse(r, i.root) if err != nil { if err == ErrNotFound { From 4a571b099b013018703951c1c8f812ceadd1fe12 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 07:50:20 -0700 Subject: [PATCH 3/6] implement arbitrary HTTP header support this commit adds the ability to specify arbitrary HTTP headers for either the Gateway or the API. simply set the desired headers on the config: ipfs config --json API.HTTPHeaders.X-MyHdr '["meow :)"]' ipfs config --json Gateway.HTTPHeaders.X-MyHdr '["meow :)"]' License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler.go | 26 ++++++++++++++++++++++---- core/corehttp/commands.go | 6 ++++-- core/corehttp/gateway.go | 4 ++++ core/corehttp/gateway_handler.go | 11 +++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/commands/http/handler.go b/commands/http/handler.go index 03300bf5026..a361aee18b6 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -21,6 +21,7 @@ var log = u.Logger("commands/http") type internalHandler struct { ctx cmds.Context root *cmds.Command + cfg *ServerConfig } // The Handler struct is funny because we want to wrap our internal handler @@ -60,14 +61,24 @@ var mimeTypes = map[string]string{ } type ServerConfig struct { - // AddHeaders is an optional function that gets to write additional - // headers to HTTP responses to the API requests. - AddHeaders func(http.Header) + // Headers is an optional map of headers that is written out. + Headers map[string][]string // CORSOpts is a set of options for CORS headers. CORSOpts *cors.Options } +func skipAPIHeader(h string) bool { + switch h { + default: + return false + case "Access-Control-Allow-Origin": + case "Access-Control-Allow-Methods": + case "Access-Control-Allow-Credentials": + } + return true +} + func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler { if cfg == nil { cfg = &ServerConfig{} @@ -89,7 +100,7 @@ func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handle // Wrap the internal handler with CORS handling-middleware. // Create a handler for the API. - internal := internalHandler{ctx, root} + internal := internalHandler{ctx, root, cfg} c := cors.New(*cfg.CORSOpts) return &Handler{internal, c.Handler(internal)} } @@ -132,6 +143,13 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // call the command res := i.root.Call(req) + // set user's headers first. + for k, v := range i.cfg.Headers { + if !skipAPIHeader(k) { + w.Header()[k] = v + } + } + // now handle responding to the client properly sendResponse(w, r, req, res) } diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index f8e676600d3..97b9c2b4be5 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -39,7 +39,7 @@ func addCORSFromEnv(c *cmdsHttp.ServerConfig) { } } -func addCORSFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { +func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders) if acao := nc.API.HTTPHeaders["Access-Control-Allow-Origin"]; acao != nil { @@ -53,6 +53,8 @@ func addCORSFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { c.CORSOpts.AllowCredentials = (strings.ToLower(v) == "true") } } + + c.Headers = nc.API.HTTPHeaders } func CommandsOption(cctx commands.Context) ServeOption { @@ -64,7 +66,7 @@ func CommandsOption(cctx commands.Context) ServeOption { }, } - addCORSFromConfig(cfg, n.Repo.Config()) + addHeadersFromConfig(cfg, n.Repo.Config()) addCORSFromEnv(cfg) cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, cfg) diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 0a84178b8c0..f70a1d11fbf 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -15,6 +15,7 @@ type Gateway struct { } type GatewayConfig struct { + Headers map[string][]string BlockList *BlockList Writable bool } @@ -27,6 +28,9 @@ func NewGateway(conf GatewayConfig) *Gateway { func (g *Gateway) ServeOption() ServeOption { return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { + // pass user's HTTP headers + g.Config.Headers = n.Repo.Config().Gateway.HTTPHeaders + gateway, err := newGatewayHandler(n, g.Config) if err != nil { return nil, err diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index 6ea7b906f38..671a1c5e85b 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -106,6 +106,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request return } + i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("X-IPFS-Path", urlPath) // Suborigin header, sandboxes apps from each other in the browser (even @@ -229,6 +230,7 @@ func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) { return } + i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("IPFS-Hash", k.String()) http.Redirect(w, r, ipfsPathPrefix+k.String(), http.StatusCreated) } @@ -242,6 +244,7 @@ func (i *gatewayHandler) putEmptyDirHandler(w http.ResponseWriter, r *http.Reque return } + i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("IPFS-Hash", key.String()) http.Redirect(w, r, ipfsPathPrefix+key.String()+"/", http.StatusCreated) } @@ -340,6 +343,7 @@ func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) { return } + i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("IPFS-Hash", key.String()) http.Redirect(w, r, ipfsPathPrefix+key.String()+"/"+strings.Join(components, "/"), http.StatusCreated) } @@ -411,10 +415,17 @@ func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { return } + i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("IPFS-Hash", key.String()) http.Redirect(w, r, ipfsPathPrefix+key.String()+"/"+strings.Join(components[:len(components)-1], "/"), http.StatusCreated) } +func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) { + for k, v := range i.config.Headers { + w.Header()[k] = v + } +} + func webError(w http.ResponseWriter, message string, err error, defaultCode int) { if _, ok := err.(path.ErrNoLink); ok { webErrorWithCode(w, message, err, http.StatusNotFound) From 5d9ee59908099df3f7e85679f7384c98d4ac8111 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 08:57:21 -0700 Subject: [PATCH 4/6] address CR comment re interface in cmds http handler https://github.com/ipfs/go-ipfs/pull/1529#discussion_r35662230 License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/commands/http/handler.go b/commands/http/handler.go index a361aee18b6..e82acd3e2ff 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -70,13 +70,15 @@ type ServerConfig struct { func skipAPIHeader(h string) bool { switch h { - default: - return false case "Access-Control-Allow-Origin": + return true case "Access-Control-Allow-Methods": + return true case "Access-Control-Allow-Credentials": + return true + default: + return false } - return true } func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler { @@ -151,7 +153,7 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // now handle responding to the client properly - sendResponse(w, r, req, res) + sendResponse(w, r, res, req) } func guessMimeType(res cmds.Response) (string, error) { @@ -167,7 +169,7 @@ func guessMimeType(res cmds.Response) (string, error) { return mimeTypes[enc], nil } -func sendResponse(w http.ResponseWriter, r *http.Request, req cmds.Request, res cmds.Response) { +func sendResponse(w http.ResponseWriter, r *http.Request, res cmds.Response, req cmds.Request) { mime, err := guessMimeType(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) From d5f94be474ebb723efa3002517a03fc79290585f Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 22:57:11 -0700 Subject: [PATCH 5/6] fix API handler to respect referer + exit on CORS this commit makes the API handler short circuit the request if the CORS headers say its not allowed. (the CORS handler only sets the headers, but does not short-circuit) It also makes the handler respect the referer again. See security discussion at https://github.com/ipfs/go-ipfs/issues/1532 License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler.go | 71 +++++++++++++++++++++++++++++++++++++++ core/corehttp/commands.go | 6 ++-- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/commands/http/handler.go b/commands/http/handler.go index e82acd3e2ff..5af088554ad 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -45,6 +45,13 @@ const ( applicationJson = "application/json" applicationOctetStream = "application/octet-stream" plainText = "text/plain" + originHeader = "origin" +) + +const ( + ACAOrigin = "Access-Control-Allow-Origin" + ACAMethods = "Access-Control-Allow-Methods" + ACACredentials = "Access-Control-Allow-Credentials" ) var localhostOrigins = []string{ @@ -115,6 +122,13 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Debug("Incoming API request: ", r.URL) + if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("403 - Forbidden")) + log.Warningf("API blocked request to %s. (possible CSRF)", r.URL) + return + } + req, err := Parse(r, i.root) if err != nil { if err == ErrNotFound { @@ -310,3 +324,60 @@ func sanitizedErrStr(err error) string { s = strings.Split(s, "\r")[0] return s } + +// allowOrigin just stops the request if the origin is not allowed. +// the CORS middleware apparently does not do this for us... +func allowOrigin(r *http.Request, cfg *ServerConfig) bool { + origin := r.Header.Get("Origin") + + // curl, or ipfs shell, typing it in manually, or clicking link + // NOT in a browser. this opens up a hole. we should close it, + // but right now it would break things. TODO + if origin == "" { + return true + } + + for _, o := range cfg.CORSOpts.AllowedOrigins { + if o == "*" { // ok! you asked for it! + return true + } + + if o == origin { // allowed explicitly + return true + } + } + + return false +} + +// allowReferer this is here to prevent some CSRF attacks that +// the API would be vulnerable to. We check that the Referer +// is allowed by CORS Origin (origins and referrers here will +// work similarly in the normla uses of the API). +// See discussion at https://github.com/ipfs/go-ipfs/issues/1532 +func allowReferer(r *http.Request, cfg *ServerConfig) bool { + referer := r.Referer() + + // curl, or ipfs shell, typing it in manually, or clicking link + // NOT in a browser. this opens up a hole. we should close it, + // but right now it would break things. TODO + if referer == "" { + return true + } + + // check CORS ACAOs and pretend Referer works like an origin. + // this is valid for many (most?) sane uses of the API in + // other applications, and will have the desired effect. + for _, o := range cfg.CORSOpts.AllowedOrigins { + if o == "*" { // ok! you asked for it! + return true + } + + // referer is allowed explicitly + if o == referer { + return true + } + } + + return false +} diff --git a/core/corehttp/commands.go b/core/corehttp/commands.go index 97b9c2b4be5..0a65fcc3a72 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -42,13 +42,13 @@ func addCORSFromEnv(c *cmdsHttp.ServerConfig) { func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders) - if acao := nc.API.HTTPHeaders["Access-Control-Allow-Origin"]; acao != nil { + if acao := nc.API.HTTPHeaders[cmdsHttp.ACAOrigin]; acao != nil { c.CORSOpts.AllowedOrigins = acao } - if acam := nc.API.HTTPHeaders["Access-Control-Allow-Methods"]; acam != nil { + if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil { c.CORSOpts.AllowedMethods = acam } - if acac := nc.API.HTTPHeaders["Access-Control-Allow-Credentials"]; acac != nil { + if acac := nc.API.HTTPHeaders[cmdsHttp.ACACredentials]; acac != nil { for _, v := range acac { c.CORSOpts.AllowCredentials = (strings.ToLower(v) == "true") } From 8f35c3bcd922b8e46cb0e4dba98e5a98ff382cea Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 28 Jul 2015 22:59:36 -0700 Subject: [PATCH 6/6] more serious CORS tests. this commit introduces more serious CORS tests that check status response codes, and run real HTTP requests. License: MIT Signed-off-by: Juan Batiz-Benet --- commands/http/handler_test.go | 359 +++++++++++++++++++++++++++++----- core/mock/mock.go | 34 +++- 2 files changed, 342 insertions(+), 51 deletions(-) diff --git a/commands/http/handler_test.go b/commands/http/handler_test.go index 17a2ba36dcd..4539d16415e 100644 --- a/commands/http/handler_test.go +++ b/commands/http/handler_test.go @@ -3,79 +3,338 @@ package http import ( "net/http" "net/http/httptest" + "net/url" "testing" cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" - "github.com/ipfs/go-ipfs/commands" + cmds "github.com/ipfs/go-ipfs/commands" + ipfscmd "github.com/ipfs/go-ipfs/core/commands" + coremock "github.com/ipfs/go-ipfs/core/mock" ) func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) { for name, value := range reqHeaders { if resHeaders.Get(name) != value { - t.Errorf("Invalid header `%s', wanted `%s', got `%s'", name, value, resHeaders.Get(name)) + t.Errorf("Invalid header '%s', wanted '%s', got '%s'", name, value, resHeaders.Get(name)) } } } -func originCfg(origin string) *ServerConfig { +func assertStatus(t *testing.T, actual, expected int) { + if actual != expected { + t.Errorf("Expected status: %d got: %d", expected, actual) + } +} + +func originCfg(origins []string) *ServerConfig { return &ServerConfig{ CORSOpts: &cors.Options{ - AllowedOrigins: []string{origin}, + AllowedOrigins: origins, + }, + } +} + +type testCase struct { + Method string + Path string + Code int + Origin string + Referer string + AllowOrigins []string + ReqHeaders map[string]string + ResHeaders map[string]string +} + +func getTestServer(t *testing.T, origins []string) *httptest.Server { + cmdsCtx, err := coremock.MockCmdsCtx() + if err != nil { + t.Error("failure to initialize mock cmds ctx", err) + return nil + } + + cmdRoot := &cmds.Command{ + Subcommands: map[string]*cmds.Command{ + "version": ipfscmd.VersionCmd, }, } + + handler := NewHandler(cmdsCtx, cmdRoot, originCfg(origins)) + return httptest.NewServer(handler) } -func TestDisallowedOrigin(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://barbaz.com") - - handler := NewHandler(commands.Context{}, nil, originCfg("")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "", - "Access-Control-Allow-Methods": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) +func (tc *testCase) test(t *testing.T) { + // defaults + method := tc.Method + if method == "" { + method = "GET" + } + + path := tc.Path + if path == "" { + path = "/api/v0/version" + } + + expectCode := tc.Code + if expectCode == 0 { + expectCode = 200 + } + + // request + req, err := http.NewRequest(method, path, nil) + if err != nil { + t.Error(err) + return + } + + for k, v := range tc.ReqHeaders { + req.Header.Add(k, v) + } + if tc.Origin != "" { + req.Header.Add("Origin", tc.Origin) + } + if tc.Referer != "" { + req.Header.Add("Referer", tc.Referer) + } + + // server + server := getTestServer(t, tc.AllowOrigins) + if server == nil { + return + } + defer server.Close() + + req.URL, err = url.Parse(server.URL + path) + if err != nil { + t.Error(err) + return + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Error(err) + return + } + + // checks + t.Log("GET", server.URL+path, req.Header, res.Header) + assertHeaders(t, res.Header, tc.ResHeaders) + assertStatus(t, res.StatusCode, expectCode) +} + +func TestDisallowedOrigins(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusForbidden, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", nil), + gtc("http://barbaz.com", []string{"http://localhost"}), + gtc("http://127.0.0.1", []string{"http://localhost"}), + gtc("http://localhost", []string{"http://127.0.0.1"}), + gtc("http://127.0.0.1:1234", nil), + gtc("http://localhost:1234", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestAllowedOrigins(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", nil), + gtc("http://127.0.0.1", nil), + } + + for _, tc := range tcs { + tc.test(t) + } } func TestWildcardOrigin(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://foobar.com") - - handler := NewHandler(commands.Context{}, nil, originCfg("*")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "http://foobar.com", - "Access-Control-Allow-Methods": "", - "Access-Control-Allow-Headers": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"*"}), + gtc("http://barbaz.com", []string{"http://localhost", "*"}), + gtc("http://127.0.0.1", []string{"http://localhost", "*"}), + gtc("http://localhost", []string{"http://127.0.0.1", "*"}), + gtc("http://127.0.0.1", []string{"*"}), + gtc("http://localhost", []string{"*"}), + gtc("http://127.0.0.1:1234", []string{"*"}), + gtc("http://localhost:1234", []string{"*"}), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestDisallowedReferer(t *testing.T) { + gtc := func(referer string, allowedOrigins []string) testCase { + return testCase{ + Origin: "http://localhost", + Referer: referer, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusForbidden, + } + } + + tcs := []testCase{ + gtc("http://foobar.com", nil), + gtc("http://localhost:1234", nil), + gtc("http://127.0.0.1:1234", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestAllowedReferer(t *testing.T) { + gtc := func(referer string, allowedOrigins []string) testCase { + return testCase{ + Origin: "http://localhost", + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", []string{"http://barbaz.com", "http://localhost"}), + gtc("http://localhost", nil), + gtc("http://127.0.0.1", nil), + } + + for _, tc := range tcs { + tc.test(t) + } +} + +func TestWildcardReferer(t *testing.T) { + gtc := func(origin string, allowedOrigins []string) testCase { + return testCase{ + Origin: origin, + AllowOrigins: allowedOrigins, + ResHeaders: map[string]string{ + ACAOrigin: origin, + ACAMethods: "", + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + }, + Code: http.StatusOK, + } + } + + tcs := []testCase{ + gtc("http://barbaz.com", []string{"*"}), + gtc("http://barbaz.com", []string{"http://localhost", "*"}), + gtc("http://127.0.0.1", []string{"http://localhost", "*"}), + gtc("http://localhost", []string{"http://127.0.0.1", "*"}), + gtc("http://127.0.0.1", []string{"*"}), + gtc("http://localhost", []string{"*"}), + gtc("http://127.0.0.1:1234", []string{"*"}), + gtc("http://localhost:1234", []string{"*"}), + } + + for _, tc := range tcs { + tc.test(t) + } } func TestAllowedMethod(t *testing.T) { - res := httptest.NewRecorder() - req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) - req.Header.Add("Origin", "http://www.foobar.com") - req.Header.Add("Access-Control-Request-Method", "PUT") - - handler := NewHandler(commands.Context{}, nil, originCfg("http://www.foobar.com")) - handler.ServeHTTP(res, req) - - assertHeaders(t, res.Header(), map[string]string{ - "Access-Control-Allow-Origin": "http://www.foobar.com", - "Access-Control-Allow-Methods": "PUT", - "Access-Control-Allow-Headers": "", - "Access-Control-Allow-Credentials": "", - "Access-Control-Max-Age": "", - "Access-Control-Expose-Headers": "", - }) + gtc := func(method string, ok bool) testCase { + code := http.StatusOK + hdrs := map[string]string{ + ACAOrigin: "http://localhost", + ACAMethods: method, + ACACredentials: "", + "Access-Control-Max-Age": "", + "Access-Control-Expose-Headers": "", + } + + if !ok { + hdrs[ACAOrigin] = "" + hdrs[ACAMethods] = "" + } + + return testCase{ + Method: "OPTIONS", + Origin: "http://localhost", + AllowOrigins: []string{"*"}, + ReqHeaders: map[string]string{ + "Access-Control-Request-Method": method, + }, + ResHeaders: hdrs, + Code: code, + } + } + + tcs := []testCase{ + gtc("PUT", true), + gtc("GET", true), + gtc("FOOBAR", false), + } + + for _, tc := range tcs { + tc.test(t) + } } diff --git a/core/mock/mock.go b/core/mock/mock.go index 14f90f56c8e..0145af6bb82 100644 --- a/core/mock/mock.go +++ b/core/mock/mock.go @@ -6,6 +6,7 @@ import ( context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" "github.com/ipfs/go-ipfs/blocks/blockstore" blockservice "github.com/ipfs/go-ipfs/blockservice" + commands "github.com/ipfs/go-ipfs/commands" core "github.com/ipfs/go-ipfs/core" "github.com/ipfs/go-ipfs/exchange/offline" mdag "github.com/ipfs/go-ipfs/merkledag" @@ -27,7 +28,7 @@ import ( // NewMockNode constructs an IpfsNode for use in tests. func NewMockNode() (*core.IpfsNode, error) { - ctx := context.TODO() + ctx := context.Background() // Generate Identity ident, err := testutil.RandIdentity() @@ -82,3 +83,34 @@ func NewMockNode() (*core.IpfsNode, error) { return nd, nil } + +func MockCmdsCtx() (commands.Context, error) { + // Generate Identity + ident, err := testutil.RandIdentity() + if err != nil { + return commands.Context{}, err + } + p := ident.ID() + + conf := config.Config{ + Identity: config.Identity{ + PeerID: p.String(), + }, + } + + node, err := core.NewIPFSNode(context.Background(), core.Offline(&repo.Mock{ + D: ds2.CloserWrap(syncds.MutexWrap(datastore.NewMapDatastore())), + C: conf, + })) + + return commands.Context{ + Online: true, + ConfigRoot: "/tmp/.mockipfsconfig", + LoadConfig: func(path string) (*config.Config, error) { + return &conf, nil + }, + ConstructNode: func() (*core.IpfsNode, error) { + return node, nil + }, + }, nil +}