mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-30 18:13:54 +08:00
@ -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{
|
||||||
|
@ -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"
|
||||||
@ -21,6 +21,7 @@ var log = u.Logger("commands/http")
|
|||||||
type internalHandler struct {
|
type internalHandler struct {
|
||||||
ctx cmds.Context
|
ctx cmds.Context
|
||||||
root *cmds.Command
|
root *cmds.Command
|
||||||
|
cfg *ServerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Handler struct is funny because we want to wrap our internal handler
|
// The Handler struct is funny because we want to wrap our internal handler
|
||||||
@ -44,35 +45,72 @@ const (
|
|||||||
applicationJson = "application/json"
|
applicationJson = "application/json"
|
||||||
applicationOctetStream = "application/octet-stream"
|
applicationOctetStream = "application/octet-stream"
|
||||||
plainText = "text/plain"
|
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{
|
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)
|
// Headers is an optional map of headers that is written out.
|
||||||
if len(allowedOrigin) > 0 {
|
Headers map[string][]string
|
||||||
log.Info("Allowing API requests from origin: " + allowedOrigin)
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, cfg}
|
||||||
|
c := cors.New(*cfg.CORSOpts)
|
||||||
return &Handler{internal, c.Handler(internal)}
|
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) {
|
func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Debug("Incoming API request: ", r.URL)
|
log.Debug("Incoming API request: ", r.URL)
|
||||||
|
|
||||||
// error on external referers (to prevent CSRF attacks)
|
if !allowOrigin(r, i.cfg) || !allowReferer(r, i.cfg) {
|
||||||
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) {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
w.Write([]byte("403 - Forbidden"))
|
w.Write([]byte("403 - Forbidden"))
|
||||||
|
log.Warningf("API blocked request to %s. (possible CSRF)", r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,8 +159,15 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// call the command
|
// call the command
|
||||||
res := i.root.Call(req)
|
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
|
// now handle responding to the client properly
|
||||||
sendResponse(w, req, res)
|
sendResponse(w, r, res, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func guessMimeType(res cmds.Response) (string, error) {
|
func guessMimeType(res cmds.Response) (string, error) {
|
||||||
@ -145,7 +183,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, res cmds.Response, req cmds.Request) {
|
||||||
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 +241,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)
|
||||||
}
|
}
|
||||||
@ -282,3 +324,60 @@ func sanitizedErrStr(err error) string {
|
|||||||
s = strings.Split(s, "\r")[0]
|
s = strings.Split(s, "\r")[0]
|
||||||
return s
|
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
|
||||||
|
}
|
||||||
|
@ -3,69 +3,338 @@ package http
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"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) {
|
func assertHeaders(t *testing.T, resHeaders http.Header, reqHeaders map[string]string) {
|
||||||
for name, value := range reqHeaders {
|
for name, value := range reqHeaders {
|
||||||
if resHeaders.Get(name) != value {
|
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) {
|
func assertStatus(t *testing.T, actual, expected int) {
|
||||||
res := httptest.NewRecorder()
|
if actual != expected {
|
||||||
req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
|
t.Errorf("Expected status: %d got: %d", expected, actual)
|
||||||
req.Header.Add("Origin", "http://barbaz.com")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler := NewHandler(commands.Context{}, nil, "")
|
func originCfg(origins []string) *ServerConfig {
|
||||||
handler.ServeHTTP(res, req)
|
return &ServerConfig{
|
||||||
|
CORSOpts: &cors.Options{
|
||||||
|
AllowedOrigins: origins,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assertHeaders(t, res.Header(), map[string]string{
|
type testCase struct {
|
||||||
"Access-Control-Allow-Origin": "",
|
Method string
|
||||||
"Access-Control-Allow-Methods": "",
|
Path string
|
||||||
"Access-Control-Allow-Credentials": "",
|
Code int
|
||||||
"Access-Control-Max-Age": "",
|
Origin string
|
||||||
"Access-Control-Expose-Headers": "",
|
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) {
|
func TestWildcardOrigin(t *testing.T) {
|
||||||
res := httptest.NewRecorder()
|
gtc := func(origin string, allowedOrigins []string) testCase {
|
||||||
req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
|
return testCase{
|
||||||
req.Header.Add("Origin", "http://foobar.com")
|
Origin: origin,
|
||||||
|
AllowOrigins: allowedOrigins,
|
||||||
|
ResHeaders: map[string]string{
|
||||||
|
ACAOrigin: origin,
|
||||||
|
ACAMethods: "",
|
||||||
|
ACACredentials: "",
|
||||||
|
"Access-Control-Max-Age": "",
|
||||||
|
"Access-Control-Expose-Headers": "",
|
||||||
|
},
|
||||||
|
Code: http.StatusOK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler := NewHandler(commands.Context{}, nil, "*")
|
tcs := []testCase{
|
||||||
handler.ServeHTTP(res, req)
|
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{"*"}),
|
||||||
|
}
|
||||||
|
|
||||||
assertHeaders(t, res.Header(), map[string]string{
|
for _, tc := range tcs {
|
||||||
"Access-Control-Allow-Origin": "http://foobar.com",
|
tc.test(t)
|
||||||
"Access-Control-Allow-Methods": "",
|
}
|
||||||
"Access-Control-Allow-Headers": "",
|
}
|
||||||
"Access-Control-Allow-Credentials": "",
|
|
||||||
"Access-Control-Max-Age": "",
|
func TestDisallowedReferer(t *testing.T) {
|
||||||
"Access-Control-Expose-Headers": "",
|
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) {
|
func TestAllowedMethod(t *testing.T) {
|
||||||
res := httptest.NewRecorder()
|
gtc := func(method string, ok bool) testCase {
|
||||||
req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil)
|
code := http.StatusOK
|
||||||
req.Header.Add("Origin", "http://www.foobar.com")
|
hdrs := map[string]string{
|
||||||
req.Header.Add("Access-Control-Request-Method", "PUT")
|
ACAOrigin: "http://localhost",
|
||||||
|
ACAMethods: method,
|
||||||
|
ACACredentials: "",
|
||||||
|
"Access-Control-Max-Age": "",
|
||||||
|
"Access-Control-Expose-Headers": "",
|
||||||
|
}
|
||||||
|
|
||||||
handler := NewHandler(commands.Context{}, nil, "http://www.foobar.com")
|
if !ok {
|
||||||
handler.ServeHTTP(res, req)
|
hdrs[ACAOrigin] = ""
|
||||||
|
hdrs[ACAMethods] = ""
|
||||||
|
}
|
||||||
|
|
||||||
assertHeaders(t, res.Header(), map[string]string{
|
return testCase{
|
||||||
"Access-Control-Allow-Origin": "http://www.foobar.com",
|
Method: "OPTIONS",
|
||||||
"Access-Control-Allow-Methods": "PUT",
|
Origin: "http://localhost",
|
||||||
"Access-Control-Allow-Headers": "",
|
AllowOrigins: []string{"*"},
|
||||||
"Access-Control-Allow-Credentials": "",
|
ReqHeaders: map[string]string{
|
||||||
"Access-Control-Max-Age": "",
|
"Access-Control-Request-Method": method,
|
||||||
"Access-Control-Expose-Headers": "",
|
},
|
||||||
})
|
ResHeaders: hdrs,
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
gtc("PUT", true),
|
||||||
|
gtc("GET", true),
|
||||||
|
gtc("FOOBAR", false),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
tc.test(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,73 @@ 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 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 {
|
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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
addHeadersFromConfig(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
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ type Gateway struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
|
Headers map[string][]string
|
||||||
BlockList *BlockList
|
BlockList *BlockList
|
||||||
Writable bool
|
Writable bool
|
||||||
}
|
}
|
||||||
@ -27,6 +28,9 @@ func NewGateway(conf GatewayConfig) *Gateway {
|
|||||||
|
|
||||||
func (g *Gateway) ServeOption() ServeOption {
|
func (g *Gateway) ServeOption() ServeOption {
|
||||||
return func(n *core.IpfsNode, mux *http.ServeMux) (*http.ServeMux, error) {
|
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)
|
gateway, err := newGatewayHandler(n, g.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -106,6 +106,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.addUserHeaders(w) // ok, _now_ write user's headers.
|
||||||
w.Header().Set("X-IPFS-Path", urlPath)
|
w.Header().Set("X-IPFS-Path", urlPath)
|
||||||
|
|
||||||
// Suborigin header, sandboxes apps from each other in the browser (even
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.addUserHeaders(w) // ok, _now_ write user's headers.
|
||||||
w.Header().Set("IPFS-Hash", k.String())
|
w.Header().Set("IPFS-Hash", k.String())
|
||||||
http.Redirect(w, r, ipfsPathPrefix+k.String(), http.StatusCreated)
|
http.Redirect(w, r, ipfsPathPrefix+k.String(), http.StatusCreated)
|
||||||
}
|
}
|
||||||
@ -242,6 +244,7 @@ func (i *gatewayHandler) putEmptyDirHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.addUserHeaders(w) // ok, _now_ write user's headers.
|
||||||
w.Header().Set("IPFS-Hash", key.String())
|
w.Header().Set("IPFS-Hash", key.String())
|
||||||
http.Redirect(w, r, ipfsPathPrefix+key.String()+"/", http.StatusCreated)
|
http.Redirect(w, r, ipfsPathPrefix+key.String()+"/", http.StatusCreated)
|
||||||
}
|
}
|
||||||
@ -340,6 +343,7 @@ func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.addUserHeaders(w) // ok, _now_ write user's headers.
|
||||||
w.Header().Set("IPFS-Hash", key.String())
|
w.Header().Set("IPFS-Hash", key.String())
|
||||||
http.Redirect(w, r, ipfsPathPrefix+key.String()+"/"+strings.Join(components, "/"), http.StatusCreated)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.addUserHeaders(w) // ok, _now_ write user's headers.
|
||||||
w.Header().Set("IPFS-Hash", key.String())
|
w.Header().Set("IPFS-Hash", key.String())
|
||||||
http.Redirect(w, r, ipfsPathPrefix+key.String()+"/"+strings.Join(components[:len(components)-1], "/"), http.StatusCreated)
|
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) {
|
func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
|
||||||
if _, ok := err.(path.ErrNoLink); ok {
|
if _, ok := err.(path.ErrNoLink); ok {
|
||||||
webErrorWithCode(w, message, err, http.StatusNotFound)
|
webErrorWithCode(w, message, err, http.StatusNotFound)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
|
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
|
||||||
"github.com/ipfs/go-ipfs/blocks/blockstore"
|
"github.com/ipfs/go-ipfs/blocks/blockstore"
|
||||||
blockservice "github.com/ipfs/go-ipfs/blockservice"
|
blockservice "github.com/ipfs/go-ipfs/blockservice"
|
||||||
|
commands "github.com/ipfs/go-ipfs/commands"
|
||||||
core "github.com/ipfs/go-ipfs/core"
|
core "github.com/ipfs/go-ipfs/core"
|
||||||
"github.com/ipfs/go-ipfs/exchange/offline"
|
"github.com/ipfs/go-ipfs/exchange/offline"
|
||||||
mdag "github.com/ipfs/go-ipfs/merkledag"
|
mdag "github.com/ipfs/go-ipfs/merkledag"
|
||||||
@ -27,7 +28,7 @@ import (
|
|||||||
|
|
||||||
// NewMockNode constructs an IpfsNode for use in tests.
|
// NewMockNode constructs an IpfsNode for use in tests.
|
||||||
func NewMockNode() (*core.IpfsNode, error) {
|
func NewMockNode() (*core.IpfsNode, error) {
|
||||||
ctx := context.TODO()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Generate Identity
|
// Generate Identity
|
||||||
ident, err := testutil.RandIdentity()
|
ident, err := testutil.RandIdentity()
|
||||||
@ -82,3 +83,34 @@ func NewMockNode() (*core.IpfsNode, error) {
|
|||||||
|
|
||||||
return nd, nil
|
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
|
||||||
|
}
|
||||||
|
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
|
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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user