repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

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
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
M pgs/calc_route.go
+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] != "" {
M pgs/calc_route_test.go
+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 	}
M pgs/header_test.go
+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 	}
M pgs/redirect_test.go
+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 	}
M pgs/ssh.go
+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 }
M pgs/ssh_test.go
+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