1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-05-17 23:16:11 +08:00
Files
kubo/core/corehttp/commands.go
2023-08-30 18:51:22 +02:00

193 lines
6.0 KiB
Go

package corehttp
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
path "github.com/ipfs/boxo/path"
cmds "github.com/ipfs/go-ipfs-cmds"
cmdsHttp "github.com/ipfs/go-ipfs-cmds/http"
version "github.com/ipfs/kubo"
oldcmds "github.com/ipfs/kubo/commands"
config "github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/core"
corecommands "github.com/ipfs/kubo/core/commands"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
var errAPIVersionMismatch = errors.New("api version mismatch")
const (
originEnvKey = "API_ORIGIN"
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 --json '{"Access-Control-Allow-Origin": ["*"]}'
ipfs daemon
`
)
// APIPath is the path at which the API is mounted.
const APIPath = "/api/v0"
var defaultLocalhostOrigins = []string{
"http://127.0.0.1:<port>",
"https://127.0.0.1:<port>",
"http://[::1]:<port>",
"https://[::1]:<port>",
"http://localhost:<port>",
"https://localhost:<port>",
}
var companionBrowserExtensionOrigins = []string{
"chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch", // ipfs-companion
"chrome-extension://hjoieblefckbooibpepigmacodalfndh", // ipfs-companion-beta
}
func addCORSFromEnv(c *cmdsHttp.ServerConfig) {
origin := os.Getenv(originEnvKey)
if origin != "" {
log.Warn(originEnvKeyDeprecate)
c.AppendAllowedOrigins(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.SetAllowedOrigins(acao...)
}
if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil {
c.SetAllowedMethods(acam...)
}
for _, v := range nc.API.HTTPHeaders[cmdsHttp.ACACredentials] {
c.SetAllowCredentials(strings.ToLower(v) == "true")
}
c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)+1)
// Copy these because the config is shared and this function is called
// in multiple places concurrently. Updating these in-place *is* racy.
for h, v := range nc.API.HTTPHeaders {
h = http.CanonicalHeaderKey(h)
switch h {
case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials:
// these are handled by the CORs library.
default:
c.Headers[h] = v
}
}
c.Headers["Server"] = []string{"kubo/" + version.CurrentVersionNumber}
}
func addCORSDefaults(c *cmdsHttp.ServerConfig) {
// always safelist certain origins
c.AppendAllowedOrigins(defaultLocalhostOrigins...)
c.AppendAllowedOrigins(companionBrowserExtensionOrigins...)
// by default, use GET, PUT, POST
if len(c.AllowedMethods()) == 0 {
c.SetAllowedMethods(http.MethodGet, http.MethodPost, http.MethodPut)
}
}
func patchCORSVars(c *cmdsHttp.ServerConfig, addr net.Addr) {
// we have to grab the port from an addr, which may be an ip6 addr.
// TODO: this should take multiaddrs and derive port from there.
port := ""
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
port = strconv.Itoa(tcpaddr.Port)
} else if udpaddr, ok := addr.(*net.UDPAddr); ok {
port = strconv.Itoa(udpaddr.Port)
}
// we're listening on tcp/udp with ports. ("udp!?" you say? yeah... it happens...)
oldOrigins := c.AllowedOrigins()
newOrigins := make([]string, len(oldOrigins))
for i, o := range oldOrigins {
// TODO: allow replacing <host>. tricky, ip4 and ip6 and hostnames...
if port != "" {
o = strings.Replace(o, "<port>", port, -1)
}
newOrigins[i] = o
}
c.SetAllowedOrigins(newOrigins...)
}
func commandsOption(cctx oldcmds.Context, command *cmds.Command, allowGet bool) ServeOption {
return func(n *core.IpfsNode, l net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
cfg := cmdsHttp.NewServerConfig()
cfg.AllowGet = allowGet
corsAllowedMethods := []string{http.MethodPost}
if allowGet {
corsAllowedMethods = append(corsAllowedMethods, http.MethodGet)
}
cfg.SetAllowedMethods(corsAllowedMethods...)
cfg.APIPath = APIPath
rcfg, err := n.Repo.Config()
if err != nil {
return nil, err
}
addHeadersFromConfig(cfg, rcfg)
addCORSFromEnv(cfg)
addCORSDefaults(cfg)
patchCORSVars(cfg, l.Addr())
cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
cmdHandler = otelhttp.NewHandler(cmdHandler, "corehttp.cmdsHandler")
mux.Handle(APIPath+"/", cmdHandler)
return mux, nil
}
}
// CommandsOption constructs a ServerOption for hooking the commands into the
// HTTP server. It will NOT allow GET requests.
func CommandsOption(cctx oldcmds.Context) ServeOption {
return commandsOption(cctx, corecommands.Root, false)
}
// CommandsROOption constructs a ServerOption for hooking the read-only commands
// into the HTTP server. It will allow GET requests.
func CommandsROOption(cctx oldcmds.Context) ServeOption {
return commandsOption(cctx, corecommands.RootRO, true)
}
// CheckVersionOption returns a ServeOption that checks whether the client ipfs version matches. Does nothing when the user agent string does not contain `/kubo/` or `/go-ipfs/`
func CheckVersionOption() ServeOption {
daemonVersion := version.ApiVersion
return func(n *core.IpfsNode, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
mux := http.NewServeMux()
parent.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, APIPath) {
cmdqry := r.URL.Path[len(APIPath):]
pth := path.SplitList(cmdqry)
// backwards compatibility to previous version check
if len(pth) >= 2 && pth[1] != "version" {
clientVersion := r.UserAgent()
// skips check if client is not kubo (go-ipfs)
if (strings.Contains(clientVersion, "/go-ipfs/") || strings.Contains(clientVersion, "/kubo/")) && daemonVersion != clientVersion {
http.Error(w, fmt.Sprintf("%s (%s != %s)", errAPIVersionMismatch, daemonVersion, clientVersion), http.StatusBadRequest)
return
}
}
}
mux.ServeHTTP(w, r)
})
return mux, nil
}
}