mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
actions: add pre-download and pre-upload
Downloads and uploads can be denied based on hook response
This commit is contained in:
@@ -231,6 +231,8 @@ var (
|
||||
extAuthPath string
|
||||
preLoginPath string
|
||||
postConnectPath string
|
||||
preDownloadPath string
|
||||
preUploadPath string
|
||||
logFilePath string
|
||||
caCrtPath string
|
||||
caCRLPath string
|
||||
@@ -333,6 +335,8 @@ func TestMain(m *testing.M) {
|
||||
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||
postConnectPath = filepath.Join(homeBasePath, "postconnect.sh")
|
||||
preDownloadPath = filepath.Join(homeBasePath, "predownload.sh")
|
||||
preUploadPath = filepath.Join(homeBasePath, "preupload.sh")
|
||||
|
||||
status := ftpd.GetStatus()
|
||||
if status.IsActive {
|
||||
@@ -401,6 +405,8 @@ func TestMain(m *testing.M) {
|
||||
os.Remove(extAuthPath)
|
||||
os.Remove(preLoginPath)
|
||||
os.Remove(postConnectPath)
|
||||
os.Remove(preDownloadPath)
|
||||
os.Remove(preUploadPath)
|
||||
os.Remove(certPath)
|
||||
os.Remove(keyPath)
|
||||
os.Remove(caCrtPath)
|
||||
@@ -707,6 +713,133 @@ func TestPreLoginHook(t *testing.T) {
|
||||
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) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
@@ -716,7 +849,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
|
||||
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(0), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true, nil)
|
||||
if assert.NoError(t, err) {
|
||||
@@ -725,7 +858,7 @@ func TestPostConnectHook(t *testing.T) {
|
||||
err := client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
|
||||
err = os.WriteFile(postConnectPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
client, err = getFTPClient(user, true, nil)
|
||||
if !assert.Error(t, err) {
|
||||
@@ -2898,7 +3031,7 @@ func getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []by
|
||||
return content
|
||||
}
|
||||
|
||||
func getPostConnectScriptContent(exitCode int) []byte {
|
||||
func getExitCodeScriptContent(exitCode int) []byte {
|
||||
content := []byte("#!/bin/sh\n\n")
|
||||
content = append(content, []byte(fmt.Sprintf("exit %v", exitCode))...)
|
||||
return content
|
||||
|
||||
@@ -113,11 +113,7 @@ func (c *Connection) RemoveAll(path string) error {
|
||||
func (c *Connection) Rename(oldname, newname string) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
if err := c.BaseConnection.Rename(oldname, newname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.BaseConnection.Rename(oldname, newname)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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")
|
||||
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)
|
||||
if err != nil {
|
||||
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")
|
||||
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)
|
||||
// ftpserverlib sets:
|
||||
// - os.O_WRONLY | os.O_APPEND for APPE and COMB
|
||||
|
||||
Reference in New Issue
Block a user