add experimental plugin system

This commit is contained in:
Nicola Murino 2021-07-11 15:26:51 +02:00
parent bfa4085932
commit bd5191dfc5
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
101 changed files with 3190 additions and 1612 deletions

View file

@ -10,7 +10,7 @@ import (
"github.com/drakkan/sftpgo/v2/config" "github.com/drakkan/sftpgo/v2/config"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -37,7 +37,7 @@ Please take a look at the usage below to customize the options.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
logger.DisableLogger() logger.DisableLogger()
logger.EnableConsoleLogger(zerolog.DebugLevel) logger.EnableConsoleLogger(zerolog.DebugLevel)
configDir = utils.CleanDirInput(configDir) configDir = util.CleanDirInput(configDir)
err := config.LoadConfig(configDir, configFile) err := config.LoadConfig(configDir, configFile)
if err != nil { if err != nil {
logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err) logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err)

View file

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/drakkan/sftpgo/v2/service" "github.com/drakkan/sftpgo/v2/service"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -23,7 +23,7 @@ sftpgo service install
Please take a look at the usage below to customize the startup options`, Please take a look at the usage below to customize the startup options`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s := service.Service{ s := service.Service{
ConfigDir: utils.CleanDirInput(configDir), ConfigDir: util.CleanDirInput(configDir),
ConfigFile: configFile, ConfigFile: configFile,
LogFilePath: logFilePath, LogFilePath: logFilePath,
LogMaxSize: logMaxSize, LogMaxSize: logMaxSize,
@ -60,7 +60,7 @@ func init() {
func getCustomServeFlags() []string { func getCustomServeFlags() []string {
result := []string{} result := []string{}
if configDir != defaultConfigDir { if configDir != defaultConfigDir {
configDir = utils.CleanDirInput(configDir) configDir = util.CleanDirInput(configDir)
result = append(result, "--"+configDirFlag) result = append(result, "--"+configDirFlag)
result = append(result, configDir) result = append(result, configDir)
} }

View file

@ -14,6 +14,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/service" "github.com/drakkan/sftpgo/v2/service"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
@ -85,9 +86,9 @@ $ sftpgo portable
Please take a look at the usage below to customize the serving parameters`, Please take a look at the usage below to customize the serving parameters`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
portableDir := directoryToServe portableDir := directoryToServe
fsProvider := vfs.GetProviderByName(portableFsProvider) fsProvider := sdk.GetProviderByName(portableFsProvider)
if !filepath.IsAbs(portableDir) { if !filepath.IsAbs(portableDir) {
if fsProvider == vfs.LocalFilesystemProvider { if fsProvider == sdk.LocalFilesystemProvider {
portableDir, _ = filepath.Abs(portableDir) portableDir, _ = filepath.Abs(portableDir)
} else { } else {
portableDir = os.TempDir() portableDir = os.TempDir()
@ -96,7 +97,7 @@ Please take a look at the usage below to customize the serving parameters`,
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = portablePermissions permissions["/"] = portablePermissions
portableGCSCredentials := "" portableGCSCredentials := ""
if fsProvider == vfs.GCSFilesystemProvider && portableGCSCredentialsFile != "" { if fsProvider == sdk.GCSFilesystemProvider && portableGCSCredentialsFile != "" {
contents, err := getFileContents(portableGCSCredentialsFile) contents, err := getFileContents(portableGCSCredentialsFile)
if err != nil { if err != nil {
fmt.Printf("Unable to get GCS credentials: %v\n", err) fmt.Printf("Unable to get GCS credentials: %v\n", err)
@ -106,7 +107,7 @@ Please take a look at the usage below to customize the serving parameters`,
portableGCSAutoCredentials = 0 portableGCSAutoCredentials = 0
} }
portableSFTPPrivateKey := "" portableSFTPPrivateKey := ""
if fsProvider == vfs.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" { if fsProvider == sdk.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" {
contents, err := getFileContents(portableSFTPPrivateKeyPath) contents, err := getFileContents(portableSFTPPrivateKeyPath)
if err != nil { if err != nil {
fmt.Printf("Unable to get SFTP private key: %v\n", err) fmt.Printf("Unable to get SFTP private key: %v\n", err)
@ -144,60 +145,72 @@ Please take a look at the usage below to customize the serving parameters`,
Shutdown: make(chan bool), Shutdown: make(chan bool),
PortableMode: 1, PortableMode: 1,
PortableUser: dataprovider.User{ PortableUser: dataprovider.User{
Username: portableUsername, BaseUser: sdk.BaseUser{
Password: portablePassword, Username: portableUsername,
PublicKeys: portablePublicKeys, Password: portablePassword,
Permissions: permissions, PublicKeys: portablePublicKeys,
HomeDir: portableDir, Permissions: permissions,
Status: 1, HomeDir: portableDir,
FsConfig: vfs.Filesystem{ Status: 1,
Provider: vfs.GetProviderByName(portableFsProvider), Filters: sdk.UserFilters{
S3Config: vfs.S3FsConfig{ FilePatterns: parsePatternsFilesFilters(),
Bucket: portableS3Bucket,
Region: portableS3Region,
AccessKey: portableS3AccessKey,
AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
Endpoint: portableS3Endpoint,
StorageClass: portableS3StorageClass,
KeyPrefix: portableS3KeyPrefix,
UploadPartSize: int64(portableS3ULPartSize),
UploadConcurrency: portableS3ULConcurrency,
},
GCSConfig: vfs.GCSFsConfig{
Bucket: portableGCSBucket,
Credentials: kms.NewPlainSecret(portableGCSCredentials),
AutomaticCredentials: portableGCSAutoCredentials,
StorageClass: portableGCSStorageClass,
KeyPrefix: portableGCSKeyPrefix,
},
AzBlobConfig: vfs.AzBlobFsConfig{
Container: portableAzContainer,
AccountName: portableAzAccountName,
AccountKey: kms.NewPlainSecret(portableAzAccountKey),
Endpoint: portableAzEndpoint,
AccessTier: portableAzAccessTier,
SASURL: kms.NewPlainSecret(portableAzSASURL),
KeyPrefix: portableAzKeyPrefix,
UseEmulator: portableAzUseEmulator,
UploadPartSize: int64(portableAzULPartSize),
UploadConcurrency: portableAzULConcurrency,
},
CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
},
SFTPConfig: vfs.SFTPFsConfig{
Endpoint: portableSFTPEndpoint,
Username: portableSFTPUsername,
Password: kms.NewPlainSecret(portableSFTPPassword),
PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
Fingerprints: portableSFTPFingerprints,
Prefix: portableSFTPPrefix,
DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
BufferSize: portableSFTPDBufferSize,
}, },
}, },
Filters: dataprovider.UserFilters{ FsConfig: vfs.Filesystem{
FilePatterns: parsePatternsFilesFilters(), Provider: sdk.GetProviderByName(portableFsProvider),
S3Config: vfs.S3FsConfig{
S3FsConfig: sdk.S3FsConfig{
Bucket: portableS3Bucket,
Region: portableS3Region,
AccessKey: portableS3AccessKey,
AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
Endpoint: portableS3Endpoint,
StorageClass: portableS3StorageClass,
KeyPrefix: portableS3KeyPrefix,
UploadPartSize: int64(portableS3ULPartSize),
UploadConcurrency: portableS3ULConcurrency,
},
},
GCSConfig: vfs.GCSFsConfig{
GCSFsConfig: sdk.GCSFsConfig{
Bucket: portableGCSBucket,
Credentials: kms.NewPlainSecret(portableGCSCredentials),
AutomaticCredentials: portableGCSAutoCredentials,
StorageClass: portableGCSStorageClass,
KeyPrefix: portableGCSKeyPrefix,
},
},
AzBlobConfig: vfs.AzBlobFsConfig{
AzBlobFsConfig: sdk.AzBlobFsConfig{
Container: portableAzContainer,
AccountName: portableAzAccountName,
AccountKey: kms.NewPlainSecret(portableAzAccountKey),
Endpoint: portableAzEndpoint,
AccessTier: portableAzAccessTier,
SASURL: kms.NewPlainSecret(portableAzSASURL),
KeyPrefix: portableAzKeyPrefix,
UseEmulator: portableAzUseEmulator,
UploadPartSize: int64(portableAzULPartSize),
UploadConcurrency: portableAzULConcurrency,
},
},
CryptConfig: vfs.CryptFsConfig{
CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
},
},
SFTPConfig: vfs.SFTPFsConfig{
SFTPFsConfig: sdk.SFTPFsConfig{
Endpoint: portableSFTPEndpoint,
Username: portableSFTPUsername,
Password: kms.NewPlainSecret(portableSFTPPassword),
PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
Fingerprints: portableSFTPFingerprints,
Prefix: portableSFTPPrefix,
DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
BufferSize: portableSFTPDBufferSize,
},
},
}, },
}, },
} }
@ -335,12 +348,12 @@ by overlapping round-trip times`)
rootCmd.AddCommand(portableCmd) rootCmd.AddCommand(portableCmd)
} }
func parsePatternsFilesFilters() []dataprovider.PatternsFilter { func parsePatternsFilesFilters() []sdk.PatternsFilter {
var patterns []dataprovider.PatternsFilter var patterns []sdk.PatternsFilter
for _, val := range portableAllowedPatterns { for _, val := range portableAllowedPatterns {
p, exts := getPatternsFilterValues(strings.TrimSpace(val)) p, exts := getPatternsFilterValues(strings.TrimSpace(val))
if p != "" { if p != "" {
patterns = append(patterns, dataprovider.PatternsFilter{ patterns = append(patterns, sdk.PatternsFilter{
Path: path.Clean(p), Path: path.Clean(p),
AllowedPatterns: exts, AllowedPatterns: exts,
DeniedPatterns: []string{}, DeniedPatterns: []string{},
@ -359,7 +372,7 @@ func parsePatternsFilesFilters() []dataprovider.PatternsFilter {
} }
} }
if !found { if !found {
patterns = append(patterns, dataprovider.PatternsFilter{ patterns = append(patterns, sdk.PatternsFilter{
Path: path.Clean(p), Path: path.Clean(p),
AllowedPatterns: []string{}, AllowedPatterns: []string{},
DeniedPatterns: exts, DeniedPatterns: exts,

View file

@ -10,7 +10,7 @@ import (
"github.com/drakkan/sftpgo/v2/config" "github.com/drakkan/sftpgo/v2/config"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -30,7 +30,7 @@ Please take a look at the usage below to customize the options.`,
logger.WarnToConsole("Unsupported target version, 10 is the only supported one") logger.WarnToConsole("Unsupported target version, 10 is the only supported one")
os.Exit(1) os.Exit(1)
} }
configDir = utils.CleanDirInput(configDir) configDir = util.CleanDirInput(configDir)
err := config.LoadConfig(configDir, configFile) err := config.LoadConfig(configDir, configFile)
if err != nil { if err != nil {
logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err) logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err)

View file

@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/drakkan/sftpgo/v2/service" "github.com/drakkan/sftpgo/v2/service"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -21,7 +21,7 @@ $ sftpgo serve
Please take a look at the usage below to customize the startup options`, Please take a look at the usage below to customize the startup options`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
service := service.Service{ service := service.Service{
ConfigDir: utils.CleanDirInput(configDir), ConfigDir: util.CleanDirInput(configDir),
ConfigFile: configFile, ConfigFile: configFile,
LogFilePath: logFilePath, LogFilePath: logFilePath,
LogMaxSize: logMaxSize, LogMaxSize: logMaxSize,

View file

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/drakkan/sftpgo/v2/service" "github.com/drakkan/sftpgo/v2/service"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -16,8 +16,8 @@ var (
Use: "start", Use: "start",
Short: "Start SFTPGo Windows Service", Short: "Start SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
configDir = utils.CleanDirInput(configDir) configDir = util.CleanDirInput(configDir)
if !filepath.IsAbs(logFilePath) && utils.IsFileInputValid(logFilePath) { if !filepath.IsAbs(logFilePath) && util.IsFileInputValid(logFilePath) {
logFilePath = filepath.Join(configDir, logFilePath) logFilePath = filepath.Join(configDir, logFilePath)
} }
s := service.Service{ s := service.Service{

View file

@ -17,8 +17,9 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -51,7 +52,8 @@ func InitializeActionHandler(handler ActionHandler) {
// ExecutePreAction executes a pre-* action and returns the result // ExecutePreAction executes a pre-* action and returns the result
func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64, openFlags int) error { func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64, openFlags int) error {
if !utils.IsStringInSlice(operation, Config.Actions.ExecuteOn) { plugin.Handler.NotifyFsEvent(operation, user.Username, filePath, "", "", protocol, fileSize, nil)
if !util.IsStringInSlice(operation, Config.Actions.ExecuteOn) {
// for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction. // 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 // Other pre action will deny the operation on error so if we have no configuration we must return
// a nil error // a nil error
@ -66,9 +68,10 @@ func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath,
// 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, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) { func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) {
plugin.Handler.NotifyFsEvent(operation, user.Username, filePath, target, sshCmd, protocol, fileSize, err)
notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, 0, err) notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, 0, err)
if utils.IsStringInSlice(operation, Config.Actions.ExecuteSync) { if util.IsStringInSlice(operation, Config.Actions.ExecuteSync) {
actionHandler.Handle(notification) //nolint:errcheck actionHandler.Handle(notification) //nolint:errcheck
return return
} }
@ -110,17 +113,17 @@ func newActionNotification(
fsConfig := user.GetFsConfigForPath(virtualPath) fsConfig := user.GetFsConfigForPath(virtualPath)
switch fsConfig.Provider { switch fsConfig.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
bucket = fsConfig.S3Config.Bucket bucket = fsConfig.S3Config.Bucket
endpoint = fsConfig.S3Config.Endpoint endpoint = fsConfig.S3Config.Endpoint
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
bucket = fsConfig.GCSConfig.Bucket bucket = fsConfig.GCSConfig.Bucket
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
bucket = fsConfig.AzBlobConfig.Container bucket = fsConfig.AzBlobConfig.Container
if fsConfig.AzBlobConfig.Endpoint != "" { if fsConfig.AzBlobConfig.Endpoint != "" {
endpoint = fsConfig.AzBlobConfig.Endpoint endpoint = fsConfig.AzBlobConfig.Endpoint
} }
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
endpoint = fsConfig.SFTPConfig.Endpoint endpoint = fsConfig.SFTPConfig.Endpoint
} }
@ -149,7 +152,7 @@ func newActionNotification(
type defaultActionHandler struct{} type defaultActionHandler struct{}
func (h *defaultActionHandler) Handle(notification *ActionNotification) error { func (h *defaultActionHandler) Handle(notification *ActionNotification) error {
if !utils.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) { if !util.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) {
return errUnconfiguredAction return errUnconfiguredAction
} }

View file

@ -12,27 +12,38 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
func TestNewActionNotification(t *testing.T) { func TestNewActionNotification(t *testing.T) {
user := &dataprovider.User{ user := &dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
Username: "username",
},
} }
user.FsConfig.Provider = vfs.LocalFilesystemProvider user.FsConfig.Provider = sdk.LocalFilesystemProvider
user.FsConfig.S3Config = vfs.S3FsConfig{ user.FsConfig.S3Config = vfs.S3FsConfig{
Bucket: "s3bucket", S3FsConfig: sdk.S3FsConfig{
Endpoint: "endpoint", Bucket: "s3bucket",
Endpoint: "endpoint",
},
} }
user.FsConfig.GCSConfig = vfs.GCSFsConfig{ user.FsConfig.GCSConfig = vfs.GCSFsConfig{
Bucket: "gcsbucket", GCSFsConfig: sdk.GCSFsConfig{
Bucket: "gcsbucket",
},
} }
user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{ user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
Container: "azcontainer", AzBlobFsConfig: sdk.AzBlobFsConfig{
Endpoint: "azendpoint", Container: "azcontainer",
Endpoint: "azendpoint",
},
} }
user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: "sftpendpoint", SFTPFsConfig: sdk.SFTPFsConfig{
Endpoint: "sftpendpoint",
},
} }
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, errors.New("fake error")) a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, errors.New("fake error"))
assert.Equal(t, user.Username, a.Username) assert.Equal(t, user.Username, a.Username)
@ -40,19 +51,19 @@ func TestNewActionNotification(t *testing.T) {
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 = sdk.S3FilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, 0, nil) a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, 0, 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 = sdk.GCSFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, ErrQuotaExceeded) a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, 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 = sdk.AzureBlobFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, nil) a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, 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)
@ -64,7 +75,7 @@ func TestNewActionNotification(t *testing.T) {
assert.Equal(t, 1, a.Status) assert.Equal(t, 1, a.Status)
assert.Equal(t, os.O_APPEND, a.OpenFlags) assert.Equal(t, os.O_APPEND, a.OpenFlags)
user.FsConfig.Provider = vfs.SFTPFilesystemProvider user.FsConfig.Provider = sdk.SFTPFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
assert.Equal(t, "sftpendpoint", a.Endpoint) assert.Equal(t, "sftpendpoint", a.Endpoint)
} }
@ -77,7 +88,9 @@ func TestActionHTTP(t *testing.T) {
Hook: fmt.Sprintf("http://%v", httpAddr), Hook: fmt.Sprintf("http://%v", httpAddr),
} }
user := &dataprovider.User{ user := &dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
Username: "username",
},
} }
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
err := actionHandler.Handle(a) err := actionHandler.Handle(a)
@ -110,7 +123,9 @@ func TestActionCMD(t *testing.T) {
Hook: hookCmd, Hook: hookCmd,
} }
user := &dataprovider.User{ user := &dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
Username: "username",
},
} }
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil)
err = actionHandler.Handle(a) err = actionHandler.Handle(a)
@ -133,7 +148,9 @@ func TestWrongActions(t *testing.T) {
Hook: badCommand, Hook: badCommand,
} }
user := &dataprovider.User{ user := &dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
Username: "username",
},
} }
a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, 0, nil) a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, 0, nil)
@ -180,8 +197,10 @@ func TestPreDeleteAction(t *testing.T) {
err = os.MkdirAll(homeDir, os.ModePerm) err = os.MkdirAll(homeDir, os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
user := dataprovider.User{ user := dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
HomeDir: homeDir, Username: "username",
HomeDir: homeDir,
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}

View file

@ -21,8 +21,8 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -330,10 +330,10 @@ func (t *ConnectionTransfer) getConnectionTransferAsString() string {
} }
result += fmt.Sprintf("%#v ", t.VirtualPath) result += fmt.Sprintf("%#v ", t.VirtualPath)
if t.Size > 0 { if t.Size > 0 {
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(t.StartTime)) elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(t.StartTime))
speed := float64(t.Size) / float64(utils.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime) speed := float64(t.Size) / float64(util.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", utils.ByteCountIEC(t.Size), result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", util.ByteCountIEC(t.Size),
utils.GetDurationAsString(elapsed), speed) util.GetDurationAsString(elapsed), speed)
} }
return result return result
} }
@ -595,7 +595,7 @@ func (conns *ActiveConnections) Add(c ActiveConnection) {
defer conns.Unlock() defer conns.Unlock()
conns.connections = append(conns.connections, c) conns.connections = append(conns.connections, c)
metrics.UpdateActiveConnectionsSize(len(conns.connections)) metric.UpdateActiveConnectionsSize(len(conns.connections))
logger.Debug(c.GetProtocol(), c.GetID(), "connection added, num open connections: %v", len(conns.connections)) logger.Debug(c.GetProtocol(), c.GetID(), "connection added, num open connections: %v", len(conns.connections))
} }
@ -629,7 +629,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
conns.connections[idx] = conns.connections[lastIdx] conns.connections[idx] = conns.connections[lastIdx]
conns.connections[lastIdx] = nil conns.connections[lastIdx] = nil
conns.connections = conns.connections[:lastIdx] conns.connections = conns.connections[:lastIdx]
metrics.UpdateActiveConnectionsSize(lastIdx) metric.UpdateActiveConnectionsSize(lastIdx)
logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, close fs error: %v, num open connections: %v", logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, close fs error: %v, num open connections: %v",
err, lastIdx) err, lastIdx)
return return
@ -721,9 +721,9 @@ func (conns *ActiveConnections) checkIdles() {
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v", logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %#v close err: %v",
time.Since(conn.GetLastActivity()), conn.GetUsername(), err) time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
if isFTPNoAuth { if isFTPNoAuth {
ip := utils.GetIPFromRemoteAddress(c.GetRemoteAddress()) ip := util.GetIPFromRemoteAddress(c.GetRemoteAddress())
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, c.GetProtocol(), "client idle") logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, c.GetProtocol(), "client idle")
metrics.AddNoAuthTryed() metric.AddNoAuthTryed()
AddDefenderEvent(ip, HostEventNoLoginTried) AddDefenderEvent(ip, HostEventNoLoginTried)
dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, c.GetProtocol(), dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, c.GetProtocol(),
dataprovider.ErrNoAuthTryed) dataprovider.ErrNoAuthTryed)
@ -794,8 +794,8 @@ func (conns *ActiveConnections) GetStats() []*ConnectionStatus {
ConnectionID: c.GetID(), ConnectionID: c.GetID(),
ClientVersion: c.GetClientVersion(), ClientVersion: c.GetClientVersion(),
RemoteAddress: c.GetRemoteAddress(), RemoteAddress: c.GetRemoteAddress(),
ConnectionTime: utils.GetTimeAsMsSinceEpoch(c.GetConnectionTime()), ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
LastActivity: utils.GetTimeAsMsSinceEpoch(c.GetLastActivity()), LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
Protocol: c.GetProtocol(), Protocol: c.GetProtocol(),
Command: c.GetCommand(), Command: c.GetCommand(),
Transfers: c.GetTransfers(), Transfers: c.GetTransfers(),
@ -829,8 +829,8 @@ type ConnectionStatus struct {
// GetConnectionDuration returns the connection duration as string // GetConnectionDuration returns the connection duration as string
func (c *ConnectionStatus) GetConnectionDuration() string { func (c *ConnectionStatus) GetConnectionDuration() string {
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(c.ConnectionTime)) elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
return utils.GetDurationAsString(elapsed) return util.GetDurationAsString(elapsed)
} }
// GetConnectionInfo returns connection info. // GetConnectionInfo returns connection info.
@ -912,7 +912,7 @@ func (s *ActiveScans) AddUserQuotaScan(username string) bool {
} }
s.UserHomeScans = append(s.UserHomeScans, ActiveQuotaScan{ s.UserHomeScans = append(s.UserHomeScans, ActiveQuotaScan{
Username: username, Username: username,
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
}) })
return true return true
} }
@ -960,7 +960,7 @@ func (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {
} }
s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{ s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{
Name: folderName, Name: folderName,
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
}) })
return true return true
} }

View file

@ -20,7 +20,8 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -323,7 +324,9 @@ func TestIdleConnections(t *testing.T) {
username := "test_user" username := "test_user"
user := dataprovider.User{ user := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Username: username,
},
} }
c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, "", user) c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, "", user)
c.lastActivity = time.Now().Add(-24 * time.Hour).UnixNano() c.lastActivity = time.Now().Add(-24 * time.Hour).UnixNano()
@ -410,7 +413,9 @@ func TestSwapConnection(t *testing.T) {
assert.Equal(t, "", Connections.GetStats()[0].Username) assert.Equal(t, "", Connections.GetStats()[0].Username)
} }
c = NewBaseConnection("id", ProtocolFTP, "", dataprovider.User{ c = NewBaseConnection("id", ProtocolFTP, "", dataprovider.User{
Username: userTestUsername, BaseUser: sdk.BaseUser{
Username: userTestUsername,
},
}) })
fakeConn = &fakeConnection{ fakeConn = &fakeConnection{
BaseConnection: c, BaseConnection: c,
@ -443,7 +448,9 @@ func TestAtomicUpload(t *testing.T) {
func TestConnectionStatus(t *testing.T) { func TestConnectionStatus(t *testing.T) {
username := "test_user" username := "test_user"
user := dataprovider.User{ user := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Username: username,
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
c1 := NewBaseConnection("id1", ProtocolSFTP, "", user) c1 := NewBaseConnection("id1", ProtocolSFTP, "", user)
@ -634,7 +641,11 @@ func TestPostConnectHook(t *testing.T) {
func TestCryptoConvertFileInfo(t *testing.T) { func TestCryptoConvertFileInfo(t *testing.T) {
name := "name" name := "name"
fs, err := vfs.NewCryptFs("connID1", os.TempDir(), "", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")}) fs, err := vfs.NewCryptFs("connID1", os.TempDir(), "", vfs.CryptFsConfig{
CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret("secret"),
},
})
require.NoError(t, err) require.NoError(t, err)
cryptFs := fs.(*vfs.CryptFs) cryptFs := fs.(*vfs.CryptFs)
info := vfs.NewFileInfo(name, true, 48, time.Now(), false) info := vfs.NewFileInfo(name, true, 48, time.Now(), false)
@ -654,15 +665,15 @@ func TestFolderCopy(t *testing.T) {
MappedPath: filepath.Clean(os.TempDir()), MappedPath: filepath.Clean(os.TempDir()),
UsedQuotaSize: 4096, UsedQuotaSize: 4096,
UsedQuotaFiles: 2, UsedQuotaFiles: 2,
LastQuotaUpdate: utils.GetTimeAsMsSinceEpoch(time.Now()), LastQuotaUpdate: util.GetTimeAsMsSinceEpoch(time.Now()),
Users: []string{"user1", "user2"}, Users: []string{"user1", "user2"},
} }
folderCopy := folder.GetACopy() folderCopy := folder.GetACopy()
folder.ID = 2 folder.ID = 2
folder.Users = []string{"user3"} folder.Users = []string{"user3"}
require.Len(t, folderCopy.Users, 2) require.Len(t, folderCopy.Users, 2)
require.True(t, utils.IsStringInSlice("user1", folderCopy.Users)) require.True(t, util.IsStringInSlice("user1", folderCopy.Users))
require.True(t, utils.IsStringInSlice("user2", folderCopy.Users)) require.True(t, util.IsStringInSlice("user2", folderCopy.Users))
require.Equal(t, int64(1), folderCopy.ID) require.Equal(t, int64(1), folderCopy.ID)
require.Equal(t, folder.Name, folderCopy.Name) require.Equal(t, folder.Name, folderCopy.Name)
require.Equal(t, folder.MappedPath, folderCopy.MappedPath) require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
@ -672,13 +683,15 @@ func TestFolderCopy(t *testing.T) {
folder.FsConfig = vfs.Filesystem{ folder.FsConfig = vfs.Filesystem{
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret("crypto secret"), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret("crypto secret"),
},
}, },
} }
folderCopy = folder.GetACopy() folderCopy = folder.GetACopy()
folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret() folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
require.Len(t, folderCopy.Users, 1) require.Len(t, folderCopy.Users, 1)
require.True(t, utils.IsStringInSlice("user3", folderCopy.Users)) require.True(t, util.IsStringInSlice("user3", folderCopy.Users))
require.Equal(t, int64(2), folderCopy.ID) require.Equal(t, int64(2), folderCopy.ID)
require.Equal(t, folder.Name, folderCopy.Name) require.Equal(t, folder.Name, folderCopy.Name)
require.Equal(t, folder.MappedPath, folderCopy.MappedPath) require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
@ -690,7 +703,9 @@ func TestFolderCopy(t *testing.T) {
func TestCachedFs(t *testing.T) { func TestCachedFs(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
conn := NewBaseConnection("id", ProtocolSFTP, "", user) conn := NewBaseConnection("id", ProtocolSFTP, "", user)
// changing the user should not affect the connection // changing the user should not affect the connection
@ -706,10 +721,10 @@ func TestCachedFs(t *testing.T) {
_, p, err = conn.GetFsAndResolvedPath("/") _, p, err = conn.GetFsAndResolvedPath("/")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, filepath.Clean(os.TempDir()), p) assert.Equal(t, filepath.Clean(os.TempDir()), p)
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
_, err = user.GetFilesystem("") _, err = user.GetFilesystem("")
assert.Error(t, err) assert.Error(t, err)
conn.User.FsConfig.Provider = vfs.S3FilesystemProvider conn.User.FsConfig.Provider = sdk.S3FilesystemProvider
_, p, err = conn.GetFsAndResolvedPath("/") _, p, err = conn.GetFsAndResolvedPath("/")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, filepath.Clean(os.TempDir()), p) assert.Equal(t, filepath.Clean(os.TempDir()), p)
@ -718,11 +733,11 @@ func TestCachedFs(t *testing.T) {
} }
func TestParseAllowedIPAndRanges(t *testing.T) { func TestParseAllowedIPAndRanges(t *testing.T) {
_, err := utils.ParseAllowedIPAndRanges([]string{"1.1.1.1", "not an ip"}) _, err := util.ParseAllowedIPAndRanges([]string{"1.1.1.1", "not an ip"})
assert.Error(t, err) assert.Error(t, err)
_, err = utils.ParseAllowedIPAndRanges([]string{"1.1.1.5", "192.168.1.0/240"}) _, err = util.ParseAllowedIPAndRanges([]string{"1.1.1.5", "192.168.1.0/240"})
assert.Error(t, err) assert.Error(t, err)
allow, err := utils.ParseAllowedIPAndRanges([]string{"192.168.1.2", "172.16.0.0/24"}) allow, err := util.ParseAllowedIPAndRanges([]string{"192.168.1.2", "172.16.0.0/24"})
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, allow[0](net.ParseIP("192.168.1.2"))) assert.True(t, allow[0](net.ParseIP("192.168.1.2")))
assert.False(t, allow[0](net.ParseIP("192.168.2.2"))) assert.False(t, allow[0](net.ParseIP("192.168.2.2")))

View file

@ -15,7 +15,8 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -40,7 +41,7 @@ type BaseConnection struct {
// NewBaseConnection returns a new BaseConnection // NewBaseConnection returns a new BaseConnection
func NewBaseConnection(id, protocol, remoteAddr string, user dataprovider.User) *BaseConnection { func NewBaseConnection(id, protocol, remoteAddr string, user dataprovider.User) *BaseConnection {
connID := id connID := id
if utils.IsStringInSlice(protocol, supportedProtocols) { if util.IsStringInSlice(protocol, supportedProtocols) {
connID = fmt.Sprintf("%v_%v", protocol, id) connID = fmt.Sprintf("%v_%v", protocol, id)
} }
return &BaseConnection{ return &BaseConnection{
@ -82,7 +83,7 @@ func (c *BaseConnection) GetProtocol() string {
// SetProtocol sets the protocol for this connection // SetProtocol sets the protocol for this connection
func (c *BaseConnection) SetProtocol(protocol string) { func (c *BaseConnection) SetProtocol(protocol string) {
c.protocol = protocol c.protocol = protocol
if utils.IsStringInSlice(c.protocol, supportedProtocols) { if util.IsStringInSlice(c.protocol, supportedProtocols) {
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID) c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
} }
} }
@ -155,7 +156,7 @@ func (c *BaseConnection) GetTransfers() []ConnectionTransfer {
transfers = append(transfers, ConnectionTransfer{ transfers = append(transfers, ConnectionTransfer{
ID: t.GetID(), ID: t.GetID(),
OperationType: operationType, OperationType: operationType,
StartTime: utils.GetTimeAsMsSinceEpoch(t.GetStartTime()), StartTime: util.GetTimeAsMsSinceEpoch(t.GetStartTime()),
Size: t.GetSize(), Size: t.GetSize(),
VirtualPath: t.GetVirtualPath(), VirtualPath: t.GetVirtualPath(),
}) })
@ -881,22 +882,22 @@ func (c *BaseConnection) isLocalOrSameFolderRename(virtualSourcePath, virtualTar
return true return true
} }
// we have different folders, only local fs is supported // we have different folders, only local fs is supported
if sourceFolder.FsConfig.Provider == vfs.LocalFilesystemProvider && if sourceFolder.FsConfig.Provider == sdk.LocalFilesystemProvider &&
dstFolder.FsConfig.Provider == vfs.LocalFilesystemProvider { dstFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
return true return true
} }
return false return false
} }
if c.User.FsConfig.Provider != vfs.LocalFilesystemProvider { if c.User.FsConfig.Provider != sdk.LocalFilesystemProvider {
return false return false
} }
if errSrc == nil { if errSrc == nil {
if sourceFolder.FsConfig.Provider == vfs.LocalFilesystemProvider { if sourceFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
return true return true
} }
} }
if errDst == nil { if errDst == nil {
if dstFolder.FsConfig.Provider == vfs.LocalFilesystemProvider { if dstFolder.FsConfig.Provider == sdk.LocalFilesystemProvider {
return true return true
} }
} }

View file

@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -47,8 +48,10 @@ func TestRemoveErrors(t *testing.T) {
homePath := filepath.Join(os.TempDir(), "home") homePath := filepath.Join(os.TempDir(), "home")
user := dataprovider.User{ user := dataprovider.User{
Username: "remove_errors_user", BaseUser: sdk.BaseUser{
HomeDir: homePath, Username: "remove_errors_user",
HomeDir: homePath,
},
VirtualFolders: []vfs.VirtualFolder{ VirtualFolders: []vfs.VirtualFolder{
{ {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
@ -78,7 +81,9 @@ func TestSetStatMode(t *testing.T) {
fakePath := "fake path" fakePath := "fake path"
user := dataprovider.User{ user := dataprovider.User{
HomeDir: os.TempDir(), BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -145,8 +150,10 @@ func TestRenameVirtualFolders(t *testing.T) {
func TestUpdateQuotaAfterRename(t *testing.T) { func TestUpdateQuotaAfterRename(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: userTestUsername, BaseUser: sdk.BaseUser{
HomeDir: filepath.Join(os.TempDir(), "home"), Username: userTestUsername,
HomeDir: filepath.Join(os.TempDir(), "home"),
},
} }
mappedPath := filepath.Join(os.TempDir(), "vdir") mappedPath := filepath.Join(os.TempDir(), "vdir")
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
@ -218,7 +225,7 @@ func TestUpdateQuotaAfterRename(t *testing.T) {
func TestErrorsMapping(t *testing.T) { func TestErrorsMapping(t *testing.T) {
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
conn := NewBaseConnection("", ProtocolSFTP, "", dataprovider.User{HomeDir: os.TempDir()}) conn := NewBaseConnection("", ProtocolSFTP, "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
for _, protocol := range supportedProtocols { for _, protocol := range supportedProtocols {
conn.SetProtocol(protocol) conn.SetProtocol(protocol)
err := conn.GetFsError(fs, os.ErrNotExist) err := conn.GetFsError(fs, os.ErrNotExist)
@ -276,9 +283,11 @@ func TestMaxWriteSize(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{ user := dataprovider.User{
Username: userTestUsername, BaseUser: sdk.BaseUser{
Permissions: permissions, Username: userTestUsername,
HomeDir: filepath.Clean(os.TempDir()), Permissions: permissions,
HomeDir: filepath.Clean(os.TempDir()),
},
} }
fs, err := user.GetFilesystem("123") fs, err := user.GetFilesystem("123")
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -13,7 +13,7 @@ import (
"github.com/yl2chen/cidranger" "github.com/yl2chen/cidranger"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// HostEvent is the enumerable for the support host event // HostEvent is the enumerable for the support host event
@ -289,7 +289,7 @@ func (d *memoryDefender) GetHost(ip string) (*DefenderEntry, error) {
} }
} }
return nil, utils.NewRecordNotFoundError("host not found") return nil, util.NewRecordNotFoundError("host not found")
} }
// IsBanned returns true if the specified IP is banned // IsBanned returns true if the specified IP is banned
@ -522,7 +522,7 @@ func loadHostListFromFile(name string) (*HostList, error) {
if name == "" { if name == "" {
return nil, nil return nil, nil
} }
if !utils.IsFileInputValid(name) { if !util.IsFileInputValid(name) {
return nil, fmt.Errorf("invalid host list file name %#v", name) return nil, fmt.Errorf("invalid host list file name %#v", name)
} }

View file

@ -11,7 +11,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
const ( const (
@ -114,7 +114,7 @@ func (p *basicAuthProvider) getHashedPassword(username string) (string, bool) {
// ValidateCredentials returns true if the credentials are valid // ValidateCredentials returns true if the credentials are valid
func (p *basicAuthProvider) ValidateCredentials(username, password string) bool { func (p *basicAuthProvider) ValidateCredentials(username, password string) bool {
if hashedPwd, ok := p.getHashedPassword(username); ok { if hashedPwd, ok := p.getHashedPassword(username); ok {
if utils.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) { if util.IsStringPrefixInSlice(hashedPwd, bcryptPwdPrefixes) {
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(password))
return err == nil return err == nil
} }

View file

@ -33,6 +33,7 @@ import (
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -354,7 +355,7 @@ func TestPermissionErrors(t *testing.T) {
func TestFileNotAllowedErrors(t *testing.T) { func TestFileNotAllowedErrors(t *testing.T) {
deniedDir := "/denied" deniedDir := "/denied"
u := getTestUser() u := getTestUser()
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: deniedDir, Path: deniedDir,
DeniedPatterns: []string{"*.txt"}, DeniedPatterns: []string{"*.txt"},
@ -2373,22 +2374,26 @@ func TestSFTPLoopError(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: "sftp", Name: "sftp",
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{ SFTPConfig: vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user2.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user2.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
VirtualPath: "/vdir", VirtualPath: "/vdir",
}) })
user2.FsConfig.Provider = vfs.SFTPFilesystemProvider user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user1.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user1.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
} }
user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated) user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
@ -2438,11 +2443,13 @@ func TestNonLocalCrossRename(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameSFTP, Name: folderNameSFTP,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{ SFTPConfig: vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: baseUser.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: baseUser.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
@ -2455,9 +2462,11 @@ func TestNonLocalCrossRename(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameCrypt, Name: folderNameCrypt,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
MappedPath: mappedPathCrypt, MappedPath: mappedPathCrypt,
@ -2556,9 +2565,11 @@ func TestNonLocalCrossRenameNonLocalBaseUser(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameCrypt, Name: folderNameCrypt,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
MappedPath: mappedPathCrypt, MappedPath: mappedPathCrypt,
@ -2639,14 +2650,14 @@ func TestProxyProtocol(t *testing.T) {
} }
func TestSetProtocol(t *testing.T) { func TestSetProtocol(t *testing.T) {
conn := common.NewBaseConnection("id", "sshd_exec", "", dataprovider.User{HomeDir: os.TempDir()}) conn := common.NewBaseConnection("id", "sshd_exec", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
conn.SetProtocol(common.ProtocolSCP) conn.SetProtocol(common.ProtocolSCP)
require.Equal(t, "SCP_id", conn.GetID()) require.Equal(t, "SCP_id", conn.GetID())
} }
func TestGetFsError(t *testing.T) { func TestGetFsError(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
conn := common.NewBaseConnection("", common.ProtocolFTP, "", u) conn := common.NewBaseConnection("", common.ProtocolFTP, "", u)
@ -2704,11 +2715,13 @@ func getSftpClient(user dataprovider.User) (*ssh.Client, *sftp.Client, error) {
func getTestUser() dataprovider.User { func getTestUser() dataprovider.User {
user := dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, BaseUser: sdk.BaseUser{
Password: defaultPassword, Username: defaultUsername,
HomeDir: filepath.Join(homeBasePath, defaultUsername), Password: defaultPassword,
Status: 1, HomeDir: filepath.Join(homeBasePath, defaultUsername),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms user.Permissions["/"] = allPerms
@ -2718,7 +2731,7 @@ func getTestUser() dataprovider.User {
func getTestSFTPUser() dataprovider.User { func getTestSFTPUser() dataprovider.User {
u := getTestUser() u := getTestUser()
u.Username = defaultSFTPUsername u.Username = defaultSFTPUsername
u.FsConfig.Provider = vfs.SFTPFilesystemProvider u.FsConfig.Provider = sdk.SFTPFilesystemProvider
u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr
u.FsConfig.SFTPConfig.Username = defaultUsername u.FsConfig.SFTPConfig.Username = defaultUsername
u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)
@ -2727,7 +2740,7 @@ func getTestSFTPUser() dataprovider.User {
func getCryptFsUser() dataprovider.User { func getCryptFsUser() dataprovider.User {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.CryptedFilesystemProvider u.FsConfig.Provider = sdk.CryptedFilesystemProvider
u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword) u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(defaultPassword)
return u return u
} }

View file

@ -10,7 +10,7 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -78,9 +78,9 @@ func (r *RateLimiterConfig) validate() error {
return fmt.Errorf("invalid entries_hard_limit %v must be > %v", r.EntriesHardLimit, r.EntriesSoftLimit) return fmt.Errorf("invalid entries_hard_limit %v must be > %v", r.EntriesHardLimit, r.EntriesSoftLimit)
} }
} }
r.Protocols = utils.RemoveDuplicates(r.Protocols) r.Protocols = util.RemoveDuplicates(r.Protocols)
for _, protocol := range r.Protocols { for _, protocol := range r.Protocols {
if !utils.IsStringInSlice(protocol, rateLimiterProtocolValues) { if !util.IsStringInSlice(protocol, rateLimiterProtocolValues) {
return fmt.Errorf("invalid protocol %#v", protocol) return fmt.Errorf("invalid protocol %#v", protocol)
} }
} }

View file

@ -11,7 +11,7 @@ import (
"time" "time"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// CertManager defines a TLS certificate manager // CertManager defines a TLS certificate manager
@ -98,7 +98,7 @@ func (m *CertManager) LoadCRLs() error {
var crls []*pkix.CertificateList var crls []*pkix.CertificateList
for _, revocationList := range m.caRevocationLists { for _, revocationList := range m.caRevocationLists {
if !utils.IsFileInputValid(revocationList) { if !util.IsFileInputValid(revocationList) {
return fmt.Errorf("invalid root CA revocation list %#v", revocationList) return fmt.Errorf("invalid root CA revocation list %#v", revocationList)
} }
if revocationList != "" && !filepath.IsAbs(revocationList) { if revocationList != "" && !filepath.IsAbs(revocationList) {
@ -145,7 +145,7 @@ func (m *CertManager) LoadRootCAs() error {
rootCAs := x509.NewCertPool() rootCAs := x509.NewCertPool()
for _, rootCA := range m.caCertificates { for _, rootCA := range m.caCertificates {
if !utils.IsFileInputValid(rootCA) { if !util.IsFileInputValid(rootCA) {
return fmt.Errorf("invalid root CA certificate %#v", rootCA) return fmt.Errorf("invalid root CA certificate %#v", rootCA)
} }
if rootCA != "" && !filepath.IsAbs(rootCA) { if rootCA != "" && !filepath.IsAbs(rootCA) {

View file

@ -9,7 +9,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -139,7 +139,7 @@ func (t *BaseTransfer) Truncate(fsPath string, size int64) (int64, error) {
if t.MaxWriteSize > 0 { if t.MaxWriteSize > 0 {
sizeDiff := initialSize - size sizeDiff := initialSize - size
t.MaxWriteSize += sizeDiff t.MaxWriteSize += sizeDiff
metrics.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer) metric.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer)
atomic.StoreInt64(&t.BytesReceived, 0) atomic.StoreInt64(&t.BytesReceived, 0)
} }
t.Unlock() t.Unlock()
@ -206,7 +206,7 @@ func (t *BaseTransfer) Close() error {
if t.isNewFile { if t.isNewFile {
numFiles = 1 numFiles = 1
} }
metrics.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer) metric.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.transferType, t.ErrTransfer)
if t.File != nil && t.Connection.IsQuotaExceededError(t.ErrTransfer) { if t.File != nil && t.Connection.IsQuotaExceededError(t.ErrTransfer) {
// if quota is exceeded we try to remove the partial file for uploads to local filesystem // if quota is exceeded we try to remove the partial file for uploads to local filesystem
err = t.Fs.Remove(t.File.Name(), false) err = t.Fs.Remove(t.File.Name(), false)

View file

@ -12,6 +12,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -50,9 +51,11 @@ func TestTransferUpdateQuota(t *testing.T) {
func TestTransferThrottling(t *testing.T) { func TestTransferThrottling(t *testing.T) {
u := dataprovider.User{ u := dataprovider.User{
Username: "test", BaseUser: sdk.BaseUser{
UploadBandwidth: 50, Username: "test",
DownloadBandwidth: 40, UploadBandwidth: 50,
DownloadBandwidth: 40,
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
testFileSize := int64(131072) testFileSize := int64(131072)
@ -88,8 +91,10 @@ func TestRealPath(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "afile.txt") testFile := filepath.Join(os.TempDir(), "afile.txt")
fs := vfs.NewOsFs("123", os.TempDir(), "") fs := vfs.NewOsFs("123", os.TempDir(), "")
u := dataprovider.User{ u := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "user",
HomeDir: os.TempDir(),
},
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/"] = []string{dataprovider.PermAny}
@ -119,8 +124,10 @@ func TestTruncate(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file") testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("123", os.TempDir(), "") fs := vfs.NewOsFs("123", os.TempDir(), "")
u := dataprovider.User{ u := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "user",
HomeDir: os.TempDir(),
},
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/"] = []string{dataprovider.PermAny}
@ -183,8 +190,10 @@ func TestTransferErrors(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file") testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs := vfs.NewOsFs("id", os.TempDir(), "") fs := vfs.NewOsFs("id", os.TempDir(), "")
u := dataprovider.User{ u := dataprovider.User{
Username: "test", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "test",
HomeDir: os.TempDir(),
},
} }
err := os.WriteFile(testFile, []byte("test data"), os.ModePerm) err := os.WriteFile(testFile, []byte("test data"), os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
@ -255,11 +264,13 @@ func TestTransferErrors(t *testing.T) {
func TestRemovePartialCryptoFile(t *testing.T) { func TestRemovePartialCryptoFile(t *testing.T) {
testFile := filepath.Join(os.TempDir(), "transfer_test_file") testFile := filepath.Join(os.TempDir(), "transfer_test_file")
fs, err := vfs.NewCryptFs("id", os.TempDir(), "", vfs.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")}) fs, err := vfs.NewCryptFs("id", os.TempDir(), "", vfs.CryptFsConfig{CryptFsConfig: sdk.CryptFsConfig{Passphrase: kms.NewPlainSecret("secret")}})
require.NoError(t, err) require.NoError(t, err)
u := dataprovider.User{ u := dataprovider.User{
Username: "test", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "test",
HomeDir: os.TempDir(),
},
} }
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, "", u) conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, "", u)
transfer := NewBaseTransfer(nil, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload, 0, 0, 0, true, fs) transfer := NewBaseTransfer(nil, conn, nil, testFile, testFile, "/transfer_test_file", TransferUpload, 0, 0, 0, true, fs)

View file

@ -18,9 +18,10 @@ import (
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/telemetry" "github.com/drakkan/sftpgo/v2/telemetry"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/webdavd" "github.com/drakkan/sftpgo/v2/webdavd"
) )
@ -94,6 +95,7 @@ type globalConfig struct {
HTTPConfig httpclient.Config `json:"http" mapstructure:"http"` HTTPConfig httpclient.Config `json:"http" mapstructure:"http"`
KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"` KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"`
TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"` TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"`
PluginsConfig []plugin.Config `json:"plugins" mapstructure:"plugins"`
} }
func init() { func init() {
@ -275,6 +277,7 @@ func Init() {
CertificateKeyFile: "", CertificateKeyFile: "",
TLSCipherSuites: nil, TLSCipherSuites: nil,
}, },
PluginsConfig: nil,
} }
viper.SetEnvPrefix(configEnvPrefix) viper.SetEnvPrefix(configEnvPrefix)
@ -371,6 +374,11 @@ func SetTelemetryConfig(config telemetry.Conf) {
globalConf.TelemetryConfig = config globalConf.TelemetryConfig = config
} }
// GetPluginsConfig returns the plugins configuration
func GetPluginsConfig() []plugin.Config {
return globalConf.PluginsConfig
}
// HasServicesToStart returns true if the config defines at least a service to start. // HasServicesToStart returns true if the config defines at least a service to start.
// Supported services are SFTP, FTP and WebDAV // Supported services are SFTP, FTP and WebDAV
func HasServicesToStart() bool { func HasServicesToStart() bool {
@ -388,17 +396,17 @@ func HasServicesToStart() bool {
func getRedactedGlobalConf() globalConfig { func getRedactedGlobalConf() globalConfig {
conf := globalConf conf := globalConf
conf.Common.Actions.Hook = utils.GetRedactedURL(conf.Common.Actions.Hook) conf.Common.Actions.Hook = util.GetRedactedURL(conf.Common.Actions.Hook)
conf.Common.StartupHook = utils.GetRedactedURL(conf.Common.StartupHook) conf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)
conf.Common.PostConnectHook = utils.GetRedactedURL(conf.Common.PostConnectHook) conf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)
conf.SFTPD.KeyboardInteractiveHook = utils.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook) conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
conf.HTTPDConfig.SigningPassphrase = "[redacted]" conf.HTTPDConfig.SigningPassphrase = "[redacted]"
conf.ProviderConf.Password = "[redacted]" conf.ProviderConf.Password = "[redacted]"
conf.ProviderConf.Actions.Hook = utils.GetRedactedURL(conf.ProviderConf.Actions.Hook) conf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook)
conf.ProviderConf.ExternalAuthHook = utils.GetRedactedURL(conf.ProviderConf.ExternalAuthHook) conf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook)
conf.ProviderConf.PreLoginHook = utils.GetRedactedURL(conf.ProviderConf.PreLoginHook) conf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook)
conf.ProviderConf.PostLoginHook = utils.GetRedactedURL(conf.ProviderConf.PostLoginHook) conf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook)
conf.ProviderConf.CheckPasswordHook = utils.GetRedactedURL(conf.ProviderConf.CheckPasswordHook) conf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook)
return conf return conf
} }
@ -406,7 +414,7 @@ func setConfigFile(configDir, configFile string) {
if configFile == "" { if configFile == "" {
return return
} }
if !filepath.IsAbs(configFile) && utils.IsFileInputValid(configFile) { if !filepath.IsAbs(configFile) && util.IsFileInputValid(configFile) {
configFile = filepath.Join(configDir, configFile) configFile = filepath.Join(configDir, configFile)
} }
viper.SetConfigFile(configFile) viper.SetConfigFile(configFile)
@ -449,7 +457,7 @@ func LoadConfig(configDir, configFile string) error {
if strings.TrimSpace(globalConf.FTPD.Banner) == "" { if strings.TrimSpace(globalConf.FTPD.Banner) == "" {
globalConf.FTPD.Banner = defaultFTPDBanner globalConf.FTPD.Banner = defaultFTPDBanner
} }
if globalConf.ProviderConf.UsersBaseDir != "" && !utils.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) { if globalConf.ProviderConf.UsersBaseDir != "" && !util.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) {
err = fmt.Errorf("invalid users base dir %#v will be ignored", globalConf.ProviderConf.UsersBaseDir) err = fmt.Errorf("invalid users base dir %#v will be ignored", globalConf.ProviderConf.UsersBaseDir)
globalConf.ProviderConf.UsersBaseDir = "" globalConf.ProviderConf.UsersBaseDir = ""
logger.Warn(logSender, "", "Configuration error: %v", err) logger.Warn(logSender, "", "Configuration error: %v", err)
@ -488,6 +496,7 @@ func LoadConfig(configDir, configFile string) error {
func loadBindingsFromEnv() { func loadBindingsFromEnv() {
for idx := 0; idx < 10; idx++ { for idx := 0; idx < 10; idx++ {
getRateLimitersFromEnv(idx) getRateLimitersFromEnv(idx)
getPluginsFromEnv(idx)
getSFTPDBindindFromEnv(idx) getSFTPDBindindFromEnv(idx)
getFTPDBindingFromEnv(idx) getFTPDBindingFromEnv(idx)
getWebDAVDBindingFromEnv(idx) getWebDAVDBindingFromEnv(idx)
@ -562,6 +571,65 @@ func getRateLimitersFromEnv(idx int) {
} }
} }
func getPluginsFromEnv(idx int) {
pluginConfig := plugin.Config{}
if len(globalConf.PluginsConfig) > idx {
pluginConfig = globalConf.PluginsConfig[idx]
}
isSet := false
pluginType, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__TYPE", idx))
if ok {
pluginConfig.Type = pluginType
isSet = true
}
notifierFsEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__FS_EVENTS", idx))
if ok {
pluginConfig.NotifierOptions.FsEvents = notifierFsEvents
isSet = true
}
notifierUserEvents, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__USER_EVENTS", idx))
if ok {
pluginConfig.NotifierOptions.UserEvents = notifierUserEvents
isSet = true
}
cmd, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__CMD", idx))
if ok {
pluginConfig.Cmd = cmd
isSet = true
}
cmdArgs, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__ARGS", idx))
if ok {
pluginConfig.Args = cmdArgs
isSet = true
}
pluginHash, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__SHA256SUM", idx))
if ok {
pluginConfig.SHA256Sum = pluginHash
isSet = true
}
autoMTLS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__AUTO_MTLS", idx))
if ok {
pluginConfig.AutoMTLS = autoMTLS
isSet = true
}
if isSet {
if len(globalConf.PluginsConfig) > idx {
globalConf.PluginsConfig[idx] = pluginConfig
} else {
globalConf.PluginsConfig = append(globalConf.PluginsConfig, pluginConfig)
}
}
}
func getSFTPDBindindFromEnv(idx int) { func getSFTPDBindindFromEnv(idx int) {
binding := sftpd.Binding{ binding := sftpd.Binding{
ApplyProxyConfig: true, ApplyProxyConfig: true,
@ -988,7 +1056,10 @@ func lookupStringListFromEnv(envName string) ([]string, bool) {
if ok { if ok {
var result []string var result []string
for _, v := range strings.Split(value, ",") { for _, v := range strings.Split(value, ",") {
result = append(result, strings.TrimSpace(v)) val := strings.TrimSpace(v)
if val != "" {
result = append(result, val)
}
} }
return result, true return result, true
} }

View file

@ -18,7 +18,7 @@ import (
"github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
const ( const (
@ -290,6 +290,77 @@ func TestServiceToStart(t *testing.T) {
assert.True(t, config.HasServicesToStart()) assert.True(t, config.HasServicesToStart())
} }
func TestPluginsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_PLUGINS__0__TYPE", "notifier")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS", "upload,download")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__USER_EVENTS", "add,update")
os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd")
os.Setenv("SFTPGO_PLUGINS__0__ARGS", "arg1,arg2")
os.Setenv("SFTPGO_PLUGINS__0__SHA256SUM", "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193")
os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_PLUGINS__0__TYPE")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__USER_EVENTS")
os.Unsetenv("SFTPGO_PLUGINS__0__CMD")
os.Unsetenv("SFTPGO_PLUGINS__0__ARGS")
os.Unsetenv("SFTPGO_PLUGINS__0__SHA256SUM")
os.Unsetenv("SFTPGO_PLUGINS__0__AUTO_MTLS")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
pluginsConf := config.GetPluginsConfig()
require.Len(t, pluginsConf, 1)
pluginConf := pluginsConf[0]
require.Equal(t, "notifier", pluginConf.Type)
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
require.Len(t, pluginConf.NotifierOptions.UserEvents, 2)
require.Equal(t, "add", pluginConf.NotifierOptions.UserEvents[0])
require.Equal(t, "update", pluginConf.NotifierOptions.UserEvents[1])
require.Equal(t, "plugin_start_cmd", pluginConf.Cmd)
require.Len(t, pluginConf.Args, 2)
require.Equal(t, "arg1", pluginConf.Args[0])
require.Equal(t, "arg2", pluginConf.Args[1])
require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum)
require.True(t, pluginConf.AutoMTLS)
configAsJSON, err := json.Marshal(pluginsConf)
require.NoError(t, err)
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err = os.WriteFile(configFilePath, configAsJSON, os.ModePerm)
assert.NoError(t, err)
os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd1")
os.Setenv("SFTPGO_PLUGINS__0__ARGS", "")
os.Setenv("SFTPGO_PLUGINS__0__AUTO_MTLS", "0")
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
pluginsConf = config.GetPluginsConfig()
require.Len(t, pluginsConf, 1)
pluginConf = pluginsConf[0]
require.Equal(t, "notifier", pluginConf.Type)
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
require.True(t, util.IsStringInSlice("upload", pluginConf.NotifierOptions.FsEvents))
require.True(t, util.IsStringInSlice("download", pluginConf.NotifierOptions.FsEvents))
require.Len(t, pluginConf.NotifierOptions.UserEvents, 2)
require.Equal(t, "add", pluginConf.NotifierOptions.UserEvents[0])
require.Equal(t, "update", pluginConf.NotifierOptions.UserEvents[1])
require.Equal(t, "plugin_start_cmd1", pluginConf.Cmd)
require.Len(t, pluginConf.Args, 0)
require.Equal(t, "0a71ded61fccd59c4f3695b51c1b3d180da8d2d77ea09ccee20dac242675c193", pluginConf.SHA256Sum)
require.False(t, pluginConf.AutoMTLS)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestRateLimitersFromEnv(t *testing.T) { func TestRateLimitersFromEnv(t *testing.T) {
reset() reset()
@ -325,8 +396,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
require.Equal(t, 2, limiters[0].Type) require.Equal(t, 2, limiters[0].Type)
protocols := limiters[0].Protocols protocols := limiters[0].Protocols
require.Len(t, protocols, 2) require.Len(t, protocols, 2)
require.True(t, utils.IsStringInSlice(common.ProtocolFTP, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolFTP, protocols))
require.True(t, utils.IsStringInSlice(common.ProtocolSSH, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolSSH, protocols))
require.True(t, limiters[0].GenerateDefenderEvents) require.True(t, limiters[0].GenerateDefenderEvents)
require.Equal(t, 50, limiters[0].EntriesSoftLimit) require.Equal(t, 50, limiters[0].EntriesSoftLimit)
require.Equal(t, 100, limiters[0].EntriesHardLimit) require.Equal(t, 100, limiters[0].EntriesHardLimit)
@ -337,10 +408,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
require.Equal(t, 2, limiters[1].Type) require.Equal(t, 2, limiters[1].Type)
protocols = limiters[1].Protocols protocols = limiters[1].Protocols
require.Len(t, protocols, 4) require.Len(t, protocols, 4)
require.True(t, utils.IsStringInSlice(common.ProtocolFTP, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolFTP, protocols))
require.True(t, utils.IsStringInSlice(common.ProtocolSSH, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolSSH, protocols))
require.True(t, utils.IsStringInSlice(common.ProtocolWebDAV, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolWebDAV, protocols))
require.True(t, utils.IsStringInSlice(common.ProtocolHTTP, protocols)) require.True(t, util.IsStringInSlice(common.ProtocolHTTP, protocols))
require.False(t, limiters[1].GenerateDefenderEvents) require.False(t, limiters[1].GenerateDefenderEvents)
require.Equal(t, 100, limiters[1].EntriesSoftLimit) require.Equal(t, 100, limiters[1].EntriesSoftLimit)
require.Equal(t, 150, limiters[1].EntriesHardLimit) require.Equal(t, 150, limiters[1].EntriesHardLimit)

View file

@ -12,7 +12,7 @@ import (
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// Available permissions for SFTPGo admins // Available permissions for SFTPGo admins
@ -66,7 +66,7 @@ type Admin struct {
} }
func (a *Admin) checkPassword() error { func (a *Admin) checkPassword() error {
if a.Password != "" && !utils.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) { if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {
if config.PasswordHashing.Algo == HashingAlgoBcrypt { if config.PasswordHashing.Algo == HashingAlgoBcrypt {
pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost) pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
if err != nil { if err != nil {
@ -86,36 +86,36 @@ func (a *Admin) checkPassword() error {
func (a *Admin) validate() error { func (a *Admin) validate() error {
if a.Username == "" { if a.Username == "" {
return utils.NewValidationError("username is mandatory") return util.NewValidationError("username is mandatory")
} }
if a.Password == "" { if a.Password == "" {
return utils.NewValidationError("please set a password") return util.NewValidationError("please set a password")
} }
if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) { if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) {
return utils.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)) return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username))
} }
if err := a.checkPassword(); err != nil { if err := a.checkPassword(); err != nil {
return err return err
} }
a.Permissions = utils.RemoveDuplicates(a.Permissions) a.Permissions = util.RemoveDuplicates(a.Permissions)
if len(a.Permissions) == 0 { if len(a.Permissions) == 0 {
return utils.NewValidationError("please grant some permissions to this admin") return util.NewValidationError("please grant some permissions to this admin")
} }
if utils.IsStringInSlice(PermAdminAny, a.Permissions) { if util.IsStringInSlice(PermAdminAny, a.Permissions) {
a.Permissions = []string{PermAdminAny} a.Permissions = []string{PermAdminAny}
} }
for _, perm := range a.Permissions { for _, perm := range a.Permissions {
if !utils.IsStringInSlice(perm, validAdminPerms) { if !util.IsStringInSlice(perm, validAdminPerms) {
return utils.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm)) return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm))
} }
} }
if a.Email != "" && !emailRegex.MatchString(a.Email) { if a.Email != "" && !emailRegex.MatchString(a.Email) {
return utils.NewValidationError(fmt.Sprintf("email %#v is not valid", a.Email)) return util.NewValidationError(fmt.Sprintf("email %#v is not valid", a.Email))
} }
for _, IPMask := range a.Filters.AllowList { for _, IPMask := range a.Filters.AllowList {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err)) return util.NewValidationError(fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err))
} }
} }
@ -182,10 +182,10 @@ func (a *Admin) HideConfidentialData() {
// HasPermission returns true if the admin has the specified permission // HasPermission returns true if the admin has the specified permission
func (a *Admin) HasPermission(perm string) bool { func (a *Admin) HasPermission(perm string) bool {
if utils.IsStringInSlice(PermAdminAny, a.Permissions) { if util.IsStringInSlice(PermAdminAny, a.Permissions) {
return true return true
} }
return utils.IsStringInSlice(perm, a.Permissions) return util.IsStringInSlice(perm, a.Permissions)
} }
// GetPermissionsAsString returns permission as string // GetPermissionsAsString returns permission as string

View file

@ -13,7 +13,7 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -43,7 +43,7 @@ func initializeBoltProvider(basePath string) error {
var err error var err error
dbPath := config.Name dbPath := config.Name
if !utils.IsFileInputValid(dbPath) { if !util.IsFileInputValid(dbPath) {
return fmt.Errorf("invalid database path: %#v", dbPath) return fmt.Errorf("invalid database path: %#v", dbPath)
} }
if !filepath.IsAbs(dbPath) { if !filepath.IsAbs(dbPath) {
@ -160,14 +160,14 @@ func (p *BoltProvider) updateLastLogin(username string) error {
} }
var u []byte var u []byte
if u = bucket.Get([]byte(username)); u == nil { if u = bucket.Get([]byte(username)); u == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to update last login", username)) return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to update last login", username))
} }
var user User var user User
err = json.Unmarshal(u, &user) err = json.Unmarshal(u, &user)
if err != nil { if err != nil {
return err return err
} }
user.LastLogin = utils.GetTimeAsMsSinceEpoch(time.Now()) user.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(user) buf, err := json.Marshal(user)
if err != nil { if err != nil {
return err return err
@ -190,7 +190,7 @@ func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64,
} }
var u []byte var u []byte
if u = bucket.Get([]byte(username)); u == nil { if u = bucket.Get([]byte(username)); u == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to update quota", username)) return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist, unable to update quota", username))
} }
var user User var user User
err = json.Unmarshal(u, &user) err = json.Unmarshal(u, &user)
@ -204,7 +204,7 @@ func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64,
user.UsedQuotaSize += sizeAdd user.UsedQuotaSize += sizeAdd
user.UsedQuotaFiles += filesAdd user.UsedQuotaFiles += filesAdd
} }
user.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now()) user.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(user) buf, err := json.Marshal(user)
if err != nil { if err != nil {
return err return err
@ -235,7 +235,7 @@ func (p *BoltProvider) adminExists(username string) (Admin, error) {
} }
a := bucket.Get([]byte(username)) a := bucket.Get([]byte(username))
if a == nil { if a == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", username)) return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", username))
} }
return json.Unmarshal(a, &admin) return json.Unmarshal(a, &admin)
}) })
@ -282,7 +282,7 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
var a []byte var a []byte
if a = bucket.Get([]byte(admin.Username)); a == nil { if a = bucket.Get([]byte(admin.Username)); a == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username)) return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
} }
var oldAdmin Admin var oldAdmin Admin
err = json.Unmarshal(a, &oldAdmin) err = json.Unmarshal(a, &oldAdmin)
@ -307,7 +307,7 @@ func (p *BoltProvider) deleteAdmin(admin *Admin) error {
} }
if bucket.Get([]byte(admin.Username)) == nil { if bucket.Get([]byte(admin.Username)) == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username)) return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
} }
return bucket.Delete([]byte(admin.Username)) return bucket.Delete([]byte(admin.Username))
@ -397,7 +397,7 @@ func (p *BoltProvider) userExists(username string) (User, error) {
} }
u := bucket.Get([]byte(username)) u := bucket.Get([]byte(username))
if u == nil { if u == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
} }
folderBucket, err := getFoldersBucket(tx) folderBucket, err := getFoldersBucket(tx)
if err != nil { if err != nil {
@ -465,7 +465,7 @@ func (p *BoltProvider) updateUser(user *User) error {
} }
var u []byte var u []byte
if u = bucket.Get([]byte(user.Username)); u == nil { if u = bucket.Get([]byte(user.Username)); u == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", user.Username)) return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", user.Username))
} }
var oldUser User var oldUser User
err = json.Unmarshal(u, &oldUser) err = json.Unmarshal(u, &oldUser)
@ -517,7 +517,7 @@ func (p *BoltProvider) deleteUser(user *User) error {
} }
exists := bucket.Get([]byte(user.Username)) exists := bucket.Get([]byte(user.Username))
if exists == nil { if exists == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username)) return util.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username))
} }
return bucket.Delete([]byte(user.Username)) return bucket.Delete([]byte(user.Username))
}) })
@ -722,7 +722,7 @@ func (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
var f []byte var f []byte
if f = bucket.Get([]byte(folder.Name)); f == nil { if f = bucket.Get([]byte(folder.Name)); f == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", folder.Name)) return util.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", folder.Name))
} }
var oldFolder vfs.BaseVirtualFolder var oldFolder vfs.BaseVirtualFolder
err = json.Unmarshal(f, &oldFolder) err = json.Unmarshal(f, &oldFolder)
@ -755,7 +755,7 @@ func (p *BoltProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
} }
var f []byte var f []byte
if f = bucket.Get([]byte(folder.Name)); f == nil { if f = bucket.Get([]byte(folder.Name)); f == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", folder.Name)) return util.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", folder.Name))
} }
var folder vfs.BaseVirtualFolder var folder vfs.BaseVirtualFolder
err = json.Unmarshal(f, &folder) err = json.Unmarshal(f, &folder)
@ -801,7 +801,7 @@ func (p *BoltProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int6
} }
var f []byte var f []byte
if f = bucket.Get([]byte(name)); f == nil { if f = bucket.Get([]byte(name)); f == nil {
return utils.NewRecordNotFoundError(fmt.Sprintf("folder %#v does not exist, unable to update quota", name)) return util.NewRecordNotFoundError(fmt.Sprintf("folder %#v does not exist, unable to update quota", name))
} }
var folder vfs.BaseVirtualFolder var folder vfs.BaseVirtualFolder
err = json.Unmarshal(f, &folder) err = json.Unmarshal(f, &folder)
@ -815,7 +815,7 @@ func (p *BoltProvider) updateFolderQuota(name string, filesAdd int, sizeAdd int6
folder.UsedQuotaSize += sizeAdd folder.UsedQuotaSize += sizeAdd
folder.UsedQuotaFiles += filesAdd folder.UsedQuotaFiles += filesAdd
} }
folder.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now()) folder.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(folder) buf, err := json.Marshal(folder)
if err != nil { if err != nil {
return err return err
@ -910,7 +910,7 @@ func folderExistsInternal(name string, bucket *bolt.Bucket) (vfs.BaseVirtualFold
var folder vfs.BaseVirtualFolder var folder vfs.BaseVirtualFolder
f := bucket.Get([]byte(name)) f := bucket.Get([]byte(name))
if f == nil { if f == nil {
err := utils.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", name)) err := util.NewRecordNotFoundError(fmt.Sprintf("folder %v does not exist", name))
return folder, err return folder, err
} }
err := json.Unmarshal(f, &folder) err := json.Unmarshal(f, &folder)
@ -950,7 +950,7 @@ func addUserToFolderMapping(baseFolder *vfs.BaseVirtualFolder, user *User, bucke
baseFolder.UsedQuotaFiles = oldFolder.UsedQuotaFiles baseFolder.UsedQuotaFiles = oldFolder.UsedQuotaFiles
baseFolder.UsedQuotaSize = oldFolder.UsedQuotaSize baseFolder.UsedQuotaSize = oldFolder.UsedQuotaSize
baseFolder.Users = oldFolder.Users baseFolder.Users = oldFolder.Users
if !utils.IsStringInSlice(user.Username, baseFolder.Users) { if !util.IsStringInSlice(user.Username, baseFolder.Users) {
baseFolder.Users = append(baseFolder.Users, user.Username) baseFolder.Users = append(baseFolder.Users, user.Username)
} }
buf, err := json.Marshal(baseFolder) buf, err := json.Marshal(baseFolder)
@ -971,7 +971,7 @@ func removeUserFromFolderMapping(folder *vfs.VirtualFolder, user *User, bucket *
if err != nil { if err != nil {
return err return err
} }
if utils.IsStringInSlice(user.Username, baseFolder.Users) { if util.IsStringInSlice(user.Username, baseFolder.Users) {
var newUserMapping []string var newUserMapping []string
for _, u := range baseFolder.Users { for _, u := range baseFolder.Users {
if u != user.Username { if u != user.Username {

View file

@ -6,7 +6,7 @@ import (
"golang.org/x/net/webdav" "golang.org/x/net/webdav"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -54,7 +54,7 @@ func (cache *usersCache) updateLastLogin(username string) {
defer cache.Unlock() defer cache.Unlock()
if cachedUser, ok := cache.users[username]; ok { if cachedUser, ok := cache.users[username]; ok {
cachedUser.User.LastLogin = utils.GetTimeAsMsSinceEpoch(time.Now()) cachedUser.User.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
cache.users[username] = cachedUser cache.users[username] = cachedUser
} }
} }

View file

@ -46,8 +46,10 @@ import (
"github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -120,7 +122,7 @@ var (
// ErrInvalidCredentials defines the error to return if the supplied credentials are invalid // ErrInvalidCredentials defines the error to return if the supplied credentials are invalid
ErrInvalidCredentials = errors.New("invalid credentials") ErrInvalidCredentials = errors.New("invalid credentials")
isAdminCreated = int32(0) isAdminCreated = int32(0)
validTLSUsernames = []string{string(TLSUsernameNone), string(TLSUsernameCN)} validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
config Config config Config
provider Provider provider Provider
sqlPlaceholders []string sqlPlaceholders []string
@ -764,7 +766,7 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
// UpdateLastLogin updates the last login fields for the given SFTP user // UpdateLastLogin updates the last login fields for the given SFTP user
func UpdateLastLogin(user *User) error { func UpdateLastLogin(user *User) error {
lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin) lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
diff := -time.Until(lastLogin) diff := -time.Until(lastLogin)
if diff < 0 || diff > lastLoginMinDelay { if diff < 0 || diff > lastLoginMinDelay {
err := provider.updateLastLogin(user.Username) err := provider.updateLastLogin(user.Username)
@ -780,7 +782,7 @@ func UpdateLastLogin(user *User) error {
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference. // If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error { func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return utils.NewMethodDisabledError(trackQuotaDisabledError) return util.NewMethodDisabledError(trackQuotaDisabledError)
} else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() { } else if config.TrackQuota == 2 && !reset && !user.HasQuotaRestrictions() {
return nil return nil
} }
@ -801,7 +803,7 @@ func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference. // If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error { func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return utils.NewMethodDisabledError(trackQuotaDisabledError) return util.NewMethodDisabledError(trackQuotaDisabledError)
} }
if filesAdd == 0 && sizeAdd == 0 && !reset { if filesAdd == 0 && sizeAdd == 0 && !reset {
return nil return nil
@ -819,7 +821,7 @@ func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, size
// GetUsedQuota returns the used quota for the given SFTP user. // GetUsedQuota returns the used quota for the given SFTP user.
func GetUsedQuota(username string) (int, int64, error) { func GetUsedQuota(username string) (int, int64, error) {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return 0, 0, utils.NewMethodDisabledError(trackQuotaDisabledError) return 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError)
} }
files, size, err := provider.getUsedQuota(username) files, size, err := provider.getUsedQuota(username)
if err != nil { if err != nil {
@ -832,7 +834,7 @@ func GetUsedQuota(username string) (int, int64, error) {
// GetUsedVirtualFolderQuota returns the used quota for the given virtual folder. // GetUsedVirtualFolderQuota returns the used quota for the given virtual folder.
func GetUsedVirtualFolderQuota(name string) (int, int64, error) { func GetUsedVirtualFolderQuota(name string) (int, int64, error) {
if config.TrackQuota == 0 { if config.TrackQuota == 0 {
return 0, 0, utils.NewMethodDisabledError(trackQuotaDisabledError) return 0, 0, util.NewMethodDisabledError(trackQuotaDisabledError)
} }
files, size, err := provider.getUsedFolderQuota(name) files, size, err := provider.getUsedFolderQuota(name)
if err != nil { if err != nil {
@ -1064,7 +1066,7 @@ func buildUserHomeDir(user *User) {
return return
} }
switch user.FsConfig.Provider { switch user.FsConfig.Provider {
case vfs.SFTPFilesystemProvider, vfs.S3FilesystemProvider, vfs.AzureBlobFilesystemProvider, vfs.GCSFilesystemProvider: case sdk.SFTPFilesystemProvider, sdk.S3FilesystemProvider, sdk.AzureBlobFilesystemProvider, sdk.GCSFilesystemProvider:
if tempPath != "" { if tempPath != "" {
user.HomeDir = filepath.Join(tempPath, user.Username) user.HomeDir = filepath.Join(tempPath, user.Username)
} else { } else {
@ -1114,13 +1116,13 @@ func isMappedDirOverlapped(dir1, dir2 string, fullCheck bool) bool {
func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
if folder.QuotaSize < -1 { if folder.QuotaSize < -1 {
return utils.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %#v", folder.QuotaSize, folder.MappedPath))
} }
if folder.QuotaFiles < -1 { if folder.QuotaFiles < -1 {
return utils.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %#v", folder.QuotaFiles, folder.MappedPath))
} }
if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) {
return utils.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v",
folder.QuotaFiles, folder.QuotaSize)) folder.QuotaFiles, folder.QuotaSize))
} }
return nil return nil
@ -1137,7 +1139,7 @@ func getVirtualFolderIfInvalid(folder *vfs.BaseVirtualFolder) *vfs.BaseVirtualFo
if folder.Name == "" { if folder.Name == "" {
return folder return folder
} }
if folder.FsConfig.Provider != vfs.LocalFilesystemProvider { if folder.FsConfig.Provider != sdk.LocalFilesystemProvider {
return folder return folder
} }
if f, err := GetFolderByName(folder.Name); err == nil { if f, err := GetFolderByName(folder.Name); err == nil {
@ -1157,7 +1159,7 @@ func validateUserVirtualFolders(user *User) error {
for _, v := range user.VirtualFolders { for _, v := range user.VirtualFolders {
cleanedVPath := filepath.ToSlash(path.Clean(v.VirtualPath)) cleanedVPath := filepath.ToSlash(path.Clean(v.VirtualPath))
if !path.IsAbs(cleanedVPath) || cleanedVPath == "/" { if !path.IsAbs(cleanedVPath) || cleanedVPath == "/" {
return utils.NewValidationError(fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath)) return util.NewValidationError(fmt.Sprintf("invalid virtual folder %#v", v.VirtualPath))
} }
if err := validateFolderQuotaLimits(v); err != nil { if err := validateFolderQuotaLimits(v); err != nil {
return err return err
@ -1169,12 +1171,12 @@ func validateUserVirtualFolders(user *User) error {
cleanedMPath := folder.MappedPath cleanedMPath := folder.MappedPath
if folder.IsLocalOrLocalCrypted() { if folder.IsLocalOrLocalCrypted() {
if isMappedDirOverlapped(cleanedMPath, user.GetHomeDir(), true) { if isMappedDirOverlapped(cleanedMPath, user.GetHomeDir(), true) {
return utils.NewValidationError(fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v", return util.NewValidationError(fmt.Sprintf("invalid mapped folder %#v cannot be inside or contain the user home dir %#v",
folder.MappedPath, user.GetHomeDir())) folder.MappedPath, user.GetHomeDir()))
} }
for mPath := range mappedPaths { for mPath := range mappedPaths {
if folder.IsLocalOrLocalCrypted() && isMappedDirOverlapped(mPath, cleanedMPath, false) { if folder.IsLocalOrLocalCrypted() && isMappedDirOverlapped(mPath, cleanedMPath, false) {
return utils.NewValidationError(fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v", return util.NewValidationError(fmt.Sprintf("invalid mapped folder %#v overlaps with mapped folder %#v",
v.MappedPath, mPath)) v.MappedPath, mPath))
} }
} }
@ -1182,7 +1184,7 @@ func validateUserVirtualFolders(user *User) error {
} }
for vPath := range virtualPaths { for vPath := range virtualPaths {
if isVirtualDirOverlapped(vPath, cleanedVPath, false) { if isVirtualDirOverlapped(vPath, cleanedVPath, false) {
return utils.NewValidationError(fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v", return util.NewValidationError(fmt.Sprintf("invalid virtual folder %#v overlaps with virtual folder %#v",
v.VirtualPath, vPath)) v.VirtualPath, vPath))
} }
} }
@ -1200,22 +1202,22 @@ func validateUserVirtualFolders(user *User) error {
func validatePermissions(user *User) error { func validatePermissions(user *User) error {
if len(user.Permissions) == 0 { if len(user.Permissions) == 0 {
return utils.NewValidationError("please grant some permissions to this user") return util.NewValidationError("please grant some permissions to this user")
} }
permissions := make(map[string][]string) permissions := make(map[string][]string)
if _, ok := user.Permissions["/"]; !ok { if _, ok := user.Permissions["/"]; !ok {
return utils.NewValidationError("permissions for the root dir \"/\" must be set") return util.NewValidationError("permissions for the root dir \"/\" must be set")
} }
for dir, perms := range user.Permissions { for dir, perms := range user.Permissions {
if len(perms) == 0 && dir == "/" { if len(perms) == 0 && dir == "/" {
return utils.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir)) return util.NewValidationError(fmt.Sprintf("no permissions granted for the directory: %#v", dir))
} }
if len(perms) > len(ValidPerms) { if len(perms) > len(ValidPerms) {
return utils.NewValidationError("invalid permissions") return util.NewValidationError("invalid permissions")
} }
for _, p := range perms { for _, p := range perms {
if !utils.IsStringInSlice(p, ValidPerms) { if !util.IsStringInSlice(p, ValidPerms) {
return utils.NewValidationError(fmt.Sprintf("invalid permission: %#v", p)) return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", p))
} }
} }
cleanedDir := filepath.ToSlash(path.Clean(dir)) cleanedDir := filepath.ToSlash(path.Clean(dir))
@ -1223,15 +1225,15 @@ func validatePermissions(user *User) error {
cleanedDir = strings.TrimSuffix(cleanedDir, "/") cleanedDir = strings.TrimSuffix(cleanedDir, "/")
} }
if !path.IsAbs(cleanedDir) { if !path.IsAbs(cleanedDir) {
return utils.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir)) return util.NewValidationError(fmt.Sprintf("cannot set permissions for non absolute path: %#v", dir))
} }
if dir != cleanedDir && cleanedDir == "/" { if dir != cleanedDir && cleanedDir == "/" {
return utils.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir)) return util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %#v is an alias for \"/\"", dir))
} }
if utils.IsStringInSlice(PermAny, perms) { if util.IsStringInSlice(PermAny, perms) {
permissions[cleanedDir] = []string{PermAny} permissions[cleanedDir] = []string{PermAny}
} else { } else {
permissions[cleanedDir] = utils.RemoveDuplicates(perms) permissions[cleanedDir] = util.RemoveDuplicates(perms)
} }
} }
user.Permissions = permissions user.Permissions = permissions
@ -1249,31 +1251,31 @@ func validatePublicKeys(user *User) error {
} }
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not parse key nr. %d: %s", i+1, err)) return util.NewValidationError(fmt.Sprintf("could not parse key nr. %d: %s", i+1, err))
} }
validatedKeys = append(validatedKeys, k) validatedKeys = append(validatedKeys, k)
} }
user.PublicKeys = utils.RemoveDuplicates(validatedKeys) user.PublicKeys = util.RemoveDuplicates(validatedKeys)
return nil return nil
} }
func validateFiltersPatternExtensions(user *User) error { func validateFiltersPatternExtensions(user *User) error {
if len(user.Filters.FilePatterns) == 0 { if len(user.Filters.FilePatterns) == 0 {
user.Filters.FilePatterns = []PatternsFilter{} user.Filters.FilePatterns = []sdk.PatternsFilter{}
return nil return nil
} }
filteredPaths := []string{} filteredPaths := []string{}
var filters []PatternsFilter var filters []sdk.PatternsFilter
for _, f := range user.Filters.FilePatterns { for _, f := range user.Filters.FilePatterns {
cleanedPath := filepath.ToSlash(path.Clean(f.Path)) cleanedPath := filepath.ToSlash(path.Clean(f.Path))
if !path.IsAbs(cleanedPath) { if !path.IsAbs(cleanedPath) {
return utils.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path)) return util.NewValidationError(fmt.Sprintf("invalid path %#v for file patterns filter", f.Path))
} }
if utils.IsStringInSlice(cleanedPath, filteredPaths) { if util.IsStringInSlice(cleanedPath, filteredPaths) {
return utils.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path)) return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %#v", f.Path))
} }
if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 {
return utils.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path)) return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %#v", f.Path))
} }
f.Path = cleanedPath f.Path = cleanedPath
allowed := make([]string, 0, len(f.AllowedPatterns)) allowed := make([]string, 0, len(f.AllowedPatterns))
@ -1281,14 +1283,14 @@ func validateFiltersPatternExtensions(user *User) error {
for _, pattern := range f.AllowedPatterns { for _, pattern := range f.AllowedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern))
} }
allowed = append(allowed, strings.ToLower(pattern)) allowed = append(allowed, strings.ToLower(pattern))
} }
for _, pattern := range f.DeniedPatterns { for _, pattern := range f.DeniedPatterns {
_, err := path.Match(pattern, "abc") _, err := path.Match(pattern, "abc")
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern)) return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %#v", pattern))
} }
denied = append(denied, strings.ToLower(pattern)) denied = append(denied, strings.ToLower(pattern))
} }
@ -1321,46 +1323,46 @@ func validateFilters(user *User) error {
for _, IPMask := range user.Filters.DeniedIP { for _, IPMask := range user.Filters.DeniedIP {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err)) return util.NewValidationError(fmt.Sprintf("could not parse denied IP/Mask %#v : %v", IPMask, err))
} }
} }
for _, IPMask := range user.Filters.AllowedIP { for _, IPMask := range user.Filters.AllowedIP {
_, _, err := net.ParseCIDR(IPMask) _, _, err := net.ParseCIDR(IPMask)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err)) return util.NewValidationError(fmt.Sprintf("could not parse allowed IP/Mask %#v : %v", IPMask, err))
} }
} }
if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) { if len(user.Filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return utils.NewValidationError("invalid denied_login_methods") return util.NewValidationError("invalid denied_login_methods")
} }
for _, loginMethod := range user.Filters.DeniedLoginMethods { for _, loginMethod := range user.Filters.DeniedLoginMethods {
if !utils.IsStringInSlice(loginMethod, ValidLoginMethods) { if !util.IsStringInSlice(loginMethod, ValidLoginMethods) {
return utils.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod)) return util.NewValidationError(fmt.Sprintf("invalid login method: %#v", loginMethod))
} }
} }
if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) { if len(user.Filters.DeniedProtocols) >= len(ValidProtocols) {
return utils.NewValidationError("invalid denied_protocols") return util.NewValidationError("invalid denied_protocols")
} }
for _, p := range user.Filters.DeniedProtocols { for _, p := range user.Filters.DeniedProtocols {
if !utils.IsStringInSlice(p, ValidProtocols) { if !util.IsStringInSlice(p, ValidProtocols) {
return utils.NewValidationError(fmt.Sprintf("invalid protocol: %#v", p)) return util.NewValidationError(fmt.Sprintf("invalid protocol: %#v", p))
} }
} }
if user.Filters.TLSUsername != "" { if user.Filters.TLSUsername != "" {
if !utils.IsStringInSlice(string(user.Filters.TLSUsername), validTLSUsernames) { if !util.IsStringInSlice(string(user.Filters.TLSUsername), validTLSUsernames) {
return utils.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername)) return util.NewValidationError(fmt.Sprintf("invalid TLS username: %#v", user.Filters.TLSUsername))
} }
} }
for _, opts := range user.Filters.WebClient { for _, opts := range user.Filters.WebClient {
if !utils.IsStringInSlice(opts, WebClientOptions) { if !util.IsStringInSlice(opts, sdk.WebClientOptions) {
return utils.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts)) return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
} }
} }
return validateFiltersPatternExtensions(user) return validateFiltersPatternExtensions(user)
} }
func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) error { func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) error {
if fsConfig.Provider != vfs.GCSFilesystemProvider { if fsConfig.Provider != sdk.GCSFilesystemProvider {
return nil return nil
} }
if fsConfig.GCSConfig.Credentials.GetPayload() == "" { if fsConfig.GCSConfig.Credentials.GetPayload() == "" {
@ -1380,21 +1382,21 @@ func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) er
fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData()) fsConfig.GCSConfig.Credentials.SetAdditionalData(helper.GetEncryptionAdditionalData())
err := fsConfig.GCSConfig.Credentials.Encrypt() err := fsConfig.GCSConfig.Credentials.Encrypt()
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err)) return util.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err))
} }
} }
creds, err := json.Marshal(fsConfig.GCSConfig.Credentials) creds, err := json.Marshal(fsConfig.GCSConfig.Credentials)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not marshal GCS credentials: %v", err)) return util.NewValidationError(fmt.Sprintf("could not marshal GCS credentials: %v", err))
} }
credentialsFilePath := helper.GetGCSCredentialsFilePath() credentialsFilePath := helper.GetGCSCredentialsFilePath()
err = os.MkdirAll(filepath.Dir(credentialsFilePath), 0700) err = os.MkdirAll(filepath.Dir(credentialsFilePath), 0700)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not create GCS credentials dir: %v", err)) return util.NewValidationError(fmt.Sprintf("could not create GCS credentials dir: %v", err))
} }
err = os.WriteFile(credentialsFilePath, creds, 0600) err = os.WriteFile(credentialsFilePath, creds, 0600)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("could not save GCS credentials: %v", err)) return util.NewValidationError(fmt.Sprintf("could not save GCS credentials: %v", err))
} }
fsConfig.GCSConfig.Credentials = kms.NewEmptySecret() fsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
return nil return nil
@ -1402,20 +1404,20 @@ func saveGCSCredentials(fsConfig *vfs.Filesystem, helper vfs.ValidatorHelper) er
func validateBaseParams(user *User) error { func validateBaseParams(user *User) error {
if user.Username == "" { if user.Username == "" {
return utils.NewValidationError("username is mandatory") return util.NewValidationError("username is mandatory")
} }
if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(user.Username) { if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(user.Username) {
return utils.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
user.Username)) user.Username))
} }
if user.HomeDir == "" { if user.HomeDir == "" {
return utils.NewValidationError("home_dir is mandatory") return util.NewValidationError("home_dir is mandatory")
} }
if user.Password == "" && len(user.PublicKeys) == 0 { if user.Password == "" && len(user.PublicKeys) == 0 {
return utils.NewValidationError("please set a password or at least a public_key") return util.NewValidationError("please set a password or at least a public_key")
} }
if !filepath.IsAbs(user.HomeDir) { if !filepath.IsAbs(user.HomeDir) {
return utils.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)) return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir))
} }
return nil return nil
} }
@ -1443,17 +1445,17 @@ func createUserPasswordHash(user *User) error {
// FIXME: this should be defined as Folder struct method // FIXME: this should be defined as Folder struct method
func ValidateFolder(folder *vfs.BaseVirtualFolder) error { func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
if folder.Name == "" { if folder.Name == "" {
return utils.NewValidationError("folder name is mandatory") return util.NewValidationError("folder name is mandatory")
} }
if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(folder.Name) { if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(folder.Name) {
return utils.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", return util.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
folder.Name)) folder.Name))
} }
if folder.FsConfig.Provider == vfs.LocalFilesystemProvider || folder.FsConfig.Provider == vfs.CryptedFilesystemProvider || if folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider ||
folder.MappedPath != "" { folder.MappedPath != "" {
cleanedMPath := filepath.Clean(folder.MappedPath) cleanedMPath := filepath.Clean(folder.MappedPath)
if !filepath.IsAbs(cleanedMPath) { if !filepath.IsAbs(cleanedMPath) {
return utils.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath)) return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %#v", folder.MappedPath))
} }
folder.MappedPath = cleanedMPath folder.MappedPath = cleanedMPath
} }
@ -1487,7 +1489,7 @@ func ValidateUser(user *User) error {
return err return err
} }
if user.Status < 0 || user.Status > 1 { if user.Status < 0 || user.Status > 1 {
return utils.NewValidationError(fmt.Sprintf("invalid user status: %v", user.Status)) return util.NewValidationError(fmt.Sprintf("invalid user status: %v", user.Status))
} }
if err := createUserPasswordHash(user); err != nil { if err := createUserPasswordHash(user); err != nil {
return err return err
@ -1505,9 +1507,9 @@ func checkLoginConditions(user *User) error {
if user.Status < 1 { if user.Status < 1 {
return fmt.Errorf("user %#v is disabled", user.Username) return fmt.Errorf("user %#v is disabled", user.Username)
} }
if user.ExpirationDate > 0 && user.ExpirationDate < utils.GetTimeAsMsSinceEpoch(time.Now()) { if user.ExpirationDate > 0 && user.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
return fmt.Errorf("user %#v is expired, expiration timestamp: %v current timestamp: %v", user.Username, return fmt.Errorf("user %#v is expired, expiration timestamp: %v current timestamp: %v", user.Username,
user.ExpirationDate, utils.GetTimeAsMsSinceEpoch(time.Now())) user.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
} }
return nil return nil
} }
@ -1534,12 +1536,12 @@ func isPasswordOK(user *User, password string) (bool, error) {
return match, ErrInvalidCredentials return match, ErrInvalidCredentials
} }
match = true match = true
} else if utils.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) { } else if util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
match, err = comparePbkdf2PasswordAndHash(password, user.Password) match, err = comparePbkdf2PasswordAndHash(password, user.Password)
if err != nil { if err != nil {
return match, err return match, err
} }
} else if utils.IsStringPrefixInSlice(user.Password, unixPwdPrefixes) { } else if util.IsStringPrefixInSlice(user.Password, unixPwdPrefixes) {
match, err = compareUnixPasswordAndHash(user, password) match, err = compareUnixPasswordAndHash(user, password)
if err != nil { if err != nil {
return match, err return match, err
@ -1558,7 +1560,7 @@ func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certi
} }
switch protocol { switch protocol {
case "FTP", "DAV": case "FTP", "DAV":
if user.Filters.TLSUsername == TLSUsernameCN { if user.Filters.TLSUsername == sdk.TLSUsernameCN {
if user.Username == tlsCert.Subject.CommonName { if user.Username == tlsCert.Subject.CommonName {
return *user, nil return *user, nil
} }
@ -1664,7 +1666,7 @@ func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error)
return false, err return false, err
} }
var salt []byte var salt []byte
if utils.IsStringPrefixInSlice(hashedPassword, pbkdfPwdB64SaltPrefixes) { if util.IsStringPrefixInSlice(hashedPassword, pbkdfPwdB64SaltPrefixes) {
salt, err = base64.StdEncoding.DecodeString(vals[3]) salt, err = base64.StdEncoding.DecodeString(vals[3])
if err != nil { if err != nil {
return false, err return false, err
@ -1690,7 +1692,7 @@ func addCredentialsToUser(user *User) error {
if err := addFolderCredentialsToUser(user); err != nil { if err := addFolderCredentialsToUser(user); err != nil {
return err return err
} }
if user.FsConfig.Provider != vfs.GCSFilesystemProvider { if user.FsConfig.Provider != sdk.GCSFilesystemProvider {
return nil return nil
} }
if user.FsConfig.GCSConfig.AutomaticCredentials > 0 { if user.FsConfig.GCSConfig.AutomaticCredentials > 0 {
@ -1712,7 +1714,7 @@ func addCredentialsToUser(user *User) error {
func addFolderCredentialsToUser(user *User) error { func addFolderCredentialsToUser(user *User) error {
for idx := range user.VirtualFolders { for idx := range user.VirtualFolders {
f := &user.VirtualFolders[idx] f := &user.VirtualFolders[idx]
if f.FsConfig.Provider != vfs.GCSFilesystemProvider { if f.FsConfig.Provider != sdk.GCSFilesystemProvider {
continue continue
} }
if f.FsConfig.GCSConfig.AutomaticCredentials > 0 { if f.FsConfig.GCSConfig.AutomaticCredentials > 0 {
@ -1780,7 +1782,7 @@ func checkDataprovider() {
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "check availability error: %v", err) providerLog(logger.LevelWarn, "check availability error: %v", err)
} }
metrics.UpdateDataProviderAvailability(err) metric.UpdateDataProviderAvailability(err)
} }
func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) { func terminateInteractiveAuthProgram(cmd *exec.Cmd, isFinished bool) {
@ -2117,11 +2119,11 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro
return u, fmt.Errorf("pre-login hook error: %v, elapsed %v", err, time.Since(startTime)) return u, fmt.Errorf("pre-login hook error: %v, elapsed %v", err, time.Since(startTime))
} }
providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime)) providerLog(logger.LevelDebug, "pre-login hook completed, elapsed: %v", time.Since(startTime))
if utils.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v", providerLog(logger.LevelDebug, "empty response from pre-login hook, no modification requested for user %#v id: %v",
username, u.ID) username, u.ID)
if u.ID == 0 { if u.ID == 0 {
return u, utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
} }
return u, nil return u, nil
} }
@ -2230,7 +2232,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
var tlsCert string var tlsCert string
if cert != nil { if cert != nil {
var err error var err error
tlsCert, err = utils.EncodeTLSCertToPem(cert) tlsCert, err = util.EncodeTLSCertToPem(cert)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2285,7 +2287,7 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
if password != "" { if password != "" {
user.Password = password user.Password = password
} }
if pkey != "" && !utils.IsStringPrefixInSlice(pkey, user.PublicKeys) { if pkey != "" && !util.IsStringPrefixInSlice(pkey, user.PublicKeys) {
user.PublicKeys = append(user.PublicKeys, pkey) user.PublicKeys = append(user.PublicKeys, pkey)
} }
} }
@ -2302,7 +2304,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
return u, nil return u, nil
} }
pkey, err := utils.GetSSHPublicKeyAsString(pubKey) pkey, err := util.GetSSHPublicKeyAsString(pubKey)
if err != nil { if err != nil {
return user, err return user, err
} }
@ -2313,11 +2315,11 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
return user, fmt.Errorf("external auth error: %v, elapsed: %v", err, time.Since(startTime)) return user, fmt.Errorf("external auth error: %v, elapsed: %v", err, time.Since(startTime))
} }
providerLog(logger.LevelDebug, "external auth completed, elapsed: %v", time.Since(startTime)) providerLog(logger.LevelDebug, "external auth completed, elapsed: %v", time.Since(startTime))
if utils.IsByteArrayEmpty(out) { if util.IsByteArrayEmpty(out) {
providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v", providerLog(logger.LevelDebug, "empty response from external hook, no modification requested for user %#v id: %v",
username, u.ID) username, u.ID)
if u.ID == 0 { if u.ID == 0 {
return u, utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return u, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
} }
return u, nil return u, nil
} }
@ -2361,12 +2363,14 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
var userAsJSON []byte var userAsJSON []byte
u, err := provider.userExists(username) u, err := provider.userExists(username)
if err != nil { if err != nil {
if _, ok := err.(*utils.RecordNotFoundError); !ok { if _, ok := err.(*util.RecordNotFoundError); !ok {
return u, userAsJSON, err return u, userAsJSON, err
} }
u = User{ u = User{
ID: 0, BaseUser: sdk.BaseUser{
Username: username, ID: 0,
Username: username,
},
} }
} }
userAsJSON, err = json.Marshal(u) userAsJSON, err = json.Marshal(u)
@ -2403,7 +2407,8 @@ func executeNotificationCommand(operation string, commandArgs []string, userAsJS
} }
func executeAction(operation string, user *User) { func executeAction(operation string, user *User) {
if !utils.IsStringInSlice(operation, config.Actions.ExecuteOn) { plugin.Handler.NotifyUserEvent(operation, user)
if !util.IsStringInSlice(operation, config.Actions.ExecuteOn) {
return return
} }
if config.Actions.Hook == "" { if config.Actions.Hook == "" {
@ -2411,17 +2416,8 @@ func executeAction(operation string, user *User) {
} }
go func() { go func() {
if operation != operationDelete {
var err error
u, err := provider.userExists(user.Username)
if err != nil {
providerLog(logger.LevelWarn, "unable to get the user to notify for operation %#v: %v", operation, err)
return
}
user = &u
}
user.PrepareForRendering() user.PrepareForRendering()
userAsJSON, err := json.Marshal(user) userAsJSON, err := user.RenderAsJSON(operation != operationDelete)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "unable to serialize user as JSON for operation %#v: %v", operation, err) providerLog(logger.LevelWarn, "unable to serialize user as JSON for operation %#v: %v", operation, err)
return return

View file

@ -11,7 +11,7 @@ import (
"time" "time"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -45,7 +45,7 @@ type MemoryProvider struct {
func initializeMemoryProvider(basePath string) { func initializeMemoryProvider(basePath string) {
configFile := "" configFile := ""
if utils.IsFileInputValid(config.Name) { if util.IsFileInputValid(config.Name) {
configFile = config.Name configFile = config.Name
if !filepath.IsAbs(configFile) { if !filepath.IsAbs(configFile) {
configFile = filepath.Join(basePath, configFile) configFile = filepath.Join(basePath, configFile)
@ -147,7 +147,7 @@ func (p *MemoryProvider) updateLastLogin(username string) error {
if err != nil { if err != nil {
return err return err
} }
user.LastLogin = utils.GetTimeAsMsSinceEpoch(time.Now()) user.LastLogin = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.users[user.Username] = user p.dbHandle.users[user.Username] = user
return nil return nil
} }
@ -170,7 +170,7 @@ func (p *MemoryProvider) updateQuota(username string, filesAdd int, sizeAdd int6
user.UsedQuotaSize += sizeAdd user.UsedQuotaSize += sizeAdd
user.UsedQuotaFiles += filesAdd user.UsedQuotaFiles += filesAdd
} }
user.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now()) user.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
providerLog(logger.LevelDebug, "quota updated for user %#v, files increment: %v size increment: %v is reset? %v", providerLog(logger.LevelDebug, "quota updated for user %#v, files increment: %v size increment: %v is reset? %v",
username, filesAdd, sizeAdd, reset) username, filesAdd, sizeAdd, reset)
p.dbHandle.users[user.Username] = user p.dbHandle.users[user.Username] = user
@ -367,7 +367,7 @@ func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
if val, ok := p.dbHandle.users[username]; ok { if val, ok := p.dbHandle.users[username]; ok {
return val.getACopy(), nil return val.getACopy(), nil
} }
return User{}, utils.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return User{}, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
} }
func (p *MemoryProvider) addAdmin(admin *Admin) error { func (p *MemoryProvider) addAdmin(admin *Admin) error {
@ -444,7 +444,7 @@ func (p *MemoryProvider) adminExistsInternal(username string) (Admin, error) {
if val, ok := p.dbHandle.admins[username]; ok { if val, ok := p.dbHandle.admins[username]; ok {
return val.getACopy(), nil return val.getACopy(), nil
} }
return Admin{}, utils.NewRecordNotFoundError(fmt.Sprintf("admin %#v does not exist", username)) return Admin{}, util.NewRecordNotFoundError(fmt.Sprintf("admin %#v does not exist", username))
} }
func (p *MemoryProvider) dumpAdmins() ([]Admin, error) { func (p *MemoryProvider) dumpAdmins() ([]Admin, error) {
@ -526,7 +526,7 @@ func (p *MemoryProvider) updateFolderQuota(name string, filesAdd int, sizeAdd in
folder.UsedQuotaSize += sizeAdd folder.UsedQuotaSize += sizeAdd
folder.UsedQuotaFiles += filesAdd folder.UsedQuotaFiles += filesAdd
} }
folder.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now()) folder.LastQuotaUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.vfolders[name] = folder p.dbHandle.vfolders[name] = folder
return nil return nil
} }
@ -574,7 +574,7 @@ func (p *MemoryProvider) removeUserFromFolderMapping(folderName, username string
func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) { func (p *MemoryProvider) updateFoldersMappingInternal(folder vfs.BaseVirtualFolder) {
p.dbHandle.vfolders[folder.Name] = folder p.dbHandle.vfolders[folder.Name] = folder
if !utils.IsStringInSlice(folder.Name, p.dbHandle.vfoldersNames) { if !util.IsStringInSlice(folder.Name, p.dbHandle.vfoldersNames) {
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name) p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
sort.Strings(p.dbHandle.vfoldersNames) sort.Strings(p.dbHandle.vfoldersNames)
} }
@ -588,13 +588,13 @@ func (p *MemoryProvider) addOrUpdateFolderInternal(baseFolder *vfs.BaseVirtualFo
folder.MappedPath = baseFolder.MappedPath folder.MappedPath = baseFolder.MappedPath
folder.Description = baseFolder.Description folder.Description = baseFolder.Description
folder.FsConfig = baseFolder.FsConfig.GetACopy() folder.FsConfig = baseFolder.FsConfig.GetACopy()
if !utils.IsStringInSlice(username, folder.Users) { if !util.IsStringInSlice(username, folder.Users) {
folder.Users = append(folder.Users, username) folder.Users = append(folder.Users, username)
} }
p.updateFoldersMappingInternal(folder) p.updateFoldersMappingInternal(folder)
return folder, nil return folder, nil
} }
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
folder = baseFolder.GetACopy() folder = baseFolder.GetACopy()
folder.ID = p.getNextFolderID() folder.ID = p.getNextFolderID()
folder.UsedQuotaSize = usedQuotaSize folder.UsedQuotaSize = usedQuotaSize
@ -611,7 +611,7 @@ func (p *MemoryProvider) folderExistsInternal(name string) (vfs.BaseVirtualFolde
if val, ok := p.dbHandle.vfolders[name]; ok { if val, ok := p.dbHandle.vfolders[name]; ok {
return val, nil return val, nil
} }
return vfs.BaseVirtualFolder{}, utils.NewRecordNotFoundError(fmt.Sprintf("folder %#v does not exist", name)) return vfs.BaseVirtualFolder{}, util.NewRecordNotFoundError(fmt.Sprintf("folder %#v does not exist", name))
} }
func (p *MemoryProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) { func (p *MemoryProvider) getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error) {

View file

@ -13,7 +13,8 @@ import (
"github.com/cockroachdb/cockroach-go/v2/crdb" "github.com/cockroachdb/cockroach-go/v2/crdb"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -271,7 +272,7 @@ func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bo
return err return err
} }
defer stmt.Close() defer stmt.Close()
_, err = stmt.ExecContext(ctx, sizeAdd, filesAdd, utils.GetTimeAsMsSinceEpoch(time.Now()), username) _, err = stmt.ExecContext(ctx, sizeAdd, filesAdd, util.GetTimeAsMsSinceEpoch(time.Now()), username)
if err == nil { if err == nil {
providerLog(logger.LevelDebug, "quota updated for user %#v, files increment: %v size increment: %v is reset? %v", providerLog(logger.LevelDebug, "quota updated for user %#v, files increment: %v size increment: %v is reset? %v",
username, filesAdd, sizeAdd, reset) username, filesAdd, sizeAdd, reset)
@ -312,7 +313,7 @@ func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
return err return err
} }
defer stmt.Close() defer stmt.Close()
_, err = stmt.ExecContext(ctx, utils.GetTimeAsMsSinceEpoch(time.Now()), username) _, err = stmt.ExecContext(ctx, util.GetTimeAsMsSinceEpoch(time.Now()), username)
if err == nil { if err == nil {
providerLog(logger.LevelDebug, "last login updated for user %#v", username) providerLog(logger.LevelDebug, "last login updated for user %#v", username)
} else { } else {
@ -494,7 +495,7 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return admin, utils.NewRecordNotFoundError(err.Error()) return admin, util.NewRecordNotFoundError(err.Error())
} }
return admin, err return admin, err
} }
@ -543,7 +544,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
&additionalInfo, &description) &additionalInfo, &description)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, utils.NewRecordNotFoundError(err.Error()) return user, util.NewRecordNotFoundError(err.Error())
} }
return user, err return user, err
} }
@ -570,7 +571,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
user.Permissions = perms user.Permissions = perms
} }
if filters.Valid { if filters.Valid {
var userFilters UserFilters var userFilters sdk.UserFilters
err = json.Unmarshal([]byte(filters.String), &userFilters) err = json.Unmarshal([]byte(filters.String), &userFilters)
if err == nil { if err == nil {
user.Filters = userFilters user.Filters = userFilters
@ -620,7 +621,7 @@ func sqlCommonGetFolder(ctx context.Context, name string, dbHandle sqlQuerier) (
err = row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate, err = row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,
&folder.Name, &description, &fsConfig) &folder.Name, &description, &fsConfig)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return folder, utils.NewRecordNotFoundError(err.Error()) return folder, util.NewRecordNotFoundError(err.Error())
} }
if mappedPath.Valid { if mappedPath.Valid {
folder.MappedPath = mappedPath.String folder.MappedPath = mappedPath.String
@ -998,7 +999,7 @@ func sqlCommonUpdateFolderQuota(name string, filesAdd int, sizeAdd int64, reset
return err return err
} }
defer stmt.Close() defer stmt.Close()
_, err = stmt.ExecContext(ctx, sizeAdd, filesAdd, utils.GetTimeAsMsSinceEpoch(time.Now()), name) _, err = stmt.ExecContext(ctx, sizeAdd, filesAdd, util.GetTimeAsMsSinceEpoch(time.Now()), name)
if err == nil { if err == nil {
providerLog(logger.LevelDebug, "quota updated for folder %#v, files increment: %v size increment: %v is reset? %v", providerLog(logger.LevelDebug, "quota updated for folder %#v, files increment: %v size increment: %v is reset? %v",
name, filesAdd, sizeAdd, reset) name, filesAdd, sizeAdd, reset)

View file

@ -15,7 +15,7 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -60,7 +60,7 @@ func initializeSQLiteProvider(basePath string) error {
if config.ConnectionString == "" { if config.ConnectionString == "" {
dbPath := config.Name dbPath := config.Name
if !utils.IsFileInputValid(dbPath) { if !util.IsFileInputValid(dbPath) {
return fmt.Errorf("invalid database path: %#v", dbPath) return fmt.Errorf("invalid database path: %#v", dbPath)
} }
if !filepath.IsAbs(dbPath) { if !filepath.IsAbs(dbPath) {

View file

@ -17,7 +17,8 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -50,16 +51,6 @@ const (
PermChtimes = "chtimes" PermChtimes = "chtimes"
) )
// Web Client restrictions
const (
WebClientPubKeyChangeDisabled = "publickey-change-disabled"
)
var (
// WebClientOptions defines the available options for the web client interface
WebClientOptions = []string{WebClientPubKeyChangeDisabled}
)
// Available login methods // Available login methods
const ( const (
LoginMethodNoAuthTryed = "no_auth_tryed" LoginMethodNoAuthTryed = "no_auth_tryed"
@ -72,168 +63,17 @@ const (
LoginMethodTLSCertificateAndPwd = "TLSCertificate+password" LoginMethodTLSCertificateAndPwd = "TLSCertificate+password"
) )
// TLSUsername defines the TLS certificate attribute to use as username
type TLSUsername string
// Supported certificate attributes to use as username
const (
TLSUsernameNone TLSUsername = "None"
TLSUsernameCN TLSUsername = "CommonName"
)
var ( var (
errNoMatchingVirtualFolder = errors.New("no matching virtual folder found") errNoMatchingVirtualFolder = errors.New("no matching virtual folder found")
) )
// DirectoryPermissions defines permissions for a directory path
type DirectoryPermissions struct {
Path string
Permissions []string
}
// HasPerm returns true if the directory has the specified permissions
func (d *DirectoryPermissions) HasPerm(perm string) bool {
return utils.IsStringInSlice(perm, d.Permissions)
}
// PatternsFilter defines filters based on shell like patterns.
// These restrictions do not apply to files listing for performance reasons, so
// a denied file cannot be downloaded/overwritten/renamed but will still be
// in the list of files.
// System commands such as Git and rsync interacts with the filesystem directly
// and they are not aware about these restrictions so they are not allowed
// inside paths with extensions filters
type PatternsFilter struct {
// Virtual path, if no other specific filter is defined, the filter apply for
// sub directories too.
// For example if filters are defined for the paths "/" and "/sub" then the
// filters for "/" are applied for any file outside the "/sub" directory
Path string `json:"path"`
// files with these, case insensitive, patterns are allowed.
// Denied file patterns are evaluated before the allowed ones
AllowedPatterns []string `json:"allowed_patterns,omitempty"`
// files with these, case insensitive, patterns are not allowed.
// Denied file patterns are evaluated before the allowed ones
DeniedPatterns []string `json:"denied_patterns,omitempty"`
}
// GetCommaSeparatedPatterns returns the first non empty patterns list comma separated
func (p *PatternsFilter) GetCommaSeparatedPatterns() string {
if len(p.DeniedPatterns) > 0 {
return strings.Join(p.DeniedPatterns, ",")
}
return strings.Join(p.AllowedPatterns, ",")
}
// IsDenied returns true if the patterns has one or more denied patterns
func (p *PatternsFilter) IsDenied() bool {
return len(p.DeniedPatterns) > 0
}
// IsAllowed returns true if the patterns has one or more allowed patterns
func (p *PatternsFilter) IsAllowed() bool {
return len(p.AllowedPatterns) > 0
}
// HooksFilter defines user specific overrides for global hooks
type HooksFilter struct {
ExternalAuthDisabled bool `json:"external_auth_disabled"`
PreLoginDisabled bool `json:"pre_login_disabled"`
CheckPasswordDisabled bool `json:"check_password_disabled"`
}
// UserFilters defines additional restrictions for a user
// TODO: rename to UserOptions in v3
type UserFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
// for example "192.0.2.0/24" or "2001:db8::/32"
AllowedIP []string `json:"allowed_ip,omitempty"`
// clients connecting from these IP/Mask are not allowed.
// Denied rules will be evaluated before allowed ones
DeniedIP []string `json:"denied_ip,omitempty"`
// these login methods are not allowed.
// If null or empty any available login method is allowed
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
// these protocols are not allowed.
// If null or empty any available protocol is allowed
DeniedProtocols []string `json:"denied_protocols,omitempty"`
// filter based on shell patterns.
// Please note that these restrictions can be easily bypassed.
FilePatterns []PatternsFilter `json:"file_patterns,omitempty"`
// max size allowed for a single upload, 0 means unlimited
MaxUploadFileSize int64 `json:"max_upload_file_size,omitempty"`
// TLS certificate attribute to use as username.
// For FTP clients it must match the name provided using the
// "USER" command
TLSUsername TLSUsername `json:"tls_username,omitempty"`
// user specific hook overrides
Hooks HooksFilter `json:"hooks,omitempty"`
// Disable checks for existence and automatic creation of home directory
// and virtual folders.
// SFTPGo requires that the user's home directory, virtual folder root,
// and intermediate paths to virtual folders exist to work properly.
// If you already know that the required directories exist, disabling
// these checks will speed up login.
// You could, for example, disable these checks after the first login
DisableFsChecks bool `json:"disable_fs_checks,omitempty"`
// WebClient related configuration options
WebClient []string `json:"web_client,omitempty"`
}
// User defines a SFTPGo user // User defines a SFTPGo user
type User struct { type User struct {
// Database unique identifier sdk.BaseUser
ID int64 `json:"id"`
// 1 enabled, 0 disabled (login is not allowed)
Status int `json:"status"`
// Username
Username string `json:"username"`
// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.
// 0 means no expiration
ExpirationDate int64 `json:"expiration_date"`
// Password used for password authentication.
// For users created using SFTPGo REST API the password is be stored using bcrypt or argon2id hashing algo.
// Checking passwords stored with pbkdf2, md5crypt and sha512crypt is supported too.
Password string `json:"password,omitempty"`
// PublicKeys used for public key authentication. At least one between password and a public key is mandatory
PublicKeys []string `json:"public_keys,omitempty"`
// The user cannot upload or download files outside this directory. Must be an absolute path
HomeDir string `json:"home_dir"`
// Mapping between virtual paths and virtual folders // Mapping between virtual paths and virtual folders
VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"` VirtualFolders []vfs.VirtualFolder `json:"virtual_folders,omitempty"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
UID int `json:"uid"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
GID int `json:"gid"`
// Maximum concurrent sessions. 0 means unlimited
MaxSessions int `json:"max_sessions"`
// Maximum size allowed as bytes. 0 means unlimited
QuotaSize int64 `json:"quota_size"`
// Maximum number of files allowed. 0 means unlimited
QuotaFiles int `json:"quota_files"`
// List of the granted permissions
Permissions map[string][]string `json:"permissions"`
// Used quota as bytes
UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files
UsedQuotaFiles int `json:"used_quota_files"`
// Last quota update as unix timestamp in milliseconds
LastQuotaUpdate int64 `json:"last_quota_update"`
// Maximum upload bandwidth as KB/s, 0 means unlimited
UploadBandwidth int64 `json:"upload_bandwidth"`
// Maximum download bandwidth as KB/s, 0 means unlimited
DownloadBandwidth int64 `json:"download_bandwidth"`
// Last login as unix timestamp in milliseconds
LastLogin int64 `json:"last_login"`
// Additional restrictions
Filters UserFilters `json:"filters"`
// Filesystem configuration details // Filesystem configuration details
FsConfig vfs.Filesystem `json:"filesystem"` FsConfig vfs.Filesystem `json:"filesystem"`
// optional description, for example full name
Description string `json:"description,omitempty"`
// free form text field for external systems
AdditionalInfo string `json:"additional_info,omitempty"`
// we store the filesystem here using the base path as key. // we store the filesystem here using the base path as key.
fsCache map[string]vfs.Fs `json:"-"` fsCache map[string]vfs.Fs `json:"-"`
} }
@ -251,17 +91,17 @@ func (u *User) GetFilesystem(connectionID string) (fs vfs.Fs, err error) {
func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) { func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {
switch u.FsConfig.Provider { switch u.FsConfig.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
return vfs.NewS3Fs(connectionID, u.GetHomeDir(), "", u.FsConfig.S3Config) return vfs.NewS3Fs(connectionID, u.GetHomeDir(), "", u.FsConfig.S3Config)
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
config := u.FsConfig.GCSConfig config := u.FsConfig.GCSConfig
config.CredentialFile = u.GetGCSCredentialsFilePath() config.CredentialFile = u.GetGCSCredentialsFilePath()
return vfs.NewGCSFs(connectionID, u.GetHomeDir(), "", config) return vfs.NewGCSFs(connectionID, u.GetHomeDir(), "", config)
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
return vfs.NewAzBlobFs(connectionID, u.GetHomeDir(), "", u.FsConfig.AzBlobConfig) return vfs.NewAzBlobFs(connectionID, u.GetHomeDir(), "", u.FsConfig.AzBlobConfig)
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
return vfs.NewCryptFs(connectionID, u.GetHomeDir(), "", u.FsConfig.CryptConfig) return vfs.NewCryptFs(connectionID, u.GetHomeDir(), "", u.FsConfig.CryptConfig)
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
forbiddenSelfUsers, err := u.getForbiddenSFTPSelfUsers(u.FsConfig.SFTPConfig.Username) forbiddenSelfUsers, err := u.getForbiddenSFTPSelfUsers(u.FsConfig.SFTPConfig.Username)
if err != nil { if err != nil {
return nil, err return nil, err
@ -308,7 +148,7 @@ func (u *User) CheckFsRoot(connectionID string) error {
// isFsEqual returns true if the fs has the same configuration // isFsEqual returns true if the fs has the same configuration
func (u *User) isFsEqual(other *User) bool { func (u *User) isFsEqual(other *User) bool {
if u.FsConfig.Provider == vfs.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() { if u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {
return false return false
} }
if !u.FsConfig.IsEqual(&other.FsConfig) { if !u.FsConfig.IsEqual(&other.FsConfig) {
@ -324,7 +164,7 @@ func (u *User) isFsEqual(other *User) bool {
f1 := &other.VirtualFolders[idx1] f1 := &other.VirtualFolders[idx1]
if f.VirtualPath == f1.VirtualPath { if f.VirtualPath == f1.VirtualPath {
found = true found = true
if f.FsConfig.Provider == vfs.LocalFilesystemProvider && f.MappedPath != f1.MappedPath { if f.FsConfig.Provider == sdk.LocalFilesystemProvider && f.MappedPath != f1.MappedPath {
return false return false
} }
if !f.FsConfig.IsEqual(&f1.FsConfig) { if !f.FsConfig.IsEqual(&f1.FsConfig) {
@ -346,13 +186,13 @@ func (u *User) hideConfidentialData() {
} }
// GetSubDirPermissions returns permissions for sub directories // GetSubDirPermissions returns permissions for sub directories
func (u *User) GetSubDirPermissions() []DirectoryPermissions { func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
var result []DirectoryPermissions var result []sdk.DirectoryPermissions
for k, v := range u.Permissions { for k, v := range u.Permissions {
if k == "/" { if k == "/" {
continue continue
} }
dirPerms := DirectoryPermissions{ dirPerms := sdk.DirectoryPermissions{
Path: k, Path: k,
Permissions: v, Permissions: v,
} }
@ -361,6 +201,21 @@ func (u *User) GetSubDirPermissions() []DirectoryPermissions {
return result return result
} }
// RenderAsJSON implements the renderer interface used within plugins
func (u *User) RenderAsJSON(reload bool) ([]byte, error) {
if reload {
user, err := provider.userExists(u.Username)
if err != nil {
providerLog(logger.LevelWarn, "unable to reload user before rendering as json: %v", err)
return nil, err
}
user.PrepareForRendering()
return json.Marshal(user)
}
u.PrepareForRendering()
return json.Marshal(u)
}
// PrepareForRendering prepares a user for rendering. // PrepareForRendering prepares a user for rendering.
// It hides confidential data and set to nil the empty secrets // It hides confidential data and set to nil the empty secrets
// so they are not serialized // so they are not serialized
@ -406,14 +261,14 @@ func (u *User) CloseFs() error {
// IsPasswordHashed returns true if the password is hashed // IsPasswordHashed returns true if the password is hashed
func (u *User) IsPasswordHashed() bool { func (u *User) IsPasswordHashed() bool {
return utils.IsStringPrefixInSlice(u.Password, hashPwdPrefixes) return util.IsStringPrefixInSlice(u.Password, hashPwdPrefixes)
} }
// IsTLSUsernameVerificationEnabled returns true if we need to extract the username // IsTLSUsernameVerificationEnabled returns true if we need to extract the username
// from the client TLS certificate // from the client TLS certificate
func (u *User) IsTLSUsernameVerificationEnabled() bool { func (u *User) IsTLSUsernameVerificationEnabled() bool {
if u.Filters.TLSUsername != "" { if u.Filters.TLSUsername != "" {
return u.Filters.TLSUsername != TLSUsernameNone return u.Filters.TLSUsername != sdk.TLSUsernameNone
} }
return false return false
} }
@ -445,7 +300,7 @@ func (u *User) GetPermissionsForPath(p string) []string {
// fallback permissions // fallback permissions
permissions = perms permissions = perms
} }
dirsForPath := utils.GetDirsForVirtualPath(p) dirsForPath := util.GetDirsForVirtualPath(p)
// dirsForPath contains all the dirs for a given path in reverse order // dirsForPath contains all the dirs for a given path in reverse order
// for example if the path is: /1/2/3/4 it contains: // for example if the path is: /1/2/3/4 it contains:
// [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ] // [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
@ -464,20 +319,20 @@ func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
if err == nil { if err == nil {
// we don't allow local nested SFTP folders // we don't allow local nested SFTP folders
var forbiddens []string var forbiddens []string
if sftpUser.FsConfig.Provider == vfs.SFTPFilesystemProvider { if sftpUser.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens = append(forbiddens, sftpUser.Username) forbiddens = append(forbiddens, sftpUser.Username)
return forbiddens, nil return forbiddens, nil
} }
for idx := range sftpUser.VirtualFolders { for idx := range sftpUser.VirtualFolders {
v := &sftpUser.VirtualFolders[idx] v := &sftpUser.VirtualFolders[idx]
if v.FsConfig.Provider == vfs.SFTPFilesystemProvider { if v.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens = append(forbiddens, sftpUser.Username) forbiddens = append(forbiddens, sftpUser.Username)
return forbiddens, nil return forbiddens, nil
} }
} }
return forbiddens, nil return forbiddens, nil
} }
if _, ok := err.(*utils.RecordNotFoundError); !ok { if _, ok := err.(*util.RecordNotFoundError); !ok {
return nil, err return nil, err
} }
@ -508,7 +363,7 @@ func (u *User) GetFilesystemForPath(virtualPath, connectionID string) (vfs.Fs, e
return fs, nil return fs, nil
} }
forbiddenSelfUsers := []string{u.Username} forbiddenSelfUsers := []string{u.Username}
if folder.FsConfig.Provider == vfs.SFTPFilesystemProvider { if folder.FsConfig.Provider == sdk.SFTPFilesystemProvider {
forbiddens, err := u.getForbiddenSFTPSelfUsers(folder.FsConfig.SFTPConfig.Username) forbiddens, err := u.getForbiddenSFTPSelfUsers(folder.FsConfig.SFTPConfig.Username)
if err != nil { if err != nil {
return nil, err return nil, err
@ -537,7 +392,7 @@ func (u *User) GetVirtualFolderForPath(virtualPath string) (vfs.VirtualFolder, e
if len(u.VirtualFolders) == 0 { if len(u.VirtualFolders) == 0 {
return folder, errNoMatchingVirtualFolder return folder, errNoMatchingVirtualFolder
} }
dirsForPath := utils.GetDirsForVirtualPath(virtualPath) dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for index := range dirsForPath { for index := range dirsForPath {
for idx := range u.VirtualFolders { for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx] v := &u.VirtualFolders[idx]
@ -584,7 +439,7 @@ func (u *User) GetVirtualFoldersInPath(virtualPath string) map[string]bool {
for idx := range u.VirtualFolders { for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx] v := &u.VirtualFolders[idx]
dirsForPath := utils.GetDirsForVirtualPath(v.VirtualPath) dirsForPath := util.GetDirsForVirtualPath(v.VirtualPath)
for index := range dirsForPath { for index := range dirsForPath {
d := dirsForPath[index] d := dirsForPath[index]
if d == "/" { if d == "/" {
@ -680,20 +535,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
// HasPerm returns true if the user has the given permission or any permission // HasPerm returns true if the user has the given permission or any permission
func (u *User) HasPerm(permission, path string) bool { func (u *User) HasPerm(permission, path string) bool {
perms := u.GetPermissionsForPath(path) perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) { if util.IsStringInSlice(PermAny, perms) {
return true return true
} }
return utils.IsStringInSlice(permission, perms) return util.IsStringInSlice(permission, perms)
} }
// HasPerms return true if the user has all the given permissions // HasPerms return true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool { func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path) perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) { if util.IsStringInSlice(PermAny, perms) {
return true return true
} }
for _, permission := range permissions { for _, permission := range permissions {
if !utils.IsStringInSlice(permission, perms) { if !util.IsStringInSlice(permission, perms) {
return false return false
} }
} }
@ -720,7 +575,7 @@ func (u *User) IsLoginMethodAllowed(loginMethod string, partialSuccessMethods []
} }
} }
} }
if utils.IsStringInSlice(loginMethod, u.Filters.DeniedLoginMethods) { if util.IsStringInSlice(loginMethod, u.Filters.DeniedLoginMethods) {
return false return false
} }
return true return true
@ -760,7 +615,7 @@ func (u *User) IsPartialAuth(loginMethod string) bool {
if method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd { if method == LoginMethodTLSCertificate || method == LoginMethodTLSCertificateAndPwd {
continue continue
} }
if !utils.IsStringInSlice(method, SSHMultiStepsLoginMethods) { if !util.IsStringInSlice(method, SSHMultiStepsLoginMethods) {
return false return false
} }
} }
@ -771,7 +626,7 @@ func (u *User) IsPartialAuth(loginMethod string) bool {
func (u *User) GetAllowedLoginMethods() []string { func (u *User) GetAllowedLoginMethods() []string {
var allowedMethods []string var allowedMethods []string
for _, method := range ValidLoginMethods { for _, method := range ValidLoginMethods {
if !utils.IsStringInSlice(method, u.Filters.DeniedLoginMethods) { if !util.IsStringInSlice(method, u.Filters.DeniedLoginMethods) {
allowedMethods = append(allowedMethods, method) allowedMethods = append(allowedMethods, method)
} }
} }
@ -780,18 +635,18 @@ func (u *User) GetAllowedLoginMethods() []string {
// GetFlatFilePatterns returns file patterns as flat list // GetFlatFilePatterns returns file patterns as flat list
// duplicating a path if it has both allowed and denied patterns // duplicating a path if it has both allowed and denied patterns
func (u *User) GetFlatFilePatterns() []PatternsFilter { func (u *User) GetFlatFilePatterns() []sdk.PatternsFilter {
var result []PatternsFilter var result []sdk.PatternsFilter
for _, pattern := range u.Filters.FilePatterns { for _, pattern := range u.Filters.FilePatterns {
if len(pattern.AllowedPatterns) > 0 { if len(pattern.AllowedPatterns) > 0 {
result = append(result, PatternsFilter{ result = append(result, sdk.PatternsFilter{
Path: pattern.Path, Path: pattern.Path,
AllowedPatterns: pattern.AllowedPatterns, AllowedPatterns: pattern.AllowedPatterns,
}) })
} }
if len(pattern.DeniedPatterns) > 0 { if len(pattern.DeniedPatterns) > 0 {
result = append(result, PatternsFilter{ result = append(result, sdk.PatternsFilter{
Path: pattern.Path, Path: pattern.Path,
DeniedPatterns: pattern.DeniedPatterns, DeniedPatterns: pattern.DeniedPatterns,
}) })
@ -809,8 +664,8 @@ func (u *User) isFilePatternAllowed(virtualPath string) bool {
if len(u.Filters.FilePatterns) == 0 { if len(u.Filters.FilePatterns) == 0 {
return true return true
} }
dirsForPath := utils.GetDirsForVirtualPath(path.Dir(virtualPath)) dirsForPath := util.GetDirsForVirtualPath(path.Dir(virtualPath))
var filter PatternsFilter var filter sdk.PatternsFilter
for _, dir := range dirsForPath { for _, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns { for _, f := range u.Filters.FilePatterns {
if f.Path == dir { if f.Path == dir {
@ -844,7 +699,7 @@ func (u *User) isFilePatternAllowed(virtualPath string) bool {
// CanManagePublicKeys return true if this user is allowed to manage public keys // CanManagePublicKeys return true if this user is allowed to manage public keys
// from the web client // from the web client
func (u *User) CanManagePublicKeys() bool { func (u *User) CanManagePublicKeys() bool {
return !utils.IsStringInSlice(WebClientPubKeyChangeDisabled, u.Filters.WebClient) return !util.IsStringInSlice(sdk.WebClientPubKeyChangeDisabled, u.Filters.WebClient)
} }
// GetSignature returns a signature for this admin. // GetSignature returns a signature for this admin.
@ -864,7 +719,7 @@ func (u *User) IsLoginFromAddrAllowed(remoteAddr string) bool {
if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 { if len(u.Filters.AllowedIP) == 0 && len(u.Filters.DeniedIP) == 0 {
return true return true
} }
remoteIP := net.ParseIP(utils.GetIPFromRemoteAddress(remoteAddr)) remoteIP := net.ParseIP(util.GetIPFromRemoteAddress(remoteAddr))
// if remoteIP is invalid we allow login, this should never happen // if remoteIP is invalid we allow login, this should never happen
if remoteIP == nil { if remoteIP == nil {
logger.Warn(logSender, "", "login allowed for invalid IP. remote address: %#v", remoteAddr) logger.Warn(logSender, "", "login allowed for invalid IP. remote address: %#v", remoteAddr)
@ -945,13 +800,13 @@ func (u *User) GetQuotaSummary() string {
result += "/" + strconv.Itoa(u.QuotaFiles) result += "/" + strconv.Itoa(u.QuotaFiles)
} }
if u.UsedQuotaSize > 0 || u.QuotaSize > 0 { if u.UsedQuotaSize > 0 || u.QuotaSize > 0 {
result += ". Size: " + utils.ByteCountIEC(u.UsedQuotaSize) result += ". Size: " + util.ByteCountIEC(u.UsedQuotaSize)
if u.QuotaSize > 0 { if u.QuotaSize > 0 {
result += "/" + utils.ByteCountIEC(u.QuotaSize) result += "/" + util.ByteCountIEC(u.QuotaSize)
} }
} }
if u.LastQuotaUpdate > 0 { if u.LastQuotaUpdate > 0 {
t := utils.GetTimeFromMsecSinceEpoch(u.LastQuotaUpdate) t := util.GetTimeFromMsecSinceEpoch(u.LastQuotaUpdate)
result += fmt.Sprintf(". Last update: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM result += fmt.Sprintf(". Last update: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM
} }
return result return result
@ -983,13 +838,13 @@ func (u *User) GetPermissionsAsString() string {
func (u *User) GetBandwidthAsString() string { func (u *User) GetBandwidthAsString() string {
result := "DL: " result := "DL: "
if u.DownloadBandwidth > 0 { if u.DownloadBandwidth > 0 {
result += utils.ByteCountIEC(u.DownloadBandwidth*1000) + "/s." result += util.ByteCountIEC(u.DownloadBandwidth*1000) + "/s."
} else { } else {
result += "unlimited." result += "unlimited."
} }
result += " UL: " result += " UL: "
if u.UploadBandwidth > 0 { if u.UploadBandwidth > 0 {
result += utils.ByteCountIEC(u.UploadBandwidth*1000) + "/s." result += util.ByteCountIEC(u.UploadBandwidth*1000) + "/s."
} else { } else {
result += "unlimited." result += "unlimited."
} }
@ -1002,10 +857,10 @@ func (u *User) GetBandwidthAsString() string {
func (u *User) GetInfoString() string { func (u *User) GetInfoString() string {
var result string var result string
if u.LastLogin > 0 { if u.LastLogin > 0 {
t := utils.GetTimeFromMsecSinceEpoch(u.LastLogin) t := util.GetTimeFromMsecSinceEpoch(u.LastLogin)
result += fmt.Sprintf("Last login: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM result += fmt.Sprintf("Last login: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM
} }
if u.FsConfig.Provider != vfs.LocalFilesystemProvider { if u.FsConfig.Provider != sdk.LocalFilesystemProvider {
result += fmt.Sprintf("Storage: %s ", u.FsConfig.Provider.ShortInfo()) result += fmt.Sprintf("Storage: %s ", u.FsConfig.Provider.ShortInfo())
} }
if len(u.PublicKeys) > 0 { if len(u.PublicKeys) > 0 {
@ -1031,7 +886,7 @@ func (u *User) GetInfoString() string {
// GetStatusAsString returns the user status as a string // GetStatusAsString returns the user status as a string
func (u *User) GetStatusAsString() string { func (u *User) GetStatusAsString() string {
if u.ExpirationDate > 0 && u.ExpirationDate < utils.GetTimeAsMsSinceEpoch(time.Now()) { if u.ExpirationDate > 0 && u.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
return "Expired" return "Expired"
} }
if u.Status == 1 { if u.Status == 1 {
@ -1043,7 +898,7 @@ func (u *User) GetStatusAsString() string {
// GetExpirationDateAsString returns expiration date formatted as YYYY-MM-DD // GetExpirationDateAsString returns expiration date formatted as YYYY-MM-DD
func (u *User) GetExpirationDateAsString() string { func (u *User) GetExpirationDateAsString() string {
if u.ExpirationDate > 0 { if u.ExpirationDate > 0 {
t := utils.GetTimeFromMsecSinceEpoch(u.ExpirationDate) t := util.GetTimeFromMsecSinceEpoch(u.ExpirationDate)
return t.Format("2006-01-02") return t.Format("2006-01-02")
} }
return "" return ""
@ -1083,7 +938,7 @@ func (u *User) getACopy() User {
copy(perms, v) copy(perms, v)
permissions[k] = perms permissions[k] = perms
} }
filters := UserFilters{} filters := sdk.UserFilters{}
filters.MaxUploadFileSize = u.Filters.MaxUploadFileSize filters.MaxUploadFileSize = u.Filters.MaxUploadFileSize
filters.TLSUsername = u.Filters.TLSUsername filters.TLSUsername = u.Filters.TLSUsername
filters.AllowedIP = make([]string, len(u.Filters.AllowedIP)) filters.AllowedIP = make([]string, len(u.Filters.AllowedIP))
@ -1092,7 +947,7 @@ func (u *User) getACopy() User {
copy(filters.DeniedIP, u.Filters.DeniedIP) copy(filters.DeniedIP, u.Filters.DeniedIP)
filters.DeniedLoginMethods = make([]string, len(u.Filters.DeniedLoginMethods)) filters.DeniedLoginMethods = make([]string, len(u.Filters.DeniedLoginMethods))
copy(filters.DeniedLoginMethods, u.Filters.DeniedLoginMethods) copy(filters.DeniedLoginMethods, u.Filters.DeniedLoginMethods)
filters.FilePatterns = make([]PatternsFilter, len(u.Filters.FilePatterns)) filters.FilePatterns = make([]sdk.PatternsFilter, len(u.Filters.FilePatterns))
copy(filters.FilePatterns, u.Filters.FilePatterns) copy(filters.FilePatterns, u.Filters.FilePatterns)
filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols)) filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols))
copy(filters.DeniedProtocols, u.Filters.DeniedProtocols) copy(filters.DeniedProtocols, u.Filters.DeniedProtocols)
@ -1104,30 +959,32 @@ func (u *User) getACopy() User {
copy(filters.WebClient, u.Filters.WebClient) copy(filters.WebClient, u.Filters.WebClient)
return User{ return User{
ID: u.ID, BaseUser: sdk.BaseUser{
Username: u.Username, ID: u.ID,
Password: u.Password, Username: u.Username,
PublicKeys: pubKeys, Password: u.Password,
HomeDir: u.HomeDir, PublicKeys: pubKeys,
VirtualFolders: virtualFolders, HomeDir: u.HomeDir,
UID: u.UID, UID: u.UID,
GID: u.GID, GID: u.GID,
MaxSessions: u.MaxSessions, MaxSessions: u.MaxSessions,
QuotaSize: u.QuotaSize, QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles, QuotaFiles: u.QuotaFiles,
Permissions: permissions, Permissions: permissions,
UsedQuotaSize: u.UsedQuotaSize, UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles, UsedQuotaFiles: u.UsedQuotaFiles,
LastQuotaUpdate: u.LastQuotaUpdate, LastQuotaUpdate: u.LastQuotaUpdate,
UploadBandwidth: u.UploadBandwidth, UploadBandwidth: u.UploadBandwidth,
DownloadBandwidth: u.DownloadBandwidth, DownloadBandwidth: u.DownloadBandwidth,
Status: u.Status, Status: u.Status,
ExpirationDate: u.ExpirationDate, ExpirationDate: u.ExpirationDate,
LastLogin: u.LastLogin, LastLogin: u.LastLogin,
Filters: filters, Filters: filters,
FsConfig: u.FsConfig.GetACopy(), AdditionalInfo: u.AdditionalInfo,
AdditionalInfo: u.AdditionalInfo, Description: u.Description,
Description: u.Description, },
VirtualFolders: virtualFolders,
FsConfig: u.FsConfig.GetACopy(),
} }
} }

View file

@ -50,7 +50,7 @@ The configuration file contains the following sections:
- **"common"**, configuration parameters shared among all the supported protocols - **"common"**, configuration parameters shared among all the supported protocols
- `idle_timeout`, integer. Time in minutes after which an idle client will be disconnected. 0 means disabled. Default: 15 - `idle_timeout`, integer. Time in minutes after which an idle client will be disconnected. 0 means disabled. Default: 15
- `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload. - `upload_mode` integer. 0 means standard: the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode, if there is an upload error, the temporary file is deleted and so the requested upload path will not contain a partial file. 2 means atomic with resume support: same as atomic but if there is an upload error, the temporary file is renamed to the requested path and not deleted. This way, a client can reconnect and resume the upload. Default: 0
- `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details - `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions. See [Custom Actions](./custom-actions.md) for more details
- `execute_on`, list of strings. Valid values are `pre-download`, `download`, `pre-upload`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions. - `execute_on`, list of strings. Valid values are `pre-download`, `download`, `pre-upload`, `upload`, `pre-delete`, `delete`, `rename`, `ssh_cmd`. Leave empty to disable actions.
- `execute_sync`, list of strings. Actions to be performed synchronously. The `pre-delete` action is always executed synchronously while the other ones are asynchronous. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. Leave empty to execute only the `pre-delete` hook synchronously - `execute_sync`, list of strings. Actions to be performed synchronously. The `pre-delete` action is always executed synchronously while the other ones are asynchronous. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your hook have completed its execution. Leave empty to execute only the `pre-delete` hook synchronously
@ -238,6 +238,17 @@ The configuration file contains the following sections:
- `secrets` - `secrets`
- `url` - `url`
- `master_key_path` - `master_key_path`
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
- `type`, string. Defines the plugin type. Supported types: `notifier`.
- `notifier_options`, struct. Defines the options for notifier plugins.
- `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin.
- `user_events`, list of strings. Defines the user events that will be notified to this plugin.
- `cmd`, string. Path to the plugin executable.
- `args`, list of strings. Optional arguments to pass to the plugin executable.
- `sha256sum`, string. SHA256 checksum for the plugin executable. If not empty it will be used to verify the integrity of the executable.
- `auto_mtls`, boolean. If enabled the client and the server automatically negotiate mTLS for transport authentication. This ensures that only the original client will be allowed to connect to the server, and all other connections will be rejected. The client will also refuse to connect to any server that isn't the original instance started by the client.
Please note that the plugin system is experimental, the exposed configuration parameters and interfaces may change in a backward incompatible way in future.
A full example showing the default config (in JSON format) can be found [here](../sftpgo.json). A full example showing the default config (in JSON format) can be found [here](../sftpgo.json).

View file

@ -19,7 +19,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/sdk"
) )
func TestBasicFTPHandlingCryptFs(t *testing.T) { func TestBasicFTPHandlingCryptFs(t *testing.T) {
@ -254,7 +254,7 @@ func TestResumeCryptFs(t *testing.T) {
func getTestUserWithCryptFs() dataprovider.User { func getTestUserWithCryptFs() dataprovider.User {
user := getTestUser() user := getTestUser()
user.FsConfig.Provider = vfs.CryptedFilesystemProvider user.FsConfig.Provider = sdk.CryptedFilesystemProvider
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("testPassphrase") user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("testPassphrase")
return user return user
} }

View file

@ -10,7 +10,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
const ( const (
@ -55,7 +55,7 @@ type Binding struct {
} }
func (b *Binding) setCiphers() { func (b *Binding) setCiphers() {
b.ciphers = utils.GetTLSCiphersFromNames(b.TLSCipherSuites) b.ciphers = util.GetTLSCiphersFromNames(b.TLSCipherSuites)
if len(b.ciphers) == 0 { if len(b.ciphers) == 0 {
b.ciphers = nil b.ciphers = nil
} }
@ -219,7 +219,7 @@ func (c *Configuration) Initialize(configDir string) error {
go func(s *Server) { go func(s *Server) {
ftpServer := ftpserver.NewFtpServer(s) ftpServer := ftpserver.NewFtpServer(s)
logger.Info(logSender, "", "starting FTP serving, binding: %v", s.binding.GetAddress()) logger.Info(logSender, "", "starting FTP serving, binding: %v", s.binding.GetAddress())
utils.CheckTCP4Port(s.binding.Port) util.CheckTCP4Port(s.binding.Port)
exitChannel <- ftpServer.ListenAndServe() exitChannel <- ftpServer.ListenAndServe()
}(server) }(server)
@ -245,7 +245,7 @@ func GetStatus() ServiceStatus {
} }
func getConfigPath(name, configDir string) string { func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) { if !util.IsFileInputValid(name) {
return "" return ""
} }
if name != "" && !filepath.IsAbs(name) { if name != "" && !filepath.IsAbs(name) {

View file

@ -32,6 +32,7 @@ import (
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -1117,7 +1118,7 @@ func TestDownloadErrors(t *testing.T) {
u.Permissions[path.Join("/", subDir1)] = []string{dataprovider.PermListItems} u.Permissions[path.Join("/", subDir1)] = []string{dataprovider.PermListItems}
u.Permissions[path.Join("/", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload, u.Permissions[path.Join("/", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,
dataprovider.PermDelete, dataprovider.PermDownload} dataprovider.PermDelete, dataprovider.PermDownload}
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/sub2", Path: "/sub2",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -1169,7 +1170,7 @@ func TestUploadErrors(t *testing.T) {
u.Permissions[path.Join("/", subDir1)] = []string{dataprovider.PermListItems} u.Permissions[path.Join("/", subDir1)] = []string{dataprovider.PermListItems}
u.Permissions[path.Join("/", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload, u.Permissions[path.Join("/", subDir2)] = []string{dataprovider.PermListItems, dataprovider.PermUpload,
dataprovider.PermDelete} dataprovider.PermDelete}
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/sub2", Path: "/sub2",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -1595,7 +1596,7 @@ func TestLoginWithIPilters(t *testing.T) {
func TestLoginWithDatabaseCredentials(t *testing.T) { func TestLoginWithDatabaseCredentials(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`) u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`)
@ -1644,7 +1645,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
func TestLoginInvalidFs(t *testing.T) { func TestLoginInvalidFs(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -2435,7 +2436,7 @@ func TestCombine(t *testing.T) {
func TestClientCertificateAuthRevokedCert(t *testing.T) { func TestClientCertificateAuthRevokedCert(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Username = tlsClient2Username u.Username = tlsClient2Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -2477,7 +2478,7 @@ func TestClientCertificateAuth(t *testing.T) {
assert.Contains(t, err.Error(), "login method password is not allowed") assert.Contains(t, err.Error(), "login method password is not allowed")
} }
user.Filters.TLSUsername = dataprovider.TLSUsernameCN user.Filters.TLSUsername = sdk.TLSUsernameCN
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
client, err := getFTPClient(user, true, tlsConfig) client, err := getFTPClient(user, true, tlsConfig)
@ -2491,7 +2492,7 @@ func TestClientCertificateAuth(t *testing.T) {
// now use a valid certificate with a CN different from username // now use a valid certificate with a CN different from username
u = getTestUser() u = getTestUser()
u.Username = tlsClient2Username u.Username = tlsClient2Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user2, _, err := httpdtest.AddUser(u, http.StatusCreated) user2, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -2537,7 +2538,7 @@ func TestClientCertificateAuth(t *testing.T) {
func TestClientCertificateAndPwdAuth(t *testing.T) { func TestClientCertificateAndPwdAuth(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Username = tlsClient1Username u.Username = tlsClient1Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate} u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -2588,7 +2589,7 @@ func TestExternatAuthWithClientCert(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Username = tlsClient1Username u.Username = tlsClient1Username
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword) u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword)
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
err := dataprovider.Close() err := dataprovider.Close()
assert.NoError(t, err) assert.NoError(t, err)
err = config.LoadConfig(configDir, "") err = config.LoadConfig(configDir, "")
@ -2655,7 +2656,7 @@ func TestPreLoginHookWithClientCert(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Username = tlsClient1Username u.Username = tlsClient1Username
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword) u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, dataprovider.LoginMethodPassword)
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
err := dataprovider.Close() err := dataprovider.Close()
assert.NoError(t, err) assert.NoError(t, err)
err = config.LoadConfig(configDir, "") err = config.LoadConfig(configDir, "")
@ -2738,9 +2739,11 @@ func TestNestedVirtualFolders(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameCrypt, Name: folderNameCrypt,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
MappedPath: mappedPathCrypt, MappedPath: mappedPathCrypt,
@ -2976,11 +2979,13 @@ func waitNoConnections() {
func getTestUser() dataprovider.User { func getTestUser() dataprovider.User {
user := dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, BaseUser: sdk.BaseUser{
Password: defaultPassword, Username: defaultUsername,
HomeDir: filepath.Join(homeBasePath, defaultUsername), Password: defaultPassword,
Status: 1, HomeDir: filepath.Join(homeBasePath, defaultUsername),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms user.Permissions["/"] = allPerms
@ -2990,7 +2995,7 @@ func getTestUser() dataprovider.User {
func getTestSFTPUser() dataprovider.User { func getTestSFTPUser() dataprovider.User {
u := getTestUser() u := getTestUser()
u.Username = u.Username + "_sftp" u.Username = u.Username + "_sftp"
u.FsConfig.Provider = vfs.SFTPFilesystemProvider u.FsConfig.Provider = sdk.SFTPFilesystemProvider
u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr
u.FsConfig.SFTPConfig.Username = defaultUsername u.FsConfig.SFTPConfig.Username = defaultUsername
u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)

View file

@ -17,6 +17,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -466,7 +467,9 @@ func TestServerGetSettings(t *testing.T) {
func TestUserInvalidParams(t *testing.T) { func TestUserInvalidParams(t *testing.T) {
u := dataprovider.User{ u := dataprovider.User{
HomeDir: "invalid", BaseUser: sdk.BaseUser{
HomeDir: "invalid",
},
} }
binding := Binding{ binding := Binding{
Port: 2121, Port: 2121,
@ -548,7 +551,9 @@ func TestDriverMethodsNotImplemented(t *testing.T) {
func TestResolvePathErrors(t *testing.T) { func TestResolvePathErrors(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: "invalid", BaseUser: sdk.BaseUser{
HomeDir: "invalid",
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -609,8 +614,10 @@ func TestUploadFileStatError(t *testing.T) {
t.Skip("this test is not available on Windows") t.Skip("this test is not available on Windows")
} }
user := dataprovider.User{ user := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "user",
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -638,8 +645,10 @@ func TestUploadFileStatError(t *testing.T) {
func TestAVBLErrors(t *testing.T) { func TestAVBLErrors(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "user",
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -658,8 +667,10 @@ func TestAVBLErrors(t *testing.T) {
func TestUploadOverwriteErrors(t *testing.T) { func TestUploadOverwriteErrors(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "user",
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -712,8 +723,10 @@ func TestTransferErrors(t *testing.T) {
file, err := os.Create(testfile) file, err := os.Create(testfile)
assert.NoError(t, err) assert.NoError(t, err)
user := dataprovider.User{ user := dataprovider.User{
Username: "user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "user",
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}

View file

@ -15,8 +15,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -135,7 +135,7 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
// ClientConnected is called to send the very first welcome message // ClientConnected is called to send the very first welcome message
func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) { func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
common.Connections.AddClientConnection(ipAddr) common.Connections.AddClientConnection(ipAddr)
if common.IsBanned(ipAddr) { if common.IsBanned(ipAddr) {
logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, ip %#v is banned", ipAddr) logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, ip %#v is banned", ipAddr)
@ -167,7 +167,7 @@ func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
s.cleanTLSConnVerification(cc.ID()) s.cleanTLSConnVerification(cc.ID())
connID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID()) connID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
common.Connections.Remove(connID) common.Connections.Remove(connID)
common.Connections.RemoveClientConnection(utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())) common.Connections.RemoveClientConnection(util.GetIPFromRemoteAddress(cc.RemoteAddr().String()))
} }
// AuthUser authenticates the user and selects an handling driver // AuthUser authenticates the user and selects an handling driver
@ -176,7 +176,7 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
if s.isTLSConnVerified(cc.ID()) { if s.isTLSConnVerified(cc.ID()) {
loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd
} }
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP) user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP)
if err != nil { if err != nil {
user.Username = username user.Username = username
@ -206,7 +206,7 @@ func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsCo
if tlsConn != nil { if tlsConn != nil {
state := tlsConn.ConnectionState() state := tlsConn.ConnectionState()
if len(state.PeerCertificates) > 0 { if len(state.PeerCertificates) > 0 {
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(cc.RemoteAddr().String())
dbUser, err := dataprovider.CheckUserBeforeTLSAuth(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0]) dbUser, err := dataprovider.CheckUserBeforeTLSAuth(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])
if err != nil { if err != nil {
dbUser.Username = user dbUser.Username = user
@ -307,7 +307,7 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
user.Username, user.HomeDir) user.Username, user.HomeDir)
return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir) return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
} }
if utils.IsStringInSlice(common.ProtocolFTP, user.Filters.DeniedProtocols) { if util.IsStringInSlice(common.ProtocolFTP, user.Filters.DeniedProtocols) {
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol FTP is not allowed", user.Username) logger.Debug(logSender, connectionID, "cannot login user %#v, protocol FTP is not allowed", user.Username)
return nil, fmt.Errorf("protocol FTP is not allowed for user %#v", user.Username) return nil, fmt.Errorf("protocol FTP is not allowed for user %#v", user.Username)
} }
@ -348,16 +348,16 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
} }
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metrics.AddLoginAttempt(loginMethod) metric.AddLoginAttempt(loginMethod)
if err != nil && err != common.ErrInternalFailure { if err != nil && err != common.ErrInternalFailure {
logger.ConnectionFailedLog(user.Username, ip, loginMethod, logger.ConnectionFailedLog(user.Username, ip, loginMethod,
common.ProtocolFTP, err.Error()) common.ProtocolFTP, err.Error())
event := common.HostEventLoginFailed event := common.HostEventLoginFailed
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
event = common.HostEventUserNotFound event = common.HostEventUserNotFound
} }
common.AddDefenderEvent(ip, event) common.AddDefenderEvent(ip, event)
} }
metrics.AddLoginResult(loginMethod, err) metric.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err) dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)
} }

23
go.mod
View file

@ -6,11 +6,12 @@ require (
cloud.google.com/go/storage v1.16.0 cloud.google.com/go/storage v1.16.0
github.com/Azure/azure-storage-blob-go v0.14.0 github.com/Azure/azure-storage-blob-go v0.14.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect github.com/StackExchange/wmi v1.2.0 // indirect
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8 github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
github.com/aws/aws-sdk-go v1.39.0 github.com/aws/aws-sdk-go v1.39.4
github.com/cockroachdb/cockroach-go/v2 v2.1.1 github.com/cockroachdb/cockroach-go/v2 v2.1.1
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
github.com/fatih/color v1.12.0 // indirect
github.com/fclairamb/ftpserverlib v0.14.0 github.com/fclairamb/ftpserverlib v0.14.0
github.com/frankban/quicktest v1.13.0 // indirect github.com/frankban/quicktest v1.13.0 // indirect
github.com/go-chi/chi/v5 v5.0.3 github.com/go-chi/chi/v5 v5.0.3
@ -18,22 +19,28 @@ require (
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
github.com/go-ole/go-ole v1.2.5 // indirect github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.7.3 // indirect github.com/goccy/go-json v0.7.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/hashicorp/go-hclog v0.16.2
github.com/hashicorp/go-plugin v1.4.2
github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.0
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c // indirect
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.13.1 github.com/klauspost/compress v1.13.1
github.com/klauspost/cpuid/v2 v2.0.7 // indirect github.com/klauspost/cpuid/v2 v2.0.8 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/jwx v1.2.1 github.com/lestrrat-go/jwx v1.2.1
github.com/lib/pq v1.10.2 github.com/lib/pq v1.10.2
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.7 github.com/mattn/go-sqlite3 v1.14.7
github.com/miekg/dns v1.1.43 // indirect github.com/miekg/dns v1.1.43 // indirect
github.com/minio/sio v0.3.0 github.com/minio/sio v0.3.0
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/otiai10/copy v1.6.0 github.com/otiai10/copy v1.6.0
github.com/pires/go-proxyproto v0.5.0 github.com/pires/go-proxyproto v0.6.0
github.com/pkg/sftp v1.13.1 github.com/pkg/sftp v1.13.2
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.29.0 // indirect github.com/prometheus/common v0.29.0 // indirect
github.com/rs/cors v1.8.0 github.com/rs/cors v1.8.0
@ -56,7 +63,9 @@ require (
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
google.golang.org/api v0.50.0 google.golang.org/api v0.50.0
google.golang.org/genproto v0.0.0-20210701191553-46259e63a0a9 // indirect google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f // indirect
google.golang.org/grpc v1.39.0
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )

60
go.sum
View file

@ -103,8 +103,8 @@ github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg3
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY= github.com/StackExchange/wmi v1.2.0 h1:noJEYkMQVlFCEAc+2ma5YyRhlfjcWfZqk5sBRYozdyM=
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v1.2.0/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -131,8 +131,8 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo= github.com/aws/aws-sdk-go v1.39.4 h1:nXBChUaG5cinrl3yg4/rUyssOOLH/ohk4S9K03kJirE=
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.39.4/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -157,6 +157,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo= github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo=
github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=
@ -223,9 +224,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fclairamb/ftpserverlib v0.14.0 h1:hF7cOVgihzmUwC4+i31iZ8MeCwK5IUipSZEDi4g6G4w= github.com/fclairamb/ftpserverlib v0.14.0 h1:hF7cOVgihzmUwC4+i31iZ8MeCwK5IUipSZEDi4g6G4w=
github.com/fclairamb/ftpserverlib v0.14.0/go.mod h1:ATLgn4bHgiM9+vfZbK+rMu/dqgkxO5nk94x/9f8ffDI= github.com/fclairamb/ftpserverlib v0.14.0/go.mod h1:ATLgn4bHgiM9+vfZbK+rMu/dqgkxO5nk94x/9f8ffDI=
@ -288,8 +291,8 @@ github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.7.3 h1:Pznres7bC8RRKT9yOn3EZ7fK+8Kle6K9rW2U33QlXZI= github.com/goccy/go-json v0.7.4 h1:B44qRUFwz/vxPKPISQ1KhvzRi9kZ28RAf6YtjriBZ5k=
github.com/goccy/go-json v0.7.3/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.7.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -419,8 +422,9 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
@ -430,6 +434,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0=
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
@ -466,6 +472,8 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:W
github.com/hashicorp/vault/sdk v0.2.0 h1:hvVswvMA9LvXwLBFDJLIoDBXi8hj90Q+gSS7vRYmLvQ= github.com/hashicorp/vault/sdk v0.2.0 h1:hvVswvMA9LvXwLBFDJLIoDBXi8hj90Q+gSS7vRYmLvQ=
github.com/hashicorp/vault/sdk v0.2.0/go.mod h1:cAGI4nVnEfAyMeqt9oB+Mase8DNn3qA/LDNHURiwssY= github.com/hashicorp/vault/sdk v0.2.0/go.mod h1:cAGI4nVnEfAyMeqt9oB+Mase8DNn3qA/LDNHURiwssY=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c h1:nqkErwUGfpZZMqj29WZ9U/wz2OpJVDuiokLhE/3Y7IQ=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -514,6 +522,8 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -549,8 +559,8 @@ github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ= github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.7 h1:U89pAFid7wpIWvTFJnMKgU+Sabb7DLEgHI7Xt8apo3Y= github.com/klauspost/cpuid/v2 v2.0.8 h1:bhR2mgIlno/Sfk4oUbH4sPlc83z1yGrN9bvqiq3C33I=
github.com/klauspost/cpuid/v2 v2.0.7/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.8/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -603,8 +613,9 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -614,8 +625,9 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
@ -637,6 +649,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
@ -664,6 +678,8 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -705,8 +721,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A=
github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo= github.com/pires/go-proxyproto v0.6.0 h1:cLJUPnuQdiNf7P/wbeOKmM1khVdaMgTFDLj8h9ZrVYk=
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pires/go-proxyproto v0.6.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -714,8 +730,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/pkg/sftp v1.13.2 h1:taJnKntsWgU+qae21Rx52lIwndAdKrj0mfUNQsz1z4Q=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.2/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -829,7 +845,9 @@ github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 h1:ipNUBPHSUmH
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tklauser/go-sysconf v0.3.6 h1:oc1sJWvKkmvIxhDHeKWvZS4f6AW+YcoguSfRF2/Hmo4=
github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
@ -865,6 +883,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@ -1169,6 +1188,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -1224,8 +1244,9 @@ google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxH
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210701191553-46259e63a0a9 h1:HBPuvo39L0DgfVn9eHR3ki/RjZoUFWa+em77e7KFDfs= google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f h1:khwpF3oSk7GIab/7DDMDyE8cPQEO6FAfOcWHIRAhO20=
google.golang.org/genproto v0.0.0-20210701191553-46259e63a0a9/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210708141623-e76da96a951f/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1254,8 +1275,9 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View file

@ -14,7 +14,7 @@ import (
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// TLSKeyPair defines the paths for a TLS key pair // TLSKeyPair defines the paths for a TLS key pair
@ -112,7 +112,7 @@ func (c *Config) loadCACerts(configDir string) (*x509.CertPool, error) {
} }
for _, ca := range c.CACertificates { for _, ca := range c.CACertificates {
if !utils.IsFileInputValid(ca) { if !util.IsFileInputValid(ca) {
return nil, fmt.Errorf("unable to load invalid CA certificate: %#v", ca) return nil, fmt.Errorf("unable to load invalid CA certificate: %#v", ca)
} }
if !filepath.IsAbs(ca) { if !filepath.IsAbs(ca) {
@ -139,10 +139,10 @@ func (c *Config) loadCertificates(configDir string) error {
for _, keyPair := range c.Certificates { for _, keyPair := range c.Certificates {
cert := keyPair.Cert cert := keyPair.Cert
key := keyPair.Key key := keyPair.Key
if !utils.IsFileInputValid(cert) { if !util.IsFileInputValid(cert) {
return fmt.Errorf("unable to load invalid certificate: %#v", cert) return fmt.Errorf("unable to load invalid certificate: %#v", cert)
} }
if !utils.IsFileInputValid(key) { if !util.IsFileInputValid(key) {
return fmt.Errorf("unable to load invalid key: %#v", key) return fmt.Errorf("unable to load invalid key: %#v", key)
} }
if !filepath.IsAbs(cert) { if !filepath.IsAbs(cert) {

View file

@ -9,7 +9,7 @@ import (
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
func getAdmins(w http.ResponseWriter, r *http.Request) { func getAdmins(w http.ResponseWriter, r *http.Request) {
@ -141,13 +141,13 @@ func changeAdminPassword(w http.ResponseWriter, r *http.Request) {
func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {
if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" {
return utils.NewValidationError("please provide the current password and the new one two times") return util.NewValidationError("please provide the current password and the new one two times")
} }
if newPassword != confirmNewPassword { if newPassword != confirmNewPassword {
return utils.NewValidationError("the two password fields do not match") return util.NewValidationError("the two password fields do not match")
} }
if currentPassword == newPassword { if currentPassword == newPassword {
return utils.NewValidationError("the new password must be different from the current one") return util.NewValidationError("the new password must be different from the current one")
} }
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)
if err != nil { if err != nil {
@ -159,7 +159,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir
} }
match, err := admin.CheckPassword(currentPassword) match, err := admin.CheckPassword(currentPassword)
if !match || err != nil { if !match || err != nil {
return utils.NewValidationError("current password does not match") return util.NewValidationError("current password does not match")
} }
admin.Password = newPassword admin.Password = newPassword

View file

@ -12,7 +12,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
func readUserFolder(w http.ResponseWriter, r *http.Request) { func readUserFolder(w http.ResponseWriter, r *http.Request) {
@ -39,7 +39,7 @@ func readUserFolder(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection) common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID()) defer common.Connections.Remove(connection.GetID())
name := utils.CleanPath(r.URL.Query().Get("path")) name := util.CleanPath(r.URL.Query().Get("path"))
contents, err := connection.ReadDir(name) contents, err := connection.ReadDir(name)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err)) sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
@ -84,7 +84,7 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection) common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID()) defer common.Connections.Remove(connection.GetID())
name := utils.CleanPath(r.URL.Query().Get("path")) name := util.CleanPath(r.URL.Query().Get("path"))
if name == "/" { if name == "/" {
sendAPIResponse(w, r, nil, "Please set the path to a valid file", http.StatusBadRequest) sendAPIResponse(w, r, nil, "Please set the path to a valid file", http.StatusBadRequest)
return return
@ -145,7 +145,7 @@ func getUserFilesAsZipStream(w http.ResponseWriter, r *http.Request) {
baseDir := "/" baseDir := "/"
for idx := range filesList { for idx := range filesList {
filesList[idx] = utils.CleanPath(filesList[idx]) filesList[idx] = util.CleanPath(filesList[idx])
} }
w.Header().Set("Content-Disposition", "attachment; filename=\"sftpgo-download.zip\"") w.Header().Set("Content-Disposition", "attachment; filename=\"sftpgo-download.zip\"")
@ -215,22 +215,22 @@ func changeUserPassword(w http.ResponseWriter, r *http.Request) {
func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error {
if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" {
return utils.NewValidationError("please provide the current password and the new one two times") return util.NewValidationError("please provide the current password and the new one two times")
} }
if newPassword != confirmNewPassword { if newPassword != confirmNewPassword {
return utils.NewValidationError("the two password fields do not match") return util.NewValidationError("the two password fields do not match")
} }
if currentPassword == newPassword { if currentPassword == newPassword {
return utils.NewValidationError("the new password must be different from the current one") return util.NewValidationError("the new password must be different from the current one")
} }
claims, err := getTokenClaims(r) claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" { if err != nil || claims.Username == "" {
return errors.New("invalid token claims") return errors.New("invalid token claims")
} }
user, err := dataprovider.CheckUserAndPass(claims.Username, currentPassword, utils.GetIPFromRemoteAddress(r.RemoteAddr), user, err := dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),
common.ProtocolHTTP) common.ProtocolHTTP)
if err != nil { if err != nil {
return utils.NewValidationError("current password does not match") return util.NewValidationError("current password does not match")
} }
user.Password = newPassword user.Password = newPassword

View file

@ -16,7 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -105,7 +105,7 @@ func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
content, err := io.ReadAll(r.Body) content, err := io.ReadAll(r.Body)
if err != nil || len(content) == 0 { if err != nil || len(content) == 0 {
if len(content) == 0 { if len(content) == 0 {
err = utils.NewValidationError("request body is required") err = util.NewValidationError("request body is required")
} }
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
@ -151,7 +151,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error { func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error {
dump, err := dataprovider.ParseDumpData(content) dump, err := dataprovider.ParseDumpData(content)
if err != nil { if err != nil {
return utils.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err)) return util.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err))
} }
if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota); err != nil { if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota); err != nil {

View file

@ -12,6 +12,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -59,17 +60,17 @@ func addUser(w http.ResponseWriter, r *http.Request) {
} }
user.SetEmptySecretsIfNil() user.SetEmptySecretsIfNil()
switch user.FsConfig.Provider { switch user.FsConfig.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
if user.FsConfig.S3Config.AccessSecret.IsRedacted() { if user.FsConfig.S3Config.AccessSecret.IsRedacted() {
sendAPIResponse(w, r, errors.New("invalid access_secret"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid access_secret"), "", http.StatusBadRequest)
return return
} }
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
if user.FsConfig.GCSConfig.Credentials.IsRedacted() { if user.FsConfig.GCSConfig.Credentials.IsRedacted() {
sendAPIResponse(w, r, errors.New("invalid credentials"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid credentials"), "", http.StatusBadRequest)
return return
} }
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
if user.FsConfig.AzBlobConfig.AccountKey.IsRedacted() { if user.FsConfig.AzBlobConfig.AccountKey.IsRedacted() {
sendAPIResponse(w, r, errors.New("invalid account_key"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid account_key"), "", http.StatusBadRequest)
return return
@ -78,12 +79,12 @@ func addUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, errors.New("invalid sas_url"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid sas_url"), "", http.StatusBadRequest)
return return
} }
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
if user.FsConfig.CryptConfig.Passphrase.IsRedacted() { if user.FsConfig.CryptConfig.Passphrase.IsRedacted() {
sendAPIResponse(w, r, errors.New("invalid passphrase"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid passphrase"), "", http.StatusBadRequest)
return return
} }
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
if user.FsConfig.SFTPConfig.Password.IsRedacted() { if user.FsConfig.SFTPConfig.Password.IsRedacted() {
sendAPIResponse(w, r, errors.New("invalid SFTP password"), "", http.StatusBadRequest) sendAPIResponse(w, r, errors.New("invalid SFTP password"), "", http.StatusBadRequest)
return return
@ -185,26 +186,26 @@ func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, cur
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret) { currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret) {
// we use the new access secret if plain or empty, otherwise the old value // we use the new access secret if plain or empty, otherwise the old value
switch fsConfig.Provider { switch fsConfig.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
if fsConfig.S3Config.AccessSecret.IsNotPlainAndNotEmpty() { if fsConfig.S3Config.AccessSecret.IsNotPlainAndNotEmpty() {
fsConfig.S3Config.AccessSecret = currentS3AccessSecret fsConfig.S3Config.AccessSecret = currentS3AccessSecret
} }
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
if fsConfig.AzBlobConfig.AccountKey.IsNotPlainAndNotEmpty() { if fsConfig.AzBlobConfig.AccountKey.IsNotPlainAndNotEmpty() {
fsConfig.AzBlobConfig.AccountKey = currentAzAccountKey fsConfig.AzBlobConfig.AccountKey = currentAzAccountKey
} }
if fsConfig.AzBlobConfig.SASURL.IsNotPlainAndNotEmpty() { if fsConfig.AzBlobConfig.SASURL.IsNotPlainAndNotEmpty() {
fsConfig.AzBlobConfig.SASURL = currentAzSASUrl fsConfig.AzBlobConfig.SASURL = currentAzSASUrl
} }
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
if fsConfig.GCSConfig.Credentials.IsNotPlainAndNotEmpty() { if fsConfig.GCSConfig.Credentials.IsNotPlainAndNotEmpty() {
fsConfig.GCSConfig.Credentials = currentGCSCredentials fsConfig.GCSConfig.Credentials = currentGCSCredentials
} }
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
if fsConfig.CryptConfig.Passphrase.IsNotPlainAndNotEmpty() { if fsConfig.CryptConfig.Passphrase.IsNotPlainAndNotEmpty() {
fsConfig.CryptConfig.Passphrase = currentCryptoPassphrase fsConfig.CryptConfig.Passphrase = currentCryptoPassphrase
} }
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
if fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() { if fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.Password = currentSFTPPassword fsConfig.SFTPConfig.Password = currentSFTPPassword
} }

View file

@ -19,8 +19,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
type pwdChange struct { type pwdChange struct {
@ -42,13 +42,13 @@ func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message
} }
func getRespStatus(err error) int { func getRespStatus(err error) int {
if _, ok := err.(*utils.ValidationError); ok { if _, ok := err.(*util.ValidationError); ok {
return http.StatusBadRequest return http.StatusBadRequest
} }
if _, ok := err.(*utils.MethodDisabledError); ok { if _, ok := err.(*util.MethodDisabledError); ok {
return http.StatusForbidden return http.StatusForbidden
} }
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
return http.StatusNotFound return http.StatusNotFound
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -362,21 +362,21 @@ func parseRangeRequest(bytesRange string, size int64) (int64, int64, error) {
} }
func updateLoginMetrics(user *dataprovider.User, ip string, err error) { func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword) metric.AddLoginAttempt(dataprovider.LoginMethodPassword)
if err != nil && err != common.ErrInternalFailure { if err != nil && err != common.ErrInternalFailure {
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolHTTP, err.Error()) logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolHTTP, err.Error())
event := common.HostEventLoginFailed event := common.HostEventLoginFailed
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
event = common.HostEventUserNotFound event = common.HostEventUserNotFound
} }
common.AddDefenderEvent(ip, event) common.AddDefenderEvent(ip, event)
} }
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err) metric.AddLoginResult(dataprovider.LoginMethodPassword, err)
dataprovider.ExecutePostLoginHook(user, dataprovider.LoginMethodPassword, ip, common.ProtocolHTTP, err) dataprovider.ExecutePostLoginHook(user, dataprovider.LoginMethodPassword, ip, common.ProtocolHTTP, err)
} }
func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string) error { func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string) error {
if utils.IsStringInSlice(common.ProtocolHTTP, user.Filters.DeniedProtocols) { if util.IsStringInSlice(common.ProtocolHTTP, user.Filters.DeniedProtocols) {
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol HTTP is not allowed", user.Username) logger.Debug(logSender, connectionID, "cannot login user %#v, protocol HTTP is not allowed", user.Username)
return fmt.Errorf("protocol HTTP is not allowed for user %#v", user.Username) return fmt.Errorf("protocol HTTP is not allowed for user %#v", user.Username)
} }

View file

@ -12,7 +12,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
type tokenAudience = string type tokenAudience = string
@ -83,24 +83,24 @@ func (c *jwtTokenClaims) Decode(token map[string]interface{}) {
} }
func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool { func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
if utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) { if util.IsStringInSlice(dataprovider.PermAdminAny, permissions) {
return false return false
} }
if (utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, c.Permissions) || if (util.IsStringInSlice(dataprovider.PermAdminManageAdmins, c.Permissions) ||
utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions)) && util.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions)) &&
!utils.IsStringInSlice(dataprovider.PermAdminManageAdmins, permissions) && !util.IsStringInSlice(dataprovider.PermAdminManageAdmins, permissions) &&
!utils.IsStringInSlice(dataprovider.PermAdminAny, permissions) { !util.IsStringInSlice(dataprovider.PermAdminAny, permissions) {
return true return true
} }
return false return false
} }
func (c *jwtTokenClaims) hasPerm(perm string) bool { func (c *jwtTokenClaims) hasPerm(perm string) bool {
if utils.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions) { if util.IsStringInSlice(dataprovider.PermAdminAny, c.Permissions) {
return true return true
} }
return utils.IsStringInSlice(perm, c.Permissions) return util.IsStringInSlice(perm, c.Permissions)
} }
func (c *jwtTokenClaims) createTokenResponse(tokenAuth *jwtauth.JWTAuth, audience tokenAudience) (map[string]interface{}, error) { func (c *jwtTokenClaims) createTokenResponse(tokenAuth *jwtauth.JWTAuth, audience tokenAudience) (map[string]interface{}, error) {
@ -253,7 +253,7 @@ func verifyCSRFToken(tokenString string) error {
return fmt.Errorf("unable to verify form token: %v", err) return fmt.Errorf("unable to verify form token: %v", err)
} }
if !utils.IsStringInSlice(tokenAudienceCSRF, token.Audience()) { if !util.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
logger.Debug(logSender, "", "error validating CSRF token audience") logger.Debug(logSender, "", "error validating CSRF token audience")
return errors.New("the form token is not valid") return errors.New("the form token is not valid")
} }

View file

@ -10,7 +10,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// Connection details for a HTTP connection used to inteact with an SFTPGo filesystem // Connection details for a HTTP connection used to inteact with an SFTPGo filesystem
@ -53,7 +53,7 @@ func (c *Connection) GetCommand() string {
func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) { func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) { if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {
return nil, c.GetPermissionDeniedError() return nil, c.GetPermissionDeniedError()
} }
@ -70,14 +70,14 @@ func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) { func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
return c.ListDir(name) return c.ListDir(name)
} }
func (c *Connection) getFileReader(name string, offset int64, method string) (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 = util.CleanPath(name)
if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) { if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {
return nil, c.GetPermissionDeniedError() return nil, c.GetPermissionDeniedError()
} }

View file

@ -25,7 +25,7 @@ import (
"github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/ftpd"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/webdavd" "github.com/drakkan/sftpgo/v2/webdavd"
) )
@ -184,7 +184,7 @@ type Binding struct {
} }
func (b *Binding) parseAllowedProxy() error { func (b *Binding) parseAllowedProxy() error {
allowedFuncs, err := utils.ParseAllowedIPAndRanges(b.ProxyAllowed) allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
if err != nil { if err != nil {
return err return err
} }
@ -382,7 +382,7 @@ func ReloadCertificateMgr() error {
} }
func getConfigPath(name, configDir string) string { func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) { if !util.IsFileInputValid(name) {
return "" return ""
} }
if name != "" && !filepath.IsAbs(name) { if name != "" && !filepath.IsAbs(name) {
@ -530,5 +530,5 @@ func getSigningKey(signingPassphrase string) []byte {
sk := sha256.Sum256([]byte(signingPassphrase)) sk := sha256.Sum256([]byte(signingPassphrase))
return sk[:] return sk[:]
} }
return utils.GenerateRandomBytes(32) return util.GenerateRandomBytes(32)
} }

View file

@ -40,8 +40,9 @@ import (
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -369,10 +370,10 @@ func TestBasicUserHandling(t *testing.T) {
user.QuotaFiles = 2 user.QuotaFiles = 2
user.UploadBandwidth = 128 user.UploadBandwidth = 128
user.DownloadBandwidth = 64 user.DownloadBandwidth = 64
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now())
user.AdditionalInfo = "some free text" user.AdditionalInfo = "some free text"
user.Filters.TLSUsername = dataprovider.TLSUsernameCN user.Filters.TLSUsername = sdk.TLSUsernameCN
user.Filters.WebClient = append(user.Filters.WebClient, dataprovider.WebClientPubKeyChangeDisabled) user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
originalUser := user originalUser := user
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@ -839,7 +840,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.DeniedLoginMethods = []string{} u.Filters.DeniedLoginMethods = []string{}
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "relative", Path: "relative",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -848,7 +849,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -857,7 +858,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/subdir", Path: "/subdir",
AllowedPatterns: []string{"*.zip"}, AllowedPatterns: []string{"*.zip"},
@ -871,7 +872,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "relative", Path: "relative",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -880,7 +881,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
AllowedPatterns: []string{}, AllowedPatterns: []string{},
@ -889,7 +890,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/subdir", Path: "/subdir",
AllowedPatterns: []string{"*.zip"}, AllowedPatterns: []string{"*.zip"},
@ -902,7 +903,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
} }
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/subdir", Path: "/subdir",
AllowedPatterns: []string{"a\\"}, AllowedPatterns: []string{"a\\"},
@ -928,7 +929,7 @@ func TestAddUserInvalidFilters(t *testing.T) {
func TestAddUserInvalidFsConfig(t *testing.T) { func TestAddUserInvalidFsConfig(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.S3FilesystemProvider u.FsConfig.Provider = sdk.S3FilesystemProvider
u.FsConfig.S3Config.Bucket = "" u.FsConfig.S3Config.Bucket = ""
_, _, err := httpdtest.AddUser(u, http.StatusBadRequest) _, _, err := httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
@ -960,7 +961,7 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u = getTestUser() u = getTestUser()
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "" u.FsConfig.GCSConfig.Bucket = ""
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
@ -983,7 +984,7 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
u = getTestUser() u = getTestUser()
u.FsConfig.Provider = vfs.AzureBlobFilesystemProvider u.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
u.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("http://foo\x7f.com/") u.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("http://foo\x7f.com/")
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
@ -1013,14 +1014,14 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
u = getTestUser() u = getTestUser()
u.FsConfig.Provider = vfs.CryptedFilesystemProvider u.FsConfig.Provider = sdk.CryptedFilesystemProvider
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.FsConfig.CryptConfig.Passphrase = kms.NewSecret(kms.SecretStatusRedacted, "akey", "", "") u.FsConfig.CryptConfig.Passphrase = kms.NewSecret(kms.SecretStatusRedacted, "akey", "", "")
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u = getTestUser() u = getTestUser()
u.FsConfig.Provider = vfs.SFTPFilesystemProvider u.FsConfig.Provider = sdk.SFTPFilesystemProvider
_, _, err = httpdtest.AddUser(u, http.StatusBadRequest) _, _, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
u.FsConfig.SFTPConfig.Password = kms.NewSecret(kms.SecretStatusRedacted, "randompkey", "", "") u.FsConfig.SFTPConfig.Password = kms.NewSecret(kms.SecretStatusRedacted, "randompkey", "", "")
@ -1047,7 +1048,7 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
func TestUserRedactedPassword(t *testing.T) { func TestUserRedactedPassword(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.S3FilesystemProvider u.FsConfig.Provider = sdk.S3FilesystemProvider
u.FsConfig.S3Config.Bucket = "b" u.FsConfig.S3Config.Bucket = "b"
u.FsConfig.S3Config.Region = "eu-west-1" u.FsConfig.S3Config.Region = "eu-west-1"
u.FsConfig.S3Config.AccessKey = "access-key" u.FsConfig.S3Config.AccessKey = "access-key"
@ -1071,9 +1072,11 @@ func TestUserRedactedPassword(t *testing.T) {
Name: folderName, Name: folderName,
MappedPath: filepath.Join(os.TempDir(), "crypted"), MappedPath: filepath.Join(os.TempDir(), "crypted"),
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewSecret(kms.SecretStatusRedacted, "crypted-secret", "", ""), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewSecret(kms.SecretStatusRedacted, "crypted-secret", "", ""),
},
}, },
}, },
}, },
@ -1303,7 +1306,7 @@ func TestUpdateUser(t *testing.T) {
u := getTestUser() u := getTestUser()
u.UsedQuotaFiles = 1 u.UsedQuotaFiles = 1
u.UsedQuotaSize = 2 u.UsedQuotaSize = 2
u.Filters.TLSUsername = dataprovider.TLSUsernameCN u.Filters.TLSUsername = sdk.TLSUsernameCN
u.Filters.Hooks.CheckPasswordDisabled = true u.Filters.Hooks.CheckPasswordDisabled = true
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -1321,12 +1324,12 @@ func TestUpdateUser(t *testing.T) {
user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"} user.Filters.DeniedIP = []string{"192.168.3.0/24", "192.168.4.0/24"}
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword} user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user.Filters.DeniedProtocols = []string{common.ProtocolWebDAV} user.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
user.Filters.TLSUsername = dataprovider.TLSUsernameNone user.Filters.TLSUsername = sdk.TLSUsernameNone
user.Filters.Hooks.ExternalAuthDisabled = true user.Filters.Hooks.ExternalAuthDisabled = true
user.Filters.Hooks.PreLoginDisabled = true user.Filters.Hooks.PreLoginDisabled = true
user.Filters.Hooks.CheckPasswordDisabled = false user.Filters.Hooks.CheckPasswordDisabled = false
user.Filters.DisableFsChecks = true user.Filters.DisableFsChecks = true
user.Filters.FilePatterns = append(user.Filters.FilePatterns, dataprovider.PatternsFilter{ user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/subdir", Path: "/subdir",
AllowedPatterns: []string{"*.zip", "*.rar"}, AllowedPatterns: []string{"*.zip", "*.rar"},
DeniedPatterns: []string{"*.jpg", "*.png"}, DeniedPatterns: []string{"*.jpg", "*.png"},
@ -1580,7 +1583,7 @@ func TestUserFolderMapping(t *testing.T) {
func TestUserS3Config(t *testing.T) { func TestUserS3Config(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)
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "test" //nolint:goconst user.FsConfig.S3Config.Bucket = "test" //nolint:goconst
user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst
user.FsConfig.S3Config.AccessKey = "Server-Access-Key" user.FsConfig.S3Config.AccessKey = "Server-Access-Key"
@ -1593,9 +1596,11 @@ func TestUserS3Config(t *testing.T) {
Name: folderName, Name: folderName,
MappedPath: filepath.Join(os.TempDir(), "folderName"), MappedPath: filepath.Join(os.TempDir(), "folderName"),
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret("Crypted-Secret"), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret("Crypted-Secret"),
},
}, },
}, },
}, },
@ -1639,7 +1644,7 @@ func TestUserS3Config(t *testing.T) {
assert.NotEmpty(t, initialSecretPayload) assert.NotEmpty(t, initialSecretPayload)
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "test-bucket" user.FsConfig.S3Config.Bucket = "test-bucket"
user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst
user.FsConfig.S3Config.AccessKey = "Server-Access-Key1" user.FsConfig.S3Config.AccessKey = "Server-Access-Key1"
@ -1653,7 +1658,7 @@ func TestUserS3Config(t *testing.T) {
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey()) assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())
// test user without access key and access secret (shared config state) // test user without access key and access secret (shared config state)
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "testbucket" user.FsConfig.S3Config.Bucket = "testbucket"
user.FsConfig.S3Config.Region = "us-east-1" user.FsConfig.S3Config.Region = "us-east-1"
user.FsConfig.S3Config.AccessKey = "" user.FsConfig.S3Config.AccessKey = ""
@ -1684,7 +1689,7 @@ func TestUserGCSConfig(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
err = os.MkdirAll(credentialsPath, 0700) err = os.MkdirAll(credentialsPath, 0700)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.GCSFilesystemProvider user.FsConfig.Provider = sdk.GCSFilesystemProvider
user.FsConfig.GCSConfig.Bucket = "test" user.FsConfig.GCSConfig.Bucket = "test"
user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") //nolint:goconst user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") //nolint:goconst
user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "") user, bb, err := httpdtest.UpdateUser(user, http.StatusOK, "")
@ -1731,7 +1736,7 @@ func TestUserGCSConfig(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NoFileExists(t, credentialFile) assert.NoFileExists(t, credentialFile)
user.FsConfig.GCSConfig = vfs.GCSFsConfig{} user.FsConfig.GCSConfig = vfs.GCSFsConfig{}
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "test1" user.FsConfig.S3Config.Bucket = "test1"
user.FsConfig.S3Config.Region = "us-east-1" user.FsConfig.S3Config.Region = "us-east-1"
user.FsConfig.S3Config.AccessKey = "Server-Access-Key1" user.FsConfig.S3Config.AccessKey = "Server-Access-Key1"
@ -1741,7 +1746,7 @@ func TestUserGCSConfig(t *testing.T) {
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.S3Config = vfs.S3FsConfig{} user.FsConfig.S3Config = vfs.S3FsConfig{}
user.FsConfig.Provider = vfs.GCSFilesystemProvider user.FsConfig.Provider = sdk.GCSFilesystemProvider
user.FsConfig.GCSConfig.Bucket = "test1" user.FsConfig.GCSConfig.Bucket = "test1"
user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
@ -1754,7 +1759,7 @@ func TestUserGCSConfig(t *testing.T) {
func TestUserAzureBlobConfig(t *testing.T) { func TestUserAzureBlobConfig(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)
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
user.FsConfig.AzBlobConfig.Container = "test" user.FsConfig.AzBlobConfig.Container = "test"
user.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name" user.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name"
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key")
@ -1793,7 +1798,7 @@ func TestUserAzureBlobConfig(t *testing.T) {
assert.NotEmpty(t, initialPayload) assert.NotEmpty(t, initialPayload)
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
user.FsConfig.AzBlobConfig.Container = "test-container" user.FsConfig.AzBlobConfig.Container = "test-container"
user.FsConfig.AzBlobConfig.Endpoint = "http://localhost:9001" user.FsConfig.AzBlobConfig.Endpoint = "http://localhost:9001"
user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir" user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir"
@ -1806,7 +1811,7 @@ func TestUserAzureBlobConfig(t *testing.T) {
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey()) assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
// test user without access key and access secret (SAS) // test user without access key and access secret (SAS)
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
user.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("https://myaccount.blob.core.windows.net/pictures/profile.jpg?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d") user.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("https://myaccount.blob.core.windows.net/pictures/profile.jpg?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d")
user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir" user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir"
user.FsConfig.AzBlobConfig.AccountName = "" user.FsConfig.AzBlobConfig.AccountName = ""
@ -1823,8 +1828,10 @@ func TestUserAzureBlobConfig(t *testing.T) {
user.ID = 0 user.ID = 0
// sas test for add instead of update // sas test for add instead of update
user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{ user.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
Container: user.FsConfig.AzBlobConfig.Container, AzBlobFsConfig: sdk.AzBlobFsConfig{
SASURL: kms.NewPlainSecret("http://127.0.0.1/fake/sass/url"), Container: user.FsConfig.AzBlobConfig.Container,
SASURL: kms.NewPlainSecret("http://127.0.0.1/fake/sass/url"),
},
} }
user, _, err = httpdtest.AddUser(user, http.StatusCreated) user, _, err = httpdtest.AddUser(user, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -1851,7 +1858,7 @@ func TestUserAzureBlobConfig(t *testing.T) {
func TestUserCryptFs(t *testing.T) { func TestUserCryptFs(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)
user.FsConfig.Provider = vfs.CryptedFilesystemProvider user.FsConfig.Provider = sdk.CryptedFilesystemProvider
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypt passphrase") user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypt passphrase")
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@ -1886,7 +1893,7 @@ func TestUserCryptFs(t *testing.T) {
assert.NotEmpty(t, initialPayload) assert.NotEmpty(t, initialPayload)
assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData()) assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetAdditionalData())
assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey()) assert.Empty(t, user.FsConfig.CryptConfig.Passphrase.GetKey())
user.FsConfig.Provider = vfs.CryptedFilesystemProvider user.FsConfig.Provider = sdk.CryptedFilesystemProvider
user.FsConfig.CryptConfig.Passphrase.SetKey("pass") user.FsConfig.CryptConfig.Passphrase.SetKey("pass")
user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err, string(bb)) assert.NoError(t, err, string(bb))
@ -1903,7 +1910,7 @@ func TestUserCryptFs(t *testing.T) {
func TestUserSFTPFs(t *testing.T) { func TestUserSFTPFs(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)
user.FsConfig.Provider = vfs.SFTPFilesystemProvider user.FsConfig.Provider = sdk.SFTPFilesystemProvider
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1" // missing port user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1" // missing port
user.FsConfig.SFTPConfig.Username = "sftp_user" user.FsConfig.SFTPConfig.Username = "sftp_user"
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp_pwd") user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp_pwd")
@ -1972,7 +1979,7 @@ func TestUserSFTPFs(t *testing.T) {
assert.NotEmpty(t, initialPkeyPayload) assert.NotEmpty(t, initialPkeyPayload)
assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData()) assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey()) assert.Empty(t, user.FsConfig.SFTPConfig.PrivateKey.GetKey())
user.FsConfig.Provider = vfs.SFTPFilesystemProvider user.FsConfig.Provider = sdk.SFTPFilesystemProvider
user.FsConfig.SFTPConfig.PrivateKey.SetKey("k") user.FsConfig.SFTPConfig.PrivateKey.SetKey("k")
user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, bb, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err, string(bb)) assert.NoError(t, err, string(bb))
@ -2000,7 +2007,7 @@ func TestUserHiddenFields(t *testing.T) {
usernames := []string{"user1", "user2", "user3", "user4", "user5"} usernames := []string{"user1", "user2", "user3", "user4", "user5"}
u1 := getTestUser() u1 := getTestUser()
u1.Username = usernames[0] u1.Username = usernames[0]
u1.FsConfig.Provider = vfs.S3FilesystemProvider u1.FsConfig.Provider = sdk.S3FilesystemProvider
u1.FsConfig.S3Config.Bucket = "test" u1.FsConfig.S3Config.Bucket = "test"
u1.FsConfig.S3Config.Region = "us-east-1" u1.FsConfig.S3Config.Region = "us-east-1"
u1.FsConfig.S3Config.AccessKey = "S3-Access-Key" u1.FsConfig.S3Config.AccessKey = "S3-Access-Key"
@ -2010,7 +2017,7 @@ func TestUserHiddenFields(t *testing.T) {
u2 := getTestUser() u2 := getTestUser()
u2.Username = usernames[1] u2.Username = usernames[1]
u2.FsConfig.Provider = vfs.GCSFilesystemProvider u2.FsConfig.Provider = sdk.GCSFilesystemProvider
u2.FsConfig.GCSConfig.Bucket = "test" u2.FsConfig.GCSConfig.Bucket = "test"
u2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") u2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials")
user2, _, err := httpdtest.AddUser(u2, http.StatusCreated) user2, _, err := httpdtest.AddUser(u2, http.StatusCreated)
@ -2018,7 +2025,7 @@ func TestUserHiddenFields(t *testing.T) {
u3 := getTestUser() u3 := getTestUser()
u3.Username = usernames[2] u3.Username = usernames[2]
u3.FsConfig.Provider = vfs.AzureBlobFilesystemProvider u3.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
u3.FsConfig.AzBlobConfig.Container = "test" u3.FsConfig.AzBlobConfig.Container = "test"
u3.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name" u3.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name"
u3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key") u3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key")
@ -2027,14 +2034,14 @@ func TestUserHiddenFields(t *testing.T) {
u4 := getTestUser() u4 := getTestUser()
u4.Username = usernames[3] u4.Username = usernames[3]
u4.FsConfig.Provider = vfs.CryptedFilesystemProvider u4.FsConfig.Provider = sdk.CryptedFilesystemProvider
u4.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("test passphrase") u4.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("test passphrase")
user4, _, err := httpdtest.AddUser(u4, http.StatusCreated) user4, _, err := httpdtest.AddUser(u4, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
u5 := getTestUser() u5 := getTestUser()
u5.Username = usernames[4] u5.Username = usernames[4]
u5.FsConfig.Provider = vfs.SFTPFilesystemProvider u5.FsConfig.Provider = sdk.SFTPFilesystemProvider
u5.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:2022" u5.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:2022"
u5.FsConfig.SFTPConfig.Username = "sftp_user" u5.FsConfig.SFTPConfig.Username = "sftp_user"
u5.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("apassword") u5.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("apassword")
@ -2555,7 +2562,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(0), folder.UsedQuotaSize) assert.Equal(t, int64(0), folder.UsedQuotaSize)
assert.Equal(t, int64(0), folder.LastQuotaUpdate) assert.Equal(t, int64(0), folder.LastQuotaUpdate)
assert.Empty(t, folder.Description) assert.Empty(t, folder.Description)
assert.Equal(t, vfs.LocalFilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.LocalFilesystemProvider, folder.FsConfig.Provider)
assert.Len(t, folder.Users, 1) assert.Len(t, folder.Users, 1)
assert.Contains(t, folder.Users, user.Username) assert.Contains(t, folder.Users, user.Username)
// update a field on the folder // update a field on the folder
@ -2569,7 +2576,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(0), folder.UsedQuotaSize) assert.Equal(t, int64(0), folder.UsedQuotaSize)
assert.Equal(t, int64(0), folder.LastQuotaUpdate) assert.Equal(t, int64(0), folder.LastQuotaUpdate)
assert.Equal(t, description, folder.Description) assert.Equal(t, description, folder.Description)
assert.Equal(t, vfs.LocalFilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.LocalFilesystemProvider, folder.FsConfig.Provider)
// check that the user gets the changes // check that the user gets the changes
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -2579,7 +2586,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(0), userFolder.UsedQuotaSize) assert.Equal(t, int64(0), userFolder.UsedQuotaSize)
assert.Equal(t, int64(0), userFolder.LastQuotaUpdate) assert.Equal(t, int64(0), userFolder.LastQuotaUpdate)
assert.Equal(t, description, userFolder.Description) assert.Equal(t, description, userFolder.Description)
assert.Equal(t, vfs.LocalFilesystemProvider, userFolder.FsConfig.Provider) assert.Equal(t, sdk.LocalFilesystemProvider, userFolder.FsConfig.Provider)
// now update the folder embedding it inside the user // now update the folder embedding it inside the user
user.VirtualFolders = []vfs.VirtualFolder{ user.VirtualFolders = []vfs.VirtualFolder{
{ {
@ -2590,13 +2597,15 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
UsedQuotaSize: 8192, UsedQuotaSize: 8192,
LastQuotaUpdate: 123, LastQuotaUpdate: 123,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.S3FilesystemProvider, Provider: sdk.S3FilesystemProvider,
S3Config: vfs.S3FsConfig{ S3Config: vfs.S3FsConfig{
Bucket: "test", S3FsConfig: sdk.S3FsConfig{
Region: "us-east-1", Bucket: "test",
AccessKey: "akey", Region: "us-east-1",
AccessSecret: kms.NewPlainSecret("asecret"), AccessKey: "akey",
Endpoint: "http://127.0.1.1:9090", AccessSecret: kms.NewPlainSecret("asecret"),
Endpoint: "http://127.0.1.1:9090",
},
}, },
}, },
}, },
@ -2612,7 +2621,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(0), userFolder.UsedQuotaSize) assert.Equal(t, int64(0), userFolder.UsedQuotaSize)
assert.Equal(t, int64(0), userFolder.LastQuotaUpdate) assert.Equal(t, int64(0), userFolder.LastQuotaUpdate)
assert.Empty(t, userFolder.Description) assert.Empty(t, userFolder.Description)
assert.Equal(t, vfs.S3FilesystemProvider, userFolder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, userFolder.FsConfig.Provider)
assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket) assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket)
assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region) assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region)
assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint) assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint)
@ -2627,7 +2636,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(0), folder.UsedQuotaSize) assert.Equal(t, int64(0), folder.UsedQuotaSize)
assert.Equal(t, int64(0), folder.LastQuotaUpdate) assert.Equal(t, int64(0), folder.LastQuotaUpdate)
assert.Empty(t, folder.Description) assert.Empty(t, folder.Description)
assert.Equal(t, vfs.S3FilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket) assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket)
assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region) assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region)
assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint) assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint)
@ -2645,7 +2654,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, 100, folder.UsedQuotaFiles) assert.Equal(t, 100, folder.UsedQuotaFiles)
assert.Equal(t, int64(32768), folder.UsedQuotaSize) assert.Equal(t, int64(32768), folder.UsedQuotaSize)
assert.Greater(t, folder.LastQuotaUpdate, int64(0)) assert.Greater(t, folder.LastQuotaUpdate, int64(0))
assert.Equal(t, vfs.S3FilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket) assert.Equal(t, "test", folder.FsConfig.S3Config.Bucket)
assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region) assert.Equal(t, "us-east-1", folder.FsConfig.S3Config.Region)
assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint) assert.Equal(t, "http://127.0.1.1:9090", folder.FsConfig.S3Config.Endpoint)
@ -2662,7 +2671,7 @@ func TestEmbeddedFoldersUpdate(t *testing.T) {
assert.Equal(t, int64(32768), userFolder.UsedQuotaSize) assert.Equal(t, int64(32768), userFolder.UsedQuotaSize)
assert.Greater(t, userFolder.LastQuotaUpdate, int64(0)) assert.Greater(t, userFolder.LastQuotaUpdate, int64(0))
assert.Empty(t, userFolder.Description) assert.Empty(t, userFolder.Description)
assert.Equal(t, vfs.S3FilesystemProvider, userFolder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, userFolder.FsConfig.Provider)
assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket) assert.Equal(t, "test", userFolder.FsConfig.S3Config.Bucket)
assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region) assert.Equal(t, "us-east-1", userFolder.FsConfig.S3Config.Region)
assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint) assert.Equal(t, "http://127.0.1.1:9090", userFolder.FsConfig.S3Config.Endpoint)
@ -2927,9 +2936,9 @@ func TestProviderErrors(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
_, _, err = httpdtest.GetAdmins(1, 0, http.StatusInternalServerError) _, _, err = httpdtest.GetAdmins(1, 0, http.StatusInternalServerError)
assert.NoError(t, err) assert.NoError(t, err)
_, _, err = httpdtest.UpdateUser(dataprovider.User{Username: "auser"}, http.StatusInternalServerError, "") _, _, err = httpdtest.UpdateUser(dataprovider.User{BaseUser: sdk.BaseUser{Username: "auser"}}, http.StatusInternalServerError, "")
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveUser(dataprovider.User{Username: "auser"}, http.StatusInternalServerError) _, err = httpdtest.RemoveUser(dataprovider.User{BaseUser: sdk.BaseUser{Username: "auser"}}, http.StatusInternalServerError)
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: "aname"}, http.StatusInternalServerError) _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: "aname"}, http.StatusInternalServerError)
assert.NoError(t, err) assert.NoError(t, err)
@ -3007,9 +3016,11 @@ func TestFolders(t *testing.T) {
MappedPath: "relative path", MappedPath: "relative path",
Users: []string{"1", "2", "3"}, Users: []string{"1", "2", "3"},
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret("asecret"), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret("asecret"),
},
}, },
}, },
} }
@ -3630,7 +3641,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
assert.Equal(t, user.MaxSessions, updatedUser.MaxSessions) assert.Equal(t, user.MaxSessions, updatedUser.MaxSessions)
assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth) assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth)
assert.Equal(t, 1, len(updatedUser.Permissions["/"])) assert.Equal(t, 1, len(updatedUser.Permissions["/"]))
assert.True(t, utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions["/"])) assert.True(t, util.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions["/"]))
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil) req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil)
setBearerForReq(req, token) setBearerForReq(req, token)
rr = executeRequest(req) rr = executeRequest(req)
@ -3961,7 +3972,7 @@ func TestUpdateUserMock(t *testing.T) {
for dir, perms := range permissions { for dir, perms := range permissions {
if actualPerms, ok := updatedUser.Permissions[dir]; ok { if actualPerms, ok := updatedUser.Permissions[dir]; ok {
for _, v := range actualPerms { for _, v := range actualPerms {
assert.True(t, utils.IsStringInSlice(v, perms)) assert.True(t, util.IsStringInSlice(v, perms))
} }
} else { } else {
assert.Fail(t, "Permissions directories mismatch") assert.Fail(t, "Permissions directories mismatch")
@ -4120,7 +4131,7 @@ func TestUserPermissionsMock(t *testing.T) {
err = render.DecodeJSON(rr.Body, &updatedUser) err = render.DecodeJSON(rr.Body, &updatedUser)
assert.NoError(t, err) assert.NoError(t, err)
if val, ok := updatedUser.Permissions["/otherdir"]; ok { if val, ok := updatedUser.Permissions["/otherdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))
assert.Equal(t, 1, len(val)) assert.Equal(t, 1, len(val))
} else { } else {
assert.Fail(t, "expected dir not found in permissions") assert.Fail(t, "expected dir not found in permissions")
@ -5176,7 +5187,7 @@ func TestMaxSessions(t *testing.T) {
func TestLoginInvalidFs(t *testing.T) { func TestLoginInvalidFs(t *testing.T) {
u := getTestUser() u := getTestUser()
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -5315,7 +5326,7 @@ func TestWebAPIPublicKeys(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr) checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "could not parse key") assert.Contains(t, rr.Body.String(), "could not parse key")
user.Filters.WebClient = append(user.Filters.WebClient, dataprovider.WebClientPubKeyChangeDisabled) user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
@ -5371,7 +5382,7 @@ func TestWebClientChangePubKeys(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse key") assert.Contains(t, rr.Body.String(), "Validation error: could not parse key")
user.Filters.WebClient = append(user.Filters.WebClient, dataprovider.WebClientPubKeyChangeDisabled) user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
webToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword) webToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
@ -5766,7 +5777,7 @@ func TestGetFilesSFTPBackend(t *testing.T) {
u.FsConfig.SFTPConfig.BufferSize = 2 u.FsConfig.SFTPConfig.BufferSize = 2
u.Permissions["/adir"] = nil u.Permissions["/adir"] = nil
u.Permissions["/adir1"] = []string{dataprovider.PermListItems} u.Permissions["/adir1"] = []string{dataprovider.PermListItems}
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/adir2", Path: "/adir2",
DeniedPatterns: []string{"*.txt"}, DeniedPatterns: []string{"*.txt"},
@ -6807,7 +6818,7 @@ func TestWebUserAddMock(t *testing.T) {
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: invalid TLS username") assert.Contains(t, rr.Body.String(), "Validation error: invalid TLS username")
form.Set("tls_username", string(dataprovider.TLSUsernameNone)) form.Set("tls_username", string(sdk.TLSUsernameNone))
form.Set(csrfFormToken, "invalid form token") form.Set(csrfFormToken, "invalid form token")
b, contentType, _ = getMultipartFormData(form, "", "") b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b) req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
@ -6853,10 +6864,10 @@ func TestWebUserAddMock(t *testing.T) {
assert.False(t, newUser.Filters.Hooks.PreLoginDisabled) assert.False(t, newUser.Filters.Hooks.PreLoginDisabled)
assert.False(t, newUser.Filters.Hooks.CheckPasswordDisabled) assert.False(t, newUser.Filters.Hooks.CheckPasswordDisabled)
assert.True(t, newUser.Filters.DisableFsChecks) assert.True(t, newUser.Filters.DisableFsChecks)
assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys)) assert.True(t, util.IsStringInSlice(testPubKey, newUser.PublicKeys))
if val, ok := newUser.Permissions["/subdir"]; ok { if val, ok := newUser.Permissions["/subdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))
assert.True(t, utils.IsStringInSlice(dataprovider.PermDownload, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermDownload, val))
} else { } else {
assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions) assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions)
} }
@ -6874,23 +6885,23 @@ func TestWebUserAddMock(t *testing.T) {
if filter.Path == "/dir1" { if filter.Path == "/dir1" {
assert.Len(t, filter.DeniedPatterns, 1) assert.Len(t, filter.DeniedPatterns, 1)
assert.Len(t, filter.AllowedPatterns, 1) assert.Len(t, filter.AllowedPatterns, 1)
assert.True(t, utils.IsStringInSlice("*.png", filter.AllowedPatterns)) assert.True(t, util.IsStringInSlice("*.png", filter.AllowedPatterns))
assert.True(t, utils.IsStringInSlice("*.zip", filter.DeniedPatterns)) assert.True(t, util.IsStringInSlice("*.zip", filter.DeniedPatterns))
} }
if filter.Path == "/dir2" { if filter.Path == "/dir2" {
assert.Len(t, filter.DeniedPatterns, 1) assert.Len(t, filter.DeniedPatterns, 1)
assert.Len(t, filter.AllowedPatterns, 2) assert.Len(t, filter.AllowedPatterns, 2)
assert.True(t, utils.IsStringInSlice("*.jpg", filter.AllowedPatterns)) assert.True(t, util.IsStringInSlice("*.jpg", filter.AllowedPatterns))
assert.True(t, utils.IsStringInSlice("*.png", filter.AllowedPatterns)) assert.True(t, util.IsStringInSlice("*.png", filter.AllowedPatterns))
assert.True(t, utils.IsStringInSlice("*.mkv", filter.DeniedPatterns)) assert.True(t, util.IsStringInSlice("*.mkv", filter.DeniedPatterns))
} }
if filter.Path == "/dir3" { if filter.Path == "/dir3" {
assert.Len(t, filter.DeniedPatterns, 1) assert.Len(t, filter.DeniedPatterns, 1)
assert.Len(t, filter.AllowedPatterns, 0) assert.Len(t, filter.AllowedPatterns, 0)
assert.True(t, utils.IsStringInSlice("*.rar", filter.DeniedPatterns)) assert.True(t, util.IsStringInSlice("*.rar", filter.DeniedPatterns))
} }
} }
assert.Equal(t, dataprovider.TLSUsernameNone, newUser.Filters.TLSUsername) assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil) req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
setBearerForReq(req, apiToken) setBearerForReq(req, apiToken)
rr = executeRequest(req) rr = executeRequest(req)
@ -6954,7 +6965,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("disconnect", "1") form.Set("disconnect", "1")
form.Set("additional_info", user.AdditionalInfo) form.Set("additional_info", user.AdditionalInfo)
form.Set("description", user.Description) form.Set("description", user.Description)
form.Set("tls_username", string(dataprovider.TLSUsernameCN)) form.Set("tls_username", string(sdk.TLSUsernameCN))
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -7017,19 +7028,19 @@ func TestWebUserUpdateMock(t *testing.T) {
assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo) assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo)
assert.Equal(t, user.Description, updateUser.Description) assert.Equal(t, user.Description, updateUser.Description)
assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize) assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize)
assert.Equal(t, dataprovider.TLSUsernameCN, updateUser.Filters.TLSUsername) assert.Equal(t, sdk.TLSUsernameCN, updateUser.Filters.TLSUsername)
if val, ok := updateUser.Permissions["/otherdir"]; ok { if val, ok := updateUser.Permissions["/otherdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))
assert.True(t, utils.IsStringInSlice(dataprovider.PermUpload, val)) assert.True(t, util.IsStringInSlice(dataprovider.PermUpload, val))
} else { } else {
assert.Fail(t, "user permissions must contains /otherdir", "actual: %v", updateUser.Permissions) assert.Fail(t, "user permissions must contains /otherdir", "actual: %v", updateUser.Permissions)
} }
assert.True(t, utils.IsStringInSlice("192.168.1.3/32", updateUser.Filters.AllowedIP)) assert.True(t, util.IsStringInSlice("192.168.1.3/32", updateUser.Filters.AllowedIP))
assert.True(t, utils.IsStringInSlice("10.0.0.2/32", updateUser.Filters.DeniedIP)) assert.True(t, util.IsStringInSlice("10.0.0.2/32", updateUser.Filters.DeniedIP))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, updateUser.Filters.DeniedLoginMethods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, updateUser.Filters.DeniedLoginMethods))
assert.True(t, utils.IsStringInSlice(common.ProtocolFTP, updateUser.Filters.DeniedProtocols)) assert.True(t, util.IsStringInSlice(common.ProtocolFTP, updateUser.Filters.DeniedProtocols))
assert.True(t, utils.IsStringInSlice("*.zip", updateUser.Filters.FilePatterns[0].DeniedPatterns)) assert.True(t, util.IsStringInSlice("*.zip", updateUser.Filters.FilePatterns[0].DeniedPatterns))
req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil) req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
assert.NoError(t, err) assert.NoError(t, err)
setBearerForReq(req, apiToken) setBearerForReq(req, apiToken)
@ -7220,7 +7231,7 @@ func TestUserTemplateMock(t *testing.T) {
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err) assert.NoError(t, err)
user := getTestUser() user := getTestUser()
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "test" user.FsConfig.S3Config.Bucket = "test"
user.FsConfig.S3Config.Region = "eu-central-1" user.FsConfig.S3Config.Region = "eu-central-1"
user.FsConfig.S3Config.AccessKey = "%username%" user.FsConfig.S3Config.AccessKey = "%username%"
@ -7322,9 +7333,9 @@ func TestUserTemplateMock(t *testing.T) {
user1 := dump.Users[0] user1 := dump.Users[0]
user2 := dump.Users[1] user2 := dump.Users[1]
require.Equal(t, "user1", user1.Username) require.Equal(t, "user1", user1.Username)
require.Equal(t, vfs.S3FilesystemProvider, user1.FsConfig.Provider) require.Equal(t, sdk.S3FilesystemProvider, user1.FsConfig.Provider)
require.Equal(t, "user2", user2.Username) require.Equal(t, "user2", user2.Username)
require.Equal(t, vfs.S3FilesystemProvider, user2.FsConfig.Provider) require.Equal(t, sdk.S3FilesystemProvider, user2.FsConfig.Provider)
require.Len(t, user2.PublicKeys, 1) require.Len(t, user2.PublicKeys, 1)
require.Equal(t, filepath.Join(os.TempDir(), user1.Username), user1.HomeDir) require.Equal(t, filepath.Join(os.TempDir(), user1.Username), user1.HomeDir)
require.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir) require.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir)
@ -7493,7 +7504,7 @@ func TestWebUserS3Mock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user) err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.S3FilesystemProvider user.FsConfig.Provider = sdk.S3FilesystemProvider
user.FsConfig.S3Config.Bucket = "test" user.FsConfig.S3Config.Bucket = "test"
user.FsConfig.S3Config.Region = "eu-west-1" user.FsConfig.S3Config.Region = "eu-west-1"
user.FsConfig.S3Config.AccessKey = "access-key" user.FsConfig.S3Config.AccessKey = "access-key"
@ -7652,7 +7663,7 @@ func TestWebUserGCSMock(t *testing.T) {
credentialsFilePath := filepath.Join(os.TempDir(), "gcs.json") credentialsFilePath := filepath.Join(os.TempDir(), "gcs.json")
err = createTestFile(credentialsFilePath, 0) err = createTestFile(credentialsFilePath, 0)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.GCSFilesystemProvider user.FsConfig.Provider = sdk.GCSFilesystemProvider
user.FsConfig.GCSConfig.Bucket = "test" user.FsConfig.GCSConfig.Bucket = "test"
user.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/" user.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/"
user.FsConfig.GCSConfig.StorageClass = "standard" user.FsConfig.GCSConfig.StorageClass = "standard"
@ -7757,7 +7768,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user) err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
user.FsConfig.AzBlobConfig.Container = "container" user.FsConfig.AzBlobConfig.Container = "container"
user.FsConfig.AzBlobConfig.AccountName = "aname" user.FsConfig.AzBlobConfig.AccountName = "aname"
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("access-skey") user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("access-skey")
@ -7924,7 +7935,7 @@ func TestWebUserCryptMock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user) err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.CryptedFilesystemProvider user.FsConfig.Provider = sdk.CryptedFilesystemProvider
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypted passphrase") user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("crypted passphrase")
form := make(url.Values) form := make(url.Values)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
@ -8019,7 +8030,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr) checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user) err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err) assert.NoError(t, err)
user.FsConfig.Provider = vfs.SFTPFilesystemProvider user.FsConfig.Provider = sdk.SFTPFilesystemProvider
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:22" user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:22"
user.FsConfig.SFTPConfig.Username = "sftpuser" user.FsConfig.SFTPConfig.Username = "sftpuser"
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("pwd") user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("pwd")
@ -8266,7 +8277,7 @@ func TestS3WebFolderMock(t *testing.T) {
assert.Equal(t, mappedPath, folder.MappedPath) assert.Equal(t, mappedPath, folder.MappedPath)
assert.Equal(t, folderName, folder.Name) assert.Equal(t, folderName, folder.Name)
assert.Equal(t, folderDesc, folder.Description) assert.Equal(t, folderDesc, folder.Description)
assert.Equal(t, vfs.S3FilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket) assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)
assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region) assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey) assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
@ -8308,7 +8319,7 @@ func TestS3WebFolderMock(t *testing.T) {
assert.Equal(t, mappedPath, folder.MappedPath) assert.Equal(t, mappedPath, folder.MappedPath)
assert.Equal(t, folderName, folder.Name) assert.Equal(t, folderName, folder.Name)
assert.Equal(t, folderDesc, folder.Description) assert.Equal(t, folderDesc, folder.Description)
assert.Equal(t, vfs.S3FilesystemProvider, folder.FsConfig.Provider) assert.Equal(t, sdk.S3FilesystemProvider, folder.FsConfig.Provider)
assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket) assert.Equal(t, S3Bucket, folder.FsConfig.S3Config.Bucket)
assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region) assert.Equal(t, S3Region, folder.FsConfig.S3Config.Region)
assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey) assert.Equal(t, S3AccessKey, folder.FsConfig.S3Config.AccessKey)
@ -8690,11 +8701,13 @@ func getTestAdmin() dataprovider.Admin {
func getTestUser() dataprovider.User { func getTestUser() dataprovider.User {
user := dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, BaseUser: sdk.BaseUser{
Password: defaultPassword, Username: defaultUsername,
HomeDir: filepath.Join(homeBasePath, defaultUsername), Password: defaultPassword,
Status: 1, HomeDir: filepath.Join(homeBasePath, defaultUsername),
Description: "test user", Status: 1,
Description: "test user",
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = defaultPerms user.Permissions["/"] = defaultPerms
@ -8704,7 +8717,7 @@ func getTestUser() dataprovider.User {
func getTestSFTPUser() dataprovider.User { func getTestSFTPUser() dataprovider.User {
u := getTestUser() u := getTestUser()
u.Username = u.Username + "_sftp" u.Username = u.Username + "_sftp"
u.FsConfig.Provider = vfs.SFTPFilesystemProvider u.FsConfig.Provider = sdk.SFTPFilesystemProvider
u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr
u.FsConfig.SFTPConfig.Username = defaultUsername u.FsConfig.SFTPConfig.Username = defaultUsername
u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)

View file

@ -32,7 +32,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -298,7 +299,7 @@ func TestShouldBind(t *testing.T) {
func TestGetRespStatus(t *testing.T) { func TestGetRespStatus(t *testing.T) {
var err error var err error
err = utils.NewMethodDisabledError("") err = util.NewMethodDisabledError("")
respStatus := getRespStatus(err) respStatus := getRespStatus(err)
assert.Equal(t, http.StatusForbidden, respStatus) assert.Equal(t, http.StatusForbidden, respStatus)
err = fmt.Errorf("generic error") err = fmt.Errorf("generic error")
@ -457,16 +458,16 @@ func TestCSRFToken(t *testing.T) {
assert.Equal(t, http.StatusForbidden, rr.Code) assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "the token is not valid") assert.Contains(t, rr.Body.String(), "the token is not valid")
csrfTokenAuth = jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil) csrfTokenAuth = jwtauth.New("PS256", util.GenerateRandomBytes(32), nil)
tokenString = createCSRFToken() tokenString = createCSRFToken()
assert.Empty(t, tokenString) assert.Empty(t, tokenString)
csrfTokenAuth = jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil) csrfTokenAuth = jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
} }
func TestCreateTokenError(t *testing.T) { func TestCreateTokenError(t *testing.T) {
server := httpdServer{ server := httpdServer{
tokenAuth: jwtauth.New("PS256", utils.GenerateRandomBytes(32), nil), tokenAuth: jwtauth.New("PS256", util.GenerateRandomBytes(32), nil),
} }
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
admin := dataprovider.Admin{ admin := dataprovider.Admin{
@ -480,8 +481,10 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
user := dataprovider.User{ user := dataprovider.User{
Username: "u", BaseUser: sdk.BaseUser{
Password: "pwd", Username: "u",
Password: "pwd",
},
} }
req, _ = http.NewRequest(http.MethodGet, userTokenPath, nil) req, _ = http.NewRequest(http.MethodGet, userTokenPath, nil)
@ -540,11 +543,13 @@ func TestCreateTokenError(t *testing.T) {
username := "webclientuser" username := "webclientuser"
user = dataprovider.User{ user = dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Password: "clientpwd", Username: username,
HomeDir: filepath.Join(os.TempDir(), username), Password: "clientpwd",
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
Description: "test user", Status: 1,
Description: "test user",
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{"*"} user.Permissions["/"] = []string{"*"}
@ -567,7 +572,7 @@ func TestCreateTokenError(t *testing.T) {
} }
func TestJWTTokenValidation(t *testing.T) { func TestJWTTokenValidation(t *testing.T) {
tokenAuth := jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil) tokenAuth := jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
claims := make(map[string]interface{}) claims := make(map[string]interface{})
claims["username"] = "admin" claims["username"] = "admin"
claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour) claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour)
@ -616,7 +621,7 @@ func TestJWTTokenValidation(t *testing.T) {
fn.ServeHTTP(rr, req.WithContext(ctx)) fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
permClientFn := checkHTTPUserPerm(dataprovider.WebClientPubKeyChangeDisabled) permClientFn := checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
fn = permClientFn(r) fn = permClientFn(r)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, nil) req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, nil)
@ -635,7 +640,7 @@ func TestJWTTokenValidation(t *testing.T) {
func TestUpdateContextFromCookie(t *testing.T) { func TestUpdateContextFromCookie(t *testing.T) {
server := httpdServer{ server := httpdServer{
tokenAuth: jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil), tokenAuth: jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil),
} }
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
claims := make(map[string]interface{}) claims := make(map[string]interface{})
@ -649,7 +654,7 @@ func TestUpdateContextFromCookie(t *testing.T) {
func TestCookieExpiration(t *testing.T) { func TestCookieExpiration(t *testing.T) {
server := httpdServer{ server := httpdServer{
tokenAuth: jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil), tokenAuth: jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil),
} }
err := errors.New("test error") err := errors.New("test error")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@ -736,11 +741,13 @@ func TestCookieExpiration(t *testing.T) {
// now check client cookie expiration // now check client cookie expiration
username := "client" username := "client"
user := dataprovider.User{ user := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Password: "clientpwd", Username: username,
HomeDir: filepath.Join(os.TempDir(), username), Password: "clientpwd",
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
Description: "test user", Status: 1,
Description: "test user",
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{"*"} user.Permissions["/"] = []string{"*"}
@ -862,10 +869,12 @@ func TestRenderInvalidTemplate(t *testing.T) {
func TestQuotaScanInvalidFs(t *testing.T) { func TestQuotaScanInvalidFs(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "test", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "test",
HomeDir: os.TempDir(),
},
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.S3FilesystemProvider, Provider: sdk.S3FilesystemProvider,
}, },
} }
common.QuotaScans.AddUserQuotaScan(user.Username) common.QuotaScans.AddUserQuotaScan(user.Username)
@ -947,24 +956,24 @@ func TestGetFolderFromTemplate(t *testing.T) {
require.Equal(t, fmt.Sprintf("Folder%v", folderName), folderTemplate.MappedPath) require.Equal(t, fmt.Sprintf("Folder%v", folderName), folderTemplate.MappedPath)
require.Equal(t, fmt.Sprintf("Folder %v desc", folderName), folderTemplate.Description) require.Equal(t, fmt.Sprintf("Folder %v desc", folderName), folderTemplate.Description)
folder.FsConfig.Provider = vfs.CryptedFilesystemProvider folder.FsConfig.Provider = sdk.CryptedFilesystemProvider
folder.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("%name%") folder.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("%name%")
folderTemplate = getFolderFromTemplate(folder, folderName) folderTemplate = getFolderFromTemplate(folder, folderName)
require.Equal(t, folderName, folderTemplate.FsConfig.CryptConfig.Passphrase.GetPayload()) require.Equal(t, folderName, folderTemplate.FsConfig.CryptConfig.Passphrase.GetPayload())
folder.FsConfig.Provider = vfs.GCSFilesystemProvider folder.FsConfig.Provider = sdk.GCSFilesystemProvider
folder.FsConfig.GCSConfig.KeyPrefix = "prefix%name%/" folder.FsConfig.GCSConfig.KeyPrefix = "prefix%name%/"
folderTemplate = getFolderFromTemplate(folder, folderName) folderTemplate = getFolderFromTemplate(folder, folderName)
require.Equal(t, fmt.Sprintf("prefix%v/", folderName), folderTemplate.FsConfig.GCSConfig.KeyPrefix) require.Equal(t, fmt.Sprintf("prefix%v/", folderName), folderTemplate.FsConfig.GCSConfig.KeyPrefix)
folder.FsConfig.Provider = vfs.AzureBlobFilesystemProvider folder.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
folder.FsConfig.AzBlobConfig.KeyPrefix = "a%name%" folder.FsConfig.AzBlobConfig.KeyPrefix = "a%name%"
folder.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("pwd%name%") folder.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("pwd%name%")
folderTemplate = getFolderFromTemplate(folder, folderName) folderTemplate = getFolderFromTemplate(folder, folderName)
require.Equal(t, "a"+folderName, folderTemplate.FsConfig.AzBlobConfig.KeyPrefix) require.Equal(t, "a"+folderName, folderTemplate.FsConfig.AzBlobConfig.KeyPrefix)
require.Equal(t, "pwd"+folderName, folderTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload()) require.Equal(t, "pwd"+folderName, folderTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload())
folder.FsConfig.Provider = vfs.SFTPFilesystemProvider folder.FsConfig.Provider = sdk.SFTPFilesystemProvider
folder.FsConfig.SFTPConfig.Prefix = "%name%" folder.FsConfig.SFTPConfig.Prefix = "%name%"
folder.FsConfig.SFTPConfig.Username = "sftp_%name%" folder.FsConfig.SFTPConfig.Username = "sftp_%name%"
folder.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp%name%") folder.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp%name%")
@ -976,7 +985,9 @@ func TestGetFolderFromTemplate(t *testing.T) {
func TestGetUserFromTemplate(t *testing.T) { func TestGetUserFromTemplate(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Status: 1, BaseUser: sdk.BaseUser{
Status: 1,
},
} }
user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{ user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
@ -995,24 +1006,24 @@ func TestGetUserFromTemplate(t *testing.T) {
require.Len(t, userTemplate.VirtualFolders, 1) require.Len(t, userTemplate.VirtualFolders, 1)
require.Equal(t, "Folder"+username, userTemplate.VirtualFolders[0].Name) require.Equal(t, "Folder"+username, userTemplate.VirtualFolders[0].Name)
user.FsConfig.Provider = vfs.CryptedFilesystemProvider user.FsConfig.Provider = sdk.CryptedFilesystemProvider
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("%password%") user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("%password%")
userTemplate = getUserFromTemplate(user, templateFields) userTemplate = getUserFromTemplate(user, templateFields)
require.Equal(t, password, userTemplate.FsConfig.CryptConfig.Passphrase.GetPayload()) require.Equal(t, password, userTemplate.FsConfig.CryptConfig.Passphrase.GetPayload())
user.FsConfig.Provider = vfs.GCSFilesystemProvider user.FsConfig.Provider = sdk.GCSFilesystemProvider
user.FsConfig.GCSConfig.KeyPrefix = "%username%%password%" user.FsConfig.GCSConfig.KeyPrefix = "%username%%password%"
userTemplate = getUserFromTemplate(user, templateFields) userTemplate = getUserFromTemplate(user, templateFields)
require.Equal(t, username+password, userTemplate.FsConfig.GCSConfig.KeyPrefix) require.Equal(t, username+password, userTemplate.FsConfig.GCSConfig.KeyPrefix)
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
user.FsConfig.AzBlobConfig.KeyPrefix = "a%username%" user.FsConfig.AzBlobConfig.KeyPrefix = "a%username%"
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("pwd%password%%username%") user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("pwd%password%%username%")
userTemplate = getUserFromTemplate(user, templateFields) userTemplate = getUserFromTemplate(user, templateFields)
require.Equal(t, "a"+username, userTemplate.FsConfig.AzBlobConfig.KeyPrefix) require.Equal(t, "a"+username, userTemplate.FsConfig.AzBlobConfig.KeyPrefix)
require.Equal(t, "pwd"+password+username, userTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload()) require.Equal(t, "pwd"+password+username, userTemplate.FsConfig.AzBlobConfig.AccountKey.GetPayload())
user.FsConfig.Provider = vfs.SFTPFilesystemProvider user.FsConfig.Provider = sdk.SFTPFilesystemProvider
user.FsConfig.SFTPConfig.Prefix = "%username%" user.FsConfig.SFTPConfig.Prefix = "%username%"
user.FsConfig.SFTPConfig.Username = "sftp_%username%" user.FsConfig.SFTPConfig.Username = "sftp_%username%"
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp%password%") user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp%password%")
@ -1024,7 +1035,7 @@ func TestGetUserFromTemplate(t *testing.T) {
func TestJWTTokenCleanup(t *testing.T) { func TestJWTTokenCleanup(t *testing.T) {
server := httpdServer{ server := httpdServer{
tokenAuth: jwtauth.New(jwa.HS256.String(), utils.GenerateRandomBytes(32), nil), tokenAuth: jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil),
} }
admin := dataprovider.Admin{ admin := dataprovider.Admin{
Username: "newtestadmin", Username: "newtestadmin",
@ -1208,7 +1219,9 @@ func TestCompressorAbortHandler(t *testing.T) {
func TestZipErrors(t *testing.T) { func TestZipErrors(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -1233,7 +1246,7 @@ func TestZipErrors(t *testing.T) {
} }
testFilePath := filepath.Join(testDir, "ziptest.zip") testFilePath := filepath.Join(testDir, "ziptest.zip")
err = os.WriteFile(testFilePath, utils.GenerateRandomBytes(65535), os.ModePerm) err = os.WriteFile(testFilePath, util.GenerateRandomBytes(65535), os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
err = addZipEntry(wr, connection, path.Join("/", filepath.Base(testDir), filepath.Base(testFilePath)), err = addZipEntry(wr, connection, path.Join("/", filepath.Base(testDir), filepath.Base(testFilePath)),
"/"+filepath.Base(testDir)) "/"+filepath.Base(testDir))
@ -1258,7 +1271,7 @@ func TestZipErrors(t *testing.T) {
err = addZipEntry(wr, connection, user.VirtualFolders[0].VirtualPath, "/") err = addZipEntry(wr, connection, user.VirtualFolders[0].VirtualPath, "/")
assert.Error(t, err) assert.Error(t, err)
user.Filters.FilePatterns = append(user.Filters.FilePatterns, dataprovider.PatternsFilter{ user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/", Path: "/",
DeniedPatterns: []string{"*.zip"}, DeniedPatterns: []string{"*.zip"},
}) })
@ -1412,13 +1425,17 @@ func TestRequestHeaderErrors(t *testing.T) {
func TestConnection(t *testing.T) { func TestConnection(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "test_httpd_user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "test_httpd_user",
HomeDir: filepath.Clean(os.TempDir()),
},
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.GCSFilesystemProvider, Provider: sdk.GCSFilesystemProvider,
GCSConfig: vfs.GCSFsConfig{ GCSConfig: vfs.GCSFsConfig{
Bucket: "test_bucket_name", GCSFsConfig: sdk.GCSFsConfig{
Credentials: kms.NewPlainSecret("invalid JSON payload"), Bucket: "test_bucket_name",
Credentials: kms.NewPlainSecret("invalid JSON payload"),
},
}, },
}, },
} }
@ -1434,15 +1451,17 @@ func TestConnection(t *testing.T) {
name := "missing file name" name := "missing file name"
_, err := connection.getFileReader(name, 0, http.MethodGet) _, 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 = sdk.LocalFilesystemProvider
_, err = connection.getFileReader(name, 0, http.MethodGet) _, err = connection.getFileReader(name, 0, http.MethodGet)
assert.ErrorIs(t, err, os.ErrNotExist) assert.ErrorIs(t, err, os.ErrNotExist)
} }
func TestHTTPDFile(t *testing.T) { func TestHTTPDFile(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "test_httpd_user", BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()), Username: "test_httpd_user",
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -1500,8 +1519,10 @@ func TestGetFilesInvalidClaims(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
user := dataprovider.User{ user := dataprovider.User{
Username: "", BaseUser: sdk.BaseUser{
Password: "pwd", Username: "",
Password: "pwd",
},
} }
c := jwtTokenClaims{ c := jwtTokenClaims{
Username: user.Username, Username: user.Username,
@ -1538,8 +1559,10 @@ func TestManageKeysInvalidClaims(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
user := dataprovider.User{ user := dataprovider.User{
Username: "", BaseUser: sdk.BaseUser{
Password: "pwd", Username: "",
Password: "pwd",
},
} }
c := jwtTokenClaims{ c := jwtTokenClaims{
Username: user.Username, Username: user.Username,
@ -1585,8 +1608,10 @@ func TestSigningKey(t *testing.T) {
server2.initializeRouter() server2.initializeRouter()
user := dataprovider.User{ user := dataprovider.User{
Username: "", BaseUser: sdk.BaseUser{
Password: "pwd", Username: "",
Password: "pwd",
},
} }
c := jwtTokenClaims{ c := jwtTokenClaims{
Username: user.Username, Username: user.Username,

View file

@ -10,7 +10,7 @@ import (
"github.com/lestrrat-go/jwx/jwt" "github.com/lestrrat-go/jwx/jwt"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
var ( var (
@ -58,7 +58,7 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
} }
return errInvalidToken return errInvalidToken
} }
if !utils.IsStringInSlice(audience, token.Audience()) { if !util.IsStringInSlice(audience, token.Audience()) {
logger.Debug(logSender, "", "the token is not valid for audience %#v", audience) logger.Debug(logSender, "", "the token is not valid for audience %#v", audience)
if isAPIToken { if isAPIToken {
sendAPIResponse(w, r, nil, "Your token audience is not valid", http.StatusUnauthorized) sendAPIResponse(w, r, nil, "Your token audience is not valid", http.StatusUnauthorized)
@ -192,7 +192,7 @@ func verifyCSRFHeader(next http.Handler) http.Handler {
return return
} }
if !utils.IsStringInSlice(tokenAudienceCSRF, token.Audience()) { if !util.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
logger.Debug(logSender, "", "error validating CSRF header audience") logger.Debug(logSender, "", "error validating CSRF header audience")
sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden) sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
return return

View file

@ -21,7 +21,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -65,7 +66,7 @@ func (s *httpdServer) listenAndServe() error {
config := &tls.Config{ config := &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(), GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
CipherSuites: utils.GetTLSCiphersFromNames(s.binding.TLSCipherSuites), CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
} }
logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(), logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
@ -76,9 +77,9 @@ func (s *httpdServer) listenAndServe() error {
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection
} }
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender) return util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender)
} }
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender) return util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender)
} }
func (s *httpdServer) verifyTLSConnection(state tls.ConnectionState) error { func (s *httpdServer) verifyTLSConnection(state tls.ConnectionState) error {
@ -122,16 +123,16 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
renderClientLoginPage(w, err.Error()) renderClientLoginPage(w, err.Error())
return return
} }
ipAddr := utils.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
username := r.Form.Get("username") username := r.Form.Get("username")
password := r.Form.Get("password") password := r.Form.Get("password")
if username == "" || password == "" { if username == "" || password == "" {
updateLoginMetrics(&dataprovider.User{Username: username}, ipAddr, common.ErrNoCredentials) updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
renderClientLoginPage(w, "Invalid credentials") renderClientLoginPage(w, "Invalid credentials")
return return
} }
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil { if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
updateLoginMetrics(&dataprovider.User{Username: username}, ipAddr, err) updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
renderClientLoginPage(w, err.Error()) renderClientLoginPage(w, err.Error())
return return
} }
@ -197,7 +198,7 @@ func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Req
renderLoginPage(w, err.Error()) renderLoginPage(w, err.Error())
return return
} }
admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr)) admin, err := dataprovider.CheckAdminAndPass(username, password, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil { if err != nil {
renderLoginPage(w, err.Error()) renderLoginPage(w, err.Error())
return return
@ -272,16 +273,16 @@ func (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {
} }
func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
ipAddr := utils.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
username, password, ok := r.BasicAuth() username, password, ok := r.BasicAuth()
if !ok { if !ok {
updateLoginMetrics(&dataprovider.User{Username: username}, ipAddr, common.ErrNoCredentials) updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
if username == "" || password == "" { if username == "" || password == "" {
updateLoginMetrics(&dataprovider.User{Username: username}, ipAddr, common.ErrNoCredentials) updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
@ -344,7 +345,7 @@ func (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
admin, err := dataprovider.CheckAdminAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr)) admin, err := dataprovider.CheckAdminAndPass(username, password, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil { if err != nil {
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm) w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
@ -384,7 +385,7 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
if time.Until(token.Expiration()) > tokenRefreshMin { if time.Until(token.Expiration()) > tokenRefreshMin {
return return
} }
if utils.IsStringInSlice(tokenAudienceWebClient, token.Audience()) { if util.IsStringInSlice(tokenAudienceWebClient, token.Audience()) {
s.refreshClientToken(w, r, tokenClaims) s.refreshClientToken(w, r, tokenClaims)
} else { } else {
s.refreshAdminToken(w, r, tokenClaims) s.refreshAdminToken(w, r, tokenClaims)
@ -422,7 +423,7 @@ func (s *httpdServer) refreshAdminToken(w http.ResponseWriter, r *http.Request,
logger.Debug(logSender, "", "signature mismatch for admin %#v, unable to refresh cookie", admin.Username) logger.Debug(logSender, "", "signature mismatch for admin %#v, unable to refresh cookie", admin.Username)
return return
} }
if !admin.CanLoginFromIP(utils.GetIPFromRemoteAddress(r.RemoteAddr)) { if !admin.CanLoginFromIP(util.GetIPFromRemoteAddress(r.RemoteAddr)) {
logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie", admin.Username, r.RemoteAddr) logger.Debug(logSender, "", "admin %#v cannot login from %v, unable to refresh cookie", admin.Username, r.RemoteAddr)
return return
} }
@ -446,12 +447,12 @@ func (s *httpdServer) updateContextFromCookie(r *http.Request) *http.Request {
func (s *httpdServer) checkConnection(next http.Handler) http.Handler { func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ipAddr := utils.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
ip := net.ParseIP(ipAddr) ip := net.ParseIP(ipAddr)
if ip != nil { if ip != nil {
for _, allow := range s.binding.allowHeadersFrom { for _, allow := range s.binding.allowHeadersFrom {
if allow(ip) { if allow(ip) {
parsedIP := utils.GetRealIP(r) parsedIP := util.GetRealIP(r)
if parsedIP != "" { if parsedIP != "" {
ipAddr = parsedIP ipAddr = parsedIP
r.RemoteAddr = ipAddr r.RemoteAddr = ipAddr
@ -628,8 +629,8 @@ func (s *httpdServer) initializeRouter() {
router.Get(userLogoutPath, s.logout) router.Get(userLogoutPath, s.logout)
router.Put(userPwdPath, changeUserPassword) router.Put(userPwdPath, changeUserPassword)
router.With(checkHTTPUserPerm(dataprovider.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys) router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
router.With(checkHTTPUserPerm(dataprovider.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys) router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
router.Get(userReadFolderPath, readUserFolder) router.Get(userReadFolderPath, readUserFolder)
router.Get(userGetFilePath, getUserFile) router.Get(userGetFilePath, getUserFile)
router.Post(userStreamZipPath, getUserFilesAsZipStream) router.Post(userStreamZipPath, getUserFilesAsZipStream)
@ -674,7 +675,7 @@ func (s *httpdServer) initializeRouter() {
router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip) router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip)
router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials) router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost) router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
router.With(checkHTTPUserPerm(dataprovider.WebClientPubKeyChangeDisabled)). router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
Post(webChangeClientKeysPath, handleWebClientManageKeysPost) Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
}) })
} }

View file

@ -17,7 +17,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -253,22 +254,22 @@ func loadAdminTemplates(templatesPath string) {
} }
rootTpl := template.New("").Funcs(template.FuncMap{ rootTpl := template.New("").Funcs(template.FuncMap{
"ListFSProviders": vfs.ListProviders, "ListFSProviders": sdk.ListProviders,
}) })
usersTmpl := utils.LoadTemplate(rootTpl, usersPaths...) usersTmpl := util.LoadTemplate(rootTpl, usersPaths...)
userTmpl := utils.LoadTemplate(rootTpl, userPaths...) userTmpl := util.LoadTemplate(rootTpl, userPaths...)
adminsTmpl := utils.LoadTemplate(rootTpl, adminsPaths...) adminsTmpl := util.LoadTemplate(rootTpl, adminsPaths...)
adminTmpl := utils.LoadTemplate(rootTpl, adminPaths...) adminTmpl := util.LoadTemplate(rootTpl, adminPaths...)
connectionsTmpl := utils.LoadTemplate(rootTpl, connectionsPaths...) connectionsTmpl := util.LoadTemplate(rootTpl, connectionsPaths...)
messageTmpl := utils.LoadTemplate(rootTpl, messagePath...) messageTmpl := util.LoadTemplate(rootTpl, messagePath...)
foldersTmpl := utils.LoadTemplate(rootTpl, foldersPath...) foldersTmpl := util.LoadTemplate(rootTpl, foldersPath...)
folderTmpl := utils.LoadTemplate(rootTpl, folderPath...) folderTmpl := util.LoadTemplate(rootTpl, folderPath...)
statusTmpl := utils.LoadTemplate(rootTpl, statusPath...) statusTmpl := util.LoadTemplate(rootTpl, statusPath...)
loginTmpl := utils.LoadTemplate(rootTpl, loginPath...) loginTmpl := util.LoadTemplate(rootTpl, loginPath...)
changePwdTmpl := utils.LoadTemplate(rootTpl, changePwdPaths...) changePwdTmpl := util.LoadTemplate(rootTpl, changePwdPaths...)
maintenanceTmpl := utils.LoadTemplate(rootTpl, maintenancePath...) maintenanceTmpl := util.LoadTemplate(rootTpl, maintenancePath...)
defenderTmpl := utils.LoadTemplate(rootTpl, defenderPath...) defenderTmpl := util.LoadTemplate(rootTpl, defenderPath...)
setupTmpl := utils.LoadTemplate(rootTpl, setupPath...) setupTmpl := util.LoadTemplate(rootTpl, setupPath...)
adminTemplates[templateUsers] = usersTmpl adminTemplates[templateUsers] = usersTmpl
adminTemplates[templateUser] = userTmpl adminTemplates[templateUser] = userTmpl
@ -441,7 +442,7 @@ func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.U
ValidPerms: dataprovider.ValidPerms, ValidPerms: dataprovider.ValidPerms,
ValidLoginMethods: dataprovider.ValidLoginMethods, ValidLoginMethods: dataprovider.ValidLoginMethods,
ValidProtocols: dataprovider.ValidProtocols, ValidProtocols: dataprovider.ValidProtocols,
WebClientOptions: dataprovider.WebClientOptions, WebClientOptions: sdk.WebClientOptions,
RootDirPerms: user.GetPermissionsForPath("/"), RootDirPerms: user.GetPermissionsForPath("/"),
VirtualFolders: folders, VirtualFolders: folders,
} }
@ -583,8 +584,8 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
return permissions return permissions
} }
func getFilePatternsFromPostField(r *http.Request) []dataprovider.PatternsFilter { func getFilePatternsFromPostField(r *http.Request) []sdk.PatternsFilter {
var result []dataprovider.PatternsFilter var result []sdk.PatternsFilter
allowedPatterns := make(map[string][]string) allowedPatterns := make(map[string][]string)
deniedPatterns := make(map[string][]string) deniedPatterns := make(map[string][]string)
@ -607,13 +608,13 @@ func getFilePatternsFromPostField(r *http.Request) []dataprovider.PatternsFilter
} }
for dirAllowed, allowPatterns := range allowedPatterns { for dirAllowed, allowPatterns := range allowedPatterns {
filter := dataprovider.PatternsFilter{ filter := sdk.PatternsFilter{
Path: dirAllowed, Path: dirAllowed,
AllowedPatterns: utils.RemoveDuplicates(allowPatterns), AllowedPatterns: util.RemoveDuplicates(allowPatterns),
} }
for dirDenied, denPatterns := range deniedPatterns { for dirDenied, denPatterns := range deniedPatterns {
if dirAllowed == dirDenied { if dirAllowed == dirDenied {
filter.DeniedPatterns = utils.RemoveDuplicates(denPatterns) filter.DeniedPatterns = util.RemoveDuplicates(denPatterns)
break break
} }
} }
@ -628,7 +629,7 @@ func getFilePatternsFromPostField(r *http.Request) []dataprovider.PatternsFilter
} }
} }
if !found { if !found {
result = append(result, dataprovider.PatternsFilter{ result = append(result, sdk.PatternsFilter{
Path: dirDenied, Path: dirDenied,
DeniedPatterns: denPatterns, DeniedPatterns: denPatterns,
}) })
@ -637,23 +638,23 @@ func getFilePatternsFromPostField(r *http.Request) []dataprovider.PatternsFilter
return result return result
} }
func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters { func getFiltersFromUserPostFields(r *http.Request) sdk.UserFilters {
var filters dataprovider.UserFilters var filters sdk.UserFilters
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",") filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["ssh_login_methods"] filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
filters.DeniedProtocols = r.Form["denied_protocols"] filters.DeniedProtocols = r.Form["denied_protocols"]
filters.FilePatterns = getFilePatternsFromPostField(r) filters.FilePatterns = getFilePatternsFromPostField(r)
filters.TLSUsername = dataprovider.TLSUsername(r.Form.Get("tls_username")) filters.TLSUsername = sdk.TLSUsername(r.Form.Get("tls_username"))
filters.WebClient = r.Form["web_client_options"] filters.WebClient = r.Form["web_client_options"]
hooks := r.Form["hooks"] hooks := r.Form["hooks"]
if utils.IsStringInSlice("external_auth_disabled", hooks) { if util.IsStringInSlice("external_auth_disabled", hooks) {
filters.Hooks.ExternalAuthDisabled = true filters.Hooks.ExternalAuthDisabled = true
} }
if utils.IsStringInSlice("pre_login_disabled", hooks) { if util.IsStringInSlice("pre_login_disabled", hooks) {
filters.Hooks.PreLoginDisabled = true filters.Hooks.PreLoginDisabled = true
} }
if utils.IsStringInSlice("check_password_disabled", hooks) { if util.IsStringInSlice("check_password_disabled", hooks) {
filters.Hooks.CheckPasswordDisabled = true filters.Hooks.CheckPasswordDisabled = true
} }
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0 filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
@ -758,29 +759,29 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) { func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
var fs vfs.Filesystem var fs vfs.Filesystem
fs.Provider = vfs.GetProviderByName(r.Form.Get("fs_provider")) fs.Provider = sdk.GetProviderByName(r.Form.Get("fs_provider"))
switch fs.Provider { switch fs.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
config, err := getS3Config(r) config, err := getS3Config(r)
if err != nil { if err != nil {
return fs, err return fs, err
} }
fs.S3Config = config fs.S3Config = config
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
config, err := getAzureConfig(r) config, err := getAzureConfig(r)
if err != nil { if err != nil {
return fs, err return fs, err
} }
fs.AzBlobConfig = config fs.AzBlobConfig = config
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
config, err := getGCSConfig(r) config, err := getGCSConfig(r)
if err != nil { if err != nil {
return fs, err return fs, err
} }
fs.GCSConfig = config fs.GCSConfig = config
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
fs.CryptConfig.Passphrase = getSecretFromFormField(r, "crypt_passphrase") fs.CryptConfig.Passphrase = getSecretFromFormField(r, "crypt_passphrase")
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
config, err := getSFTPConfig(r) config, err := getSFTPConfig(r)
if err != nil { if err != nil {
return fs, err return fs, err
@ -826,15 +827,15 @@ func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVi
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements) folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
folder.Description = replacePlaceholders(folder.Description, replacements) folder.Description = replacePlaceholders(folder.Description, replacements)
switch folder.FsConfig.Provider { switch folder.FsConfig.Provider {
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
folder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements) folder.FsConfig.CryptConfig = getCryptFsFromTemplate(folder.FsConfig.CryptConfig, replacements)
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
folder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements) folder.FsConfig.S3Config = getS3FsFromTemplate(folder.FsConfig.S3Config, replacements)
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
folder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements) folder.FsConfig.GCSConfig = getGCSFsFromTemplate(folder.FsConfig.GCSConfig, replacements)
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements) folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements) folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
} }
@ -910,15 +911,15 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements) user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
switch user.FsConfig.Provider { switch user.FsConfig.Provider {
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements) user.FsConfig.CryptConfig = getCryptFsFromTemplate(user.FsConfig.CryptConfig, replacements)
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements) user.FsConfig.S3Config = getS3FsFromTemplate(user.FsConfig.S3Config, replacements)
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements) user.FsConfig.GCSConfig = getGCSFsFromTemplate(user.FsConfig.GCSConfig, replacements)
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements) user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements) user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
} }
@ -970,32 +971,34 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
if err != nil { if err != nil {
return user, err return user, err
} }
expirationDateMillis = utils.GetTimeAsMsSinceEpoch(expirationDate) expirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate)
} }
fsConfig, err := getFsConfigFromPostFields(r) fsConfig, err := getFsConfigFromPostFields(r)
if err != nil { if err != nil {
return user, err return user, err
} }
user = dataprovider.User{ user = dataprovider.User{
Username: r.Form.Get("username"), BaseUser: sdk.BaseUser{
Password: r.Form.Get("password"), Username: r.Form.Get("username"),
PublicKeys: r.Form["public_keys"], Password: r.Form.Get("password"),
HomeDir: r.Form.Get("home_dir"), PublicKeys: r.Form["public_keys"],
VirtualFolders: getVirtualFoldersFromPostFields(r), HomeDir: r.Form.Get("home_dir"),
UID: uid, UID: uid,
GID: gid, GID: gid,
Permissions: getUserPermissionsFromPostFields(r), Permissions: getUserPermissionsFromPostFields(r),
MaxSessions: maxSessions, MaxSessions: maxSessions,
QuotaSize: quotaSize, QuotaSize: quotaSize,
QuotaFiles: quotaFiles, QuotaFiles: quotaFiles,
UploadBandwidth: bandwidthUL, UploadBandwidth: bandwidthUL,
DownloadBandwidth: bandwidthDL, DownloadBandwidth: bandwidthDL,
Status: status, Status: status,
ExpirationDate: expirationDateMillis, ExpirationDate: expirationDateMillis,
Filters: getFiltersFromUserPostFields(r), Filters: getFiltersFromUserPostFields(r),
FsConfig: fsConfig, AdditionalInfo: r.Form.Get("additional_info"),
AdditionalInfo: r.Form.Get("additional_info"), Description: r.Form.Get("description"),
Description: r.Form.Get("description"), },
VirtualFolders: getVirtualFoldersFromPostFields(r),
FsConfig: fsConfig,
} }
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64) maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
user.Filters.MaxUploadFileSize = maxFileSize user.Filters.MaxUploadFileSize = maxFileSize
@ -1146,7 +1149,7 @@ func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
admin, err := dataprovider.AdminExists(username) admin, err := dataprovider.AdminExists(username)
if err == nil { if err == nil {
renderAddUpdateAdminPage(w, r, &admin, "", false) renderAddUpdateAdminPage(w, r, &admin, "", false)
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
@ -1177,7 +1180,7 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
username := getURLParam(r, "username") username := getURLParam(r, "username")
admin, err := dataprovider.AdminExists(username) admin, err := dataprovider.AdminExists(username)
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
return return
} else if err != nil { } else if err != nil {
@ -1265,7 +1268,7 @@ func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
folder, err := dataprovider.GetFolderByName(name) folder, err := dataprovider.GetFolderByName(name)
if err == nil { if err == nil {
renderFolderPage(w, r, folder, folderPageModeTemplate, "") renderFolderPage(w, r, folder, folderPageModeTemplate, "")
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
@ -1328,13 +1331,13 @@ func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
if err == nil { if err == nil {
user.SetEmptySecrets() user.SetEmptySecrets()
renderUserPage(w, r, &user, userPageModeTemplate, "") renderUserPage(w, r, &user, userPageModeTemplate, "")
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
} }
} else { } else {
user := dataprovider.User{Status: 1} user := dataprovider.User{BaseUser: sdk.BaseUser{Status: 1}}
renderUserPage(w, r, &user, userPageModeTemplate, "") renderUserPage(w, r, &user, userPageModeTemplate, "")
} }
} }
@ -1388,13 +1391,13 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
user.Password = "" user.Password = ""
user.SetEmptySecrets() user.SetEmptySecrets()
renderUserPage(w, r, &user, userPageModeAdd, "") renderUserPage(w, r, &user, userPageModeAdd, "")
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
} }
} else { } else {
user := dataprovider.User{Status: 1} user := dataprovider.User{BaseUser: sdk.BaseUser{Status: 1}}
renderUserPage(w, r, &user, userPageModeAdd, "") renderUserPage(w, r, &user, userPageModeAdd, "")
} }
} }
@ -1404,7 +1407,7 @@ func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
user, err := dataprovider.UserExists(username) user, err := dataprovider.UserExists(username)
if err == nil { if err == nil {
renderUserPage(w, r, &user, userPageModeUpdate, "") renderUserPage(w, r, &user, userPageModeUpdate, "")
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
@ -1434,7 +1437,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
username := getURLParam(r, "username") username := getURLParam(r, "username")
user, err := dataprovider.UserExists(username) user, err := dataprovider.UserExists(username)
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
return return
} else if err != nil { } else if err != nil {
@ -1527,7 +1530,7 @@ func handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
folder, err := dataprovider.GetFolderByName(name) folder, err := dataprovider.GetFolderByName(name)
if err == nil { if err == nil {
renderFolderPage(w, r, folder, folderPageModeUpdate, "") renderFolderPage(w, r, folder, folderPageModeUpdate, "")
} else if _, ok := err.(*utils.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
} else { } else {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
@ -1538,7 +1541,7 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name") name := getURLParam(r, "name")
folder, err := dataprovider.GetFolderByName(name) folder, err := dataprovider.GetFolderByName(name)
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
renderNotFoundPage(w, r, err) renderNotFoundPage(w, r, err)
return return
} else if err != nil { } else if err != nil {

View file

@ -16,7 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -123,10 +123,10 @@ func loadClientTemplates(templatesPath string) {
filepath.Join(templatesPath, templateClientDir, templateClientMessage), filepath.Join(templatesPath, templateClientDir, templateClientMessage),
} }
filesTmpl := utils.LoadTemplate(nil, filesPaths...) filesTmpl := util.LoadTemplate(nil, filesPaths...)
credentialsTmpl := utils.LoadTemplate(nil, credentialsPaths...) credentialsTmpl := util.LoadTemplate(nil, credentialsPaths...)
loginTmpl := utils.LoadTemplate(nil, loginPath...) loginTmpl := util.LoadTemplate(nil, loginPath...)
messageTmpl := utils.LoadTemplate(nil, messagePath...) messageTmpl := util.LoadTemplate(nil, messagePath...)
clientTemplates[templateClientFiles] = filesTmpl clientTemplates[templateClientFiles] = filesTmpl
clientTemplates[templateClientCredentials] = credentialsTmpl clientTemplates[templateClientCredentials] = credentialsTmpl
@ -291,7 +291,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
name := "/" name := "/"
if _, ok := r.URL.Query()["path"]; ok { if _, ok := r.URL.Query()["path"]; ok {
name = utils.CleanPath(r.URL.Query().Get("path")) name = util.CleanPath(r.URL.Query().Get("path"))
} }
files := r.URL.Query().Get("files") files := r.URL.Query().Get("files")
@ -334,7 +334,7 @@ func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
name := "/" name := "/"
if _, ok := r.URL.Query()["path"]; ok { if _, ok := r.URL.Query()["path"]; ok {
name = utils.CleanPath(r.URL.Query().Get("path")) name = util.CleanPath(r.URL.Query().Get("path"))
} }
contents, err := connection.ReadDir(name) contents, err := connection.ReadDir(name)
@ -354,7 +354,7 @@ func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
if info.Mode()&os.ModeSymlink != 0 { if info.Mode()&os.ModeSymlink != 0 {
res["size"] = "" res["size"] = ""
} else { } else {
res["size"] = utils.ByteCountIEC(info.Size()) res["size"] = util.ByteCountIEC(info.Size())
} }
} }
res["name"] = info.Name() res["name"] = info.Name()
@ -394,7 +394,7 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
name := "/" name := "/"
if _, ok := r.URL.Query()["path"]; ok { if _, ok := r.URL.Query()["path"]; ok {
name = utils.CleanPath(r.URL.Query().Get("path")) name = util.CleanPath(r.URL.Query().Get("path"))
} }
var info os.FileInfo var info os.FileInfo
if name == "/" { if name == "/" {

View file

@ -21,7 +21,7 @@ import (
"github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -914,7 +914,7 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
return errors.New("permissions mismatch") return errors.New("permissions mismatch")
} }
for _, p := range expected.Permissions { for _, p := range expected.Permissions {
if !utils.IsStringInSlice(p, actual.Permissions) { if !util.IsStringInSlice(p, actual.Permissions) {
return errors.New("permissions content mismatch") return errors.New("permissions content mismatch")
} }
} }
@ -922,7 +922,7 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
return errors.New("allow list mismatch") return errors.New("allow list mismatch")
} }
for _, v := range expected.Filters.AllowList { for _, v := range expected.Filters.AllowList {
if !utils.IsStringInSlice(v, actual.Filters.AllowList) { if !util.IsStringInSlice(v, actual.Filters.AllowList) {
return errors.New("allow list content mismatch") return errors.New("allow list content mismatch")
} }
} }
@ -968,7 +968,7 @@ func checkUser(expected *dataprovider.User, actual *dataprovider.User) error {
for dir, perms := range expected.Permissions { for dir, perms := range expected.Permissions {
if actualPerms, ok := actual.Permissions[dir]; ok { if actualPerms, ok := actual.Permissions[dir]; ok {
for _, v := range actualPerms { for _, v := range actualPerms {
if !utils.IsStringInSlice(v, perms) { if !util.IsStringInSlice(v, perms) {
return errors.New("permissions contents mismatch") return errors.New("permissions contents mismatch")
} }
} }
@ -1112,7 +1112,7 @@ func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
return errors.New("SFTPFs fingerprints mismatch") return errors.New("SFTPFs fingerprints mismatch")
} }
for _, value := range actual.SFTPConfig.Fingerprints { for _, value := range actual.SFTPConfig.Fingerprints {
if !utils.IsStringInSlice(value, expected.SFTPConfig.Fingerprints) { if !util.IsStringInSlice(value, expected.SFTPConfig.Fingerprints) {
return errors.New("SFTPFs fingerprints mismatch") return errors.New("SFTPFs fingerprints mismatch")
} }
} }
@ -1197,27 +1197,27 @@ func checkEncryptedSecret(expected, actual *kms.Secret) error {
func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovider.User) error { func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovider.User) error {
for _, IPMask := range expected.Filters.AllowedIP { for _, IPMask := range expected.Filters.AllowedIP {
if !utils.IsStringInSlice(IPMask, actual.Filters.AllowedIP) { if !util.IsStringInSlice(IPMask, actual.Filters.AllowedIP) {
return errors.New("allowed IP contents mismatch") return errors.New("allowed IP contents mismatch")
} }
} }
for _, IPMask := range expected.Filters.DeniedIP { for _, IPMask := range expected.Filters.DeniedIP {
if !utils.IsStringInSlice(IPMask, actual.Filters.DeniedIP) { if !util.IsStringInSlice(IPMask, actual.Filters.DeniedIP) {
return errors.New("denied IP contents mismatch") return errors.New("denied IP contents mismatch")
} }
} }
for _, method := range expected.Filters.DeniedLoginMethods { for _, method := range expected.Filters.DeniedLoginMethods {
if !utils.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) { if !util.IsStringInSlice(method, actual.Filters.DeniedLoginMethods) {
return errors.New("denied login methods contents mismatch") return errors.New("denied login methods contents mismatch")
} }
} }
for _, protocol := range expected.Filters.DeniedProtocols { for _, protocol := range expected.Filters.DeniedProtocols {
if !utils.IsStringInSlice(protocol, actual.Filters.DeniedProtocols) { if !util.IsStringInSlice(protocol, actual.Filters.DeniedProtocols) {
return errors.New("denied protocols contents mismatch") return errors.New("denied protocols contents mismatch")
} }
} }
for _, options := range expected.Filters.WebClient { for _, options := range expected.Filters.WebClient {
if !utils.IsStringInSlice(options, actual.Filters.WebClient) { if !util.IsStringInSlice(options, actual.Filters.WebClient) {
return errors.New("web client options contents mismatch") return errors.New("web client options contents mismatch")
} }
} }
@ -1269,7 +1269,7 @@ func checkFilterMatch(expected []string, actual []string) bool {
return false return false
} }
for _, e := range expected { for _, e := range expected {
if !utils.IsStringInSlice(strings.ToLower(e), actual) { if !util.IsStringInSlice(strings.ToLower(e), actual) {
return false return false
} }
} }

View file

@ -9,7 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
// SecretProvider defines the interface for a KMS secrets provider // SecretProvider defines the interface for a KMS secrets provider
@ -98,7 +98,7 @@ func NewPlainSecret(payload string) *Secret {
// GetSecretFromCompatString returns a secret from the previous format // GetSecretFromCompatString returns a secret from the previous format
func GetSecretFromCompatString(secret string) (*Secret, error) { func GetSecretFromCompatString(secret string) (*Secret, error) {
plain, err := utils.DecryptData(secret) plain, err := util.DecryptData(secret)
if err != nil { if err != nil {
return &Secret{}, errMalformedCiphertext return &Secret{}, errMalformedCiphertext
} }
@ -401,7 +401,7 @@ func (s *Secret) IsValidInput() bool {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
if !utils.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) { if !util.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) {
return false return false
} }
if s.provider.GetPayload() == "" { if s.provider.GetPayload() == "" {

66
logger/hclog_adapter.go Normal file
View file

@ -0,0 +1,66 @@
package logger
import (
"io"
"log"
"github.com/hashicorp/go-hclog"
"github.com/rs/zerolog"
)
type HCLogAdapter struct {
hclog.Logger
}
func (l *HCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
var ev *zerolog.Event
switch level {
case hclog.Info:
ev = logger.Info()
case hclog.Warn:
ev = logger.Warn()
case hclog.Error:
ev = logger.Error()
default:
ev = logger.Debug()
}
ev.Timestamp().Str("sender", l.Name())
addKeysAndValues(ev, args...)
ev.Msg(msg)
}
func (l *HCLogAdapter) Trace(msg string, args ...interface{}) {
l.Log(hclog.Debug, msg, args...)
}
func (l *HCLogAdapter) Debug(msg string, args ...interface{}) {
l.Log(hclog.Debug, msg, args...)
}
func (l *HCLogAdapter) Info(msg string, args ...interface{}) {
l.Log(hclog.Info, msg, args...)
}
func (l *HCLogAdapter) Warn(msg string, args ...interface{}) {
l.Log(hclog.Warn, msg, args...)
}
func (l *HCLogAdapter) Error(msg string, args ...interface{}) {
l.Log(hclog.Error, msg, args...)
}
func (l *HCLogAdapter) With(args ...interface{}) hclog.Logger {
return &HCLogAdapter{Logger: l.Logger.With(args...)}
}
func (l *HCLogAdapter) Named(name string) hclog.Logger {
return &HCLogAdapter{Logger: l.Logger.Named(name)}
}
func (l *HCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(&StdLoggerWrapper{Sender: l.Name()}, "", 0)
}
func (l *HCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return &StdLoggerWrapper{Sender: l.Name()}
}

View file

@ -63,7 +63,7 @@ type LeveledLogger struct {
Sender string Sender string
} }
func (l *LeveledLogger) addKeysAndValues(ev *zerolog.Event, keysAndValues ...interface{}) { func addKeysAndValues(ev *zerolog.Event, keysAndValues ...interface{}) {
kvLen := len(keysAndValues) kvLen := len(keysAndValues)
if kvLen%2 != 0 { if kvLen%2 != 0 {
extra := keysAndValues[kvLen-1] extra := keysAndValues[kvLen-1]
@ -71,7 +71,7 @@ func (l *LeveledLogger) addKeysAndValues(ev *zerolog.Event, keysAndValues ...int
} }
for i := 0; i < len(keysAndValues); i += 2 { for i := 0; i < len(keysAndValues); i += 2 {
key, val := keysAndValues[i], keysAndValues[i+1] key, val := keysAndValues[i], keysAndValues[i+1]
if keyStr, ok := key.(string); ok { if keyStr, ok := key.(string); ok && keyStr != "timestamp" {
ev.Str(keyStr, fmt.Sprintf("%v", val)) ev.Str(keyStr, fmt.Sprintf("%v", val))
} }
} }
@ -81,7 +81,7 @@ func (l *LeveledLogger) addKeysAndValues(ev *zerolog.Event, keysAndValues ...int
func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) { func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
ev := logger.Error() ev := logger.Error()
ev.Timestamp().Str("sender", l.Sender) ev.Timestamp().Str("sender", l.Sender)
l.addKeysAndValues(ev, keysAndValues...) addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg) ev.Msg(msg)
} }
@ -89,7 +89,7 @@ func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) {
func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) { func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
ev := logger.Info() ev := logger.Info()
ev.Timestamp().Str("sender", l.Sender) ev.Timestamp().Str("sender", l.Sender)
l.addKeysAndValues(ev, keysAndValues...) addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg) ev.Msg(msg)
} }
@ -97,7 +97,7 @@ func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) {
func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) { func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
ev := logger.Debug() ev := logger.Debug()
ev.Timestamp().Str("sender", l.Sender) ev.Timestamp().Str("sender", l.Sender)
l.addKeysAndValues(ev, keysAndValues...) addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg) ev.Msg(msg)
} }
@ -105,7 +105,7 @@ func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) {
func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) { func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) {
ev := logger.Warn() ev := logger.Warn()
ev.Timestamp().Str("sender", l.Sender) ev.Timestamp().Str("sender", l.Sender)
l.addKeysAndValues(ev, keysAndValues...) addKeysAndValues(ev, keysAndValues...)
ev.Msg(msg) ev.Msg(msg)
} }

View file

@ -8,7 +8,7 @@ import (
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
) )
// StructuredLogger defines a simple wrapper around zerolog logger. // StructuredLogger defines a simple wrapper around zerolog logger.
@ -56,7 +56,7 @@ func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry {
// Write logs a new entry at the end of the HTTP request // Write logs a new entry at the end of the HTTP request
func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
metrics.HTTPRequestServed(status) metric.HTTPRequestServed(status)
l.Logger.Info(). l.Logger.Info().
Timestamp(). Timestamp().
Str("sender", "httpd"). Str("sender", "httpd").

View file

@ -1,7 +1,7 @@
// +build !nometrics // +build !nometrics
// Package metrics provides Prometheus metrics support // Package metrics provides Prometheus metrics support
package metrics package metric
import ( import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"

View file

@ -1,6 +1,6 @@
// +build nometrics // +build nometrics
package metrics package metric
import ( import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
NFPM_VERSION=2.5.1 NFPM_VERSION=2.6.0
NFPM_ARCH=${NFPM_ARCH:-amd64} NFPM_ARCH=${NFPM_ARCH:-amd64}
if [ -z ${SFTPGO_VERSION} ] if [ -z ${SFTPGO_VERSION} ]
then then

202
sdk/filesystem.go Normal file
View file

@ -0,0 +1,202 @@
package sdk
import "github.com/drakkan/sftpgo/v2/kms"
// FilesystemProvider defines the supported storage filesystems
type FilesystemProvider int
// supported values for FilesystemProvider
const (
LocalFilesystemProvider FilesystemProvider = iota // Local
S3FilesystemProvider // AWS S3 compatible
GCSFilesystemProvider // Google Cloud Storage
AzureBlobFilesystemProvider // Azure Blob Storage
CryptedFilesystemProvider // Local encrypted
SFTPFilesystemProvider // SFTP
)
// GetProviderByName returns the FilesystemProvider matching a given name
// to provide backwards compatibility, numeric strings are accepted as well
func GetProviderByName(name string) FilesystemProvider {
switch name {
case "0", "osfs":
return LocalFilesystemProvider
case "1", "s3fs":
return S3FilesystemProvider
case "2", "gcsfs":
return GCSFilesystemProvider
case "3", "azblobfs":
return AzureBlobFilesystemProvider
case "4", "cryptfs":
return CryptedFilesystemProvider
case "5", "sftpfs":
return SFTPFilesystemProvider
}
// TODO think about returning an error value instead of silently defaulting to LocalFilesystemProvider
return LocalFilesystemProvider
}
// Name returns the Provider's unique name
func (p FilesystemProvider) Name() string {
switch p {
case LocalFilesystemProvider:
return "osfs"
case S3FilesystemProvider:
return "s3fs"
case GCSFilesystemProvider:
return "gcsfs"
case AzureBlobFilesystemProvider:
return "azblobfs"
case CryptedFilesystemProvider:
return "cryptfs"
case SFTPFilesystemProvider:
return "sftpfs"
}
return "" // let's not claim to be
}
// ShortInfo returns a human readable, short description for the given FilesystemProvider
func (p FilesystemProvider) ShortInfo() string {
switch p {
case LocalFilesystemProvider:
return "Local"
case S3FilesystemProvider:
return "AWS S3 (Compatible)"
case GCSFilesystemProvider:
return "Google Cloud Storage"
case AzureBlobFilesystemProvider:
return "Azure Blob Storage"
case CryptedFilesystemProvider:
return "Local encrypted"
case SFTPFilesystemProvider:
return "SFTP"
}
return ""
}
// ListProviders returns a list of available FilesystemProviders.
func ListProviders() []FilesystemProvider {
return []FilesystemProvider{
LocalFilesystemProvider, S3FilesystemProvider,
GCSFilesystemProvider, AzureBlobFilesystemProvider,
CryptedFilesystemProvider, SFTPFilesystemProvider,
}
}
// S3FsConfig defines the configuration for S3 based filesystem
type S3FsConfig struct {
Bucket string `json:"bucket,omitempty"`
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTP user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
Region string `json:"region,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret *kms.Secret `json:"access_secret,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
// The minimum allowed value is 5.
// Please note that if the upload bandwidth between the SFTP client and SFTPGo is greater than
// the upload bandwidth between SFTPGo and S3 then the SFTP client have to wait for the upload
// of the last parts to S3 after it ends the file upload to SFTPGo, and it may time out.
// Keep this in mind if you customize these parameters.
UploadPartSize int64 `json:"upload_part_size,omitempty"`
// How many parts are uploaded in parallel
UploadConcurrency int `json:"upload_concurrency,omitempty"`
}
// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
type GCSFsConfig struct {
Bucket string `json:"bucket,omitempty"`
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTP user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
CredentialFile string `json:"-"`
Credentials *kms.Secret `json:"credentials,omitempty"`
// 0 explicit, 1 automatic
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
}
// AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
type AzBlobFsConfig struct {
Container string `json:"container,omitempty"`
// Storage Account Name, leave blank to use SAS URL
AccountName string `json:"account_name,omitempty"`
// Storage Account Key leave blank to use SAS URL.
// The access key is stored encrypted based on the kms configuration
AccountKey *kms.Secret `json:"account_key,omitempty"`
// Optional endpoint. Default is "blob.core.windows.net".
// If you use the emulator the endpoint must include the protocol,
// for example "http://127.0.0.1:10000"
Endpoint string `json:"endpoint,omitempty"`
// Shared access signature URL, leave blank if using account/key
SASURL *kms.Secret `json:"sas_url,omitempty"`
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTPGo user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
// The buffer size (in MB) to use for multipart uploads.
// If this value is set to zero, the default value (1MB) for the Azure SDK will be used.
// Please note that if the upload bandwidth between the SFTPGo client and SFTPGo server is
// greater than the upload bandwidth between SFTPGo and Azure then the SFTP client have
// to wait for the upload of the last parts to Azure after it ends the file upload to SFTPGo,
// and it may time out.
// Keep this in mind if you customize these parameters.
UploadPartSize int64 `json:"upload_part_size,omitempty"`
// How many parts are uploaded in parallel
UploadConcurrency int `json:"upload_concurrency,omitempty"`
// Set to true if you use an Azure emulator such as Azurite
UseEmulator bool `json:"use_emulator,omitempty"`
// Blob Access Tier
AccessTier string `json:"access_tier,omitempty"`
}
// CryptFsConfig defines the configuration to store local files as encrypted
type CryptFsConfig struct {
Passphrase *kms.Secret `json:"passphrase,omitempty"`
}
// SFTPFsConfig defines the configuration for SFTP based filesystem
type SFTPFsConfig struct {
Endpoint string `json:"endpoint,omitempty"`
Username string `json:"username,omitempty"`
Password *kms.Secret `json:"password,omitempty"`
PrivateKey *kms.Secret `json:"private_key,omitempty"`
Fingerprints []string `json:"fingerprints,omitempty"`
// Prefix is the path prefix to strip from SFTP resource paths.
Prefix string `json:"prefix,omitempty"`
// Concurrent reads are safe to use and disabling them will degrade performance.
// Some servers automatically delete files once they are downloaded.
// Using concurrent reads is problematic with such servers.
DisableCouncurrentReads bool `json:"disable_concurrent_reads,omitempty"`
// The buffer size (in MB) to use for transfers.
// Buffering could improve performance for high latency networks.
// With buffering enabled upload resume is not supported and a file
// cannot be opened for both reading and writing at the same time
// 0 means disabled.
BufferSize int64 `json:"buffer_size,omitempty"`
}
// Filesystem defines filesystem details
type Filesystem struct {
Provider FilesystemProvider `json:"provider"`
S3Config S3FsConfig `json:"s3config,omitempty"`
GCSConfig GCSFsConfig `json:"gcsconfig,omitempty"`
AzBlobConfig AzBlobFsConfig `json:"azblobconfig,omitempty"`
CryptConfig CryptFsConfig `json:"cryptconfig,omitempty"`
SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"`
}

35
sdk/folder.go Normal file
View file

@ -0,0 +1,35 @@
package sdk
// BaseVirtualFolder defines the path for the virtual folder and the used quota limits.
// The same folder can be shared among multiple users and each user can have different
// quota limits or a different virtual path.
type BaseVirtualFolder struct {
ID int64 `json:"id"`
Name string `json:"name"`
MappedPath string `json:"mapped_path,omitempty"`
Description string `json:"description,omitempty"`
UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files
UsedQuotaFiles int `json:"used_quota_files"`
// Last quota update as unix timestamp in milliseconds
LastQuotaUpdate int64 `json:"last_quota_update"`
// list of usernames associated with this virtual folder
Users []string `json:"users,omitempty"`
// Filesystem configuration details
FsConfig Filesystem `json:"filesystem"`
}
// VirtualFolder defines a mapping between an SFTPGo exposed virtual path and a
// filesystem path outside the user home directory.
// The specified paths must be absolute and the virtual path cannot be "/",
// it must be a sub directory. The parent directory for the specified virtual
// path must exist. SFTPGo will, by default, try to automatically create any missing
// parent directory for the configured virtual folders at user login.
type VirtualFolder struct {
BaseVirtualFolder
VirtualPath string `json:"virtual_path"`
// Maximum size allowed as bytes. 0 means unlimited, -1 included in user quota
QuotaSize int64 `json:"quota_size"`
// Maximum number of files allowed. 0 means unlimited, -1 included in user quota
QuotaFiles int `json:"quota_files"`
}

5
sdk/plugin/mkproto.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
protoc notifier/proto/notifier.proto --go_out=plugins=grpc:../.. --go_out=../../..

135
sdk/plugin/notifier.go Normal file
View file

@ -0,0 +1,135 @@
package plugin
import (
"crypto/sha256"
"fmt"
"os/exec"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/util"
)
// NotifierConfig defines configuration parameters for notifiers plugins
type NotifierConfig struct {
FsEvents []string `json:"fs_events" mapstructure:"fs_events"`
UserEvents []string `json:"user_events" mapstructure:"user_events"`
}
func (c *NotifierConfig) hasActions() bool {
if len(c.FsEvents) > 0 {
return true
}
if len(c.UserEvents) > 0 {
return true
}
return false
}
type notifierPlugin struct {
config Config
notifier notifier.Notifier
client *plugin.Client
}
func newNotifierPlugin(config Config) (*notifierPlugin, error) {
p := &notifierPlugin{
config: config,
}
if err := p.initialize(); err != nil {
logger.Warn(logSender, "", "unable to create notifier plugin: %v, config %v", err, config)
return nil, err
}
return p, nil
}
func (p *notifierPlugin) exited() bool {
return p.client.Exited()
}
func (p *notifierPlugin) cleanup() {
p.client.Kill()
}
func (p *notifierPlugin) initialize() error {
killProcess(p.config.Cmd)
logger.Debug(logSender, "", "create new plugin %v", p.config.Cmd)
if !p.config.NotifierOptions.hasActions() {
return fmt.Errorf("no actions defined for the notifier plugin %v", p.config.Cmd)
}
var secureConfig *plugin.SecureConfig
if p.config.SHA256Sum != "" {
secureConfig.Checksum = []byte(p.config.SHA256Sum)
secureConfig.Hash = sha256.New()
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: notifier.Handshake,
Plugins: notifier.PluginMap,
Cmd: exec.Command(p.config.Cmd, p.config.Args...),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolGRPC,
},
AutoMTLS: p.config.AutoMTLS,
SecureConfig: secureConfig,
Managed: false,
Logger: &logger.HCLogAdapter{
Logger: hclog.New(&hclog.LoggerOptions{
Name: fmt.Sprintf("%v.%v", logSender, notifier.PluginName),
Level: pluginsLogLevel,
DisableTime: true,
}),
},
})
rpcClient, err := client.Client()
if err != nil {
logger.Debug(logSender, "", "unable to get rpc client for plugin %v: %v", p.config.Cmd, err)
return err
}
raw, err := rpcClient.Dispense(notifier.PluginName)
if err != nil {
logger.Debug(logSender, "", "unable to get plugin %v from rpc client for plugin %v: %v",
notifier.PluginName, p.config.Cmd, err)
return err
}
p.client = client
p.notifier = raw.(notifier.Notifier)
return nil
}
func (p *notifierPlugin) notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, errAction error) {
if !util.IsStringInSlice(action, p.config.NotifierOptions.FsEvents) {
return
}
go func() {
status := 1
if errAction != nil {
status = 0
}
if err := p.notifier.NotifyFsEvent(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, status); err != nil {
logger.Warn(logSender, "", "unable to send fs action notification to plugin %v: %v", p.config.Cmd, err)
}
}()
}
func (p *notifierPlugin) notifyUserAction(action string, user Renderer) {
if !util.IsStringInSlice(action, p.config.NotifierOptions.UserEvents) {
return
}
go func() {
userAsJSON, err := user.RenderAsJSON(action != "delete")
if err != nil {
logger.Warn(logSender, "", "unable to render user as json for action %v: %v", action, err)
return
}
if err := p.notifier.NotifyUserEvent(action, userAsJSON); err != nil {
logger.Warn(logSender, "", "unable to send user action notification to plugin %v: %v", p.config.Cmd, err)
}
}()
}

View file

@ -0,0 +1,72 @@
package notifier
import (
"context"
"time"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier/proto"
)
const (
rpcTimeout = 20 * time.Second
)
// GRPCClient is an implementation of Notifier interface that talks over RPC.
type GRPCClient struct {
client proto.NotifierClient
}
// NotifyFsEvent implements the Notifier interface
func (c *GRPCClient) NotifyFsEvent(action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, status int) error {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
_, err := c.client.SendFsEvent(ctx, &proto.FsEvent{
Timestamp: timestamppb.New(time.Now()),
Action: action,
Username: username,
FsPath: fsPath,
FsTargetPath: fsTargetPath,
SshCmd: sshCmd,
FileSize: fileSize,
Protocol: protocol,
Status: int32(status),
})
return err
}
// NotifyUserEvent implements the Notifier interface
func (c *GRPCClient) NotifyUserEvent(action string, user []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout)
defer cancel()
_, err := c.client.SendUserEvent(ctx, &proto.UserEvent{
Timestamp: timestamppb.New(time.Now()),
Action: action,
User: user,
})
return err
}
// GRPCServer defines the gRPC server that GRPCClient talks to.
type GRPCServer struct {
Impl Notifier
}
// SendFsEvent implements the serve side fs notify method
func (s *GRPCServer) SendFsEvent(ctx context.Context, req *proto.FsEvent) (*emptypb.Empty, error) {
err := s.Impl.NotifyFsEvent(req.Action, req.Username, req.FsPath, req.FsTargetPath, req.SshCmd,
req.Protocol, req.FileSize, int(req.Status))
return &emptypb.Empty{}, err
}
// SendUserEvent implements the serve side user notify method
func (s *GRPCServer) SendUserEvent(ctx context.Context, req *proto.UserEvent) (*emptypb.Empty, error) {
err := s.Impl.NotifyUserEvent(req.Action, req.User)
return &emptypb.Empty{}, err
}

View file

@ -0,0 +1,57 @@
// Package notifier defines the implementation for event notifier plugin.
// Notifier plugins allow to receive filesystem events such as file uploads,
// downloads etc. and user events such as add, update, delete.
package notifier
import (
"context"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier/proto"
)
const (
// PluginName defines the name for a notifier plugin
PluginName = "notifier"
)
// Handshake is a common handshake that is shared by plugin and host.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "SFTPGO_NOTIFIER_PLUGIN",
MagicCookieValue: "c499b98b-cd59-4df2-92b3-6268817f4d80",
}
// PluginMap is the map of plugins we can dispense.
var PluginMap = map[string]plugin.Plugin{
PluginName: &Plugin{},
}
// Notifier defines the interface for notifiers plugins
type Notifier interface {
NotifyFsEvent(action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, status int) error
NotifyUserEvent(action string, user []byte) error
}
// Plugin defines the implementation to serve/connect to a notifier plugin
type Plugin struct {
plugin.Plugin
Impl Notifier
}
// GRPCServer defines the GRPC server implementation for this plugin
func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
proto.RegisterNotifierServer(s, &GRPCServer{
Impl: p.Impl,
})
return nil
}
// GRPCClient defines the GRPC client implementation for this plugin
func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCClient{
client: proto.NewNotifierClient(c),
}, nil
}

View file

@ -0,0 +1,448 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.17.3
// source: notifier/proto/notifier.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type FsEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"`
FsPath string `protobuf:"bytes,4,opt,name=fs_path,json=fsPath,proto3" json:"fs_path,omitempty"`
FsTargetPath string `protobuf:"bytes,5,opt,name=fs_target_path,json=fsTargetPath,proto3" json:"fs_target_path,omitempty"`
SshCmd string `protobuf:"bytes,6,opt,name=ssh_cmd,json=sshCmd,proto3" json:"ssh_cmd,omitempty"`
FileSize int64 `protobuf:"varint,7,opt,name=file_size,json=fileSize,proto3" json:"file_size,omitempty"`
Protocol string `protobuf:"bytes,8,opt,name=protocol,proto3" json:"protocol,omitempty"`
Status int32 `protobuf:"varint,9,opt,name=status,proto3" json:"status,omitempty"`
}
func (x *FsEvent) Reset() {
*x = FsEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_notifier_proto_notifier_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *FsEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FsEvent) ProtoMessage() {}
func (x *FsEvent) ProtoReflect() protoreflect.Message {
mi := &file_notifier_proto_notifier_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FsEvent.ProtoReflect.Descriptor instead.
func (*FsEvent) Descriptor() ([]byte, []int) {
return file_notifier_proto_notifier_proto_rawDescGZIP(), []int{0}
}
func (x *FsEvent) GetTimestamp() *timestamppb.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
func (x *FsEvent) GetAction() string {
if x != nil {
return x.Action
}
return ""
}
func (x *FsEvent) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *FsEvent) GetFsPath() string {
if x != nil {
return x.FsPath
}
return ""
}
func (x *FsEvent) GetFsTargetPath() string {
if x != nil {
return x.FsTargetPath
}
return ""
}
func (x *FsEvent) GetSshCmd() string {
if x != nil {
return x.SshCmd
}
return ""
}
func (x *FsEvent) GetFileSize() int64 {
if x != nil {
return x.FileSize
}
return 0
}
func (x *FsEvent) GetProtocol() string {
if x != nil {
return x.Protocol
}
return ""
}
func (x *FsEvent) GetStatus() int32 {
if x != nil {
return x.Status
}
return 0
}
type UserEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"`
User []byte `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` // SFTPGo user json serialized
}
func (x *UserEvent) Reset() {
*x = UserEvent{}
if protoimpl.UnsafeEnabled {
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UserEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserEvent) ProtoMessage() {}
func (x *UserEvent) ProtoReflect() protoreflect.Message {
mi := &file_notifier_proto_notifier_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserEvent.ProtoReflect.Descriptor instead.
func (*UserEvent) Descriptor() ([]byte, []int) {
return file_notifier_proto_notifier_proto_rawDescGZIP(), []int{1}
}
func (x *UserEvent) GetTimestamp() *timestamppb.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
func (x *UserEvent) GetAction() string {
if x != nil {
return x.Action
}
return ""
}
func (x *UserEvent) GetUser() []byte {
if x != nil {
return x.User
}
return nil
}
var File_notifier_proto_notifier_proto protoreflect.FileDescriptor
var file_notifier_proto_notifier_proto_rawDesc = []byte{
0x0a, 0x1d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x02, 0x0a, 0x07, 0x46, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17,
0x0a, 0x07, 0x66, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x66, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x73, 0x5f, 0x74, 0x61,
0x72, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0c, 0x66, 0x73, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x17, 0x0a,
0x07, 0x73, 0x73, 0x68, 0x5f, 0x63, 0x6d, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x73, 0x73, 0x68, 0x43, 0x6d, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73,
0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53,
0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12,
0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x71, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x16,
0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0x7c, 0x0a, 0x08, 0x4e, 0x6f,
0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x46, 0x73,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x73,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a,
0x0d, 0x53, 0x65, 0x6e, 0x64, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x10,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x1b, 0x5a, 0x19, 0x73, 0x64, 0x6b, 0x2f,
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_notifier_proto_notifier_proto_rawDescOnce sync.Once
file_notifier_proto_notifier_proto_rawDescData = file_notifier_proto_notifier_proto_rawDesc
)
func file_notifier_proto_notifier_proto_rawDescGZIP() []byte {
file_notifier_proto_notifier_proto_rawDescOnce.Do(func() {
file_notifier_proto_notifier_proto_rawDescData = protoimpl.X.CompressGZIP(file_notifier_proto_notifier_proto_rawDescData)
})
return file_notifier_proto_notifier_proto_rawDescData
}
var file_notifier_proto_notifier_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_notifier_proto_notifier_proto_goTypes = []interface{}{
(*FsEvent)(nil), // 0: proto.FsEvent
(*UserEvent)(nil), // 1: proto.UserEvent
(*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
}
var file_notifier_proto_notifier_proto_depIdxs = []int32{
2, // 0: proto.FsEvent.timestamp:type_name -> google.protobuf.Timestamp
2, // 1: proto.UserEvent.timestamp:type_name -> google.protobuf.Timestamp
0, // 2: proto.Notifier.SendFsEvent:input_type -> proto.FsEvent
1, // 3: proto.Notifier.SendUserEvent:input_type -> proto.UserEvent
3, // 4: proto.Notifier.SendFsEvent:output_type -> google.protobuf.Empty
3, // 5: proto.Notifier.SendUserEvent:output_type -> google.protobuf.Empty
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_notifier_proto_notifier_proto_init() }
func file_notifier_proto_notifier_proto_init() {
if File_notifier_proto_notifier_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_notifier_proto_notifier_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FsEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_notifier_proto_notifier_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserEvent); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_notifier_proto_notifier_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_notifier_proto_notifier_proto_goTypes,
DependencyIndexes: file_notifier_proto_notifier_proto_depIdxs,
MessageInfos: file_notifier_proto_notifier_proto_msgTypes,
}.Build()
File_notifier_proto_notifier_proto = out.File
file_notifier_proto_notifier_proto_rawDesc = nil
file_notifier_proto_notifier_proto_goTypes = nil
file_notifier_proto_notifier_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// NotifierClient is the client API for Notifier service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type NotifierClient interface {
SendFsEvent(ctx context.Context, in *FsEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
SendUserEvent(ctx context.Context, in *UserEvent, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type notifierClient struct {
cc grpc.ClientConnInterface
}
func NewNotifierClient(cc grpc.ClientConnInterface) NotifierClient {
return &notifierClient{cc}
}
func (c *notifierClient) SendFsEvent(ctx context.Context, in *FsEvent, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/proto.Notifier/SendFsEvent", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierClient) SendUserEvent(ctx context.Context, in *UserEvent, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/proto.Notifier/SendUserEvent", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// NotifierServer is the server API for Notifier service.
type NotifierServer interface {
SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error)
SendUserEvent(context.Context, *UserEvent) (*emptypb.Empty, error)
}
// UnimplementedNotifierServer can be embedded to have forward compatible implementations.
type UnimplementedNotifierServer struct {
}
func (*UnimplementedNotifierServer) SendFsEvent(context.Context, *FsEvent) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendFsEvent not implemented")
}
func (*UnimplementedNotifierServer) SendUserEvent(context.Context, *UserEvent) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendUserEvent not implemented")
}
func RegisterNotifierServer(s *grpc.Server, srv NotifierServer) {
s.RegisterService(&_Notifier_serviceDesc, srv)
}
func _Notifier_SendFsEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FsEvent)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServer).SendFsEvent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.Notifier/SendFsEvent",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServer).SendFsEvent(ctx, req.(*FsEvent))
}
return interceptor(ctx, in, info, handler)
}
func _Notifier_SendUserEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UserEvent)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServer).SendUserEvent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/proto.Notifier/SendUserEvent",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServer).SendUserEvent(ctx, req.(*UserEvent))
}
return interceptor(ctx, in, info, handler)
}
var _Notifier_serviceDesc = grpc.ServiceDesc{
ServiceName: "proto.Notifier",
HandlerType: (*NotifierServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SendFsEvent",
Handler: _Notifier_SendFsEvent_Handler,
},
{
MethodName: "SendUserEvent",
Handler: _Notifier_SendUserEvent_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "notifier/proto/notifier.proto",
}

View file

@ -0,0 +1,30 @@
syntax = "proto3";
package proto;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option go_package = "sdk/plugin/notifier/proto";
message FsEvent {
google.protobuf.Timestamp timestamp = 1;
string action = 2;
string username = 3;
string fs_path = 4;
string fs_target_path = 5;
string ssh_cmd = 6;
int64 file_size = 7;
string protocol = 8;
int32 status = 9;
}
message UserEvent {
google.protobuf.Timestamp timestamp = 1;
string action = 2;
bytes user = 3; // SFTPGo user json serialized
}
service Notifier {
rpc SendFsEvent(FsEvent) returns (google.protobuf.Empty);
rpc SendUserEvent(UserEvent) returns (google.protobuf.Empty);
}

166
sdk/plugin/plugin.go Normal file
View file

@ -0,0 +1,166 @@
// Package plugin provides support for the SFTPGo plugin system
package plugin
import (
"fmt"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin/notifier"
)
const (
logSender = "plugins"
)
var (
// Handler defines the plugins manager
Handler Manager
pluginsLogLevel = hclog.Debug
)
// Renderer defines the interface for generic objects rendering
type Renderer interface {
RenderAsJSON(reload bool) ([]byte, error)
}
// Config defines a plugin configuration
type Config struct {
// Plugin type
Type string `json:"type" mapstructure:"type"`
// NotifierOptions defines additional options for notifiers plugins
NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"`
// Path to the plugin executable
Cmd string `json:"cmd" mapstructure:"cmd"`
// Args to pass to the plugin executable
Args []string `json:"args" mapstructure:"args"`
// SHA256 checksum for the plugin executable.
// If not empty it will be used to verify the integrity of the executable
SHA256Sum string `json:"sha256sum" mapstructure:"sha256sum"`
// If enabled the client and the server automatically negotiate mTLS for
// transport authentication. This ensures that only the original client will
// be allowed to connect to the server, and all other connections will be
// rejected. The client will also refuse to connect to any server that isn't
// the original instance started by the client.
AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"`
}
// Manager handles enabled plugins
type Manager struct {
// List of configured plugins
Configs []Config `json:"plugins" mapstructure:"plugins"`
mu sync.RWMutex
notifiers []*notifierPlugin
}
// Initialize initializes the configured plugins
func Initialize(configs []Config, logVerbose bool) error {
Handler = Manager{
Configs: configs,
}
if logVerbose {
pluginsLogLevel = hclog.Debug
} else {
pluginsLogLevel = hclog.Info
}
for _, config := range configs {
switch config.Type {
case notifier.PluginName:
plugin, err := newNotifierPlugin(config)
if err != nil {
return err
}
Handler.notifiers = append(Handler.notifiers, plugin)
default:
return fmt.Errorf("unsupported plugin type: %v", config.Type)
}
}
return nil
}
// NotifyFsEvent sends the fs event notifications using any defined notifier plugins
func (m *Manager) NotifyFsEvent(action, username, fsPath, fsTargetPath, sshCmd, protocol string, fileSize int64, err error) {
m.mu.RLock()
var crashedIdxs []int
for idx, n := range m.notifiers {
if n.exited() {
crashedIdxs = append(crashedIdxs, idx)
} else {
n.notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err)
}
}
m.mu.RUnlock()
if len(crashedIdxs) > 0 {
m.restartCrashedNotifiers(crashedIdxs)
m.mu.RLock()
defer m.mu.RUnlock()
for idx := range crashedIdxs {
if !m.notifiers[idx].exited() {
m.notifiers[idx].notifyFsAction(action, username, fsPath, fsTargetPath, sshCmd, protocol, fileSize, err)
}
}
}
}
// NotifyUserEvent sends the user event notifications using any defined notifier plugins
func (m *Manager) NotifyUserEvent(action string, user Renderer) {
m.mu.RLock()
var crashedIdxs []int
for idx, n := range m.notifiers {
if n.exited() {
crashedIdxs = append(crashedIdxs, idx)
} else {
n.notifyUserAction(action, user)
}
}
m.mu.RUnlock()
if len(crashedIdxs) > 0 {
m.restartCrashedNotifiers(crashedIdxs)
m.mu.RLock()
defer m.mu.RUnlock()
for idx := range crashedIdxs {
if !m.notifiers[idx].exited() {
m.notifiers[idx].notifyUserAction(action, user)
}
}
}
}
func (m *Manager) restartCrashedNotifiers(crashedIdxs []int) {
for _, idx := range crashedIdxs {
m.mu.Lock()
defer m.mu.Unlock()
if m.notifiers[idx].exited() {
logger.Info(logSender, "", "try to restart crashed plugin %v", m.Configs[idx].Cmd)
plugin, err := newNotifierPlugin(m.Configs[idx])
if err == nil {
m.notifiers[idx] = plugin
} else {
logger.Warn(logSender, "", "plugin %v crashed and restart failed: %v", m.Configs[idx].Cmd, err)
}
}
}
}
// Cleanup releases all the active plugins
func (m *Manager) Cleanup() {
for _, n := range m.notifiers {
logger.Debug(logSender, "", "cleanup plugin %v", n.config.Cmd)
n.cleanup()
}
}

25
sdk/plugin/util.go Normal file
View file

@ -0,0 +1,25 @@
package plugin
import (
"github.com/shirou/gopsutil/v3/process"
"github.com/drakkan/sftpgo/v2/logger"
)
func killProcess(processPath string) {
procs, err := process.Processes()
if err != nil {
return
}
for _, p := range procs {
cmdLine, err := p.Exe()
if err == nil {
if cmdLine == processPath {
err = p.Kill()
logger.Debug(logSender, "", "killed process %v, pid %v, err %v", cmdLine, p.Pid, err)
return
}
}
}
logger.Debug(logSender, "", "no match for plugin process %v", processPath)
}

2
sdk/sdk.go Normal file
View file

@ -0,0 +1,2 @@
// Package sdk provides SFTPGo data structures primarily intended for use within plugins
package sdk

181
sdk/user.go Normal file
View file

@ -0,0 +1,181 @@
package sdk
import (
"strings"
"github.com/drakkan/sftpgo/v2/util"
)
// Web Client restrictions
const (
WebClientPubKeyChangeDisabled = "publickey-change-disabled"
)
var (
// WebClientOptions defines the available options for the web client interface
WebClientOptions = []string{WebClientPubKeyChangeDisabled}
)
// TLSUsername defines the TLS certificate attribute to use as username
type TLSUsername string
// Supported certificate attributes to use as username
const (
TLSUsernameNone TLSUsername = "None"
TLSUsernameCN TLSUsername = "CommonName"
)
// DirectoryPermissions defines permissions for a directory virtual path
type DirectoryPermissions struct {
Path string
Permissions []string
}
// HasPerm returns true if the directory has the specified permissions
func (d *DirectoryPermissions) HasPerm(perm string) bool {
return util.IsStringInSlice(perm, d.Permissions)
}
// PatternsFilter defines filters based on shell like patterns.
// These restrictions do not apply to files listing for performance reasons, so
// a denied file cannot be downloaded/overwritten/renamed but will still be
// in the list of files.
// System commands such as Git and rsync interacts with the filesystem directly
// and they are not aware about these restrictions so they are not allowed
// inside paths with extensions filters
type PatternsFilter struct {
// Virtual path, if no other specific filter is defined, the filter apply for
// sub directories too.
// For example if filters are defined for the paths "/" and "/sub" then the
// filters for "/" are applied for any file outside the "/sub" directory
Path string `json:"path"`
// files with these, case insensitive, patterns are allowed.
// Denied file patterns are evaluated before the allowed ones
AllowedPatterns []string `json:"allowed_patterns,omitempty"`
// files with these, case insensitive, patterns are not allowed.
// Denied file patterns are evaluated before the allowed ones
DeniedPatterns []string `json:"denied_patterns,omitempty"`
}
// GetCommaSeparatedPatterns returns the first non empty patterns list comma separated
func (p *PatternsFilter) GetCommaSeparatedPatterns() string {
if len(p.DeniedPatterns) > 0 {
return strings.Join(p.DeniedPatterns, ",")
}
return strings.Join(p.AllowedPatterns, ",")
}
// IsDenied returns true if the patterns has one or more denied patterns
func (p *PatternsFilter) IsDenied() bool {
return len(p.DeniedPatterns) > 0
}
// IsAllowed returns true if the patterns has one or more allowed patterns
func (p *PatternsFilter) IsAllowed() bool {
return len(p.AllowedPatterns) > 0
}
// HooksFilter defines user specific overrides for global hooks
type HooksFilter struct {
ExternalAuthDisabled bool `json:"external_auth_disabled"`
PreLoginDisabled bool `json:"pre_login_disabled"`
CheckPasswordDisabled bool `json:"check_password_disabled"`
}
// UserFilters defines additional restrictions for a user
// TODO: rename to UserOptions in v3
type UserFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
// for example "192.0.2.0/24" or "2001:db8::/32"
AllowedIP []string `json:"allowed_ip,omitempty"`
// clients connecting from these IP/Mask are not allowed.
// Denied rules will be evaluated before allowed ones
DeniedIP []string `json:"denied_ip,omitempty"`
// these login methods are not allowed.
// If null or empty any available login method is allowed
DeniedLoginMethods []string `json:"denied_login_methods,omitempty"`
// these protocols are not allowed.
// If null or empty any available protocol is allowed
DeniedProtocols []string `json:"denied_protocols,omitempty"`
// filter based on shell patterns.
// Please note that these restrictions can be easily bypassed.
FilePatterns []PatternsFilter `json:"file_patterns,omitempty"`
// max size allowed for a single upload, 0 means unlimited
MaxUploadFileSize int64 `json:"max_upload_file_size,omitempty"`
// TLS certificate attribute to use as username.
// For FTP clients it must match the name provided using the
// "USER" command
TLSUsername TLSUsername `json:"tls_username,omitempty"`
// user specific hook overrides
Hooks HooksFilter `json:"hooks,omitempty"`
// Disable checks for existence and automatic creation of home directory
// and virtual folders.
// SFTPGo requires that the user's home directory, virtual folder root,
// and intermediate paths to virtual folders exist to work properly.
// If you already know that the required directories exist, disabling
// these checks will speed up login.
// You could, for example, disable these checks after the first login
DisableFsChecks bool `json:"disable_fs_checks,omitempty"`
// WebClient related configuration options
WebClient []string `json:"web_client,omitempty"`
}
type BaseUser struct {
// Data provider unique identifier
ID int64 `json:"id"`
// 1 enabled, 0 disabled (login is not allowed)
Status int `json:"status"`
// Username
Username string `json:"username"`
// Account expiration date as unix timestamp in milliseconds. An expired account cannot login.
// 0 means no expiration
ExpirationDate int64 `json:"expiration_date"`
// Password used for password authentication.
// For users created using SFTPGo REST API the password is be stored using bcrypt or argon2id hashing algo.
// Checking passwords stored with pbkdf2, md5crypt and sha512crypt is supported too.
Password string `json:"password,omitempty"`
// PublicKeys used for public key authentication. At least one between password and a public key is mandatory
PublicKeys []string `json:"public_keys,omitempty"`
// The user cannot upload or download files outside this directory. Must be an absolute path
HomeDir string `json:"home_dir"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system UID
UID int `json:"uid"`
// If sftpgo runs as root system user then the created files and directories will be assigned to this system GID
GID int `json:"gid"`
// Maximum concurrent sessions. 0 means unlimited
MaxSessions int `json:"max_sessions"`
// Maximum size allowed as bytes. 0 means unlimited
QuotaSize int64 `json:"quota_size"`
// Maximum number of files allowed. 0 means unlimited
QuotaFiles int `json:"quota_files"`
// List of the granted permissions
Permissions map[string][]string `json:"permissions"`
// Used quota as bytes
UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files
UsedQuotaFiles int `json:"used_quota_files"`
// Last quota update as unix timestamp in milliseconds
LastQuotaUpdate int64 `json:"last_quota_update"`
// Maximum upload bandwidth as KB/s, 0 means unlimited
UploadBandwidth int64 `json:"upload_bandwidth"`
// Maximum download bandwidth as KB/s, 0 means unlimited
DownloadBandwidth int64 `json:"download_bandwidth"`
// Last login as unix timestamp in milliseconds
LastLogin int64 `json:"last_login"`
// Additional restrictions
Filters UserFilters `json:"filters"`
// optional description, for example full name
Description string `json:"description,omitempty"`
// free form text field for external systems
AdditionalInfo string `json:"additional_info,omitempty"`
}
// User defines a SFTPGo user
type User struct {
BaseUser
// Mapping between virtual paths and virtual folders
VirtualFolders []VirtualFolder `json:"virtual_folders,omitempty"`
// Filesystem configuration details
FsConfig Filesystem `json:"filesystem"`
}

View file

@ -14,7 +14,8 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -51,7 +52,7 @@ func (s *Service) initLogger() {
if !s.LogVerbose { if !s.LogVerbose {
logLevel = zerolog.InfoLevel logLevel = zerolog.InfoLevel
} }
if !filepath.IsAbs(s.LogFilePath) && utils.IsFileInputValid(s.LogFilePath) { if !filepath.IsAbs(s.LogFilePath) && util.IsFileInputValid(s.LogFilePath) {
s.LogFilePath = filepath.Join(s.ConfigDir, s.LogFilePath) s.LogFilePath = filepath.Join(s.ConfigDir, s.LogFilePath)
} }
logger.InitLogger(s.LogFilePath, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogCompress, logLevel) logger.InitLogger(s.LogFilePath, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogCompress, logLevel)
@ -97,6 +98,11 @@ func (s *Service) Start() error {
logger.ErrorToConsole("unable to initialize KMS: %v", err) logger.ErrorToConsole("unable to initialize KMS: %v", err)
os.Exit(1) os.Exit(1)
} }
if err := plugin.Initialize(config.GetPluginsConfig(), s.LogVerbose); err != nil {
logger.Error(logSender, "", "unable to initialize plugin: %v", err)
logger.ErrorToConsole("unable to initialize plugin: %v", err)
os.Exit(1)
}
providerConf := config.GetProviderConf() providerConf := config.GetProviderConf()
@ -146,7 +152,7 @@ func (s *Service) startServices() {
if sftpdConf.ShouldBind() { if sftpdConf.ShouldBind() {
go func() { go func() {
redactedConf := sftpdConf redactedConf := sftpdConf
redactedConf.KeyboardInteractiveHook = utils.GetRedactedURL(sftpdConf.KeyboardInteractiveHook) redactedConf.KeyboardInteractiveHook = util.GetRedactedURL(sftpdConf.KeyboardInteractiveHook)
logger.Debug(logSender, "", "initializing SFTP server with config %+v", redactedConf) logger.Debug(logSender, "", "initializing SFTP server with config %+v", redactedConf)
if err := sftpdConf.Initialize(s.ConfigDir); err != nil { if err := sftpdConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start SFTP server: %v", err) logger.Error(logSender, "", "could not start SFTP server: %v", err)

View file

@ -18,10 +18,10 @@ import (
"github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/ftpd"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs"
"github.com/drakkan/sftpgo/v2/webdavd" "github.com/drakkan/sftpgo/v2/webdavd"
) )
@ -67,7 +67,7 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
// dynamic ports starts from 49152 // dynamic ports starts from 49152
sftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000) sftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000)
} }
if utils.IsStringInSlice("*", enabledSSHCommands) { if util.IsStringInSlice("*", enabledSSHCommands) {
sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands() sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
} else { } else {
sftpdConf.EnabledSSHCommands = enabledSSHCommands sftpdConf.EnabledSSHCommands = enabledSSHCommands
@ -230,9 +230,9 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
func (s *Service) getPortableDirToServe() string { func (s *Service) getPortableDirToServe() string {
var dirToServe string var dirToServe string
if s.PortableUser.FsConfig.Provider == vfs.S3FilesystemProvider { if s.PortableUser.FsConfig.Provider == sdk.S3FilesystemProvider {
dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix
} else if s.PortableUser.FsConfig.Provider == vfs.GCSFilesystemProvider { } else if s.PortableUser.FsConfig.Provider == sdk.GCSFilesystemProvider {
dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix
} else { } else {
dirToServe = s.PortableUser.HomeDir dirToServe = s.PortableUser.HomeDir
@ -264,19 +264,19 @@ func (s *Service) configurePortableUser() string {
func (s *Service) configurePortableSecrets() { func (s *Service) configurePortableSecrets() {
// we created the user before to initialize the KMS so we need to create the secret here // we created the user before to initialize the KMS so we need to create the secret here
switch s.PortableUser.FsConfig.Provider { switch s.PortableUser.FsConfig.Provider {
case vfs.S3FilesystemProvider: case sdk.S3FilesystemProvider:
payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload() payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload()
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret() s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
if payload != "" { if payload != "" {
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload) s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload)
} }
case vfs.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload() payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload()
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret() s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
if payload != "" { if payload != "" {
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload) s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload)
} }
case vfs.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload() payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret() s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
if payload != "" { if payload != "" {
@ -287,13 +287,13 @@ func (s *Service) configurePortableSecrets() {
if payload != "" { if payload != "" {
s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(payload) s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(payload)
} }
case vfs.CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
payload := s.PortableUser.FsConfig.CryptConfig.Passphrase.GetPayload() payload := s.PortableUser.FsConfig.CryptConfig.Passphrase.GetPayload()
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret() s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
if payload != "" { if payload != "" {
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(payload) s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(payload)
} }
case vfs.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
payload := s.PortableUser.FsConfig.SFTPConfig.Password.GetPayload() payload := s.PortableUser.FsConfig.SFTPConfig.Password.GetPayload()
s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewEmptySecret() s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
if payload != "" { if payload != "" {

View file

@ -16,6 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/ftpd"
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/telemetry" "github.com/drakkan/sftpgo/v2/telemetry"
"github.com/drakkan/sftpgo/v2/webdavd" "github.com/drakkan/sftpgo/v2/webdavd"
) )
@ -330,6 +331,7 @@ func (s *WindowsService) Stop() error {
return fmt.Errorf("could not retrieve service status: %v", err) return fmt.Errorf("could not retrieve service status: %v", err)
} }
} }
plugin.Handler.Cleanup()
return nil return nil
} }

View file

@ -12,6 +12,7 @@ import (
"github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/ftpd"
"github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/httpd"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk/plugin"
"github.com/drakkan/sftpgo/v2/telemetry" "github.com/drakkan/sftpgo/v2/telemetry"
"github.com/drakkan/sftpgo/v2/webdavd" "github.com/drakkan/sftpgo/v2/webdavd"
) )
@ -71,5 +72,6 @@ func handleSIGUSR1() {
func handleInterrupt() { func handleInterrupt() {
logger.Debug(logSender, "", "Received interrupt request") logger.Debug(logSender, "", "Received interrupt request")
plugin.Handler.Cleanup()
os.Exit(0) os.Exit(0)
} }

View file

@ -16,6 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -487,7 +488,7 @@ func getEncryptedFileSize(size int64) (int64, error) {
func getTestUserWithCryptFs(usePubKey bool) dataprovider.User { func getTestUserWithCryptFs(usePubKey bool) dataprovider.User {
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.FsConfig.Provider = vfs.CryptedFilesystemProvider u.FsConfig.Provider = sdk.CryptedFilesystemProvider
u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase) u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase)
return u return u
} }

View file

@ -21,7 +21,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -155,7 +156,9 @@ func TestUploadResumeInvalidOffset(t *testing.T) {
file, err := os.Create(testfile) file, err := os.Create(testfile)
assert.NoError(t, err) assert.NoError(t, err)
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
Username: "testuser",
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user) conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user)
@ -183,7 +186,9 @@ func TestReadWriteErrors(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
Username: "testuser",
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user) conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user)
@ -252,7 +257,9 @@ func TestTransferCancelFn(t *testing.T) {
isCancelled = true isCancelled = true
} }
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
Username: "testuser",
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user) conn := common.NewBaseConnection("", common.ProtocolSFTP, "", user)
@ -377,7 +384,7 @@ func TestSupportedSSHCommands(t *testing.T) {
assert.Equal(t, len(supportedSSHCommands), len(cmds)) assert.Equal(t, len(supportedSSHCommands), len(cmds))
for _, c := range cmds { for _, c := range cmds {
assert.True(t, utils.IsStringInSlice(c, supportedSSHCommands)) assert.True(t, util.IsStringInSlice(c, supportedSSHCommands))
} }
} }
@ -588,11 +595,13 @@ func TestCommandsWithExtensionsFilter(t *testing.T) {
defer server.Close() defer server.Close()
defer client.Close() defer client.Close()
user := dataprovider.User{ user := dataprovider.User{
Username: "test", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "test",
Status: 1, HomeDir: os.TempDir(),
Status: 1,
},
} }
user.Filters.FilePatterns = []dataprovider.PatternsFilter{ user.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/subdir", Path: "/subdir",
AllowedPatterns: []string{".jpg"}, AllowedPatterns: []string{".jpg"},
@ -654,11 +663,13 @@ func TestSSHCommandsRemoteFs(t *testing.T) {
} }
user := dataprovider.User{} user := dataprovider.User{}
user.FsConfig = vfs.Filesystem{ user.FsConfig = vfs.Filesystem{
Provider: vfs.S3FilesystemProvider, Provider: sdk.S3FilesystemProvider,
S3Config: vfs.S3FsConfig{ S3Config: vfs.S3FsConfig{
Bucket: "s3bucket", S3FsConfig: sdk.S3FsConfig{
Endpoint: "endpoint", Bucket: "s3bucket",
Region: "eu-west-1", Endpoint: "endpoint",
Region: "eu-west-1",
},
}, },
} }
connection := &Connection{ connection := &Connection{
@ -702,7 +713,9 @@ func TestSSHCmdGetFsErrors(t *testing.T) {
StdErrBuffer: bytes.NewBuffer(stdErrBuf), StdErrBuffer: bytes.NewBuffer(stdErrBuf),
} }
user := dataprovider.User{ user := dataprovider.User{
HomeDir: "relative path", BaseUser: sdk.BaseUser{
HomeDir: "relative path",
},
} }
user.Permissions = map[string][]string{} user.Permissions = map[string][]string{}
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -754,8 +767,10 @@ func TestGitVirtualFolders(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{ user := dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Permissions: permissions,
HomeDir: os.TempDir(),
},
} }
conn := &Connection{ conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", user), BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", user),
@ -800,8 +815,10 @@ func TestRsyncOptions(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{ user := dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Permissions: permissions,
HomeDir: os.TempDir(),
},
} }
conn := &Connection{ conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", user), BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", user),
@ -813,7 +830,7 @@ func TestRsyncOptions(t *testing.T) {
} }
cmd, err := sshCmd.getSystemCommand() cmd, err := sshCmd.getSystemCommand()
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, utils.IsStringInSlice("--safe-links", cmd.cmd.Args), assert.True(t, util.IsStringInSlice("--safe-links", cmd.cmd.Args),
"--safe-links must be added if the user has the create symlinks permission") "--safe-links must be added if the user has the create symlinks permission")
permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
@ -830,7 +847,7 @@ func TestRsyncOptions(t *testing.T) {
} }
cmd, err = sshCmd.getSystemCommand() cmd, err = sshCmd.getSystemCommand()
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, utils.IsStringInSlice("--munge-links", cmd.cmd.Args), assert.True(t, util.IsStringInSlice("--munge-links", cmd.cmd.Args),
"--munge-links must be added if the user has the create symlinks permission") "--munge-links must be added if the user has the create symlinks permission")
sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{ sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{
@ -847,8 +864,10 @@ func TestSystemCommandSizeForPath(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{ user := dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Permissions: permissions,
HomeDir: os.TempDir(),
},
} }
fs, err := user.GetFilesystem("123") fs, err := user.GetFilesystem("123")
assert.NoError(t, err) assert.NoError(t, err)
@ -909,8 +928,10 @@ func TestSystemCommandErrors(t *testing.T) {
err = os.WriteFile(filepath.Join(homeDir, "afile"), []byte("content"), os.ModePerm) err = os.WriteFile(filepath.Join(homeDir, "afile"), []byte("content"), os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
user := dataprovider.User{ user := dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: homeDir, Permissions: permissions,
HomeDir: homeDir,
},
} }
fs, err := user.GetFilesystem("123") fs, err := user.GetFilesystem("123")
assert.NoError(t, err) assert.NoError(t, err)
@ -987,7 +1008,7 @@ func TestSystemCommandErrors(t *testing.T) {
func TestCommandGetFsError(t *testing.T) { func TestCommandGetFsError(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
}, },
} }
conn := &Connection{ conn := &Connection{
@ -1095,8 +1116,10 @@ func TestSCPUploadError(t *testing.T) {
WriteError: writeErr, WriteError: writeErr,
} }
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Join(os.TempDir()), BaseUser: sdk.BaseUser{
Permissions: make(map[string][]string), HomeDir: filepath.Join(os.TempDir()),
Permissions: make(map[string][]string),
},
} }
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -1141,7 +1164,9 @@ func TestSCPInvalidEndDir(t *testing.T) {
} }
connection := &Connection{ connection := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", dataprovider.User{ BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", dataprovider.User{
HomeDir: os.TempDir(), BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(),
},
}), }),
channel: &mockSSHChannel, channel: &mockSSHChannel,
} }
@ -1167,7 +1192,9 @@ func TestSCPParseUploadMessage(t *testing.T) {
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
connection := &Connection{ connection := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", dataprovider.User{ BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", dataprovider.User{
HomeDir: os.TempDir(), BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(),
},
}), }),
channel: &mockSSHChannel, channel: &mockSSHChannel,
} }
@ -1422,7 +1449,9 @@ func TestSCPRecursiveDownloadErrors(t *testing.T) {
fs := vfs.NewOsFs("123", os.TempDir(), "") fs := vfs.NewOsFs("123", os.TempDir(), "")
connection := &Connection{ connection := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", dataprovider.User{ BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", dataprovider.User{
HomeDir: os.TempDir(), BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(),
},
}), }),
channel: &mockSSHChannel, channel: &mockSSHChannel,
} }
@ -1542,7 +1571,7 @@ func TestSCPDownloadFileData(t *testing.T) {
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
connection := &Connection{ connection := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", dataprovider.User{HomeDir: os.TempDir()}), BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}}),
channel: &mockSSHChannelReadErr, channel: &mockSSHChannelReadErr,
} }
scpCommand := scpCommand{ scpCommand := scpCommand{
@ -1588,7 +1617,9 @@ func TestSCPUploadFiledata(t *testing.T) {
WriteError: writeErr, WriteError: writeErr,
} }
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
Username: "testuser",
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
connection := &Connection{ connection := &Connection{
@ -1677,7 +1708,9 @@ func TestUploadError(t *testing.T) {
common.Config.UploadMode = common.UploadModeAtomic common.Config.UploadMode = common.UploadModeAtomic
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
Username: "testuser",
},
} }
fs := vfs.NewOsFs("", os.TempDir(), "") fs := vfs.NewOsFs("", os.TempDir(), "")
connection := &Connection{ connection := &Connection{
@ -1711,12 +1744,16 @@ func TestUploadError(t *testing.T) {
func TestTransferFailingReader(t *testing.T) { func TestTransferFailingReader(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
Username: "testuser", BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Username: "testuser",
HomeDir: os.TempDir(),
},
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret("crypt secret"), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret("crypt secret"),
},
}, },
}, },
} }
@ -1770,13 +1807,13 @@ func TestConnectionStatusStruct(t *testing.T) {
var transfers []common.ConnectionTransfer var transfers []common.ConnectionTransfer
transferUL := common.ConnectionTransfer{ transferUL := common.ConnectionTransfer{
OperationType: "upload", OperationType: "upload",
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
Size: 123, Size: 123,
VirtualPath: "/test.upload", VirtualPath: "/test.upload",
} }
transferDL := common.ConnectionTransfer{ transferDL := common.ConnectionTransfer{
OperationType: "download", OperationType: "download",
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
Size: 123, Size: 123,
VirtualPath: "/test.download", VirtualPath: "/test.download",
} }
@ -1787,8 +1824,8 @@ func TestConnectionStatusStruct(t *testing.T) {
ConnectionID: "123", ConnectionID: "123",
ClientVersion: "fakeClient-1.0.0", ClientVersion: "fakeClient-1.0.0",
RemoteAddress: "127.0.0.1:1234", RemoteAddress: "127.0.0.1:1234",
ConnectionTime: utils.GetTimeAsMsSinceEpoch(time.Now()), ConnectionTime: util.GetTimeAsMsSinceEpoch(time.Now()),
LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()), LastActivity: util.GetTimeAsMsSinceEpoch(time.Now()),
Protocol: "SFTP", Protocol: "SFTP",
Transfers: transfers, Transfers: transfers,
} }
@ -1878,8 +1915,10 @@ func TestRecursiveCopyErrors(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{ user := dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Permissions: permissions,
HomeDir: os.TempDir(),
},
} }
fs, err := user.GetFilesystem("123") fs, err := user.GetFilesystem("123")
assert.NoError(t, err) assert.NoError(t, err)
@ -1900,13 +1939,15 @@ func TestSFTPSubSystem(t *testing.T) {
permissions := make(map[string][]string) permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny} permissions["/"] = []string{dataprovider.PermAny}
user := &dataprovider.User{ user := &dataprovider.User{
Permissions: permissions, BaseUser: sdk.BaseUser{
HomeDir: os.TempDir(), Permissions: permissions,
HomeDir: os.TempDir(),
},
} }
user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
err := ServeSubSystemConnection(user, "connID", nil, nil) err := ServeSubSystemConnection(user, "connID", nil, nil)
assert.Error(t, err) assert.Error(t, err)
user.FsConfig.Provider = vfs.LocalFilesystemProvider user.FsConfig.Provider = sdk.LocalFilesystemProvider
buf := make([]byte, 0, 4096) buf := make([]byte, 0, 4096)
stdErrBuf := make([]byte, 0, 4096) stdErrBuf := make([]byte, 0, 4096)

View file

@ -15,7 +15,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -548,11 +548,11 @@ func (c *scpCommand) getCommandType() string {
} }
func (c *scpCommand) sendFileTime() bool { func (c *scpCommand) sendFileTime() bool {
return utils.IsStringInSlice("-p", c.args) return util.IsStringInSlice("-p", c.args)
} }
func (c *scpCommand) isRecursive() bool { func (c *scpCommand) isRecursive() bool {
return utils.IsStringInSlice("-r", c.args) return util.IsStringInSlice("-r", c.args)
} }
// read the SCP confirmation message and the optional text message // read the SCP confirmation message and the optional text message

View file

@ -19,8 +19,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -210,7 +210,7 @@ func (c *Configuration) Initialize(configDir string) error {
go func(binding Binding) { go func(binding Binding) {
addr := binding.GetAddress() addr := binding.GetAddress()
utils.CheckTCP4Port(binding.Port) util.CheckTCP4Port(binding.Port)
listener, err := net.Listen("tcp", addr) listener, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
logger.Warn(logSender, "", "error starting listener on address %v: %v", addr, err) logger.Warn(logSender, "", "error starting listener on address %v: %v", addr, err)
@ -355,7 +355,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
} }
}() }()
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
common.Connections.AddClientConnection(ipAddr) common.Connections.AddClientConnection(ipAddr)
defer common.Connections.RemoveClientConnection(ipAddr) defer common.Connections.RemoveClientConnection(ipAddr)
@ -515,7 +515,7 @@ func checkAuthError(ip string, err error) {
} }
} else { } else {
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, common.ProtocolSSH, err.Error()) logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, common.ProtocolSSH, err.Error())
metrics.AddNoAuthTryed() metric.AddNoAuthTryed()
common.AddDefenderEvent(ip, common.HostEventNoLoginTried) common.AddDefenderEvent(ip, common.HostEventNoLoginTried)
dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, common.ProtocolSSH, err) dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, common.ProtocolSSH, err)
} }
@ -531,7 +531,7 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
user.Username, user.HomeDir) user.Username, user.HomeDir)
return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir) return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
} }
if utils.IsStringInSlice(common.ProtocolSSH, user.Filters.DeniedProtocols) { if util.IsStringInSlice(common.ProtocolSSH, user.Filters.DeniedProtocols) {
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol SSH is not allowed", user.Username) logger.Debug(logSender, connectionID, "cannot login user %#v, protocol SSH is not allowed", user.Username)
return nil, fmt.Errorf("protocol SSH is not allowed for user %#v", user.Username) return nil, fmt.Errorf("protocol SSH is not allowed for user %#v", user.Username)
} }
@ -569,13 +569,13 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
} }
func (c *Configuration) checkSSHCommands() { func (c *Configuration) checkSSHCommands() {
if utils.IsStringInSlice("*", c.EnabledSSHCommands) { if util.IsStringInSlice("*", c.EnabledSSHCommands) {
c.EnabledSSHCommands = GetSupportedSSHCommands() c.EnabledSSHCommands = GetSupportedSSHCommands()
return return
} }
sshCommands := []string{} sshCommands := []string{}
for _, command := range c.EnabledSSHCommands { for _, command := range c.EnabledSSHCommands {
if utils.IsStringInSlice(command, supportedSSHCommands) { if util.IsStringInSlice(command, supportedSSHCommands) {
sshCommands = append(sshCommands, command) sshCommands = append(sshCommands, command)
} else { } else {
logger.Warn(logSender, "", "unsupported ssh command: %#v ignored", command) logger.Warn(logSender, "", "unsupported ssh command: %#v ignored", command)
@ -594,11 +594,11 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error {
logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile) logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile)
logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile) logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile)
if k == defaultPrivateRSAKeyName { if k == defaultPrivateRSAKeyName {
err = utils.GenerateRSAKeys(autoFile) err = util.GenerateRSAKeys(autoFile)
} else if k == defaultPrivateECDSAKeyName { } else if k == defaultPrivateECDSAKeyName {
err = utils.GenerateECDSAKeys(autoFile) err = util.GenerateECDSAKeys(autoFile)
} else { } else {
err = utils.GenerateEd25519Keys(autoFile) err = util.GenerateEd25519Keys(autoFile)
} }
if err != nil { if err != nil {
logger.Warn(logSender, "", "error creating host key %#v: %v", autoFile, err) logger.Warn(logSender, "", "error creating host key %#v: %v", autoFile, err)
@ -621,7 +621,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
case defaultPrivateRSAKeyName: case defaultPrivateRSAKeyName:
logger.Info(logSender, "", "try to create non-existent host key %#v", k) logger.Info(logSender, "", "try to create non-existent host key %#v", k)
logger.InfoToConsole("try to create non-existent host key %#v", k) logger.InfoToConsole("try to create non-existent host key %#v", k)
err = utils.GenerateRSAKeys(k) err = util.GenerateRSAKeys(k)
if err != nil { if err != nil {
logger.Warn(logSender, "", "error creating host key %#v: %v", k, err) logger.Warn(logSender, "", "error creating host key %#v: %v", k, err)
logger.WarnToConsole("error creating host key %#v: %v", k, err) logger.WarnToConsole("error creating host key %#v: %v", k, err)
@ -630,7 +630,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
case defaultPrivateECDSAKeyName: case defaultPrivateECDSAKeyName:
logger.Info(logSender, "", "try to create non-existent host key %#v", k) logger.Info(logSender, "", "try to create non-existent host key %#v", k)
logger.InfoToConsole("try to create non-existent host key %#v", k) logger.InfoToConsole("try to create non-existent host key %#v", k)
err = utils.GenerateECDSAKeys(k) err = util.GenerateECDSAKeys(k)
if err != nil { if err != nil {
logger.Warn(logSender, "", "error creating host key %#v: %v", k, err) logger.Warn(logSender, "", "error creating host key %#v: %v", k, err)
logger.WarnToConsole("error creating host key %#v: %v", k, err) logger.WarnToConsole("error creating host key %#v: %v", k, err)
@ -639,7 +639,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
case defaultPrivateEd25519KeyName: case defaultPrivateEd25519KeyName:
logger.Info(logSender, "", "try to create non-existent host key %#v", k) logger.Info(logSender, "", "try to create non-existent host key %#v", k)
logger.InfoToConsole("try to create non-existent host key %#v", k) logger.InfoToConsole("try to create non-existent host key %#v", k)
err = utils.GenerateEd25519Keys(k) err = util.GenerateEd25519Keys(k)
if err != nil { if err != nil {
logger.Warn(logSender, "", "error creating host key %#v: %v", k, err) logger.Warn(logSender, "", "error creating host key %#v: %v", k, err)
logger.WarnToConsole("error creating host key %#v: %v", k, err) logger.WarnToConsole("error creating host key %#v: %v", k, err)
@ -667,7 +667,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
} }
serviceStatus.HostKeys = nil serviceStatus.HostKeys = nil
for _, hostKey := range c.HostKeys { for _, hostKey := range c.HostKeys {
if !utils.IsFileInputValid(hostKey) { if !util.IsFileInputValid(hostKey) {
logger.Warn(logSender, "", "unable to load invalid host key %#v", hostKey) logger.Warn(logSender, "", "unable to load invalid host key %#v", hostKey)
logger.WarnToConsole("unable to load invalid host key %#v", hostKey) logger.WarnToConsole("unable to load invalid host key %#v", hostKey)
continue continue
@ -708,7 +708,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
func (c *Configuration) initializeCertChecker(configDir string) error { func (c *Configuration) initializeCertChecker(configDir string) error {
for _, keyPath := range c.TrustedUserCAKeys { for _, keyPath := range c.TrustedUserCAKeys {
if !utils.IsFileInputValid(keyPath) { if !util.IsFileInputValid(keyPath) {
logger.Warn(logSender, "", "unable to load invalid trusted user CA key: %#v", keyPath) logger.Warn(logSender, "", "unable to load invalid trusted user CA key: %#v", keyPath)
logger.WarnToConsole("unable to load invalid trusted user CA key: %#v", keyPath) logger.WarnToConsole("unable to load invalid trusted user CA key: %#v", keyPath)
continue continue
@ -755,7 +755,7 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
connectionID := hex.EncodeToString(conn.SessionID()) connectionID := hex.EncodeToString(conn.SessionID())
method := dataprovider.SSHLoginMethodPublicKey method := dataprovider.SSHLoginMethodPublicKey
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
cert, ok := pubKey.(*ssh.Certificate) cert, ok := pubKey.(*ssh.Certificate)
if ok { if ok {
if cert.CertType != ssh.UserCert { if cert.CertType != ssh.UserCert {
@ -808,7 +808,7 @@ func (c *Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass
if len(conn.PartialSuccessMethods()) == 1 { if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndPassword method = dataprovider.SSHLoginMethodKeyAndPassword
} }
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass), ipAddr, common.ProtocolSSH); err == nil { if user, err = dataprovider.CheckUserAndPass(conn.User(), string(pass), ipAddr, common.ProtocolSSH); err == nil {
sshPerm, err = loginUser(&user, method, "", conn) sshPerm, err = loginUser(&user, method, "", conn)
} }
@ -826,7 +826,7 @@ func (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMeta
if len(conn.PartialSuccessMethods()) == 1 { if len(conn.PartialSuccessMethods()) == 1 {
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
} }
ipAddr := utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()) ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client, if user, err = dataprovider.CheckKeyboardInteractiveAuth(conn.User(), c.KeyboardInteractiveHook, client,
ipAddr, common.ProtocolSSH); err == nil { ipAddr, common.ProtocolSSH); err == nil {
sshPerm, err = loginUser(&user, method, "", conn) sshPerm, err = loginUser(&user, method, "", conn)
@ -837,7 +837,7 @@ func (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMeta
} }
func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
metrics.AddLoginAttempt(method) metric.AddLoginAttempt(method)
if err != nil { if err != nil {
logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error()) logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())
if method != dataprovider.SSHLoginMethodPublicKey { if method != dataprovider.SSHLoginMethodPublicKey {
@ -845,12 +845,12 @@ func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
// record failed login key auth only once for session if the // record failed login key auth only once for session if the
// authentication fails in checkAuthError // authentication fails in checkAuthError
event := common.HostEventLoginFailed event := common.HostEventLoginFailed
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
event = common.HostEventUserNotFound event = common.HostEventUserNotFound
} }
common.AddDefenderEvent(ip, event) common.AddDefenderEvent(ip, event)
} }
} }
metrics.AddLoginResult(method, err) metric.AddLoginResult(method, err)
dataprovider.ExecutePostLoginHook(user, method, ip, common.ProtocolSSH, err) dataprovider.ExecutePostLoginHook(user, method, ip, common.ProtocolSSH, err)
} }

View file

@ -42,8 +42,9 @@ import (
"github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/sftpd"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -1865,7 +1866,7 @@ func TestLoginUserExpiration(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin) assert.Greater(t, user.LastLogin, int64(0), "last login must be updated after a successful login: %v", user.LastLogin)
} }
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) - 120000 user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now()) - 120000
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
conn, client, err = getSftpClient(user, usePubKey) conn, client, err = getSftpClient(user, usePubKey)
@ -1873,7 +1874,7 @@ func TestLoginUserExpiration(t *testing.T) {
client.Close() client.Close()
conn.Close() conn.Close()
} }
user.ExpirationDate = utils.GetTimeAsMsSinceEpoch(time.Now()) + 120000 user.ExpirationDate = util.GetTimeAsMsSinceEpoch(time.Now()) + 120000
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err) assert.NoError(t, err)
conn, client, err = getSftpClient(user, usePubKey) conn, client, err = getSftpClient(user, usePubKey)
@ -1891,7 +1892,7 @@ func TestLoginUserExpiration(t *testing.T) {
func TestLoginWithDatabaseCredentials(t *testing.T) { func TestLoginWithDatabaseCredentials(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "testbucket" u.FsConfig.GCSConfig.Bucket = "testbucket"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`) u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`)
@ -1941,7 +1942,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
func TestLoginInvalidFs(t *testing.T) { func TestLoginInvalidFs(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.FsConfig.Provider = vfs.GCSFilesystemProvider u.FsConfig.Provider = sdk.GCSFilesystemProvider
u.FsConfig.GCSConfig.Bucket = "test" u.FsConfig.GCSConfig.Bucket = "test"
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials") u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -3680,7 +3681,7 @@ func TestPatternsFilters(t *testing.T) {
err = sftpUploadFile(testFilePath, testFileName+".zip", testFileSize, client) err = sftpUploadFile(testFilePath, testFileName+".zip", testFileSize, client)
assert.NoError(t, err) assert.NoError(t, err)
} }
user.Filters.FilePatterns = []dataprovider.PatternsFilter{ user.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
AllowedPatterns: []string{"*.zIp"}, AllowedPatterns: []string{"*.zIp"},
@ -3953,17 +3954,21 @@ func TestSFTPLoopSimple(t *testing.T) {
user2 := getTestSFTPUser(usePubKey) user2 := getTestSFTPUser(usePubKey)
user1.Username += "1" user1.Username += "1"
user2.Username += "2" user2.Username += "2"
user1.FsConfig.Provider = vfs.SFTPFilesystemProvider user1.FsConfig.Provider = sdk.SFTPFilesystemProvider
user2.FsConfig.Provider = vfs.SFTPFilesystemProvider user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
user1.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user1.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user2.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user2.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
} }
user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user1.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user1.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
} }
user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated) user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
assert.NoError(t, err, string(resp)) assert.NoError(t, err, string(resp))
@ -4009,28 +4014,34 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: sftpFloderName, Name: sftpFloderName,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{ SFTPConfig: vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user2.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user2.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
VirtualPath: "/vdir", VirtualPath: "/vdir",
}) })
user2.FsConfig.Provider = vfs.SFTPFilesystemProvider user2.FsConfig.Provider = sdk.SFTPFilesystemProvider
user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user1.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user1.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
} }
user3.FsConfig.Provider = vfs.SFTPFilesystemProvider user3.FsConfig.Provider = sdk.SFTPFilesystemProvider
user3.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ user3.FsConfig.SFTPConfig = vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user1.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user1.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
} }
user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated) user1, resp, err := httpdtest.AddUser(user1, http.StatusCreated)
@ -4056,17 +4067,19 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
// user1 -> local account with the SFTP virtual folder /vdir to user2 // user1 -> local account with the SFTP virtual folder /vdir to user2
// user2 -> local account with the SFTP virtual folder /vdir2 to user3 // user2 -> local account with the SFTP virtual folder /vdir2 to user3
// user3 -> sftp user with user1 as fs // user3 -> sftp user with user1 as fs
user2.FsConfig.Provider = vfs.LocalFilesystemProvider user2.FsConfig.Provider = sdk.LocalFilesystemProvider
user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{} user2.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
user2.VirtualFolders = append(user2.VirtualFolders, vfs.VirtualFolder{ user2.VirtualFolders = append(user2.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: sftpFloderName, Name: sftpFloderName,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{ SFTPConfig: vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: user3.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: user3.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
@ -4116,9 +4129,11 @@ func TestNestedVirtualFolders(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameCrypt, Name: folderNameCrypt,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
MappedPath: mappedPathCrypt, MappedPath: mappedPathCrypt,
@ -6736,18 +6751,24 @@ func TestRelativePaths(t *testing.T) {
filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")} filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")}
keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/" keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/"
s3config := vfs.S3FsConfig{ s3config := vfs.S3FsConfig{
KeyPrefix: keyPrefix, S3FsConfig: sdk.S3FsConfig{
KeyPrefix: keyPrefix,
},
} }
s3fs, _ := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config) s3fs, _ := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config)
gcsConfig := vfs.GCSFsConfig{ gcsConfig := vfs.GCSFsConfig{
KeyPrefix: keyPrefix, GCSFsConfig: sdk.GCSFsConfig{
KeyPrefix: keyPrefix,
},
} }
gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), "", gcsConfig) gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), "", gcsConfig)
sftpconfig := vfs.SFTPFsConfig{ sftpconfig := vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: defaultUsername, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: defaultUsername,
Prefix: keyPrefix, Password: kms.NewPlainSecret(defaultPassword),
Prefix: keyPrefix,
},
} }
sftpfs, _ := vfs.NewSFTPFs("", "", os.TempDir(), []string{user.Username}, sftpconfig) sftpfs, _ := vfs.NewSFTPFs("", "", os.TempDir(), []string{user.Username}, sftpconfig)
if runtime.GOOS != osWindows { if runtime.GOOS != osWindows {
@ -6795,16 +6816,20 @@ func TestResolvePaths(t *testing.T) {
filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")} filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir(), "")}
keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/" keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/"
s3config := vfs.S3FsConfig{ s3config := vfs.S3FsConfig{
KeyPrefix: keyPrefix, S3FsConfig: sdk.S3FsConfig{
Bucket: "bucket", KeyPrefix: keyPrefix,
Region: "us-east-1", Bucket: "bucket",
Region: "us-east-1",
},
} }
err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) err = os.MkdirAll(user.GetHomeDir(), os.ModePerm)
assert.NoError(t, err) assert.NoError(t, err)
s3fs, err := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config) s3fs, err := vfs.NewS3Fs("", user.GetHomeDir(), "", s3config)
assert.NoError(t, err) assert.NoError(t, err)
gcsConfig := vfs.GCSFsConfig{ gcsConfig := vfs.GCSFsConfig{
KeyPrefix: keyPrefix, GCSFsConfig: sdk.GCSFsConfig{
KeyPrefix: keyPrefix,
},
} }
gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), "", gcsConfig) gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), "", gcsConfig)
if runtime.GOOS != osWindows { if runtime.GOOS != osWindows {
@ -6900,20 +6925,20 @@ func TestUserPerms(t *testing.T) {
func TestFilterFilePatterns(t *testing.T) { func TestFilterFilePatterns(t *testing.T) {
user := getTestUser(true) user := getTestUser(true)
pattern := dataprovider.PatternsFilter{ pattern := sdk.PatternsFilter{
Path: "/test", Path: "/test",
AllowedPatterns: []string{"*.jpg", "*.png"}, AllowedPatterns: []string{"*.jpg", "*.png"},
DeniedPatterns: []string{"*.pdf"}, DeniedPatterns: []string{"*.pdf"},
} }
filters := dataprovider.UserFilters{ filters := sdk.UserFilters{
FilePatterns: []dataprovider.PatternsFilter{pattern}, FilePatterns: []sdk.PatternsFilter{pattern},
} }
user.Filters = filters user.Filters = filters
assert.True(t, user.IsFileAllowed("/test/test.jPg")) assert.True(t, user.IsFileAllowed("/test/test.jPg"))
assert.False(t, user.IsFileAllowed("/test/test.pdf")) assert.False(t, user.IsFileAllowed("/test/test.pdf"))
assert.True(t, user.IsFileAllowed("/test.pDf")) assert.True(t, user.IsFileAllowed("/test.pDf"))
filters.FilePatterns = append(filters.FilePatterns, dataprovider.PatternsFilter{ filters.FilePatterns = append(filters.FilePatterns, sdk.PatternsFilter{
Path: "/", Path: "/",
AllowedPatterns: []string{"*.zip", "*.rar", "*.pdf"}, AllowedPatterns: []string{"*.zip", "*.rar", "*.pdf"},
DeniedPatterns: []string{"*.gz"}, DeniedPatterns: []string{"*.gz"},
@ -6924,7 +6949,7 @@ func TestFilterFilePatterns(t *testing.T) {
assert.False(t, user.IsFileAllowed("/test/sub/test.pdf")) assert.False(t, user.IsFileAllowed("/test/sub/test.pdf"))
assert.False(t, user.IsFileAllowed("/test1/test.png")) assert.False(t, user.IsFileAllowed("/test1/test.png"))
filters.FilePatterns = append(filters.FilePatterns, dataprovider.PatternsFilter{ filters.FilePatterns = append(filters.FilePatterns, sdk.PatternsFilter{
Path: "/test/sub", Path: "/test/sub",
DeniedPatterns: []string{"*.tar"}, DeniedPatterns: []string{"*.tar"},
}) })
@ -6948,8 +6973,8 @@ func TestUserAllowedLoginMethods(t *testing.T) {
allowedMethods = user.GetAllowedLoginMethods() allowedMethods = user.GetAllowedLoginMethods()
assert.Equal(t, 4, len(allowedMethods)) assert.Equal(t, 4, len(allowedMethods))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndKeyboardInt, allowedMethods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndKeyboardInt, allowedMethods))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndPassword, allowedMethods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyAndPassword, allowedMethods))
} }
func TestUserPartialAuth(t *testing.T) { func TestUserPartialAuth(t *testing.T) {
@ -7000,11 +7025,11 @@ func TestUserGetNextAuthMethods(t *testing.T) {
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true) methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true)
assert.Equal(t, 2, len(methods)) assert.Equal(t, 2, len(methods))
assert.True(t, utils.IsStringInSlice(dataprovider.LoginMethodPassword, methods)) assert.True(t, util.IsStringInSlice(dataprovider.LoginMethodPassword, methods))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods))
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, false) methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, false)
assert.Equal(t, 1, len(methods)) assert.Equal(t, 1, len(methods))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods))
user.Filters.DeniedLoginMethods = []string{ user.Filters.DeniedLoginMethods = []string{
dataprovider.LoginMethodPassword, dataprovider.LoginMethodPassword,
@ -7014,7 +7039,7 @@ func TestUserGetNextAuthMethods(t *testing.T) {
} }
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true) methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true)
assert.Equal(t, 1, len(methods)) assert.Equal(t, 1, len(methods))
assert.True(t, utils.IsStringInSlice(dataprovider.LoginMethodPassword, methods)) assert.True(t, util.IsStringInSlice(dataprovider.LoginMethodPassword, methods))
user.Filters.DeniedLoginMethods = []string{ user.Filters.DeniedLoginMethods = []string{
dataprovider.LoginMethodPassword, dataprovider.LoginMethodPassword,
@ -7024,7 +7049,7 @@ func TestUserGetNextAuthMethods(t *testing.T) {
} }
methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true) methods = user.GetNextAuthMethods([]string{dataprovider.SSHLoginMethodPublicKey}, true)
assert.Equal(t, 1, len(methods)) assert.Equal(t, 1, len(methods))
assert.True(t, utils.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods)) assert.True(t, util.IsStringInSlice(dataprovider.SSHLoginMethodKeyboardInteractive, methods))
} }
func TestUserIsLoginMethodAllowed(t *testing.T) { func TestUserIsLoginMethodAllowed(t *testing.T) {
@ -7242,7 +7267,7 @@ func TestStatVFS(t *testing.T) {
func TestStatVFSCloudBackend(t *testing.T) { func TestStatVFSCloudBackend(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.FsConfig.Provider = vfs.AzureBlobFilesystemProvider u.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
u.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("https://myaccount.blob.core.windows.net/sasurl") u.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret("https://myaccount.blob.core.windows.net/sasurl")
user, _, err := httpdtest.AddUser(u, http.StatusCreated) user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -7386,7 +7411,7 @@ func TestSSHCopy(t *testing.T) {
QuotaFiles: 100, QuotaFiles: 100,
QuotaSize: 0, QuotaSize: 0,
}) })
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
DeniedPatterns: []string{"*.denied"}, DeniedPatterns: []string{"*.denied"},
@ -7664,7 +7689,7 @@ func TestSSHCopyQuotaLimits(t *testing.T) {
QuotaFiles: 3, QuotaFiles: 3,
QuotaSize: testFileSize + testFileSize1 + 1, QuotaSize: testFileSize + testFileSize1 + 1,
}) })
u.Filters.FilePatterns = []dataprovider.PatternsFilter{ u.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
DeniedPatterns: []string{"*.denied"}, DeniedPatterns: []string{"*.denied"},
@ -7984,9 +8009,11 @@ func TestSSHRemoveCryptFs(t *testing.T) {
Name: folderName2, Name: folderName2,
MappedPath: mappedPath2, MappedPath: mappedPath2,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
@ -8444,7 +8471,7 @@ func TestSCPPatternsFilter(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
err = scpUpload(testFilePath, remoteUpPath, false, false) err = scpUpload(testFilePath, remoteUpPath, false, false)
assert.NoError(t, err) assert.NoError(t, err)
user.Filters.FilePatterns = []dataprovider.PatternsFilter{ user.Filters.FilePatterns = []sdk.PatternsFilter{
{ {
Path: "/", Path: "/",
AllowedPatterns: []string{"*.zip"}, AllowedPatterns: []string{"*.zip"},
@ -8570,11 +8597,13 @@ func TestSCPNestedFolders(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameSFTP, Name: folderNameSFTP,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.SFTPFilesystemProvider, Provider: sdk.SFTPFilesystemProvider,
SFTPConfig: vfs.SFTPFsConfig{ SFTPConfig: vfs.SFTPFsConfig{
Endpoint: sftpServerAddr, SFTPFsConfig: sdk.SFTPFsConfig{
Username: baseUser.Username, Endpoint: sftpServerAddr,
Password: kms.NewPlainSecret(defaultPassword), Username: baseUser.Username,
Password: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
}, },
@ -8587,9 +8616,11 @@ func TestSCPNestedFolders(t *testing.T) {
BaseVirtualFolder: vfs.BaseVirtualFolder{ BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderNameCrypt, Name: folderNameCrypt,
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: vfs.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
CryptConfig: vfs.CryptFsConfig{ CryptConfig: vfs.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: kms.NewPlainSecret(defaultPassword),
},
}, },
}, },
MappedPath: mappedPathCrypt, MappedPath: mappedPathCrypt,
@ -9193,11 +9224,13 @@ func waitTCPListening(address string) {
func getTestUser(usePubKey bool) dataprovider.User { func getTestUser(usePubKey bool) dataprovider.User {
user := dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, BaseUser: sdk.BaseUser{
Password: defaultPassword, Username: defaultUsername,
HomeDir: filepath.Join(homeBasePath, defaultUsername), Password: defaultPassword,
Status: 1, HomeDir: filepath.Join(homeBasePath, defaultUsername),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms user.Permissions["/"] = allPerms
@ -9211,7 +9244,7 @@ func getTestUser(usePubKey bool) dataprovider.User {
func getTestSFTPUser(usePubKey bool) dataprovider.User { func getTestSFTPUser(usePubKey bool) dataprovider.User {
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Username = defaultSFTPUsername u.Username = defaultSFTPUsername
u.FsConfig.Provider = vfs.SFTPFilesystemProvider u.FsConfig.Provider = sdk.SFTPFilesystemProvider
u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr u.FsConfig.SFTPConfig.Endpoint = sftpServerAddr
u.FsConfig.SFTPConfig.Username = defaultUsername u.FsConfig.SFTPConfig.Username = defaultUsername
u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword) u.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)

View file

@ -23,8 +23,9 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -56,7 +57,7 @@ func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommand
name, args, err := parseCommandPayload(msg.Command) name, args, err := parseCommandPayload(msg.Command)
connection.Log(logger.LevelDebug, "new ssh command: %#v args: %v num args: %v user: %v, error: %v", connection.Log(logger.LevelDebug, "new ssh command: %#v args: %v num args: %v user: %v, error: %v",
name, args, len(args), connection.User.Username, err) name, args, len(args), connection.User.Username, err)
if err == nil && utils.IsStringInSlice(name, enabledSSHCommands) { if err == nil && util.IsStringInSlice(name, enabledSSHCommands) {
connection.command = msg.Command connection.command = msg.Command
if name == scpCmdName && len(args) >= 2 { if name == scpCmdName && len(args) >= 2 {
connection.SetProtocol(common.ProtocolSCP) connection.SetProtocol(common.ProtocolSCP)
@ -99,9 +100,9 @@ func (c *sshCommand) handle() (err error) {
defer common.Connections.Remove(c.connection.GetID()) defer common.Connections.Remove(c.connection.GetID())
c.connection.UpdateLastActivity() c.connection.UpdateLastActivity()
if utils.IsStringInSlice(c.command, sshHashCommands) { if util.IsStringInSlice(c.command, sshHashCommands) {
return c.handleHashCommands() return c.handleHashCommands()
} else if utils.IsStringInSlice(c.command, systemCommands) { } else if util.IsStringInSlice(c.command, systemCommands) {
command, err := c.getSystemCommand() command, err := c.getSystemCommand()
if err != nil { if err != nil {
return c.sendErrorResponse(err) return c.sendErrorResponse(err)
@ -492,11 +493,11 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
// If the user cannot create symlinks we add the option --munge-links, if it is not // If the user cannot create symlinks we add the option --munge-links, if it is not
// already set. This should make symlinks unusable (but manually recoverable) // already set. This should make symlinks unusable (but manually recoverable)
if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) { if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
if !utils.IsStringInSlice("--safe-links", args) { if !util.IsStringInSlice("--safe-links", args) {
args = append([]string{"--safe-links"}, args...) args = append([]string{"--safe-links"}, args...)
} }
} else { } else {
if !utils.IsStringInSlice("--munge-links", args) { if !util.IsStringInSlice("--munge-links", args) {
args = append([]string{"--munge-links"}, args...) args = append([]string{"--munge-links"}, args...)
} }
} }
@ -533,7 +534,7 @@ func (c *sshCommand) getSourcePath() string {
func cleanCommandPath(name string) string { func cleanCommandPath(name string) string {
name = strings.Trim(name, "'") name = strings.Trim(name, "'")
name = strings.Trim(name, "\"") name = strings.Trim(name, "\"")
result := utils.CleanPath(name) result := util.CleanPath(name)
if strings.HasSuffix(name, "/") && !strings.HasSuffix(result, "/") { if strings.HasSuffix(name, "/") && !strings.HasSuffix(result, "/") {
result += "/" result += "/"
} }
@ -636,9 +637,9 @@ func (c *sshCommand) getRemovePath() (string, error) {
func (c *sshCommand) isLocalPath(virtualPath string) bool { func (c *sshCommand) isLocalPath(virtualPath string) bool {
folder, err := c.connection.User.GetVirtualFolderForPath(virtualPath) folder, err := c.connection.User.GetVirtualFolderForPath(virtualPath)
if err != nil { if err != nil {
return c.connection.User.FsConfig.Provider == vfs.LocalFilesystemProvider return c.connection.User.FsConfig.Provider == sdk.LocalFilesystemProvider
} }
return folder.FsConfig.Provider == vfs.LocalFilesystemProvider return folder.FsConfig.Provider == sdk.LocalFilesystemProvider
} }
func (c *sshCommand) isLocalCopy(virtualSourcePath, virtualTargetPath string) bool { func (c *sshCommand) isLocalCopy(virtualSourcePath, virtualTargetPath string) bool {
@ -735,7 +736,7 @@ func (c *sshCommand) sendExitStatus(err error) {
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 {
metrics.SSHCommandCompleted(err) metric.SSHCommandCompleted(err)
if cmdPath != "" { if cmdPath != "" {
_, p, errFs := c.connection.GetFsAndResolvedPath(cmdPath) _, p, errFs := c.connection.GetFsAndResolvedPath(cmdPath)
if errFs == nil { if errFs == nil {

View file

@ -8,7 +8,7 @@ import (
"github.com/eikenb/pipeat" "github.com/eikenb/pipeat"
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -224,7 +224,7 @@ func (t *transfer) copyFromReaderToWriter(dst io.Writer, src io.Reader) (int64,
} }
t.ErrTransfer = err t.ErrTransfer = err
if written > 0 || err != nil { if written > 0 || err != nil {
metrics.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.GetType(), t.ErrTransfer) metric.TransferCompleted(atomic.LoadInt64(&t.BytesSent), atomic.LoadInt64(&t.BytesReceived), t.GetType(), t.ErrTransfer)
} }
return written, err return written, err
} }

View file

@ -227,5 +227,6 @@
"url": "", "url": "",
"master_key_path": "" "master_key_path": ""
} }
} },
"plugins": []
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
) )
func initializeRouter(enableProfiler bool) { func initializeRouter(enableProfiler bool) {
@ -26,7 +26,7 @@ func initializeRouter(enableProfiler bool) {
router.Group(func(router chi.Router) { router.Group(func(router chi.Router) {
router.Use(checkAuth) router.Use(checkAuth)
metrics.AddMetricsEndpoint(metricsPath, router) metric.AddMetricsEndpoint(metricsPath, router)
if enableProfiler { if enableProfiler {
logger.InfoToConsole("enabling the built-in profiler") logger.InfoToConsole("enabling the built-in profiler")

View file

@ -16,7 +16,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
const ( const (
@ -105,14 +105,14 @@ func (c Conf) Initialize(configDir string) error {
config := &tls.Config{ config := &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(), GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
CipherSuites: utils.GetTLSCiphersFromNames(c.TLSCipherSuites), CipherSuites: util.GetTLSCiphersFromNames(c.TLSCipherSuites),
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
} }
logger.Debug(logSender, "", "configured TLS cipher suites: %v", config.CipherSuites) logger.Debug(logSender, "", "configured TLS cipher suites: %v", config.CipherSuites)
httpServer.TLSConfig = config httpServer.TLSConfig = config
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender) return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, true, logSender)
} }
return utils.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender) return util.HTTPListenAndServe(httpServer, c.BindAddress, c.BindPort, false, logSender)
} }
// ReloadCertificateMgr reloads the certificate manager // ReloadCertificateMgr reloads the certificate manager
@ -124,7 +124,7 @@ func ReloadCertificateMgr() error {
} }
func getConfigPath(name, configDir string) string { func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) { if !util.IsFileInputValid(name) {
return "" return ""
} }
if name != "" && !filepath.IsAbs(name) { if name != "" && !filepath.IsAbs(name) {

View file

@ -1,4 +1,4 @@
package utils package util
import "fmt" import "fmt"

View file

@ -1,4 +1,4 @@
package utils package util
import ( import (
"net" "net"

View file

@ -1,5 +1,5 @@
// Package utils provides some common utility methods // Package util provides some common utility methods
package utils package util
import ( import (
"bytes" "bytes"

View file

@ -24,7 +24,7 @@ import (
"github.com/pkg/sftp" "github.com/pkg/sftp"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -176,7 +176,7 @@ func (fs *AzureBlobFs) Stat(name string) (os.FileInfo, error) {
attrs, err := fs.headObject(name) attrs, err := fs.headObject(name)
if err == nil { if err == nil {
isDir := (attrs.ContentType() == dirMimeType) isDir := (attrs.ContentType() == dirMimeType)
metrics.AZListObjectsCompleted(nil) metric.AZListObjectsCompleted(nil)
return NewFileInfo(name, isDir, attrs.ContentLength(), attrs.LastModified(), false), nil return NewFileInfo(name, isDir, attrs.ContentLength(), attrs.LastModified(), false), nil
} }
if !fs.IsNotExist(err) { if !fs.IsNotExist(err) {
@ -225,7 +225,7 @@ func (fs *AzureBlobFs) Open(name string, offset int64) (File, *pipeat.PipeReader
n, err := io.Copy(w, body) n, err := io.Copy(w, body)
w.CloseWithError(err) //nolint:errcheck w.CloseWithError(err) //nolint:errcheck
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err) fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err)
metrics.AZTransferCompleted(n, 1, err) metric.AZTransferCompleted(n, 1, err)
}() }()
return nil, r, cancelFn, nil return nil, r, cancelFn, nil
@ -268,7 +268,7 @@ func (fs *AzureBlobFs) Create(name string, flag int) (File, *PipeWriter, func(),
r.CloseWithError(err) //nolint:errcheck r.CloseWithError(err) //nolint:errcheck
p.Done(err) p.Done(err)
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %v", name, r.GetReadedBytes(), err) fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %v", name, r.GetReadedBytes(), err)
metrics.AZTransferCompleted(r.GetReadedBytes(), 0, err) metric.AZTransferCompleted(r.GetReadedBytes(), 0, err)
}() }()
return nil, p, cancelFn, nil return nil, p, cancelFn, nil
@ -307,7 +307,7 @@ func (fs *AzureBlobFs) Rename(source, target string) error {
resp, err := dstBlobURL.StartCopyFromURL(ctx, srcURL, md, mac, bac, azblob.AccessTierType(fs.config.AccessTier), nil) resp, err := dstBlobURL.StartCopyFromURL(ctx, srcURL, md, mac, bac, azblob.AccessTierType(fs.config.AccessTier), nil)
if err != nil { if err != nil {
metrics.AZCopyObjectCompleted(err) metric.AZCopyObjectCompleted(err)
return err return err
} }
copyStatus := resp.CopyStatus() copyStatus := resp.CopyStatus()
@ -321,7 +321,7 @@ func (fs *AzureBlobFs) Rename(source, target string) error {
// of them before giving up. // of them before giving up.
nErrors++ nErrors++
if ctx.Err() != nil || nErrors == 3 { if ctx.Err() != nil || nErrors == 3 {
metrics.AZCopyObjectCompleted(err) metric.AZCopyObjectCompleted(err)
return err return err
} }
} else { } else {
@ -330,10 +330,10 @@ func (fs *AzureBlobFs) Rename(source, target string) error {
} }
if copyStatus != azblob.CopyStatusSuccess { if copyStatus != azblob.CopyStatusSuccess {
err := fmt.Errorf("copy failed with status: %s", copyStatus) err := fmt.Errorf("copy failed with status: %s", copyStatus)
metrics.AZCopyObjectCompleted(err) metric.AZCopyObjectCompleted(err)
return err return err
} }
metrics.AZCopyObjectCompleted(nil) metric.AZCopyObjectCompleted(nil)
return fs.Remove(source, fi.IsDir()) return fs.Remove(source, fi.IsDir())
} }
@ -353,7 +353,7 @@ func (fs *AzureBlobFs) Remove(name string, isDir bool) error {
defer cancelFn() defer cancelFn()
_, err := blobBlockURL.Delete(ctx, azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{}) _, err := blobBlockURL.Delete(ctx, azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
metrics.AZDeleteObjectCompleted(err) metric.AZDeleteObjectCompleted(err)
return err return err
} }
@ -437,7 +437,7 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
Prefix: prefix, Prefix: prefix,
}) })
if err != nil { if err != nil {
metrics.AZListObjectsCompleted(err) metric.AZListObjectsCompleted(err)
return nil, err return nil, err
} }
marker = listBlob.NextMarker marker = listBlob.NextMarker
@ -476,7 +476,7 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
} }
} }
metrics.AZListObjectsCompleted(nil) metric.AZListObjectsCompleted(nil)
return result, nil return result, nil
} }
@ -569,7 +569,7 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
Prefix: fs.config.KeyPrefix, Prefix: fs.config.KeyPrefix,
}) })
if err != nil { if err != nil {
metrics.AZListObjectsCompleted(err) metric.AZListObjectsCompleted(err)
return numFiles, size, err return numFiles, size, err
} }
marker = listBlob.NextMarker marker = listBlob.NextMarker
@ -591,7 +591,7 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
} }
} }
metrics.AZListObjectsCompleted(nil) metric.AZListObjectsCompleted(nil)
return numFiles, size, nil return numFiles, size, nil
} }
@ -654,7 +654,7 @@ func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
Prefix: prefix, Prefix: prefix,
}) })
if err != nil { if err != nil {
metrics.AZListObjectsCompleted(err) metric.AZListObjectsCompleted(err)
return err return err
} }
marker = listBlob.NextMarker marker = listBlob.NextMarker
@ -678,7 +678,7 @@ func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
} }
} }
metrics.AZListObjectsCompleted(nil) metric.AZListObjectsCompleted(nil)
return walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), nil) return walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), nil)
} }
@ -709,7 +709,7 @@ func (fs *AzureBlobFs) headObject(name string) (*azblob.BlobGetPropertiesRespons
blobBlockURL := fs.containerURL.NewBlockBlobURL(name) blobBlockURL := fs.containerURL.NewBlockBlobURL(name)
response, err := blobBlockURL.GetProperties(ctx, azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{}) response, err := blobBlockURL.GetProperties(ctx, azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
metrics.AZHeadObjectCompleted(err) metric.AZHeadObjectCompleted(err)
return response, err return response, err
} }
@ -766,7 +766,7 @@ func (fs *AzureBlobFs) checkIfBucketExists() error {
defer cancelFn() defer cancelFn()
_, err := fs.containerURL.GetProperties(ctx, azblob.LeaseAccessConditions{}) _, err := fs.containerURL.GetProperties(ctx, azblob.LeaseAccessConditions{})
metrics.AZHeadContainerCompleted(err) metric.AZHeadContainerCompleted(err)
return err return err
} }
@ -793,7 +793,7 @@ func (fs *AzureBlobFs) hasContents(name string) (bool, error) {
Prefix: prefix, Prefix: prefix,
MaxResults: 1, MaxResults: 1,
}) })
metrics.AZListObjectsCompleted(err) metric.AZListObjectsCompleted(err)
if err != nil { if err != nil {
return result, err return result, err
} }

View file

@ -4,93 +4,10 @@ import (
"fmt" "fmt"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
) )
// FilesystemProvider defines the supported storage filesystems
type FilesystemProvider int
// supported values for FilesystemProvider
const (
LocalFilesystemProvider FilesystemProvider = iota // Local
S3FilesystemProvider // AWS S3 compatible
GCSFilesystemProvider // Google Cloud Storage
AzureBlobFilesystemProvider // Azure Blob Storage
CryptedFilesystemProvider // Local encrypted
SFTPFilesystemProvider // SFTP
)
// GetProviderByName returns the FilesystemProvider matching a given name
//
// to provide backwards compatibility, numeric strings are accepted as well
func GetProviderByName(name string) FilesystemProvider {
switch name {
case "0", "osfs":
return LocalFilesystemProvider
case "1", "s3fs":
return S3FilesystemProvider
case "2", "gcsfs":
return GCSFilesystemProvider
case "3", "azblobfs":
return AzureBlobFilesystemProvider
case "4", "cryptfs":
return CryptedFilesystemProvider
case "5", "sftpfs":
return SFTPFilesystemProvider
}
// TODO think about returning an error value instead of silently defaulting to LocalFilesystemProvider
return LocalFilesystemProvider
}
// Name returns the Provider's unique name
func (p FilesystemProvider) Name() string {
switch p {
case LocalFilesystemProvider:
return "osfs"
case S3FilesystemProvider:
return "s3fs"
case GCSFilesystemProvider:
return "gcsfs"
case AzureBlobFilesystemProvider:
return "azblobfs"
case CryptedFilesystemProvider:
return "cryptfs"
case SFTPFilesystemProvider:
return "sftpfs"
}
return "" // let's not claim to be
}
// ShortInfo returns a human readable, short description for the given FilesystemProvider
func (p FilesystemProvider) ShortInfo() string {
switch p {
case LocalFilesystemProvider:
return "Local"
case S3FilesystemProvider:
return "AWS S3 (Compatible)"
case GCSFilesystemProvider:
return "Google Cloud Storage"
case AzureBlobFilesystemProvider:
return "Azure Blob Storage"
case CryptedFilesystemProvider:
return "Local encrypted"
case SFTPFilesystemProvider:
return "SFTP"
}
return ""
}
// ListProviders returns a list of available FilesystemProviders
func ListProviders() []FilesystemProvider {
// TODO this should ultimately be dynamic (i.e. each provider registers itself)
return []FilesystemProvider{
LocalFilesystemProvider, S3FilesystemProvider,
GCSFilesystemProvider, AzureBlobFilesystemProvider,
CryptedFilesystemProvider, SFTPFilesystemProvider,
}
}
// ValidatorHelper implements methods we need for Filesystem.ValidateConfig. // ValidatorHelper implements methods we need for Filesystem.ValidateConfig.
// It is implemented by vfs.Folder and dataprovider.User // It is implemented by vfs.Folder and dataprovider.User
type ValidatorHelper interface { type ValidatorHelper interface {
@ -98,15 +15,15 @@ type ValidatorHelper interface {
GetEncryptionAdditionalData() string GetEncryptionAdditionalData() string
} }
// Filesystem defines cloud storage filesystem details // Filesystem defines filesystem details
type Filesystem struct { type Filesystem struct {
RedactedSecret string `json:"-"` RedactedSecret string `json:"-"`
Provider FilesystemProvider `json:"provider"` Provider sdk.FilesystemProvider `json:"provider"`
S3Config S3FsConfig `json:"s3config,omitempty"` S3Config S3FsConfig `json:"s3config,omitempty"`
GCSConfig GCSFsConfig `json:"gcsconfig,omitempty"` GCSConfig GCSFsConfig `json:"gcsconfig,omitempty"`
AzBlobConfig AzBlobFsConfig `json:"azblobconfig,omitempty"` AzBlobConfig AzBlobFsConfig `json:"azblobconfig,omitempty"`
CryptConfig CryptFsConfig `json:"cryptconfig,omitempty"` CryptConfig CryptFsConfig `json:"cryptconfig,omitempty"`
SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"` SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"`
} }
// SetEmptySecretsIfNil sets the secrets to empty if nil // SetEmptySecretsIfNil sets the secrets to empty if nil
@ -167,15 +84,15 @@ func (f *Filesystem) IsEqual(other *Filesystem) bool {
return false return false
} }
switch f.Provider { switch f.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
return f.S3Config.isEqual(&other.S3Config) return f.S3Config.isEqual(&other.S3Config)
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
return f.GCSConfig.isEqual(&other.GCSConfig) return f.GCSConfig.isEqual(&other.GCSConfig)
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
return f.AzBlobConfig.isEqual(&other.AzBlobConfig) return f.AzBlobConfig.isEqual(&other.AzBlobConfig)
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
return f.CryptConfig.isEqual(&other.CryptConfig) return f.CryptConfig.isEqual(&other.CryptConfig)
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
return f.SFTPConfig.isEqual(&other.SFTPConfig) return f.SFTPConfig.isEqual(&other.SFTPConfig)
default: default:
return true return true
@ -186,57 +103,57 @@ func (f *Filesystem) IsEqual(other *Filesystem) bool {
// Filesystem.*Config to their zero value if successful // Filesystem.*Config to their zero value if successful
func (f *Filesystem) Validate(helper ValidatorHelper) error { func (f *Filesystem) Validate(helper ValidatorHelper) error {
switch f.Provider { switch f.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
if err := f.S3Config.Validate(); err != nil { if err := f.S3Config.Validate(); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not validate s3config: %v", err)) return util.NewValidationError(fmt.Sprintf("could not validate s3config: %v", err))
} }
if err := f.S3Config.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { if err := f.S3Config.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not encrypt s3 access secret: %v", err)) return util.NewValidationError(fmt.Sprintf("could not encrypt s3 access secret: %v", err))
} }
f.GCSConfig = GCSFsConfig{} f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{} f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{} f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{} f.SFTPConfig = SFTPFsConfig{}
return nil return nil
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
if err := f.GCSConfig.Validate(helper.GetGCSCredentialsFilePath()); err != nil { if err := f.GCSConfig.Validate(helper.GetGCSCredentialsFilePath()); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not validate GCS config: %v", err)) return util.NewValidationError(fmt.Sprintf("could not validate GCS config: %v", err))
} }
f.S3Config = S3FsConfig{} f.S3Config = S3FsConfig{}
f.AzBlobConfig = AzBlobFsConfig{} f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{} f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{} f.SFTPConfig = SFTPFsConfig{}
return nil return nil
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
if err := f.AzBlobConfig.Validate(); err != nil { if err := f.AzBlobConfig.Validate(); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not validate Azure Blob config: %v", err)) return util.NewValidationError(fmt.Sprintf("could not validate Azure Blob config: %v", err))
} }
if err := f.AzBlobConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { if err := f.AzBlobConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not encrypt Azure blob account key: %v", err)) return util.NewValidationError(fmt.Sprintf("could not encrypt Azure blob account key: %v", err))
} }
f.S3Config = S3FsConfig{} f.S3Config = S3FsConfig{}
f.GCSConfig = GCSFsConfig{} f.GCSConfig = GCSFsConfig{}
f.CryptConfig = CryptFsConfig{} f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{} f.SFTPConfig = SFTPFsConfig{}
return nil return nil
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
if err := f.CryptConfig.Validate(); err != nil { if err := f.CryptConfig.Validate(); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not validate Crypt fs config: %v", err)) return util.NewValidationError(fmt.Sprintf("could not validate Crypt fs config: %v", err))
} }
if err := f.CryptConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { if err := f.CryptConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err)) return util.NewValidationError(fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err))
} }
f.S3Config = S3FsConfig{} f.S3Config = S3FsConfig{}
f.GCSConfig = GCSFsConfig{} f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{} f.AzBlobConfig = AzBlobFsConfig{}
f.SFTPConfig = SFTPFsConfig{} f.SFTPConfig = SFTPFsConfig{}
return nil return nil
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
if err := f.SFTPConfig.Validate(); err != nil { if err := f.SFTPConfig.Validate(); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err)) return util.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err))
} }
if err := f.SFTPConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil { if err := f.SFTPConfig.EncryptCredentials(helper.GetEncryptionAdditionalData()); err != nil {
return utils.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs credentials: %v", err)) return util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs credentials: %v", err))
} }
f.S3Config = S3FsConfig{} f.S3Config = S3FsConfig{}
f.GCSConfig = GCSFsConfig{} f.GCSConfig = GCSFsConfig{}
@ -244,7 +161,7 @@ func (f *Filesystem) Validate(helper ValidatorHelper) error {
f.CryptConfig = CryptFsConfig{} f.CryptConfig = CryptFsConfig{}
return nil return nil
default: default:
f.Provider = LocalFilesystemProvider f.Provider = sdk.LocalFilesystemProvider
f.S3Config = S3FsConfig{} f.S3Config = S3FsConfig{}
f.GCSConfig = GCSFsConfig{} f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{} f.AzBlobConfig = AzBlobFsConfig{}
@ -258,23 +175,23 @@ func (f *Filesystem) Validate(helper ValidatorHelper) error {
func (f *Filesystem) HasRedactedSecret() bool { func (f *Filesystem) HasRedactedSecret() bool {
// TODO move vfs specific code into each *FsConfig struct // TODO move vfs specific code into each *FsConfig struct
switch f.Provider { switch f.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
if f.S3Config.AccessSecret.IsRedacted() { if f.S3Config.AccessSecret.IsRedacted() {
return true return true
} }
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
if f.GCSConfig.Credentials.IsRedacted() { if f.GCSConfig.Credentials.IsRedacted() {
return true return true
} }
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
if f.AzBlobConfig.AccountKey.IsRedacted() { if f.AzBlobConfig.AccountKey.IsRedacted() {
return true return true
} }
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
if f.CryptConfig.Passphrase.IsRedacted() { if f.CryptConfig.Passphrase.IsRedacted() {
return true return true
} }
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
if f.SFTPConfig.Password.IsRedacted() { if f.SFTPConfig.Password.IsRedacted() {
return true return true
} }
@ -289,16 +206,16 @@ func (f *Filesystem) HasRedactedSecret() bool {
// HideConfidentialData hides filesystem confidential data // HideConfidentialData hides filesystem confidential data
func (f *Filesystem) HideConfidentialData() { func (f *Filesystem) HideConfidentialData() {
switch f.Provider { switch f.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
f.S3Config.AccessSecret.Hide() f.S3Config.AccessSecret.Hide()
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
f.GCSConfig.Credentials.Hide() f.GCSConfig.Credentials.Hide()
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
f.AzBlobConfig.AccountKey.Hide() f.AzBlobConfig.AccountKey.Hide()
f.AzBlobConfig.SASURL.Hide() f.AzBlobConfig.SASURL.Hide()
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
f.CryptConfig.Passphrase.Hide() f.CryptConfig.Passphrase.Hide()
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
f.SFTPConfig.Password.Hide() f.SFTPConfig.Password.Hide()
f.SFTPConfig.PrivateKey.Hide() f.SFTPConfig.PrivateKey.Hide()
} }
@ -310,47 +227,57 @@ func (f *Filesystem) GetACopy() Filesystem {
fs := Filesystem{ fs := Filesystem{
Provider: f.Provider, Provider: f.Provider,
S3Config: S3FsConfig{ S3Config: S3FsConfig{
Bucket: f.S3Config.Bucket, S3FsConfig: sdk.S3FsConfig{
Region: f.S3Config.Region, Bucket: f.S3Config.Bucket,
AccessKey: f.S3Config.AccessKey, Region: f.S3Config.Region,
AccessSecret: f.S3Config.AccessSecret.Clone(), AccessKey: f.S3Config.AccessKey,
Endpoint: f.S3Config.Endpoint, AccessSecret: f.S3Config.AccessSecret.Clone(),
StorageClass: f.S3Config.StorageClass, Endpoint: f.S3Config.Endpoint,
KeyPrefix: f.S3Config.KeyPrefix, StorageClass: f.S3Config.StorageClass,
UploadPartSize: f.S3Config.UploadPartSize, KeyPrefix: f.S3Config.KeyPrefix,
UploadConcurrency: f.S3Config.UploadConcurrency, UploadPartSize: f.S3Config.UploadPartSize,
UploadConcurrency: f.S3Config.UploadConcurrency,
},
}, },
GCSConfig: GCSFsConfig{ GCSConfig: GCSFsConfig{
Bucket: f.GCSConfig.Bucket, GCSFsConfig: sdk.GCSFsConfig{
CredentialFile: f.GCSConfig.CredentialFile, Bucket: f.GCSConfig.Bucket,
Credentials: f.GCSConfig.Credentials.Clone(), CredentialFile: f.GCSConfig.CredentialFile,
AutomaticCredentials: f.GCSConfig.AutomaticCredentials, Credentials: f.GCSConfig.Credentials.Clone(),
StorageClass: f.GCSConfig.StorageClass, AutomaticCredentials: f.GCSConfig.AutomaticCredentials,
KeyPrefix: f.GCSConfig.KeyPrefix, StorageClass: f.GCSConfig.StorageClass,
KeyPrefix: f.GCSConfig.KeyPrefix,
},
}, },
AzBlobConfig: AzBlobFsConfig{ AzBlobConfig: AzBlobFsConfig{
Container: f.AzBlobConfig.Container, AzBlobFsConfig: sdk.AzBlobFsConfig{
AccountName: f.AzBlobConfig.AccountName, Container: f.AzBlobConfig.Container,
AccountKey: f.AzBlobConfig.AccountKey.Clone(), AccountName: f.AzBlobConfig.AccountName,
Endpoint: f.AzBlobConfig.Endpoint, AccountKey: f.AzBlobConfig.AccountKey.Clone(),
SASURL: f.AzBlobConfig.SASURL.Clone(), Endpoint: f.AzBlobConfig.Endpoint,
KeyPrefix: f.AzBlobConfig.KeyPrefix, SASURL: f.AzBlobConfig.SASURL.Clone(),
UploadPartSize: f.AzBlobConfig.UploadPartSize, KeyPrefix: f.AzBlobConfig.KeyPrefix,
UploadConcurrency: f.AzBlobConfig.UploadConcurrency, UploadPartSize: f.AzBlobConfig.UploadPartSize,
UseEmulator: f.AzBlobConfig.UseEmulator, UploadConcurrency: f.AzBlobConfig.UploadConcurrency,
AccessTier: f.AzBlobConfig.AccessTier, UseEmulator: f.AzBlobConfig.UseEmulator,
AccessTier: f.AzBlobConfig.AccessTier,
},
}, },
CryptConfig: CryptFsConfig{ CryptConfig: CryptFsConfig{
Passphrase: f.CryptConfig.Passphrase.Clone(), CryptFsConfig: sdk.CryptFsConfig{
Passphrase: f.CryptConfig.Passphrase.Clone(),
},
}, },
SFTPConfig: SFTPFsConfig{ SFTPConfig: SFTPFsConfig{
Endpoint: f.SFTPConfig.Endpoint, SFTPFsConfig: sdk.SFTPFsConfig{
Username: f.SFTPConfig.Username, Endpoint: f.SFTPConfig.Endpoint,
Password: f.SFTPConfig.Password.Clone(), Username: f.SFTPConfig.Username,
PrivateKey: f.SFTPConfig.PrivateKey.Clone(), Password: f.SFTPConfig.Password.Clone(),
Prefix: f.SFTPConfig.Prefix, PrivateKey: f.SFTPConfig.PrivateKey.Clone(),
DisableCouncurrentReads: f.SFTPConfig.DisableCouncurrentReads, Prefix: f.SFTPConfig.Prefix,
BufferSize: f.SFTPConfig.BufferSize, DisableCouncurrentReads: f.SFTPConfig.DisableCouncurrentReads,
BufferSize: f.SFTPConfig.BufferSize,
},
}, },
} }
if len(f.SFTPConfig.Fingerprints) > 0 { if len(f.SFTPConfig.Fingerprints) > 0 {

View file

@ -6,7 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
) )
// BaseVirtualFolder defines the path for the virtual folder and the used quota limits. // BaseVirtualFolder defines the path for the virtual folder and the used quota limits.
@ -65,10 +66,10 @@ func (v *BaseVirtualFolder) GetQuotaSummary() string {
var result string var result string
result = "Files: " + strconv.Itoa(v.UsedQuotaFiles) result = "Files: " + strconv.Itoa(v.UsedQuotaFiles)
if v.UsedQuotaSize > 0 { if v.UsedQuotaSize > 0 {
result += ". Size: " + utils.ByteCountIEC(v.UsedQuotaSize) result += ". Size: " + util.ByteCountIEC(v.UsedQuotaSize)
} }
if v.LastQuotaUpdate > 0 { if v.LastQuotaUpdate > 0 {
t := utils.GetTimeFromMsecSinceEpoch(v.LastQuotaUpdate) t := util.GetTimeFromMsecSinceEpoch(v.LastQuotaUpdate)
result += fmt.Sprintf(". Last update: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM result += fmt.Sprintf(". Last update: %v ", t.Format("2006-01-02 15:04")) // YYYY-MM-DD HH:MM
} }
return result return result
@ -77,17 +78,17 @@ func (v *BaseVirtualFolder) GetQuotaSummary() string {
// GetStorageDescrition returns the storage description // GetStorageDescrition returns the storage description
func (v *BaseVirtualFolder) GetStorageDescrition() string { func (v *BaseVirtualFolder) GetStorageDescrition() string {
switch v.FsConfig.Provider { switch v.FsConfig.Provider {
case LocalFilesystemProvider: case sdk.LocalFilesystemProvider:
return fmt.Sprintf("Local: %v", v.MappedPath) return fmt.Sprintf("Local: %v", v.MappedPath)
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
return fmt.Sprintf("S3: %v", v.FsConfig.S3Config.Bucket) return fmt.Sprintf("S3: %v", v.FsConfig.S3Config.Bucket)
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
return fmt.Sprintf("GCS: %v", v.FsConfig.GCSConfig.Bucket) return fmt.Sprintf("GCS: %v", v.FsConfig.GCSConfig.Bucket)
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
return fmt.Sprintf("AzBlob: %v", v.FsConfig.AzBlobConfig.Container) return fmt.Sprintf("AzBlob: %v", v.FsConfig.AzBlobConfig.Container)
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
return fmt.Sprintf("Encrypted: %v", v.MappedPath) return fmt.Sprintf("Encrypted: %v", v.MappedPath)
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
return fmt.Sprintf("SFTP: %v", v.FsConfig.SFTPConfig.Endpoint) return fmt.Sprintf("SFTP: %v", v.FsConfig.SFTPConfig.Endpoint)
default: default:
return "" return ""
@ -96,22 +97,22 @@ func (v *BaseVirtualFolder) GetStorageDescrition() string {
// IsLocalOrLocalCrypted returns true if the folder provider is local or local encrypted // IsLocalOrLocalCrypted returns true if the folder provider is local or local encrypted
func (v *BaseVirtualFolder) IsLocalOrLocalCrypted() bool { func (v *BaseVirtualFolder) IsLocalOrLocalCrypted() bool {
return v.FsConfig.Provider == LocalFilesystemProvider || v.FsConfig.Provider == CryptedFilesystemProvider return v.FsConfig.Provider == sdk.LocalFilesystemProvider || v.FsConfig.Provider == sdk.CryptedFilesystemProvider
} }
// hideConfidentialData hides folder confidential data // hideConfidentialData hides folder confidential data
func (v *BaseVirtualFolder) hideConfidentialData() { func (v *BaseVirtualFolder) hideConfidentialData() {
switch v.FsConfig.Provider { switch v.FsConfig.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
v.FsConfig.S3Config.AccessSecret.Hide() v.FsConfig.S3Config.AccessSecret.Hide()
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
v.FsConfig.GCSConfig.Credentials.Hide() v.FsConfig.GCSConfig.Credentials.Hide()
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
v.FsConfig.AzBlobConfig.AccountKey.Hide() v.FsConfig.AzBlobConfig.AccountKey.Hide()
v.FsConfig.AzBlobConfig.SASURL.Hide() v.FsConfig.AzBlobConfig.SASURL.Hide()
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
v.FsConfig.CryptConfig.Passphrase.Hide() v.FsConfig.CryptConfig.Passphrase.Hide()
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
v.FsConfig.SFTPConfig.Password.Hide() v.FsConfig.SFTPConfig.Password.Hide()
v.FsConfig.SFTPConfig.PrivateKey.Hide() v.FsConfig.SFTPConfig.PrivateKey.Hide()
} }
@ -128,26 +129,26 @@ func (v *BaseVirtualFolder) PrepareForRendering() {
// HasRedactedSecret returns true if the folder has a redacted secret // HasRedactedSecret returns true if the folder has a redacted secret
func (v *BaseVirtualFolder) HasRedactedSecret() bool { func (v *BaseVirtualFolder) HasRedactedSecret() bool {
switch v.FsConfig.Provider { switch v.FsConfig.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
if v.FsConfig.S3Config.AccessSecret.IsRedacted() { if v.FsConfig.S3Config.AccessSecret.IsRedacted() {
return true return true
} }
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
if v.FsConfig.GCSConfig.Credentials.IsRedacted() { if v.FsConfig.GCSConfig.Credentials.IsRedacted() {
return true return true
} }
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
if v.FsConfig.AzBlobConfig.AccountKey.IsRedacted() { if v.FsConfig.AzBlobConfig.AccountKey.IsRedacted() {
return true return true
} }
if v.FsConfig.AzBlobConfig.SASURL.IsRedacted() { if v.FsConfig.AzBlobConfig.SASURL.IsRedacted() {
return true return true
} }
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
if v.FsConfig.CryptConfig.Passphrase.IsRedacted() { if v.FsConfig.CryptConfig.Passphrase.IsRedacted() {
return true return true
} }
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
if v.FsConfig.SFTPConfig.Password.IsRedacted() { if v.FsConfig.SFTPConfig.Password.IsRedacted() {
return true return true
} }
@ -176,17 +177,17 @@ type VirtualFolder struct {
// GetFilesystem returns the filesystem for this folder // GetFilesystem returns the filesystem for this folder
func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []string) (Fs, error) { func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []string) (Fs, error) {
switch v.FsConfig.Provider { switch v.FsConfig.Provider {
case S3FilesystemProvider: case sdk.S3FilesystemProvider:
return NewS3Fs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.S3Config) return NewS3Fs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.S3Config)
case GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
config := v.FsConfig.GCSConfig config := v.FsConfig.GCSConfig
config.CredentialFile = v.GetGCSCredentialsFilePath() config.CredentialFile = v.GetGCSCredentialsFilePath()
return NewGCSFs(connectionID, v.MappedPath, v.VirtualPath, config) return NewGCSFs(connectionID, v.MappedPath, v.VirtualPath, config)
case AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
return NewAzBlobFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.AzBlobConfig) return NewAzBlobFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.AzBlobConfig)
case CryptedFilesystemProvider: case sdk.CryptedFilesystemProvider:
return NewCryptFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.CryptConfig) return NewCryptFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.CryptConfig)
case SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
return NewSFTPFs(connectionID, v.VirtualPath, v.MappedPath, forbiddenSelfUsers, v.FsConfig.SFTPConfig) return NewSFTPFs(connectionID, v.VirtualPath, v.MappedPath, forbiddenSelfUsers, v.FsConfig.SFTPConfig)
default: default:
return NewOsFs(connectionID, v.MappedPath, v.VirtualPath), nil return NewOsFs(connectionID, v.MappedPath, v.VirtualPath), nil

View file

@ -24,7 +24,7 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -156,7 +156,7 @@ func (fs *GCSFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, fu
n, err := io.Copy(w, objectReader) n, err := io.Copy(w, objectReader)
w.CloseWithError(err) //nolint:errcheck w.CloseWithError(err) //nolint:errcheck
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err) fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err)
metrics.GCSTransferCompleted(n, 1, err) metric.GCSTransferCompleted(n, 1, err)
}() }()
return nil, r, cancelFn, nil return nil, r, cancelFn, nil
} }
@ -195,7 +195,7 @@ func (fs *GCSFs) Create(name string, flag int) (File, *PipeWriter, func(), error
r.CloseWithError(err) //nolint:errcheck r.CloseWithError(err) //nolint:errcheck
p.Done(err) p.Done(err)
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %v", name, n, err) fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %v", name, n, err)
metrics.GCSTransferCompleted(n, 0, err) metric.GCSTransferCompleted(n, 0, err)
}() }()
return nil, p, cancelFn, nil return nil, p, cancelFn, nil
} }
@ -243,7 +243,7 @@ func (fs *GCSFs) Rename(source, target string) error {
copier.ContentType = contentType copier.ContentType = contentType
} }
_, err = copier.Run(ctx) _, err = copier.Run(ctx)
metrics.GCSCopyObjectCompleted(err) metric.GCSCopyObjectCompleted(err)
if err != nil { if err != nil {
return err return err
} }
@ -272,7 +272,7 @@ func (fs *GCSFs) Remove(name string, isDir bool) error {
// we can have directories without a trailing "/" (created using v2.1.0 and before) // we can have directories without a trailing "/" (created using v2.1.0 and before)
err = fs.svc.Bucket(fs.config.Bucket).Object(strings.TrimSuffix(name, "/")).Delete(ctx) err = fs.svc.Bucket(fs.config.Bucket).Object(strings.TrimSuffix(name, "/")).Delete(ctx)
} }
metrics.GCSDeleteObjectCompleted(err) metric.GCSDeleteObjectCompleted(err)
return err return err
} }
@ -354,7 +354,7 @@ func (fs *GCSFs) ReadDir(dirname string) ([]os.FileInfo, error) {
break break
} }
if err != nil { if err != nil {
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return result, err return result, err
} }
if attrs.Prefix != "" { if attrs.Prefix != "" {
@ -389,7 +389,7 @@ func (fs *GCSFs) ReadDir(dirname string) ([]os.FileInfo, error) {
result = append(result, fi) result = append(result, fi)
} }
} }
metrics.GCSListObjectsCompleted(nil) metric.GCSListObjectsCompleted(nil)
return result, nil return result, nil
} }
@ -472,7 +472,7 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
break break
} }
if err != nil { if err != nil {
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return numFiles, size, err return numFiles, size, err
} }
if !attrs.Deleted.IsZero() { if !attrs.Deleted.IsZero() {
@ -485,7 +485,7 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
numFiles++ numFiles++
size += attrs.Size size += attrs.Size
} }
metrics.GCSListObjectsCompleted(nil) metric.GCSListObjectsCompleted(nil)
return numFiles, size, err return numFiles, size, err
} }
@ -552,7 +552,7 @@ func (fs *GCSFs) Walk(root string, walkFn filepath.WalkFunc) error {
} }
if err != nil { if err != nil {
walkFn(root, nil, err) //nolint:errcheck walkFn(root, nil, err) //nolint:errcheck
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return err return err
} }
if !attrs.Deleted.IsZero() { if !attrs.Deleted.IsZero() {
@ -572,7 +572,7 @@ func (fs *GCSFs) Walk(root string, walkFn filepath.WalkFunc) error {
} }
walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), err) //nolint:errcheck walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), err) //nolint:errcheck
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return err return err
} }
@ -641,7 +641,7 @@ func (fs *GCSFs) checkIfBucketExists() error {
defer cancelFn() defer cancelFn()
bkt := fs.svc.Bucket(fs.config.Bucket) bkt := fs.svc.Bucket(fs.config.Bucket)
_, err := bkt.Attrs(ctx) _, err := bkt.Attrs(ctx)
metrics.GCSHeadBucketCompleted(err) metric.GCSHeadBucketCompleted(err)
return err return err
} }
@ -671,7 +671,7 @@ func (fs *GCSFs) hasContents(name string) (bool, error) {
break break
} }
if err != nil { if err != nil {
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return result, err return result, err
} }
name, _ := fs.resolve(attrs.Name, prefix) name, _ := fs.resolve(attrs.Name, prefix)
@ -683,7 +683,7 @@ func (fs *GCSFs) hasContents(name string) (bool, error) {
break break
} }
metrics.GCSListObjectsCompleted(err) metric.GCSListObjectsCompleted(err)
return result, nil return result, nil
} }
@ -705,7 +705,7 @@ func (fs *GCSFs) headObject(name string) (*storage.ObjectAttrs, error) {
bkt := fs.svc.Bucket(fs.config.Bucket) bkt := fs.svc.Bucket(fs.config.Bucket)
obj := bkt.Object(name) obj := bkt.Object(name)
attrs, err := obj.Attrs(ctx) attrs, err := obj.Attrs(ctx)
metrics.GCSHeadObjectCompleted(err) metric.GCSHeadObjectCompleted(err)
return attrs, err return attrs, err
} }

View file

@ -23,8 +23,8 @@ import (
"github.com/pkg/sftp" "github.com/pkg/sftp"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -178,6 +178,10 @@ func (fs *S3Fs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, fun
} }
ctx, cancelFn := context.WithCancel(context.Background()) ctx, cancelFn := context.WithCancel(context.Background())
downloader := s3manager.NewDownloaderWithClient(fs.svc) downloader := s3manager.NewDownloaderWithClient(fs.svc)
/*downloader.RequestOptions = append(downloader.RequestOptions, func(r *request.Request) {
newCtx, _ := context.WithTimeout(r.Context(), time.Minute)
r.SetContext(newCtx)
})*/
var streamRange *string var streamRange *string
if offset > 0 { if offset > 0 {
streamRange = aws.String(fmt.Sprintf("bytes=%v-", offset)) streamRange = aws.String(fmt.Sprintf("bytes=%v-", offset))
@ -192,7 +196,7 @@ func (fs *S3Fs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, fun
}) })
w.CloseWithError(err) //nolint:errcheck w.CloseWithError(err) //nolint:errcheck
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err) fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err)
metrics.S3TransferCompleted(n, 1, err) metric.S3TransferCompleted(n, 1, err)
}() }()
return nil, r, cancelFn, nil return nil, r, cancelFn, nil
} }
@ -219,8 +223,8 @@ func (fs *S3Fs) Create(name string, flag int) (File, *PipeWriter, func(), error)
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
Key: aws.String(key), Key: aws.String(key),
Body: r, Body: r,
StorageClass: utils.NilIfEmpty(fs.config.StorageClass), StorageClass: util.NilIfEmpty(fs.config.StorageClass),
ContentType: utils.NilIfEmpty(contentType), ContentType: util.NilIfEmpty(contentType),
}, func(u *s3manager.Uploader) { }, func(u *s3manager.Uploader) {
u.Concurrency = fs.config.UploadConcurrency u.Concurrency = fs.config.UploadConcurrency
u.PartSize = fs.config.UploadPartSize u.PartSize = fs.config.UploadPartSize
@ -229,7 +233,7 @@ func (fs *S3Fs) Create(name string, flag int) (File, *PipeWriter, func(), error)
p.Done(err) p.Done(err)
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, response: %v, readed bytes: %v, err: %+v", fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, response: %v, readed bytes: %v, err: %+v",
name, response, r.GetReadedBytes(), err) name, response, r.GetReadedBytes(), err)
metrics.S3TransferCompleted(r.GetReadedBytes(), 0, err) metric.S3TransferCompleted(r.GetReadedBytes(), 0, err)
}() }()
return nil, p, cancelFn, nil return nil, p, cancelFn, nil
} }
@ -280,10 +284,10 @@ func (fs *S3Fs) Rename(source, target string) error {
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
CopySource: aws.String(url.PathEscape(copySource)), CopySource: aws.String(url.PathEscape(copySource)),
Key: aws.String(target), Key: aws.String(target),
StorageClass: utils.NilIfEmpty(fs.config.StorageClass), StorageClass: util.NilIfEmpty(fs.config.StorageClass),
ContentType: utils.NilIfEmpty(contentType), ContentType: util.NilIfEmpty(contentType),
}) })
metrics.S3CopyObjectCompleted(err) metric.S3CopyObjectCompleted(err)
if err != nil { if err != nil {
return err return err
} }
@ -310,7 +314,7 @@ func (fs *S3Fs) Remove(name string, isDir bool) error {
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
Key: aws.String(name), Key: aws.String(name),
}) })
metrics.S3DeleteObjectCompleted(err) metric.S3DeleteObjectCompleted(err)
return err return err
} }
@ -418,7 +422,7 @@ func (fs *S3Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
} }
return true return true
}) })
metrics.S3ListObjectsCompleted(err) metric.S3ListObjectsCompleted(err)
return result, err return result, err
} }
@ -505,7 +509,7 @@ func (fs *S3Fs) ScanRootDirContents() (int, int64, error) {
} }
return true return true
}) })
metrics.S3ListObjectsCompleted(err) metric.S3ListObjectsCompleted(err)
return numFiles, size, err return numFiles, size, err
} }
@ -574,7 +578,7 @@ func (fs *S3Fs) Walk(root string, walkFn filepath.WalkFunc) error {
} }
return true return true
}) })
metrics.S3ListObjectsCompleted(err) metric.S3ListObjectsCompleted(err)
walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), err) //nolint:errcheck walkFn(root, NewFileInfo(root, true, 0, time.Now(), false), err) //nolint:errcheck
return err return err
@ -621,7 +625,7 @@ func (fs *S3Fs) checkIfBucketExists() error {
_, err := fs.svc.HeadBucketWithContext(ctx, &s3.HeadBucketInput{ _, err := fs.svc.HeadBucketWithContext(ctx, &s3.HeadBucketInput{
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
}) })
metrics.S3HeadBucketCompleted(err) metric.S3HeadBucketCompleted(err)
return err return err
} }
@ -641,7 +645,7 @@ func (fs *S3Fs) hasContents(name string) (bool, error) {
Prefix: aws.String(prefix), Prefix: aws.String(prefix),
MaxKeys: &maxResults, MaxKeys: &maxResults,
}) })
metrics.S3ListObjectsCompleted(err) metric.S3ListObjectsCompleted(err)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -664,7 +668,7 @@ func (fs *S3Fs) headObject(name string) (*s3.HeadObjectOutput, error) {
Bucket: aws.String(fs.config.Bucket), Bucket: aws.String(fs.config.Bucket),
Key: aws.String(name), Key: aws.String(name),
}) })
metrics.S3HeadObjectCompleted(err) metric.S3HeadObjectCompleted(err)
return obj, err return obj, err
} }

View file

@ -21,7 +21,8 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version" "github.com/drakkan/sftpgo/v2/version"
) )
@ -35,23 +36,7 @@ var ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
// SFTPFsConfig defines the configuration for SFTP based filesystem // SFTPFsConfig defines the configuration for SFTP based filesystem
type SFTPFsConfig struct { type SFTPFsConfig struct {
Endpoint string `json:"endpoint,omitempty"` sdk.SFTPFsConfig
Username string `json:"username,omitempty"`
Password *kms.Secret `json:"password,omitempty"`
PrivateKey *kms.Secret `json:"private_key,omitempty"`
Fingerprints []string `json:"fingerprints,omitempty"`
// Prefix is the path prefix to strip from SFTP resource paths.
Prefix string `json:"prefix,omitempty"`
// Concurrent reads are safe to use and disabling them will degrade performance.
// Some servers automatically delete files once they are downloaded.
// Using concurrent reads is problematic with such servers.
DisableCouncurrentReads bool `json:"disable_concurrent_reads,omitempty"`
// The buffer size (in MB) to use for transfers.
// Buffering could improve performance for high latency networks.
// With buffering enabled upload resume is not supported and a file
// cannot be opened for both reading and writing at the same time
// 0 means disabled.
BufferSize int64 `json:"buffer_size,omitempty"`
forbiddenSelfUsernames []string `json:"-"` forbiddenSelfUsernames []string `json:"-"`
} }
@ -75,7 +60,7 @@ func (c *SFTPFsConfig) isEqual(other *SFTPFsConfig) bool {
return false return false
} }
for _, fp := range c.Fingerprints { for _, fp := range c.Fingerprints {
if !utils.IsStringInSlice(fp, other.Fingerprints) { if !util.IsStringInSlice(fp, other.Fingerprints) {
return false return false
} }
} }
@ -116,7 +101,7 @@ func (c *SFTPFsConfig) Validate() error {
return err return err
} }
if c.Prefix != "" { if c.Prefix != "" {
c.Prefix = utils.CleanPath(c.Prefix) c.Prefix = util.CleanPath(c.Prefix)
} else { } else {
c.Prefix = "/" c.Prefix = "/"
} }
@ -745,8 +730,8 @@ func (fs *SFTPFs) createConnection() error {
User: fs.config.Username, User: fs.config.Username,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
fp := ssh.FingerprintSHA256(key) fp := ssh.FingerprintSHA256(key)
if utils.IsStringInSlice(fp, sftpFingerprints) { if util.IsStringInSlice(fp, sftpFingerprints) {
if utils.IsStringInSlice(fs.config.Username, fs.config.forbiddenSelfUsernames) { if util.IsStringInSlice(fs.config.Username, fs.config.forbiddenSelfUsernames) {
fsLog(fs, logger.LevelWarn, "SFTP loop or nested local SFTP folders detected, mount path %#v, username %#v, forbidden usernames: %+v", fsLog(fs, logger.LevelWarn, "SFTP loop or nested local SFTP folders detected, mount path %#v, username %#v, forbidden usernames: %+v",
fs.mountPath, fs.config.Username, fs.config.forbiddenSelfUsernames) fs.mountPath, fs.config.Username, fs.config.forbiddenSelfUsernames)
return ErrSFTPLoop return ErrSFTPLoop

View file

@ -18,7 +18,8 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
) )
const dirMimeType = "inode/directory" const dirMimeType = "inode/directory"
@ -139,29 +140,7 @@ func (q *QuotaCheckResult) GetRemainingFiles() int {
// S3FsConfig defines the configuration for S3 based filesystem // S3FsConfig defines the configuration for S3 based filesystem
type S3FsConfig struct { type S3FsConfig struct {
Bucket string `json:"bucket,omitempty"` sdk.S3FsConfig
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTP user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
Region string `json:"region,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret *kms.Secret `json:"access_secret,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
// The minimum allowed value is 5.
// Please note that if the upload bandwidth between the SFTP client and SFTPGo is greater than
// the upload bandwidth between SFTPGo and S3 then the SFTP client have to wait for the upload
// of the last parts to S3 after it ends the file upload to SFTPGo, and it may time out.
// Keep this in mind if you customize these parameters.
UploadPartSize int64 `json:"upload_part_size,omitempty"`
// How many parts are uploaded in parallel
UploadConcurrency int `json:"upload_concurrency,omitempty"`
} }
func (c *S3FsConfig) isEqual(other *S3FsConfig) bool { func (c *S3FsConfig) isEqual(other *S3FsConfig) bool {
@ -260,19 +239,7 @@ func (c *S3FsConfig) Validate() error {
// GCSFsConfig defines the configuration for Google Cloud Storage based filesystem // GCSFsConfig defines the configuration for Google Cloud Storage based filesystem
type GCSFsConfig struct { type GCSFsConfig struct {
Bucket string `json:"bucket,omitempty"` sdk.GCSFsConfig
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTP user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
CredentialFile string `json:"-"`
Credentials *kms.Secret `json:"credentials,omitempty"`
// 0 explicit, 1 automatic
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
StorageClass string `json:"storage_class,omitempty"`
} }
func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool { func (c *GCSFsConfig) isEqual(other *GCSFsConfig) bool {
@ -331,39 +298,7 @@ func (c *GCSFsConfig) Validate(credentialsFilePath string) error {
// AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem // AzBlobFsConfig defines the configuration for Azure Blob Storage based filesystem
type AzBlobFsConfig struct { type AzBlobFsConfig struct {
Container string `json:"container,omitempty"` sdk.AzBlobFsConfig
// Storage Account Name, leave blank to use SAS URL
AccountName string `json:"account_name,omitempty"`
// Storage Account Key leave blank to use SAS URL.
// The access key is stored encrypted based on the kms configuration
AccountKey *kms.Secret `json:"account_key,omitempty"`
// Optional endpoint. Default is "blob.core.windows.net".
// If you use the emulator the endpoint must include the protocol,
// for example "http://127.0.0.1:10000"
Endpoint string `json:"endpoint,omitempty"`
// Shared access signature URL, leave blank if using account/key
SASURL *kms.Secret `json:"sas_url,omitempty"`
// KeyPrefix is similar to a chroot directory for local filesystem.
// If specified then the SFTPGo user will only see objects that starts
// with this prefix and so you can restrict access to a specific
// folder. The prefix, if not empty, must not start with "/" and must
// end with "/".
// If empty the whole bucket contents will be available
KeyPrefix string `json:"key_prefix,omitempty"`
// The buffer size (in MB) to use for multipart uploads.
// If this value is set to zero, the default value (1MB) for the Azure SDK will be used.
// Please note that if the upload bandwidth between the SFTPGo client and SFTPGo server is
// greater than the upload bandwidth between SFTPGo and Azure then the SFTP client have
// to wait for the upload of the last parts to Azure after it ends the file upload to SFTPGo,
// and it may time out.
// Keep this in mind if you customize these parameters.
UploadPartSize int64 `json:"upload_part_size,omitempty"`
// How many parts are uploaded in parallel
UploadConcurrency int `json:"upload_concurrency,omitempty"`
// Set to true if you use an Azure emulator such as Azurite
UseEmulator bool `json:"use_emulator,omitempty"`
// Blob Access Tier
AccessTier string `json:"access_tier,omitempty"`
} }
func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool { func (c *AzBlobFsConfig) isEqual(other *AzBlobFsConfig) bool {
@ -476,7 +411,7 @@ func (c *AzBlobFsConfig) Validate() error {
if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 { if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency) return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
} }
if !utils.IsStringInSlice(c.AccessTier, validAzAccessTier) { if !util.IsStringInSlice(c.AccessTier, validAzAccessTier) {
return fmt.Errorf("invalid access tier %#v, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", ")) return fmt.Errorf("invalid access tier %#v, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
} }
return nil return nil
@ -484,7 +419,7 @@ func (c *AzBlobFsConfig) Validate() error {
// CryptFsConfig defines the configuration to store local files as encrypted // CryptFsConfig defines the configuration to store local files as encrypted
type CryptFsConfig struct { type CryptFsConfig struct {
Passphrase *kms.Secret `json:"passphrase,omitempty"` sdk.CryptFsConfig
} }
func (c *CryptFsConfig) isEqual(other *CryptFsConfig) bool { func (c *CryptFsConfig) isEqual(other *CryptFsConfig) bool {

View file

@ -13,7 +13,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -56,7 +56,7 @@ func (c *Connection) GetCommand() string {
func (c *Connection) Mkdir(ctx context.Context, name string, perm os.FileMode) error { func (c *Connection) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
return c.CreateDir(name) return c.CreateDir(name)
} }
@ -64,8 +64,8 @@ func (c *Connection) Mkdir(ctx context.Context, name string, perm os.FileMode) e
func (c *Connection) Rename(ctx context.Context, oldName, newName string) error { func (c *Connection) Rename(ctx context.Context, oldName, newName string) error {
c.UpdateLastActivity() c.UpdateLastActivity()
oldName = utils.CleanPath(oldName) oldName = util.CleanPath(oldName)
newName = utils.CleanPath(newName) newName = util.CleanPath(newName)
return c.BaseConnection.Rename(oldName, newName) return c.BaseConnection.Rename(oldName, newName)
} }
@ -75,7 +75,7 @@ func (c *Connection) Rename(ctx context.Context, oldName, newName string) error
func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error) { func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error) {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) { if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {
return nil, c.GetPermissionDeniedError() return nil, c.GetPermissionDeniedError()
} }
@ -93,7 +93,7 @@ func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error)
func (c *Connection) RemoveAll(ctx context.Context, name string) error { func (c *Connection) RemoveAll(ctx context.Context, name string) error {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
fs, p, err := c.GetFsAndResolvedPath(name) fs, p, err := c.GetFsAndResolvedPath(name)
if err != nil { if err != nil {
return err return err
@ -116,7 +116,7 @@ func (c *Connection) RemoveAll(ctx context.Context, name string) error {
func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
c.UpdateLastActivity() c.UpdateLastActivity()
name = utils.CleanPath(name) name = util.CleanPath(name)
fs, p, err := c.GetFsAndResolvedPath(name) fs, p, err := c.GetFsAndResolvedPath(name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -351,7 +351,7 @@ func (c *Connection) orderDirsToRemove(fs vfs.Fs, dirsToRemove []objectMapping)
for len(orderedDirs) < len(dirsToRemove) { for len(orderedDirs) < len(dirsToRemove) {
for idx, d := range dirsToRemove { for idx, d := range dirsToRemove {
if utils.IsStringInSlice(d.fsPath, removedDirs) { if util.IsStringInSlice(d.fsPath, removedDirs) {
continue continue
} }
isEmpty := true isEmpty := true
@ -359,7 +359,7 @@ func (c *Connection) orderDirsToRemove(fs vfs.Fs, dirsToRemove []objectMapping)
if idx == idx1 { if idx == idx1 {
continue continue
} }
if utils.IsStringInSlice(d1.fsPath, removedDirs) { if util.IsStringInSlice(d1.fsPath, removedDirs) {
continue continue
} }
if strings.HasPrefix(d1.fsPath, d.fsPath+pathSeparator) { if strings.HasPrefix(d1.fsPath, d.fsPath+pathSeparator) {

View file

@ -23,7 +23,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -381,8 +382,10 @@ func TestOrderDirsToRemove(t *testing.T) {
func TestUserInvalidParams(t *testing.T) { func TestUserInvalidParams(t *testing.T) {
u := &dataprovider.User{ u := &dataprovider.User{
Username: "username", BaseUser: sdk.BaseUser{
HomeDir: "invalid", Username: "username",
HomeDir: "invalid",
},
} }
c := &Configuration{ c := &Configuration{
Bindings: []Binding{ Bindings: []Binding{
@ -433,15 +436,15 @@ func TestRemoteAddress(t *testing.T) {
assert.Empty(t, req.RemoteAddr) assert.Empty(t, req.RemoteAddr)
req.Header.Set("True-Client-IP", remoteAddr1) req.Header.Set("True-Client-IP", remoteAddr1)
ip := utils.GetRealIP(req) ip := util.GetRealIP(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.Header.Del("True-Client-IP") req.Header.Del("True-Client-IP")
req.Header.Set("CF-Connecting-IP", remoteAddr1) req.Header.Set("CF-Connecting-IP", remoteAddr1)
ip = utils.GetRealIP(req) ip = util.GetRealIP(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.Header.Del("CF-Connecting-IP") req.Header.Del("CF-Connecting-IP")
req.Header.Set("X-Forwarded-For", remoteAddr1) req.Header.Set("X-Forwarded-For", remoteAddr1)
ip = utils.GetRealIP(req) ip = util.GetRealIP(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
// this will be ignored, remoteAddr1 is not allowed to se this header // this will be ignored, remoteAddr1 is not allowed to se this header
req.Header.Set("X-Forwarded-For", remoteAddr2) req.Header.Set("X-Forwarded-For", remoteAddr2)
@ -453,7 +456,7 @@ func TestRemoteAddress(t *testing.T) {
assert.Empty(t, ip) assert.Empty(t, ip)
req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", remoteAddr2, remoteAddr1)) req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", remoteAddr2, remoteAddr1))
ip = utils.GetRealIP(req) ip = util.GetRealIP(req)
assert.Equal(t, remoteAddr2, ip) assert.Equal(t, remoteAddr2, ip)
req.RemoteAddr = remoteAddr2 req.RemoteAddr = remoteAddr2
@ -477,7 +480,7 @@ func TestRemoteAddress(t *testing.T) {
req.Header.Del("X-Forwarded-For") req.Header.Del("X-Forwarded-For")
req.RemoteAddr = "" req.RemoteAddr = ""
req.Header.Set("X-Real-IP", remoteAddr1) req.Header.Set("X-Real-IP", remoteAddr1)
ip = utils.GetRealIP(req) ip = util.GetRealIP(req)
assert.Equal(t, remoteAddr1, ip) assert.Equal(t, remoteAddr1, ip)
req.RemoteAddr = "" req.RemoteAddr = ""
} }
@ -492,7 +495,9 @@ func TestConnWithNilRequest(t *testing.T) {
func TestResolvePathErrors(t *testing.T) { func TestResolvePathErrors(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := dataprovider.User{ user := dataprovider.User{
HomeDir: "invalid", BaseUser: sdk.BaseUser{
HomeDir: "invalid",
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -561,7 +566,9 @@ func TestResolvePathErrors(t *testing.T) {
func TestFileAccessErrors(t *testing.T) { func TestFileAccessErrors(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -622,7 +629,9 @@ func TestFileAccessErrors(t *testing.T) {
func TestRemoveDirTree(t *testing.T) { func TestRemoveDirTree(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -673,7 +682,9 @@ func TestRemoveDirTree(t *testing.T) {
func TestContentType(t *testing.T) { func TestContentType(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -722,7 +733,9 @@ func TestContentType(t *testing.T) {
func TestTransferReadWriteErrors(t *testing.T) { func TestTransferReadWriteErrors(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -815,7 +828,9 @@ func TestTransferReadWriteErrors(t *testing.T) {
func TestTransferSeek(t *testing.T) { func TestTransferSeek(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
HomeDir: filepath.Clean(os.TempDir()), BaseUser: sdk.BaseUser{
HomeDir: filepath.Clean(os.TempDir()),
},
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny} user.Permissions["/"] = []string{dataprovider.PermAny}
@ -910,11 +925,13 @@ func TestBasicUsersCache(t *testing.T) {
username := "webdav_internal_test" username := "webdav_internal_test"
password := "pwd" password := "pwd"
u := dataprovider.User{ u := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Password: password, Username: username,
HomeDir: filepath.Join(os.TempDir(), username), Password: password,
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/"] = []string{dataprovider.PermAny}
@ -1032,11 +1049,13 @@ func TestCachedUserWithFolders(t *testing.T) {
password := "dav_pwd" password := "dav_pwd"
folderName := "test_folder" folderName := "test_folder"
u := dataprovider.User{ u := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Password: password, Username: username,
HomeDir: filepath.Join(os.TempDir(), username), Password: password,
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/"] = []string{dataprovider.PermAny}
@ -1140,9 +1159,11 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
username := "webdav_internal_test" username := "webdav_internal_test"
password := "pwd" password := "pwd"
u := dataprovider.User{ u := dataprovider.User{
HomeDir: filepath.Join(os.TempDir(), username), BaseUser: sdk.BaseUser{
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
u.Username = username + "1" u.Username = username + "1"
u.Password = password + "1" u.Password = password + "1"
@ -1348,11 +1369,13 @@ func TestUserCacheIsolation(t *testing.T) {
username := "webdav_internal_cache_test" username := "webdav_internal_cache_test"
password := "dav_pwd" password := "dav_pwd"
u := dataprovider.User{ u := dataprovider.User{
Username: username, BaseUser: sdk.BaseUser{
Password: password, Username: username,
HomeDir: filepath.Join(os.TempDir(), username), Password: password,
Status: 1, HomeDir: filepath.Join(os.TempDir(), username),
ExpirationDate: 0, Status: 1,
ExpirationDate: 0,
},
} }
u.Permissions = make(map[string][]string) u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny} u.Permissions["/"] = []string{dataprovider.PermAny}
@ -1382,13 +1405,13 @@ func TestUserCacheIsolation(t *testing.T) {
assert.True(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted()) assert.True(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
err = cachedUser.User.FsConfig.S3Config.AccessSecret.Decrypt() err = cachedUser.User.FsConfig.S3Config.AccessSecret.Decrypt()
assert.NoError(t, err) assert.NoError(t, err)
cachedUser.User.FsConfig.Provider = vfs.S3FilesystemProvider cachedUser.User.FsConfig.Provider = sdk.S3FilesystemProvider
_, err = cachedUser.User.GetFilesystem("") _, err = cachedUser.User.GetFilesystem("")
assert.Error(t, err, "we don't have to get the previously cached filesystem!") assert.Error(t, err, "we don't have to get the previously cached filesystem!")
} }
cachedUser, ok = dataprovider.GetCachedWebDAVUser(username) cachedUser, ok = dataprovider.GetCachedWebDAVUser(username)
if assert.True(t, ok) { if assert.True(t, ok) {
assert.Equal(t, vfs.LocalFilesystemProvider, cachedUser.User.FsConfig.Provider) assert.Equal(t, sdk.LocalFilesystemProvider, cachedUser.User.FsConfig.Provider)
assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted()) assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
} }

View file

@ -22,8 +22,8 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metrics" "github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
type webDavServer struct { type webDavServer struct {
@ -59,7 +59,7 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
httpServer.TLSConfig = &tls.Config{ httpServer.TLSConfig = &tls.Config{
GetCertificate: certMgr.GetCertificateFunc(), GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
CipherSuites: utils.GetTLSCiphersFromNames(s.binding.TLSCipherSuites), CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
} }
logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(), logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
@ -74,11 +74,11 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
httpServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven httpServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
} }
} }
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender) return util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, true, logSender)
} }
s.binding.EnableHTTPS = false s.binding.EnableHTTPS = false
serviceStatus.Bindings = append(serviceStatus.Bindings, s.binding) serviceStatus.Bindings = append(serviceStatus.Bindings, s.binding)
return utils.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender) return util.HTTPListenAndServe(httpServer, s.binding.Address, s.binding.Port, false, logSender)
} }
func (s *webDavServer) verifyTLSConnection(state tls.ConnectionState) error { func (s *webDavServer) verifyTLSConnection(state tls.ConnectionState) error {
@ -299,7 +299,7 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
user.Username, user.HomeDir) user.Username, user.HomeDir)
return connID, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir) return connID, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
} }
if utils.IsStringInSlice(common.ProtocolWebDAV, user.Filters.DeniedProtocols) { if util.IsStringInSlice(common.ProtocolWebDAV, user.Filters.DeniedProtocols) {
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol DAV is not allowed", user.Username) logger.Debug(logSender, connectionID, "cannot login user %#v, protocol DAV is not allowed", user.Username)
return connID, fmt.Errorf("protocol DAV is not allowed for user %#v", user.Username) return connID, fmt.Errorf("protocol DAV is not allowed for user %#v", user.Username)
} }
@ -323,12 +323,12 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
} }
func (s *webDavServer) checkRemoteAddress(r *http.Request) string { func (s *webDavServer) checkRemoteAddress(r *http.Request) string {
ipAddr := utils.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
ip := net.ParseIP(ipAddr) ip := net.ParseIP(ipAddr)
if ip != nil { if ip != nil {
for _, allow := range s.binding.allowHeadersFrom { for _, allow := range s.binding.allowHeadersFrom {
if allow(ip) { if allow(ip) {
parsedIP := utils.GetRealIP(r) parsedIP := util.GetRealIP(r)
if parsedIP != "" { if parsedIP != "" {
ipAddr = parsedIP ipAddr = parsedIP
r.RemoteAddr = ipAddr r.RemoteAddr = ipAddr
@ -366,15 +366,15 @@ func writeLog(r *http.Request, err error) {
} }
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metrics.AddLoginAttempt(loginMethod) metric.AddLoginAttempt(loginMethod)
if err != nil && err != common.ErrInternalFailure { if err != nil && err != common.ErrInternalFailure {
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error()) logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
event := common.HostEventLoginFailed event := common.HostEventLoginFailed
if _, ok := err.(*utils.RecordNotFoundError); ok { if _, ok := err.(*util.RecordNotFoundError); ok {
event = common.HostEventUserNotFound event = common.HostEventUserNotFound
} }
common.AddDefenderEvent(ip, event) common.AddDefenderEvent(ip, event)
} }
metrics.AddLoginResult(loginMethod, err) metric.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err) dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)
} }

View file

@ -11,7 +11,7 @@ import (
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/utils" "github.com/drakkan/sftpgo/v2/util"
) )
type ctxReqParams int type ctxReqParams int
@ -97,7 +97,7 @@ type Binding struct {
} }
func (b *Binding) parseAllowedProxy() error { func (b *Binding) parseAllowedProxy() error {
allowedFuncs, err := utils.ParseAllowedIPAndRanges(b.ProxyAllowed) allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
if err != nil { if err != nil {
return err return err
} }
@ -227,7 +227,7 @@ func ReloadCertificateMgr() error {
} }
func getConfigPath(name, configDir string) string { func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) { if !util.IsFileInputValid(name) {
return "" return ""
} }
if name != "" && !filepath.IsAbs(name) { if name != "" && !filepath.IsAbs(name) {

Some files were not shown because too many files have changed in this diff Show more