@cryptotaxi247 / kubo / commits / 7cf5e87cf

Added API + Gateway support for arbitrary HTTP headers

This commit fixes + improves CORS support License: MIT Signed-off-by: Juan Batiz-Benet <juan@benet.ai>

Juan Batiz-Benet committed Jul 23, 2015 at 18:44 UTC 7cf5e87cfe881fd09486ea038fa7870f60afe54f
7 files changed +151 -36
cmd/ipfs/daemon.go
+36 -9
@@ -61,20 +61,47 @@ The API address can be changed the same way:
61
62 Make sure to restart the daemon after changing addresses.
63
64 -By default, the gateway is only accessible locally. To expose it to other computers
65 -in the network, use 0.0.0.0 as the ip address:
64 +By default, the gateway is only accessible locally. To expose it to
65 +other computers in the network, use 0.0.0.0 as the ip address:
66
67 ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080
68
69 -Be careful if you expose the API. It is a security risk, as anyone could control
70 -your node remotely. If you need to control the node remotely, make sure to protect
71 -the port as you would other services or database (firewall, authenticated proxy, etc).
69 +Be careful if you expose the API. It is a security risk, as anyone could
70 +control your node remotely. If you need to control the node remotely,
71 +make sure to protect the port as you would other services or database
72 +(firewall, authenticated proxy, etc).
73
73 -In order to explicitly allow Cross-Origin requests, export the root url as
74 -environment variable API_ORIGIN. For example, to allow a local server at port 8888,
75 -run this then restart the daemon:
74 +HTTP Headers
75
77 - export API_ORIGIN="http://localhost:8888/`,
76 +IPFS supports passing arbitrary headers to the API and Gateway. You can
77 +do this by setting headers on the API.HTTPHeaders and Gateway.HTTPHeaders
78 +keys:
79 +
80 + ipfs config --json API.HTTPHeaders.X-Special-Header '["so special :)"]'
81 + ipfs config --json Gateway.HTTPHeaders.X-Special-Header '["so special :)"]'
82 +
83 +Note that the value of the keys is an _array_ of strings. This is because
84 +headers can have more than one value, and it is convenient to pass through
85 +to other libraries.
86 +
87 +CORS Headers (for API)
88 +
89 +You can setup CORS headers the same way:
90 +
91 + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
92 + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
93 + ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
94 +
95 +
96 +DEPRECATION NOTICE
97 +
98 +Previously, IPFS used an environment variable as seen below:
99 +
100 + export API_ORIGIN="http://localhost:8888/"
101 +
102 +This is deprecated. It is still honored in this version, but will be removed in a
103 +future version, along with this notice. Please move to setting the HTTP Headers.
104 +`,
105 },
106
107 Options: []cmds.Option{
commands/http/handler.go
+40 -18
@@ -9,7 +9,7 @@ import (
9 "strconv"
10 "strings"
11
12 - "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
12 + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
13
14 cmds "github.com/ipfs/go-ipfs/commands"
15 u "github.com/ipfs/go-ipfs/util"
@@ -46,33 +46,51 @@ const (
46 plainText = "text/plain"
47 )
48
49 +var localhostOrigins = []string{
50 + "http://127.0.0.1",
51 + "https://127.0.0.1",
52 + "http://localhost",
53 + "https://localhost",
54 +}
55 +
56 var mimeTypes = map[string]string{
57 cmds.JSON: "application/json",
58 cmds.XML: "application/xml",
59 cmds.Text: "text/plain",
60 }
61
55 -func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Handler {
56 - // allow whitelisted origins (so we can make API requests from the browser)
57 - if len(allowedOrigin) > 0 {
58 - log.Info("Allowing API requests from origin: " + allowedOrigin)
62 +type ServerConfig struct {
63 + // AddHeaders is an optional function that gets to write additional
64 + // headers to HTTP responses to the API requests.
65 + AddHeaders func(http.Header)
66 +
67 + // CORSOpts is a set of options for CORS headers.
68 + CORSOpts *cors.Options
69 +}
70 +
71 +func NewHandler(ctx cmds.Context, root *cmds.Command, cfg *ServerConfig) *Handler {
72 + if cfg == nil {
73 + cfg = &ServerConfig{}
74 }
75
61 - // Create a handler for the API.
62 - internal := internalHandler{ctx, root}
76 + if cfg.CORSOpts == nil {
77 + cfg.CORSOpts = new(cors.Options)
78 + }
79
64 - // Create a CORS object for wrapping the internal handler.
65 - c := cors.New(cors.Options{
66 - AllowedMethods: []string{"GET", "POST", "PUT"},
80 + // by default, use GET, PUT, POST
81 + if cfg.CORSOpts.AllowedMethods == nil {
82 + cfg.CORSOpts.AllowedMethods = []string{"GET", "POST", "PUT"}
83 + }
84
68 - // use AllowOriginFunc instead of AllowedOrigins because we want to be
69 - // restrictive by default.
70 - AllowOriginFunc: func(origin string) bool {
71 - return (allowedOrigin == "*") || (origin == allowedOrigin)
72 - },
73 - })
85 + // by default, only let 127.0.0.1 through.
86 + if cfg.CORSOpts.AllowedOrigins == nil {
87 + cfg.CORSOpts.AllowedOrigins = localhostOrigins
88 + }
89
90 // Wrap the internal handler with CORS handling-middleware.
91 + // Create a handler for the API.
92 + internal := internalHandler{ctx, root}
93 + c := cors.New(*cfg.CORSOpts)
94 return &Handler{internal, c.Handler(internal)}
95 }
96
@@ -129,7 +147,7 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
147 res := i.root.Call(req)
148
149 // now handle responding to the client properly
132 - sendResponse(w, req, res)
150 + sendResponse(w, r, req, res)
151 }
152
153 func guessMimeType(res cmds.Response) (string, error) {
@@ -145,7 +163,7 @@ func guessMimeType(res cmds.Response) (string, error) {
163 return mimeTypes[enc], nil
164 }
165
148 -func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) {
166 +func sendResponse(w http.ResponseWriter, r *http.Request, req cmds.Request, res cmds.Response) {
167 mime, err := guessMimeType(res)
168 if err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -203,6 +221,10 @@ func sendResponse(w http.ResponseWriter, req cmds.Request, res cmds.Response) {
221 }
222 h.Set(transferEncodingHeader, "chunked")
223
224 + if r.Method == "HEAD" { // after all the headers.
225 + return
226 + }
227 +
228 if err := writeResponse(status, w, out); err != nil {
229 log.Error("error while writing stream", err)
230 }
commands/http/handler_test.go
+13 -3
@@ -5,6 +5,8 @@ import (
5 "net/http/httptest"
6 "testing"
7
8 + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
9 +
10 "github.com/ipfs/go-ipfs/commands"
11 )
12
@@ -16,12 +18,20 @@ func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]s
18 }
19 }
20
21 +func originCfg(origin string) *ServerConfig {
22 + return &ServerConfig{
23 + CORSOpts: &cors.Options{
24 + AllowedOrigins: []string{origin},
25 + },
26 + }
27 +}
28 +
29 func TestDisallowedOrigin(t *testing.T) {
30 res := httptest.NewRecorder()
31 req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
32 req.Header.Add("Origin", "http://barbaz.com")
33
24 - handler := NewHandler(commands.Context{}, nil, "")
34 + handler := NewHandler(commands.Context{}, nil, originCfg(""))
35 handler.ServeHTTP(res, req)
36
37 assertHeaders(t, res.Header(), map[string]string{
@@ -38,7 +48,7 @@ func TestWildcardOrigin(t *testing.T) {
48 req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
49 req.Header.Add("Origin", "http://foobar.com")
50
41 - handler := NewHandler(commands.Context{}, nil, "*")
51 + handler := NewHandler(commands.Context{}, nil, originCfg("*"))
52 handler.ServeHTTP(res, req)
53
54 assertHeaders(t, res.Header(), map[string]string{
@@ -57,7 +67,7 @@ func TestAllowedMethod(t *testing.T) {
67 req.Header.Add("Origin", "http://www.foobar.com")
68 req.Header.Add("Access-Control-Request-Method", "PUT")
69
60 - handler := NewHandler(commands.Context{}, nil, "http://www.foobar.com")
70 + handler := NewHandler(commands.Context{}, nil, originCfg("http://www.foobar.com"))
71 handler.ServeHTTP(res, req)
72
73 assertHeaders(t, res.Header(), map[string]string{
core/corehttp/commands.go
+55 -6
@@ -3,22 +3,71 @@ package corehttp
3 import (
4 "net/http"
5 "os"
6 + "strings"
7 +
8 + cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
9
10 commands "github.com/ipfs/go-ipfs/commands"
11 cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
12 core "github.com/ipfs/go-ipfs/core"
13 corecommands "github.com/ipfs/go-ipfs/core/commands"
14 + config "github.com/ipfs/go-ipfs/repo/config"
15 )
16
13 -const (
14 - // TODO rename
15 - originEnvKey = "API_ORIGIN"
16 -)
17 +const originEnvKey = "API_ORIGIN"
18 +const originEnvKeyDeprecate = `You are using the ` + originEnvKey + `ENV Variable.
19 +This functionality is deprecated, and will be removed in future versions.
20 +Instead, try either adding headers to the config, or passing them via
21 +cli arguments:
22 +
23 + ipfs config API.HTTPHeaders 'Access-Control-Allow-Origin' '*'
24 + ipfs daemon
25 +
26 +or
27 +
28 + ipfs daemon --api-http-header 'Access-Control-Allow-Origin: *'
29 +`
30 +
31 +func addCORSFromEnv(c *cmdsHttp.ServerConfig) {
32 + origin := os.Getenv(originEnvKey)
33 + if origin != "" {
34 + log.Warning(originEnvKeyDeprecate)
35 + if c.CORSOpts == nil {
36 + c.CORSOpts.AllowedOrigins = []string{origin}
37 + }
38 + c.CORSOpts.AllowedOrigins = append(c.CORSOpts.AllowedOrigins, origin)
39 + }
40 +}
41 +
42 +func addCORSFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) {
43 + log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders)
44 +
45 + if acao := nc.API.HTTPHeaders["Access-Control-Allow-Origin"]; acao != nil {
46 + c.CORSOpts.AllowedOrigins = acao
47 + }
48 + if acam := nc.API.HTTPHeaders["Access-Control-Allow-Methods"]; acam != nil {
49 + c.CORSOpts.AllowedMethods = acam
50 + }
51 + if acac := nc.API.HTTPHeaders["Access-Control-Allow-Credentials"]; acac != nil {
52 + for _, v := range acac {
53 + c.CORSOpts.AllowCredentials = (strings.ToLower(v) == "true")
54 + }
55 + }
56 +}
57
58 func CommandsOption(cctx commands.Context) ServeOption {
59 return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) {
20 - origin := os.Getenv(originEnvKey)
21 - cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, origin)
60 +
61 + cfg := &cmdsHttp.ServerConfig{
62 + CORSOpts: &cors.Options{
63 + AllowedMethods: []string{"GET", "POST", "PUT"},
64 + },
65 + }
66 +
67 + addCORSFromConfig(cfg, n.Repo.Config())
68 + addCORSFromEnv(cfg)
69 +
70 + cmdHandler := cmdsHttp.NewHandler(cctx, corecommands.Root, cfg)
71 mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler)
72 return mux, nil
73 }
repo/config/api.go new
+5
@@ -0,0 +1,5 @@
1 +package config
2 +
3 +type API struct {
4 + HTTPHeaders map[string][]string // HTTP headers to return with the API.
5 +}
repo/config/config.go
+1
@@ -26,6 +26,7 @@ type Config struct {
26 Tour Tour // local node's tour position
27 Gateway Gateway // local node's gateway server options
28 SupernodeRouting SupernodeClientConfig // local node's routing servers (if SupernodeRouting enabled)
29 + API API // local node's API settings
30 Swarm SwarmConfig
31 Log Log
32 }
repo/config/gateway.go
+1
@@ -2,6 +2,7 @@ package config
2
3 // Gateway contains options for the HTTP gateway server.
4 type Gateway struct {
5 + HTTPHeaders map[string][]string // HTTP headers to return with the gateway
6 RootRedirect string
7 Writable bool
8 }