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..5af088554ad 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" @@ -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 @@ -44,35 +45,72 @@ 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{ + "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 { + // 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 { + case "Access-Control-Allow-Origin": + return true + case "Access-Control-Allow-Methods": + return true + case "Access-Control-Allow-Credentials": + return true + default: + return false } +} - // Create a handler for the API. - internal := internalHandler{ctx, root} +func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler { + if cfg == nil { + cfg = &ServerConfig{} + } - // Create a CORS object for wrapping the internal handler. - c := cors.New(cors.Options{ - AllowedMethods: []string{"GET", "POST", "PUT"}, + if cfg.CORSOpts == nil { + cfg.CORSOpts = new(cors.Options) + } - // use AllowOriginFunc instead of AllowedOrigins because we want to be - // restrictive by default. - AllowOriginFunc: func(origin string) bool { - return (allowedOrigin == "*") || (origin == allowedOrigin) - }, - }) + // by default, use GET, PUT, POST + if cfg.CORSOpts.AllowedMethods == nil { + cfg.CORSOpts.AllowedMethods = []string{"GET", "POST", "PUT"} + } + + // 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, cfg} + c := cors.New(*cfg.CORSOpts) return &Handler{internal, c.Handler(internal)} } @@ -84,17 +122,10 @@ 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) { + 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 } @@ -128,8 +159,15 @@ 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, req, res) + sendResponse(w, r, res, req) } func guessMimeType(res cmds.Response) (string, error) { @@ -145,7 +183,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, res cmds.Response, req cmds.Request) { mime, err := guessMimeType(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -203,6 +241,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) } @@ -282,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/commands/http/handler_test.go b/commands/http/handler_test.go index 1d622e048c4..4539d16415e 100644 --- a/commands/http/handler_test.go +++ b/commands/http/handler_test.go @@ -3,69 +3,338 @@ package http import ( "net/http" "net/http/httptest" + "net/url" "testing" - "github.com/ipfs/go-ipfs/commands" + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors" + + 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 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.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 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: 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 (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, "*") - 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, "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/corehttp/commands.go b/core/corehttp/commands.go index f3e5c8a456f..0a65fcc3a72 100644 --- a/core/corehttp/commands.go +++ b/core/corehttp/commands.go @@ -3,22 +3,73 @@ 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 addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) { + log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders) + + if acao := nc.API.HTTPHeaders[cmdsHttp.ACAOrigin]; acao != nil { + c.CORSOpts.AllowedOrigins = acao + } + if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil { + c.CORSOpts.AllowedMethods = acam + } + if acac := nc.API.HTTPHeaders[cmdsHttp.ACACredentials]; acac != nil { + for _, v := range acac { + c.CORSOpts.AllowCredentials = (strings.ToLower(v) == "true") + } + } + + c.Headers = nc.API.HTTPHeaders +} 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"}, + }, + } + + addHeadersFromConfig(cfg, n.Repo.Config()) + addCORSFromEnv(cfg) + + cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, cfg) mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler) return mux, nil } 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) 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 +} 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 }