actions.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package common
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "strings"
  14. "time"
  15. "github.com/drakkan/sftpgo/dataprovider"
  16. "github.com/drakkan/sftpgo/httpclient"
  17. "github.com/drakkan/sftpgo/logger"
  18. "github.com/drakkan/sftpgo/utils"
  19. )
  20. var (
  21. errUnconfiguredAction = errors.New("no hook is configured for this action")
  22. errNoHook = errors.New("unable to execute action, no hook defined")
  23. errUnexpectedHTTResponse = errors.New("unexpected HTTP response code")
  24. )
  25. // ProtocolActions defines the action to execute on file operations and SSH commands
  26. type ProtocolActions struct {
  27. // Valid values are download, upload, pre-delete, delete, rename, ssh_cmd. Empty slice to disable
  28. ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
  29. // Absolute path to an external program or an HTTP URL
  30. Hook string `json:"hook" mapstructure:"hook"`
  31. }
  32. // actionNotification defines a notification for a Protocol Action
  33. type actionNotification struct {
  34. Action string `json:"action"`
  35. Username string `json:"username"`
  36. Path string `json:"path"`
  37. TargetPath string `json:"target_path,omitempty"`
  38. SSHCmd string `json:"ssh_cmd,omitempty"`
  39. FileSize int64 `json:"file_size,omitempty"`
  40. FsProvider int `json:"fs_provider"`
  41. Bucket string `json:"bucket,omitempty"`
  42. Endpoint string `json:"endpoint,omitempty"`
  43. Status int `json:"status"`
  44. Protocol string `json:"protocol"`
  45. }
  46. // SSHCommandActionNotification executes the defined action for the specified SSH command
  47. func SSHCommandActionNotification(user *dataprovider.User, filePath, target, sshCmd string, err error) {
  48. action := newActionNotification(user, operationSSHCmd, filePath, target, sshCmd, ProtocolSSH, 0, err)
  49. go action.execute() //nolint:errcheck
  50. }
  51. func newActionNotification(user *dataprovider.User, operation, filePath, target, sshCmd, protocol string, fileSize int64,
  52. err error) actionNotification {
  53. bucket := ""
  54. endpoint := ""
  55. status := 1
  56. if user.FsConfig.Provider == 1 {
  57. bucket = user.FsConfig.S3Config.Bucket
  58. endpoint = user.FsConfig.S3Config.Endpoint
  59. } else if user.FsConfig.Provider == 2 {
  60. bucket = user.FsConfig.GCSConfig.Bucket
  61. }
  62. if err == ErrQuotaExceeded {
  63. status = 2
  64. } else if err != nil {
  65. status = 0
  66. }
  67. return actionNotification{
  68. Action: operation,
  69. Username: user.Username,
  70. Path: filePath,
  71. TargetPath: target,
  72. SSHCmd: sshCmd,
  73. FileSize: fileSize,
  74. FsProvider: user.FsConfig.Provider,
  75. Bucket: bucket,
  76. Endpoint: endpoint,
  77. Status: status,
  78. Protocol: protocol,
  79. }
  80. }
  81. func (a *actionNotification) asJSON() []byte {
  82. res, _ := json.Marshal(a)
  83. return res
  84. }
  85. func (a *actionNotification) asEnvVars() []string {
  86. return []string{fmt.Sprintf("SFTPGO_ACTION=%v", a.Action),
  87. fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", a.Username),
  88. fmt.Sprintf("SFTPGO_ACTION_PATH=%v", a.Path),
  89. fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", a.TargetPath),
  90. fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", a.SSHCmd),
  91. fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", a.FileSize),
  92. fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", a.FsProvider),
  93. fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", a.Bucket),
  94. fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", a.Endpoint),
  95. fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", a.Status),
  96. fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", a.Protocol),
  97. }
  98. }
  99. func (a *actionNotification) executeNotificationCommand() error {
  100. if !filepath.IsAbs(Config.Actions.Hook) {
  101. err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
  102. logger.Warn(a.Protocol, "", "unable to execute notification command: %v", err)
  103. return err
  104. }
  105. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  106. defer cancel()
  107. cmd := exec.CommandContext(ctx, Config.Actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd)
  108. cmd.Env = append(os.Environ(), a.asEnvVars()...)
  109. startTime := time.Now()
  110. err := cmd.Run()
  111. logger.Debug(a.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
  112. Config.Actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd, time.Since(startTime), err)
  113. return err
  114. }
  115. func (a *actionNotification) execute() error {
  116. if !utils.IsStringInSlice(a.Action, Config.Actions.ExecuteOn) {
  117. return errUnconfiguredAction
  118. }
  119. if len(Config.Actions.Hook) == 0 {
  120. logger.Warn(a.Protocol, "", "Unable to send notification, no hook is defined")
  121. return errNoHook
  122. }
  123. if strings.HasPrefix(Config.Actions.Hook, "http") {
  124. var url *url.URL
  125. url, err := url.Parse(Config.Actions.Hook)
  126. if err != nil {
  127. logger.Warn(a.Protocol, "", "Invalid hook %#v for operation %#v: %v", Config.Actions.Hook, a.Action, err)
  128. return err
  129. }
  130. startTime := time.Now()
  131. httpClient := httpclient.GetHTTPClient()
  132. resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(a.asJSON()))
  133. respCode := 0
  134. if err == nil {
  135. respCode = resp.StatusCode
  136. resp.Body.Close()
  137. if respCode != http.StatusOK {
  138. err = errUnexpectedHTTResponse
  139. }
  140. }
  141. logger.Debug(a.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
  142. a.Action, url.String(), respCode, time.Since(startTime), err)
  143. return err
  144. }
  145. return a.executeNotificationCommand()
  146. }