add portable mode

Portable mode is a convenient way to share a single directory on demand
This commit is contained in:
Nicola Murino 2019-10-24 18:50:35 +02:00
parent d970e757eb
commit a4cddf4f7f
9 changed files with 213 additions and 8 deletions

View file

@ -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:

69
cmd/portable.go Normal file
View file

@ -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)
}

View file

@ -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)

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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()

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)