sftpd actions: execute defined command on error too

add a new field inside the notification to indicate if an error is
detected
This commit is contained in:
Nicola Murino 2020-04-03 19:25:38 +02:00
parent 9046acbe68
commit 94b46e57f1
6 changed files with 38 additions and 26 deletions

View file

@ -2,7 +2,8 @@
The `actions` struct inside the "sftpd" configuration section allows to configure the actions for file operations and SSH commands.
Actions will not be executed if an error is detected, and so a partial file is uploaded or an SSH command is not successfully completed. The `upload` condition includes both uploads to new files and overwrite of existing files. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
The `upload` condition includes both uploads to new files and overwrite of existing files. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
The `command`, if defined, is invoked with the following arguments:
@ -23,6 +24,7 @@ The `command` can also read the following environment variables:
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend
- `SFTPGO_ACTION_BUCKET`, non-empty for S3 and GCS backends
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 backend if configured
- `SFTPGO_ACTION_STATUS`, integer. 0 means an error occurred. 1 means no error
Previous global environment variables aren't cleared when the script is called.
The `command` must finish within 30 seconds.
@ -38,6 +40,7 @@ The `http_notification_url`, if defined, will be invoked as HTTP POST. The reque
- `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend
- `bucket`, not null for S3 and GCS backends
- `endpoint`, not null for S3 backend if configured
- `status`, integer. 0 means an error occurred. 1 means no error
The HTTP request is executed with a 15-second timeout.

View file

@ -335,7 +335,7 @@ func (c Connection) handleSFTPRename(sourcePath string, targetPath string, reque
return vfs.GetSFTPError(c.fs, err)
}
logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "")
go executeAction(newActionNotification(c.User, operationRename, sourcePath, targetPath, "", 0))
go executeAction(newActionNotification(c.User, operationRename, sourcePath, targetPath, "", 0, nil))
return nil
}
@ -443,7 +443,7 @@ func (c Connection) handleSFTPRemove(filePath string, request *sftp.Request) err
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
}
go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size()))
go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size(), nil))
return sftp.ErrSSHFxOk
}

View file

@ -131,7 +131,7 @@ func TestNewActionNotification(t *testing.T) {
user.FsConfig.GCSConfig = vfs.GCSFsConfig{
Bucket: "gcsbucket",
}
a := newActionNotification(user, operationDownload, "path", "target", "", 123)
a := newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
if a.Username != "username" {
t.Errorf("unexpected username")
}
@ -142,7 +142,7 @@ func TestNewActionNotification(t *testing.T) {
t.Errorf("unexpected endpoint")
}
user.FsConfig.Provider = 1
a = newActionNotification(user, operationDownload, "path", "target", "", 123)
a = newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
if a.Bucket != "s3bucket" {
t.Errorf("unexpected s3 bucket")
}
@ -150,7 +150,7 @@ func TestNewActionNotification(t *testing.T) {
t.Errorf("unexpected endpoint")
}
user.FsConfig.Provider = 2
a = newActionNotification(user, operationDownload, "path", "target", "", 123)
a = newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
if a.Bucket != "gcsbucket" {
t.Errorf("unexpected gcs bucket")
}
@ -173,17 +173,17 @@ func TestWrongActions(t *testing.T) {
user := dataprovider.User{
Username: "username",
}
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
if err == nil {
t.Errorf("action with bad command must fail")
}
err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0))
err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0, nil))
if err != nil {
t.Errorf("action not configured must silently fail")
}
actions.Command = ""
actions.HTTPNotificationURL = "http://foo\x7f.com/"
err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
if err == nil {
t.Errorf("action with bad url must fail")
}
@ -200,7 +200,7 @@ func TestActionHTTP(t *testing.T) {
user := dataprovider.User{
Username: "username",
}
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
if err != nil {
t.Errorf("unexpected error: %v", err)
}

View file

@ -138,17 +138,23 @@ type actionNotification struct {
FsProvider int `json:"fs_provider"`
Bucket string `json:"bucket,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Status int `json:"status"`
}
func newActionNotification(user dataprovider.User, operation, filePath, target, sshCmd string, fileSize int64) actionNotification {
func newActionNotification(user dataprovider.User, operation, filePath, target, sshCmd string, fileSize int64,
err error) actionNotification {
bucket := ""
endpoint := ""
status := 1
if user.FsConfig.Provider == 1 {
bucket = user.FsConfig.S3Config.Bucket
endpoint = user.FsConfig.S3Config.Endpoint
} else if user.FsConfig.Provider == 2 {
bucket = user.FsConfig.GCSConfig.Bucket
}
if err != nil {
status = 0
}
return actionNotification{
Action: operation,
Username: user.Username,
@ -159,6 +165,7 @@ func newActionNotification(user dataprovider.User, operation, filePath, target,
FsProvider: user.FsConfig.Provider,
Bucket: bucket,
Endpoint: endpoint,
Status: status,
}
}
@ -176,7 +183,9 @@ func (a *actionNotification) AsEnvVars() []string {
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", a.FileSize),
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", a.FsProvider),
fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", a.Bucket),
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", a.Endpoint)}
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", a.Endpoint),
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", a.Status),
}
}
func init() {

View file

@ -444,17 +444,17 @@ func (c *sshCommand) sendExitStatus(err error) {
}
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
c.connection.channel.Close()
metrics.SSHCommandCompleted(err)
// for scp we notify single uploads/downloads
if err == nil && c.command != "scp" {
if c.command != "scp" {
metrics.SSHCommandCompleted(err)
realPath := c.getDestPath()
if len(realPath) > 0 {
p, err := c.connection.fs.ResolvePath(realPath)
if err == nil {
p, e := c.connection.fs.ResolvePath(realPath)
if e == nil {
realPath = p
}
}
go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0))
go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0, err))
}
}

View file

@ -147,16 +147,16 @@ func (t *Transfer) Close() error {
}
}
}
if t.transferError == nil {
elapsed := time.Since(t.start).Nanoseconds() / 1000000
if t.transferType == transferDownload {
logger.TransferLog(downloadLogSender, t.path, elapsed, t.bytesSent, t.user.Username, t.connectionID, t.protocol)
go executeAction(newActionNotification(t.user, operationDownload, t.path, "", "", t.bytesSent))
} else {
logger.TransferLog(uploadLogSender, t.path, elapsed, t.bytesReceived, t.user.Username, t.connectionID, t.protocol)
go executeAction(newActionNotification(t.user, operationUpload, t.path, "", "", t.bytesReceived+t.minWriteOffset))
}
elapsed := time.Since(t.start).Nanoseconds() / 1000000
if t.transferType == transferDownload {
logger.TransferLog(downloadLogSender, t.path, elapsed, t.bytesSent, t.user.Username, t.connectionID, t.protocol)
go executeAction(newActionNotification(t.user, operationDownload, t.path, "", "", t.bytesSent, t.transferError))
} else {
logger.TransferLog(uploadLogSender, t.path, elapsed, t.bytesReceived, t.user.Username, t.connectionID, t.protocol)
go executeAction(newActionNotification(t.user, operationUpload, t.path, "", "", t.bytesReceived+t.minWriteOffset,
t.transferError))
}
if t.transferError != nil {
logger.Warn(logSender, t.connectionID, "transfer error: %v, path: %#v", t.transferError, t.path)
if err == nil {
err = t.transferError