diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx
index b0e0ceed0..a173feba5 100644
--- a/client/web/src/components/app.tsx
+++ b/client/web/src/components/app.tsx
@@ -7,12 +7,19 @@ export default function App() {
return (
-
-
-
-
-
-
+ {!data ? (
+ // TODO(sonia): add a loading view
+
Loading...
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
)
}
diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts
index fd30b1c2e..3060721f9 100644
--- a/client/web/src/hooks/node-data.ts
+++ b/client/web/src/hooks/node-data.ts
@@ -1,8 +1,4 @@
-export type UserProfile = {
- LoginName: string
- DisplayName: string
- ProfilePicURL: string
-}
+import { useEffect, useState } from "react"
export type NodeData = {
Profile: UserProfile
@@ -20,29 +16,22 @@ export type NodeData = {
IPNVersion: string
}
-// testData is static set of nodedata used during development.
-// This can be removed once we have a real node data API.
-const testData: NodeData = {
- Profile: {
- LoginName: "amelie",
- DisplayName: "Amelie Pangolin",
- ProfilePicURL: "https://login.tailscale.com/logo192.png",
- },
- Status: "Running",
- DeviceName: "amelies-laptop",
- IP: "100.1.2.3",
- AdvertiseExitNode: false,
- AdvertiseRoutes: "",
- LicensesURL: "https://tailscale.com/licenses/tailscale",
- TUNMode: false,
- IsSynology: true,
- DSMVersion: 7,
- IsUnraid: false,
- UnraidToken: "",
- IPNVersion: "0.1.0",
+export type UserProfile = {
+ LoginName: string
+ DisplayName: string
+ ProfilePicURL: string
}
// useNodeData returns basic data about the current node.
export default function useNodeData() {
- return testData
+ const [data, setData] = useState()
+
+ useEffect(() => {
+ fetch("/api/data")
+ .then((response) => response.json())
+ .then((json) => setData(json))
+ .catch((error) => console.error(error))
+ }, [])
+
+ return data
}
diff --git a/client/web/web.go b/client/web/web.go
index 487ffe36a..c49cf404d 100644
--- a/client/web/web.go
+++ b/client/web/web.go
@@ -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,80 +300,49 @@ 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 {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ return nil, err
}
prefs, err := s.lc.GetPrefs(ctx)
if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if r.Method == "POST" {
- defer r.Body.Close()
- var postData postedData
- type mi map[string]any
- if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
- w.WriteHeader(400)
- json.NewEncoder(w).Encode(mi{"error": err.Error()})
- return
- }
-
- routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- json.NewEncoder(w).Encode(mi{"error": err.Error()})
- return
- }
- mp := &ipn.MaskedPrefs{
- AdvertiseRoutesSet: true,
- WantRunningSet: true,
- }
- mp.Prefs.WantRunning = true
- mp.Prefs.AdvertiseRoutes = routes
- log.Printf("Doing edit: %v", mp.Pretty())
-
- if _, err := s.lc.EditPrefs(ctx, mp); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- json.NewEncoder(w).Encode(mi{"error": err.Error()})
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- var reauth, logout bool
- if postData.Reauthenticate {
- reauth = true
- }
- if postData.ForceLogout {
- logout = true
- }
- log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
- url, err := s.tailscaleUp(r.Context(), st, postData)
- log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- json.NewEncoder(w).Encode(mi{"error": err.Error()})
- return
- }
- if url != "" {
- json.NewEncoder(w).Encode(mi{"url": url})
- } else {
- io.WriteString(w, "{}")
- }
- return
+ return nil, err
}
-
profile := st.User[st.Self.UserID]
deviceName := strings.Split(st.Self.DNSName, ".")[0]
versionShort := strings.Split(st.Version, "-")[0]
- data := tmplData{
+ data := &nodeData{
SynologyUser: user,
Profile: profile,
Status: st.BackendState,
@@ -410,16 +370,106 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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
+ }
buf := new(bytes.Buffer)
- if err := tmpl.Execute(buf, data); err != nil {
+ 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) 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
+}
+
+func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
+ defer r.Body.Close()
+
+ 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)
+ json.NewEncoder(w).Encode(mi{"error": err.Error()})
+ return
+ }
+
+ routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(mi{"error": err.Error()})
+ return
+ }
+ mp := &ipn.MaskedPrefs{
+ AdvertiseRoutesSet: true,
+ WantRunningSet: true,
+ }
+ mp.Prefs.WantRunning = true
+ mp.Prefs.AdvertiseRoutes = routes
+ log.Printf("Doing edit: %v", mp.Pretty())
+
+ if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(mi{"error": err.Error()})
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ var reauth, logout bool
+ if postData.Reauthenticate {
+ reauth = true
+ }
+ if postData.ForceLogout {
+ logout = true
+ }
+ log.Printf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
+ url, err := s.tailscaleUp(r.Context(), st, postData)
+ log.Printf("tailscaleUp = (URL %v, %v)", url != "", err)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(mi{"error": err.Error()})
+ return
+ }
+ if url != "" {
+ json.NewEncoder(w).Encode(mi{"url": url})
+ } else {
+ io.WriteString(w, "{}")
+ }
+ return
+}
+
+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)
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index cf02c0b65..5865de17a 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -138,7 +138,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web
- tailscale.com/util/httpm from tailscale.com/client/tailscale
+ tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/net/interfaces+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+