Headscale: Include last seen in node info

Add debugging API endpoint to run arbitrary headscale command.

Change-Id: I713d55b44d9657971c0ecb8befc8a1408e4b76af
diff --git a/core/headscale/client.go b/core/headscale/client.go
index 766ae52..ebeb8b2 100644
--- a/core/headscale/client.go
+++ b/core/headscale/client.go
@@ -24,6 +24,18 @@
 	}
 }
 
+func (c *client) run(cc ...string) (string, error) {
+	// TODO(giolekva): make expiration configurable, and auto-refresh
+	cc = append(cc, c.config)
+	cmd := exec.Command("headscale", cc...)
+	out, err := cmd.Output()
+	if err != nil {
+		return "", err
+	}
+	return string(out), nil
+
+}
+
 func (c *client) createUser(name string) error {
 	cmd := exec.Command("headscale", c.config, "users", "create", name)
 	out, err := cmd.Output()
@@ -90,10 +102,17 @@
 	return err
 }
 
+type timeInfo struct {
+	Seconds int `json:"seconds"`
+	Nanos   int `json:"nanos"`
+}
+
 type nodeInfo struct {
 	Id          int      `json:"id"`
 	Name        string   `json:"name"`
+	GivenName   string   `json:"given_name"`
 	IPAddresses []net.IP `json:"ip_addresses"`
+	LastSeen    timeInfo `json:"last_seen"`
 }
 
 func (c *client) getUserNodes(user string) ([]nodeInfo, error) {
diff --git a/core/headscale/main.go b/core/headscale/main.go
index fb20274..50917d7 100644
--- a/core/headscale/main.go
+++ b/core/headscale/main.go
@@ -115,6 +115,7 @@
 	r.HandleFunc("/user/{user}/node", s.getUserNodes).Methods(http.MethodGet)
 	r.HandleFunc("/user", s.createUser).Methods(http.MethodPost)
 	r.HandleFunc("/routes/{id}/enable", s.enableRoute).Methods(http.MethodPost)
+	r.HandleFunc("/cmd", s.runCmd).Methods(http.MethodPost)
 	go func() {
 		rand.Seed(uint64(time.Now().UnixNano()))
 		s.syncUsers()
@@ -127,6 +128,24 @@
 	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 }
 
+type cmdReq struct {
+	Cmd string `json:"cmd"`
+}
+
+func (s *server) runCmd(w http.ResponseWriter, r *http.Request) {
+	var req cmdReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	if out, err := s.client.run(strings.Split(req.Cmd, " ")...); err != nil && !errors.Is(err, ErrorAlreadyExists) {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	} else {
+		fmt.Fprint(w, out)
+	}
+}
+
 type createUserReq struct {
 	Name string `json:"name"`
 }