Eric Bower
·
2026-02-25
1package util
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strings"
12
13 "golang.org/x/crypto/ssh"
14)
15
16func CreateTmpDir() string {
17 tmp, err := os.MkdirTemp(os.TempDir(), "patchbin*")
18 if err != nil {
19 panic(err)
20 }
21 return tmp
22}
23
24func CreateCfgFile(dataDir, cfgTmpl string, adminKey UserSSH) string {
25 cfgPath := filepath.Join(dataDir, "patchbin.toml")
26 cfgFi, err := os.Create(cfgPath)
27 if err != nil {
28 panic(err)
29 }
30 _, _ = fmt.Fprintf(cfgFi, cfgTmpl, dataDir, adminKey.Public())
31 _ = cfgFi.Close()
32 return cfgPath
33}
34
35type UserSSH struct {
36 username string
37 signer ssh.Signer
38}
39
40func NewUserSSH(username string, signer ssh.Signer) *UserSSH {
41 return &UserSSH{
42 username: username,
43 signer: signer,
44 }
45}
46
47func (s UserSSH) Public() string {
48 pubkey := s.signer.PublicKey()
49 return string(ssh.MarshalAuthorizedKey(pubkey))
50}
51
52func (s UserSSH) MustCmd(patch []byte, cmd string) string {
53 res, err := s.Cmd(patch, cmd)
54 if err != nil {
55 panic(err)
56 }
57 return res
58}
59
60func (s UserSSH) Cmd(patch []byte, cmd string) (string, error) {
61 host := "localhost:2222"
62
63 config := &ssh.ClientConfig{
64 User: s.username,
65 Auth: []ssh.AuthMethod{
66 ssh.PublicKeys(s.signer),
67 },
68 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
69 }
70
71 client, err := ssh.Dial("tcp", host, config)
72 if err != nil {
73 return "", err
74 }
75 defer func() {
76 _ = client.Close()
77 }()
78
79 session, err := client.NewSession()
80 if err != nil {
81 return "", err
82 }
83 defer func() {
84 _ = session.Close()
85 }()
86
87 stdinPipe, err := session.StdinPipe()
88 if err != nil {
89 return "", err
90 }
91
92 stdoutPipe, err := session.StdoutPipe()
93 if err != nil {
94 return "", err
95 }
96
97 stderrPipe, err := session.StderrPipe()
98 if err != nil {
99 return "", err
100 }
101
102 if err := session.Start(cmd); err != nil {
103 return "", err
104 }
105
106 if patch != nil {
107 _, err = stdinPipe.Write(patch)
108 if err != nil {
109 return "", err
110 }
111 }
112
113 _ = stdinPipe.Close()
114
115 var stdoutBuf, stderrBuf strings.Builder
116 go func() { _, _ = io.Copy(&stderrBuf, stderrPipe) }()
117 _, _ = io.Copy(&stdoutBuf, stdoutPipe)
118
119 err = session.Wait()
120 stderr := stderrBuf.String()
121 if err != nil {
122 return "", fmt.Errorf("ssh command failed: %w (stderr: %s)", err, stderr)
123 }
124
125 return stdoutBuf.String(), nil
126}
127
128// ParsePRID extracts the PR ID from the output of `pr create`.
129// Looks for the URL line: "URL: https://host/prs/123"
130func ParsePRID(output string) string {
131 re := regexp.MustCompile(`/prs/(\d+)`)
132 matches := re.FindStringSubmatch(output)
133 if len(matches) < 2 {
134 return "1" // fallback
135 }
136 return matches[1]
137}
138
139func GenerateKeys() (UserSSH, UserSSH) {
140 _, adminKey, err := ed25519.GenerateKey(rand.Reader)
141 if err != nil {
142 panic(err)
143 }
144
145 adminSigner, err := ssh.NewSignerFromKey(adminKey)
146 if err != nil {
147 panic(err)
148 }
149
150 _, userKey, err := ed25519.GenerateKey(rand.Reader)
151 if err != nil {
152 panic(err)
153 }
154
155 userSigner, err := ssh.NewSignerFromKey(userKey)
156 if err != nil {
157 panic(err)
158 }
159
160 return UserSSH{
161 username: "admin",
162 signer: adminSigner,
163 }, UserSSH{
164 username: "contributor",
165 signer: userSigner,
166 }
167}