mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 14:50:55 +03:00
@@ -503,7 +503,7 @@ func AddUser(p Provider, user User) error {
|
|||||||
}
|
}
|
||||||
err := p.addUser(user)
|
err := p.addUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
go executeAction(operationAdd, user) //nolint:errcheck // the error is used in test cases only
|
go executeAction(operationAdd, user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -516,7 +516,7 @@ func UpdateUser(p Provider, user User) error {
|
|||||||
}
|
}
|
||||||
err := p.updateUser(user)
|
err := p.updateUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
go executeAction(operationUpdate, user) //nolint:errcheck // the error is used in test cases only
|
go executeAction(operationUpdate, user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -529,7 +529,7 @@ func DeleteUser(p Provider, user User) error {
|
|||||||
}
|
}
|
||||||
err := p.deleteUser(user)
|
err := p.deleteUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
go executeAction(operationDelete, user) //nolint:errcheck // the error is used in test cases only
|
go executeAction(operationDelete, user)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ 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 `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action.
|
||||||
|
|
||||||
If the `hook` defines a path to an external program, then this program 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`, `pre-delete`,`delete`, `rename`, `ssh_cmd`
|
||||||
- `username`
|
- `username`
|
||||||
- `path` is the full filesystem path, can be empty for some ssh commands
|
- `path` is the full filesystem path, can be empty for some ssh commands
|
||||||
- `target_path`, non-empty for `rename` action
|
- `target_path`, non-empty for `rename` action
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ The configuration file contains the following sections:
|
|||||||
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
|
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
|
||||||
- `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload.
|
- `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload.
|
||||||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See the "Custom Actions" paragraph for more details
|
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See the "Custom Actions" paragraph for more details
|
||||||
- `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
|
- `execute_on`, list of strings. Valid values are `download`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
|
||||||
- `command`, string. Absolute path to the command to execute. Leave empty to disable.
|
- `command`, string. Deprecated please use `hook`.
|
||||||
- `http_notification_url`, a valid URL. An HTTP GET request will be executed to this URL. Leave empty to disable.
|
- `http_notification_url`, a valid URL. Deprecated please use `hook`.
|
||||||
|
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||||
- `keys`, struct array. Deprecated, please use `host_keys`.
|
- `keys`, struct array. Deprecated, please use `host_keys`.
|
||||||
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
||||||
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty or missing, the daemon will search or try to generate `id_rsa` and `id_ecdsa` keys inside the configuration directory.
|
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty or missing, the daemon will search or try to generate `id_rsa` and `id_ecdsa` keys inside the configuration directory.
|
||||||
@@ -93,8 +94,9 @@ The configuration file contains the following sections:
|
|||||||
- `users_base_dir`, string. Users default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
|
- `users_base_dir`, string. Users default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
|
||||||
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See the "Custom Actions" paragraph for more details
|
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See the "Custom Actions" paragraph for more details
|
||||||
- `execute_on`, list of strings. Valid values are `add`, `update`, `delete`. `update` action will not be fired for internal updates such as the last login or the user quota fields.
|
- `execute_on`, list of strings. Valid values are `add`, `update`, `delete`. `update` action will not be fired for internal updates such as the last login or the user quota fields.
|
||||||
- `command`, string. Absolute path to the command to execute. Leave empty to disable.
|
- `command`, string. Deprecated please use `hook`.
|
||||||
- `http_notification_url`, a valid URL. Leave empty to disable.
|
- `http_notification_url`, a valid URL. Deprecated please use `hook`.
|
||||||
|
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||||
- `external_auth_program`, string. Deprecated, please use `external_auth_hook`.
|
- `external_auth_program`, string. Deprecated, please use `external_auth_hook`.
|
||||||
- `external_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for users authentication. See the "External Authentication" paragraph for more details. Leave empty to disable.
|
- `external_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for users authentication. See the "External Authentication" paragraph for more details. Leave empty to disable.
|
||||||
- `external_auth_scope`, integer. 0 means all supported authetication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. The flags can be combined, for example 6 means public keys and keyboard interactive
|
- `external_auth_scope`, integer. 0 means all supported authetication scopes (passwords, public keys and keyboard interactive). 1 means passwords only. 2 means public keys only. 4 means key keyboard interactive only. The flags can be combined, for example 6 means public keys and keyboard interactive
|
||||||
|
|||||||
@@ -425,9 +425,14 @@ func (c Connection) handleSFTPRemove(filePath string, request *sftp.Request) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
size = fi.Size()
|
size = fi.Size()
|
||||||
if err := c.fs.Remove(filePath, false); err != nil {
|
actionErr := executeAction(newActionNotification(c.User, operationPreDelete, filePath, "", "", fi.Size(), nil))
|
||||||
c.Log(logger.LevelWarn, logSender, "failed to remove a file/symlink %#v: %+v", filePath, err)
|
if actionErr == nil {
|
||||||
return vfs.GetSFTPError(c.fs, err)
|
c.Log(logger.LevelDebug, logSender, "remove for path %#v handled by pre-delete action", filePath)
|
||||||
|
} else {
|
||||||
|
if err := c.fs.Remove(filePath, false); err != nil {
|
||||||
|
c.Log(logger.LevelWarn, logSender, "failed to remove a file/symlink %#v: %+v", filePath, err)
|
||||||
|
return vfs.GetSFTPError(c.fs, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.CommandLog(removeLogSender, filePath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "")
|
logger.CommandLog(removeLogSender, filePath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "")
|
||||||
@@ -436,7 +441,9 @@ func (c Connection) handleSFTPRemove(filePath string, request *sftp.Request) err
|
|||||||
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false) //nolint:errcheck
|
dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false) //nolint:errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size(), nil)) //nolint:errcheck
|
if actionErr != nil {
|
||||||
|
go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size(), nil)) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
return sftp.ErrSSHFxOk
|
return sftp.ErrSSHFxOk
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -187,6 +188,7 @@ func TestWrongActions(t *testing.T) {
|
|||||||
|
|
||||||
func TestActionHTTP(t *testing.T) {
|
func TestActionHTTP(t *testing.T) {
|
||||||
actionsCopy := actions
|
actionsCopy := actions
|
||||||
|
|
||||||
actions = Actions{
|
actions = Actions{
|
||||||
ExecuteOn: []string{operationDownload},
|
ExecuteOn: []string{operationDownload},
|
||||||
Hook: "http://127.0.0.1:8080/",
|
Hook: "http://127.0.0.1:8080/",
|
||||||
@@ -195,7 +197,42 @@ func TestActionHTTP(t *testing.T) {
|
|||||||
Username: "username",
|
Username: "username",
|
||||||
}
|
}
|
||||||
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
|
err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
|
||||||
|
assert.EqualError(t, err, errUnexpectedHTTResponse.Error())
|
||||||
|
|
||||||
|
actions = actionsCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreDeleteAction(t *testing.T) {
|
||||||
|
actionsCopy := actions
|
||||||
|
|
||||||
|
hookCmd, err := exec.LookPath("true")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
actions = Actions{
|
||||||
|
ExecuteOn: []string{operationPreDelete},
|
||||||
|
Hook: hookCmd,
|
||||||
|
}
|
||||||
|
homeDir := filepath.Join(os.TempDir(), "test_user")
|
||||||
|
err = os.MkdirAll(homeDir, 0777)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
user := dataprovider.User{
|
||||||
|
Username: "username",
|
||||||
|
HomeDir: homeDir,
|
||||||
|
}
|
||||||
|
user.Permissions = make(map[string][]string)
|
||||||
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
|
c := Connection{
|
||||||
|
fs: vfs.NewOsFs("id", homeDir, nil),
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
testfile := filepath.Join(user.HomeDir, "testfile")
|
||||||
|
request := sftp.NewRequest("Remove", "/testfile")
|
||||||
|
err = ioutil.WriteFile(testfile, []byte("test"), 0666)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = c.handleSFTPRemove(testfile, request)
|
||||||
|
assert.EqualError(t, err, sftp.ErrSSHFxOk.Error())
|
||||||
|
assert.FileExists(t, testfile)
|
||||||
|
|
||||||
|
os.RemoveAll(homeDir)
|
||||||
|
|
||||||
actions = actionsCopy
|
actions = actionsCopy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -42,6 +43,7 @@ const (
|
|||||||
operationDownload = "download"
|
operationDownload = "download"
|
||||||
operationUpload = "upload"
|
operationUpload = "upload"
|
||||||
operationDelete = "delete"
|
operationDelete = "delete"
|
||||||
|
operationPreDelete = "pre-delete"
|
||||||
operationRename = "rename"
|
operationRename = "rename"
|
||||||
operationSSHCmd = "ssh_cmd"
|
operationSSHCmd = "ssh_cmd"
|
||||||
protocolSFTP = "SFTP"
|
protocolSFTP = "SFTP"
|
||||||
@@ -68,11 +70,12 @@ 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")
|
errUnconfiguredAction = errors.New("no hook is configured for this action")
|
||||||
errNoHook = errors.New("unable to execute action, no hook defined")
|
errNoHook = errors.New("unable to execute action, no hook defined")
|
||||||
|
errUnexpectedHTTResponse = errors.New("unexpected HTTP response code")
|
||||||
)
|
)
|
||||||
|
|
||||||
type connectionTransfer struct {
|
type connectionTransfer struct {
|
||||||
@@ -496,7 +499,6 @@ func executeNotificationCommand(a actionNotification) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 errUnconfiguredAction
|
return errUnconfiguredAction
|
||||||
@@ -519,6 +521,9 @@ func executeAction(a actionNotification) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
respCode = resp.StatusCode
|
respCode = resp.StatusCode
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
if respCode != http.StatusOK {
|
||||||
|
err = errUnexpectedHTTResponse
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ var (
|
|||||||
scpPath string
|
scpPath string
|
||||||
gitPath string
|
gitPath string
|
||||||
sshPath string
|
sshPath string
|
||||||
|
hookCmdPath string
|
||||||
pubKeyPath string
|
pubKeyPath string
|
||||||
privateKeyPath string
|
privateKeyPath string
|
||||||
trustedCAUserKey string
|
trustedCAUserKey string
|
||||||
@@ -134,7 +135,6 @@ func TestMain(m *testing.M) {
|
|||||||
err := ioutil.WriteFile(loginBannerFile, []byte("simple login banner\n"), 0777)
|
err := ioutil.WriteFile(loginBannerFile, []byte("simple login banner\n"), 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnToConsole("error creating login banner: %v", err)
|
logger.WarnToConsole("error creating login banner: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
err = config.LoadConfig(configDir, "")
|
err = config.LoadConfig(configDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,21 +169,15 @@ func TestMain(m *testing.M) {
|
|||||||
// work in non atomic mode too
|
// work in non atomic mode too
|
||||||
sftpdConf.UploadMode = 2
|
sftpdConf.UploadMode = 2
|
||||||
homeBasePath = os.TempDir()
|
homeBasePath = os.TempDir()
|
||||||
|
checkSystemCommands()
|
||||||
var scriptArgs string
|
var scriptArgs string
|
||||||
if runtime.GOOS == osWindows {
|
if runtime.GOOS == osWindows {
|
||||||
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.Hook = "/bin/true"
|
sftpdConf.Actions.Hook = hookCmdPath
|
||||||
scriptArgs = "$@"
|
scriptArgs = "$@"
|
||||||
scpPath, err = exec.LookPath("scp")
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn(logSender, "", "unable to get scp command. SCP tests will be skipped, err: %v", err)
|
|
||||||
logger.WarnToConsole("unable to get scp command. SCP tests will be skipped, err: %v", err)
|
|
||||||
scpPath = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
checkGitCommand()
|
|
||||||
|
|
||||||
keyIntAuthPath = filepath.Join(homeBasePath, "keyintauth.sh")
|
keyIntAuthPath = filepath.Join(homeBasePath, "keyintauth.sh")
|
||||||
err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), 0755)
|
err = ioutil.WriteFile(keyIntAuthPath, getKeyboardInteractiveScriptContent([]string{"1", "2"}, 0, false, 1), 0755)
|
||||||
@@ -4447,7 +4441,7 @@ func waitQuotaScans() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGitCommand() {
|
func checkSystemCommands() {
|
||||||
var err error
|
var err error
|
||||||
gitPath, err = exec.LookPath("git")
|
gitPath, err = exec.LookPath("git")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4462,6 +4456,21 @@ func checkGitCommand() {
|
|||||||
logger.WarnToConsole("unable to get ssh command. GIT tests will be skipped, err: %v", err)
|
logger.WarnToConsole("unable to get ssh command. GIT tests will be skipped, err: %v", err)
|
||||||
gitPath = ""
|
gitPath = ""
|
||||||
}
|
}
|
||||||
|
hookCmdPath, err = exec.LookPath("true")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(logSender, "", "unable to get hook command: %v", err)
|
||||||
|
logger.WarnToConsole("unable to get hook command: %v", err)
|
||||||
|
}
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
scpPath = ""
|
||||||
|
} else {
|
||||||
|
scpPath, err = exec.LookPath("scp")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(logSender, "", "unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||||
|
logger.WarnToConsole("unable to get scp command. SCP tests will be skipped, err: %v", err)
|
||||||
|
scpPath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGitRepo(path string) ([]byte, error) {
|
func initGitRepo(path string) ([]byte, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user