From a4cddf4f7f38eb0930a686007a80ad1e46b4c475 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 24 Oct 2019 18:50:35 +0200 Subject: [PATCH] add portable mode Portable mode is a convenient way to share a single directory on demand --- README.md | 26 ++++++++++++ cmd/portable.go | 69 +++++++++++++++++++++++++++++++ config/config.go | 17 +++++++- config/config_test.go | 21 ++++++++++ dataprovider/bolt.go | 2 +- dataprovider/dataprovider.go | 2 +- service/service.go | 78 +++++++++++++++++++++++++++++++++++- sftpd/server.go | 4 +- sftpd/transfer.go | 2 +- 9 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 cmd/portable.go diff --git a/README.md b/README.md index a2c6ce80..a8868b4f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Full featured and highly configurable SFTP server - Web based interface to easily manage users and connections. - Easy migration from Unix system user accounts. - Configuration is a your choice: JSON, TOML, YAML, HCL, envfile are supported. +- Portable mode: a convenient way to share a single directory on demand. - Log files are accurate and they are saved in the easily parsable JSON format. ## Platforms @@ -93,6 +94,7 @@ Usage: Available Commands: help Help about any command + portable Serve a single directory serve Start the SFTP Server Flags: @@ -276,6 +278,30 @@ netsh advfirewall firewall add rule name="SFTPGo Service" dir=in action=allow pr or through the Windows Firewall GUI. +SFTPGo allows to share a single directory on demand using the `portable` subcommand: + +``` +sftpgo portable --help +To serve the current working directory with auto generated credentials simply use: + +sftpgo portable + +Please take a look at the usage below to customize the serving parameters + +Usage: + sftpgo portable [flags] + +Flags: + -d, --directory string Path to the directory to serve. This can be an absolute path or a path relative to the current directory (default ".") + -h, --help help for portable + -p, --password string Leave empty to use an auto generated value + -g, --permissions strings User's permissions. "*" means any permission (default [list,download]) + -k, --public-key strings + --scp Enable SCP + -s, --sftpd-port int 0 means a random non privileged port + -u, --username string Leave empty to use an auto generated value +``` + ## Account's configuration properties For each account the following properties can be configured: diff --git a/cmd/portable.go b/cmd/portable.go new file mode 100644 index 00000000..bd4b54c6 --- /dev/null +++ b/cmd/portable.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "path/filepath" + + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/service" + "github.com/spf13/cobra" +) + +var ( + directoryToServe string + portableSFTPDPort int + portableEnableSCP bool + portableUsername string + portablePassword string + portablePublicKeys []string + portablePermissions []string + portableCmd = &cobra.Command{ + Use: "portable", + Short: "Serve a single directory", + Long: `To serve the current working directory with auto generated credentials simply use: + +sftpgo portable + +Please take a look at the usage below to customize the serving parameters`, + Run: func(cmd *cobra.Command, args []string) { + portableDir := directoryToServe + if !filepath.IsAbs(portableDir) { + portableDir, _ = filepath.Abs(portableDir) + } + service := service.Service{ + ConfigDir: defaultConfigDir, + ConfigFile: defaultConfigName, + LogFilePath: defaultLogFile, + LogMaxSize: defaultLogMaxSize, + LogMaxBackups: defaultLogMaxBackup, + LogMaxAge: defaultLogMaxAge, + LogCompress: defaultLogCompress, + LogVerbose: defaultLogVerbose, + Shutdown: make(chan bool), + PortableMode: 1, + PortableUser: dataprovider.User{ + Username: portableUsername, + Password: portablePassword, + PublicKeys: portablePublicKeys, + Permissions: portablePermissions, + HomeDir: portableDir, + }, + } + if err := service.StartPortableMode(portableSFTPDPort, portableEnableSCP); err == nil { + service.Wait() + } + }, + } +) + +func init() { + portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", + "Path to the directory to serve. This can be an absolute path or a path relative to the current directory") + portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, "0 means a random non privileged port") + portableCmd.Flags().BoolVarP(&portableEnableSCP, "scp", "", false, "Enable SCP") + portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", "Leave empty to use an auto generated value") + portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", "Leave empty to use an auto generated value") + portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "") + portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"}, + "User's permissions. \"*\" means any permission") + rootCmd.AddCommand(portableCmd) +} diff --git a/config/config.go b/config/config.go index f4773019..ae2c7473 100644 --- a/config/config.go +++ b/config/config.go @@ -97,16 +97,31 @@ func GetSFTPDConfig() sftpd.Configuration { return globalConf.SFTPD } +// SetSFTPDConfig sets the configuration for the SFTP server +func SetSFTPDConfig(config sftpd.Configuration) { + globalConf.SFTPD = config +} + // GetHTTPDConfig returns the configuration for the HTTP server func GetHTTPDConfig() httpd.Conf { return globalConf.HTTPDConfig } +// SetHTTPDConfig sets the configuration for the HTTP server +func SetHTTPDConfig(config httpd.Conf) { + globalConf.HTTPDConfig = config +} + //GetProviderConf returns the configuration for the data provider func GetProviderConf() dataprovider.Config { return globalConf.ProviderConf } +//SetProviderConf sets the configuration for the data provider +func SetProviderConf(config dataprovider.Config) { + globalConf.ProviderConf = config +} + func getRedactedGlobalConf() globalConfig { conf := globalConf conf.ProviderConf.Password = "[redacted]" @@ -141,7 +156,7 @@ func LoadConfig(configDir, configName string) error { globalConf.SFTPD.Banner = defaultBanner } if globalConf.SFTPD.UploadMode < 0 || globalConf.SFTPD.UploadMode > 2 { - err = fmt.Errorf("Invalid upload_mode 0 and 1 are supported, configured: %v reset upload_mode to 0", + err = fmt.Errorf("invalid upload_mode 0 and 1 are supported, configured: %v reset upload_mode to 0", globalConf.SFTPD.UploadMode) globalConf.SFTPD.UploadMode = 0 logger.Warn(logSender, "", "Configuration error: %v", err) diff --git a/config/config_test.go b/config/config_test.go index 553f01e5..641b569f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -97,3 +97,24 @@ func TestInvalidUploadMode(t *testing.T) { } os.Remove(configFilePath) } + +func TestSetGetConfig(t *testing.T) { + sftpdConf := config.GetSFTPDConfig() + sftpdConf.IdleTimeout = 3 + config.SetSFTPDConfig(sftpdConf) + if config.GetSFTPDConfig().IdleTimeout != sftpdConf.IdleTimeout { + t.Errorf("set sftpd conf failed") + } + dataProviderConf := config.GetProviderConf() + dataProviderConf.Host = "test host" + config.SetProviderConf(dataProviderConf) + if config.GetProviderConf().Host != dataProviderConf.Host { + t.Errorf("set data provider conf failed") + } + httpdConf := config.GetHTTPDConfig() + httpdConf.BindAddress = "0.0.0.0" + config.SetHTTPDConfig(httpdConf) + if config.GetHTTPDConfig().BindAddress != httpdConf.BindAddress { + t.Errorf("set httpd conf failed") + } +} diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index 06d91b82..0821ffef 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -318,7 +318,7 @@ func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) { bucket := tx.Bucket(usersBucket) idxBucket := tx.Bucket(usersIDIdxBucket) if bucket == nil || idxBucket == nil { - err = fmt.Errorf("Unable to find required buckets, bolt database structure not correcly defined") + err = fmt.Errorf("unable to find required buckets, bolt database structure not correcly defined") } return bucket, idxBucket, err } diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 93d51e48..afc07457 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -180,7 +180,7 @@ func Initialize(cnf Config, basePath string) error { } else if config.Driver == BoltDataProviderName { err = initializeBoltProvider(basePath) } else { - err = fmt.Errorf("Unsupported data provider: %v", config.Driver) + err = fmt.Errorf("unsupported data provider: %v", config.Driver) } if err == nil { startAvailabilityTimer() diff --git a/service/service.go b/service/service.go index 8d09f428..4240d4ca 100644 --- a/service/service.go +++ b/service/service.go @@ -2,12 +2,20 @@ package service import ( + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/utils" + "github.com/rs/xid" "github.com/rs/zerolog" ) @@ -15,6 +23,10 @@ const ( logSender = "service" ) +var ( + chars = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +) + // Service defines the SFTPGo service type Service struct { ConfigDir string @@ -25,6 +37,8 @@ type Service struct { LogMaxAge int LogCompress bool LogVerbose bool + PortableMode int + PortableUser dataprovider.User Shutdown chan bool } @@ -39,7 +53,10 @@ func (s *Service) Start() error { logger.Info(logSender, "", "starting SFTPGo %v, config dir: %v, config file: %v, log max size: %v log max backups: %v "+ "log max age: %v log verbose: %v, log compress: %v", version.GetVersionAsString(), s.ConfigDir, s.ConfigFile, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogVerbose, s.LogCompress) - config.LoadConfig(s.ConfigDir, s.ConfigFile) + // in portable mode we don't read configuration from file + if s.PortableMode != 1 { + config.LoadConfig(s.ConfigDir, s.ConfigFile) + } providerConf := config.GetProviderConf() err := dataprovider.Initialize(providerConf, s.ConfigDir) @@ -53,6 +70,15 @@ func (s *Service) Start() error { sftpdConf := config.GetSFTPDConfig() httpdConf := config.GetHTTPDConfig() + if s.PortableMode == 1 { + // create the user for portable mode + err = dataprovider.AddUser(dataProvider, s.PortableUser) + if err != nil { + logger.ErrorToConsole("error adding portable user: %v", err) + return err + } + } + sftpd.SetDataProvider(dataProvider) go func() { @@ -76,7 +102,14 @@ func (s *Service) Start() error { }() } else { logger.Debug(logSender, "", "HTTP server not started, disabled in config file") - logger.DebugToConsole("HTTP server not started, disabled in config file") + if s.PortableMode != 1 { + logger.DebugToConsole("HTTP server not started, disabled in config file") + } + } + if s.PortableMode == 1 { + logger.InfoToConsole("Portable mode ready, SFTP port: %v, user: %#v, password: %#v, public keys: %v, directory: %#v, permissions: %v,"+ + " SCP enabled: %v", sftpdConf.BindPort, s.PortableUser.Username, s.PortableUser.Password, s.PortableUser.PublicKeys, + s.PortableUser.HomeDir, s.PortableUser.Permissions, sftpdConf.IsSCPEnabled) } return nil } @@ -91,3 +124,44 @@ func (s *Service) Stop() { close(s.Shutdown) logger.Debug(logSender, "", "Service stopped") } + +// StartPortableMode starts the service in portable mode +func (s *Service) StartPortableMode(sftpdPort int, enableSCP bool) error { + rand.Seed(time.Now().UnixNano()) + if s.PortableMode != 1 { + return fmt.Errorf("service is not configured for portable mode") + } + if len(s.PortableUser.Username) == 0 { + s.PortableUser.Username = "user" + } + if len(s.PortableUser.PublicKeys) == 0 && len(s.PortableUser.Password) == 0 { + var b strings.Builder + for i := 0; i < 8; i++ { + b.WriteRune(chars[rand.Intn(len(chars))]) + } + s.PortableUser.Password = b.String() + } + tempDir := os.TempDir() + instanceID := xid.New().String() + databasePath := filepath.Join(tempDir, instanceID+".db") + s.LogFilePath = filepath.Join(tempDir, instanceID+".log") + dataProviderConf := config.GetProviderConf() + dataProviderConf.Driver = dataprovider.BoltDataProviderName + dataProviderConf.Name = databasePath + config.SetProviderConf(dataProviderConf) + httpdConf := config.GetHTTPDConfig() + httpdConf.BindPort = 0 + config.SetHTTPDConfig(httpdConf) + sftpdConf := config.GetSFTPDConfig() + sftpdConf.MaxAuthTries = 12 + if sftpdPort > 0 { + sftpdConf.BindPort = sftpdPort + } else { + // dynamic ports starts from 49152 + sftpdConf.BindPort = 49152 + rand.Intn(15000) + } + sftpdConf.IsSCPEnabled = enableSCP + config.SetSFTPDConfig(sftpdConf) + + return s.Start() +} diff --git a/sftpd/server.go b/sftpd/server.go index f9c532d1..6c7bf30f 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -329,7 +329,7 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro if !filepath.IsAbs(user.HomeDir) { logger.Warn(logSender, "", "user %v has invalid home dir: %#v. Home dir must be an absolute path, login not allowed", 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 _, err := os.Stat(user.HomeDir); os.IsNotExist(err) { err := os.MkdirAll(user.HomeDir, 0777) @@ -345,7 +345,7 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro if activeSessions >= user.MaxSessions { logger.Debug(logSender, "", "authentication refused for user: %v, too many open sessions: %v/%v", user.Username, activeSessions, user.MaxSessions) - return nil, fmt.Errorf("Too many open sessions: %v", activeSessions) + return nil, fmt.Errorf("too many open sessions: %v", activeSessions) } } diff --git a/sftpd/transfer.go b/sftpd/transfer.go index d7acf31c..f1d3e42b 100644 --- a/sftpd/transfer.go +++ b/sftpd/transfer.go @@ -59,7 +59,7 @@ func (t *Transfer) WriteAt(p []byte, off int64) (n int, err error) { t.lastActivity = time.Now() if off < t.minWriteOffset { logger.Warn(logSender, t.connectionID, "Invalid write offset %v minimum valid value %v", off, t.minWriteOffset) - return 0, fmt.Errorf("Invalid write offset %v", off) + return 0, fmt.Errorf("invalid write offset %v", off) } written, e := t.file.WriteAt(p, off) t.bytesReceived += int64(written)