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 &cli.StringFlag{
45 Name: "guard-path",
46 Usage: "path to the knot binary for the authorized_keys forced command (defaults to os.Executable)",
47 },
48 &cli.BoolFlag{
49 Name: "secure-mode",
50 Usage: "emit -secure-mode in the authorized_keys forced command",
51 },
52 },
53 }
54}
55
56func Run(ctx context.Context, cmd *cli.Command) error {
57 l := log.FromContext(ctx)
58
59 internalApi := cmd.String("internal-api")
60 gitDir := cmd.String("git-dir")
61 logPath := cmd.String("log-path")
62 output := cmd.String("output")
63
64 executablePath := cmd.String("guard-path")
65 if executablePath == "" {
66 var err error
67 executablePath, err = os.Executable()
68 if err != nil {
69 l.Error("error getting path of executable", "error", err)
70 return err
71 }
72 }
73
74 resp, err := http.Get(internalApi + "/keys")
75 if err != nil {
76 l.Error("error reaching internal API endpoint; is the knot server running?", "error", err)
77 return err
78 }
79 defer resp.Body.Close()
80
81 body, err := io.ReadAll(resp.Body)
82 if err != nil {
83 l.Error("error reading response body", "error", err)
84 return err
85 }
86
87 var data []map[string]any
88 err = json.Unmarshal(body, &data)
89 if err != nil {
90 l.Error("error unmarshalling response body", "error", err)
91 return err
92 }
93
94 switch output {
95 case "json":
96 prettyJSON, err := json.MarshalIndent(data, "", " ")
97 if err != nil {
98 l.Error("error pretty printing JSON", "error", err)
99 return err
100 }
101
102 if _, err := os.Stdout.Write(prettyJSON); err != nil {
103 l.Error("error writing to stdout", "error", err)
104 return err
105 }
106 case "authorized-keys":
107 formatted := formatKeyData(executablePath, gitDir, logPath, internalApi, cmd.Bool("secure-mode"), data)
108 _, err := os.Stdout.Write([]byte(formatted))
109 if err != nil {
110 l.Error("error writing to stdout", "error", err)
111 return err
112 }
113 case "table":
114 fmt.Printf("%-40s %-40s\n", "DID", "KEY")
115 fmt.Println(strings.Repeat("-", 80))
116
117 for _, entry := range data {
118 did, _ := entry["did"].(string)
119 key, _ := entry["key"].(string)
120 fmt.Printf("%-40s %-40s\n", did, key)
121 }
122 }
123 return nil
124}
125
126func formatKeyData(executablePath, gitDir, logPath, endpoint string, secureMode bool, data []map[string]any) string {
127 secureFlag := ""
128 if secureMode {
129 secureFlag = " -secure-mode"
130 }
131 var result string
132 for _, entry := range data {
133 raw, _ := entry["key"].(string)
134 key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(raw))
135 if err != nil {
136 continue
137 }
138 result += fmt.Sprintf(
139 `command="%s guard -git-dir %s -user %s -log-path %s -internal-api %s%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s`+"\n",
140 executablePath, gitDir, entry["did"], logPath, endpoint, secureFlag, ssh.MarshalAuthorizedKey(key))
141 }
142 return result
143}