mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
db13b2d0c8
Adding a subcommand which prints and logs a log marker. This should help diagnose any issues that users face. Fixes #1466 Signed-off-by: Maisem Ali <maisem@tailscale.com>
242 lines
5.7 KiB
Go
242 lines
5.7 KiB
Go
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package localapi contains the HTTP server handlers for tailscaled's API server.
|
|
package localapi
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"inet.af/netaddr"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
func randHex(n int) string {
|
|
b := make([]byte, n)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID string) *Handler {
|
|
return &Handler{b: b, logf: logf, backendLogID: logID}
|
|
}
|
|
|
|
type Handler struct {
|
|
// RequiredPassword, if non-empty, forces all HTTP
|
|
// requests to have HTTP basic auth with this password.
|
|
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
|
|
RequiredPassword string
|
|
|
|
// PermitRead is whether read-only HTTP handlers are allowed.
|
|
PermitRead bool
|
|
|
|
// PermitWrite is whether mutating HTTP handlers are allowed.
|
|
PermitWrite bool
|
|
|
|
b *ipnlocal.LocalBackend
|
|
logf logger.Logf
|
|
backendLogID string
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if h.b == nil {
|
|
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if h.RequiredPassword != "" {
|
|
_, pass, ok := r.BasicAuth()
|
|
if !ok {
|
|
http.Error(w, "auth required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if pass != h.RequiredPassword {
|
|
http.Error(w, "bad password", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
|
|
h.serveFiles(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/status":
|
|
h.serveStatus(w, r)
|
|
case "/localapi/v0/check-ip-forwarding":
|
|
h.serveCheckIPForwarding(w, r)
|
|
case "/localapi/v0/bugreport":
|
|
h.serveBugReport(w, r)
|
|
default:
|
|
io.WriteString(w, "tailscaled\n")
|
|
}
|
|
}
|
|
|
|
func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
|
if !h.PermitRead {
|
|
http.Error(w, "bugreport access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
|
h.logf("user bugreport: %s", logMarker)
|
|
if note := r.FormValue("note"); len(note) > 0 {
|
|
h.logf("user bugreport note: %s", note)
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
fmt.Fprintln(w, logMarker)
|
|
}
|
|
|
|
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
|
if !h.PermitRead {
|
|
http.Error(w, "whois access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
b := h.b
|
|
var ipp netaddr.IPPort
|
|
if v := r.FormValue("addr"); v != "" {
|
|
var err error
|
|
ipp, err = netaddr.ParseIPPort(v)
|
|
if err != nil {
|
|
http.Error(w, "invalid 'addr' parameter", 400)
|
|
return
|
|
}
|
|
} else {
|
|
http.Error(w, "missing 'addr' parameter", 400)
|
|
return
|
|
}
|
|
n, u, ok := b.WhoIs(ipp)
|
|
if !ok {
|
|
http.Error(w, "no match for IP:port", 404)
|
|
return
|
|
}
|
|
res := &tailcfg.WhoIsResponse{
|
|
Node: n,
|
|
UserProfile: &u,
|
|
}
|
|
j, err := json.MarshalIndent(res, "", "\t")
|
|
if err != nil {
|
|
http.Error(w, "JSON encoding error", 500)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(j)
|
|
}
|
|
|
|
func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
|
|
// Require write access out of paranoia that the goroutine dump
|
|
// (at least its arguments) might contain something sensitive.
|
|
if !h.PermitWrite {
|
|
http.Error(w, "goroutine dump access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
buf := make([]byte, 2<<20)
|
|
buf = buf[:runtime.Stack(buf, true)]
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Write(buf)
|
|
}
|
|
|
|
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
|
if !h.PermitRead {
|
|
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
var warning string
|
|
if err := h.b.CheckIPForwarding(); err != nil {
|
|
warning = err.Error()
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(struct {
|
|
Warning string
|
|
}{
|
|
Warning: warning,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
|
if !h.PermitRead {
|
|
http.Error(w, "status access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var st *ipnstate.Status
|
|
if defBool(r.FormValue("peers"), true) {
|
|
st = h.b.Status()
|
|
} else {
|
|
st = h.b.StatusWithoutPeers()
|
|
}
|
|
e := json.NewEncoder(w)
|
|
e.SetIndent("", "\t")
|
|
e.Encode(st)
|
|
}
|
|
|
|
func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
|
if !h.PermitWrite {
|
|
http.Error(w, "file access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
suffix := strings.TrimPrefix(r.URL.Path, "/localapi/v0/files/")
|
|
if suffix == "" {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "want GET to list files", 400)
|
|
return
|
|
}
|
|
wfs, err := h.b.WaitingFiles()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(wfs)
|
|
return
|
|
}
|
|
name, err := url.PathUnescape(suffix)
|
|
if err != nil {
|
|
http.Error(w, "bad filename", 400)
|
|
return
|
|
}
|
|
if r.Method == "DELETE" {
|
|
if err := h.b.DeleteFile(name); err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
rc, size, err := h.b.OpenFile(name)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
defer rc.Close()
|
|
w.Header().Set("Content-Length", fmt.Sprint(size))
|
|
io.Copy(w, rc)
|
|
}
|
|
|
|
func defBool(a string, def bool) bool {
|
|
if a == "" {
|
|
return def
|
|
}
|
|
v, err := strconv.ParseBool(a)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return v
|
|
}
|