diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 0c31ce01a..d14770ebf 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 7fa7f4552..763da51b7 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 1d622e048..17a2ba36d 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 f3e5c8a45..f8e676600 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 000000000..b36b10803 --- /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 ad493a189..42b56550c 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 dfb72880c..07bc9aad2 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 }