Browse Source

ipn/localapi: put all the LocalAPI methods into a map

Rather than a bunch of switch cases.

Change-Id: Id1db813ec255bfab59cbc982bee351eb36373245
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 3 years ago
parent
commit
b2994568fe
1 changed files with 82 additions and 69 deletions
  1. 82 69
      ipn/localapi/localapi.go

+ 82 - 69
ipn/localapi/localapi.go

@@ -37,9 +37,49 @@ import (
 	"tailscale.com/types/logger"
 	"tailscale.com/util/clientmetric"
 	"tailscale.com/util/mak"
+	"tailscale.com/util/strs"
 	"tailscale.com/version"
 )
 
+type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
+
+// handler is the set of LocalAPI handlers, keyed by the part of the
+// Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
+// then it's a prefix match.
+var handler = map[string]localAPIHandler{
+	// The prefix match handlers end with a slash:
+	"cert/":     (*Handler).serveCert,
+	"file-put/": (*Handler).serveFilePut,
+	"files/":    (*Handler).serveFiles,
+
+	// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
+	// without a trailing slash:
+	"bugreport":               (*Handler).serveBugReport,
+	"check-ip-forwarding":     (*Handler).serveCheckIPForwarding,
+	"check-prefs":             (*Handler).serveCheckPrefs,
+	"component-debug-logging": (*Handler).serveComponentDebugLogging,
+	"debug":                   (*Handler).serveDebug,
+	"derpmap":                 (*Handler).serveDERPMap,
+	"dial":                    (*Handler).serveDial,
+	"file-targets":            (*Handler).serveFileTargets,
+	"goroutines":              (*Handler).serveGoroutines,
+	"id-token":                (*Handler).serveIDToken,
+	"login-interactive":       (*Handler).serveLoginInteractive,
+	"logout":                  (*Handler).serveLogout,
+	"metrics":                 (*Handler).serveMetrics,
+	"ping":                    (*Handler).servePing,
+	"prefs":                   (*Handler).servePrefs,
+	"profile":                 (*Handler).serveProfile,
+	"set-dns":                 (*Handler).serveSetDNS,
+	"set-expiry-sooner":       (*Handler).serveSetExpirySooner,
+	"status":                  (*Handler).serveStatus,
+	"tka/init":                (*Handler).serveTKAInit,
+	"tka/modify":              (*Handler).serveTKAModify,
+	"tka/status":              (*Handler).serveTKAStatus,
+	"upload-client-metrics":   (*Handler).serveUploadClientMetrics,
+	"whois":                   (*Handler).serveWhoIs,
+}
+
 func randHex(n int) string {
 	b := make([]byte, n)
 	rand.Read(b)
@@ -101,72 +141,45 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
-		h.serveFiles(w, r)
-		return
-	}
-	if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") {
-		h.serveFilePut(w, r)
-		return
-	}
-	if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
-		h.serveCert(w, r)
-		return
-	}
-	switch r.URL.Path {
-	case "/localapi/v0/whois":
-		h.serveWhoIs(w, r)
-	case "/localapi/v0/goroutines":
-		h.serveGoroutines(w, r)
-	case "/localapi/v0/profile":
-		h.serveProfile(w, r)
-	case "/localapi/v0/status":
-		h.serveStatus(w, r)
-	case "/localapi/v0/logout":
-		h.serveLogout(w, r)
-	case "/localapi/v0/login-interactive":
-		h.serveLoginInteractive(w, r)
-	case "/localapi/v0/prefs":
-		h.servePrefs(w, r)
-	case "/localapi/v0/ping":
-		h.servePing(w, r)
-	case "/localapi/v0/check-prefs":
-		h.serveCheckPrefs(w, r)
-	case "/localapi/v0/check-ip-forwarding":
-		h.serveCheckIPForwarding(w, r)
-	case "/localapi/v0/bugreport":
-		h.serveBugReport(w, r)
-	case "/localapi/v0/file-targets":
-		h.serveFileTargets(w, r)
-	case "/localapi/v0/set-dns":
-		h.serveSetDNS(w, r)
-	case "/localapi/v0/derpmap":
-		h.serveDERPMap(w, r)
-	case "/localapi/v0/metrics":
-		h.serveMetrics(w, r)
-	case "/localapi/v0/debug":
-		h.serveDebug(w, r)
-	case "/localapi/v0/component-debug-logging":
-		h.serveComponentDebugLogging(w, r)
-	case "/localapi/v0/set-expiry-sooner":
-		h.serveSetExpirySooner(w, r)
-	case "/localapi/v0/dial":
-		h.serveDial(w, r)
-	case "/localapi/v0/id-token":
-		h.serveIDToken(w, r)
-	case "/localapi/v0/upload-client-metrics":
-		h.serveUploadClientMetrics(w, r)
-	case "/localapi/v0/tka/status":
-		h.serveTkaStatus(w, r)
-	case "/localapi/v0/tka/init":
-		h.serveTkaInit(w, r)
-	case "/localapi/v0/tka/modify":
-		h.serveTkaModify(w, r)
-	case "/":
-		io.WriteString(w, "tailscaled\n")
-	default:
-		http.Error(w, "404 not found", 404)
+	if fn, ok := handlerForPath(r.URL.Path); ok {
+		fn(h, w, r)
+	} else {
+		http.NotFound(w, r)
+	}
+}
+
+// handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
+// (the path doesn't include any query parameters)
+func handlerForPath(urlPath string) (h localAPIHandler, ok bool) {
+	if urlPath == "/" {
+		return (*Handler).serveLocalAPIRoot, true
+	}
+	suff, ok := strs.CutPrefix(urlPath, "/localapi/v0/")
+	if !ok {
+		// Currently all LocalAPI methods start with "/localapi/v0/" to signal
+		// to people that they're not necessarily stable APIs. In practice we'll
+		// probably need to keep them pretty stable anyway, but for now treat
+		// them as an internal implementation detail.
+		return nil, false
+	}
+	if fn, ok := handler[suff]; ok {
+		// Here we match exact handler suffixes like "status" or ones with a
+		// slash already in their name, like "tka/status".
+		return fn, true
+	}
+	// Otherwise, it might be a prefix match like "files/*" which we look up
+	// by the prefix including first trailing slash.
+	if i := strings.IndexByte(suff, '/'); i != -1 {
+		suff = suff[:i+1]
+		if fn, ok := handler[suff]; ok {
+			return fn, true
+		}
 	}
+	return nil, false
+}
+
+func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) {
+	io.WriteString(w, "tailscaled\n")
 }
 
 // serveIDToken handles requests to get an OIDC ID token.
@@ -834,13 +847,13 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
 	json.NewEncoder(w).Encode(struct{}{})
 }
 
-func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
 	if !h.PermitRead {
 		http.Error(w, "lock status access denied", http.StatusForbidden)
 		return
 	}
 	if r.Method != http.MethodGet {
-		http.Error(w, "use Get", http.StatusMethodNotAllowed)
+		http.Error(w, "use GET", http.StatusMethodNotAllowed)
 		return
 	}
 
@@ -853,7 +866,7 @@ func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
 	w.Write(j)
 }
 
-func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
 	if !h.PermitWrite {
 		http.Error(w, "lock init access denied", http.StatusForbidden)
 		return
@@ -886,7 +899,7 @@ func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
 	w.Write(j)
 }
 
-func (h *Handler) serveTkaModify(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
 	if !h.PermitWrite {
 		http.Error(w, "network-lock modify access denied", http.StatusForbidden)
 		return