blob: ae1206dedb62d4c5a2e09e1459c704cb494216f4 [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 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040052 if _, err := client.GetPublicKeys(); err != nil {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040053 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 Lekveishvili106a9352023-12-04 11:20:11 +0400145 fmt.Printf("Cloning repository: %s %s\n", addr.Addr, addr.Name)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400146 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
147 URL: addr.FullAddress(),
148 Auth: &gitssh.PublicKeys{
149 User: "git",
150 Signer: signer,
151 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
152 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
153 // TODO(giolekva): verify server public key
154 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
155 return nil
156 },
157 },
158 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400159 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400160 ReferenceName: "refs/heads/master",
161 Depth: 1,
162 InsecureSkipTLS: true,
163 Progress: os.Stdout,
164 })
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400165 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400166 return nil, err
167 }
168 return &Repository{
169 Repository: c,
170 Addr: addr,
171 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400172}
173
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400174// TODO(giolekva): dead code
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400175func (ss *Client) authSSH() gitssh.AuthMethod {
176 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400177 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400178 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400179 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400180 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
181 // TODO(giolekva): verify server public key
182 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
183 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400184 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400185 return a
186 // return &gitssh.PublicKeys{
187 // User: "git",
188 // Signer: ss.Signer,
189 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
190 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
191 // // TODO(giolekva): verify server public key
192 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
193 // return nil
194 // },
195 // },
196 // }
giolekva8aa73e82022-07-09 11:34:39 +0400197}
198
199func (ss *Client) authGit() *gitssh.PublicKeys {
200 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400201 User: "git",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400202 Signer: ss.Signer,
giolekva8aa73e82022-07-09 11:34:39 +0400203 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
204 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
205 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400206 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400207 return nil
208 },
209 },
210 }
211}
212
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400213func (ss *Client) GetPublicKeys() ([]string, error) {
214 var ret []string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400215 config := &ssh.ClientConfig{
216 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400217 ssh.PublicKeys(ss.Signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400218 },
219 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400220 ret = append(ret, string(ssh.MarshalAuthorizedKey(key)))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400221 return nil
222 },
223 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400224 client, err := ssh.Dial("tcp", ss.Addr, config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400225 if err != nil {
226 return nil, err
227 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400228 defer client.Close()
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400229 return ret, nil
230}
231
giolekva8aa73e82022-07-09 11:34:39 +0400232func (ss *Client) sshClientConfig() *ssh.ClientConfig {
233 return &ssh.ClientConfig{
234 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400235 ssh.PublicKeys(ss.Signer),
giolekva8aa73e82022-07-09 11:34:39 +0400236 },
237 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
238 // TODO(giolekva): verify server public key
239 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400240 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400241 return nil
242 },
243 }
244}
245
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400246func (ss *Client) GetRepoAddress(name string) string {
247 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
248}
249
giolekva8aa73e82022-07-09 11:34:39 +0400250func (ss *Client) addressGit() string {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400251 return fmt.Sprintf("ssh://%s", ss.Addr)
giolekva8aa73e82022-07-09 11:34:39 +0400252}