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
}