blob: 635ee0e8fdf4c938bc5341841c31fbb7f6756670 [file] [log] [blame]
giolekva8aa73e82022-07-09 11:34:39 +04001package soft
2
3import (
4 "fmt"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04005 "golang.org/x/crypto/ssh"
giolekva8aa73e82022-07-09 11:34:39 +04006 "log"
7 "net"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +04008 "net/netip"
giolekva8aa73e82022-07-09 11:34:39 +04009 "os"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040010 "regexp"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040011 "strings"
giolekva8aa73e82022-07-09 11:34:39 +040012
13 "github.com/go-git/go-billy/v5/memfs"
14 "github.com/go-git/go-git/v5"
giolekva8aa73e82022-07-09 11:34:39 +040015 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
16 "github.com/go-git/go-git/v5/storage/memory"
giolekva8aa73e82022-07-09 11:34:39 +040017)
18
19type Client struct {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040020 Addr netip.AddrPort
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040021 Signer ssh.Signer
22 log *log.Logger
23 pemBytes []byte
giolekva8aa73e82022-07-09 11:34:39 +040024}
25
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040026func NewClient(addr netip.AddrPort, clientPrivateKey []byte, log *log.Logger) (*Client, error) {
giolekva8aa73e82022-07-09 11:34:39 +040027 signer, err := ssh.ParsePrivateKey(clientPrivateKey)
28 if err != nil {
29 return nil, err
30 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040031 log.SetPrefix("SOFT-SERVE: ")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040032 log.Printf("Created signer")
giolekva8aa73e82022-07-09 11:34:39 +040033 return &Client{
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040034 addr,
giolekva8aa73e82022-07-09 11:34:39 +040035 signer,
36 log,
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040037 clientPrivateKey,
giolekva8aa73e82022-07-09 11:34:39 +040038 }, nil
39}
40
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040041func (ss *Client) AddUser(name, pubKey string) error {
42 log.Printf("Adding user %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040043 if err := ss.RunCommand("user", "create", name); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +040044 return err
45 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040046 return ss.AddPublicKey(name, pubKey)
giolekva8aa73e82022-07-09 11:34:39 +040047}
48
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040049func (ss *Client) MakeUserAdmin(name string) error {
50 log.Printf("Making user %s admin", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040051 return ss.RunCommand("user", "set-admin", name, "true")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040052}
53
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040054func (ss *Client) AddPublicKey(user string, pubKey string) error {
55 log.Printf("Adding public key: %s %s\n", user, pubKey)
56 return ss.RunCommand("user", "add-pubkey", user, pubKey)
57}
58
59func (ss *Client) RemovePublicKey(user string, pubKey string) error {
60 log.Printf("Adding public key: %s %s\n", user, pubKey)
61 return ss.RunCommand("user", "remove-pubkey", user, pubKey)
62}
63
64func (ss *Client) RunCommand(args ...string) error {
65 cmd := strings.Join(args, " ")
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040066 log.Printf("Running command %s", cmd)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040067 client, err := ssh.Dial("tcp", ss.Addr.String(), ss.sshClientConfig())
giolekva8aa73e82022-07-09 11:34:39 +040068 if err != nil {
69 return err
70 }
71 session, err := client.NewSession()
72 if err != nil {
73 return err
74 }
75 defer session.Close()
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040076 session.Stdout = os.Stdout
77 session.Stderr = os.Stderr
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040078 return session.Run(cmd)
giolekva8aa73e82022-07-09 11:34:39 +040079}
80
81func (ss *Client) AddRepository(name, readme string) error {
82 log.Printf("Adding repository %s", name)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040083 return ss.RunCommand("repo", "create", name, "-d", fmt.Sprintf("\"%s\"", readme))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040084}
85
86func (ss *Client) AddCollaborator(repo, user string) error {
87 log.Printf("Adding collaborator %s %s", repo, user)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040088 return ss.RunCommand("repo", "collab", "add", repo, user)
giolekva8aa73e82022-07-09 11:34:39 +040089}
90
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040091type Repository struct {
92 *git.Repository
93 Addr RepositoryAddress
94}
95
96func (ss *Client) GetRepo(name string) (*Repository, error) {
97 return CloneRepo(RepositoryAddress{ss.Addr, name}, ss.Signer)
98}
99
100type RepositoryAddress struct {
101 Addr netip.AddrPort
102 Name string
103}
104
105func ParseRepositoryAddress(addr string) (RepositoryAddress, error) {
106 items := regexp.MustCompile(`ssh://.*)/(.*)`).FindStringSubmatch(addr)
107 if len(items) != 2 {
108 return RepositoryAddress{}, fmt.Errorf("Invalid address")
109 }
110 ipPort, err := netip.ParseAddrPort(items[1])
111 if err != nil {
112 return RepositoryAddress{}, err
113 }
114 return RepositoryAddress{ipPort, items[2]}, nil
115}
116
117func (r RepositoryAddress) FullAddress() string {
118 return fmt.Sprintf("ssh://%s/%s", r.Addr, r.Name)
119}
120
121func CloneRepo(addr RepositoryAddress, signer ssh.Signer) (*Repository, error) {
122 c, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
123 URL: addr.FullAddress(),
124 Auth: &gitssh.PublicKeys{
125 User: "git",
126 Signer: signer,
127 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
128 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
129 // TODO(giolekva): verify server public key
130 fmt.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
131 return nil
132 },
133 },
134 },
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400135 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400136 ReferenceName: "refs/heads/master",
137 Depth: 1,
138 InsecureSkipTLS: true,
139 Progress: os.Stdout,
140 })
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400141 if err != nil {
142 return nil, err
143 }
144 return &Repository{
145 Repository: c,
146 Addr: addr,
147 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400148}
149
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400150// TODO(giolekva): dead code
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400151func (ss *Client) authSSH() gitssh.AuthMethod {
152 a, err := gitssh.NewPublicKeys("git", ss.pemBytes, "")
giolekva8aa73e82022-07-09 11:34:39 +0400153 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400154 panic(err)
giolekva8aa73e82022-07-09 11:34:39 +0400155 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400156 a.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
157 // TODO(giolekva): verify server public key
158 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
159 return nil
giolekva8aa73e82022-07-09 11:34:39 +0400160 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400161 return a
162 // return &gitssh.PublicKeys{
163 // User: "git",
164 // Signer: ss.Signer,
165 // HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
166 // HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
167 // // TODO(giolekva): verify server public key
168 // ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
169 // return nil
170 // },
171 // },
172 // }
giolekva8aa73e82022-07-09 11:34:39 +0400173}
174
175func (ss *Client) authGit() *gitssh.PublicKeys {
176 return &gitssh.PublicKeys{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400177 User: "git",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400178 Signer: ss.Signer,
giolekva8aa73e82022-07-09 11:34:39 +0400179 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
180 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
181 // TODO(giolekva): verify server public key
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400182 ss.log.Printf("--- %+v\n", ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400183 return nil
184 },
185 },
186 }
187}
188
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400189func (ss *Client) GetPublicKey() ([]byte, error) {
190 var ret []byte
191 config := &ssh.ClientConfig{
192 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400193 ssh.PublicKeys(ss.Signer),
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400194 },
195 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
196 ret = ssh.MarshalAuthorizedKey(key)
197 return nil
198 },
199 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400200 _, err := ssh.Dial("tcp", ss.Addr.String(), config)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400201 if err != nil {
202 return nil, err
203 }
204 return ret, nil
205}
206
giolekva8aa73e82022-07-09 11:34:39 +0400207func (ss *Client) sshClientConfig() *ssh.ClientConfig {
208 return &ssh.ClientConfig{
209 Auth: []ssh.AuthMethod{
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400210 ssh.PublicKeys(ss.Signer),
giolekva8aa73e82022-07-09 11:34:39 +0400211 },
212 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
213 // TODO(giolekva): verify server public key
214 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400215 fmt.Printf("%s %s %s", hostname, remote, ssh.MarshalAuthorizedKey(key))
giolekva8aa73e82022-07-09 11:34:39 +0400216 return nil
217 },
218 }
219}
220
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400221func (ss *Client) GetRepoAddress(name string) string {
222 return fmt.Sprintf("%s/%s", ss.addressGit(), name)
223}
224
giolekva8aa73e82022-07-09 11:34:39 +0400225func (ss *Client) addressGit() string {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400226 return fmt.Sprintf("ssh://%s", ss.Addr)
giolekva8aa73e82022-07-09 11:34:39 +0400227}