AppManager: Clean up VPN node and auth keys upon app removal

Change-Id: Ie76278556247d16806ba81286621adca973e3f6e
diff --git a/core/headscale/client.go b/core/headscale/client.go
index ce82291..18c37df 100644
--- a/core/headscale/client.go
+++ b/core/headscale/client.go
@@ -1,9 +1,12 @@
 package main
 
 import (
+	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"os/exec"
+	"strconv"
 	"strings"
 )
 
@@ -33,21 +36,81 @@
 	// TODO(giolekva): make expiration configurable, and auto-refresh
 	cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "create", "--reusable", "--expiration", "365d")
 	out, err := cmd.Output()
+	fmt.Println(string(out))
 	if err != nil {
 		return "", err
 	}
-	fmt.Println(string(out))
 	return extractLastLine(string(out))
 }
 
+func (c *client) expirePreAuthKey(user, authKey string) error {
+	cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "expire", authKey)
+	out, err := cmd.Output()
+	fmt.Println(string(out))
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) expireUserNode(user, node string) error {
+	id, err := c.getNodeId(user, node)
+	if err != nil {
+		return err
+	}
+	cmd := exec.Command("headscale", c.config, "node", "expire", "--identifier", id)
+	out, err := cmd.Output()
+	fmt.Println(string(out))
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) removeUserNode(user, node string) error {
+	id, err := c.getNodeId(user, node)
+	if err != nil {
+		return err
+	}
+	cmd := exec.Command("headscale", c.config, "node", "delete", "--identifier", id, "--force")
+	out, err := cmd.Output()
+	fmt.Println(string(out))
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 func (c *client) enableRoute(id string) error {
-	// TODO(giolekva): make expiration configurable, and auto-refresh
 	cmd := exec.Command("headscale", c.config, "routes", "enable", "-r", id)
 	out, err := cmd.Output()
 	fmt.Println(string(out))
 	return err
 }
 
+type nodeInfo struct {
+	Id   int    `json:"id"`
+	Name string `json:"name"`
+}
+
+func (c *client) getNodeId(user, node string) (string, error) {
+	cmd := exec.Command("headscale", c.config, "--user", user, "node", "list", "-o", "json")
+	out, err := cmd.Output()
+	if err != nil {
+		return "", err
+	}
+	var nodes []nodeInfo
+	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&nodes); err != nil {
+		return "", err
+	}
+	for _, n := range nodes {
+		if n.Name == node {
+			return strconv.Itoa(n.Id), nil
+		}
+	}
+	return "", fmt.Errorf("not found")
+}
+
 func extractLastLine(s string) (string, error) {
 	items := strings.Split(s, "\n")
 	for i := len(items) - 1; i >= 0; i-- {
diff --git a/core/headscale/main.go b/core/headscale/main.go
index da0ff6c..698d9d2 100644
--- a/core/headscale/main.go
+++ b/core/headscale/main.go
@@ -88,6 +88,9 @@
 	r := mux.NewRouter()
 	r.HandleFunc("/sync-users", s.handleSyncUsers).Methods(http.MethodGet)
 	r.HandleFunc("/user/{user}/preauthkey", s.createReusablePreAuthKey).Methods(http.MethodPost)
+	r.HandleFunc("/user/{user}/preauthkey", s.expireReusablePreAuthKey).Methods(http.MethodDelete)
+	r.HandleFunc("/user/{user}/node/{node}/expire", s.expireUserNode).Methods(http.MethodPost)
+	r.HandleFunc("/user/{user}/node/{node}", s.removeUserNode).Methods(http.MethodDelete)
 	r.HandleFunc("/user", s.createUser).Methods(http.MethodPost)
 	r.HandleFunc("/routes/{id}/enable", s.enableRoute).Methods(http.MethodPost)
 	go func() {
@@ -132,6 +135,63 @@
 	}
 }
 
+type expirePreAuthKeyReq struct {
+	AuthKey string `json:"authKey"`
+}
+
+func (s *server) expireReusablePreAuthKey(w http.ResponseWriter, r *http.Request) {
+	user, ok := mux.Vars(r)["user"]
+	if !ok {
+		http.Error(w, "no user", http.StatusBadRequest)
+		return
+	}
+	var req expirePreAuthKeyReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	if err := s.client.expirePreAuthKey(user, req.AuthKey); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *server) expireUserNode(w http.ResponseWriter, r *http.Request) {
+	fmt.Println("expire node")
+	user, ok := mux.Vars(r)["user"]
+	if !ok {
+		http.Error(w, "no user", http.StatusBadRequest)
+		return
+	}
+	node, ok := mux.Vars(r)["node"]
+	if !ok {
+		http.Error(w, "no user", http.StatusBadRequest)
+		return
+	}
+	if err := s.client.expireUserNode(user, node); err != nil {
+		fmt.Println(err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *server) removeUserNode(w http.ResponseWriter, r *http.Request) {
+	user, ok := mux.Vars(r)["user"]
+	if !ok {
+		http.Error(w, "no user", http.StatusBadRequest)
+		return
+	}
+	node, ok := mux.Vars(r)["node"]
+	if !ok {
+		http.Error(w, "no user", http.StatusBadRequest)
+		return
+	}
+	if err := s.client.removeUserNode(user, node); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func (s *server) handleSyncUsers(_ http.ResponseWriter, _ *http.Request) {
 	go s.syncUsers()
 }