mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
add portable mode
Portable mode is a convenient way to share a single directory on demand
This commit is contained in:
parent
d970e757eb
commit
a4cddf4f7f
9 changed files with 213 additions and 8 deletions
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.
|
||||
- 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
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue