actions: add a generic hook to define external commands and HTTP URL

We can only define a single hook now and it can be an HTTP notification
or an external command, not both
This commit is contained in:
Nicola Murino
2020-05-24 15:29:39 +02:00
parent 760cc9ba5a
commit c27e3ef436
8 changed files with 130 additions and 71 deletions

View File

@@ -53,9 +53,8 @@ func init() {
Umask: "0022", Umask: "0022",
UploadMode: 0, UploadMode: 0,
Actions: sftpd.Actions{ Actions: sftpd.Actions{
ExecuteOn: []string{}, ExecuteOn: []string{},
Command: "", Hook: "",
HTTPNotificationURL: "",
}, },
HostKeys: []string{}, HostKeys: []string{},
KexAlgorithms: []string{}, KexAlgorithms: []string{},
@@ -83,9 +82,8 @@ func init() {
PoolSize: 0, PoolSize: 0,
UsersBaseDir: "", UsersBaseDir: "",
Actions: dataprovider.Actions{ Actions: dataprovider.Actions{
ExecuteOn: []string{}, ExecuteOn: []string{},
Command: "", Hook: "",
HTTPNotificationURL: "",
}, },
ExternalAuthHook: "", ExternalAuthHook: "",
ExternalAuthScope: 0, ExternalAuthScope: 0,
@@ -240,6 +238,28 @@ func checkHooksCompatibility() {
logger.WarnToConsole("keyboard_interactive_auth_program is deprecated, please use keyboard_interactive_auth_hook") logger.WarnToConsole("keyboard_interactive_auth_program is deprecated, please use keyboard_interactive_auth_hook")
globalConf.SFTPD.KeyboardInteractiveHook = globalConf.SFTPD.KeyboardInteractiveProgram //nolint:staticcheck globalConf.SFTPD.KeyboardInteractiveHook = globalConf.SFTPD.KeyboardInteractiveProgram //nolint:staticcheck
} }
if len(globalConf.SFTPD.Actions.Hook) == 0 {
if len(globalConf.SFTPD.Actions.HTTPNotificationURL) > 0 { //nolint:staticcheck
logger.Warn(logSender, "", "http_notification_url is deprecated, please use hook")
logger.WarnToConsole("http_notification_url is deprecated, please use hook")
globalConf.SFTPD.Actions.Hook = globalConf.SFTPD.Actions.HTTPNotificationURL //nolint:staticcheck
} else if len(globalConf.SFTPD.Actions.Command) > 0 { //nolint:staticcheck
logger.Warn(logSender, "", "command is deprecated, please use hook")
logger.WarnToConsole("command is deprecated, please use hook")
globalConf.SFTPD.Actions.Hook = globalConf.SFTPD.Actions.Command //nolint:staticcheck
}
}
if len(globalConf.ProviderConf.Actions.Hook) == 0 {
if len(globalConf.ProviderConf.Actions.HTTPNotificationURL) > 0 { //nolint:staticcheck
logger.Warn(logSender, "", "http_notification_url is deprecated, please use hook")
logger.WarnToConsole("http_notification_url is deprecated, please use hook")
globalConf.ProviderConf.Actions.Hook = globalConf.ProviderConf.Actions.HTTPNotificationURL //nolint:staticcheck
} else if len(globalConf.ProviderConf.Actions.Command) > 0 { //nolint:staticcheck
logger.Warn(logSender, "", "command is deprecated, please use hook")
logger.WarnToConsole("command is deprecated, please use hook")
globalConf.ProviderConf.Actions.Hook = globalConf.ProviderConf.Actions.Command //nolint:staticcheck
}
}
} }
func checkHostKeyCompatibility() { func checkHostKeyCompatibility() {

View File

@@ -177,6 +177,7 @@ func TestHookCompatibity(t *testing.T) {
providerConf := config.GetProviderConf() providerConf := config.GetProviderConf()
providerConf.ExternalAuthProgram = "ext_auth_program" //nolint:staticcheck providerConf.ExternalAuthProgram = "ext_auth_program" //nolint:staticcheck
providerConf.PreLoginProgram = "pre_login_program" //nolint:staticcheck providerConf.PreLoginProgram = "pre_login_program" //nolint:staticcheck
providerConf.Actions.Command = "/tmp/test_cmd" //nolint:staticcheck
c := make(map[string]dataprovider.Config) c := make(map[string]dataprovider.Config)
c["data_provider"] = providerConf c["data_provider"] = providerConf
jsonConf, err := json.Marshal(c) jsonConf, err := json.Marshal(c)
@@ -188,10 +189,26 @@ func TestHookCompatibity(t *testing.T) {
providerConf = config.GetProviderConf() providerConf = config.GetProviderConf()
assert.Equal(t, "ext_auth_program", providerConf.ExternalAuthHook) assert.Equal(t, "ext_auth_program", providerConf.ExternalAuthHook)
assert.Equal(t, "pre_login_program", providerConf.PreLoginHook) assert.Equal(t, "pre_login_program", providerConf.PreLoginHook)
assert.Equal(t, "/tmp/test_cmd", providerConf.Actions.Hook)
err = os.Remove(configFilePath)
assert.NoError(t, err)
providerConf.Actions.Hook = ""
providerConf.Actions.HTTPNotificationURL = "http://example.com/notify" //nolint:staticcheck
c = make(map[string]dataprovider.Config)
c["data_provider"] = providerConf
jsonConf, err = json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, 0666)
assert.NoError(t, err)
err = config.LoadConfig(configDir, tempConfigName)
assert.NoError(t, err)
providerConf = config.GetProviderConf()
assert.Equal(t, "http://example.com/notify", providerConf.Actions.Hook)
err = os.Remove(configFilePath) err = os.Remove(configFilePath)
assert.NoError(t, err) assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig() sftpdConf := config.GetSFTPDConfig()
sftpdConf.KeyboardInteractiveProgram = "key_int_program" //nolint:staticcheck sftpdConf.KeyboardInteractiveProgram = "key_int_program" //nolint:staticcheck
sftpdConf.Actions.Command = "/tmp/sftp_cmd" //nolint:staticcheck
cnf := make(map[string]sftpd.Configuration) cnf := make(map[string]sftpd.Configuration)
cnf["sftpd"] = sftpdConf cnf["sftpd"] = sftpdConf
jsonConf, err = json.Marshal(cnf) jsonConf, err = json.Marshal(cnf)
@@ -202,6 +219,21 @@ func TestHookCompatibity(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig() sftpdConf = config.GetSFTPDConfig()
assert.Equal(t, "key_int_program", sftpdConf.KeyboardInteractiveHook) assert.Equal(t, "key_int_program", sftpdConf.KeyboardInteractiveHook)
assert.Equal(t, "/tmp/sftp_cmd", sftpdConf.Actions.Hook)
err = os.Remove(configFilePath)
assert.NoError(t, err)
sftpdConf.Actions.Hook = ""
sftpdConf.Actions.HTTPNotificationURL = "http://example.com/sftp" //nolint:staticcheck
cnf = make(map[string]sftpd.Configuration)
cnf["sftpd"] = sftpdConf
jsonConf, err = json.Marshal(cnf)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, 0666)
assert.NoError(t, err)
err = config.LoadConfig(configDir, tempConfigName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
assert.Equal(t, "http://example.com/sftp", sftpdConf.Actions.Hook)
err = os.Remove(configFilePath) err = os.Remove(configFilePath)
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@@ -111,13 +111,12 @@ type schemaVersion struct {
type Actions struct { type Actions struct {
// Valid values are add, update, delete. Empty slice to disable // Valid values are add, update, delete. Empty slice to disable
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"` ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
// Absolute path to the command to execute, empty to disable // Deprecated: please use Hook
Command string `json:"command" mapstructure:"command"` Command string `json:"command" mapstructure:"command"`
// The URL to notify using an HTTP POST. // Deprecated: please use Hook
// The action is added to the query string. For example <url>?action=update.
// The user is sent serialized as json inside the POST body.
// Empty to disable
HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"` HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"`
// Absolute path to an external program or an HTTP URL
Hook string `json:"hook" mapstructure:"hook"`
} }
// Config provider configuration // Config provider configuration
@@ -1520,15 +1519,20 @@ func providerLog(level logger.LogLevel, format string, v ...interface{}) {
} }
func executeNotificationCommand(operation string, user User) error { func executeNotificationCommand(operation string, user User) error {
if !filepath.IsAbs(config.Actions.Hook) {
err := fmt.Errorf("invalid notification command %#v", config.Actions.Hook)
logger.Warn(logSender, "", "unable to execute notification command: %v", err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
commandArgs := user.getNotificationFieldsAsSlice(operation) commandArgs := user.getNotificationFieldsAsSlice(operation)
cmd := exec.CommandContext(ctx, config.Actions.Command, commandArgs...) cmd := exec.CommandContext(ctx, config.Actions.Hook, commandArgs...)
cmd.Env = append(os.Environ(), user.getNotificationFieldsAsEnvVars(operation)...) cmd.Env = append(os.Environ(), user.getNotificationFieldsAsEnvVars(operation)...)
startTime := time.Now() startTime := time.Now()
err := cmd.Run() err := cmd.Run()
providerLog(logger.LevelDebug, "executed command %#v with arguments: %+v, elapsed: %v, error: %v", providerLog(logger.LevelDebug, "executed command %#v with arguments: %+v, elapsed: %v, error: %v",
config.Actions.Command, commandArgs, time.Since(startTime), err) config.Actions.Hook, commandArgs, time.Since(startTime), err)
return err return err
} }
@@ -1537,6 +1541,9 @@ func executeAction(operation string, user User) {
if !utils.IsStringInSlice(operation, config.Actions.ExecuteOn) { if !utils.IsStringInSlice(operation, config.Actions.ExecuteOn) {
return return
} }
if len(config.Actions.Hook) == 0 {
return
}
if operation != operationDelete { if operation != operationDelete {
var err error var err error
user, err = provider.userExists(user.Username) user, err = provider.userExists(user.Username)
@@ -1545,21 +1552,11 @@ func executeAction(operation string, user User) {
return return
} }
} }
if len(config.Actions.Command) > 0 && filepath.IsAbs(config.Actions.Command) { if strings.HasPrefix(config.Actions.Hook, "http") {
// we are in a goroutine but if we have to send an HTTP notification we don't want to wait for the
// end of the command
if len(config.Actions.HTTPNotificationURL) > 0 {
go executeNotificationCommand(operation, user) //nolint:errcheck // the error is used in test cases only
} else {
executeNotificationCommand(operation, user) //nolint:errcheck // the error is used in test cases only
}
}
if len(config.Actions.HTTPNotificationURL) > 0 {
var url *url.URL var url *url.URL
url, err := url.Parse(config.Actions.HTTPNotificationURL) url, err := url.Parse(config.Actions.Hook)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "Invalid http_notification_url %#v for operation %#v: %v", config.Actions.HTTPNotificationURL, providerLog(logger.LevelWarn, "Invalid http_notification_url %#v for operation %#v: %v", config.Actions.Hook, operation, err)
operation, err)
return return
} }
q := url.Query() q := url.Query()
@@ -1580,5 +1577,7 @@ func executeAction(operation string, user User) {
} }
providerLog(logger.LevelDebug, "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v", providerLog(logger.LevelDebug, "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
operation, url.String(), respCode, time.Since(startTime), err) operation, url.String(), respCode, time.Since(startTime), err)
} else {
executeNotificationCommand(operation, user) //nolint:errcheck // the error is used in test cases only
} }
} }

View File

@@ -1,11 +1,12 @@
# Custom Actions # Custom Actions
The `actions` struct inside the "sftpd" configuration section allows to configure the actions for file operations and SSH commands. The `actions` struct inside the "sftpd" configuration section allows to configure the actions for file operations and SSH commands.
The `hook` can be defined as the absolute path of your program or an HTTP URL.
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 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: If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
- `action`, string, possible values are: `download`, `upload`, `delete`, `rename`, `ssh_cmd` - `action`, string, possible values are: `download`, `upload`, `delete`, `rename`, `ssh_cmd`
- `username` - `username`
@@ -13,7 +14,7 @@ The `command`, if defined, is invoked with the following arguments:
- `target_path`, non-empty for `rename` action - `target_path`, non-empty for `rename` action
- `ssh_cmd`, non-empty for `ssh_cmd` action - `ssh_cmd`, non-empty for `ssh_cmd` action
The `command` can also read the following environment variables: The external program can also read the following environment variables:
- `SFTPGO_ACTION` - `SFTPGO_ACTION`
- `SFTPGO_ACTION_USERNAME` - `SFTPGO_ACTION_USERNAME`
@@ -27,9 +28,9 @@ The `command` can also read the following environment variables:
- `SFTPGO_ACTION_STATUS`, integer. 0 means an error occurred. 1 means no error - `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. Previous global environment variables aren't cleared when the script is called.
The `command` must finish within 30 seconds. The program must finish within 30 seconds.
The `http_notification_url`, if defined, will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields: If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The request body will contain a JSON serialized struct with the following fields:
- `action` - `action`
- `username` - `username`
@@ -49,7 +50,7 @@ The `actions` struct inside the "data_provider" configuration section allows you
Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication. Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication.
The `command`, if defined, is invoked with the following arguments: If the `hook` defines a path to an external program, then this program is invoked with the following arguments:
- `action`, string, possible values are: `add`, `update`, `delete` - `action`, string, possible values are: `add`, `update`, `delete`
- `username` - `username`
@@ -60,7 +61,7 @@ The `command`, if defined, is invoked with the following arguments:
- `uid` - `uid`
- `gid` - `gid`
The `command` can also read the following environment variables: The external program can also read the following environment variables:
- `SFTPGO_USER_ACTION` - `SFTPGO_USER_ACTION`
- `SFTPGO_USER_USERNAME` - `SFTPGO_USER_USERNAME`
@@ -79,8 +80,8 @@ The `command` can also read the following environment variables:
- `SFTPGO_USER_FS_PROVIDER` - `SFTPGO_USER_FS_PROVIDER`
Previous global environment variables aren't cleared when the script is called. Previous global environment variables aren't cleared when the script is called.
The `command` must finish within 15 seconds. The program must finish within 15 seconds.
The `http_notification_url`, if defined, will be invoked as HTTP POST. The action is added to the query string, for example `<http_notification_url>?action=update`, and the user is sent serialized as JSON inside the POST body with sensitive fields removed. If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. The action is added to the query string, for example `<hook>?action=update`, and the user is sent serialized as JSON inside the POST body with sensitive fields removed.
The HTTP request will use the global configuration for HTTP clients. The HTTP request will use the global configuration for HTTP clients.

View File

@@ -159,9 +159,8 @@ func TestWrongActions(t *testing.T) {
badCommand = "C:\\bad\\command" badCommand = "C:\\bad\\command"
} }
actions = Actions{ actions = Actions{
ExecuteOn: []string{operationDownload}, ExecuteOn: []string{operationDownload},
Command: badCommand, Hook: badCommand,
HTTPNotificationURL: "",
} }
user := dataprovider.User{ user := dataprovider.User{
Username: "username", Username: "username",
@@ -170,21 +169,27 @@ func TestWrongActions(t *testing.T) {
assert.Error(t, err, "action with bad command must fail") assert.Error(t, err, "action with bad command must fail")
err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0, nil)) err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0, nil))
assert.NoError(t, err) assert.EqualError(t, err, errUnconfiguredAction.Error())
actions.Command = "" actions.Hook = "http://foo\x7f.com/"
actions.HTTPNotificationURL = "http://foo\x7f.com/"
err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil)) err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
assert.Error(t, err, "action with bad url must fail") assert.Error(t, err, "action with bad url must fail")
actions.Hook = ""
err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
assert.Error(t, err, errNoHook.Error())
actions.Hook = "relative path"
err = executeNotificationCommand(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
assert.EqualError(t, err, fmt.Sprintf("invalid notification command %#v", actions.Hook))
actions = actionsCopy actions = actionsCopy
} }
func TestActionHTTP(t *testing.T) { func TestActionHTTP(t *testing.T) {
actionsCopy := actions actionsCopy := actions
actions = Actions{ actions = Actions{
ExecuteOn: []string{operationDownload}, ExecuteOn: []string{operationDownload},
Command: "", Hook: "http://127.0.0.1:8080/",
HTTPNotificationURL: "http://127.0.0.1:8080/",
} }
user := dataprovider.User{ user := dataprovider.User{
Username: "username", Username: "username",

View File

@@ -7,11 +7,13 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
@@ -66,9 +68,11 @@ var (
setstatMode int setstatMode int
supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd", supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd",
"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"} "git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd", "scp"} defaultSSHCommands = []string{"md5sum", "sha1sum", "cd", "pwd", "scp"}
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"} sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"} systemCommands = []string{"git-receive-pack", "git-upload-pack", "git-upload-archive", "rsync"}
errUnconfiguredAction = errors.New("no hook is configured for this action")
errNoHook = errors.New("unable to execute action, no hook defined")
) )
type connectionTransfer struct { type connectionTransfer struct {
@@ -92,10 +96,12 @@ type ActiveQuotaScan struct {
type Actions struct { type Actions struct {
// Valid values are download, upload, delete, rename, ssh_cmd. Empty slice to disable // Valid values are download, upload, delete, rename, ssh_cmd. Empty slice to disable
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"` ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
// Absolute path to the command to execute, empty to disable // Deprecated: please use Hook
Command string `json:"command" mapstructure:"command"` Command string `json:"command" mapstructure:"command"`
// The URL to notify using an HTTP GET, empty to disable // Deprecated: please use Hook
HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"` HTTPNotificationURL string `json:"http_notification_url" mapstructure:"http_notification_url"`
// Absolute path to an external program or an HTTP URL
Hook string `json:"hook" mapstructure:"hook"`
} }
// ConnectionStatus status for an active connection // ConnectionStatus status for an active connection
@@ -474,38 +480,36 @@ func isAtomicUploadEnabled() bool {
} }
func executeNotificationCommand(a actionNotification) error { func executeNotificationCommand(a actionNotification) error {
if !filepath.IsAbs(actions.Hook) {
err := fmt.Errorf("invalid notification command %#v", actions.Hook)
logger.Warn(logSender, "", "unable to execute notification command: %v", err)
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, actions.Command, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd) cmd := exec.CommandContext(ctx, actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd)
cmd.Env = append(os.Environ(), a.AsEnvVars()...) cmd.Env = append(os.Environ(), a.AsEnvVars()...)
startTime := time.Now() startTime := time.Now()
err := cmd.Run() err := cmd.Run()
logger.Debug(logSender, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v", logger.Debug(logSender, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
actions.Command, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd, time.Since(startTime), err) actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd, time.Since(startTime), err)
return err return err
} }
// executed in a goroutine // executed in a goroutine
func executeAction(a actionNotification) error { func executeAction(a actionNotification) error {
if !utils.IsStringInSlice(a.Action, actions.ExecuteOn) { if !utils.IsStringInSlice(a.Action, actions.ExecuteOn) {
return nil return errUnconfiguredAction
} }
var err error if len(actions.Hook) == 0 {
if len(actions.Command) > 0 && filepath.IsAbs(actions.Command) { logger.Warn(logSender, "", "Unable to send notification, no hook is defined")
// we are in a goroutine but if we have to send an HTTP notification we don't want to wait for the return errNoHook
// end of the command
if len(actions.HTTPNotificationURL) > 0 {
go executeNotificationCommand(a) //nolint:errcheck
} else {
err = executeNotificationCommand(a) //nolint:errcheck
}
} }
if len(actions.HTTPNotificationURL) > 0 { if strings.HasPrefix(actions.Hook, "http") {
var url *url.URL var url *url.URL
url, err = url.Parse(actions.HTTPNotificationURL) url, err := url.Parse(actions.Hook)
if err != nil { if err != nil {
logger.Warn(logSender, "", "Invalid http_notification_url %#v for operation %#v: %v", actions.HTTPNotificationURL, logger.Warn(logSender, "", "Invalid hook %#v for operation %#v: %v", actions.Hook, a.Action, err)
a.Action, err)
return err return err
} }
startTime := time.Now() startTime := time.Now()
@@ -518,6 +522,7 @@ func executeAction(a actionNotification) error {
} }
logger.Debug(logSender, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v", logger.Debug(logSender, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
a.Action, url.String(), respCode, time.Since(startTime), err) a.Action, url.String(), respCode, time.Since(startTime), err)
return err
} }
return err return executeNotificationCommand(a)
} }

View File

@@ -174,8 +174,7 @@ func TestMain(m *testing.M) {
scriptArgs = "%*" scriptArgs = "%*"
} else { } else {
sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete", "ssh_cmd"} sftpdConf.Actions.ExecuteOn = []string{"download", "upload", "rename", "delete", "ssh_cmd"}
sftpdConf.Actions.Command = "/bin/true" sftpdConf.Actions.Hook = "/bin/true"
sftpdConf.Actions.HTTPNotificationURL = "http://127.0.0.1:8083/"
scriptArgs = "$@" scriptArgs = "$@"
scpPath, err = exec.LookPath("scp") scpPath, err = exec.LookPath("scp")
if err != nil { if err != nil {

View File

@@ -9,8 +9,7 @@
"upload_mode": 0, "upload_mode": 0,
"actions": { "actions": {
"execute_on": [], "execute_on": [],
"command": "", "hook": ""
"http_notification_url": ""
}, },
"host_keys": [], "host_keys": [],
"kex_algorithms": [], "kex_algorithms": [],
@@ -46,8 +45,7 @@
"users_base_dir": "", "users_base_dir": "",
"actions": { "actions": {
"execute_on": [], "execute_on": [],
"command": "", "hook": ""
"http_notification_url": ""
}, },
"external_auth_hook": "", "external_auth_hook": "",
"external_auth_scope": 0, "external_auth_scope": 0,