master
go 233 lines 7.18 KB
Raw
1 package corehttp
2
3 import (
4 "errors"
5 "fmt"
6 "net"
7 "net/http"
8 "os"
9 "strconv"
10 "strings"
11
12 cmds "github.com/ipfs/go-ipfs-cmds"
13 cmdsHttp "github.com/ipfs/go-ipfs-cmds/http"
14 version "github.com/ipfs/kubo"
15 oldcmds "github.com/ipfs/kubo/commands"
16 config "github.com/ipfs/kubo/config"
17 "github.com/ipfs/kubo/core"
18 corecommands "github.com/ipfs/kubo/core/commands"
19 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
20 )
21
22 var errAPIVersionMismatch = errors.New("api version mismatch")
23
24 const (
25 originEnvKey = "API_ORIGIN"
26 originEnvKeyDeprecate = `You are using the ` + originEnvKey + `ENV Variable.
27 This functionality is deprecated, and will be removed in future versions.
28 Instead, try either adding headers to the config, or passing them via
29 cli arguments:
30
31 ipfs config API.HTTPHeaders --json '{"Access-Control-Allow-Origin": ["*"]}'
32 ipfs daemon
33 `
34 )
35
36 // APIPath is the path at which the API is mounted.
37 const APIPath = "/api/v0"
38
39 var defaultLocalhostOrigins = []string{
40 "http://127.0.0.1:<port>",
41 "https://127.0.0.1:<port>",
42 "http://[::1]:<port>",
43 "https://[::1]:<port>",
44 "http://localhost:<port>",
45 "https://localhost:<port>",
46 }
47
48 var companionBrowserExtensionOrigins = []string{
49 "chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch", // ipfs-companion
50 "chrome-extension://hjoieblefckbooibpepigmacodalfndh", // ipfs-companion-beta
51 }
52
53 func addCORSFromEnv(c *cmdsHttp.ServerConfig) {
54 origin := os.Getenv(originEnvKey)
55 if origin != "" {
56 log.Warn(originEnvKeyDeprecate)
57 c.AppendAllowedOrigins(origin)
58 }
59 }
60
61 func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) {
62 log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders)
63
64 if acao := nc.API.HTTPHeaders[cmdsHttp.ACAOrigin]; acao != nil {
65 c.SetAllowedOrigins(acao...)
66 }
67 if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil {
68 c.SetAllowedMethods(acam...)
69 }
70 for _, v := range nc.API.HTTPHeaders[cmdsHttp.ACACredentials] {
71 c.SetAllowCredentials(strings.ToLower(v) == "true")
72 }
73
74 c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)+1)
75
76 // Copy these because the config is shared and this function is called
77 // in multiple places concurrently. Updating these in-place *is* racy.
78 for h, v := range nc.API.HTTPHeaders {
79 h = http.CanonicalHeaderKey(h)
80 switch h {
81 case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials:
82 // these are handled by the CORs library.
83 default:
84 c.Headers[h] = v
85 }
86 }
87 c.Headers["Server"] = []string{"kubo/" + version.CurrentVersionNumber}
88 }
89
90 func addCORSDefaults(c *cmdsHttp.ServerConfig) {
91 // always safelist certain origins
92 c.AppendAllowedOrigins(defaultLocalhostOrigins...)
93 c.AppendAllowedOrigins(companionBrowserExtensionOrigins...)
94
95 // by default, use GET, PUT, POST
96 if len(c.AllowedMethods()) == 0 {
97 c.SetAllowedMethods(http.MethodGet, http.MethodPost, http.MethodPut)
98 }
99 }
100
101 func patchCORSVars(c *cmdsHttp.ServerConfig, addr net.Addr) {
102 // we have to grab the port from an addr, which may be an ip6 addr.
103 // TODO: this should take multiaddrs and derive port from there.
104 port := ""
105 if tcpaddr, ok := addr.(*net.TCPAddr); ok {
106 port = strconv.Itoa(tcpaddr.Port)
107 } else if udpaddr, ok := addr.(*net.UDPAddr); ok {
108 port = strconv.Itoa(udpaddr.Port)
109 }
110
111 // we're listening on tcp/udp with ports. ("udp!?" you say? yeah... it happens...)
112 oldOrigins := c.AllowedOrigins()
113 newOrigins := make([]string, len(oldOrigins))
114 for i, o := range oldOrigins {
115 // TODO: allow replacing <host>. tricky, ip4 and ip6 and hostnames...
116 if port != "" {
117 o = strings.Replace(o, "<port>", port, -1)
118 }
119 newOrigins[i] = o
120 }
121 c.SetAllowedOrigins(newOrigins...)
122 }
123
124 func commandsOption(cctx oldcmds.Context, command *cmds.Command) ServeOption {
125 return func(n *core.IpfsNode, l net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
126 cfg := cmdsHttp.NewServerConfig()
127
128 cfg.AddAllowedHeaders("Origin", "Accept", "Content-Type", "X-Requested-With")
129 cfg.SetAllowedMethods(http.MethodPost)
130
131 cfg.APIPath = APIPath
132 rcfg, err := n.Repo.Config()
133 if err != nil {
134 return nil, err
135 }
136
137 addHeadersFromConfig(cfg, rcfg)
138 addCORSFromEnv(cfg)
139 addCORSDefaults(cfg)
140 patchCORSVars(cfg, l.Addr())
141
142 cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
143
144 if len(rcfg.API.Authorizations) > 0 {
145 authorizations := convertAuthorizationsMap(rcfg.API.Authorizations)
146 cmdHandler = withAuthSecrets(authorizations, cmdHandler)
147 }
148
149 cmdHandler = otelhttp.NewHandler(withMetricLabels(cmdHandler, staticServerDomainAttrFn("api")), "corehttp.cmdsHandler")
150 mux.Handle(APIPath+"/", cmdHandler)
151 return mux, nil
152 }
153 }
154
155 type rpcAuthScopeWithUser struct {
156 config.RPCAuthScope
157 User string
158 }
159
160 func convertAuthorizationsMap(authScopes map[string]*config.RPCAuthScope) map[string]rpcAuthScopeWithUser {
161 // authorizations is a map where we can just check for the header value to match.
162 authorizations := map[string]rpcAuthScopeWithUser{}
163 for user, authScope := range authScopes {
164 expectedHeader := config.ConvertAuthSecret(authScope.AuthSecret)
165 if expectedHeader != "" {
166 authorizations[expectedHeader] = rpcAuthScopeWithUser{
167 RPCAuthScope: *authScopes[user],
168 User: user,
169 }
170 }
171 }
172
173 return authorizations
174 }
175
176 func withAuthSecrets(authorizations map[string]rpcAuthScopeWithUser, next http.Handler) http.Handler {
177 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
178 authorizationHeader := r.Header.Get("Authorization")
179 auth, ok := authorizations[authorizationHeader]
180
181 if ok {
182 // version check is implicitly allowed
183 if r.URL.Path == "/api/v0/version" {
184 next.ServeHTTP(w, r)
185 return
186 }
187 // everything else has to be safelisted via AllowedPaths
188 for _, prefix := range auth.AllowedPaths {
189 if strings.HasPrefix(r.URL.Path, prefix) {
190 next.ServeHTTP(w, r)
191 return
192 }
193 }
194 }
195
196 http.Error(w, "Kubo RPC Access Denied: Please provide a valid authorization token as defined in the API.Authorizations configuration.", http.StatusForbidden)
197 })
198 }
199
200 // CommandsOption constructs a ServerOption for hooking the commands into the
201 // HTTP server. It will NOT allow GET requests.
202 func CommandsOption(cctx oldcmds.Context) ServeOption {
203 return commandsOption(cctx, corecommands.Root)
204 }
205
206 // 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/`
207 func CheckVersionOption() ServeOption {
208 daemonVersion := version.ApiVersion
209
210 return func(n *core.IpfsNode, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
211 mux := http.NewServeMux()
212 parent.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
213 if strings.HasPrefix(r.URL.Path, APIPath) {
214 cmdqry := r.URL.Path[len(APIPath):]
215 pth := strings.Split(cmdqry, "/")
216
217 // backwards compatibility to previous version check
218 if len(pth) >= 2 && pth[1] != "version" {
219 clientVersion := r.UserAgent()
220 // skips check if client is not kubo (go-ipfs)
221 if (strings.Contains(clientVersion, "/go-ipfs/") || strings.Contains(clientVersion, "/kubo/")) && daemonVersion != clientVersion {
222 http.Error(w, fmt.Sprintf("%s (%s != %s)", errAPIVersionMismatch, daemonVersion, clientVersion), http.StatusBadRequest)
223 return
224 }
225 }
226 }
227
228 mux.ServeHTTP(w, r)
229 })
230
231 return mux, nil
232 }
233 }