From f41ce6619fe0ad4674eb76dff88cb0cca340a723 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 20 Aug 2020 13:54:36 +0200 Subject: [PATCH] sftpd: add SSH_FXP_FSETSTAT support This change will fix file editing from sshfs, we need this patch https://github.com/pkg/sftp/pull/373 for pkg/sftp to support this feature --- common/common.go | 6 ++++ common/connection.go | 75 +++++++++++++++++++++++++++++++-------- common/connection_test.go | 24 +++++++++++++ common/transfer.go | 16 +++++++++ common/transfer_test.go | 47 ++++++++++++++++++++++++ docs/logs.md | 3 +- go.mod | 16 ++++----- go.sum | 34 +++++++++--------- logger/logger.go | 4 ++- sftpd/handler.go | 4 +++ sftpd/sftpd_test.go | 16 ++++++++- sftpd/ssh_cmd.go | 2 +- vfs/gcsfs.go | 7 ++++ vfs/osfs.go | 5 +++ vfs/s3fs.go | 7 ++++ vfs/vfs.go | 1 + 16 files changed, 222 insertions(+), 45 deletions(-) diff --git a/common/common.go b/common/common.go index de36ac44..436001b4 100644 --- a/common/common.go +++ b/common/common.go @@ -37,6 +37,7 @@ const ( chownLogSender = "Chown" chmodLogSender = "Chmod" chtimesLogSender = "Chtimes" + truncateLogSender = "Truncate" operationDownload = "download" operationUpload = "upload" operationDelete = "delete" @@ -52,6 +53,7 @@ const ( StatAttrUIDGID = 1 StatAttrPerms = 2 StatAttrTimes = 4 + StatAttrSize = 8 ) // Transfer types @@ -85,6 +87,8 @@ var ( ErrQuotaExceeded = errors.New("denying write due to space limit") ErrSkipPermissionsCheck = errors.New("permission check skipped") ErrConnectionDenied = errors.New("You are not allowed to connect") + errNoTransfer = errors.New("requested transfer not found") + errTransferMismatch = errors.New("transfer mismatch") ) var ( @@ -141,6 +145,7 @@ type ActiveTransfer interface { GetVirtualPath() string GetStartTime() time.Time SignalClose() + Truncate(fsPath string, size int64) error } // ActiveConnection defines the interface for the current active connections @@ -168,6 +173,7 @@ type StatAttributes struct { UID int GID int Flags int + Size int64 } // ConnectionTransfer defines the trasfer details to expose diff --git a/common/connection.go b/common/connection.go index 3f8c2369..9c6f110a 100644 --- a/common/connection.go +++ b/common/connection.go @@ -172,6 +172,20 @@ func (c *BaseConnection) SignalTransfersAbort() error { return nil } +func (c *BaseConnection) truncateOpenHandle(fsPath string, size int64) error { + c.RLock() + defer c.RUnlock() + + for _, t := range c.activeTransfers { + err := t.Truncate(fsPath, size) + if err != errTransferMismatch { + return err + } + } + + return errNoTransfer +} + // ListDir reads the directory named by fsPath and returns a list of directory entries func (c *BaseConnection) ListDir(fsPath, virtualPath string) ([]os.FileInfo, error) { if !c.User.HasPerm(dataprovider.PermListItems, virtualPath) { @@ -200,7 +214,7 @@ func (c *BaseConnection) CreateDir(fsPath, virtualPath string) error { } vfs.SetPathPermissions(c.Fs, fsPath, c.User.GetUID(), c.User.GetGID()) - logger.CommandLog(mkdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "") + logger.CommandLog(mkdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1) return nil } @@ -233,7 +247,7 @@ func (c *BaseConnection) RemoveFile(fsPath, virtualPath string, info os.FileInfo } } - logger.CommandLog(removeLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "") + logger.CommandLog(removeLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1) if info.Mode()&os.ModeSymlink != os.ModeSymlink { vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath)) if err == nil { @@ -302,7 +316,7 @@ func (c *BaseConnection) RemoveDir(fsPath, virtualPath string) error { return c.GetFsError(err) } - logger.CommandLog(rmdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "") + logger.CommandLog(rmdirLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1) return nil } @@ -363,7 +377,7 @@ func (c *BaseConnection) Rename(fsSourcePath, fsTargetPath, virtualSourcePath, v c.updateQuotaAfterRename(virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize) //nolint:errcheck } logger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, - "", "", "") + "", "", "", -1) action := newActionNotification(&c.User, operationRename, fsSourcePath, fsTargetPath, "", c.protocol, 0, nil) // the returned error is used in test cases only, we already log the error inside action.execute go action.execute() //nolint:errcheck @@ -400,21 +414,27 @@ func (c *BaseConnection) CreateSymlink(fsSourcePath, fsTargetPath, virtualSource c.Log(logger.LevelWarn, "failed to create symlink %#v -> %#v: %+v", fsSourcePath, fsTargetPath, err) return c.GetFsError(err) } - logger.CommandLog(symlinkLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "") + logger.CommandLog(symlinkLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1) return nil } +func (c *BaseConnection) getPathForSetStatPerms(fsPath, virtualPath string) string { + pathForPerms := virtualPath + if fi, err := c.Fs.Lstat(fsPath); err == nil { + if fi.IsDir() { + pathForPerms = path.Dir(virtualPath) + } + } + return pathForPerms +} + // SetStat set StatAttributes for the specified fsPath func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAttributes) error { if Config.SetstatMode == 1 { return nil } - pathForPerms := virtualPath - if fi, err := c.Fs.Lstat(fsPath); err == nil { - if fi.IsDir() { - pathForPerms = path.Dir(virtualPath) - } - } + pathForPerms := c.getPathForSetStatPerms(fsPath, virtualPath) + if attributes.Flags&StatAttrPerms != 0 { if !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) { return c.GetPermissionDeniedError() @@ -424,7 +444,7 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt return c.GetFsError(err) } logger.CommandLog(chmodLogSender, fsPath, "", c.User.Username, attributes.Mode.String(), c.ID, c.protocol, - -1, -1, "", "", "") + -1, -1, "", "", "", -1) } if attributes.Flags&StatAttrUIDGID != 0 { @@ -437,12 +457,12 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt return c.GetFsError(err) } logger.CommandLog(chownLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, attributes.UID, attributes.GID, - "", "", "") + "", "", "", -1) } if attributes.Flags&StatAttrTimes != 0 { if !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) { - return sftp.ErrSSHFxPermissionDenied + return c.GetPermissionDeniedError() } if err := c.Fs.Chtimes(fsPath, attributes.Atime, attributes.Mtime); err != nil { @@ -453,12 +473,37 @@ func (c *BaseConnection) SetStat(fsPath, virtualPath string, attributes *StatAtt accessTimeString := attributes.Atime.Format(chtimesFormat) modificationTimeString := attributes.Mtime.Format(chtimesFormat) logger.CommandLog(chtimesLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, - accessTimeString, modificationTimeString, "") + accessTimeString, modificationTimeString, "", -1) + } + + if attributes.Flags&StatAttrSize != 0 { + if !c.User.HasPerm(dataprovider.PermOverwrite, pathForPerms) { + return c.GetPermissionDeniedError() + } + + if err := c.truncateFile(fsPath, attributes.Size); err != nil { + c.Log(logger.LevelWarn, "failed to truncate path %#v, size: %v, err: %+v", fsPath, attributes.Size, err) + return c.GetFsError(err) + } + logger.CommandLog(truncateLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", attributes.Size) } return nil } +func (c *BaseConnection) truncateFile(fsPath string, size int64) error { + // check first if we have an open transfer for the given path and try to truncate the file already opened + // if we found no transfer we truncate by path. + // pkg/sftp should expose an optional interface and call truncate directly on the opened handle ... + // If we try to truncate by path an already opened file we get an error on Windows + err := c.truncateOpenHandle(fsPath, size) + if err == errNoTransfer { + c.Log(logger.LevelDebug, "file path %#v not found in active transfers, execute trucate by path", fsPath) + err = c.Fs.Truncate(fsPath, size) + } + return err +} + func (c *BaseConnection) checkRecursiveRenameDirPermissions(sourcePath, targetPath string) error { dstPerms := []string{ dataprovider.PermCreateDirs, diff --git a/common/connection_test.go b/common/connection_test.go index f76bbd7e..6152d2d6 100644 --- a/common/connection_test.go +++ b/common/connection_test.go @@ -529,6 +529,30 @@ func TestSetStat(t *testing.T) { Flags: StatAttrTimes, }) assert.Error(t, err) + // truncate + err = c.SetStat(filepath.Join(user.GetHomeDir(), "missing"), "/missing", &StatAttributes{ + Size: 1, + Flags: StatAttrSize, + }) + assert.Error(t, err) + err = c.SetStat(filepath.Join(dir3, "afile.txt"), "/dir3/afile.txt", &StatAttributes{ + Size: 1, + Flags: StatAttrSize, + }) + assert.Error(t, err) + + filePath := filepath.Join(user.GetHomeDir(), "afile.txt") + err = ioutil.WriteFile(filePath, []byte("hello"), os.ModePerm) + assert.NoError(t, err) + err = c.SetStat(filePath, "/afile.txt", &StatAttributes{ + Flags: StatAttrSize, + Size: 1, + }) + assert.NoError(t, err) + fi, err := os.Stat(filePath) + if assert.NoError(t, err) { + assert.Equal(t, int64(1), fi.Size()) + } err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) diff --git a/common/transfer.go b/common/transfer.go index 4b1dd222..4a978fc3 100644 --- a/common/transfer.go +++ b/common/transfer.go @@ -108,6 +108,22 @@ func (t *BaseTransfer) SetCancelFn(cancelFn func()) { t.cancelFn = cancelFn } +// Truncate changes the size of the opened file. +// Supported for local fs only +func (t *BaseTransfer) Truncate(fsPath string, size int64) error { + if fsPath == t.GetFsPath() { + if t.File != nil { + return t.File.Truncate(size) + } + if size == 0 { + // for cloud providers the file is always truncated to zero, we don't support append/resume for uploads + return nil + } + return ErrOpUnsupported + } + return errTransferMismatch +} + // TransferError is called if there is an unexpected error. // For example network or client issues func (t *BaseTransfer) TransferError(err error) { diff --git a/common/transfer_test.go b/common/transfer_test.go index 9dfc506c..5c690460 100644 --- a/common/transfer_test.go +++ b/common/transfer_test.go @@ -81,6 +81,53 @@ func TestTransferThrottling(t *testing.T) { assert.NoError(t, err) } +func TestTruncate(t *testing.T) { + testFile := filepath.Join(os.TempDir(), "transfer_test_file") + fs := vfs.NewOsFs("123", os.TempDir(), nil) + u := dataprovider.User{ + Username: "user", + HomeDir: os.TempDir(), + } + u.Permissions = make(map[string][]string) + u.Permissions["/"] = []string{dataprovider.PermAny} + file, err := os.Create(testFile) + if !assert.NoError(t, err) { + assert.FailNow(t, "unable to open test file") + } + _, err = file.Write([]byte("hello")) + assert.NoError(t, err) + conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, u, fs) + transfer := NewBaseTransfer(file, conn, nil, testFile, "/transfer_test_file", TransferUpload, 0, 0, true) + + err = conn.SetStat(testFile, "/transfer_test_file", &StatAttributes{ + Size: 2, + Flags: StatAttrSize, + }) + assert.NoError(t, err) + err = transfer.Close() + assert.NoError(t, err) + fi, err := os.Stat(testFile) + if assert.NoError(t, err) { + assert.Equal(t, int64(2), fi.Size()) + } + + transfer = NewBaseTransfer(nil, conn, nil, testFile, "", TransferUpload, 0, 0, true) + err = transfer.Truncate("mismatch", 0) + assert.EqualError(t, err, errTransferMismatch.Error()) + err = transfer.Truncate(testFile, 0) + assert.NoError(t, err) + err = transfer.Truncate(testFile, 1) + assert.EqualError(t, err, ErrOpUnsupported.Error()) + + err = transfer.Close() + assert.NoError(t, err) + + err = os.Remove(testFile) + assert.NoError(t, err) + + assert.Len(t, conn.GetTransfers(), 0) +} + func TestTransferErrors(t *testing.T) { isCancelled := false cancelFn := func() { diff --git a/docs/logs.md b/docs/logs.md index 40abb968..0526ce34 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -20,7 +20,7 @@ The logs can be divided into the following categories: - `connection_id` string. Unique connection identifier - `protocol` string. `SFTP` or `SCP` - **"command logs"**, SFTP/SCP command logs: - - `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`, `Chtimes`, `SSHCommand` + - `sender` string. `Rename`, `Rmdir`, `Mkdir`, `Symlink`, `Remove`, `Chmod`, `Chown`, `Chtimes`, `Truncate`, `SSHCommand` - `level` string - `username`, string - `file_path` string @@ -30,6 +30,7 @@ The logs can be divided into the following categories: - `gid` integer. Valid for sender `Chown` otherwise -1 - `access_time` datetime as YYYY-MM-DDTHH:MM:SS. Valid for sender `Chtimes` otherwise empty - `modification_time` datetime as YYYY-MM-DDTHH:MM:SS. Valid for sender `Chtimes` otherwise empty + - `size` int64. Valid for sender `Truncate` otherwise -1 - `ssh_command`, string. Valid for sender `SSHCommand` otherwise empty - `connection_id` string. Unique connection identifier - `protocol` string. `SFTP`, `SCP` or `SSH` diff --git a/go.mod b/go.mod index f44bdb99..e1a8ed85 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/drakkan/sftpgo go 1.13 require ( - cloud.google.com/go v0.63.0 // indirect + cloud.google.com/go v0.64.0 // indirect cloud.google.com/go/storage v1.10.0 github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b - github.com/aws/aws-sdk-go v1.34.5 + github.com/aws/aws-sdk-go v1.34.8 github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d github.com/fclairamb/ftpserverlib v0.8.1-0.20200729230026-7f0ab9d81bb6 github.com/fsnotify/fsnotify v1.4.9 // indirect @@ -17,7 +17,7 @@ require ( github.com/grandcat/zeroconf v1.0.0 github.com/jlaffaye/ftp v0.0.0-20200720194710-13949d38913e github.com/lib/pq v1.8.0 - github.com/mattn/go-sqlite3 v1.14.0 + github.com/mattn/go-sqlite3 v1.14.1 github.com/miekg/dns v1.1.31 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/nathanaelle/password/v2 v2.0.1 @@ -26,7 +26,7 @@ require ( github.com/pires/go-proxyproto v0.1.3 github.com/pkg/sftp v1.11.1-0.20200819110714-3ee8d0ba91c0 github.com/prometheus/client_golang v1.7.1 - github.com/prometheus/common v0.12.0 // indirect + github.com/prometheus/common v0.13.0 // indirect github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b github.com/rs/xid v1.2.1 github.com/rs/zerolog v1.19.0 @@ -41,17 +41,17 @@ require ( go.etcd.io/bbolt v1.3.5 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc - golang.org/x/sys v0.0.0-20200819091447-39769834ee22 - golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d // indirect + golang.org/x/sys v0.0.0-20200819171115-d785dc25833f + golang.org/x/tools v0.0.0-20200820010801-b793a1359eac // indirect google.golang.org/api v0.30.0 - google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70 // indirect - gopkg.in/ini.v1 v1.58.0 // indirect + gopkg.in/ini.v1 v1.60.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) replace ( github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20200814103339-511fcfd63dfe github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20200730125632-b21eac28818c + github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20200820090459-de8eb908f763 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20200731130417-7674a892f9b1 golang.org/x/net => github.com/drakkan/net v0.0.0-20200807161257-daa5cda5ae27 ) diff --git a/go.sum b/go.sum index b274d853..78d9f2fe 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.63.0 h1:A+DfAZQ/eWca7gvu42CS6FNSDX4R8cghF+XfWLn4R6g= -cloud.google.com/go v0.63.0/go.mod h1:GmezbQc7T2snqkEXWfZ0sy0VfkB/ivI2DdtJL2DEmlg= +cloud.google.com/go v0.64.0 h1:xVP3LPvMjGT4J0a55y02Gw5y/dkY/rxGz58sfK1jqIo= +cloud.google.com/go v0.64.0/go.mod h1:xfORb36jGvE+6EexW71nMEtL025s3x6xvuYUKM4JLv4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -60,8 +60,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.34.5 h1:FwubVVX9u+kW9qDCjVzyWOdsL+W5wPq683wMk2R2GXk= -github.com/aws/aws-sdk-go v1.34.5/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.34.8 h1:GDfVeXG8XQDbpOeAj7415F8qCQZwvY/k/fj+HBqUnBA= +github.com/aws/aws-sdk-go v1.34.8/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -111,6 +111,8 @@ github.com/drakkan/ftpserverlib v0.0.0-20200814103339-511fcfd63dfe h1:yiliFcCxu5 github.com/drakkan/ftpserverlib v0.0.0-20200814103339-511fcfd63dfe/go.mod h1:ShLpSOXbtoMDYxTb5eRs9wDBfkQ7VINYghclB4P2z4E= github.com/drakkan/net v0.0.0-20200807161257-daa5cda5ae27 h1:hh14GxmE3PMKL+4nvMmX7O8CUtbD/52IKDjbMTYX7IY= github.com/drakkan/net v0.0.0-20200807161257-daa5cda5ae27/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +github.com/drakkan/sftp v0.0.0-20200820090459-de8eb908f763 h1:2s1IvldI3h8S4FAF7K9uAB05/L0k8TlYF60GIEc2LUY= +github.com/drakkan/sftp v0.0.0-20200820090459-de8eb908f763/go.mod h1:i24A96cQ6ZvWut9G/Uv3LvC4u3VebGsBR5JFvPyChLc= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= @@ -291,8 +293,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/mattn/go-sqlite3 v1.14.1 h1:AHx9Ra40wIzl+GelgX2X6AWxmT5tfxhI1PL0523HcSw= +github.com/mattn/go-sqlite3 v1.14.1/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -364,9 +366,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.11.1-0.20200819110714-3ee8d0ba91c0 h1:3LRfXAQrcWKQ0LQZ9wjp9wgzYRsxFKVjP5zZIEv9NFY= -github.com/pkg/sftp v1.11.1-0.20200819110714-3ee8d0ba91c0/go.mod h1:i24A96cQ6ZvWut9G/Uv3LvC4u3VebGsBR5JFvPyChLc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -390,8 +389,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.12.0 h1:mj4ewtVukAfkS37JU7IXPJPr7zwLEjwgWO6nZo8ROvk= -github.com/prometheus/common v0.12.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.13.0 h1:vJlpe9wPgDRM1Z+7Wj3zUUjY1nr6/1jNKyl7llliccg= +github.com/prometheus/common v0.13.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -578,8 +577,8 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200819091447-39769834ee22 h1:YUxhQGxYV280Da2a0XZiHblyJZN6NuXS1f4dahsm0SM= -golang.org/x/sys v0.0.0-20200819091447-39769834ee22/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200819171115-d785dc25833f h1:KJuwZVtZBVzDmEDtB2zro9CXkD9O0dpCv4o2LHbQIAw= +golang.org/x/sys v0.0.0-20200819171115-d785dc25833f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -638,8 +637,8 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200806022845-90696ccdc692/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -698,7 +697,6 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70 h1:wboULUXGF3c5qdUnKp+6gLAccE6PRpa/czkYvQ4UXv8= google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -742,8 +740,8 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.58.0 h1:VdDvTzv/005R8vEFyQ56bpEnOKTNPbpJhL0VCohxlQw= -gopkg.in/ini.v1 v1.58.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60= +gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/logger/logger.go b/logger/logger.go index 7c705f4a..fdf2f02f 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -161,7 +161,8 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user } // CommandLog logs an SFTP/SCP/SSH command -func CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int, atime, mtime, sshCommand string) { +func CommandLog(command, path, target, user, fileMode, connectionID, protocol string, uid, gid int, atime, mtime, + sshCommand string, size int64) { logger.Info(). Timestamp(). Str("sender", command). @@ -173,6 +174,7 @@ func CommandLog(command, path, target, user, fileMode, connectionID, protocol st Int("gid", gid). Str("access_time", atime). Str("modification_time", atime). + Int64("size", size). Str("ssh_command", sshCommand). Str("connection_id", connectionID). Str("protocol", protocol). diff --git a/sftpd/handler.go b/sftpd/handler.go index c601a846..515b5349 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -232,6 +232,10 @@ func (c *Connection) handleSFTPSetstat(filePath string, request *sftp.Request) e attrs.Atime = time.Unix(int64(request.Attributes().Atime), 0) attrs.Mtime = time.Unix(int64(request.Attributes().Mtime), 0) } + if request.AttrFlags().Size { + attrs.Flags |= common.StatAttrSize + attrs.Size = int64(request.Attributes().Size) + } return c.SetStat(filePath, request.Filepath, &attrs) } diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index bfe175c9..96e86f54 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -657,8 +657,22 @@ func TestStat(t *testing.T) { assert.Equal(t, newPerm, newFi.Mode().Perm()) _, err = client.ReadLink(testFileName) assert.Error(t, err, "readlink is not supported and must fail") - err = client.Truncate(testFileName, 0) + newPerm = os.FileMode(0666) + err = client.Chmod(testFileName, newPerm) assert.NoError(t, err) + err = client.Truncate(testFileName, 100) + assert.NoError(t, err) + fi, err := client.Stat(testFileName) + if assert.NoError(t, err) { + assert.Equal(t, int64(100), fi.Size()) + } + f, err := client.OpenFile(testFileName, os.O_WRONLY) + if assert.NoError(t, err) { + err = f.Truncate(5) + assert.NoError(t, err) + err = f.Close() + assert.NoError(t, err) + } err = os.Remove(testFilePath) assert.NoError(t, err) } diff --git a/sftpd/ssh_cmd.go b/sftpd/ssh_cmd.go index eed88b9b..adf30135 100644 --- a/sftpd/ssh_cmd.go +++ b/sftpd/ssh_cmd.go @@ -704,7 +704,7 @@ func (c *sshCommand) sendExitStatus(err error) { c.command, c.args, c.connection.User.Username, err) } else { logger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, "", c.connection.ID, - common.ProtocolSSH, -1, -1, "", "", c.connection.command) + common.ProtocolSSH, -1, -1, "", "", c.connection.command, -1) } exitStatus := sshSubsystemExitStatus{ Status: status, diff --git a/vfs/gcsfs.go b/vfs/gcsfs.go index aa8ce5e5..990979c4 100644 --- a/vfs/gcsfs.go +++ b/vfs/gcsfs.go @@ -298,6 +298,13 @@ func (GCSFs) Chtimes(name string, atime, mtime time.Time) error { return errors.New("403 chtimes is not supported") } +// Truncate changes the size of the named file. +// Truncate by path is not supported, while truncating an opened +// file is handled inside base transfer +func (GCSFs) Truncate(name string, size int64) error { + return errors.New("403 truncate is not supported") +} + // ReadDir reads the directory named by dirname and returns // a list of directory entries. func (fs GCSFs) ReadDir(dirname string) ([]os.FileInfo, error) { diff --git a/vfs/osfs.go b/vfs/osfs.go index eb991647..a58f70ab 100644 --- a/vfs/osfs.go +++ b/vfs/osfs.go @@ -131,6 +131,11 @@ func (OsFs) Chtimes(name string, atime, mtime time.Time) error { return os.Chtimes(name, atime, mtime) } +// Truncate changes the size of the named file +func (OsFs) Truncate(name string, size int64) error { + return os.Truncate(name, size) +} + // ReadDir reads the directory named by dirname and returns // a list of directory entries. func (OsFs) ReadDir(dirname string) ([]os.FileInfo, error) { diff --git a/vfs/s3fs.go b/vfs/s3fs.go index e7378883..44a12304 100644 --- a/vfs/s3fs.go +++ b/vfs/s3fs.go @@ -330,6 +330,13 @@ func (S3Fs) Chtimes(name string, atime, mtime time.Time) error { return errors.New("403 chtimes is not supported") } +// Truncate changes the size of the named file. +// Truncate by path is not supported, while truncating an opened +// file is handled inside base transfer +func (S3Fs) Truncate(name string, size int64) error { + return errors.New("403 truncate is not supported") +} + // ReadDir reads the directory named by dirname and returns // a list of directory entries. func (fs S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) { diff --git a/vfs/vfs.go b/vfs/vfs.go index f5dbcdfe..df72def4 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -31,6 +31,7 @@ type Fs interface { Chown(name string, uid int, gid int) error Chmod(name string, mode os.FileMode) error Chtimes(name string, atime, mtime time.Time) error + Truncate(name string, size int64) error ReadDir(dirname string) ([]os.FileInfo, error) IsUploadResumeSupported() bool IsAtomicUploadSupported() bool