blob: 08103be0b82b7c6ca79bc58717a42cf6185ee17e [file] [log] [blame]
giolekva8aa73e82022-07-09 11:34:39 +04001package soft
2
3import (
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +04004 "errors"
giolekva8aa73e82022-07-09 11:34:39 +04005 "fmt"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04006 "golang.org/x/crypto/ssh"
giolekva8aa73e82022-07-09 11:34:39 +04007 "log"
8 "net"
9 "os"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040010 "regexp"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040011 "strings"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040012 "time"
giolekva8aa73e82022-07-09 11:34:39 +040013
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040014 "github.com/cenkalti/backoff/v4"
giolekva8aa73e82022-07-09 11:34:39 +040015 "github.com/go-git/go-billy/v5/memfs"
16 "github.com/go-git/go-git/v5"
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040017 "github.com/go-git/go-git/v5/plumbing/transport"
giolekva8aa73e82022-07-09 11:34:39 +040018 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
19 "github.com/go-git/go-git/v5/storage/memory"
giolekva8aa73e82022-07-09 11:34:39 +040020)
21
gio0eaf2712024-04-14 13:08:46 +040022var ErrorAlreadyExists = errors.New("already exists")
23
gioe72b54f2024-04-22 10:44:41 +040024type Client interface {
25 Address() string
26 Signer() ssh.Signer
27 GetPublicKeys() ([]string, error)
28 GetRepo(name string) (RepoIO, error)
29 GetRepoAddress(name string) string
30 AddRepository(name string) error
31 AddUser(name, pubKey string) error
32 AddPublicKey(user string, pubKey string) error
33 RemovePublicKey(user string, pubKey string) error
34 MakeUserAdmin(name string) error
35 AddReadWriteCollaborator(repo, user string) error
36 AddReadOnlyCollaborator(repo, user string) error
gio0eaf2712024-04-14 13:08:46 +040037 AddWebhook(repo, url string, opts ...string) error
gioe72b54f2024-04-22 10:44:41 +040038}
39
40type realClient struct {
41 addr string
42 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040043 log *log.Logger
44 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040045}
46
gioe72b54f2024-04-22 10:44:41 +040047func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040048 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
49 if err != nil {
50 return nil, err
51 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040052 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040053 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040054 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040055 addr,
giolekva8aa73e82022-07-09 11:34:39 +040056 signer,
57 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040058 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040059 }, nil
60}
61
gioe72b54f2024-04-22 10:44:41 +040062type ClientGetter interface {
63 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
64}
65
66type RealClientGetter struct{}
67
68func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
69 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040070 err := backoff.RetryNotify(func() error {
71 var err error
72 client, err = NewClient(addr, clientPrivateKey, log)
73 if err != nil {
74 return err
75 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040076 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040077 return err
78 }
79 return nil
80 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
81 log.Printf("Failed to create client: %s\n", err.Error())
82 })
83 return client, err
84}
85
gioe72b54f2024-04-22 10:44:41 +040086func (ss *realClient) Address() string {
87 return ss.addr
88}
89
90func (ss *realClient) Signer() ssh.Signer {
91 return ss.signer
92}
93
94func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040095 log.Printf("Adding user %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040096 if err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040097 return err
98 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040099 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +0400100}
101
gioe72b54f2024-04-22 10:44:41 +0400102func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400103 log.Printf("Making user %s admin", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400104 return ss.RunCommand("user", "set-admin", name, "true")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400105}
106
gioe72b54f2024-04-22 10:44:41 +0400107func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400108 log.Printf("Adding public key: %s %s\n", user, pubKey)
109 return ss.RunCommand("user", "add-pubkey", user, pubKey)
110}
111
gioe72b54f2024-04-22 10:44:41 +0400112func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400113 log.Printf("Removing public key: %s %s\n", user, pubKey)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400114 return ss.RunCommand("user", "remove-pubkey", user, pubKey)
115}
116
gioe72b54f2024-04-22 10:44:41 +0400117func (ss *realClient) RunCommand(args ...string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400118 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400119 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400120 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400121 if err != nil {
122 return err
123 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400124 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400125 session, err := client.NewSession()
126 if err != nil {
127 return err
128 }
129 defer session.Close()
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400130 session.Stdout = os.Stdout
131 session.Stderr = os.Stderr
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400132 return session.Run(cmd)
giolekva8aa73e82022-07-09 11:34:39 +0400133}
134
gioe72b54f2024-04-22 10:44:41 +0400135func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400136 log.Printf("Adding repository %s", name)
gio0eaf2712024-04-14 13:08:46 +0400137 if err := ss.RunCommand("repo", "info", name); err == nil {
138 return ErrorAlreadyExists
139 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400140 return ss.RunCommand("repo", "create", name)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400141}
142
gioe72b54f2024-04-22 10:44:41 +0400143func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400144 log.Printf("Adding read-write collaborator %s %s", repo, user)
145 return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
146}
147
gioe72b54f2024-04-22 10:44:41 +0400148func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400149 log.Printf("Adding read-only collaborator %s %s", repo, user)
150 return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
giolekva8aa73e82022-07-09 11:34:39 +0400151}
152
gio0eaf2712024-04-14 13:08:46 +0400153func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
154 log.Printf("Adding webhook %s %s", repo, url)
155 return ss.RunCommand(append(
156 []string{"repo", "webhook", "create", repo, url},
157 opts...,
158 )...)
159}
160
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400161type Repository struct {
162 *git.Repository
163 Addr RepositoryAddress
164}
165
gioe72b54f2024-04-22 10:44:41 +0400166func (ss *realClient) GetRepo(name string) (RepoIO, error) {
167 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
168 if err != nil {
169 return nil, err
170 }
171 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400172}
173
174type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400175 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400176 Name string
177}
178
179func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400180 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
181 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400182 return RepositoryAddress{}, fmt.Errorf("Invalid address")
183 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400184 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400185}
186
187func (r RepositoryAddress) FullAddress() string {
188 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
189}
190
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400191func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400192 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400193 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
194 URL: addr.FullAddress(),
195 Auth: &gitssh.PublicKeys{
196 User: "git",
197 Signer: signer,
198 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
199 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
200 // TODO(giolekva): verify server public key
201 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
202 return nil
203 },
204 },
205 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400206 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400207 ReferenceName: "refs/heads/master",
208 Depth: 1,
209 InsecureSkipTLS: true,
210 Progress: os.Stdout,
211 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400212 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400213 return nil, err
214 }
215 return &Repository{
216 Repository: c,
217 Addr: addr,
218 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400219}
220
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400221// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400222func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400223 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400224 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400225 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400226 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400227 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
228 // TODO(giolekva): verify server public key
229 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
230 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400231 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400232 return a
233 // return &gitssh.PublicKeys{
234 // User: "git",
235 // Signer: ss.Signer,
236 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
237 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
238 // // TODO(giolekva): verify server public key
239 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
240 // return nil
241 // },
242 // },
243 // }
giolekva8aa73e82022-07-09 11:34:39 +0400244}
245
gioe72b54f2024-04-22 10:44:41 +0400246func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400247 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400248 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400249 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400250 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
251 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
252 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400253 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400254 return nil
255 },
256 },
257 }
258}
259
gioe72b54f2024-04-22 10:44:41 +0400260func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400261 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400262 config := &ssh.ClientConfig{
263 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400264 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400265 },
266 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400267 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400268 return nil
269 },
270 }
gioe72b54f2024-04-22 10:44:41 +0400271 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400272 if err != nil {
273 return nil, err
274 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400275 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400276 return ret, nil
277}
278
gioe72b54f2024-04-22 10:44:41 +0400279func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400280 return &ssh.ClientConfig{
281 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400282 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400283 },
284 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
285 // TODO(giolekva): verify server public key
286 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400287 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400288 return nil
289 },
290 }
291}
292
gioe72b54f2024-04-22 10:44:41 +0400293func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400294 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
295}
296
gioe72b54f2024-04-22 10:44:41 +0400297func (ss *realClient) addressGit() string {
298 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400299}