|
@@ -19,6 +19,7 @@ import (
|
|
|
"github.com/drakkan/sftpgo/v2/logger"
|
|
|
"github.com/drakkan/sftpgo/v2/sdk"
|
|
|
"github.com/drakkan/sftpgo/v2/sdk/plugin"
|
|
|
+ "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
|
|
|
"github.com/drakkan/sftpgo/v2/util"
|
|
|
)
|
|
|
|
|
@@ -50,67 +51,63 @@ func InitializeActionHandler(handler ActionHandler) {
|
|
|
actionHandler = handler
|
|
|
}
|
|
|
|
|
|
+func handleUnconfiguredPreAction(operation string) error {
|
|
|
+ // for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction.
|
|
|
+ // Other pre action will deny the operation on error so if we have no configuration we must return
|
|
|
+ // a nil error
|
|
|
+ if operation == operationPreDelete {
|
|
|
+ return errUnconfiguredAction
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
// ExecutePreAction executes a pre-* action and returns the result
|
|
|
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) error {
|
|
|
- remoteIP := conn.GetRemoteIP()
|
|
|
- plugin.Handler.NotifyFsEvent(time.Now().UnixNano(), operation, conn.User.Username, filePath, "", "", conn.protocol,
|
|
|
- remoteIP, virtualPath, "", conn.ID, fileSize, nil)
|
|
|
- if !util.IsStringInSlice(operation, Config.Actions.ExecuteOn) {
|
|
|
- // for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction.
|
|
|
- // Other pre action will deny the operation on error so if we have no configuration we must return
|
|
|
- // a nil error
|
|
|
- if operation == operationPreDelete {
|
|
|
- return errUnconfiguredAction
|
|
|
- }
|
|
|
- return nil
|
|
|
+ var event *notifier.FsEvent
|
|
|
+ hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
|
|
+ hasHook := util.IsStringInSlice(operation, Config.Actions.ExecuteOn)
|
|
|
+ if !hasHook && !hasNotifiersPlugin {
|
|
|
+ return handleUnconfiguredPreAction(operation)
|
|
|
+ }
|
|
|
+ event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
|
|
|
+ conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, nil)
|
|
|
+ if hasNotifiersPlugin {
|
|
|
+ plugin.Handler.NotifyFsEvent(event)
|
|
|
}
|
|
|
- notification := newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
|
|
|
- conn.protocol, remoteIP, conn.ID, fileSize, openFlags, nil)
|
|
|
- return actionHandler.Handle(notification)
|
|
|
+ if !hasHook {
|
|
|
+ return handleUnconfiguredPreAction(operation)
|
|
|
+ }
|
|
|
+ return actionHandler.Handle(event)
|
|
|
}
|
|
|
|
|
|
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
|
|
func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtualPath, target, virtualTarget, sshCmd string,
|
|
|
fileSize int64, err error,
|
|
|
) {
|
|
|
- remoteIP := conn.GetRemoteIP()
|
|
|
- plugin.Handler.NotifyFsEvent(time.Now().UnixNano(), operation, conn.User.Username, filePath, target, sshCmd, conn.protocol,
|
|
|
- remoteIP, virtualPath, virtualTarget, conn.ID, fileSize, err)
|
|
|
- notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
|
|
|
- conn.protocol, remoteIP, conn.ID, fileSize, 0, err)
|
|
|
-
|
|
|
- if util.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
|
|
- actionHandler.Handle(notification) //nolint:errcheck
|
|
|
+ hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
|
|
+ hasHook := util.IsStringInSlice(operation, Config.Actions.ExecuteOn)
|
|
|
+ if !hasHook && !hasNotifiersPlugin {
|
|
|
return
|
|
|
}
|
|
|
+ notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
|
|
|
+ conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, err)
|
|
|
+ if hasNotifiersPlugin {
|
|
|
+ plugin.Handler.NotifyFsEvent(notification)
|
|
|
+ }
|
|
|
+
|
|
|
+ if hasHook {
|
|
|
+ if util.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
|
|
+ actionHandler.Handle(notification) //nolint:errcheck
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- go actionHandler.Handle(notification) //nolint:errcheck
|
|
|
+ go actionHandler.Handle(notification) //nolint:errcheck
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// ActionHandler handles a notification for a Protocol Action.
|
|
|
type ActionHandler interface {
|
|
|
- Handle(notification *ActionNotification) error
|
|
|
-}
|
|
|
-
|
|
|
-// ActionNotification defines a notification for a Protocol Action.
|
|
|
-type ActionNotification struct {
|
|
|
- Action string `json:"action"`
|
|
|
- Username string `json:"username"`
|
|
|
- Path string `json:"path"`
|
|
|
- TargetPath string `json:"target_path,omitempty"`
|
|
|
- VirtualPath string `json:"virtual_path"`
|
|
|
- VirtualTargetPath string `json:"virtual_target_path,omitempty"`
|
|
|
- SSHCmd string `json:"ssh_cmd,omitempty"`
|
|
|
- FileSize int64 `json:"file_size,omitempty"`
|
|
|
- FsProvider int `json:"fs_provider"`
|
|
|
- Bucket string `json:"bucket,omitempty"`
|
|
|
- Endpoint string `json:"endpoint,omitempty"`
|
|
|
- Status int `json:"status"`
|
|
|
- Protocol string `json:"protocol"`
|
|
|
- IP string `json:"ip"`
|
|
|
- SessionID string `json:"session_id"`
|
|
|
- Timestamp int64 `json:"timestamp"`
|
|
|
- OpenFlags int `json:"open_flags,omitempty"`
|
|
|
+ Handle(notification *notifier.FsEvent) error
|
|
|
}
|
|
|
|
|
|
func newActionNotification(
|
|
@@ -119,7 +116,7 @@ func newActionNotification(
|
|
|
fileSize int64,
|
|
|
openFlags int,
|
|
|
err error,
|
|
|
-) *ActionNotification {
|
|
|
+) *notifier.FsEvent {
|
|
|
var bucket, endpoint string
|
|
|
status := 1
|
|
|
|
|
@@ -146,7 +143,7 @@ func newActionNotification(
|
|
|
status = 2
|
|
|
}
|
|
|
|
|
|
- return &ActionNotification{
|
|
|
+ return ¬ifier.FsEvent{
|
|
|
Action: operation,
|
|
|
Username: user.Username,
|
|
|
Path: filePath,
|
|
@@ -169,28 +166,29 @@ func newActionNotification(
|
|
|
|
|
|
type defaultActionHandler struct{}
|
|
|
|
|
|
-func (h *defaultActionHandler) Handle(notification *ActionNotification) error {
|
|
|
- if !util.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) {
|
|
|
+func (h *defaultActionHandler) Handle(event *notifier.FsEvent) error {
|
|
|
+ if !util.IsStringInSlice(event.Action, Config.Actions.ExecuteOn) {
|
|
|
return errUnconfiguredAction
|
|
|
}
|
|
|
|
|
|
if Config.Actions.Hook == "" {
|
|
|
- logger.Warn(notification.Protocol, "", "Unable to send notification, no hook is defined")
|
|
|
+ logger.Warn(event.Protocol, "", "Unable to send notification, no hook is defined")
|
|
|
|
|
|
return errNoHook
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(Config.Actions.Hook, "http") {
|
|
|
- return h.handleHTTP(notification)
|
|
|
+ return h.handleHTTP(event)
|
|
|
}
|
|
|
|
|
|
- return h.handleCommand(notification)
|
|
|
+ return h.handleCommand(event)
|
|
|
}
|
|
|
|
|
|
-func (h *defaultActionHandler) handleHTTP(notification *ActionNotification) error {
|
|
|
+func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error {
|
|
|
u, err := url.Parse(Config.Actions.Hook)
|
|
|
if err != nil {
|
|
|
- logger.Warn(notification.Protocol, "", "Invalid hook %#v for operation %#v: %v", Config.Actions.Hook, notification.Action, err)
|
|
|
+ logger.Error(event.Protocol, "", "Invalid hook %#v for operation %#v: %v",
|
|
|
+ Config.Actions.Hook, event.Action, err)
|
|
|
return err
|
|
|
}
|
|
|
|
|
@@ -198,7 +196,7 @@ func (h *defaultActionHandler) handleHTTP(notification *ActionNotification) erro
|
|
|
respCode := 0
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
- _ = json.NewEncoder(&b).Encode(notification)
|
|
|
+ _ = json.NewEncoder(&b).Encode(event)
|
|
|
|
|
|
resp, err := httpclient.RetryablePost(Config.Actions.Hook, "application/json", &b)
|
|
|
if err == nil {
|
|
@@ -210,16 +208,16 @@ func (h *defaultActionHandler) handleHTTP(notification *ActionNotification) erro
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- logger.Debug(notification.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
|
|
- notification.Action, u.Redacted(), respCode, time.Since(startTime), err)
|
|
|
+ logger.Debug(event.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
|
|
+ event.Action, u.Redacted(), respCode, time.Since(startTime), err)
|
|
|
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
-func (h *defaultActionHandler) handleCommand(notification *ActionNotification) error {
|
|
|
+func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
|
|
|
if !filepath.IsAbs(Config.Actions.Hook) {
|
|
|
err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
|
|
|
- logger.Warn(notification.Protocol, "", "unable to execute notification command: %v", err)
|
|
|
+ logger.Warn(event.Protocol, "", "unable to execute notification command: %v", err)
|
|
|
|
|
|
return err
|
|
|
}
|
|
@@ -228,35 +226,35 @@ func (h *defaultActionHandler) handleCommand(notification *ActionNotification) e
|
|
|
defer cancel()
|
|
|
|
|
|
cmd := exec.CommandContext(ctx, Config.Actions.Hook)
|
|
|
- cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
|
|
|
+ cmd.Env = append(os.Environ(), notificationAsEnvVars(event)...)
|
|
|
|
|
|
startTime := time.Now()
|
|
|
err := cmd.Run()
|
|
|
|
|
|
- logger.Debug(notification.Protocol, "", "executed command %#v, elapsed: %v, error: %v",
|
|
|
+ logger.Debug(event.Protocol, "", "executed command %#v, elapsed: %v, error: %v",
|
|
|
Config.Actions.Hook, time.Since(startTime), err)
|
|
|
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
-func notificationAsEnvVars(notification *ActionNotification) []string {
|
|
|
+func notificationAsEnvVars(event *notifier.FsEvent) []string {
|
|
|
return []string{
|
|
|
- fmt.Sprintf("SFTPGO_ACTION=%v", notification.Action),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", notification.Username),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_PATH=%v", notification.Path),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", notification.TargetPath),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%v", notification.VirtualPath),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%v", notification.VirtualTargetPath),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", notification.SSHCmd),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", notification.FileSize),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", notification.FsProvider),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", notification.Bucket),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_IP=%v", notification.IP),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%v", notification.SessionID),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", notification.OpenFlags),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%v", notification.Timestamp),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION=%v", event.Action),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", event.Username),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_PATH=%v", event.Path),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", event.TargetPath),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%v", event.VirtualPath),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%v", event.VirtualTargetPath),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", event.SSHCmd),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", event.FileSize),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", event.FsProvider),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", event.Bucket),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", event.Endpoint),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", event.Status),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", event.Protocol),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_IP=%v", event.IP),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%v", event.SessionID),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", event.OpenFlags),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%v", event.Timestamp),
|
|
|
}
|
|
|
}
|