mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-27 21:37:57 +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:
parent
e517b657fc
commit
7cf5e87cfe
@ -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{
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
5
repo/config/api.go
Normal file
5
repo/config/api.go
Normal file
@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type API struct {
|
||||
HTTPHeaders map[string][]string // HTTP headers to return with the API.
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user