aboutsummaryrefslogtreecommitdiff
path: root/rsh/e2e_test.go
blob: e6f82b9454dcfc0b7f82880973a1cd958fd534e8 (plain) (blame)
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)
	}
}