From 94b46e57f19d5dc93e124d799376bc113f7fd4a3 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 3 Apr 2020 19:25:38 +0200 Subject: [PATCH] sftpd actions: execute defined command on error too add a new field inside the notification to indicate if an error is detected --- docs/custom-actions.md | 5 ++++- sftpd/handler.go | 4 ++-- sftpd/internal_test.go | 14 +++++++------- sftpd/sftpd.go | 13 +++++++++++-- sftpd/ssh_cmd.go | 10 +++++----- sftpd/transfer.go | 18 +++++++++--------- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/docs/custom-actions.md b/docs/custom-actions.md index 2e604388..56a9e0f0 100644 --- a/docs/custom-actions.md +++ b/docs/custom-actions.md @@ -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. diff --git a/sftpd/handler.go b/sftpd/handler.go index b9b5b89b..9cde40ad 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -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 } diff --git a/sftpd/internal_test.go b/sftpd/internal_test.go index 39078de9..003a44ea 100644 --- a/sftpd/internal_test.go +++ b/sftpd/internal_test.go @@ -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) } diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index 24ba52a6..50723b91 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -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() { diff --git a/sftpd/ssh_cmd.go b/sftpd/ssh_cmd.go index d672bcd0..7fc84277 100644 --- a/sftpd/ssh_cmd.go +++ b/sftpd/ssh_cmd.go @@ -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)) } } diff --git a/sftpd/transfer.go b/sftpd/transfer.go index 6c6edadb..1c577b7d 100644 --- a/sftpd/transfer.go +++ b/sftpd/transfer.go @@ -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