Browse Source

add portable mode

Portable mode is a convenient way to share a single directory on demand
Nicola Murino 5 years ago
parent
commit
a4cddf4f7f
9 changed files with 213 additions and 8 deletions
  1. 26 0
      README.md
  2. 69 0
      cmd/portable.go
  3. 16 1
      config/config.go
  4. 21 0
      config/config_test.go
  5. 1 1
      dataprovider/bolt.go
  6. 1 1
      dataprovider/dataprovider.go
  7. 76 2
      service/service.go
  8. 2 2
      sftpd/server.go
  9. 1 1
      sftpd/transfer.go

+ 26 - 0
README.md

@@ -23,6 +23,7 @@ Full featured and highly configurable SFTP server
 - Web based interface to easily manage users and connections.
 - Web based interface to easily manage users and connections.
 - Easy migration from Unix system user accounts.
 - Easy migration from Unix system user accounts.
 - Configuration is a your choice: JSON, TOML, YAML, HCL, envfile are supported.
 - 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.
 - Log files are accurate and they are saved in the easily parsable JSON format.
 
 
 ## Platforms
 ## Platforms
@@ -93,6 +94,7 @@ Usage:
 
 
 Available Commands:
 Available Commands:
   help        Help about any command
   help        Help about any command
+  portable    Serve a single directory
   serve       Start the SFTP Server
   serve       Start the SFTP Server
 
 
 Flags:
 Flags:
@@ -276,6 +278,30 @@ netsh advfirewall firewall add rule name="SFTPGo Service" dir=in action=allow pr
 
 
 or through the Windows Firewall GUI.
 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
 ## Account's configuration properties
 
 
 For each account the following properties can be configured:
 For each account the following properties can be configured:

+ 69 - 0
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)
+}

+ 16 - 1
config/config.go

@@ -97,16 +97,31 @@ func GetSFTPDConfig() sftpd.Configuration {
 	return globalConf.SFTPD
 	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
 // GetHTTPDConfig returns the configuration for the HTTP server
 func GetHTTPDConfig() httpd.Conf {
 func GetHTTPDConfig() httpd.Conf {
 	return globalConf.HTTPDConfig
 	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
 //GetProviderConf returns the configuration for the data provider
 func GetProviderConf() dataprovider.Config {
 func GetProviderConf() dataprovider.Config {
 	return globalConf.ProviderConf
 	return globalConf.ProviderConf
 }
 }
 
 
+//SetProviderConf sets the configuration for the data provider
+func SetProviderConf(config dataprovider.Config) {
+	globalConf.ProviderConf = config
+}
+
 func getRedactedGlobalConf() globalConfig {
 func getRedactedGlobalConf() globalConfig {
 	conf := globalConf
 	conf := globalConf
 	conf.ProviderConf.Password = "[redacted]"
 	conf.ProviderConf.Password = "[redacted]"
@@ -141,7 +156,7 @@ func LoadConfig(configDir, configName string) error {
 		globalConf.SFTPD.Banner = defaultBanner
 		globalConf.SFTPD.Banner = defaultBanner
 	}
 	}
 	if globalConf.SFTPD.UploadMode < 0 || globalConf.SFTPD.UploadMode > 2 {
 	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)
 		globalConf.SFTPD.UploadMode = 0
 		globalConf.SFTPD.UploadMode = 0
 		logger.Warn(logSender, "", "Configuration error: %v", err)
 		logger.Warn(logSender, "", "Configuration error: %v", err)

+ 21 - 0
config/config_test.go

@@ -97,3 +97,24 @@ func TestInvalidUploadMode(t *testing.T) {
 	}
 	}
 	os.Remove(configFilePath)
 	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")
+	}
+}

+ 1 - 1
dataprovider/bolt.go

@@ -318,7 +318,7 @@ func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) {
 	bucket := tx.Bucket(usersBucket)
 	bucket := tx.Bucket(usersBucket)
 	idxBucket := tx.Bucket(usersIDIdxBucket)
 	idxBucket := tx.Bucket(usersIDIdxBucket)
 	if bucket == nil || idxBucket == nil {
 	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
 	return bucket, idxBucket, err
 }
 }

+ 1 - 1
dataprovider/dataprovider.go

@@ -180,7 +180,7 @@ func Initialize(cnf Config, basePath string) error {
 	} else if config.Driver == BoltDataProviderName {
 	} else if config.Driver == BoltDataProviderName {
 		err = initializeBoltProvider(basePath)
 		err = initializeBoltProvider(basePath)
 	} else {
 	} else {
-		err = fmt.Errorf("Unsupported data provider: %v", config.Driver)
+		err = fmt.Errorf("unsupported data provider: %v", config.Driver)
 	}
 	}
 	if err == nil {
 	if err == nil {
 		startAvailabilityTimer()
 		startAvailabilityTimer()

+ 76 - 2
service/service.go

@@ -2,12 +2,20 @@
 package service
 package service
 
 
 import (
 import (
+	"fmt"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
 	"github.com/drakkan/sftpgo/config"
 	"github.com/drakkan/sftpgo/config"
 	"github.com/drakkan/sftpgo/dataprovider"
 	"github.com/drakkan/sftpgo/dataprovider"
 	"github.com/drakkan/sftpgo/httpd"
 	"github.com/drakkan/sftpgo/httpd"
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/sftpd"
 	"github.com/drakkan/sftpgo/sftpd"
 	"github.com/drakkan/sftpgo/utils"
 	"github.com/drakkan/sftpgo/utils"
+	"github.com/rs/xid"
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog"
 )
 )
 
 
@@ -15,6 +23,10 @@ const (
 	logSender = "service"
 	logSender = "service"
 )
 )
 
 
+var (
+	chars = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
+)
+
 // Service defines the SFTPGo service
 // Service defines the SFTPGo service
 type Service struct {
 type Service struct {
 	ConfigDir     string
 	ConfigDir     string
@@ -25,6 +37,8 @@ type Service struct {
 	LogMaxAge     int
 	LogMaxAge     int
 	LogCompress   bool
 	LogCompress   bool
 	LogVerbose    bool
 	LogVerbose    bool
+	PortableMode  int
+	PortableUser  dataprovider.User
 	Shutdown      chan bool
 	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 "+
 	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,
 		"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)
 		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()
 	providerConf := config.GetProviderConf()
 
 
 	err := dataprovider.Initialize(providerConf, s.ConfigDir)
 	err := dataprovider.Initialize(providerConf, s.ConfigDir)
@@ -53,6 +70,15 @@ func (s *Service) Start() error {
 	sftpdConf := config.GetSFTPDConfig()
 	sftpdConf := config.GetSFTPDConfig()
 	httpdConf := config.GetHTTPDConfig()
 	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)
 	sftpd.SetDataProvider(dataProvider)
 
 
 	go func() {
 	go func() {
@@ -76,7 +102,14 @@ func (s *Service) Start() error {
 		}()
 		}()
 	} else {
 	} else {
 		logger.Debug(logSender, "", "HTTP server not started, disabled in config file")
 		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
 	return nil
 }
 }
@@ -91,3 +124,44 @@ func (s *Service) Stop() {
 	close(s.Shutdown)
 	close(s.Shutdown)
 	logger.Debug(logSender, "", "Service stopped")
 	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()
+}

+ 2 - 2
sftpd/server.go

@@ -329,7 +329,7 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro
 	if !filepath.IsAbs(user.HomeDir) {
 	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",
 		logger.Warn(logSender, "", "user %v has invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
 			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 _, err := os.Stat(user.HomeDir); os.IsNotExist(err) {
 	if _, err := os.Stat(user.HomeDir); os.IsNotExist(err) {
 		err := os.MkdirAll(user.HomeDir, 0777)
 		err := os.MkdirAll(user.HomeDir, 0777)
@@ -345,7 +345,7 @@ func loginUser(user dataprovider.User, loginType string) (*ssh.Permissions, erro
 		if activeSessions >= user.MaxSessions {
 		if activeSessions >= user.MaxSessions {
 			logger.Debug(logSender, "", "authentication refused for user: %v, too many open sessions: %v/%v", user.Username,
 			logger.Debug(logSender, "", "authentication refused for user: %v, too many open sessions: %v/%v", user.Username,
 				activeSessions, user.MaxSessions)
 				activeSessions, user.MaxSessions)
-			return nil, fmt.Errorf("Too many open sessions: %v", activeSessions)
+			return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
sftpd/transfer.go

@@ -59,7 +59,7 @@ func (t *Transfer) WriteAt(p []byte, off int64) (n int, err error) {
 	t.lastActivity = time.Now()
 	t.lastActivity = time.Now()
 	if off < t.minWriteOffset {
 	if off < t.minWriteOffset {
 		logger.Warn(logSender, t.connectionID, "Invalid write offset %v minimum valid value %v", 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)
 	written, e := t.file.WriteAt(p, off)
 	t.bytesReceived += int64(written)
 	t.bytesReceived += int64(written)