|
|
|
|
@ -31,6 +31,7 @@ import ( |
|
|
|
|
"tailscale.com/net/netutil" |
|
|
|
|
"tailscale.com/tailcfg" |
|
|
|
|
"tailscale.com/util/groupmember" |
|
|
|
|
"tailscale.com/util/httpm" |
|
|
|
|
"tailscale.com/version/distro" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
@ -78,30 +79,6 @@ func init() { |
|
|
|
|
template.Must(tmpl.New("web.css").Parse(webCSS)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type tmplData struct { |
|
|
|
|
Profile tailcfg.UserProfile |
|
|
|
|
SynologyUser string |
|
|
|
|
Status string |
|
|
|
|
DeviceName string |
|
|
|
|
IP string |
|
|
|
|
AdvertiseExitNode bool |
|
|
|
|
AdvertiseRoutes string |
|
|
|
|
LicensesURL string |
|
|
|
|
TUNMode bool |
|
|
|
|
IsSynology bool |
|
|
|
|
DSMVersion int // 6 or 7, if IsSynology=true
|
|
|
|
|
IsUnraid bool |
|
|
|
|
UnraidToken string |
|
|
|
|
IPNVersion string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type postedData struct { |
|
|
|
|
AdvertiseRoutes string |
|
|
|
|
AdvertiseExitNode bool |
|
|
|
|
Reauthenticate bool |
|
|
|
|
ForceLogout bool |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// authorize returns the name of the user accessing the web UI after verifying
|
|
|
|
|
// whether the user has access to the web UI. The function will write the
|
|
|
|
|
// error to the provided http.ResponseWriter.
|
|
|
|
|
@ -294,12 +271,26 @@ req.send(null); |
|
|
|
|
// ServeHTTP processes all requests for the Tailscale web client.
|
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
if s.devMode { |
|
|
|
|
if r.URL.Path == "/api/data" { |
|
|
|
|
user, err := authorize(w, r) |
|
|
|
|
if err != nil { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
switch r.Method { |
|
|
|
|
case httpm.GET: |
|
|
|
|
s.serveGetNodeDataJSON(w, r, user) |
|
|
|
|
case httpm.POST: |
|
|
|
|
s.servePostNodeUpdate(w, r) |
|
|
|
|
default: |
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
// When in dev mode, proxy to the Vite dev server.
|
|
|
|
|
s.devProxy.ServeHTTP(w, r) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ctx := r.Context() |
|
|
|
|
if authRedirect(w, r) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
@ -309,25 +300,124 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if r.URL.Path == "/redirect" || r.URL.Path == "/redirect/" { |
|
|
|
|
switch { |
|
|
|
|
case r.URL.Path == "/redirect" || r.URL.Path == "/redirect/": |
|
|
|
|
io.WriteString(w, authenticationRedirectHTML) |
|
|
|
|
return |
|
|
|
|
case r.Method == "POST": |
|
|
|
|
s.servePostNodeUpdate(w, r) |
|
|
|
|
return |
|
|
|
|
default: |
|
|
|
|
s.serveGetNodeData(w, r, user) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type nodeData struct { |
|
|
|
|
Profile tailcfg.UserProfile |
|
|
|
|
SynologyUser string |
|
|
|
|
Status string |
|
|
|
|
DeviceName string |
|
|
|
|
IP string |
|
|
|
|
AdvertiseExitNode bool |
|
|
|
|
AdvertiseRoutes string |
|
|
|
|
LicensesURL string |
|
|
|
|
TUNMode bool |
|
|
|
|
IsSynology bool |
|
|
|
|
DSMVersion int // 6 or 7, if IsSynology=true
|
|
|
|
|
IsUnraid bool |
|
|
|
|
UnraidToken string |
|
|
|
|
IPNVersion string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *Server) getNodeData(ctx context.Context, user string) (*nodeData, error) { |
|
|
|
|
st, err := s.lc.Status(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
prefs, err := s.lc.GetPrefs(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
profile := st.User[st.Self.UserID] |
|
|
|
|
deviceName := strings.Split(st.Self.DNSName, ".")[0] |
|
|
|
|
versionShort := strings.Split(st.Version, "-")[0] |
|
|
|
|
data := &nodeData{ |
|
|
|
|
SynologyUser: user, |
|
|
|
|
Profile: profile, |
|
|
|
|
Status: st.BackendState, |
|
|
|
|
DeviceName: deviceName, |
|
|
|
|
LicensesURL: licenses.LicensesURL(), |
|
|
|
|
TUNMode: st.TUN, |
|
|
|
|
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"), |
|
|
|
|
DSMVersion: distro.DSMVersion(), |
|
|
|
|
IsUnraid: distro.Get() == distro.Unraid, |
|
|
|
|
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"), |
|
|
|
|
IPNVersion: versionShort, |
|
|
|
|
} |
|
|
|
|
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0") |
|
|
|
|
exitNodeRouteV6 := netip.MustParsePrefix("::/0") |
|
|
|
|
for _, r := range prefs.AdvertiseRoutes { |
|
|
|
|
if r == exitNodeRouteV4 || r == exitNodeRouteV6 { |
|
|
|
|
data.AdvertiseExitNode = true |
|
|
|
|
} else { |
|
|
|
|
if data.AdvertiseRoutes != "" { |
|
|
|
|
data.AdvertiseRoutes += "," |
|
|
|
|
} |
|
|
|
|
data.AdvertiseRoutes += r.String() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if len(st.TailscaleIPs) != 0 { |
|
|
|
|
data.IP = st.TailscaleIPs[0].String() |
|
|
|
|
} |
|
|
|
|
return data, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request, user string) { |
|
|
|
|
data, err := s.getNodeData(r.Context(), user) |
|
|
|
|
if err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
prefs, err := s.lc.GetPrefs(ctx) |
|
|
|
|
buf := new(bytes.Buffer) |
|
|
|
|
if err := tmpl.Execute(buf, *data); err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Write(buf.Bytes()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *Server) serveGetNodeDataJSON(w http.ResponseWriter, r *http.Request, user string) { |
|
|
|
|
data, err := s.getNodeData(r.Context(), user) |
|
|
|
|
if err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if err := json.NewEncoder(w).Encode(*data); err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type nodeUpdate struct { |
|
|
|
|
AdvertiseRoutes string |
|
|
|
|
AdvertiseExitNode bool |
|
|
|
|
Reauthenticate bool |
|
|
|
|
ForceLogout bool |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if r.Method == "POST" { |
|
|
|
|
func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
defer r.Body.Close() |
|
|
|
|
var postData postedData |
|
|
|
|
|
|
|
|
|
st, err := s.lc.Status(r.Context()) |
|
|
|
|
if err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var postData nodeUpdate |
|
|
|
|
type mi map[string]any |
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&postData); err != nil { |
|
|
|
|
w.WriteHeader(400) |
|
|
|
|
@ -349,7 +439,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
mp.Prefs.AdvertiseRoutes = routes |
|
|
|
|
log.Printf("Doing edit: %v", mp.Pretty()) |
|
|
|
|
|
|
|
|
|
if _, err := s.lc.EditPrefs(ctx, mp); err != nil { |
|
|
|
|
if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil { |
|
|
|
|
w.WriteHeader(http.StatusInternalServerError) |
|
|
|
|
json.NewEncoder(w).Encode(mi{"error": err.Error()}) |
|
|
|
|
return |
|
|
|
|
@ -379,47 +469,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
profile := st.User[st.Self.UserID] |
|
|
|
|
deviceName := strings.Split(st.Self.DNSName, ".")[0] |
|
|
|
|
versionShort := strings.Split(st.Version, "-")[0] |
|
|
|
|
data := tmplData{ |
|
|
|
|
SynologyUser: user, |
|
|
|
|
Profile: profile, |
|
|
|
|
Status: st.BackendState, |
|
|
|
|
DeviceName: deviceName, |
|
|
|
|
LicensesURL: licenses.LicensesURL(), |
|
|
|
|
TUNMode: st.TUN, |
|
|
|
|
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"), |
|
|
|
|
DSMVersion: distro.DSMVersion(), |
|
|
|
|
IsUnraid: distro.Get() == distro.Unraid, |
|
|
|
|
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"), |
|
|
|
|
IPNVersion: versionShort, |
|
|
|
|
} |
|
|
|
|
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0") |
|
|
|
|
exitNodeRouteV6 := netip.MustParsePrefix("::/0") |
|
|
|
|
for _, r := range prefs.AdvertiseRoutes { |
|
|
|
|
if r == exitNodeRouteV4 || r == exitNodeRouteV6 { |
|
|
|
|
data.AdvertiseExitNode = true |
|
|
|
|
} else { |
|
|
|
|
if data.AdvertiseRoutes != "" { |
|
|
|
|
data.AdvertiseRoutes += "," |
|
|
|
|
} |
|
|
|
|
data.AdvertiseRoutes += r.String() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if len(st.TailscaleIPs) != 0 { |
|
|
|
|
data.IP = st.TailscaleIPs[0].String() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer) |
|
|
|
|
if err := tmpl.Execute(buf, data); err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Write(buf.Bytes()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData postedData) (authURL string, retErr error) { |
|
|
|
|
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) { |
|
|
|
|
if postData.ForceLogout { |
|
|
|
|
if err := s.lc.Logout(ctx); err != nil { |
|
|
|
|
return "", fmt.Errorf("Logout error: %w", err) |
|
|
|
|
|