- commit
- e522d71
- parent
- 64dea57
- author
- Antonio Mika
- date
- 2025-02-23 10:53:44 -0500 EST
Merge pull request #186 from picosh/rsync-test Rsync test
7 files changed,
+206,
-17
M
go.mod
+1,
-1
1@@ -59,6 +59,7 @@ require (
2 github.com/picosh/tunkit v0.0.0-20240905223921-532404cef9d9
3 github.com/picosh/utils v0.0.0-20241120033529-8ca070c09bf4
4 github.com/pkg/sftp v1.13.7
5+ github.com/prometheus/client_golang v1.20.5
6 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
7 github.com/sendgrid/sendgrid-go v3.16.0+incompatible
8 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
9@@ -263,7 +264,6 @@ require (
10 github.com/pkg/errors v0.9.1 // indirect
11 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
12 github.com/pquerna/cachecontrol v0.2.0 // indirect
13- github.com/prometheus/client_golang v1.20.5 // indirect
14 github.com/prometheus/client_model v0.6.1 // indirect
15 github.com/prometheus/common v0.60.1 // indirect
16 github.com/prometheus/procfs v0.15.1 // indirect
+0,
-1
1@@ -229,7 +229,6 @@ func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpRe
2 case "none":
3 fallthrough
4 default:
5- break
6 }
7
8 if len(match) > 0 && match[0] != "" {
+1,
-1
1@@ -650,7 +650,7 @@ func TestCalcRoutes(t *testing.T) {
2 t.Run(fixture.Name, func(t *testing.T) {
3 if cmp.Equal(fixture.Actual, fixture.Expected) == false {
4 //nolint
5- t.Fatalf(cmp.Diff(fixture.Expected, fixture.Actual))
6+ t.Fatal(cmp.Diff(fixture.Expected, fixture.Actual))
7 }
8 })
9 }
+1,
-1
1@@ -173,7 +173,7 @@ func TestParseHeaderText(t *testing.T) {
2 fmt.Println(results)
3 if cmp.Equal(results, fixture.expect) == false {
4 //nolint
5- t.Fatalf(cmp.Diff(fixture.expect, results))
6+ t.Fatal(cmp.Diff(fixture.expect, results))
7 }
8 })
9 }
+1,
-1
1@@ -109,7 +109,7 @@ func TestParseRedirectText(t *testing.T) {
2 }
3 if cmp.Equal(results, fixture.expect) == false {
4 //nolint
5- t.Fatalf(cmp.Diff(fixture.expect, results))
6+ t.Fatal(cmp.Diff(fixture.expect, results))
7 }
8 })
9 }
+12,
-5
1@@ -90,15 +90,17 @@ func StartSshServer(cfg *PgsConfig, killCh chan error) {
2 done := make(chan os.Signal, 1)
3 signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
4 logger.Info("starting SSH server on", "host", host, "port", port)
5+
6 go func() {
7 if err = s.ListenAndServe(); err != nil {
8- logger.Error("serve", "err", err.Error())
9- os.Exit(1)
10+ if err != ssh.ErrServerClosed {
11+ logger.Error("serve", "err", err.Error())
12+ os.Exit(1)
13+ }
14 }
15 }()
16
17- select {
18- case <-done:
19+ exit := func() {
20 logger.Info("stopping ssh server")
21 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
22 defer func() { cancel() }()
23@@ -106,7 +108,12 @@ func StartSshServer(cfg *PgsConfig, killCh chan error) {
24 logger.Error("shutdown", "err", err.Error())
25 os.Exit(1)
26 }
27+ }
28+
29+ select {
30 case <-killCh:
31- logger.Info("stopping ssh server")
32+ exit()
33+ case <-done:
34+ exit()
35 }
36 }
+190,
-7
1@@ -3,9 +3,13 @@ package pgs
2 import (
3 "crypto/ed25519"
4 "crypto/rand"
5+ "encoding/pem"
6+ "fmt"
7 "io"
8 "log/slog"
9 "os"
10+ "os/exec"
11+ "path/filepath"
12 "strings"
13 "testing"
14 "time"
15@@ -15,11 +19,18 @@ import (
16 "github.com/picosh/pico/shared/storage"
17 "github.com/picosh/utils"
18 "github.com/pkg/sftp"
19+ "github.com/prometheus/client_golang/prometheus"
20 "golang.org/x/crypto/ssh"
21 )
22
23-func TestSshServer(t *testing.T) {
24- logger := slog.Default()
25+func TestSshServerSftp(t *testing.T) {
26+ opts := &slog.HandlerOptions{
27+ AddSource: true,
28+ Level: slog.LevelInfo,
29+ }
30+ logger := slog.New(
31+ slog.NewTextHandler(os.Stdout, opts),
32+ )
33 dbpool := pgsdb.NewDBMemory(logger)
34 // setup test data
35 dbpool.SetupTestData()
36@@ -29,6 +40,7 @@ func TestSshServer(t *testing.T) {
37 }
38 cfg := NewPgsConfig(logger, dbpool, st)
39 done := make(chan error)
40+ prometheus.DefaultRegisterer = prometheus.NewRegistry()
41 go StartSshServer(cfg, done)
42 // Hack to wait for startup
43 time.Sleep(time.Millisecond * 100)
44@@ -54,12 +66,177 @@ func TestSshServer(t *testing.T) {
45 return
46 }
47
48- done <- nil
49+ close(done)
50+
51+ p, err := os.FindProcess(os.Getpid())
52+ if err != nil {
53+ t.Fatal(err)
54+ return
55+ }
56+
57+ err = p.Signal(os.Interrupt)
58+ if err != nil {
59+ t.Fatal(err)
60+ return
61+ }
62+ <-time.After(10 * time.Millisecond)
63+}
64+
65+func TestSshServerRsync(t *testing.T) {
66+ opts := &slog.HandlerOptions{
67+ AddSource: true,
68+ Level: slog.LevelInfo,
69+ }
70+ logger := slog.New(
71+ slog.NewTextHandler(os.Stdout, opts),
72+ )
73+ dbpool := pgsdb.NewDBMemory(logger)
74+ // setup test data
75+ dbpool.SetupTestData()
76+ st, err := storage.NewStorageMemory(map[string]map[string]string{})
77+ if err != nil {
78+ panic(err)
79+ }
80+ cfg := NewPgsConfig(logger, dbpool, st)
81+ done := make(chan error)
82+ prometheus.DefaultRegisterer = prometheus.NewRegistry()
83+ go StartSshServer(cfg, done)
84+ // Hack to wait for startup
85+ time.Sleep(time.Millisecond * 100)
86+
87+ user := GenerateUser()
88+ key := utils.KeyForKeyText(user.signer.PublicKey())
89+ // add user's pubkey to the default test account
90+ dbpool.Pubkeys = append(dbpool.Pubkeys, &db.PublicKey{
91+ ID: "nice-pubkey",
92+ UserID: dbpool.Users[0].ID,
93+ Key: key,
94+ })
95+
96+ conn, err := user.NewClient()
97+ if err != nil {
98+ t.Error(err)
99+ return
100+ }
101+ defer conn.Close()
102+
103+ // open an SFTP session over an existing ssh connection.
104+ client, err := sftp.NewClient(conn)
105+ if err != nil {
106+ cfg.Logger.Error("could not create sftp client", "err", err)
107+ panic(err)
108+ }
109+ defer client.Close()
110+
111+ name, err := os.MkdirTemp("", "rsync-")
112+ if err != nil {
113+ panic(err)
114+ }
115+
116+ // remove the temporary directory at the end of the program
117+ // defer os.RemoveAll(name)
118+
119+ block := &pem.Block{
120+ Type: "OPENSSH PRIVATE KEY",
121+ Bytes: user.privateKey,
122+ }
123+ keyFile := filepath.Join(name, "id_ed25519")
124+ err = os.WriteFile(
125+ keyFile,
126+ pem.EncodeToMemory(block), 0600,
127+ )
128+ if err != nil {
129+ t.Fatal(err)
130+ }
131+
132+ index := "<!doctype html><html><body>index</body></html>"
133+ err = os.WriteFile(
134+ filepath.Join(name, "index.html"),
135+ []byte(index), 0666,
136+ )
137+ if err != nil {
138+ t.Fatal(err)
139+ }
140+
141+ about := "<!doctype html><html><body>about</body></html>"
142+ aboutFile := filepath.Join(name, "about.html")
143+ err = os.WriteFile(
144+ aboutFile,
145+ []byte(about), 0666,
146+ )
147+ if err != nil {
148+ t.Fatal(err)
149+ }
150+
151+ contact := "<!doctype html><html><body>contact</body></html>"
152+ err = os.WriteFile(
153+ filepath.Join(name, "contact.html"),
154+ []byte(contact), 0666,
155+ )
156+ if err != nil {
157+ t.Fatal(err)
158+ }
159+
160+ eCmd := fmt.Sprintf(
161+ "ssh -p 2222 -o IdentitiesOnly=yes -i %s -o StrictHostKeyChecking=no",
162+ keyFile,
163+ )
164+
165+ // copy files
166+ cmd := exec.Command("rsync", "-rv", "-e", eCmd, name+"/", "localhost:/test")
167+ fmt.Println(cmd.Args)
168+ result, err := cmd.CombinedOutput()
169+ if err != nil {
170+ fmt.Println(string(result), err)
171+ t.Error(err)
172+ return
173+ }
174+
175+ // check it's there
176+ fi, err := client.Lstat("/test/about.html")
177+ if err != nil {
178+ cfg.Logger.Error("could not get stat for file", "err", err)
179+ t.Error("about.html not found")
180+ return
181+ }
182+ if fi.Size() != 46 {
183+ cfg.Logger.Error("about.html wrong size", "size", fi.Size())
184+ t.Error("about.html wrong size")
185+ return
186+ }
187+
188+ // remove about file
189+ os.Remove(aboutFile)
190+
191+ // copy files with delete
192+ delCmd := exec.Command("rsync", "-rv", "-e", eCmd, name+"/", "localhost:/test")
193+ result, err = delCmd.CombinedOutput()
194+ if err != nil {
195+ fmt.Println(string(result), err)
196+ t.Error(err)
197+ return
198+ }
199+
200+ close(done)
201+
202+ p, err := os.FindProcess(os.Getpid())
203+ if err != nil {
204+ t.Fatal(err)
205+ return
206+ }
207+
208+ err = p.Signal(os.Interrupt)
209+ if err != nil {
210+ t.Fatal(err)
211+ return
212+ }
213+ <-time.After(10 * time.Millisecond)
214 }
215
216 type UserSSH struct {
217- username string
218- signer ssh.Signer
219+ username string
220+ signer ssh.Signer
221+ privateKey []byte
222 }
223
224 func NewUserSSH(username string, signer ssh.Signer) *UserSSH {
225@@ -146,14 +323,20 @@ func GenerateUser() UserSSH {
226 panic(err)
227 }
228
229+ b, err := ssh.MarshalPrivateKey(userKey, "")
230+ if err != nil {
231+ panic(err)
232+ }
233+
234 userSigner, err := ssh.NewSignerFromKey(userKey)
235 if err != nil {
236 panic(err)
237 }
238
239 return UserSSH{
240- username: "testuser",
241- signer: userSigner,
242+ username: "testuser",
243+ signer: userSigner,
244+ privateKey: b.Bytes,
245 }
246 }
247