From b2994568feff57a3a9ff1e3a7d1e89f6f57f2faa Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 9 Oct 2022 08:57:02 -0700 Subject: [PATCH] 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 --- ipn/localapi/localapi.go | 151 +++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 446b1df40..3ebd109f8 100644 --- a/ipn/localapi/localapi.go +++ b/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