|
|
|
|
@ -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 |
|
|
|
|
|