1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-09-10 14:34:24 +08:00

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>
This commit is contained in:
Juan Batiz-Benet
2015-07-23 18:44:46 -07:00
parent e517b657fc
commit 7cf5e87cfe
7 changed files with 151 additions and 36 deletions

View File

@ -61,20 +61,47 @@ The API address can be changed the same way:
Make sure to restart the daemon after changing addresses. Make sure to restart the daemon after changing addresses.
By default, the gateway is only accessible locally. To expose it to other computers By default, the gateway is only accessible locally. To expose it to
in the network, use 0.0.0.0 as the ip address: 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 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 Be careful if you expose the API. It is a security risk, as anyone could
your node remotely. If you need to control the node remotely, make sure to protect control your node remotely. If you need to control the node remotely,
the port as you would other services or database (firewall, authenticated proxy, etc). 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 HTTP Headers
environment variable API_ORIGIN. For example, to allow a local server at port 8888,
run this then restart the daemon:
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{ Options: []cmds.Option{

View File

@ -9,7 +9,7 @@ import (
"strconv" "strconv"
"strings" "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" cmds "github.com/ipfs/go-ipfs/commands"
u "github.com/ipfs/go-ipfs/util" u "github.com/ipfs/go-ipfs/util"
@ -46,33 +46,51 @@ const (
plainText = "text/plain" 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{ var mimeTypes = map[string]string{
cmds.JSON: "application/json", cmds.JSON: "application/json",
cmds.XML: "application/xml", cmds.XML: "application/xml",
cmds.Text: "text/plain", cmds.Text: "text/plain",
} }
func NewHandler(ctx cmds.Context, root *cmds.Command, allowedOrigin string) *Handler { type ServerConfig struct {
// allow whitelisted origins (so we can make API requests from the browser) // AddHeaders is an optional function that gets to write additional
if len(allowedOrigin) > 0 { // headers to HTTP responses to the API requests.
log.Info("Allowing API requests from origin: " + allowedOrigin) 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. if cfg.CORSOpts == nil {
internal := internalHandler{ctx, root} cfg.CORSOpts = new(cors.Options)
}
// Create a CORS object for wrapping the internal handler. // by default, use GET, PUT, POST
c := cors.New(cors.Options{ if cfg.CORSOpts.AllowedMethods == nil {
AllowedMethods: []string{"GET", "POST", "PUT"}, cfg.CORSOpts.AllowedMethods = []string{"GET", "POST", "PUT"}
}
// use AllowOriginFunc instead of AllowedOrigins because we want to be // by default, only let 127.0.0.1 through.
// restrictive by default. if cfg.CORSOpts.AllowedOrigins == nil {
AllowOriginFunc: func(origin string) bool { cfg.CORSOpts.AllowedOrigins = localhostOrigins
return (allowedOrigin == "*") || (origin == allowedOrigin) }
},
})
// Wrap the internal handler with CORS handling-middleware. // 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)} 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) res := i.root.Call(req)
// now handle responding to the client properly // now handle responding to the client properly
sendResponse(w, req, res) sendResponse(w, r, req, res)
} }
func guessMimeType(res cmds.Response) (string, error) { func guessMimeType(res cmds.Response) (string, error) {
@ -145,7 +163,7 @@ func guessMimeType(res cmds.Response) (string, error) {
return mimeTypes[enc], nil 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) mime, err := guessMimeType(res)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) 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") h.Set(transferEncodingHeader, "chunked")
if r.Method == "HEAD" { // after all the headers.
return
}
if err := writeResponse(status, w, out); err != nil { if err := writeResponse(status, w, out); err != nil {
log.Error("error while writing stream", err) log.Error("error while writing stream", err)
} }

View File

@ -5,6 +5,8 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
"github.com/ipfs/go-ipfs/commands" "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) { func TestDisallowedOrigin(t *testing.T) {
res := httptest.NewRecorder() res := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "http://example.com/foo", nil) req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
req.Header.Add("Origin", "http://barbaz.com") req.Header.Add("Origin", "http://barbaz.com")
handler := NewHandler(commands.Context{}, nil, "") handler := NewHandler(commands.Context{}, nil, originCfg(""))
handler.ServeHTTP(res, req) handler.ServeHTTP(res, req)
assertHeaders(t, res.Header(), map[string]string{ 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, _ := http.NewRequest("GET", "http://example.com/foo", nil)
req.Header.Add("Origin", "http://foobar.com") req.Header.Add("Origin", "http://foobar.com")
handler := NewHandler(commands.Context{}, nil, "*") handler := NewHandler(commands.Context{}, nil, originCfg("*"))
handler.ServeHTTP(res, req) handler.ServeHTTP(res, req)
assertHeaders(t, res.Header(), map[string]string{ 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("Origin", "http://www.foobar.com")
req.Header.Add("Access-Control-Request-Method", "PUT") 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) handler.ServeHTTP(res, req)
assertHeaders(t, res.Header(), map[string]string{ assertHeaders(t, res.Header(), map[string]string{

View File

@ -3,22 +3,71 @@ package corehttp
import ( import (
"net/http" "net/http"
"os" "os"
"strings"
cors "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/rs/cors"
commands "github.com/ipfs/go-ipfs/commands" commands "github.com/ipfs/go-ipfs/commands"
cmdsHttp "github.com/ipfs/go-ipfs/commands/http" cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
core "github.com/ipfs/go-ipfs/core" core "github.com/ipfs/go-ipfs/core"
corecommands "github.com/ipfs/go-ipfs/core/commands" corecommands "github.com/ipfs/go-ipfs/core/commands"
config "github.com/ipfs/go-ipfs/repo/config"
) )
const ( const originEnvKey = "API_ORIGIN"
// TODO rename const originEnvKeyDeprecate = `You are using the ` + originEnvKey + `ENV Variable.
originEnvKey = "API_ORIGIN" 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 { func CommandsOption(cctx commands.Context) ServeOption {
return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) { 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) mux.Handle(cmdsHttp.ApiPath+"/", cmdHandler)
return mux, nil return mux, nil
} }

5
repo/config/api.go Normal file
View File

@ -0,0 +1,5 @@
package config
type API struct {
HTTPHeaders map[string][]string // HTTP headers to return with the API.
}

View File

@ -26,6 +26,7 @@ type Config struct {
Tour Tour // local node's tour position Tour Tour // local node's tour position
Gateway Gateway // local node's gateway server options Gateway Gateway // local node's gateway server options
SupernodeRouting SupernodeClientConfig // local node's routing servers (if SupernodeRouting enabled) SupernodeRouting SupernodeClientConfig // local node's routing servers (if SupernodeRouting enabled)
API API // local node's API settings
Swarm SwarmConfig Swarm SwarmConfig
Log Log Log Log
} }

View File

@ -2,6 +2,7 @@ package config
// Gateway contains options for the HTTP gateway server. // Gateway contains options for the HTTP gateway server.
type Gateway struct { type Gateway struct {
HTTPHeaders map[string][]string // HTTP headers to return with the gateway
RootRedirect string RootRedirect string
Writable bool Writable bool
} }