1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
package main
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
)
// TestEndToEndRealRsync drives the actual rsync binary through the actual rsh
// transport binary into the in-process SSH server, which runs the remote rsync
// via /bin/sh. It proves real rsync + the Go transport move files intact.
func TestEndToEndRealRsync(t *testing.T) {
if testing.Short() {
t.Skip("short mode")
}
rsyncBin, err := exec.LookPath("rsync")
if err != nil {
t.Skip("rsync not installed")
}
// Build the rsh binary so rsync can exec it as its remote shell.
rshBin := filepath.Join(t.TempDir(), "rsh")
if out, err := exec.Command("go", "build", "-buildvcs=false", "-o", rshBin, ".").CombinedOutput(); err != nil {
t.Fatalf("build rsh: %v\n%s", err, out)
}
keyPath, pub := genClientKey(t)
remoteRoot := t.TempDir()
execute := func(cmd string, stdin io.Reader, stdout, stderr io.Writer) int {
return shellExecIn(remoteRoot, cmd, stdin, stdout, stderr)
}
srv := newTestServer(t, pub, execute)
kh := writeKnownHosts(t, "127.0.0.1", srv.port(), srv.hostKey.PublicKey())
src := t.TempDir()
// A normal rsync-over-SSH destination may contain shell punctuation. rsync
// escapes the remote command before handing it to rsh; this catches any
// transport change that loses those escapes while joining the command.
marker := filepath.Join(remoteRoot, "injected")
dst := filepath.Join(t.TempDir(), "backup path;literal$dollar'quote$(touch injected)")
mustWrite(t, filepath.Join(src, "a.txt"), "alpha")
mustWrite(t, filepath.Join(src, "sub", "b.bin"), "\x00\x01\x02\x03beta")
mustWrite(t, filepath.Join(src, "sub", "c.txt"), "gamma gamma gamma")
runRsync := func(mirror bool) {
t.Helper()
args := []string{
"-rt", "--out-format=%o %n", "--partial-dir=.rsend-partial", "--timeout=300",
"--no-perms", "--no-owner", "--no-group", "--omit-dir-times", "-e", rshBin,
}
if mirror {
args = append(args, "--delete-after")
}
args = append(args, src+"/", "u@127.0.0.1:"+dst+"/")
cmd := exec.Command(rsyncBin, args...)
cmd.Env = append(os.Environ(),
"RSH_KEY="+keyPath,
"RSH_KNOWN_HOSTS="+kh,
"RSH_PORT="+strconv.Itoa(srv.port()),
)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("rsync: %v\n%s", err, out)
}
}
runRsync(false)
checkSame(t, src, dst)
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("remote path was evaluated by the shell: %s", marker)
}
// An empty but readable source is a valid mirror. The app intentionally
// follows rsync here: every ordinary item in the destination is removed.
entries, err := os.ReadDir(src)
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if err := os.RemoveAll(filepath.Join(src, entry.Name())); err != nil {
t.Fatal(err)
}
}
runRsync(true)
entries, err = os.ReadDir(dst)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("empty mirror left destination entries: %v", entries)
}
}
func mustWrite(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func checkSame(t *testing.T, src, dst string) {
t.Helper()
err := filepath.Walk(src, func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rel, _ := filepath.Rel(src, p)
want, _ := os.ReadFile(p)
got, err := os.ReadFile(filepath.Join(dst, rel))
if err != nil {
t.Errorf("missing on dest: %s (%v)", rel, err)
return nil
}
if !bytes.Equal(got, want) {
t.Errorf("content mismatch for %s", rel)
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
|