mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 14:50:55 +03:00
@@ -44,6 +44,7 @@ It can serve local filesystem, S3 (compatible) Object Storage, Google Cloud Stor
|
|||||||
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
|
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
|
||||||
- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts.
|
- Easy [migration](./examples/rest-api-cli#convert-users-from-other-stores) from Linux system user accounts.
|
||||||
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
|
- [Portable mode](./docs/portable-mode.md): a convenient way to share a single directory on demand.
|
||||||
|
- [SFTP subsystem mode](./docs/sftp-subsystem.md): you can use SFTPGo as OpenSSH's SFTP subsystem.
|
||||||
- Performance analysis using built-in [profiler](./docs/profiling.md).
|
- Performance analysis using built-in [profiler](./docs/profiling.md).
|
||||||
- Configuration format is at your choice: JSON, TOML, YAML, HCL, envfile are supported.
|
- Configuration format is at your choice: JSON, TOML, YAML, HCL, envfile are supported.
|
||||||
- Log files are accurate and they are saved in the easily parsable JSON format ([more information](./docs/logs.md)).
|
- Log files are accurate and they are saved in the easily parsable JSON format ([more information](./docs/logs.md)).
|
||||||
|
|||||||
167
cmd/startsubsys.go
Normal file
167
cmd/startsubsys.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/xid"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/common"
|
||||||
|
"github.com/drakkan/sftpgo/config"
|
||||||
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/logger"
|
||||||
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
|
"github.com/drakkan/sftpgo/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logJournalD = false
|
||||||
|
preserveHomeDir = false
|
||||||
|
subsystemCmd = &cobra.Command{
|
||||||
|
Use: "startsubsys",
|
||||||
|
Short: "Use SFTPGo as SFTP file transfer subsystem",
|
||||||
|
Long: `In this mode SFTPGo speaks the server side of SFTP protocol to stdout and
|
||||||
|
expects client requests from stdin.
|
||||||
|
This mode is not intended to be called directly, but from sshd using the
|
||||||
|
Subsystem option.
|
||||||
|
For example adding a line like this one in "/etc/ssh/sshd_config":
|
||||||
|
|
||||||
|
Subsystem sftp sftpgo startsubsys
|
||||||
|
|
||||||
|
Command-line flags should be specified in the Subsystem declaration.
|
||||||
|
`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logSender := "startsubsys"
|
||||||
|
connectionID := xid.New().String()
|
||||||
|
logLevel := zerolog.DebugLevel
|
||||||
|
if !logVerbose {
|
||||||
|
logLevel = zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
if logJournalD {
|
||||||
|
logger.InitJournalDLogger(logLevel)
|
||||||
|
} else {
|
||||||
|
logger.InitStdErrLogger(logLevel)
|
||||||
|
}
|
||||||
|
osUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to get the current user: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
username := osUser.Username
|
||||||
|
homedir := osUser.HomeDir
|
||||||
|
logger.Info(logSender, connectionID, "starting SFTPGo %v as subsystem, user %#v home dir %#v config dir %#v",
|
||||||
|
version.Get(), username, homedir, configDir)
|
||||||
|
err = config.LoadConfig(configDir, configFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to load configuration: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
commonConfig := config.GetCommonConfig()
|
||||||
|
// idle connection are managed externally
|
||||||
|
commonConfig.IdleTimeout = 0
|
||||||
|
config.SetCommonConfig(commonConfig)
|
||||||
|
common.Initialize(config.GetCommonConfig())
|
||||||
|
dataProviderConf := config.GetProviderConf()
|
||||||
|
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
|
||||||
|
logger.Debug(logSender, connectionID, "data provider %#v not supported in subsystem mode, using %#v provider",
|
||||||
|
dataProviderConf.Driver, dataprovider.MemoryDataProviderName)
|
||||||
|
dataProviderConf.Driver = dataprovider.MemoryDataProviderName
|
||||||
|
dataProviderConf.Name = ""
|
||||||
|
dataProviderConf.PreferDatabaseCredentials = true
|
||||||
|
}
|
||||||
|
config.SetProviderConf(dataProviderConf)
|
||||||
|
err = dataprovider.Initialize(dataProviderConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
httpConfig := config.GetHTTPConfig()
|
||||||
|
httpConfig.Initialize(configDir)
|
||||||
|
user, err := dataprovider.UserExists(username)
|
||||||
|
if err == nil {
|
||||||
|
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||||
|
// update the user
|
||||||
|
user.HomeDir = filepath.Clean(homedir)
|
||||||
|
err = dataprovider.UpdateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to update user %#v: %v", username, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.Username = username
|
||||||
|
user.HomeDir = homedir
|
||||||
|
user.Password = connectionID
|
||||||
|
user.Permissions = make(map[string][]string)
|
||||||
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
|
err = dataprovider.AddUser(user)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to add user %#v: %v", username, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = sftpd.ServeSubSystemConnection(user, connectionID, os.Stdin, os.Stdout)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
logger.Warn(logSender, connectionID, "serving subsystem finished with error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info(logSender, connectionID, "serving subsystem finished")
|
||||||
|
os.Exit(0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
subsystemCmd.Flags().BoolVarP(&preserveHomeDir, "preserve-home", "p", false, `If the user already exists, the existing home
|
||||||
|
directory will not be changed`)
|
||||||
|
subsystemCmd.Flags().BoolVarP(&logJournalD, "log-to-journald", "j", false, `Send logs to journald. Only available on Linux.
|
||||||
|
Use:
|
||||||
|
|
||||||
|
$ journalctl -o verbose -f
|
||||||
|
|
||||||
|
To see full logs.
|
||||||
|
If not set, the logs will be sent to the standard
|
||||||
|
error`)
|
||||||
|
viper.SetDefault(configDirKey, defaultConfigDir)
|
||||||
|
viper.BindEnv(configDirKey, "SFTPGO_CONFIG_DIR") //nolint:errcheck // err is not nil only if the key to bind is missing
|
||||||
|
subsystemCmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey),
|
||||||
|
`Location for SFTPGo config dir. This directory
|
||||||
|
should contain the "sftpgo" configuration file
|
||||||
|
or the configured config-file and it is used as
|
||||||
|
the base for files with a relative path (eg. the
|
||||||
|
private keys for the SFTP server, the SQLite
|
||||||
|
database if you use SQLite as data provider).
|
||||||
|
This flag can be set using SFTPGO_CONFIG_DIR
|
||||||
|
env var too.`)
|
||||||
|
viper.BindPFlag(configDirKey, subsystemCmd.Flags().Lookup(configDirFlag)) //nolint:errcheck
|
||||||
|
|
||||||
|
viper.SetDefault(configFileKey, defaultConfigName)
|
||||||
|
viper.BindEnv(configFileKey, "SFTPGO_CONFIG_FILE") //nolint:errcheck
|
||||||
|
subsystemCmd.Flags().StringVarP(&configFile, configFileFlag, "f", viper.GetString(configFileKey),
|
||||||
|
`Name for SFTPGo configuration file. It must be
|
||||||
|
the name of a file stored in config-dir not the
|
||||||
|
absolute path to the configuration file. The
|
||||||
|
specified file name must have no extension we
|
||||||
|
automatically load JSON, YAML, TOML, HCL and
|
||||||
|
Java properties. Therefore if you set "sftpgo"
|
||||||
|
then "sftpgo.json", "sftpgo.yaml" and so on
|
||||||
|
are searched.
|
||||||
|
This flag can be set using SFTPGO_CONFIG_FILE
|
||||||
|
env var too.`)
|
||||||
|
viper.BindPFlag(configFileKey, subsystemCmd.Flags().Lookup(configFileFlag)) //nolint:errcheck
|
||||||
|
|
||||||
|
viper.SetDefault(logVerboseKey, defaultLogVerbose)
|
||||||
|
viper.BindEnv(logVerboseKey, "SFTPGO_LOG_VERBOSE") //nolint:errcheck
|
||||||
|
subsystemCmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey),
|
||||||
|
`Enable verbose logs. This flag can be set
|
||||||
|
using SFTPGO_LOG_VERBOSE env var too.
|
||||||
|
`)
|
||||||
|
viper.BindPFlag(logVerboseKey, subsystemCmd.Flags().Lookup(logVerboseFlag)) //nolint:errcheck
|
||||||
|
|
||||||
|
rootCmd.AddCommand(subsystemCmd)
|
||||||
|
}
|
||||||
@@ -384,9 +384,11 @@ func Initialize(cnf Config, basePath string) error {
|
|||||||
if err = validateHooks(); err != nil {
|
if err = validateHooks(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !cnf.PreferDatabaseCredentials {
|
||||||
if err = validateCredentialsDir(basePath); err != nil {
|
if err = validateCredentialsDir(basePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
err = createProvider(basePath)
|
err = createProvider(basePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
57
docs/sftp-subsystem.md
Normal file
57
docs/sftp-subsystem.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# SFTP subsystem mode
|
||||||
|
|
||||||
|
In this mode SFTPGo speaks the server side of SFTP protocol to stdout and expects client requests from stdin.
|
||||||
|
You can use SFTPGo as subsystem via the `startsubsys` command.
|
||||||
|
This mode is not intended to be called directly, but from sshd using the `Subsystem` option.
|
||||||
|
For example adding a line like this one in `/etc/ssh/sshd_config`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
Subsystem sftp sftpgo startsubsys
|
||||||
|
```
|
||||||
|
|
||||||
|
Command-line flags should be specified in the Subsystem declaration.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
Usage:
|
||||||
|
sftpgo startsubsys [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-c, --config-dir string Location for SFTPGo config dir. This directory
|
||||||
|
should contain the "sftpgo" configuration file
|
||||||
|
or the configured config-file and it is used as
|
||||||
|
the base for files with a relative path (eg. the
|
||||||
|
private keys for the SFTP server, the SQLite
|
||||||
|
database if you use SQLite as data provider).
|
||||||
|
This flag can be set using SFTPGO_CONFIG_DIR
|
||||||
|
env var too. (default ".")
|
||||||
|
-f, --config-file string Name for SFTPGo configuration file. It must be
|
||||||
|
the name of a file stored in config-dir not the
|
||||||
|
absolute path to the configuration file. The
|
||||||
|
specified file name must have no extension we
|
||||||
|
automatically load JSON, YAML, TOML, HCL and
|
||||||
|
Java properties. Therefore if you set "sftpgo"
|
||||||
|
then "sftpgo.json", "sftpgo.yaml" and so on
|
||||||
|
are searched.
|
||||||
|
This flag can be set using SFTPGO_CONFIG_FILE
|
||||||
|
env var too. (default "sftpgo")
|
||||||
|
-h, --help help for startsubsys
|
||||||
|
-j, --log-to-journald Send logs to journald. Only available on Linux.
|
||||||
|
Use:
|
||||||
|
|
||||||
|
$ journalctl -o verbose -f
|
||||||
|
|
||||||
|
To see full logs.
|
||||||
|
If not set, the logs will be sent to the standard
|
||||||
|
error
|
||||||
|
-v, --log-verbose Enable verbose logs. This flag can be set
|
||||||
|
using SFTPGO_LOG_VERBOSE env var too.
|
||||||
|
(default true)
|
||||||
|
-p, --preserve-home If the user already exists, the existing home
|
||||||
|
directory will not be changed
|
||||||
|
```
|
||||||
|
|
||||||
|
In this mode `bolt` and `sqlite` providers are not usable as the same database file cannot be shared among multiple processes, if one of these provider is configured it will be automatically changed to `memory` provider.
|
||||||
|
|
||||||
|
The username and home directory for the logged in user are determined using [user.Current()](https://golang.org/pkg/os/user/#Current).
|
||||||
|
If the user who is logging is not found within the SFTPGo data provider, it is added automatically.
|
||||||
|
You can pre-configure the users inside the SFTPGo data provider, this way you can use a different home directory, restrict permissions and such.
|
||||||
@@ -58,7 +58,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, ela
|
|||||||
Int("resp_status", status).
|
Int("resp_status", status).
|
||||||
Int("resp_size", bytes).
|
Int("resp_size", bytes).
|
||||||
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
|
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panic logs panics
|
// Panic logs panics
|
||||||
@@ -69,5 +69,5 @@ func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
|||||||
Fields(l.fields).
|
Fields(l.fields).
|
||||||
Str("stack", string(stack)).
|
Str("stack", string(stack)).
|
||||||
Str("panic", fmt.Sprintf("%+v", v)).
|
Str("panic", fmt.Sprintf("%+v", v)).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.sum
1
go.sum
@@ -106,6 +106,7 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
|||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
|||||||
14
logger/journald.go
Normal file
14
logger/journald.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/journald"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitJournalDLogger configures the logger to write to journald
|
||||||
|
func InitJournalDLogger(level zerolog.Level) {
|
||||||
|
logger = zerolog.New(journald.NewJournalDWriter()).Level(level)
|
||||||
|
consoleLogger = zerolog.Nop()
|
||||||
|
}
|
||||||
10
logger/journald_nolinux.go
Normal file
10
logger/journald_nolinux.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import "github.com/rs/zerolog"
|
||||||
|
|
||||||
|
// InitJournalDLogger configures the logger to write to journald
|
||||||
|
func InitJournalDLogger(level zerolog.Level) {
|
||||||
|
InitStdErrLogger(level)
|
||||||
|
}
|
||||||
@@ -67,6 +67,14 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge
|
|||||||
logger = logger.Level(level)
|
logger = logger.Level(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitStdErrLogger configures the logger to write to stderr
|
||||||
|
func InitStdErrLogger(level zerolog.Level) {
|
||||||
|
logger = zerolog.New(&logSyncWrapper{
|
||||||
|
output: os.Stderr,
|
||||||
|
}).Level(level)
|
||||||
|
consoleLogger = zerolog.Nop()
|
||||||
|
}
|
||||||
|
|
||||||
// DisableLogger disable the main logger.
|
// DisableLogger disable the main logger.
|
||||||
// ConsoleLogger will not be affected
|
// ConsoleLogger will not be affected
|
||||||
func DisableLogger() {
|
func DisableLogger() {
|
||||||
@@ -163,7 +171,7 @@ func TransferLog(operation string, path string, elapsed int64, size int64, user
|
|||||||
Str("file_path", path).
|
Str("file_path", path).
|
||||||
Str("connection_id", connectionID).
|
Str("connection_id", connectionID).
|
||||||
Str("protocol", protocol).
|
Str("protocol", protocol).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandLog logs an SFTP/SCP/SSH command
|
// CommandLog logs an SFTP/SCP/SSH command
|
||||||
@@ -184,7 +192,7 @@ func CommandLog(command, path, target, user, fileMode, connectionID, protocol st
|
|||||||
Str("ssh_command", sshCommand).
|
Str("ssh_command", sshCommand).
|
||||||
Str("connection_id", connectionID).
|
Str("connection_id", connectionID).
|
||||||
Str("protocol", protocol).
|
Str("protocol", protocol).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionFailedLog logs failed attempts to initialize a connection.
|
// ConnectionFailedLog logs failed attempts to initialize a connection.
|
||||||
@@ -200,7 +208,7 @@ func ConnectionFailedLog(user, ip, loginType, protocol, errorString string) {
|
|||||||
Str("login_type", loginType).
|
Str("login_type", loginType).
|
||||||
Str("protocol", protocol).
|
Str("protocol", protocol).
|
||||||
Str("error", errorString).
|
Str("error", errorString).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLogFilePathValid(logFilePath string) bool {
|
func isLogFilePathValid(logFilePath string) bool {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, ela
|
|||||||
Int("resp_status", status).
|
Int("resp_status", status).
|
||||||
Int("resp_size", bytes).
|
Int("resp_size", bytes).
|
||||||
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
|
Int64("elapsed_ms", elapsed.Nanoseconds()/1000000).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panic logs panics
|
// Panic logs panics
|
||||||
@@ -75,5 +75,5 @@ func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) {
|
|||||||
Fields(l.fields).
|
Fields(l.fields).
|
||||||
Str("stack", string(stack)).
|
Str("stack", string(stack)).
|
||||||
Str("panic", fmt.Sprintf("%+v", v)).
|
Str("panic", fmt.Sprintf("%+v", v)).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/common"
|
"github.com/drakkan/sftpgo/common"
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
@@ -23,7 +22,7 @@ type Connection struct {
|
|||||||
ClientVersion string
|
ClientVersion string
|
||||||
// Remote address for this connection
|
// Remote address for this connection
|
||||||
RemoteAddr net.Addr
|
RemoteAddr net.Addr
|
||||||
channel ssh.Channel
|
channel io.ReadWriteCloser
|
||||||
command string
|
command string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1814,3 +1814,35 @@ func TestRecursiveCopyErrors(t *testing.T) {
|
|||||||
err = sshCmd.checkRecursiveCopyPermissions("adir", "another", "/another")
|
err = sshCmd.checkRecursiveCopyPermissions("adir", "another", "/another")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSFTPSubSystem(t *testing.T) {
|
||||||
|
permissions := make(map[string][]string)
|
||||||
|
permissions["/"] = []string{dataprovider.PermAny}
|
||||||
|
user := dataprovider.User{
|
||||||
|
Permissions: permissions,
|
||||||
|
HomeDir: os.TempDir(),
|
||||||
|
}
|
||||||
|
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||||
|
err := ServeSubSystemConnection(user, "connID", nil, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
user.FsConfig.Provider = dataprovider.LocalFilesystemProvider
|
||||||
|
|
||||||
|
buf := make([]byte, 0, 4096)
|
||||||
|
stdErrBuf := make([]byte, 0, 4096)
|
||||||
|
mockSSHChannel := &MockChannel{
|
||||||
|
Buffer: bytes.NewBuffer(buf),
|
||||||
|
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
|
||||||
|
}
|
||||||
|
// this is 327680 and it will result in packet too long error
|
||||||
|
_, err = mockSSHChannel.Write([]byte{0x00, 0x05, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ServeSubSystemConnection(user, "id", mockSSHChannel, mockSSHChannel)
|
||||||
|
assert.EqualError(t, err, "packet too long")
|
||||||
|
|
||||||
|
subsystemChannel := newSubsystemChannel(mockSSHChannel, mockSSHChannel)
|
||||||
|
n, err := subsystemChannel.Write([]byte{0x00})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, n, 1)
|
||||||
|
err = subsystemChannel.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
|||||||
common.TransferDownload, 0, 0, 0, false, c.connection.Fs)
|
common.TransferDownload, 0, 0, 0, false, c.connection.Fs)
|
||||||
transfer := newTransfer(baseTransfer, nil, nil, nil)
|
transfer := newTransfer(baseTransfer, nil, nil, nil)
|
||||||
|
|
||||||
w, e := transfer.copyFromReaderToWriter(c.connection.channel.Stderr(), stderr)
|
w, e := transfer.copyFromReaderToWriter(c.connection.channel.(ssh.Channel).Stderr(), stderr)
|
||||||
c.connection.Log(logger.LevelDebug, "command: %#v, copy from sdterr to remote command ended, written: %v err: %v",
|
c.connection.Log(logger.LevelDebug, "command: %#v, copy from sdterr to remote command ended, written: %v err: %v",
|
||||||
c.connection.command, w, e)
|
c.connection.command, w, e)
|
||||||
// os.ErrClosed means that the command is finished so we don't need to do anything
|
// os.ErrClosed means that the command is finished so we don't need to do anything
|
||||||
@@ -707,7 +707,7 @@ func (c *sshCommand) sendExitStatus(err error) {
|
|||||||
exitStatus := sshSubsystemExitStatus{
|
exitStatus := sshSubsystemExitStatus{
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus)) //nolint:errcheck
|
c.connection.channel.(ssh.Channel).SendRequest("exit-status", false, ssh.Marshal(&exitStatus)) //nolint:errcheck
|
||||||
c.connection.channel.Close()
|
c.connection.channel.Close()
|
||||||
// for scp we notify single uploads/downloads
|
// for scp we notify single uploads/downloads
|
||||||
if c.command != scpCmdName {
|
if c.command != scpCmdName {
|
||||||
|
|||||||
63
sftpd/subsystem.go
Normal file
63
sftpd/subsystem.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package sftpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/common"
|
||||||
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type subsystemChannel struct {
|
||||||
|
reader io.Reader
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsystemChannel) Read(p []byte) (int, error) {
|
||||||
|
return s.reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsystemChannel) Write(p []byte) (int, error) {
|
||||||
|
return s.writer.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsystemChannel) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubsystemChannel(reader io.Reader, writer io.Writer) *subsystemChannel {
|
||||||
|
return &subsystemChannel{
|
||||||
|
reader: reader,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeSubSystemConnection handles a connection as SSH subsystem
|
||||||
|
func ServeSubSystemConnection(user dataprovider.User, connectionID string, reader io.Reader, writer io.Writer) error {
|
||||||
|
fs, err := user.GetFilesystem(connectionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fs.CheckRootPath(user.Username, user.GetUID(), user.GetGID())
|
||||||
|
dataprovider.UpdateLastLogin(user) //nolint:errcheck
|
||||||
|
|
||||||
|
connection := &Connection{
|
||||||
|
BaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolSFTP, user, fs),
|
||||||
|
ClientVersion: "",
|
||||||
|
RemoteAddr: &net.IPAddr{},
|
||||||
|
channel: newSubsystemChannel(reader, writer),
|
||||||
|
}
|
||||||
|
common.Connections.Add(connection)
|
||||||
|
defer common.Connections.Remove(connection.GetID())
|
||||||
|
|
||||||
|
server := sftp.NewRequestServer(connection.channel, sftp.Handlers{
|
||||||
|
FileGet: connection,
|
||||||
|
FilePut: connection,
|
||||||
|
FileCmd: connection,
|
||||||
|
FileList: connection,
|
||||||
|
}, sftp.WithRSAllocator())
|
||||||
|
|
||||||
|
return server.Serve()
|
||||||
|
}
|
||||||
@@ -255,7 +255,7 @@ func writeLog(r *http.Request, err error) {
|
|||||||
Str("sender", logSender).
|
Str("sender", logSender).
|
||||||
Fields(fields).
|
Fields(fields).
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("")
|
Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRemoteAddress(r *http.Request) {
|
func checkRemoteAddress(r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user