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