mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 09:00:27 +00:00
actions: add pre-download and pre-upload
Downloads and uploads can be denied based on hook response
This commit is contained in:
parent
600268ebb8
commit
25a44030f9
24 changed files with 710 additions and 176 deletions
|
@ -31,7 +31,7 @@ linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- golint
|
- revive
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
|
|
@ -49,9 +49,24 @@ func InitializeActionHandler(handler ActionHandler) {
|
||||||
actionHandler = handler
|
actionHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecutePreAction executes a pre-* action and returns the result
|
||||||
|
func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64) error {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
notification := newActionNotification(user, operation, filePath, virtualPath, "", "", protocol, fileSize, nil)
|
||||||
|
return actionHandler.Handle(notification)
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
// ExecuteActionNotification executes the defined hook, if any, for the specified action
|
||||||
func ExecuteActionNotification(user *dataprovider.User, operation, filePath, target, sshCmd, protocol string, fileSize int64, err error) {
|
func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) {
|
||||||
notification := newActionNotification(user, operation, filePath, target, sshCmd, protocol, fileSize, err)
|
notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, err)
|
||||||
|
|
||||||
if utils.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
if utils.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
|
||||||
actionHandler.Handle(notification) //nolint:errcheck
|
actionHandler.Handle(notification) //nolint:errcheck
|
||||||
|
@ -83,25 +98,30 @@ type ActionNotification struct {
|
||||||
|
|
||||||
func newActionNotification(
|
func newActionNotification(
|
||||||
user *dataprovider.User,
|
user *dataprovider.User,
|
||||||
operation, filePath, target, sshCmd, protocol string,
|
operation, filePath, virtualPath, target, sshCmd, protocol string,
|
||||||
fileSize int64,
|
fileSize int64,
|
||||||
err error,
|
err error,
|
||||||
) *ActionNotification {
|
) *ActionNotification {
|
||||||
var bucket, endpoint string
|
var bucket, endpoint string
|
||||||
status := 1
|
status := 1
|
||||||
|
|
||||||
if user.FsConfig.Provider == vfs.S3FilesystemProvider {
|
fsConfig := user.GetFsConfigForPath(virtualPath)
|
||||||
bucket = user.FsConfig.S3Config.Bucket
|
|
||||||
endpoint = user.FsConfig.S3Config.Endpoint
|
switch fsConfig.Provider {
|
||||||
} else if user.FsConfig.Provider == vfs.GCSFilesystemProvider {
|
case vfs.S3FilesystemProvider:
|
||||||
bucket = user.FsConfig.GCSConfig.Bucket
|
bucket = fsConfig.S3Config.Bucket
|
||||||
} else if user.FsConfig.Provider == vfs.AzureBlobFilesystemProvider {
|
endpoint = fsConfig.S3Config.Endpoint
|
||||||
bucket = user.FsConfig.AzBlobConfig.Container
|
case vfs.GCSFilesystemProvider:
|
||||||
if user.FsConfig.AzBlobConfig.SASURL != "" {
|
bucket = fsConfig.GCSConfig.Bucket
|
||||||
endpoint = user.FsConfig.AzBlobConfig.SASURL
|
case vfs.AzureBlobFilesystemProvider:
|
||||||
|
bucket = fsConfig.AzBlobConfig.Container
|
||||||
|
if fsConfig.AzBlobConfig.SASURL != "" {
|
||||||
|
endpoint = fsConfig.AzBlobConfig.SASURL
|
||||||
} else {
|
} else {
|
||||||
endpoint = user.FsConfig.AzBlobConfig.Endpoint
|
endpoint = fsConfig.AzBlobConfig.Endpoint
|
||||||
}
|
}
|
||||||
|
case vfs.SFTPFilesystemProvider:
|
||||||
|
endpoint = fsConfig.SFTPConfig.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == ErrQuotaExceeded {
|
if err == ErrQuotaExceeded {
|
||||||
|
@ -117,7 +137,7 @@ func newActionNotification(
|
||||||
TargetPath: target,
|
TargetPath: target,
|
||||||
SSHCmd: sshCmd,
|
SSHCmd: sshCmd,
|
||||||
FileSize: fileSize,
|
FileSize: fileSize,
|
||||||
FsProvider: int(user.FsConfig.Provider),
|
FsProvider: int(fsConfig.Provider),
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Endpoint: endpoint,
|
Endpoint: endpoint,
|
||||||
Status: status,
|
Status: status,
|
||||||
|
@ -168,8 +188,8 @@ 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,
|
logger.Debug(notification.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
||||||
u.Redacted(), respCode, time.Since(startTime), err)
|
notification.Action, u.Redacted(), respCode, time.Since(startTime), err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,35 +32,42 @@ func TestNewActionNotification(t *testing.T) {
|
||||||
SASURL: "azsasurl",
|
SASURL: "azsasurl",
|
||||||
Endpoint: "azendpoint",
|
Endpoint: "azendpoint",
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, errors.New("fake error"))
|
user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
|
||||||
|
Endpoint: "sftpendpoint",
|
||||||
|
}
|
||||||
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, errors.New("fake error"))
|
||||||
assert.Equal(t, user.Username, a.Username)
|
assert.Equal(t, user.Username, a.Username)
|
||||||
assert.Equal(t, 0, len(a.Bucket))
|
assert.Equal(t, 0, len(a.Bucket))
|
||||||
assert.Equal(t, 0, len(a.Endpoint))
|
assert.Equal(t, 0, len(a.Endpoint))
|
||||||
assert.Equal(t, 0, a.Status)
|
assert.Equal(t, 0, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = vfs.S3FilesystemProvider
|
user.FsConfig.Provider = vfs.S3FilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSSH, 123, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, nil)
|
||||||
assert.Equal(t, "s3bucket", a.Bucket)
|
assert.Equal(t, "s3bucket", a.Bucket)
|
||||||
assert.Equal(t, "endpoint", a.Endpoint)
|
assert.Equal(t, "endpoint", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = vfs.GCSFilesystemProvider
|
user.FsConfig.Provider = vfs.GCSFilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, ErrQuotaExceeded)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, ErrQuotaExceeded)
|
||||||
assert.Equal(t, "gcsbucket", a.Bucket)
|
assert.Equal(t, "gcsbucket", a.Bucket)
|
||||||
assert.Equal(t, 0, len(a.Endpoint))
|
assert.Equal(t, 0, len(a.Endpoint))
|
||||||
assert.Equal(t, 2, a.Status)
|
assert.Equal(t, 2, a.Status)
|
||||||
|
|
||||||
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider
|
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider
|
||||||
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, nil)
|
||||||
assert.Equal(t, "azcontainer", a.Bucket)
|
assert.Equal(t, "azcontainer", a.Bucket)
|
||||||
assert.Equal(t, "azsasurl", a.Endpoint)
|
assert.Equal(t, "azsasurl", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
|
|
||||||
user.FsConfig.AzBlobConfig.SASURL = ""
|
user.FsConfig.AzBlobConfig.SASURL = ""
|
||||||
a = newActionNotification(user, operationDownload, "path", "target", "", ProtocolSCP, 123, nil)
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, nil)
|
||||||
assert.Equal(t, "azcontainer", a.Bucket)
|
assert.Equal(t, "azcontainer", a.Bucket)
|
||||||
assert.Equal(t, "azendpoint", a.Endpoint)
|
assert.Equal(t, "azendpoint", a.Endpoint)
|
||||||
assert.Equal(t, 1, a.Status)
|
assert.Equal(t, 1, a.Status)
|
||||||
|
|
||||||
|
user.FsConfig.Provider = vfs.SFTPFilesystemProvider
|
||||||
|
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil)
|
||||||
|
assert.Equal(t, "sftpendpoint", a.Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActionHTTP(t *testing.T) {
|
func TestActionHTTP(t *testing.T) {
|
||||||
|
@ -73,7 +80,7 @@ func TestActionHTTP(t *testing.T) {
|
||||||
user := &dataprovider.User{
|
user := &dataprovider.User{
|
||||||
Username: "username",
|
Username: "username",
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, nil)
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil)
|
||||||
err := actionHandler.Handle(a)
|
err := actionHandler.Handle(a)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -106,11 +113,11 @@ func TestActionCMD(t *testing.T) {
|
||||||
user := &dataprovider.User{
|
user := &dataprovider.User{
|
||||||
Username: "username",
|
Username: "username",
|
||||||
}
|
}
|
||||||
a := newActionNotification(user, operationDownload, "path", "target", "", ProtocolSFTP, 123, nil)
|
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil)
|
||||||
err = actionHandler.Handle(a)
|
err = actionHandler.Handle(a)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
ExecuteActionNotification(user, operationSSHCmd, "path", "target", "sha1sum", ProtocolSSH, 0, nil)
|
ExecuteActionNotification(user, OperationSSHCmd, "path", "vpath", "target", "sha1sum", ProtocolSSH, 0, nil)
|
||||||
|
|
||||||
Config.Actions = actionsCopy
|
Config.Actions = actionsCopy
|
||||||
}
|
}
|
||||||
|
@ -130,7 +137,7 @@ func TestWrongActions(t *testing.T) {
|
||||||
Username: "username",
|
Username: "username",
|
||||||
}
|
}
|
||||||
|
|
||||||
a := newActionNotification(user, operationUpload, "", "", "", ProtocolSFTP, 123, nil)
|
a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, nil)
|
||||||
err := actionHandler.Handle(a)
|
err := actionHandler.Handle(a)
|
||||||
assert.Error(t, err, "action with bad command must fail")
|
assert.Error(t, err, "action with bad command must fail")
|
||||||
|
|
||||||
|
|
|
@ -27,24 +27,29 @@ import (
|
||||||
|
|
||||||
// constants
|
// constants
|
||||||
const (
|
const (
|
||||||
logSender = "common"
|
logSender = "common"
|
||||||
uploadLogSender = "Upload"
|
uploadLogSender = "Upload"
|
||||||
downloadLogSender = "Download"
|
downloadLogSender = "Download"
|
||||||
renameLogSender = "Rename"
|
renameLogSender = "Rename"
|
||||||
rmdirLogSender = "Rmdir"
|
rmdirLogSender = "Rmdir"
|
||||||
mkdirLogSender = "Mkdir"
|
mkdirLogSender = "Mkdir"
|
||||||
symlinkLogSender = "Symlink"
|
symlinkLogSender = "Symlink"
|
||||||
removeLogSender = "Remove"
|
removeLogSender = "Remove"
|
||||||
chownLogSender = "Chown"
|
chownLogSender = "Chown"
|
||||||
chmodLogSender = "Chmod"
|
chmodLogSender = "Chmod"
|
||||||
chtimesLogSender = "Chtimes"
|
chtimesLogSender = "Chtimes"
|
||||||
truncateLogSender = "Truncate"
|
truncateLogSender = "Truncate"
|
||||||
operationDownload = "download"
|
operationDownload = "download"
|
||||||
operationUpload = "upload"
|
operationUpload = "upload"
|
||||||
operationDelete = "delete"
|
operationDelete = "delete"
|
||||||
operationPreDelete = "pre-delete"
|
// Pre-download action name
|
||||||
operationRename = "rename"
|
OperationPreDownload = "pre-download"
|
||||||
operationSSHCmd = "ssh_cmd"
|
// Pre-upload action name
|
||||||
|
OperationPreUpload = "pre-upload"
|
||||||
|
operationPreDelete = "pre-delete"
|
||||||
|
operationRename = "rename"
|
||||||
|
// SSH command action name
|
||||||
|
OperationSSHCmd = "ssh_cmd"
|
||||||
chtimesFormat = "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS
|
chtimesFormat = "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS
|
||||||
idleTimeoutCheckInterval = 3 * time.Minute
|
idleTimeoutCheckInterval = 3 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
|
@ -261,8 +261,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
||||||
}
|
}
|
||||||
|
|
||||||
size := info.Size()
|
size := info.Size()
|
||||||
action := newActionNotification(&c.User, operationPreDelete, fsPath, "", "", c.protocol, size, nil)
|
actionErr := ExecutePreAction(&c.User, operationPreDelete, fsPath, virtualPath, c.protocol, size)
|
||||||
actionErr := actionHandler.Handle(action)
|
|
||||||
if actionErr == nil {
|
if actionErr == nil {
|
||||||
c.Log(logger.LevelDebug, "remove for path %#v handled by pre-delete action", fsPath)
|
c.Log(logger.LevelDebug, "remove for path %#v handled by pre-delete action", fsPath)
|
||||||
} else {
|
} else {
|
||||||
|
@ -285,7 +284,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if actionErr != nil {
|
if actionErr != nil {
|
||||||
ExecuteActionNotification(&c.User, operationDelete, fsPath, "", "", c.protocol, size, nil)
|
ExecuteActionNotification(&c.User, operationDelete, fsPath, virtualPath, "", "", c.protocol, size, nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -404,7 +403,7 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
|
||||||
c.updateQuotaAfterRename(fsDst, virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize) //nolint:errcheck
|
c.updateQuotaAfterRename(fsDst, virtualSourcePath, virtualTargetPath, fsTargetPath, initialSize) //nolint:errcheck
|
||||||
logger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
|
logger.CommandLog(renameLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
|
||||||
"", "", "", -1)
|
"", "", "", -1)
|
||||||
ExecuteActionNotification(&c.User, operationRename, fsSourcePath, fsTargetPath, "", c.protocol, 0, nil)
|
ExecuteActionNotification(&c.User, operationRename, fsSourcePath, virtualSourcePath, fsTargetPath, "", c.protocol, 0, nil)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,7 +235,7 @@ func (t *BaseTransfer) Close() error {
|
||||||
if t.transferType == TransferDownload {
|
if t.transferType == TransferDownload {
|
||||||
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
|
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesSent), t.Connection.User.Username,
|
||||||
t.Connection.ID, t.Connection.protocol)
|
t.Connection.ID, t.Connection.protocol)
|
||||||
ExecuteActionNotification(&t.Connection.User, operationDownload, t.fsPath, "", "", t.Connection.protocol,
|
ExecuteActionNotification(&t.Connection.User, operationDownload, t.fsPath, t.requestPath, "", "", t.Connection.protocol,
|
||||||
atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
atomic.LoadInt64(&t.BytesSent), t.ErrTransfer)
|
||||||
} else {
|
} else {
|
||||||
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
fileSize := atomic.LoadInt64(&t.BytesReceived) + t.MinWriteOffset
|
||||||
|
@ -246,7 +246,7 @@ func (t *BaseTransfer) Close() error {
|
||||||
t.updateQuota(numFiles, fileSize)
|
t.updateQuota(numFiles, fileSize)
|
||||||
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
|
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, atomic.LoadInt64(&t.BytesReceived), t.Connection.User.Username,
|
||||||
t.Connection.ID, t.Connection.protocol)
|
t.Connection.ID, t.Connection.protocol)
|
||||||
ExecuteActionNotification(&t.Connection.User, operationUpload, t.fsPath, "", "", t.Connection.protocol, fileSize,
|
ExecuteActionNotification(&t.Connection.User, operationUpload, t.fsPath, t.requestPath, "", "", t.Connection.protocol, fileSize,
|
||||||
t.ErrTransfer)
|
t.ErrTransfer)
|
||||||
}
|
}
|
||||||
if t.ErrTransfer != nil {
|
if t.ErrTransfer != nil {
|
||||||
|
|
|
@ -1560,10 +1560,7 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||||
if err := validateFilesystemConfig(&folder.FsConfig, folder); err != nil {
|
if err := validateFilesystemConfig(&folder.FsConfig, folder); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := saveGCSCredentials(&folder.FsConfig, folder); err != nil {
|
return saveGCSCredentials(&folder.FsConfig, folder)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateUser returns an error if the user is not valid
|
// ValidateUser returns an error if the user is not valid
|
||||||
|
@ -1598,10 +1595,7 @@ func ValidateUser(user *User) error {
|
||||||
if err := validateFilters(user); err != nil {
|
if err := validateFilters(user); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := saveGCSCredentials(&user.FsConfig, user); err != nil {
|
return saveGCSCredentials(&user.FsConfig, user)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkLoginConditions(user *User) error {
|
func checkLoginConditions(user *User) error {
|
||||||
|
|
|
@ -517,6 +517,18 @@ func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFsConfigForPath returns the file system configuration for the specified virtual path
|
||||||
|
func (u *User) GetFsConfigForPath(virtualPath string) vfs.Filesystem {
|
||||||
|
if virtualPath != "" && virtualPath != "/" && len(u.VirtualFolders) > 0 {
|
||||||
|
folder, err := u.GetVirtualFolderForPath(virtualPath)
|
||||||
|
if err == nil {
|
||||||
|
return folder.FsConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.FsConfig
|
||||||
|
}
|
||||||
|
|
||||||
// GetFilesystemForPath returns the filesystem for the given path
|
// GetFilesystemForPath returns the filesystem for the given path
|
||||||
func (u *User) GetFilesystemForPath(virtualPath, connectionID string) (vfs.Fs, error) {
|
func (u *User) GetFilesystemForPath(virtualPath, connectionID string) (vfs.Fs, error) {
|
||||||
if u.fsCache == nil {
|
if u.fsCache == nil {
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
# Custom Actions
|
# Custom Actions
|
||||||
|
|
||||||
The `actions` struct inside the "common" configuration section allows to configure the actions for file operations and SSH commands.
|
The `actions` struct inside the `common` 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 `hook` can be defined as the absolute path of your program or an HTTP URL.
|
||||||
|
|
||||||
|
The following `actions` are supported:
|
||||||
|
|
||||||
|
- `download`
|
||||||
|
- `pre-download`
|
||||||
|
- `upload`
|
||||||
|
- `pre-upload`
|
||||||
|
- `delete`
|
||||||
|
- `pre-delete`
|
||||||
|
- `rename`
|
||||||
|
- `ssh_cmd`
|
||||||
|
|
||||||
The `upload` condition includes both uploads to new files and overwrite of existing files. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. 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. If an upload is aborted for quota limits SFTPGo tries to remove the partial file, so if the notification reports a zero size file and a quota exceeded error the file has been deleted. 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.
|
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.
|
||||||
|
|
||||||
|
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo allows the operation, otherwise the client will get a permission denied error.
|
||||||
|
|
||||||
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`, `pre-delete`,`delete`, `rename`, `ssh_cmd`
|
- `action`, string, supported action
|
||||||
- `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 and for `sftpgo-copy` SSH command
|
- `target_path`, non-empty for `rename` action and for `sftpgo-copy` SSH command
|
||||||
|
@ -22,12 +37,12 @@ The external program can also read the following environment variables:
|
||||||
- `SFTPGO_ACTION_PATH`
|
- `SFTPGO_ACTION_PATH`
|
||||||
- `SFTPGO_ACTION_TARGET`, non-empty for `rename` `SFTPGO_ACTION`
|
- `SFTPGO_ACTION_TARGET`, non-empty for `rename` `SFTPGO_ACTION`
|
||||||
- `SFTPGO_ACTION_SSH_CMD`, non-empty for `ssh_cmd` `SFTPGO_ACTION`
|
- `SFTPGO_ACTION_SSH_CMD`, non-empty for `ssh_cmd` `SFTPGO_ACTION`
|
||||||
- `SFTPGO_ACTION_FILE_SIZE`, non-empty for `upload`, `download` and `delete` `SFTPGO_ACTION`
|
- `SFTPGO_ACTION_FILE_SIZE`, non-zero for `pre-upload`,`upload`, `download` and `delete` actions if the file size is greater than `0`
|
||||||
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend
|
- `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
||||||
- `SFTPGO_ACTION_BUCKET`, non-empty for S3, GCS and Azure backends
|
- `SFTPGO_ACTION_BUCKET`, non-empty for S3, GCS and Azure backends
|
||||||
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint
|
- `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint
|
||||||
- `SFTPGO_ACTION_STATUS`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
||||||
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`
|
- `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`
|
||||||
|
|
||||||
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 program must finish within 30 seconds.
|
The program must finish within 30 seconds.
|
||||||
|
@ -37,18 +52,18 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th
|
||||||
- `action`
|
- `action`
|
||||||
- `username`
|
- `username`
|
||||||
- `path`
|
- `path`
|
||||||
- `target_path`, not null for `rename` action
|
- `target_path`, included for `rename` action
|
||||||
- `ssh_cmd`, not null for `ssh_cmd` action
|
- `ssh_cmd`, included for `ssh_cmd` action
|
||||||
- `file_size`, not null for `upload`, `download`, `delete` actions
|
- `file_size`, included for `pre-upload`, `upload`, `download`, `delete` actions if the file size is greater than `0`
|
||||||
- `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend
|
- `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend
|
||||||
- `bucket`, not null for S3, GCS and Azure backends
|
- `bucket`, inlcuded for S3, GCS and Azure backends
|
||||||
- `endpoint`, not null for S3 and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint
|
- `endpoint`, included for S3, SFTP and Azure backend if configured. For Azure this is the SAS URL, if configured, otherwise the endpoint
|
||||||
- `status`, integer. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error
|
||||||
- `protocol`, string. Possible values are `SSH`, `FTP`, `DAV`
|
- `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP`
|
||||||
|
|
||||||
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations.
|
||||||
|
|
||||||
The `pre-delete` action is always executed synchronously while the other ones are asynchronous. You can specify the actions to run synchronously via the `execute_sync` configuration key. 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. If your hook takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
|
The `pre-*` actions are always executed synchronously while the other ones are asynchronous. You can specify the actions to run synchronously via the `execute_sync` configuration key. 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. If your hook takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
|
||||||
|
|
||||||
The `actions` struct inside the `data_provider` configuration section allows you to configure actions on user add, update, delete.
|
The `actions` struct inside the `data_provider` configuration section allows you to configure actions on user add, update, delete.
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,8 @@ var (
|
||||||
extAuthPath string
|
extAuthPath string
|
||||||
preLoginPath string
|
preLoginPath string
|
||||||
postConnectPath string
|
postConnectPath string
|
||||||
|
preDownloadPath string
|
||||||
|
preUploadPath string
|
||||||
logFilePath string
|
logFilePath string
|
||||||
caCrtPath string
|
caCrtPath string
|
||||||
caCRLPath string
|
caCRLPath string
|
||||||
|
@ -333,6 +335,8 @@ func TestMain(m *testing.M) {
|
||||||
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
||||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||||
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
||||||
|
preDownloadPath = filepath.Join(homeBasePath, "predownload.sh")
|
||||||
|
preUploadPath = filepath.Join(homeBasePath, "preupload.sh")
|
||||||
|
|
||||||
status := ftpd.GetStatus()
|
status := ftpd.GetStatus()
|
||||||
if status.IsActive {
|
if status.IsActive {
|
||||||
|
@ -401,6 +405,8 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(extAuthPath)
|
os.Remove(extAuthPath)
|
||||||
os.Remove(preLoginPath)
|
os.Remove(preLoginPath)
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
|
os.Remove(preDownloadPath)
|
||||||
|
os.Remove(preUploadPath)
|
||||||
os.Remove(certPath)
|
os.Remove(certPath)
|
||||||
os.Remove(keyPath)
|
os.Remove(keyPath)
|
||||||
os.Remove(caCrtPath)
|
os.Remove(caCrtPath)
|
||||||
|
@ -707,6 +713,133 @@ func TestPreLoginHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreDownloadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
common.Config.Actions.Hook = preDownloadPath
|
||||||
|
|
||||||
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := getFTPClient(user, true, nil)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err := client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
// now return an error from the pre-download hook
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
client, err = getFTPClient(user, true, nil)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "permission denied")
|
||||||
|
}
|
||||||
|
err := client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreUploadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}
|
||||||
|
common.Config.Actions.Hook = preUploadPath
|
||||||
|
|
||||||
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := getFTPClient(user, true, nil)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err := client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
// now return an error from the pre-upload hook
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
client, err = getFTPClient(user, true, nil)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "permission denied")
|
||||||
|
}
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName+"1", testFileSize, client, 0)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "permission denied")
|
||||||
|
}
|
||||||
|
err := client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostConnectHook(t *testing.T) {
|
func TestPostConnectHook(t *testing.T) {
|
||||||
if runtime.GOOS == osWindows {
|
if runtime.GOOS == osWindows {
|
||||||
t.Skip("this test is not available on Windows")
|
t.Skip("this test is not available on Windows")
|
||||||
|
@ -716,7 +849,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client, err := getFTPClient(user, true, nil)
|
client, err := getFTPClient(user, true, nil)
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
|
@ -725,7 +858,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
err := client.Quit()
|
err := client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client, err = getFTPClient(user, true, nil)
|
client, err = getFTPClient(user, true, nil)
|
||||||
if !assert.Error(t, err) {
|
if !assert.Error(t, err) {
|
||||||
|
@ -2898,7 +3031,7 @@ func getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []by
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPostConnectScriptContent(exitCode int) []byte {
|
func getExitCodeScriptContent(exitCode int) []byte {
|
||||||
content := []byte("#!/bin/sh\n\n")
|
content := []byte("#!/bin/sh\n\n")
|
||||||
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
||||||
return content
|
return content
|
||||||
|
|
|
@ -113,11 +113,7 @@ func (c *Connection) RemoveAll(path string) error {
|
||||||
func (c *Connection) Rename(oldname, newname string) error {
|
func (c *Connection) Rename(oldname, newname string) error {
|
||||||
c.UpdateLastActivity()
|
c.UpdateLastActivity()
|
||||||
|
|
||||||
if err := c.BaseConnection.Rename(oldname, newname); err != nil {
|
return c.BaseConnection.Rename(oldname, newname)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the named file/directory, or an error,
|
// Stat returns a FileInfo describing the named file/directory, or an error,
|
||||||
|
@ -301,6 +297,11 @@ func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int6
|
||||||
return nil, c.GetPermissionDeniedError()
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, fsPath, ftpPath, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
file, r, cancelFn, err := fs.Open(fsPath, offset)
|
file, r, cancelFn, err := fs.Open(fsPath, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", fsPath, err)
|
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", fsPath, err)
|
||||||
|
@ -357,6 +358,10 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath,
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
file, w, cancelFn, err := fs.Create(filePath, 0)
|
file, w, cancelFn, err := fs.Create(filePath, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
||||||
|
@ -383,6 +388,10 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
minWriteOffset := int64(0)
|
minWriteOffset := int64(0)
|
||||||
// ftpserverlib sets:
|
// ftpserverlib sets:
|
||||||
// - os.O_WRONLY | os.O_APPEND for APPE and COMB
|
// - os.O_WRONLY | os.O_APPEND for APPE and COMB
|
||||||
|
|
|
@ -74,17 +74,17 @@ func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
|
||||||
return c.ListDir(name)
|
return c.ListDir(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) getFileReader(name string, offset int64) (io.ReadCloser, error) {
|
func (c *Connection) getFileReader(name string, offset int64, method string) (io.ReadCloser, error) {
|
||||||
c.UpdateLastActivity()
|
c.UpdateLastActivity()
|
||||||
|
|
||||||
name = utils.CleanPath(name)
|
name = utils.CleanPath(name)
|
||||||
if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {
|
if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {
|
||||||
return nil, os.ErrPermission
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.User.IsFileAllowed(name) {
|
if !c.User.IsFileAllowed(name) {
|
||||||
c.Log(logger.LevelWarn, "reading file %#v is not allowed", name)
|
c.Log(logger.LevelWarn, "reading file %#v is not allowed", name)
|
||||||
return nil, os.ErrPermission
|
return nil, c.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
fs, p, err := c.GetFsAndResolvedPath(name)
|
fs, p, err := c.GetFsAndResolvedPath(name)
|
||||||
|
@ -92,6 +92,13 @@ func (c *Connection) getFileReader(name string, offset int64) (io.ReadCloser, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method != http.MethodHead {
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, name, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file, r, cancelFn, err := fs.Open(p, offset)
|
file, r, cancelFn, err := fs.Open(p, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", p, err)
|
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", p, err)
|
||||||
|
|
|
@ -141,6 +141,7 @@ var (
|
||||||
testServer *httptest.Server
|
testServer *httptest.Server
|
||||||
providerDriverName string
|
providerDriverName string
|
||||||
postConnectPath string
|
postConnectPath string
|
||||||
|
preDownloadPath string
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeConnection struct {
|
type fakeConnection struct {
|
||||||
|
@ -196,6 +197,7 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
||||||
|
preDownloadPath = filepath.Join(homeBasePath, "predownload.sh")
|
||||||
|
|
||||||
httpConfig := config.GetHTTPConfig()
|
httpConfig := config.GetHTTPConfig()
|
||||||
httpConfig.Initialize(configDir) //nolint:errcheck
|
httpConfig.Initialize(configDir) //nolint:errcheck
|
||||||
|
@ -285,6 +287,7 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(hostKeyPath)
|
os.Remove(hostKeyPath)
|
||||||
os.Remove(hostKeyPath + ".pub")
|
os.Remove(hostKeyPath + ".pub")
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
|
os.Remove(preDownloadPath)
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4709,13 +4712,13 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
_, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
|
@ -4896,6 +4899,56 @@ func TestWebClientChangePubKeys(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreDownloadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
common.Config.Actions.Hook = preDownloadPath
|
||||||
|
|
||||||
|
u := getTestUser()
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testFileName := "testfile"
|
||||||
|
testFileContents := []byte("file contents")
|
||||||
|
err = os.MkdirAll(filepath.Join(user.GetHomeDir()), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), testFileContents, os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Equal(t, testFileContents, rr.Body.Bytes())
|
||||||
|
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "permission denied")
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebGetFiles(t *testing.T) {
|
func TestWebGetFiles(t *testing.T) {
|
||||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -8068,7 +8121,7 @@ func createTestFile(path string, size int64) error {
|
||||||
return os.WriteFile(path, content, os.ModePerm)
|
return os.WriteFile(path, content, os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPostConnectScriptContent(exitCode int) []byte {
|
func getExitCodeScriptContent(exitCode int) []byte {
|
||||||
content := []byte("#!/bin/sh\n\n")
|
content := []byte("#!/bin/sh\n\n")
|
||||||
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
||||||
return content
|
return content
|
||||||
|
|
|
@ -1251,10 +1251,10 @@ func TestConnection(t *testing.T) {
|
||||||
assert.Empty(t, connection.GetRemoteAddress())
|
assert.Empty(t, connection.GetRemoteAddress())
|
||||||
assert.Empty(t, connection.GetCommand())
|
assert.Empty(t, connection.GetCommand())
|
||||||
name := "missing file name"
|
name := "missing file name"
|
||||||
_, err := connection.getFileReader(name, 0)
|
_, err := connection.getFileReader(name, 0, http.MethodGet)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
connection.User.FsConfig.Provider = vfs.LocalFilesystemProvider
|
connection.User.FsConfig.Provider = vfs.LocalFilesystemProvider
|
||||||
_, err = connection.getFileReader(name, 0)
|
_, err = connection.getFileReader(name, 0, http.MethodGet)
|
||||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -461,7 +461,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection
|
||||||
}
|
}
|
||||||
responseStatus = http.StatusPartialContent
|
responseStatus = http.StatusPartialContent
|
||||||
}
|
}
|
||||||
reader, err := connection.getFileReader(name, offset)
|
reader, err := connection.getFileReader(name, offset, r.Method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderFilesPage(w, r, nil, name, fmt.Sprintf("unable to read file %#v: %v", name, err))
|
renderFilesPage(w, r, nil, name, fmt.Sprintf("unable to read file %#v: %v", name, err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -837,10 +837,7 @@ func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder)
|
||||||
if expected.Description != actual.Description {
|
if expected.Description != actual.Description {
|
||||||
return errors.New("description mismatch")
|
return errors.New("description mismatch")
|
||||||
}
|
}
|
||||||
if err := compareFsConfig(&expected.FsConfig, &actual.FsConfig); err != nil {
|
return compareFsConfig(&expected.FsConfig, &actual.FsConfig)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
|
func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
|
||||||
|
@ -981,10 +978,7 @@ func compareFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
||||||
if err := checkEncryptedSecret(expected.CryptConfig.Passphrase, actual.CryptConfig.Passphrase); err != nil {
|
if err := checkEncryptedSecret(expected.CryptConfig.Passphrase, actual.CryptConfig.Passphrase); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := compareSFTPFsConfig(expected, actual); err != nil {
|
return compareSFTPFsConfig(expected, actual)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
||||||
|
|
|
@ -59,6 +59,11 @@ func (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, request.Filepath, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
file, r, cancelFn, err := fs.Open(p, 0)
|
file, r, cancelFn, err := fs.Open(p, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", p, err)
|
c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", p, err)
|
||||||
|
@ -325,6 +330,11 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
file, w, cancelFn, err := fs.Create(filePath, 0)
|
file, w, cancelFn, err := fs.Create(filePath, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
||||||
|
@ -351,6 +361,10 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, sftp.ErrSSHFxFailure
|
return nil, sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
minWriteOffset := int64(0)
|
minWriteOffset := int64(0)
|
||||||
osFlags := getOSOpenFlags(pflags)
|
osFlags := getOSOpenFlags(pflags)
|
||||||
|
|
13
sftpd/scp.go
13
sftpd/scp.go
|
@ -219,6 +219,13 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
|
||||||
c.sendErrorMessage(fs, err)
|
c.sendErrorMessage(fs, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err := common.ExecutePreAction(&c.connection.User, common.OperationPreUpload, resolvedPath, requestPath, c.connection.GetProtocol(), fileSize)
|
||||||
|
if err != nil {
|
||||||
|
c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
err = c.connection.GetPermissionDeniedError()
|
||||||
|
c.sendErrorMessage(fs, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
maxWriteSize, _ := c.connection.GetMaxWriteSize(quotaResult, false, fileSize, fs.IsUploadResumeSupported())
|
maxWriteSize, _ := c.connection.GetMaxWriteSize(quotaResult, false, fileSize, fs.IsUploadResumeSupported())
|
||||||
|
|
||||||
|
@ -507,6 +514,12 @@ func (c *scpCommand) handleDownload(filePath string) error {
|
||||||
return common.ErrPermissionDenied
|
return common.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := common.ExecutePreAction(&c.connection.User, common.OperationPreDownload, p, filePath, c.connection.GetProtocol(), 0); err != nil {
|
||||||
|
c.connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", filePath, err)
|
||||||
|
c.sendErrorMessage(fs, common.ErrPermissionDenied)
|
||||||
|
return common.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
file, r, cancelFn, err := fs.Open(p, 0)
|
file, r, cancelFn, err := fs.Open(p, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.connection.Log(logger.LevelError, "could not open file %#v for reading: %v", p, err)
|
c.connection.Log(logger.LevelError, "could not open file %#v for reading: %v", p, err)
|
||||||
|
|
|
@ -132,6 +132,8 @@ var (
|
||||||
keyIntAuthPath string
|
keyIntAuthPath string
|
||||||
preLoginPath string
|
preLoginPath string
|
||||||
postConnectPath string
|
postConnectPath string
|
||||||
|
preDownloadPath string
|
||||||
|
preUploadPath string
|
||||||
checkPwdPath string
|
checkPwdPath string
|
||||||
logFilePath string
|
logFilePath string
|
||||||
hostKeyFPs []string
|
hostKeyFPs []string
|
||||||
|
@ -284,6 +286,8 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(extAuthPath)
|
os.Remove(extAuthPath)
|
||||||
os.Remove(preLoginPath)
|
os.Remove(preLoginPath)
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
|
os.Remove(preDownloadPath)
|
||||||
|
os.Remove(preUploadPath)
|
||||||
os.Remove(keyIntAuthPath)
|
os.Remove(keyIntAuthPath)
|
||||||
os.Remove(checkPwdPath)
|
os.Remove(checkPwdPath)
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
|
@ -2280,6 +2284,210 @@ func TestPreLoginUserCreation(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreDownloadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
common.Config.Actions.Hook = preDownloadPath
|
||||||
|
|
||||||
|
usePubKey := true
|
||||||
|
u := getTestUser(usePubKey)
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
testFileSize := int64(131072)
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
conn, client, err := getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteSCPDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", testFileName))
|
||||||
|
err = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = client.Remove(testFileName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.Hook = "http://127.0.0.1:8080/web/admin/login"
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = client.Remove(testFileName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
err = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.Hook = "http://127.0.0.1:8080/"
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = client.Remove(testFileName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
}
|
||||||
|
err = scpDownload(localDownloadPath, remoteSCPDownPath, false, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreUploadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}
|
||||||
|
common.Config.Actions.Hook = preUploadPath
|
||||||
|
|
||||||
|
usePubKey := true
|
||||||
|
u := getTestUser(usePubKey)
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
testFileSize := int64(131072)
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
conn, client, err := getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteSCPUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", testFileName))
|
||||||
|
err = scpUpload(testFilePath, remoteSCPUpPath, true, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName+"1", testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
}
|
||||||
|
err = scpUpload(testFilePath, remoteSCPUpPath, true, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.Hook = "http://127.0.0.1:8080/web/client/login"
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
err = scpUpload(testFilePath, remoteSCPUpPath, true, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.Hook = "http://127.0.0.1:8080/web"
|
||||||
|
|
||||||
|
conn, client, err = getSftpClient(user, usePubKey)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
err = sftpUploadFile(testFilePath, testFileName+"1", testFileSize, client)
|
||||||
|
assert.ErrorIs(t, err, os.ErrPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scpUpload(testFilePath, remoteSCPUpPath, true, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostConnectHook(t *testing.T) {
|
func TestPostConnectHook(t *testing.T) {
|
||||||
if runtime.GOOS == osWindows {
|
if runtime.GOOS == osWindows {
|
||||||
t.Skip("this test is not available on Windows")
|
t.Skip("this test is not available on Windows")
|
||||||
|
@ -2290,7 +2498,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
conn, client, err := getSftpClient(u, usePubKey)
|
conn, client, err := getSftpClient(u, usePubKey)
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
|
@ -2299,7 +2507,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
err = checkBasicSFTP(client)
|
err = checkBasicSFTP(client)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
conn, client, err = getSftpClient(u, usePubKey)
|
conn, client, err = getSftpClient(u, usePubKey)
|
||||||
if !assert.Error(t, err) {
|
if !assert.Error(t, err) {
|
||||||
|
@ -3453,7 +3661,6 @@ func TestBandwidthAndConnections(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:dupl
|
|
||||||
func TestPatternsFilters(t *testing.T) {
|
func TestPatternsFilters(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
|
@ -3512,68 +3719,6 @@ func TestPatternsFilters(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:dupl
|
|
||||||
func TestExtensionsFilters(t *testing.T) {
|
|
||||||
usePubKey := true
|
|
||||||
u := getTestUser(usePubKey)
|
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
testFileSize := int64(131072)
|
|
||||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
|
||||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
|
||||||
conn, client, err := getSftpClient(user, usePubKey)
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
defer conn.Close()
|
|
||||||
defer client.Close()
|
|
||||||
err = createTestFile(testFilePath, testFileSize)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = sftpUploadFile(testFilePath, testFileName+".zip", testFileSize, client)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = sftpUploadFile(testFilePath, testFileName+".jpg", testFileSize, client)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
user.Filters.FilePatterns = []dataprovider.PatternsFilter{
|
|
||||||
{
|
|
||||||
Path: "/",
|
|
||||||
AllowedPatterns: []string{"*.jPg", "*.zIp"},
|
|
||||||
DeniedPatterns: []string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
conn, client, err = getSftpClient(user, usePubKey)
|
|
||||||
if assert.NoError(t, err) {
|
|
||||||
defer conn.Close()
|
|
||||||
defer client.Close()
|
|
||||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = client.Rename(testFileName, testFileName+"1")
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = client.Remove(testFileName)
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = sftpDownloadFile(testFileName+".zip", localDownloadPath, testFileSize, client)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = sftpDownloadFile(testFileName+".jpg", localDownloadPath, testFileSize, client)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = client.Mkdir("dir.zip")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = client.Rename("dir.zip", "dir1.zip")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = os.Remove(testFilePath)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = os.Remove(localDownloadPath)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVirtualFolders(t *testing.T) {
|
func TestVirtualFolders(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
|
@ -9637,7 +9782,7 @@ func getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []by
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPostConnectScriptContent(exitCode int) []byte {
|
func getExitCodeScriptContent(exitCode int) []byte {
|
||||||
content := []byte("#!/bin/sh\n\n")
|
content := []byte("#!/bin/sh\n\n")
|
||||||
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
||||||
return content
|
return content
|
||||||
|
@ -9710,6 +9855,8 @@ func createInitialFiles(scriptArgs string) {
|
||||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||||
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
||||||
checkPwdPath = filepath.Join(homeBasePath, "checkpwd.sh")
|
checkPwdPath = filepath.Join(homeBasePath, "checkpwd.sh")
|
||||||
|
preDownloadPath = filepath.Join(homeBasePath, "predownload.sh")
|
||||||
|
preUploadPath = filepath.Join(homeBasePath, "preupload.sh")
|
||||||
err := os.WriteFile(pubKeyPath, []byte(testPubKey+"\n"), 0600)
|
err := os.WriteFile(pubKeyPath, []byte(testPubKey+"\n"), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnToConsole("unable to save public key to file: %v", err)
|
logger.WarnToConsole("unable to save public key to file: %v", err)
|
||||||
|
|
|
@ -729,8 +729,8 @@ func (c *sshCommand) sendExitStatus(err error) {
|
||||||
exitStatus := sshSubsystemExitStatus{
|
exitStatus := sshSubsystemExitStatus{
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
_, err = c.connection.channel.(ssh.Channel).SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
_, errClose := c.connection.channel.(ssh.Channel).SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
|
||||||
c.connection.Log(logger.LevelDebug, "exit status sent, error: %v", err)
|
c.connection.Log(logger.LevelDebug, "exit status sent, error: %v", errClose)
|
||||||
c.connection.channel.Close()
|
c.connection.channel.Close()
|
||||||
// for scp we notify single uploads/downloads
|
// for scp we notify single uploads/downloads
|
||||||
if c.command != scpCmdName {
|
if c.command != scpCmdName {
|
||||||
|
@ -747,7 +747,8 @@ func (c *sshCommand) sendExitStatus(err error) {
|
||||||
targetPath = p
|
targetPath = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.ExecuteActionNotification(&c.connection.User, "ssh_cmd", cmdPath, targetPath, c.command, common.ProtocolSSH, 0, err)
|
common.ExecuteActionNotification(&c.connection.User, common.OperationSSHCmd, cmdPath, c.getDestPath(), targetPath, c.command,
|
||||||
|
common.ProtocolSSH, 0, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -246,6 +246,9 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
f, err := fs.sftpClient.Open(name)
|
f, err := fs.sftpClient.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
if fs.config.BufferSize == 0 {
|
if fs.config.BufferSize == 0 {
|
||||||
return f, nil, nil, err
|
return f, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,12 @@ func (f *webDavFile) Read(p []byte) (n int, err error) {
|
||||||
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
|
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
|
||||||
return 0, f.Connection.GetPermissionDeniedError()
|
return 0, f.Connection.GetPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
err := common.ExecutePreAction(&f.Connection.User, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(),
|
||||||
|
f.Connection.GetProtocol(), 0)
|
||||||
|
if err != nil {
|
||||||
|
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
||||||
|
return 0, f.Connection.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
atomic.StoreInt32(&f.readTryed, 1)
|
atomic.StoreInt32(&f.readTryed, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,11 +67,7 @@ func (c *Connection) Rename(ctx context.Context, oldName, newName string) error
|
||||||
oldName = utils.CleanPath(oldName)
|
oldName = utils.CleanPath(oldName)
|
||||||
newName = utils.CleanPath(newName)
|
newName = utils.CleanPath(newName)
|
||||||
|
|
||||||
if err := c.BaseConnection.Rename(oldName, newName); err != nil {
|
return c.BaseConnection.Rename(oldName, newName)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the named file/directory, or an error,
|
// Stat returns a FileInfo describing the named file/directory, or an error,
|
||||||
|
@ -198,6 +194,10 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
file, w, cancelFn, err := fs.Create(filePath, 0)
|
file, w, cancelFn, err := fs.Create(filePath, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
|
||||||
|
@ -223,6 +223,10 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
||||||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
|
if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil {
|
||||||
|
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||||
|
return nil, c.GetPermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace
|
// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace
|
||||||
// will return false in this case and we deny the upload before
|
// will return false in this case and we deny the upload before
|
||||||
|
|
|
@ -235,6 +235,8 @@ var (
|
||||||
extAuthPath string
|
extAuthPath string
|
||||||
preLoginPath string
|
preLoginPath string
|
||||||
postConnectPath string
|
postConnectPath string
|
||||||
|
preDownloadPath string
|
||||||
|
preUploadPath string
|
||||||
logFilePath string
|
logFilePath string
|
||||||
certPath string
|
certPath string
|
||||||
keyPath string
|
keyPath string
|
||||||
|
@ -365,6 +367,8 @@ func TestMain(m *testing.M) {
|
||||||
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
||||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||||
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
||||||
|
preDownloadPath = filepath.Join(homeBasePath, "predownload.sh")
|
||||||
|
preUploadPath = filepath.Join(homeBasePath, "preupload.sh")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.Debug(logSender, "", "initializing WebDAV server with config %+v", webDavConf)
|
logger.Debug(logSender, "", "initializing WebDAV server with config %+v", webDavConf)
|
||||||
|
@ -400,6 +404,8 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(extAuthPath)
|
os.Remove(extAuthPath)
|
||||||
os.Remove(preLoginPath)
|
os.Remove(preLoginPath)
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
|
os.Remove(preDownloadPath)
|
||||||
|
os.Remove(preUploadPath)
|
||||||
os.Remove(certPath)
|
os.Remove(certPath)
|
||||||
os.Remove(keyPath)
|
os.Remove(keyPath)
|
||||||
os.Remove(caCrtPath)
|
os.Remove(caCrtPath)
|
||||||
|
@ -885,6 +891,98 @@ func TestPreLoginHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreDownloadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
common.Config.Actions.Hook = preDownloadPath
|
||||||
|
|
||||||
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client := getWebDavClient(user, true, nil)
|
||||||
|
assert.NoError(t, checkBasicFunc(client))
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = uploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||||
|
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(preDownloadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = downloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||||
|
assert.Error(t, err)
|
||||||
|
err = os.Remove(localDownloadPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, common.Connections.GetStats(), 0)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
common.Config.Actions.Hook = preDownloadPath
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreUploadHook(t *testing.T) {
|
||||||
|
if runtime.GOOS == osWindows {
|
||||||
|
t.Skip("this test is not available on Windows")
|
||||||
|
}
|
||||||
|
oldExecuteOn := common.Config.Actions.ExecuteOn
|
||||||
|
oldHook := common.Config.Actions.Hook
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreUpload}
|
||||||
|
common.Config.Actions.Hook = preUploadPath
|
||||||
|
|
||||||
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client := getWebDavClient(user, true, nil)
|
||||||
|
assert.NoError(t, checkBasicFunc(client))
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = uploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(preUploadPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = uploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = uploadFile(testFilePath, testFileName+"1", testFileSize, client)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, common.Connections.GetStats(), 0)
|
||||||
|
|
||||||
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
common.Config.Actions.Hook = oldHook
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostConnectHook(t *testing.T) {
|
func TestPostConnectHook(t *testing.T) {
|
||||||
if runtime.GOOS == osWindows {
|
if runtime.GOOS == osWindows {
|
||||||
t.Skip("this test is not available on Windows")
|
t.Skip("this test is not available on Windows")
|
||||||
|
@ -894,11 +992,11 @@ func TestPostConnectHook(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client := getWebDavClient(user, false, nil)
|
client := getWebDavClient(user, false, nil)
|
||||||
assert.NoError(t, checkBasicFunc(client))
|
assert.NoError(t, checkBasicFunc(client))
|
||||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
|
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Error(t, checkBasicFunc(client))
|
assert.Error(t, checkBasicFunc(client))
|
||||||
|
|
||||||
|
@ -2563,7 +2661,7 @@ func getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []by
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPostConnectScriptContent(exitCode int) []byte {
|
func getExitCodeScriptContent(exitCode int) []byte {
|
||||||
content := []byte("#!/bin/sh\n\n")
|
content := []byte("#!/bin/sh\n\n")
|
||||||
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
||||||
return content
|
return content
|
||||||
|
|
Loading…
Reference in a new issue