blob: 269f3d337b53d956340c83a5de2afdd171fb3df1 [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
gioe72b54f2024-04-22 10:44:41 +040022type Client interface {
23 Address() string
24 Signer() ssh.Signer
25 GetPublicKeys() ([]string, error)
26 GetRepo(name string) (RepoIO, error)
27 GetRepoAddress(name string) string
28 AddRepository(name string) error
29 AddUser(name, pubKey string) error
30 AddPublicKey(user string, pubKey string) error
31 RemovePublicKey(user string, pubKey string) error
32 MakeUserAdmin(name string) error
33 AddReadWriteCollaborator(repo, user string) error
34 AddReadOnlyCollaborator(repo, user string) error
35}
36
37type realClient struct {
38 addr string
39 signer ssh.Signer
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040040 log *log.Logger
41 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040042}
43
gioe72b54f2024-04-22 10:44:41 +040044func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040045 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
46 if err != nil {
47 return nil, err
48 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040049 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040050 log.Printf("Created signer")
gioe72b54f2024-04-22 10:44:41 +040051 return &realClient{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040052 addr,
giolekva8aa73e82022-07-09 11:34:39 +040053 signer,
54 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040055 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040056 }, nil
57}
58
gioe72b54f2024-04-22 10:44:41 +040059type ClientGetter interface {
60 Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error)
61}
62
63type RealClientGetter struct{}
64
65func (c RealClientGetter) Get(addr string, clientPrivateKey []byte, log *log.Logger) (Client, error) {
66 var client Client
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040067 err := backoff.RetryNotify(func() error {
68 var err error
69 client, err = NewClient(addr, clientPrivateKey, log)
70 if err != nil {
71 return err
72 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040073 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040074 return err
75 }
76 return nil
77 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
78 log.Printf("Failed to create client: %s\n", err.Error())
79 })
80 return client, err
81}
82
gioe72b54f2024-04-22 10:44:41 +040083func (ss *realClient) Address() string {
84 return ss.addr
85}
86
87func (ss *realClient) Signer() ssh.Signer {
88 return ss.signer
89}
90
91func (ss *realClient) AddUser(name, pubKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040092 log.Printf("Adding user %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040093 if err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040094 return err
95 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040096 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +040097}
98
gioe72b54f2024-04-22 10:44:41 +040099func (ss *realClient) MakeUserAdmin(name string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400100 log.Printf("Making user %s admin", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400101 return ss.RunCommand("user", "set-admin", name, "true")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400102}
103
gioe72b54f2024-04-22 10:44:41 +0400104func (ss *realClient) AddPublicKey(user string, pubKey string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400105 log.Printf("Adding public key: %s %s\n", user, pubKey)
106 return ss.RunCommand("user", "add-pubkey", user, pubKey)
107}
108
gioe72b54f2024-04-22 10:44:41 +0400109func (ss *realClient) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400110 log.Printf("Removing public key: %s %s\n", user, pubKey)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400111 return ss.RunCommand("user", "remove-pubkey", user, pubKey)
112}
113
gioe72b54f2024-04-22 10:44:41 +0400114func (ss *realClient) RunCommand(args ...string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400115 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400116 log.Printf("Running command %s", cmd)
gioe72b54f2024-04-22 10:44:41 +0400117 client, err := ssh.Dial("tcp", ss.addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +0400118 if err != nil {
119 return err
120 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400121 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +0400122 session, err := client.NewSession()
123 if err != nil {
124 return err
125 }
126 defer session.Close()
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400127 session.Stdout = os.Stdout
128 session.Stderr = os.Stderr
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400129 return session.Run(cmd)
giolekva8aa73e82022-07-09 11:34:39 +0400130}
131
gioe72b54f2024-04-22 10:44:41 +0400132func (ss *realClient) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400133 log.Printf("Adding repository %s", name)
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400134 return ss.RunCommand("repo", "create", name)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400135}
136
gioe72b54f2024-04-22 10:44:41 +0400137func (ss *realClient) AddReadWriteCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400138 log.Printf("Adding read-write collaborator %s %s", repo, user)
139 return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
140}
141
gioe72b54f2024-04-22 10:44:41 +0400142func (ss *realClient) AddReadOnlyCollaborator(repo, user string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400143 log.Printf("Adding read-only collaborator %s %s", repo, user)
144 return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
giolekva8aa73e82022-07-09 11:34:39 +0400145}
146
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400147type Repository struct {
148 *git.Repository
149 Addr RepositoryAddress
150}
151
gioe72b54f2024-04-22 10:44:41 +0400152func (ss *realClient) GetRepo(name string) (RepoIO, error) {
153 r, err := CloneRepository(RepositoryAddress{ss.addr, name}, ss.signer)
154 if err != nil {
155 return nil, err
156 }
157 return NewRepoIO(r, ss.signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400158}
159
160type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400161 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400162 Name string
163}
164
165func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400166 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
167 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400168 return RepositoryAddress{}, fmt.Errorf("Invalid address")
169 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400170 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400171}
172
173func (r RepositoryAddress) FullAddress() string {
174 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
175}
176
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400177func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400178 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400179 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
180 URL: addr.FullAddress(),
181 Auth: &gitssh.PublicKeys{
182 User: "git",
183 Signer: signer,
184 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
185 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
186 // TODO(giolekva): verify server public key
187 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
188 return nil
189 },
190 },
191 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400192 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400193 ReferenceName: "refs/heads/master",
194 Depth: 1,
195 InsecureSkipTLS: true,
196 Progress: os.Stdout,
197 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400198 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400199 return nil, err
200 }
201 return &Repository{
202 Repository: c,
203 Addr: addr,
204 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400205}
206
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400207// TODO(giolekva): dead code
gioe72b54f2024-04-22 10:44:41 +0400208func (ss *realClient) authSSH() gitssh.AuthMethod {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400209 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400210 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400211 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400212 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400213 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
214 // TODO(giolekva): verify server public key
215 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
216 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400217 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400218 return a
219 // return &gitssh.PublicKeys{
220 // User: "git",
221 // Signer: ss.Signer,
222 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
223 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
224 // // TODO(giolekva): verify server public key
225 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
226 // return nil
227 // },
228 // },
229 // }
giolekva8aa73e82022-07-09 11:34:39 +0400230}
231
gioe72b54f2024-04-22 10:44:41 +0400232func (ss *realClient) authGit() *gitssh.PublicKeys {
giolekva8aa73e82022-07-09 11:34:39 +0400233 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400234 User: "git",
gioe72b54f2024-04-22 10:44:41 +0400235 Signer: ss.signer,
giolekva8aa73e82022-07-09 11:34:39 +0400236 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
237 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
238 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400239 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400240 return nil
241 },
242 },
243 }
244}
245
gioe72b54f2024-04-22 10:44:41 +0400246func (ss *realClient) GetPublicKeys() ([]string, error) {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400247 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400248 config := &ssh.ClientConfig{
249 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400250 ssh.PublicKeys(ss.signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400251 },
252 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400253 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400254 return nil
255 },
256 }
gioe72b54f2024-04-22 10:44:41 +0400257 client, err := ssh.Dial("tcp", ss.addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400258 if err != nil {
259 return nil, err
260 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400261 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400262 return ret, nil
263}
264
gioe72b54f2024-04-22 10:44:41 +0400265func (ss *realClient) sshClientConfig() *ssh.ClientConfig {
giolekva8aa73e82022-07-09 11:34:39 +0400266 return &ssh.ClientConfig{
267 Auth: []ssh.AuthMethod{
gioe72b54f2024-04-22 10:44:41 +0400268 ssh.PublicKeys(ss.signer),
giolekva8aa73e82022-07-09 11:34:39 +0400269 },
270 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
271 // TODO(giolekva): verify server public key
272 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400273 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400274 return nil
275 },
276 }
277}
278
gioe72b54f2024-04-22 10:44:41 +0400279func (ss *realClient) GetRepoAddress(name string) string {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400280 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
281}
282
gioe72b54f2024-04-22 10:44:41 +0400283func (ss *realClient) addressGit() string {
284 return fmt.Sprintf("ssh://%s", ss.addr)
giolekva8aa73e82022-07-09 11:34:39 +0400285}