blob: 5686ba5c7e88906c80381a12c8fba65cad213114 [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
22type Client struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040023 Addr string
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040024 Signer ssh.Signer
25 log *log.Logger
26 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040027}
28
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040029func NewClient(addr string, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040030 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
31 if err != nil {
32 return nil, err
33 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040034 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040035 log.Printf("Created signer")
giolekva8aa73e82022-07-09 11:34:39 +040036 return &Client{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040037 addr,
giolekva8aa73e82022-07-09 11:34:39 +040038 signer,
39 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040040 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040041 }, nil
42}
43
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040044func WaitForClient(addr string, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
45 var client *Client
46 err := backoff.RetryNotify(func() error {
47 var err error
48 client, err = NewClient(addr, clientPrivateKey, log)
49 if err != nil {
50 return err
51 }
52 if _, err := client.GetPublicKey(); err != nil {
53 return err
54 }
55 return nil
56 }, backoff.NewConstantBackOff(5*time.Second), func(err error, _ time.Duration) {
57 log.Printf("Failed to create client: %s\n", err.Error())
58 })
59 return client, err
60}
61
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040062func (ss *Client) AddUser(name, pubKey string) error {
63 log.Printf("Adding user %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040064 if err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040065 return err
66 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040067 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +040068}
69
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040070func (ss *Client) MakeUserAdmin(name string) error {
71 log.Printf("Making user %s admin", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040072 return ss.RunCommand("user", "set-admin", name, "true")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040073}
74
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040075func (ss *Client) AddPublicKey(user string, pubKey string) error {
76 log.Printf("Adding public key: %s %s\n", user, pubKey)
77 return ss.RunCommand("user", "add-pubkey", user, pubKey)
78}
79
80func (ss *Client) RemovePublicKey(user string, pubKey string) error {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040081 log.Printf("Removing public key: %s %s\n", user, pubKey)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040082 return ss.RunCommand("user", "remove-pubkey", user, pubKey)
83}
84
85func (ss *Client) RunCommand(args ...string) error {
86 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040087 log.Printf("Running command %s", cmd)
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040088 client, err := ssh.Dial("tcp", ss.Addr, ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +040089 if err != nil {
90 return err
91 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040092 defer client.Close()
giolekva8aa73e82022-07-09 11:34:39 +040093 session, err := client.NewSession()
94 if err != nil {
95 return err
96 }
97 defer session.Close()
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040098 session.Stdout = os.Stdout
99 session.Stderr = os.Stderr
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400100 return session.Run(cmd)
giolekva8aa73e82022-07-09 11:34:39 +0400101}
102
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400103func (ss *Client) AddRepository(name string) error {
giolekva8aa73e82022-07-09 11:34:39 +0400104 log.Printf("Adding repository %s", name)
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400105 return ss.RunCommand("repo", "create", name)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400106}
107
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400108func (ss *Client) AddReadWriteCollaborator(repo, user string) error {
109 log.Printf("Adding read-write collaborator %s %s", repo, user)
110 return ss.RunCommand("repo", "collab", "add", repo, user, "read-write")
111}
112
113func (ss *Client) AddReadOnlyCollaborator(repo, user string) error {
114 log.Printf("Adding read-only collaborator %s %s", repo, user)
115 return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
giolekva8aa73e82022-07-09 11:34:39 +0400116}
117
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400118type Repository struct {
119 *git.Repository
120 Addr RepositoryAddress
121}
122
123func (ss *Client) GetRepo(name string) (*Repository, error) {
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400124 return CloneRepository(RepositoryAddress{ss.Addr, name}, ss.Signer)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400125}
126
127type RepositoryAddress struct {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400128 Addr string
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400129 Name string
130}
131
132func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400133 items := regexp.MustCompile(`ssh://(.*)/(.*)`).FindStringSubmatch(addr)
134 if len(items) != 3 {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400135 return RepositoryAddress{}, fmt.Errorf("Invalid address")
136 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400137 return RepositoryAddress{items[1], items[2]}, nil
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400138}
139
140func (r RepositoryAddress) FullAddress() string {
141 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
142}
143
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400144func CloneRepository(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400145 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
146 URL: addr.FullAddress(),
147 Auth: &gitssh.PublicKeys{
148 User: "git",
149 Signer: signer,
150 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
151 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
152 // TODO(giolekva): verify server public key
153 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
154 return nil
155 },
156 },
157 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400158 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400159 ReferenceName: "refs/heads/master",
160 Depth: 1,
161 InsecureSkipTLS: true,
162 Progress: os.Stdout,
163 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400164 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400165 return nil, err
166 }
167 return &Repository{
168 Repository: c,
169 Addr: addr,
170 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400171}
172
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400173// TODO(giolekva): dead code
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400174func (ss *Client) authSSH() gitssh.AuthMethod {
175 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400176 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400177 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400178 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400179 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
180 // TODO(giolekva): verify server public key
181 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
182 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400183 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400184 return a
185 // return &gitssh.PublicKeys{
186 // User: "git",
187 // Signer: ss.Signer,
188 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
189 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
190 // // TODO(giolekva): verify server public key
191 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
192 // return nil
193 // },
194 // },
195 // }
giolekva8aa73e82022-07-09 11:34:39 +0400196}
197
198func (ss *Client) authGit() *gitssh.PublicKeys {
199 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400200 User: "git",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400201 Signer: ss.Signer,
giolekva8aa73e82022-07-09 11:34:39 +0400202 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
203 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
204 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400205 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400206 return nil
207 },
208 },
209 }
210}
211
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400212func (ss *Client) GetPublicKey() ([]byte, error) {
213 var ret []byte
214 config := &ssh.ClientConfig{
215 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400216 ssh.PublicKeys(ss.Signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400217 },
218 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
219 ret = ssh.MarshalAuthorizedKey(key)
220 return nil
221 },
222 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400223 client, err := ssh.Dial("tcp", ss.Addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400224 if err != nil {
225 return nil, err
226 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400227 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400228 return ret, nil
229}
230
giolekva8aa73e82022-07-09 11:34:39 +0400231func (ss *Client) sshClientConfig() *ssh.ClientConfig {
232 return &ssh.ClientConfig{
233 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400234 ssh.PublicKeys(ss.Signer),
giolekva8aa73e82022-07-09 11:34:39 +0400235 },
236 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
237 // TODO(giolekva): verify server public key
238 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400239 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400240 return nil
241 },
242 }
243}
244
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400245func (ss *Client) GetRepoAddress(name string) string {
246 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
247}
248
giolekva8aa73e82022-07-09 11:34:39 +0400249func (ss *Client) addressGit() string {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400250 return fmt.Sprintf("ssh://%s", ss.Addr)
giolekva8aa73e82022-07-09 11:34:39 +0400251}