1package keyfetch
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "os"
10 "strings"
11
12 "github.com/urfave/cli/v3"
13 "tangled.sh/tangled.sh/core/log"
14)
15
16func Command() *cli.Command {
17 return &cli.Command{
18 Name: "keys",
19 Usage: "fetch public keys from the knot server",
20 Action: Run,
21 Flags: []cli.Flag{
22 &cli.StringFlag{
23 Name: "output",
24 Aliases: []string{"o"},
25 Usage: "output format (table, json, authorized-keys)",
26 Value: "table",
27 },
28 &cli.StringFlag{
29 Name: "internal-api",
30 Usage: "internal API endpoint",
31 Value: "http://localhost:5444",
32 },
33 &cli.StringFlag{
34 Name: "git-dir",
35 Usage: "base directory for git repos",
36 Value: "/home/git",
37 },
38 &cli.StringFlag{
39 Name: "log-path",
40 Usage: "path to log file",
41 Value: "/home/git/log",
42 },
43 },
44 }
45}
46
47func Run(ctx context.Context, cmd *cli.Command) error {
48 l := log.FromContext(ctx)
49
50 internalApi := cmd.String("internal-api")
51 gitDir := cmd.String("git-dir")
52 logPath := cmd.String("log-path")
53 output := cmd.String("output")
54
55 executablePath, err := os.Executable()
56 if err != nil {
57 l.Error("error getting path of executable", "error", err)
58 return err
59 }
60
61 resp, err := http.Get(internalApi + "/keys")
62 if err != nil {
63 l.Error("error reaching internal API endpoint; is the knot server running?", "error", err)
64 return err
65 }
66 defer resp.Body.Close()
67
68 body, err := io.ReadAll(resp.Body)
69 if err != nil {
70 l.Error("error reading response body", "error", err)
71 return err
72 }
73
74 var data []map[string]any
75 err = json.Unmarshal(body, &data)
76 if err != nil {
77 l.Error("error unmarshalling response body", "error", err)
78 return err
79 }
80
81 switch output {
82 case "json":
83 prettyJSON, err := json.MarshalIndent(data, "", " ")
84 if err != nil {
85 l.Error("error pretty printing JSON", "error", err)
86 return err
87 }
88
89 if _, err := os.Stdout.Write(prettyJSON); err != nil {
90 l.Error("error writing to stdout", "error", err)
91 return err
92 }
93 case "authorized-keys":
94 formatted := formatKeyData(executablePath, gitDir, logPath, internalApi, data)
95 _, err := os.Stdout.Write([]byte(formatted))
96 if err != nil {
97 l.Error("error writing to stdout", "error", err)
98 return err
99 }
100 case "table":
101 fmt.Printf("%-40s %-40s\n", "DID", "KEY")
102 fmt.Println(strings.Repeat("-", 80))
103
104 for _, entry := range data {
105 did, _ := entry["did"].(string)
106 key, _ := entry["key"].(string)
107 fmt.Printf("%-40s %-40s\n", did, key)
108 }
109 }
110 return nil
111}
112
113func formatKeyData(executablePath, gitDir, logPath, endpoint string, data []map[string]any) string {
114 var result string
115 for _, entry := range data {
116 result += fmt.Sprintf(
117 `command="%s guard -git-dir %s -user %s -log-path %s -internal-api %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s`+"\n",
118 executablePath, gitDir, entry["did"], logPath, endpoint, entry["key"])
119 }
120 return result
121}