2020-07-24 21:39:38 +00:00
|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/drakkan/sftpgo/dataprovider"
|
|
|
|
"github.com/drakkan/sftpgo/httpclient"
|
|
|
|
"github.com/drakkan/sftpgo/logger"
|
|
|
|
"github.com/drakkan/sftpgo/utils"
|
2021-03-21 18:15:47 +00:00
|
|
|
"github.com/drakkan/sftpgo/vfs"
|
2020-07-24 21:39:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errUnconfiguredAction = errors.New("no hook is configured for this action")
|
|
|
|
errNoHook = errors.New("unable to execute action, no hook defined")
|
|
|
|
errUnexpectedHTTResponse = errors.New("unexpected HTTP response code")
|
|
|
|
)
|
|
|
|
|
|
|
|
// ProtocolActions defines the action to execute on file operations and SSH commands
|
|
|
|
type ProtocolActions struct {
|
|
|
|
// Valid values are download, upload, pre-delete, delete, rename, ssh_cmd. Empty slice to disable
|
|
|
|
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
|
2021-05-11 10:45:14 +00:00
|
|
|
// Actions to be performed synchronously.
|
|
|
|
// The pre-delete action is always executed synchronously while the other ones are asynchronous.
|
|
|
|
// Executing an action synchronously means that SFTPGo will not return a result code to the client
|
|
|
|
// (which is waiting for it) until your hook have completed its execution.
|
|
|
|
ExecuteSync []string `json:"execute_sync" mapstructure:"execute_sync"`
|
2020-07-24 21:39:38 +00:00
|
|
|
// Absolute path to an external program or an HTTP URL
|
|
|
|
Hook string `json:"hook" mapstructure:"hook"`
|
|
|
|
}
|
|
|
|
|
2021-02-12 20:42:49 +00:00
|
|
|
var actionHandler ActionHandler = &defaultActionHandler{}
|
2020-10-19 23:46:51 +00:00
|
|
|
|
|
|
|
// InitializeActionHandler lets the user choose an action handler implementation.
|
|
|
|
//
|
|
|
|
// Do NOT call this function after application initialization.
|
|
|
|
func InitializeActionHandler(handler ActionHandler) {
|
|
|
|
actionHandler = handler
|
|
|
|
}
|
|
|
|
|
2021-05-26 05:48:37 +00:00
|
|
|
// ExecutePreAction executes a pre-* action and returns the result
|
2021-05-31 20:33:23 +00:00
|
|
|
func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64, openFlags int) error {
|
2021-05-26 05:48:37 +00:00
|
|
|
if !utils.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
|
|
|
|
}
|
2021-05-31 20:33:23 +00:00
|
|
|
notification := newActionNotification(user, operation, filePath, virtualPath, "", "", protocol, fileSize, openFlags, nil)
|
2021-05-26 05:48:37 +00:00
|
|
|
return actionHandler.Handle(notification)
|
|
|
|
}
|
|
|
|
|
2021-05-11 10:45:14 +00:00
|
|
|
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
2021-05-26 05:48:37 +00:00
|
|
|
func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) {
|
2021-05-31 20:33:23 +00:00
|
|
|
notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, 0, err)
|
2020-10-19 23:46:51 +00:00
|
|
|
|
2021-05-11 10:45:14 +00:00
|
|
|
if utils.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
|
|
|
actionHandler.Handle(notification) //nolint:errcheck
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go actionHandler.Handle(notification) //nolint:errcheck
|
2020-10-19 23:46:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ActionHandler handles a notification for a Protocol Action.
|
|
|
|
type ActionHandler interface {
|
2021-02-16 18:11:36 +00:00
|
|
|
Handle(notification *ActionNotification) error
|
2020-10-19 23:46:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ActionNotification defines a notification for a Protocol Action.
|
|
|
|
type ActionNotification struct {
|
2020-07-24 21:39:38 +00:00
|
|
|
Action string `json:"action"`
|
|
|
|
Username string `json:"username"`
|
|
|
|
Path string `json:"path"`
|
|
|
|
TargetPath string `json:"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"`
|
2021-05-31 20:33:23 +00:00
|
|
|
OpenFlags int `json:"open_flags,omitempty"`
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 23:46:51 +00:00
|
|
|
func newActionNotification(
|
|
|
|
user *dataprovider.User,
|
2021-05-26 05:48:37 +00:00
|
|
|
operation, filePath, virtualPath, target, sshCmd, protocol string,
|
2020-10-19 23:46:51 +00:00
|
|
|
fileSize int64,
|
2021-05-31 20:33:23 +00:00
|
|
|
openFlags int,
|
2020-10-19 23:46:51 +00:00
|
|
|
err error,
|
2021-02-16 18:11:36 +00:00
|
|
|
) *ActionNotification {
|
2020-10-19 23:46:51 +00:00
|
|
|
var bucket, endpoint string
|
2020-07-24 21:39:38 +00:00
|
|
|
status := 1
|
2020-10-19 23:46:51 +00:00
|
|
|
|
2021-05-26 05:48:37 +00:00
|
|
|
fsConfig := user.GetFsConfigForPath(virtualPath)
|
|
|
|
|
|
|
|
switch fsConfig.Provider {
|
|
|
|
case vfs.S3FilesystemProvider:
|
|
|
|
bucket = fsConfig.S3Config.Bucket
|
|
|
|
endpoint = fsConfig.S3Config.Endpoint
|
|
|
|
case vfs.GCSFilesystemProvider:
|
|
|
|
bucket = fsConfig.GCSConfig.Bucket
|
|
|
|
case vfs.AzureBlobFilesystemProvider:
|
|
|
|
bucket = fsConfig.AzBlobConfig.Container
|
2021-06-11 20:27:36 +00:00
|
|
|
if fsConfig.AzBlobConfig.Endpoint != "" {
|
2021-05-26 05:48:37 +00:00
|
|
|
endpoint = fsConfig.AzBlobConfig.Endpoint
|
2020-10-25 07:18:48 +00:00
|
|
|
}
|
2021-05-26 05:48:37 +00:00
|
|
|
case vfs.SFTPFilesystemProvider:
|
|
|
|
endpoint = fsConfig.SFTPConfig.Endpoint
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
2020-10-19 23:46:51 +00:00
|
|
|
|
2020-07-24 21:39:38 +00:00
|
|
|
if err == ErrQuotaExceeded {
|
|
|
|
status = 2
|
|
|
|
} else if err != nil {
|
|
|
|
status = 0
|
|
|
|
}
|
2020-10-19 23:46:51 +00:00
|
|
|
|
2021-02-16 18:11:36 +00:00
|
|
|
return &ActionNotification{
|
2020-07-24 21:39:38 +00:00
|
|
|
Action: operation,
|
|
|
|
Username: user.Username,
|
|
|
|
Path: filePath,
|
|
|
|
TargetPath: target,
|
|
|
|
SSHCmd: sshCmd,
|
|
|
|
FileSize: fileSize,
|
2021-05-26 05:48:37 +00:00
|
|
|
FsProvider: int(fsConfig.Provider),
|
2020-07-24 21:39:38 +00:00
|
|
|
Bucket: bucket,
|
|
|
|
Endpoint: endpoint,
|
|
|
|
Status: status,
|
|
|
|
Protocol: protocol,
|
2021-05-31 20:33:23 +00:00
|
|
|
OpenFlags: openFlags,
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-19 23:46:51 +00:00
|
|
|
type defaultActionHandler struct{}
|
|
|
|
|
2021-02-16 18:11:36 +00:00
|
|
|
func (h *defaultActionHandler) Handle(notification *ActionNotification) error {
|
2020-10-19 23:46:51 +00:00
|
|
|
if !utils.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) {
|
|
|
|
return errUnconfiguredAction
|
|
|
|
}
|
|
|
|
|
|
|
|
if Config.Actions.Hook == "" {
|
|
|
|
logger.Warn(notification.Protocol, "", "Unable to send notification, no hook is defined")
|
|
|
|
|
|
|
|
return errNoHook
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.HasPrefix(Config.Actions.Hook, "http") {
|
|
|
|
return h.handleHTTP(notification)
|
|
|
|
}
|
|
|
|
|
|
|
|
return h.handleCommand(notification)
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 18:11:36 +00:00
|
|
|
func (h *defaultActionHandler) handleHTTP(notification *ActionNotification) error {
|
2020-10-19 23:46:51 +00:00
|
|
|
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)
|
|
|
|
return err
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
2020-10-19 23:46:51 +00:00
|
|
|
|
|
|
|
startTime := time.Now()
|
|
|
|
respCode := 0
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
_ = json.NewEncoder(&b).Encode(notification)
|
|
|
|
|
2021-05-25 06:36:01 +00:00
|
|
|
resp, err := httpclient.RetryablePost(Config.Actions.Hook, "application/json", &b)
|
2020-10-19 23:46:51 +00:00
|
|
|
if err == nil {
|
|
|
|
respCode = resp.StatusCode
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
|
|
if respCode != http.StatusOK {
|
|
|
|
err = errUnexpectedHTTResponse
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-26 05:48:37 +00:00
|
|
|
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)
|
2020-10-19 23:46:51 +00:00
|
|
|
|
|
|
|
return err
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 18:11:36 +00:00
|
|
|
func (h *defaultActionHandler) handleCommand(notification *ActionNotification) error {
|
2020-07-24 21:39:38 +00:00
|
|
|
if !filepath.IsAbs(Config.Actions.Hook) {
|
|
|
|
err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
|
2020-10-19 23:46:51 +00:00
|
|
|
logger.Warn(notification.Protocol, "", "unable to execute notification command: %v", err)
|
|
|
|
|
2020-07-24 21:39:38 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-10-19 23:46:51 +00:00
|
|
|
|
2020-07-24 21:39:38 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
defer cancel()
|
2020-10-19 23:46:51 +00:00
|
|
|
|
|
|
|
cmd := exec.CommandContext(ctx, Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd)
|
|
|
|
cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
|
|
|
|
|
2020-07-24 21:39:38 +00:00
|
|
|
startTime := time.Now()
|
|
|
|
err := cmd.Run()
|
2020-10-19 23:46:51 +00:00
|
|
|
|
|
|
|
logger.Debug(notification.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
|
|
|
|
Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd, time.Since(startTime), err)
|
|
|
|
|
2020-07-24 21:39:38 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-02-16 18:11:36 +00:00
|
|
|
func notificationAsEnvVars(notification *ActionNotification) []string {
|
2020-10-19 23:46:51 +00:00
|
|
|
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_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),
|
2021-05-31 20:33:23 +00:00
|
|
|
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", notification.OpenFlags),
|
2020-07-24 21:39:38 +00:00
|
|
|
}
|
|
|
|
}
|