mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 14:20:55 +03:00
add portable mode
Portable mode is a convenient way to share a single directory on demand
This commit is contained in:
26
README.md
26
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
cmd/portable.go
Normal file
69
cmd/portable.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user