mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 13:39:37 +08:00
976 lines
27 KiB
Go
976 lines
27 KiB
Go
/*
|
|
Copyright 2018-2020 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
package scp
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/gravitational/trace"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
utils.InitLoggerForTests()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestHTTPSendFile(t *testing.T) {
|
|
outDir := t.TempDir()
|
|
|
|
expectedBytes := []byte("hello")
|
|
buf := bytes.NewReader(expectedBytes)
|
|
req, err := http.NewRequest("POST", "/", buf)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(expectedBytes)))
|
|
|
|
stdOut := bytes.NewBufferString("")
|
|
cmd, err := CreateHTTPUpload(
|
|
HTTPTransferRequest{
|
|
FileName: "filename",
|
|
RemoteLocation: outDir,
|
|
HTTPRequest: req,
|
|
Progress: stdOut,
|
|
User: "test-user",
|
|
})
|
|
require.NoError(t, err)
|
|
err = runSCP(cmd, "-v", "-t", outDir)
|
|
require.NoError(t, err)
|
|
bytesReceived, err := os.ReadFile(filepath.Join(outDir, "filename"))
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(string(bytesReceived), string(expectedBytes)))
|
|
}
|
|
|
|
func TestHTTPReceiveFile(t *testing.T) {
|
|
source := filepath.Join(t.TempDir(), "target")
|
|
|
|
contents := []byte("hello, file contents!")
|
|
err := os.WriteFile(source, contents, 0666)
|
|
require.NoError(t, err)
|
|
|
|
w := httptest.NewRecorder()
|
|
stdOut := bytes.NewBufferString("")
|
|
cmd, err := CreateHTTPDownload(
|
|
HTTPTransferRequest{
|
|
RemoteLocation: "/home/robots.txt",
|
|
HTTPResponse: w,
|
|
User: "test-user",
|
|
Progress: stdOut,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = runSCP(cmd, "-v", "-f", source)
|
|
require.NoError(t, err)
|
|
|
|
data, err := io.ReadAll(w.Body)
|
|
contentLengthStr := strconv.Itoa(len(data))
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(string(data), string(contents)))
|
|
require.Empty(t, cmp.Diff(contentLengthStr, w.Header().Get("Content-Length")))
|
|
require.Empty(t, cmp.Diff("application/octet-stream", w.Header().Get("Content-Type")))
|
|
require.Empty(t, cmp.Diff(`attachment;filename="robots.txt"`, w.Header().Get("Content-Disposition")))
|
|
}
|
|
|
|
func TestSend(t *testing.T) {
|
|
t.Parallel()
|
|
modtime := testNow
|
|
atime := testNow.Add(1 * time.Second)
|
|
dirModtime := testNow.Add(2 * time.Second)
|
|
dirAtime := testNow.Add(3 * time.Second)
|
|
logger := logrus.WithField(trace.Component, "t:send")
|
|
var testCases = []struct {
|
|
desc string
|
|
config Config
|
|
fs *testFS
|
|
args []string
|
|
}{
|
|
{
|
|
desc: "regular file preserving the attributes",
|
|
config: newSourceConfig("file", Flags{PreserveAttrs: true}),
|
|
args: args("-v", "-t", "-p"),
|
|
fs: newTestFS(logger, newFileTimes("file", modtime, atime, "file contents")),
|
|
},
|
|
{
|
|
desc: "directory preserving the attributes",
|
|
config: newSourceConfig("dir", Flags{PreserveAttrs: true, Recursive: true}),
|
|
args: args("-v", "-t", "-r", "-p"),
|
|
fs: newTestFS(
|
|
logger,
|
|
// Use timestamps extending backwards to test time application
|
|
newDirTimes("dir", dirModtime.Add(1*time.Second), dirAtime.Add(2*time.Second),
|
|
newFileTimes("dir/file", modtime.Add(1*time.Minute), atime.Add(2*time.Minute), "file contents"),
|
|
newDirTimes("dir/dir2", dirModtime, dirAtime,
|
|
newFileTimes("dir/dir2/file2", modtime, atime, "file2 contents")),
|
|
),
|
|
),
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
cmd, err := CreateCommand(tt.config)
|
|
require.NoError(t, err)
|
|
|
|
targetDir := t.TempDir()
|
|
target := filepath.Join(targetDir, tt.config.Flags.Target[0])
|
|
args := append(tt.args, target)
|
|
|
|
// Source is missing, expect an error.
|
|
err = runSCP(cmd, args...)
|
|
require.Regexp(t, "could not access local path.*no such file or directory", err)
|
|
|
|
tt.config.FileSystem = tt.fs
|
|
cmd, err = CreateCommand(tt.config)
|
|
require.NoError(t, err)
|
|
// Resend the data
|
|
err = runSCP(cmd, args...)
|
|
require.NoError(t, err)
|
|
|
|
fs := newEmptyTestFS(logger)
|
|
fromOS(t, targetDir, fs)
|
|
validateSCPTimes(t, fs, tt.fs)
|
|
validateSCPContents(t, fs, tt.fs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReceive(t *testing.T) {
|
|
t.Parallel()
|
|
modtime := testNow
|
|
atime := testNow.Add(1 * time.Second)
|
|
dirModtime := testNow.Add(2 * time.Second)
|
|
dirAtime := testNow.Add(3 * time.Second)
|
|
logger := logrus.WithField(trace.Component, "t:recv")
|
|
var testCases = []struct {
|
|
desc string
|
|
config Config
|
|
source string
|
|
sourceFS *testFS
|
|
expectedFS *testFS
|
|
}{
|
|
{
|
|
desc: "regular file preserving the attributes",
|
|
config: newTargetConfig("file", Flags{PreserveAttrs: true}),
|
|
source: "file",
|
|
sourceFS: newTestFS(logger, newFileTimes("file", modtime, atime, "file contents")),
|
|
},
|
|
{
|
|
desc: "directory preserving the attributes",
|
|
config: newTargetConfig("dir", Flags{PreserveAttrs: true, Recursive: true}),
|
|
source: "dir",
|
|
sourceFS: newTestFS(
|
|
logger,
|
|
// Use timestamps extending backwards to test time application
|
|
newDirTimes("dir", dirModtime.Add(1*time.Second), dirAtime.Add(2*time.Second),
|
|
newFileTimes("dir/file", modtime.Add(1*time.Minute), atime.Add(2*time.Minute), "file contents"),
|
|
newDirTimes("dir/dir2", dirModtime, dirAtime,
|
|
newFileTimes("dir/dir2/file2", modtime, atime, "file2 contents")),
|
|
),
|
|
),
|
|
},
|
|
{
|
|
desc: "regular file into different filename (rename)",
|
|
config: newTargetConfig("remote_file", Flags{}),
|
|
source: "file",
|
|
expectedFS: newTestFS(logger, newFile("remote_file", "file contents")),
|
|
sourceFS: newTestFS(logger, newFile("file", "file contents")),
|
|
},
|
|
{
|
|
desc: "regular file into different filename in a directory (rename)",
|
|
config: newTargetConfigWithFS("dir/remote_file", Flags{}, newTestFS(logger, newDir("dir"))),
|
|
source: "file",
|
|
expectedFS: newTestFS(logger, newDir("dir", newFile("dir/remote_file", "file contents"))),
|
|
sourceFS: newTestFS(logger, newFile("file", "file contents")),
|
|
},
|
|
{
|
|
desc: "directory into different directory name (rename)",
|
|
config: newTargetConfig("remote_dir", Flags{Recursive: true}),
|
|
source: "dir",
|
|
expectedFS: newTestFS(logger, newDir("remote_dir", newFile("remote_dir/file", "file contents"))),
|
|
sourceFS: newTestFS(logger, newDir("dir", newFile("dir/file", "file contents"))),
|
|
},
|
|
{
|
|
desc: "directory into different directory name in subdirectory (rename)",
|
|
config: newTargetConfigWithFS("dir/remote_dir", Flags{Recursive: true}, newTestFS(logger, newDir("dir"))),
|
|
source: "dir",
|
|
expectedFS: newTestFS(logger, newDir("dir/remote_dir", newFile("dir/remote_dir/file", "file contents"))),
|
|
sourceFS: newTestFS(logger, newDir("dir", newFile("dir/file", "file contents"))),
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
logger := logger.WithField("test", tt.desc)
|
|
t.Parallel()
|
|
|
|
sourceDir := t.TempDir()
|
|
source := filepath.Join(sourceDir, tt.source)
|
|
args := []string{"-v", "-f"}
|
|
if tt.config.Flags.PreserveAttrs {
|
|
args = append(args, "-p")
|
|
}
|
|
if tt.config.Flags.Recursive {
|
|
args = append(args, "-r")
|
|
}
|
|
args = append(args, source)
|
|
|
|
if tt.config.FileSystem == nil {
|
|
tt.config.FileSystem = newEmptyTestFS(logger)
|
|
}
|
|
cmd, err := CreateCommand(tt.config)
|
|
require.NoError(t, err)
|
|
|
|
writeData(t, sourceDir, tt.sourceFS)
|
|
if tt.config.Flags.PreserveAttrs {
|
|
writeFileTimes(t, sourceDir, tt.sourceFS)
|
|
}
|
|
|
|
// Send the data
|
|
err = runSCP(cmd, args...)
|
|
require.NoError(t, err)
|
|
|
|
expectedFS := tt.sourceFS
|
|
if tt.expectedFS != nil {
|
|
expectedFS = tt.expectedFS
|
|
}
|
|
if tt.config.Flags.PreserveAttrs {
|
|
validateSCPTimes(t, expectedFS, tt.config.FileSystem)
|
|
} else {
|
|
validateSCP(t, expectedFS, tt.config.FileSystem)
|
|
}
|
|
validateSCPContents(t, expectedFS, tt.config.FileSystem)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSCPFailsIfNoSource(t *testing.T) {
|
|
t.Parallel()
|
|
config := newTargetConfig("file", Flags{})
|
|
|
|
cmd, err := CreateCommand(config)
|
|
require.NoError(t, err)
|
|
|
|
sourceDir := t.TempDir()
|
|
source := filepath.Join(sourceDir, config.Flags.Target[0])
|
|
|
|
// Source is missing, expect an error.
|
|
err = runSCP(cmd, "-v", "-f", source)
|
|
require.Regexp(t, ".*No such file or directory", err)
|
|
}
|
|
|
|
// TestReceiveIntoExistingDirectory validates that the target remote directory
|
|
// is respected during copy.
|
|
//
|
|
// See https://github.com/gravitational/teleport/issues/5497
|
|
func TestReceiveIntoExistingDirectory(t *testing.T) {
|
|
logger := logrus.WithField("test", t.Name())
|
|
config := newTargetConfigWithFS("dir",
|
|
Flags{PreserveAttrs: true, Recursive: true},
|
|
newTestFS(logger, newDir("dir")),
|
|
)
|
|
sourceFS := newTestFS(
|
|
logger,
|
|
newDir("dir",
|
|
newFile("dir/file", "file contents"),
|
|
newDir("dir/dir2",
|
|
newFile("dir/dir2/file2", "file2 contents")),
|
|
),
|
|
)
|
|
expectedFS := newTestFS(
|
|
logger,
|
|
// Source is copied into an existing directory
|
|
newDir("dir/dir",
|
|
newFile("dir/dir/file", "file contents"),
|
|
newDir("dir/dir/dir2",
|
|
newFile("dir/dir/dir2/file2", "file2 contents")),
|
|
),
|
|
)
|
|
sourceDir := t.TempDir()
|
|
source := filepath.Join(sourceDir, config.Flags.Target[0])
|
|
args := append(args("-v", "-f", "-r", "-p"), source)
|
|
|
|
cmd, err := CreateCommand(config)
|
|
require.NoError(t, err)
|
|
|
|
writeData(t, sourceDir, sourceFS)
|
|
|
|
err = runSCP(cmd, args...)
|
|
require.NoError(t, err)
|
|
|
|
validateSCP(t, expectedFS, config.FileSystem)
|
|
validateSCPContents(t, expectedFS, config.FileSystem)
|
|
}
|
|
|
|
// TestReceiveIntoNonExistingDirectoryFailsWithCorrectMessage validates that copying a file into a non-existing
|
|
// directory fails with a correct error.
|
|
//
|
|
// See https://github.com/gravitational/teleport/issues/5695
|
|
func TestReceiveIntoNonExistingDirectoryFailsWithCorrectMessage(t *testing.T) {
|
|
logger := logrus.WithField("test", t.Name())
|
|
// Target configuration with no existing directory
|
|
root := t.TempDir()
|
|
config := newTargetConfigWithFS(filepath.Join(root, "dir"),
|
|
Flags{PreserveAttrs: true},
|
|
newTestFS(logger),
|
|
)
|
|
sourceFS := newTestFS(
|
|
logger,
|
|
newFile("file", "file contents"),
|
|
)
|
|
sourceDir := t.TempDir()
|
|
source := filepath.Join(sourceDir, "file")
|
|
args := append(args("-v", "-f"), source)
|
|
|
|
cmd, err := CreateCommand(config)
|
|
require.NoError(t, err)
|
|
|
|
writeData(t, sourceDir, sourceFS)
|
|
|
|
err = runSCP(cmd, args...)
|
|
require.Error(t, err)
|
|
require.Equal(t, fmt.Sprintf("no such file or directory %q", root), err.Error())
|
|
}
|
|
|
|
// TestCopyIntoNestedNonExistingDirectoriesDoesNotCreateIntermediateDirectories validates that copying a directory
|
|
// into a remote '/path/to/remote' where '/path/to' does not exist causes an error.
|
|
func TestCopyIntoNestedNonExistingDirectoriesDoesNotCreateIntermediateDirectories(t *testing.T) {
|
|
logger := logrus.WithField("test", t.Name())
|
|
|
|
config := newTargetConfig("non-existing/remote_dir", Flags{Recursive: true})
|
|
sourceFS := newTestFS(logger, newDir("dir"))
|
|
|
|
cmd, err := CreateCommand(config)
|
|
require.NoError(t, err)
|
|
|
|
sourceDir := t.TempDir()
|
|
writeData(t, sourceDir, sourceFS)
|
|
|
|
// Send the data
|
|
err = runSCP(cmd, "-v", "-f", "-r", filepath.Join(sourceDir, "dir"))
|
|
require.Error(t, err)
|
|
require.Equal(t, "mkdir non-existing/remote_dir: no such file or directory", err.Error())
|
|
}
|
|
|
|
func TestInvalidDir(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd, err := CreateCommand(Config{
|
|
User: "test-user",
|
|
Flags: Flags{
|
|
Sink: true,
|
|
// Target is always defined
|
|
Target: []string{"./dir"},
|
|
Recursive: true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
inDirName string
|
|
err string
|
|
}{
|
|
{
|
|
desc: "no directory",
|
|
inDirName: "",
|
|
err: ".*No such file or directory.*",
|
|
},
|
|
{
|
|
desc: "current directory",
|
|
inDirName: ".",
|
|
err: ".*invalid name.*",
|
|
},
|
|
{
|
|
desc: "parent directory",
|
|
inDirName: "..",
|
|
err: ".*invalid name.*",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scp, stdin, stdout, stderr := newCmd("scp", "-v", "-r", "-f", tt.inDirName)
|
|
rw := &readWriter{r: stdout, w: stdin}
|
|
|
|
doneC := make(chan struct{})
|
|
// Service stderr
|
|
go func() {
|
|
io.Copy(io.Discard, stderr)
|
|
close(doneC)
|
|
}()
|
|
|
|
err := scp.Start()
|
|
require.NoError(t, err)
|
|
|
|
err = cmd.Execute(rw)
|
|
require.Regexp(t, tt.err, err)
|
|
|
|
stdin.Close()
|
|
<-doneC
|
|
scp.Wait()
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVerifyDirectoryModeFailsWithFile makes sure that if scp was started in directory mode (the
|
|
// user attempts to copy multiple files or a directory), the target is a
|
|
// directory.
|
|
func TestVerifyDirectoryModeFailsWithFile(t *testing.T) {
|
|
// Create temporary directory with a file "target" in it.
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "target")
|
|
err := os.WriteFile(target, []byte{}, 0666)
|
|
require.NoError(t, err)
|
|
|
|
cmd, err := CreateCommand(
|
|
Config{
|
|
User: "test-user",
|
|
Flags: Flags{
|
|
Source: true,
|
|
Target: []string{target},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Run command with -d flag (directory mode). Since the target is a file,
|
|
// it should fail.
|
|
err = runSCP(cmd, "-t", "-d", target)
|
|
require.Regexp(t, ".*Not a directory", err)
|
|
}
|
|
|
|
// TestVerifyDirectoryModeIsRequiredForDirectory verifies that if a directory
|
|
// scp is attempted in non-recursive mode, the command fails as expected.
|
|
func TestVerifyDirectoryModeIsRequiredForDirectory(t *testing.T) {
|
|
// Create temporary directory with a file "target" in it.
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "target")
|
|
err := os.WriteFile(target, []byte{}, 0666)
|
|
require.NoError(t, err)
|
|
|
|
cmd, err := CreateCommand(
|
|
Config{
|
|
User: "test-user",
|
|
Flags: Flags{
|
|
Source: true,
|
|
Target: []string{dir},
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Run command in non-recursive mode. Since the source is a directory,
|
|
// it should fail.
|
|
err = runSCP(cmd, "-t", dir)
|
|
require.Regexp(t, fmt.Sprintf("%s is a directory, use -r flag to copy recursively", filepath.Base(dir)), err)
|
|
}
|
|
|
|
func TestSCPParsing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var testCases = []struct {
|
|
comment string
|
|
in string
|
|
dest Destination
|
|
err error
|
|
}{
|
|
{
|
|
comment: "full spec of the remote destination",
|
|
in: "root@remote.host:/etc/nginx.conf",
|
|
dest: Destination{Login: "root", Host: utils.NetAddr{Addr: "remote.host", AddrNetwork: "tcp"}, Path: "/etc/nginx.conf"},
|
|
},
|
|
{
|
|
comment: "spec with just the remote host",
|
|
in: "remote.host:/etc/nginx.co:nf",
|
|
dest: Destination{Host: utils.NetAddr{Addr: "remote.host", AddrNetwork: "tcp"}, Path: "/etc/nginx.co:nf"},
|
|
},
|
|
{
|
|
comment: "ipv6 remote destination address",
|
|
in: "[::1]:/etc/nginx.co:nf",
|
|
dest: Destination{Host: utils.NetAddr{Addr: "[::1]", AddrNetwork: "tcp"}, Path: "/etc/nginx.co:nf"},
|
|
},
|
|
{
|
|
comment: "full spec of the remote destination using ipv4 address",
|
|
in: "root@123.123.123.123:/var/www/html/",
|
|
dest: Destination{Login: "root", Host: utils.NetAddr{Addr: "123.123.123.123", AddrNetwork: "tcp"}, Path: "/var/www/html/"},
|
|
},
|
|
{
|
|
comment: "target location using wildcard",
|
|
in: "myusername@myremotehost.com:/home/hope/*",
|
|
dest: Destination{Login: "myusername", Host: utils.NetAddr{Addr: "myremotehost.com", AddrNetwork: "tcp"}, Path: "/home/hope/*"},
|
|
},
|
|
{
|
|
comment: "complex login",
|
|
in: "complex@example.com@remote.com:/anything.txt",
|
|
dest: Destination{Login: "complex@example.com", Host: utils.NetAddr{Addr: "remote.com", AddrNetwork: "tcp"}, Path: "/anything.txt"},
|
|
},
|
|
{
|
|
comment: "implicit user's home directory",
|
|
in: "root@remote.host:",
|
|
dest: Destination{Login: "root", Host: utils.NetAddr{Addr: "remote.host", AddrNetwork: "tcp"}, Path: "."},
|
|
},
|
|
}
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.comment, func(t *testing.T) {
|
|
resp, err := ParseSCPDestination(tt.in)
|
|
if tt.err != nil {
|
|
require.IsType(t, err, tt.err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(resp, &tt.dest))
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func runSCP(cmd Command, args ...string) error {
|
|
scp, stdin, stdout, _ := newCmd("scp", args...)
|
|
rw := &readWriter{r: stdout, w: stdin}
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
defer close(errCh)
|
|
if err := scp.Start(); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
if err := cmd.Execute(rw); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
stdin.Close()
|
|
if err := scp.Wait(); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(2 * time.Second):
|
|
return trace.BadParameter("timed out waiting for command")
|
|
case err := <-errCh:
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
// fromOS recreates the structure of the specified directory dir
|
|
// into the provided file system fs
|
|
func fromOS(t *testing.T, dir string, fs *testFS) {
|
|
err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relpath, err := filepath.Rel(dir, path)
|
|
require.NoError(t, err)
|
|
if relpath == "." {
|
|
// Skip top-level directory
|
|
return nil
|
|
}
|
|
if fi.IsDir() {
|
|
require.NoError(t, fs.MkDir(relpath, int(fi.Mode())))
|
|
require.NoError(t, fs.Chtimes(relpath, GetAtime(fi), fi.ModTime()))
|
|
return nil
|
|
}
|
|
wc, err := fs.CreateFile(relpath, uint64(fi.Size()))
|
|
require.NoError(t, err)
|
|
defer wc.Close()
|
|
require.NoError(t, fs.Chtimes(relpath, GetAtime(fi), fi.ModTime()))
|
|
f, err := os.Open(path)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
_, err = io.Copy(wc, f)
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// writeData recreates the file/directory structure in dir
|
|
// as specified with the file system fs
|
|
func writeData(t *testing.T, dir string, fs *testFS) {
|
|
for _, f := range fs.fs {
|
|
if f.IsDir() {
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, f.path), f.perms))
|
|
continue
|
|
}
|
|
rc, err := fs.OpenFile(f.path)
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
targetPath := filepath.Join(dir, f.path)
|
|
if parentDir := filepath.Dir(f.path); parentDir != "." {
|
|
fi := fs.fs[parentDir]
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(targetPath), fi.perms))
|
|
}
|
|
f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, f.perms)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
_, err = io.Copy(f, rc)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
// writeFileTimes applies access/modification times on files/directories in dir
|
|
// as specified in the file system fs.
|
|
func writeFileTimes(t *testing.T, dir string, fs *testFS) {
|
|
for _, f := range fs.fs {
|
|
require.NoError(t, os.Chtimes(filepath.Join(dir, f.path), f.atime, f.modtime))
|
|
}
|
|
}
|
|
|
|
// validateSCPContents verifies that the file contents in the specified
|
|
// file systems match in the corresponding files
|
|
func validateSCPContents(t *testing.T, expected *testFS, actual FileSystem) {
|
|
for path, fileinfo := range expected.fs {
|
|
if fileinfo.IsDir() {
|
|
continue
|
|
}
|
|
rc, err := actual.OpenFile(path)
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
bytes, err := io.ReadAll(rc)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(fileinfo.contents.String(), string(bytes)))
|
|
}
|
|
}
|
|
|
|
// validateSCP verifies that the specified pair of FileSystems match.
|
|
func validateSCP(t *testing.T, expected *testFS, actual FileSystem) {
|
|
for path, fileinfo := range expected.fs {
|
|
targetFileinfo, err := actual.GetFileInfo(path)
|
|
require.NoError(t, err, "expected %v (%v)", path, fileinfo)
|
|
if fileinfo.IsDir() {
|
|
require.True(t, targetFileinfo.IsDir())
|
|
} else {
|
|
require.True(t, targetFileinfo.GetModePerm().IsRegular())
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateSCPTimes verifies that the specified pair of FileSystems match.
|
|
// FileSystem match if their contents match incl. access/modification times
|
|
func validateSCPTimes(t *testing.T, expected *testFS, actual FileSystem) {
|
|
for path, fileinfo := range expected.fs {
|
|
targetFileinfo, err := actual.GetFileInfo(path)
|
|
require.NoError(t, err, "expected %v (%v)", path, fileinfo)
|
|
if fileinfo.IsDir() {
|
|
require.True(t, targetFileinfo.IsDir())
|
|
} else {
|
|
require.True(t, targetFileinfo.GetModePerm().IsRegular())
|
|
}
|
|
validateFileTimes(t, fileinfo, targetFileinfo)
|
|
}
|
|
}
|
|
|
|
// validateFileTimes verifies that the specified pair of FileInfos match
|
|
func validateFileTimes(t *testing.T, expected *testFileInfo, actual FileInfo) {
|
|
require.Empty(t, cmp.Diff(
|
|
expected.GetModTime().UTC().Format(time.RFC3339),
|
|
actual.GetModTime().UTC().Format(time.RFC3339),
|
|
), "validating modification times for %v", actual)
|
|
require.Empty(t, cmp.Diff(
|
|
expected.GetAccessTime().UTC().Format(time.RFC3339),
|
|
actual.GetAccessTime().UTC().Format(time.RFC3339),
|
|
), "validating access times for %v", actual)
|
|
}
|
|
|
|
type readWriter struct {
|
|
r io.Reader
|
|
w io.Writer
|
|
}
|
|
|
|
func (c *readWriter) Read(b []byte) (int, error) {
|
|
return c.r.Read(b)
|
|
}
|
|
|
|
func (c *readWriter) Write(b []byte) (int, error) {
|
|
return c.w.Write(b)
|
|
}
|
|
|
|
func newCmd(name string, args ...string) (cmd *exec.Cmd, stdin io.WriteCloser, stdout io.ReadCloser, stderr io.ReadCloser) {
|
|
cmd = exec.Command(name, args...)
|
|
|
|
var err error
|
|
stdin, err = cmd.StdinPipe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
stdout, err = cmd.StdoutPipe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
stderr, err = cmd.StderrPipe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return cmd, stdin, stdout, stderr
|
|
}
|
|
|
|
// newTestFS creates a new test FileSystem using the specified logger
|
|
// and the set of top-level files
|
|
func newTestFS(logger logrus.FieldLogger, files ...*testFileInfo) *testFS {
|
|
fs := newEmptyTestFS(logger)
|
|
addFiles(fs.fs, files...)
|
|
return fs
|
|
}
|
|
|
|
// newEmptyTestFS creates a new test FileSystem without content
|
|
func newEmptyTestFS(logger logrus.FieldLogger) *testFS {
|
|
return &testFS{
|
|
fs: make(map[string]*testFileInfo),
|
|
l: logger,
|
|
}
|
|
}
|
|
|
|
func (r *testFS) IsDir(path string) bool {
|
|
r.l.WithField("path", path).Debug("IsDir.")
|
|
if fi, exists := r.fs[path]; exists {
|
|
return fi.IsDir()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *testFS) GetFileInfo(path string) (FileInfo, error) {
|
|
r.l.WithField("path", path).Debug("GetFileInfo.")
|
|
fi, exists := r.fs[path]
|
|
if !exists {
|
|
return nil, newErrMissingFile(path)
|
|
}
|
|
return fi, nil
|
|
}
|
|
|
|
func (r *testFS) MkDir(path string, mode int) error {
|
|
r.l.WithFields(logrus.Fields{"path": path, "mode": mode}).Debug("MkDir.")
|
|
_, exists := r.fs[path]
|
|
if exists {
|
|
return trace.AlreadyExists("directory %v already exists", path)
|
|
}
|
|
r.fs[path] = &testFileInfo{
|
|
path: path,
|
|
dir: true,
|
|
perms: os.FileMode(mode) | os.ModeDir,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *testFS) OpenFile(path string) (io.ReadCloser, error) {
|
|
r.l.WithField("path", path).Debug("OpenFile.")
|
|
fi, exists := r.fs[path]
|
|
if !exists {
|
|
return nil, newErrMissingFile(path)
|
|
}
|
|
rc := nopReadCloser{Reader: bytes.NewReader(fi.contents.Bytes())}
|
|
return rc, nil
|
|
}
|
|
|
|
func (r *testFS) CreateFile(path string, length uint64) (io.WriteCloser, error) {
|
|
r.l.WithFields(logrus.Fields{"path": path, "len": length}).Debug("CreateFile.")
|
|
baseDir := filepath.Dir(path)
|
|
if _, exists := r.fs[baseDir]; baseDir != "." && !exists {
|
|
return nil, newErrMissingFile(baseDir)
|
|
}
|
|
fi := &testFileInfo{
|
|
path: path,
|
|
size: int64(length),
|
|
perms: 0666,
|
|
contents: new(bytes.Buffer),
|
|
}
|
|
r.fs[path] = fi
|
|
wc := utils.NopWriteCloser(fi.contents)
|
|
return wc, nil
|
|
}
|
|
|
|
func (r *testFS) Chmod(path string, mode int) error {
|
|
r.l.WithFields(logrus.Fields{"path": path, "mode": mode}).Debug("Chmod.")
|
|
fi, exists := r.fs[path]
|
|
if !exists {
|
|
return newErrMissingFile(path)
|
|
}
|
|
fi.perms = os.FileMode(mode)
|
|
return nil
|
|
}
|
|
|
|
func (r *testFS) Chtimes(path string, atime, mtime time.Time) error {
|
|
r.l.WithFields(logrus.Fields{
|
|
"path": path,
|
|
"atime": atime,
|
|
"mtime": mtime,
|
|
}).Debug("Chtimes.")
|
|
fi, exists := r.fs[path]
|
|
if !exists {
|
|
return newErrMissingFile(path)
|
|
}
|
|
fi.modtime = mtime
|
|
fi.atime = atime
|
|
return nil
|
|
}
|
|
|
|
// testFS implements a fake FileSystem
|
|
type testFS struct {
|
|
l logrus.FieldLogger
|
|
fs map[string]*testFileInfo
|
|
}
|
|
|
|
type testFileInfo struct {
|
|
dir bool
|
|
perms os.FileMode
|
|
path string
|
|
modtime time.Time
|
|
atime time.Time
|
|
ents []*testFileInfo
|
|
size int64
|
|
contents *bytes.Buffer
|
|
}
|
|
|
|
func (r *testFileInfo) String() string {
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "fileinfo(path=%s,perms=%d,size=%d", r.path, r.perms, r.size)
|
|
if r.dir {
|
|
fmt.Fprintf(&buf, ",dir(ents=%d)", len(r.ents))
|
|
}
|
|
fmt.Fprint(&buf, ")")
|
|
return buf.String()
|
|
}
|
|
func (r *testFileInfo) IsDir() bool { return r.dir }
|
|
func (r *testFileInfo) ReadDir() (fis []FileInfo, err error) {
|
|
fis = make([]FileInfo, 0, len(r.ents))
|
|
for _, e := range r.ents {
|
|
fis = append(fis, e)
|
|
}
|
|
return fis, nil
|
|
}
|
|
func (r *testFileInfo) GetName() string { return filepath.Base(r.path) }
|
|
func (r *testFileInfo) GetPath() string { return r.path }
|
|
func (r *testFileInfo) GetModePerm() os.FileMode { return r.perms }
|
|
func (r *testFileInfo) GetSize() int64 { return r.size }
|
|
func (r *testFileInfo) GetModTime() time.Time { return r.modtime }
|
|
func (r *testFileInfo) GetAccessTime() time.Time { return r.atime }
|
|
|
|
func (r nopReadCloser) Close() error { return nil }
|
|
|
|
type nopReadCloser struct {
|
|
io.Reader
|
|
}
|
|
|
|
func newErrMissingFile(path string) error {
|
|
return fmt.Errorf("no such file or directory %q", path)
|
|
}
|
|
|
|
func newSourceConfig(path string, flags Flags) Config {
|
|
flags.Source = true
|
|
flags.Target = []string{path}
|
|
return Config{
|
|
User: "test-user",
|
|
Flags: flags,
|
|
}
|
|
}
|
|
|
|
func newTargetConfigWithFS(path string, flags Flags, fs *testFS) Config {
|
|
config := newTargetConfig(path, flags)
|
|
config.FileSystem = fs
|
|
return config
|
|
}
|
|
|
|
func newTargetConfig(path string, flags Flags) Config {
|
|
flags.Sink = true
|
|
flags.Target = []string{path}
|
|
return Config{
|
|
User: "test-user",
|
|
Flags: flags,
|
|
}
|
|
}
|
|
|
|
func newDir(name string, ents ...*testFileInfo) *testFileInfo {
|
|
return &testFileInfo{
|
|
path: name,
|
|
ents: ents,
|
|
dir: true,
|
|
perms: 0755,
|
|
}
|
|
}
|
|
|
|
func newFile(name string, contents string) *testFileInfo {
|
|
return &testFileInfo{
|
|
path: name,
|
|
perms: 0666,
|
|
size: int64(len(contents)),
|
|
contents: bytes.NewBufferString(contents),
|
|
}
|
|
}
|
|
|
|
func newDirTimes(name string, modtime, atime time.Time, ents ...*testFileInfo) *testFileInfo {
|
|
return &testFileInfo{
|
|
path: name,
|
|
ents: ents,
|
|
modtime: modtime,
|
|
atime: atime,
|
|
dir: true,
|
|
perms: 0755,
|
|
}
|
|
}
|
|
|
|
func newFileTimes(name string, modtime, atime time.Time, contents string) *testFileInfo {
|
|
return &testFileInfo{
|
|
path: name,
|
|
modtime: modtime,
|
|
atime: atime,
|
|
perms: 0666,
|
|
size: int64(len(contents)),
|
|
contents: bytes.NewBufferString(contents),
|
|
}
|
|
}
|
|
|
|
func addFiles(fs map[string]*testFileInfo, ents ...*testFileInfo) {
|
|
for _, f := range ents {
|
|
fs[f.path] = f
|
|
if f.IsDir() {
|
|
addFiles(fs, f.ents...)
|
|
}
|
|
}
|
|
}
|
|
|
|
var testNow = time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC)
|
|
|
|
func args(params ...string) []string {
|
|
return params
|
|
}
|