The FTP control channel is line oriented and the ftp library writes command arguments (paths) straight onto it without escaping, so a filename containing CR/LF can inject an independent FTP command. This fix makes sure CR/LF are therefore always encoded to safe symbols regardless of the configured encoding, which is what the default encoding already does.
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package ftp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
"github.com/rclone/rclone/fs/object"
|
|
"github.com/rclone/rclone/fstest"
|
|
"github.com/rclone/rclone/fstest/fstests"
|
|
"github.com/rclone/rclone/lib/encoder"
|
|
"github.com/rclone/rclone/lib/readers"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// A filename containing raw CR/LF must never survive encoding into an FTP
|
|
// command argument, whatever encoding the user configured, otherwise it could
|
|
// inject an independent FTP command onto the line-oriented control channel.
|
|
func TestCommandEncodingNeutralisesCRLF(t *testing.T) {
|
|
// Internal Standard-encoded name as produced by e.g. a Unix local
|
|
// source, carrying raw CR/LF.
|
|
for _, std := range []string{
|
|
"victim\r\nDELE other-secret\r\nNOOP",
|
|
"victim␍␊DELE other-secret␍␊NOOP",
|
|
} {
|
|
for _, configured := range []encoder.MultiEncoder{
|
|
encoder.EncodeSlash, // "Slash" - omits Ctl and CrLf
|
|
encoder.EncodeZero, // "None"
|
|
encoder.Display | encoder.EncodeRightSpace, // the FTP default
|
|
} {
|
|
got := commandEncoding(configured).FromStandardPath(std)
|
|
assert.NotContains(t, got, "\r", "encoding %v leaked raw CR: %q", configured, got)
|
|
assert.NotContains(t, got, "\n", "encoding %v leaked raw LF: %q", configured, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
type settings map[string]any
|
|
|
|
func deriveFs(ctx context.Context, t *testing.T, f fs.Fs, opts settings) fs.Fs {
|
|
fsName := strings.Split(f.Name(), "{")[0] // strip off hash
|
|
configMap := configmap.Simple{}
|
|
for key, val := range opts {
|
|
configMap[key] = fmt.Sprintf("%v", val)
|
|
}
|
|
remote := fmt.Sprintf("%s,%s:%s", fsName, configMap.String(), f.Root())
|
|
fixFs, err := fs.NewFs(ctx, remote)
|
|
require.NoError(t, err)
|
|
return fixFs
|
|
}
|
|
|
|
// test that big file uploads do not cause network i/o timeout
|
|
func (f *Fs) testUploadTimeout(t *testing.T) {
|
|
const (
|
|
fileSize = 100000000 // 100 MiB
|
|
idleTimeout = 1 * time.Second // small because test server is local
|
|
maxTime = 10 * time.Second // prevent test hangup
|
|
)
|
|
|
|
if testing.Short() {
|
|
t.Skip("not running with -short")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ci := fs.GetConfig(ctx)
|
|
saveLowLevelRetries := ci.LowLevelRetries
|
|
saveTimeout := ci.Timeout
|
|
defer func() {
|
|
ci.LowLevelRetries = saveLowLevelRetries
|
|
ci.Timeout = saveTimeout
|
|
}()
|
|
ci.LowLevelRetries = 1
|
|
|
|
upload := func(concurrency int, shutTimeout time.Duration) (obj fs.Object, err error) {
|
|
// Use the saved (long) timeout while NewFs dials the FTP control
|
|
// connection and reads the welcome banner. On slow CI servers this
|
|
// can take longer than idleTimeout, which would cause a spurious
|
|
// i/o timeout before the test can run. The connection's idle
|
|
// deadline is captured at dial time, so the data connection
|
|
// dialled later inside Put will still get idleTimeout - which is
|
|
// what the test is actually trying to exercise.
|
|
ci.Timeout = saveTimeout
|
|
fixFs := deriveFs(ctx, t, f, settings{
|
|
"concurrency": concurrency,
|
|
"shut_timeout": shutTimeout,
|
|
})
|
|
ci.Timeout = fs.Duration(idleTimeout)
|
|
|
|
// Make test object
|
|
fileTime := fstest.Time("2020-03-08T09:30:00.000000000Z")
|
|
meta := object.NewStaticObjectInfo("upload-timeout.test", fileTime, int64(fileSize), true, nil, nil)
|
|
data := readers.NewPatternReader(int64(fileSize))
|
|
|
|
// Run upload and ensure maximum time
|
|
done := make(chan bool)
|
|
deadline := time.After(maxTime)
|
|
go func() {
|
|
obj, err = fixFs.Put(ctx, data, meta)
|
|
done <- true
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-deadline:
|
|
t.Fatalf("Upload got stuck for %v !", maxTime)
|
|
}
|
|
|
|
return obj, err
|
|
}
|
|
|
|
// non-zero shut_timeout should fix i/o errors
|
|
obj, err := upload(f.opt.Concurrency, time.Second)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, obj)
|
|
if obj != nil {
|
|
_ = obj.Remove(ctx)
|
|
}
|
|
}
|
|
|
|
// rclone must support precise time with ProFtpd and PureFtpd out of the box.
|
|
// The VsFtpd server does not support the MFMT command to set file time like
|
|
// other servers but by default supports the MDTM command in the non-standard
|
|
// two-argument form for the same purpose.
|
|
// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
|
|
func (f *Fs) testTimePrecision(t *testing.T) {
|
|
name := f.Name()
|
|
if pos := strings.Index(name, "{"); pos != -1 {
|
|
name = name[:pos]
|
|
}
|
|
switch name {
|
|
case "TestFTPProftpd", "TestFTPPureftpd", "TestFTPVsftpd":
|
|
assert.LessOrEqual(t, f.Precision(), time.Second)
|
|
}
|
|
}
|
|
|
|
// InternalTest dispatches all internal tests
|
|
func (f *Fs) InternalTest(t *testing.T) {
|
|
t.Run("UploadTimeout", f.testUploadTimeout)
|
|
t.Run("TimePrecision", f.testTimePrecision)
|
|
}
|
|
|
|
var _ fstests.InternalTester = (*Fs)(nil)
|