commands.go 5.61 KB
Newer Older
Brian Tiger Chow's avatar
Brian Tiger Chow committed
1 2 3
package corehttp

import (
keks's avatar
keks committed
4 5
	"errors"
	"fmt"
6
	"net"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
7 8
	"net/http"
	"os"
9
	"strconv"
10 11
	"strings"

tavit ohanian's avatar
tavit ohanian committed
12 13 14 15 16 17 18 19 20
	version "gitlab.dms3.io/dms3/go-dms3"
	oldcmds "gitlab.dms3.io/dms3/go-dms3/commands"
	"gitlab.dms3.io/dms3/go-dms3/core"
	corecommands "gitlab.dms3.io/dms3/go-dms3/core/commands"

	cmds "gitlab.dms3.io/dms3/go-dms3-cmds"
	cmdsHttp "gitlab.dms3.io/dms3/go-dms3-cmds/http"
	config "gitlab.dms3.io/dms3/go-dms3-config"
	path "gitlab.dms3.io/dms3/go-path"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
21 22
)

keks's avatar
keks committed
23
var (
Steven Allen's avatar
Steven Allen committed
24
	errAPIVersionMismatch = errors.New("api version mismatch")
keks's avatar
keks committed
25 26
)

27 28 29 30 31 32
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:

tavit ohanian's avatar
tavit ohanian committed
33 34
	dms3 config API.HTTPHeaders --json '{"Access-Control-Allow-Origin": ["*"]}'
	dms3 daemon
35 36
`

Steven Allen's avatar
Steven Allen committed
37
// APIPath is the path at which the API is mounted.
38 39
const APIPath = "/api/v0"

40 41 42
var defaultLocalhostOrigins = []string{
	"http://127.0.0.1:<port>",
	"https://127.0.0.1:<port>",
43 44
	"http://[::1]:<port>",
	"https://[::1]:<port>",
45 46 47 48
	"http://localhost:<port>",
	"https://localhost:<port>",
}

49 50 51
func addCORSFromEnv(c *cmdsHttp.ServerConfig) {
	origin := os.Getenv(originEnvKey)
	if origin != "" {
52
		log.Warn(originEnvKeyDeprecate)
Artem Andreenko's avatar
Artem Andreenko committed
53
		c.AppendAllowedOrigins(origin)
54 55 56
	}
}

57
func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) {
58 59
	log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders)

60
	if acao := nc.API.HTTPHeaders[cmdsHttp.ACAOrigin]; acao != nil {
Artem Andreenko's avatar
Artem Andreenko committed
61
		c.SetAllowedOrigins(acao...)
62
	}
63
	if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil {
Artem Andreenko's avatar
Artem Andreenko committed
64
		c.SetAllowedMethods(acam...)
65
	}
Steven Allen's avatar
Steven Allen committed
66 67
	for _, v := range nc.API.HTTPHeaders[cmdsHttp.ACACredentials] {
		c.SetAllowCredentials(strings.ToLower(v) == "true")
68
	}
69

70
	c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)+1)
71 72 73 74

	// 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 {
75
		h = http.CanonicalHeaderKey(h)
76 77 78 79 80 81
		switch h {
		case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials:
			// these are handled by the CORs library.
		default:
			c.Headers[h] = v
		}
82
	}
tavit ohanian's avatar
tavit ohanian committed
83
	c.Headers["Server"] = []string{"go-dms3/" + version.CurrentVersionNumber}
84
}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
85

86 87
func addCORSDefaults(c *cmdsHttp.ServerConfig) {
	// by default use localhost origins
Artem Andreenko's avatar
Artem Andreenko committed
88 89
	if len(c.AllowedOrigins()) == 0 {
		c.SetAllowedOrigins(defaultLocalhostOrigins...)
90 91 92
	}

	// by default, use GET, PUT, POST
Artem Andreenko's avatar
Artem Andreenko committed
93
	if len(c.AllowedMethods()) == 0 {
Steven Allen's avatar
Steven Allen committed
94
		c.SetAllowedMethods(http.MethodGet, http.MethodPost, http.MethodPut)
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
	}
}

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...)
110 111 112
	oldOrigins := c.AllowedOrigins()
	newOrigins := make([]string, len(oldOrigins))
	for i, o := range oldOrigins {
113 114 115 116
		// TODO: allow replacing <host>. tricky, ip4 and ip6 and hostnames...
		if port != "" {
			o = strings.Replace(o, "<port>", port, -1)
		}
117
		newOrigins[i] = o
118
	}
119
	c.SetAllowedOrigins(newOrigins...)
120 121
}

122
func commandsOption(cctx oldcmds.Context, command *cmds.Command, allowGet bool) ServeOption {
tavit ohanian's avatar
tavit ohanian committed
123
	return func(n *core.Dms3Node, l net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
124

Artem Andreenko's avatar
Artem Andreenko committed
125
		cfg := cmdsHttp.NewServerConfig()
126 127 128 129 130 131 132
		cfg.AllowGet = allowGet
		corsAllowedMethods := []string{http.MethodPost}
		if allowGet {
			corsAllowedMethods = append(corsAllowedMethods, http.MethodGet)
		}

		cfg.SetAllowedMethods(corsAllowedMethods...)
keks's avatar
keks committed
133
		cfg.APIPath = APIPath
134 135 136 137
		rcfg, err := n.Repo.Config()
		if err != nil {
			return nil, err
		}
138

139
		addHeadersFromConfig(cfg, rcfg)
140
		addCORSFromEnv(cfg)
141 142
		addCORSDefaults(cfg)
		patchCORSVars(cfg, l.Addr())
143

144
		cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
keks's avatar
keks committed
145
		mux.Handle(APIPath+"/", cmdHandler)
146
		return mux, nil
Brian Tiger Chow's avatar
Brian Tiger Chow committed
147 148
	}
}
rht's avatar
rht committed
149

Steven Allen's avatar
Steven Allen committed
150
// CommandsOption constructs a ServerOption for hooking the commands into the
151
// HTTP server. It will NOT allow GET requests.
152
func CommandsOption(cctx oldcmds.Context) ServeOption {
153
	return commandsOption(cctx, corecommands.Root, false)
rht's avatar
rht committed
154 155
}

156
// CommandsROOption constructs a ServerOption for hooking the read-only commands
157
// into the HTTP server. It will allow GET requests.
158
func CommandsROOption(cctx oldcmds.Context) ServeOption {
159
	return commandsOption(cctx, corecommands.RootRO, true)
rht's avatar
rht committed
160
}
keks's avatar
keks committed
161

tavit ohanian's avatar
tavit ohanian committed
162
// CheckVersionOption returns a ServeOption that checks whether the client dms3 version matches. Does nothing when the user agent string does not contain `/go-dms3/`
keks's avatar
keks committed
163
func CheckVersionOption() ServeOption {
164
	daemonVersion := version.ApiVersion
keks's avatar
keks committed
165

tavit ohanian's avatar
tavit ohanian committed
166
	return ServeOption(func(n *core.Dms3Node, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
keks's avatar
keks committed
167
		mux := http.NewServeMux()
keks's avatar
keks committed
168
		parent.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
keks's avatar
keks committed
169
			if strings.HasPrefix(r.URL.Path, APIPath) {
keks's avatar
keks committed
170 171 172
				cmdqry := r.URL.Path[len(APIPath):]
				pth := path.SplitList(cmdqry)

keks's avatar
keks committed
173
				// backwards compatibility to previous version check
174
				if len(pth) >= 2 && pth[1] != "version" {
keks's avatar
keks committed
175
					clientVersion := r.UserAgent()
tavit ohanian's avatar
tavit ohanian committed
176 177
					// skips check if client is not go-dms3
					if strings.Contains(clientVersion, "/go-dms3/") && daemonVersion != clientVersion {
Steven Allen's avatar
Steven Allen committed
178
						http.Error(w, fmt.Sprintf("%s (%s != %s)", errAPIVersionMismatch, daemonVersion, clientVersion), http.StatusBadRequest)
keks's avatar
keks committed
179 180
						return
					}
keks's avatar
keks committed
181 182 183
				}
			}

keks's avatar
keks committed
184
			mux.ServeHTTP(w, r)
keks's avatar
keks committed
185 186 187 188 189
		})

		return mux, nil
	})
}