add support for multiple bindings

Fixes #253
This commit is contained in:
Nicola Murino 2020-12-23 16:12:30 +01:00
parent 743b350fdd
commit c69d63c1f8
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
24 changed files with 1173 additions and 269 deletions

View file

@ -88,6 +88,7 @@ var (
ErrQuotaExceeded = errors.New("denying write due to space limit")
ErrSkipPermissionsCheck = errors.New("permission check skipped")
ErrConnectionDenied = errors.New("You are not allowed to connect")
ErrNoBinding = errors.New("No binding configured")
errNoTransfer = errors.New("requested transfer not found")
errTransferMismatch = errors.New("transfer mismatch")
)

View file

@ -3,7 +3,9 @@ package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/viper"
@ -33,9 +35,24 @@ const (
)
var (
globalConf globalConfig
defaultSFTPDBanner = fmt.Sprintf("SFTPGo_%v", version.Get().Version)
defaultFTPDBanner = fmt.Sprintf("SFTPGo %v ready", version.Get().Version)
globalConf globalConfig
defaultSFTPDBanner = fmt.Sprintf("SFTPGo_%v", version.Get().Version)
defaultFTPDBanner = fmt.Sprintf("SFTPGo %v ready", version.Get().Version)
defaultSFTPDBinding = sftpd.Binding{
Address: "",
Port: 2022,
ApplyProxyConfig: true,
}
defaultFTPDBinding = ftpd.Binding{
Address: "",
Port: 0,
ApplyProxyConfig: true,
}
defaultWebDAVDBinding = webdavd.Binding{
Address: "",
Port: 0,
EnableHTTPS: false,
}
)
type globalConfig struct {
@ -75,8 +92,7 @@ func Init() {
},
SFTPD: sftpd.Configuration{
Banner: defaultSFTPDBanner,
BindPort: 2022,
BindAddress: "",
Bindings: []sftpd.Binding{defaultSFTPDBinding},
MaxAuthTries: 0,
HostKeys: []string{},
KexAlgorithms: []string{},
@ -89,12 +105,10 @@ func Init() {
PasswordAuthentication: true,
},
FTPD: ftpd.Configuration{
BindPort: 0,
BindAddress: "",
Bindings: []ftpd.Binding{defaultFTPDBinding},
Banner: defaultFTPDBanner,
BannerFile: "",
ActiveTransfersPortNon20: false,
ForcePassiveIP: "",
ActiveTransfersPortNon20: true,
PassivePortRange: ftpd.PortRange{
Start: 50000,
End: 50100,
@ -103,8 +117,7 @@ func Init() {
CertificateKeyFile: "",
},
WebDAVD: webdavd.Configuration{
BindPort: 0,
BindAddress: "",
Bindings: []webdavd.Binding{defaultWebDAVDBinding},
CertificateFile: "",
CertificateKeyFile: "",
Cors: webdavd.Cors{
@ -291,13 +304,13 @@ func SetTelemetryConfig(config telemetry.Conf) {
// HasServicesToStart returns true if the config defines at least a service to start.
// Supported services are SFTP, FTP and WebDAV
func HasServicesToStart() bool {
if globalConf.SFTPD.BindPort > 0 {
if globalConf.SFTPD.ShouldBind() {
return true
}
if globalConf.FTPD.BindPort > 0 {
if globalConf.FTPD.ShouldBind() {
return true
}
if globalConf.WebDAVD.BindPort > 0 {
if globalConf.WebDAVD.ShouldBind() {
return true
}
return false
@ -340,6 +353,8 @@ func LoadConfig(configDir, configFile string) error {
logger.WarnToConsole("error parsing configuration file: %v", err)
return err
}
// viper only supports slice of strings from env vars, so we use our custom method
loadBindingsFromEnv()
checkCommonParamsCompatibility()
if strings.TrimSpace(globalConf.SFTPD.Banner) == "" {
globalConf.SFTPD.Banner = defaultSFTPDBanner
@ -426,6 +441,199 @@ func checkCommonParamsCompatibility() {
}
}
func checkSFTPDBindingsCompatibility() {
if len(globalConf.SFTPD.Bindings) > 0 {
return
}
// we copy deprecated fields to new ones to keep backward compatibility so lint is disabled
binding := sftpd.Binding{
ApplyProxyConfig: true,
}
if globalConf.SFTPD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.SFTPD.BindPort //nolint:staticcheck
}
if globalConf.SFTPD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.SFTPD.BindAddress //nolint:staticcheck
}
globalConf.SFTPD.Bindings = append(globalConf.SFTPD.Bindings, binding)
}
func checkFTPDBindingCompatibility() {
if len(globalConf.FTPD.Bindings) > 0 {
return
}
binding := ftpd.Binding{
ApplyProxyConfig: true,
}
if globalConf.FTPD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.FTPD.BindPort //nolint:staticcheck
}
if globalConf.FTPD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.FTPD.BindAddress //nolint:staticcheck
}
if globalConf.FTPD.TLSMode > 0 { //nolint:staticcheck
binding.TLSMode = globalConf.FTPD.TLSMode //nolint:staticcheck
}
if globalConf.FTPD.ForcePassiveIP != "" { //nolint:staticcheck
binding.ForcePassiveIP = globalConf.FTPD.ForcePassiveIP //nolint:staticcheck
}
globalConf.FTPD.Bindings = append(globalConf.FTPD.Bindings, binding)
}
func checkWebDAVDBindingCompatibility() {
if len(globalConf.WebDAVD.Bindings) > 0 {
return
}
binding := webdavd.Binding{
EnableHTTPS: globalConf.WebDAVD.CertificateFile != "" && globalConf.WebDAVD.CertificateKeyFile != "",
}
if globalConf.WebDAVD.BindPort > 0 { //nolint:staticcheck
binding.Port = globalConf.WebDAVD.BindPort //nolint:staticcheck
}
if globalConf.WebDAVD.BindAddress != "" { //nolint:staticcheck
binding.Address = globalConf.WebDAVD.BindAddress //nolint:staticcheck
}
globalConf.WebDAVD.Bindings = append(globalConf.WebDAVD.Bindings, binding)
}
func loadBindingsFromEnv() {
checkSFTPDBindingsCompatibility()
checkFTPDBindingCompatibility()
checkWebDAVDBindingCompatibility()
maxBindings := make([]int, 10)
for idx := range maxBindings {
getSFTPDBindindFromEnv(idx)
getFTPDBindingFromEnv(idx)
getWebDAVDBindingFromEnv(idx)
}
}
func getSFTPDBindindFromEnv(idx int) {
binding := sftpd.Binding{}
if len(globalConf.SFTPD.Bindings) > idx {
binding = globalConf.SFTPD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
if ok {
binding.ApplyProxyConfig = applyProxyConfig
isSet = true
}
if isSet {
if len(globalConf.SFTPD.Bindings) > idx {
globalConf.SFTPD.Bindings[idx] = binding
} else {
globalConf.SFTPD.Bindings = append(globalConf.SFTPD.Bindings, binding)
}
}
}
func getFTPDBindingFromEnv(idx int) {
binding := ftpd.Binding{}
if len(globalConf.FTPD.Bindings) > idx {
binding = globalConf.FTPD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
applyProxyConfig, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__APPLY_PROXY_CONFIG", idx))
if ok {
binding.ApplyProxyConfig = applyProxyConfig
isSet = true
}
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
if ok {
binding.TLSMode = tlsMode
isSet = true
}
passiveIP, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__FORCE_PASSIVE_IP", idx))
if ok {
binding.ForcePassiveIP = passiveIP
isSet = true
}
if isSet {
if len(globalConf.FTPD.Bindings) > idx {
globalConf.FTPD.Bindings[idx] = binding
} else {
globalConf.FTPD.Bindings = append(globalConf.FTPD.Bindings, binding)
}
}
}
func getWebDAVDBindingFromEnv(idx int) {
binding := webdavd.Binding{}
if len(globalConf.WebDAVD.Bindings) > idx {
binding = globalConf.WebDAVD.Bindings[idx]
}
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx))
if ok {
binding.Port = port
isSet = true
}
address, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ADDRESS", idx))
if ok {
binding.Address = address
isSet = true
}
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ENABLE_HTTPS", idx))
if ok {
binding.EnableHTTPS = enableHTTPS
isSet = true
}
if isSet {
if len(globalConf.WebDAVD.Bindings) > idx {
globalConf.WebDAVD.Bindings[idx] = binding
} else {
globalConf.WebDAVD.Bindings = append(globalConf.WebDAVD.Bindings, binding)
}
}
}
func setViperDefaults() {
viper.SetDefault("common.idle_timeout", globalConf.Common.IdleTimeout)
viper.SetDefault("common.upload_mode", globalConf.Common.UploadMode)
@ -436,8 +644,6 @@ func setViperDefaults() {
viper.SetDefault("common.proxy_allowed", globalConf.Common.ProxyAllowed)
viper.SetDefault("common.post_connect_hook", globalConf.Common.PostConnectHook)
viper.SetDefault("common.max_total_connections", globalConf.Common.MaxTotalConnections)
viper.SetDefault("sftpd.bind_port", globalConf.SFTPD.BindPort)
viper.SetDefault("sftpd.bind_address", globalConf.SFTPD.BindAddress)
viper.SetDefault("sftpd.max_auth_tries", globalConf.SFTPD.MaxAuthTries)
viper.SetDefault("sftpd.banner", globalConf.SFTPD.Banner)
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
@ -449,19 +655,13 @@ func setViperDefaults() {
viper.SetDefault("sftpd.enabled_ssh_commands", globalConf.SFTPD.EnabledSSHCommands)
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
viper.SetDefault("ftpd.bind_port", globalConf.FTPD.BindPort)
viper.SetDefault("ftpd.bind_address", globalConf.FTPD.BindAddress)
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
viper.SetDefault("ftpd.banner_file", globalConf.FTPD.BannerFile)
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
viper.SetDefault("ftpd.force_passive_ip", globalConf.FTPD.ForcePassiveIP)
viper.SetDefault("ftpd.passive_port_range.start", globalConf.FTPD.PassivePortRange.Start)
viper.SetDefault("ftpd.passive_port_range.end", globalConf.FTPD.PassivePortRange.End)
viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile)
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
viper.SetDefault("ftpd.tls_mode", globalConf.FTPD.TLSMode)
viper.SetDefault("webdavd.bind_port", globalConf.WebDAVD.BindPort)
viper.SetDefault("webdavd.bind_address", globalConf.WebDAVD.BindAddress)
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
viper.SetDefault("webdavd.certificate_key_file", globalConf.WebDAVD.CertificateKeyFile)
viper.SetDefault("webdavd.cors.enabled", globalConf.WebDAVD.Cors.Enabled)
@ -523,3 +723,27 @@ func setViperDefaults() {
viper.SetDefault("telemetry.certificate_file", globalConf.TelemetryConfig.CertificateFile)
viper.SetDefault("telemetry.certificate_key_file", globalConf.TelemetryConfig.CertificateKeyFile)
}
func lookupBoolFromEnv(envName string) (bool, bool) {
value, ok := os.LookupEnv(envName)
if ok {
converted, err := strconv.ParseBool(value)
if err == nil {
return converted, ok
}
}
return false, false
}
func lookupIntFromEnv(envName string) (int, bool) {
value, ok := os.LookupEnv(envName)
if ok {
converted, err := strconv.ParseInt(value, 10, 16)
if err == nil {
return int(converted), ok
}
}
return 0, false
}

View file

@ -10,6 +10,7 @@ import (
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/config"
@ -19,6 +20,7 @@ import (
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/webdavd"
)
const (
@ -331,38 +333,300 @@ func TestServiceToStart(t *testing.T) {
assert.NoError(t, err)
assert.True(t, config.HasServicesToStart())
sftpdConf := config.GetSFTPDConfig()
sftpdConf.BindPort = 0
sftpdConf.Bindings[0].Port = 0
config.SetSFTPDConfig(sftpdConf)
assert.False(t, config.HasServicesToStart())
ftpdConf := config.GetFTPDConfig()
ftpdConf.BindPort = 2121
ftpdConf.Bindings[0].Port = 2121
config.SetFTPDConfig(ftpdConf)
assert.True(t, config.HasServicesToStart())
ftpdConf.BindPort = 0
ftpdConf.Bindings[0].Port = 0
config.SetFTPDConfig(ftpdConf)
webdavdConf := config.GetWebDAVDConfig()
webdavdConf.BindPort = 9000
webdavdConf.Bindings[0].Port = 9000
config.SetWebDAVDConfig(webdavdConf)
assert.True(t, config.HasServicesToStart())
webdavdConf.BindPort = 0
webdavdConf.Bindings[0].Port = 0
config.SetWebDAVDConfig(webdavdConf)
assert.False(t, config.HasServicesToStart())
sftpdConf.BindPort = 2022
sftpdConf.Bindings[0].Port = 2022
config.SetSFTPDConfig(sftpdConf)
assert.True(t, config.HasServicesToStart())
}
//nolint:dupl
func TestSFTPDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
require.Len(t, sftpdConf.Bindings, 1)
sftpdConf.Bindings = nil
sftpdConf.BindPort = 9022 //nolint:staticcheck
sftpdConf.BindAddress = "127.0.0.1" //nolint:staticcheck
c := make(map[string]sftpd.Configuration)
c["sftpd"] = sftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
// even if there is no binding configuration in sftpd conf we load the default
require.Len(t, sftpdConf.Bindings, 1)
require.Equal(t, 2022, sftpdConf.Bindings[0].Port)
require.Empty(t, sftpdConf.Bindings[0].Address)
require.True(t, sftpdConf.Bindings[0].ApplyProxyConfig)
// now set the global value to nil and reload the configuration
// this time we should get the values setted using the deprecated configuration
sftpdConf.Bindings = nil
sftpdConf.BindPort = 2022 //nolint:staticcheck
sftpdConf.BindAddress = "" //nolint:staticcheck
config.SetSFTPDConfig(sftpdConf)
require.Nil(t, config.GetSFTPDConfig().Bindings)
require.Equal(t, 2022, config.GetSFTPDConfig().BindPort) //nolint:staticcheck
require.Empty(t, config.GetSFTPDConfig().BindAddress) //nolint:staticcheck
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
sftpdConf = config.GetSFTPDConfig()
require.Len(t, sftpdConf.Bindings, 1)
require.Equal(t, 9022, sftpdConf.Bindings[0].Port)
require.Equal(t, "127.0.0.1", sftpdConf.Bindings[0].Address)
require.True(t, sftpdConf.Bindings[0].ApplyProxyConfig)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
//nolint:dupl
func TestFTPDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
ftpdConf := config.GetFTPDConfig()
require.Len(t, ftpdConf.Bindings, 1)
ftpdConf.Bindings = nil
ftpdConf.BindPort = 9022 //nolint:staticcheck
ftpdConf.BindAddress = "127.1.0.1" //nolint:staticcheck
ftpdConf.ForcePassiveIP = "127.1.1.1" //nolint:staticcheck
ftpdConf.TLSMode = 2 //nolint:staticcheck
c := make(map[string]ftpd.Configuration)
c["ftpd"] = ftpdConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
ftpdConf = config.GetFTPDConfig()
// even if there is no binding configuration in ftpd conf we load the default
require.Len(t, ftpdConf.Bindings, 1)
require.Equal(t, 0, ftpdConf.Bindings[0].Port)
require.Empty(t, ftpdConf.Bindings[0].Address)
require.True(t, ftpdConf.Bindings[0].ApplyProxyConfig)
// now set the global value to nil and reload the configuration
// this time we should get the values setted using the deprecated configuration
ftpdConf.Bindings = nil
ftpdConf.BindPort = 0 //nolint:staticcheck
ftpdConf.BindAddress = "" //nolint:staticcheck
config.SetFTPDConfig(ftpdConf)
require.Nil(t, config.GetFTPDConfig().Bindings)
require.Equal(t, 0, config.GetFTPDConfig().BindPort) //nolint:staticcheck
require.Empty(t, config.GetFTPDConfig().BindAddress) //nolint:staticcheck
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
ftpdConf = config.GetFTPDConfig()
require.Len(t, ftpdConf.Bindings, 1)
require.Equal(t, 9022, ftpdConf.Bindings[0].Port)
require.Equal(t, "127.1.0.1", ftpdConf.Bindings[0].Address)
require.True(t, ftpdConf.Bindings[0].ApplyProxyConfig)
require.Equal(t, 2, ftpdConf.Bindings[0].TLSMode)
require.Equal(t, "127.1.1.1", ftpdConf.Bindings[0].ForcePassiveIP)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
//nolint:dupl
func TestWebDAVDBindingsCompatibility(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
webdavConf := config.GetWebDAVDConfig()
require.Len(t, webdavConf.Bindings, 1)
webdavConf.Bindings = nil
webdavConf.BindPort = 9080 //nolint:staticcheck
webdavConf.BindAddress = "127.0.0.1" //nolint:staticcheck
c := make(map[string]webdavd.Configuration)
c["webdavd"] = webdavConf
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = ioutil.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
webdavConf = config.GetWebDAVDConfig()
// even if there is no binding configuration in webdav conf we load the default
require.Len(t, webdavConf.Bindings, 1)
require.Equal(t, 0, webdavConf.Bindings[0].Port)
require.Empty(t, webdavConf.Bindings[0].Address)
require.False(t, webdavConf.Bindings[0].EnableHTTPS)
// now set the global value to nil and reload the configuration
// this time we should get the values setted using the deprecated configuration
webdavConf.Bindings = nil
webdavConf.BindPort = 10080 //nolint:staticcheck
webdavConf.BindAddress = "" //nolint:staticcheck
config.SetWebDAVDConfig(webdavConf)
require.Nil(t, config.GetWebDAVDConfig().Bindings)
require.Equal(t, 10080, config.GetWebDAVDConfig().BindPort) //nolint:staticcheck
require.Empty(t, config.GetWebDAVDConfig().BindAddress) //nolint:staticcheck
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
webdavConf = config.GetWebDAVDConfig()
require.Len(t, webdavConf.Bindings, 1)
require.Equal(t, 9080, webdavConf.Bindings[0].Port)
require.Equal(t, "127.0.0.1", webdavConf.Bindings[0].Address)
require.False(t, webdavConf.Bindings[0].EnableHTTPS)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestSFTPDBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__PORT", "2200")
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "false")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__PORT", "2203")
os.Setenv("SFTPGO_SFTPD__BINDINGS__3__APPLY_PROXY_CONFIG", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__ADDRESS")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__PORT")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__3__APPLY_PROXY_CONFIG")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetSFTPDConfig().Bindings
require.Len(t, bindings, 2)
require.Equal(t, 2200, bindings[0].Port)
require.Equal(t, "127.0.0.1", bindings[0].Address)
require.False(t, bindings[0].ApplyProxyConfig)
require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig)
}
func TestFTPDBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PORT", "2200")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "f")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP", "127.0.1.2")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__PORT", "2203")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG", "t")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE", "1")
os.Setenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP", "127.0.1.1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ADDRESS")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__PORT")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__FORCE_PASSIVE_IP")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetFTPDConfig().Bindings
require.Len(t, bindings, 2)
require.Equal(t, 2200, bindings[0].Port)
require.Equal(t, "127.0.0.1", bindings[0].Address)
require.False(t, bindings[0].ApplyProxyConfig)
require.Equal(t, bindings[0].TLSMode, 2)
require.Equal(t, bindings[0].ForcePassiveIP, "127.0.1.2")
require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig)
require.Equal(t, bindings[1].TLSMode, 1)
require.Equal(t, bindings[1].ForcePassiveIP, "127.0.1.1")
}
func TestWebDAVBindingsFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT", "8000")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
})
configDir := ".."
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
bindings := config.GetWebDAVDConfig().Bindings
require.Len(t, bindings, 3)
require.Equal(t, 0, bindings[0].Port)
require.Empty(t, bindings[0].Address)
require.False(t, bindings[0].EnableHTTPS)
require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS)
require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address)
require.True(t, bindings[2].EnableHTTPS)
}
func TestConfigFromEnv(t *testing.T) {
reset()
os.Setenv("SFTPGO_SFTPD__BIND_ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS", "127.0.0.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT", "12000")
os.Setenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS", "41")
os.Setenv("SFTPGO_DATA_PROVIDER__POOL_SIZE", "10")
os.Setenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON", "add")
os.Setenv("SFTPGO_KMS__SECRETS__URL", "local")
os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_SFTPD__BIND_ADDRESS")
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS")
os.Unsetenv("SFTPGO_DATA_PROVIDER__POOL_SIZE")
os.Unsetenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON")
@ -372,7 +636,8 @@ func TestConfigFromEnv(t *testing.T) {
err := config.LoadConfig(".", "invalid config")
assert.NoError(t, err)
sftpdConfig := config.GetSFTPDConfig()
assert.Equal(t, "127.0.0.1", sftpdConfig.BindAddress)
assert.Equal(t, "127.0.0.1", sftpdConfig.Bindings[0].Address)
assert.Equal(t, 12000, config.GetWebDAVDConfig().Bindings[0].Port)
dataProviderConf := config.GetProviderConf()
assert.Equal(t, uint32(41), dataProviderConf.PasswordHashing.Argon2Options.Iterations)
assert.Equal(t, 10, dataProviderConf.PoolSize)

View file

@ -65,8 +65,12 @@ The configuration file contains the following sections:
- `post_connect_hook`, string. Absolute path to the command to execute or HTTP URL to notify. See [Post connect hook](./post-connect-hook.md) for more details. Leave empty to disable
- `max_total_connections`, integer. Maximum number of concurrent client connections. 0 means unlimited
- **"sftpd"**, the configuration for the SFTP server
- `bind_port`, integer. The port used for serving SFTP requests. 0 means disabled. Default: 2022
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: ""
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving SFTP requests. 0 means disabled. Default: 2022
- `address`, string. Leave blank to listen on all available network interfaces. Default: ""
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true`
- `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings`
- `idle_timeout`, integer. Deprecated, please use the same key in `common` section.
- `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts is unlimited. If set to zero, the number of attempts is limited to 6.
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
@ -87,21 +91,31 @@ The configuration file contains the following sections:
- `proxy_protocol`, integer. Deprecated, please use the same key in `common` section.
- `proxy_allowed`, list of strings. Deprecated, please use the same key in `common` section.
- **"ftpd"**, the configuration for the FTP server
- `bind_port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0.
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0
- `address`, string. Leave blank to listen on all available network interfaces. Default: ""
- `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Default `true`
- `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
- `force_passive_ip`, ip address. External IP address to expose for passive connections. Leavy empty to autodetect. Defaut: "".
- `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings`
- `banner`, string. Greeting banner displayed when a connection first comes in. Leave empty to use the default banner. Default `SFTPGo <version> ready`, for example `SFTPGo 1.0.0-dev ready`.
- `banner_file`, path to the banner file. The contents of the specified file, if any, are displayed when someone connects to the server. It can be a path relative to the config dir or an absolute one. If set, it overrides the banner string provided by the `banner` option. Leave empty to disable.
- `active_transfers_port_non_20`, boolean. Do not impose the port 20 for active data transfers. Enabling this option allows to run SFTPGo with less privilege. Default: false.
- `force_passive_ip`, ip address. External IP address to expose for passive connections. Leavy empty to autodetect. Defaut: "".
- `force_passive_ip`, ip address. Deprecated, please use `bindings`
- `passive_port_range`, struct containing the key `start` and `end`. Port Range for data connections. Random if not specified. Default range is 50000-50100.
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided the server will accept both plain FTP an explicit FTP over TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. Do not enable this blindly, please check that a proper TLS config is in place or no login will be allowed if `tls_mode` is 1.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `tls_mode`, integer. Deprecated, please use `bindings`
- **webdavd**, the configuration for the WebDAV server, more info [here](./webdav.md)
- `bind_port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
- `address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`
- `bind_port`, integer. Deprecated, please use `bindings`
- `bind_address`, string. Deprecated, please use `bindings`
- `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
- `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
- `enabled`, boolean, set to true to enable CORS.
- `allowed_origins`, list of strings.
@ -210,7 +224,7 @@ You can also override all the available configuration options using environment
Let's see some examples:
- To set sftpd `bind_port`, you need to define the env var `SFTPGO_SFTPD__BIND_PORT`
- To set the `port` for the first sftpd binding, you need to define the env var `SFTPGO_SFTPD__BINDINGS__0__PORT`
- To set the `execute_on` actions, you need to define the env var `SFTPGO_COMMON__ACTIONS__EXECUTE_ON`. For example `SFTPGO_COMMON__ACTIONS__EXECUTE_ON=upload,download`
## Telemetry Server

View file

@ -7,6 +7,7 @@ import (
ftpserver "github.com/fclairamb/ftpserverlib"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
@ -16,9 +17,54 @@ const (
)
var (
server *Server
certMgr *common.CertManager
serviceStatus ServiceStatus
)
// Binding defines the configuration for a network listener
type Binding struct {
// The address to listen on. A blank value means listen on all available network interfaces.
Address string `json:"address" mapstructure:"address"`
// The port used for serving requests
Port int `json:"port" mapstructure:"port"`
// apply the proxy configuration, if any, for this binding
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
// set to 1 to require TLS for both data and control connection
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
// External IP address to expose for passive connections.
ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"`
}
// GetAddress returns the binding address
func (b *Binding) GetAddress() string {
return fmt.Sprintf("%s:%d", b.Address, b.Port)
}
// IsValid returns true if the binding port is > 0
func (b *Binding) IsValid() bool {
return b.Port > 0
}
// HasProxy returns true if the proxy protocol is active for this binding
func (b *Binding) HasProxy() bool {
return b.ApplyProxyConfig && common.Config.ProxyProtocol > 0
}
// GetTLSDescription returns the TLS mode as string
func (b *Binding) GetTLSDescription() string {
if certMgr == nil {
return "Disabled"
}
switch b.TLSMode {
case 1:
return "Explicit required"
case 2:
return "Implicit"
}
return "Plain and explicit"
}
// PortRange defines a port range
type PortRange struct {
// Range start
@ -30,18 +76,19 @@ type PortRange struct {
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
Bindings []Binding `json:"bindings"`
PassivePortRange PortRange `json:"passive_port_range"`
FTPES string `json:"ftpes"`
}
// Configuration defines the configuration for the ftp server
type Configuration struct {
// The port used for serving FTP requests
// Addresses and ports to bind to
Bindings []Binding `json:"bindings" mapstructure:"bindings"`
// Deprecated: please use Bindings
BindPort int `json:"bind_port" mapstructure:"bind_port"`
// The address to listen on. A blank value means listen on all available network interfaces.
// Deprecated: please use Bindings
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
// External IP address to expose for passive connections.
// Deprecated: please use Bindings
ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"`
// Greeting banner displayed when a connection first comes in
Banner string `json:"banner" mapstructure:"banner"`
@ -58,56 +105,82 @@ type Configuration struct {
ActiveTransfersPortNon20 bool `json:"active_transfers_port_non_20" mapstructure:"active_transfers_port_non_20"`
// Port Range for data connections. Random if not specified
PassivePortRange PortRange `json:"passive_port_range" mapstructure:"passive_port_range"`
// set to 1 to require TLS for both data and control connection
// Deprecated: please use Bindings
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
}
// ShouldBind returns true if there is at least a valid binding
func (c *Configuration) ShouldBind() bool {
for _, binding := range c.Bindings {
if binding.IsValid() {
return true
}
}
return false
}
// Initialize configures and starts the FTP server
func (c *Configuration) Initialize(configDir string) error {
var err error
logger.Debug(logSender, "", "initializing FTP server with config %+v", *c)
server, err = NewServer(c, configDir)
if err != nil {
return err
if !c.ShouldBind() {
return common.ErrNoBinding
}
server.status = ServiceStatus{
IsActive: true,
Address: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
PassivePortRange: c.PassivePortRange,
FTPES: "Disabled",
}
if c.CertificateFile != "" && c.CertificateKeyFile != "" {
if c.TLSMode == 1 {
server.status.FTPES = "Required"
} else {
server.status.FTPES = "Enabled"
certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
if certificateFile != "" && certificateKeyFile != "" {
mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, logSender)
if err != nil {
return err
}
certMgr = mgr
}
ftpServer := ftpserver.NewFtpServer(server)
return ftpServer.ListenAndServe()
serviceStatus = ServiceStatus{
Bindings: nil,
PassivePortRange: c.PassivePortRange,
}
exitChannel := make(chan error)
for idx, binding := range c.Bindings {
if !binding.IsValid() {
continue
}
server := NewServer(c, configDir, binding, idx)
go func(s *Server) {
ftpServer := ftpserver.NewFtpServer(s)
exitChannel <- ftpServer.ListenAndServe()
}(server)
serviceStatus.Bindings = append(serviceStatus.Bindings, binding)
}
serviceStatus.IsActive = true
return <-exitChannel
}
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths
func ReloadTLSCertificate() error {
if server != nil && server.certMgr != nil {
return server.certMgr.LoadCertificate(logSender)
if certMgr != nil {
return certMgr.LoadCertificate(logSender)
}
return nil
}
// GetStatus returns the server status
func GetStatus() ServiceStatus {
if server == nil {
return ServiceStatus{}
}
return server.status
return serviceStatus
}
func getConfigPath(name, configDir string) string {
if !utils.IsFileInputValid(name) {
return ""
}
if len(name) > 0 && !filepath.IsAbs(name) {
if name != "" && !filepath.IsAbs(name) {
return filepath.Join(configDir, name)
}
return name

View file

@ -20,6 +20,7 @@ import (
"github.com/jlaffaye/ftp"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/config"
@ -28,6 +29,7 @@ import (
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/vfs"
)
@ -145,7 +147,11 @@ func TestMain(m *testing.M) {
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "")
ftpdConf := config.GetFTPDConfig()
ftpdConf.BindPort = 2121
ftpdConf.Bindings = []ftpd.Binding{
{
Port: 2121,
},
}
ftpdConf.PassivePortRange.Start = 0
ftpdConf.PassivePortRange.End = 0
ftpdConf.BannerFile = bannerFileName
@ -154,7 +160,11 @@ func TestMain(m *testing.M) {
// required to test sftpfs
sftpdConf := config.GetSFTPDConfig()
sftpdConf.BindPort = 2122
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 2122,
},
}
sftpdConf.HostKeys = []string{filepath.Join(os.TempDir(), "id_ed25519")}
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
@ -190,9 +200,9 @@ func TestMain(m *testing.M) {
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", ftpdConf.BindAddress, ftpdConf.BindPort))
waitTCPListening(ftpdConf.Bindings[0].GetAddress())
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
ftpd.ReloadTLSCertificate() //nolint:errcheck
exitCode := m.Run()
@ -206,16 +216,35 @@ func TestMain(m *testing.M) {
os.Exit(exitCode)
}
func TestInitialization(t *testing.T) {
func TestInitializationFailure(t *testing.T) {
ftpdConf := config.GetFTPDConfig()
ftpdConf.BindPort = 2121
ftpdConf.Bindings = []ftpd.Binding{}
ftpdConf.CertificateFile = filepath.Join(os.TempDir(), "test_ftpd.crt")
ftpdConf.CertificateKeyFile = filepath.Join(os.TempDir(), "test_ftpd.key")
ftpdConf.TLSMode = 1
err := ftpdConf.Initialize(configDir)
assert.Error(t, err)
status := ftpd.GetStatus()
assert.True(t, status.IsActive)
require.EqualError(t, err, common.ErrNoBinding.Error())
ftpdConf.Bindings = []ftpd.Binding{
{
Port: 0,
},
{
Port: 2121,
},
}
ftpdConf.BannerFile = "a-missing-file"
err = ftpdConf.Initialize(configDir)
require.Error(t, err)
ftpdConf.BannerFile = ""
ftpdConf.Bindings[1].TLSMode = 10
err = ftpdConf.Initialize(configDir)
require.Error(t, err)
ftpdConf.CertificateFile = ""
ftpdConf.CertificateKeyFile = ""
ftpdConf.Bindings[1].TLSMode = 1
err = ftpdConf.Initialize(configDir)
require.Error(t, err)
}
func TestBasicFTPHandling(t *testing.T) {

View file

@ -45,7 +45,7 @@ func (c *Connection) GetRemoteAddress() string {
// Disconnect disconnects the client
func (c *Connection) Disconnect() error {
return c.clientContext.Close(ftpserver.StatusServiceNotAvailable, "connection closed")
return c.clientContext.Close(0, "")
}
// GetCommand returns an empty string

View file

@ -120,37 +120,56 @@ func newMockOsFs(err, statErr error, atomicUpload bool, connectionID, rootDir st
}
func TestInitialization(t *testing.T) {
oldMgr := certMgr
certMgr = nil
binding := Binding{
Port: 2121,
}
c := &Configuration{
BindPort: 2121,
Bindings: []Binding{binding},
CertificateFile: "acert",
CertificateKeyFile: "akey",
}
assert.False(t, binding.HasProxy())
assert.Equal(t, "Disabled", binding.GetTLSDescription())
err := c.Initialize(configDir)
assert.Error(t, err)
c.CertificateFile = ""
c.CertificateKeyFile = ""
c.BannerFile = "afile"
server, err := NewServer(c, configDir)
if assert.NoError(t, err) {
assert.Equal(t, "", server.initialMsg)
_, err = server.GetTLSConfig()
assert.Error(t, err)
}
server := NewServer(c, configDir, binding, 0)
assert.Equal(t, "", server.initialMsg)
_, err = server.GetTLSConfig()
assert.Error(t, err)
binding.TLSMode = 1
server = NewServer(c, configDir, binding, 0)
_, err = server.GetSettings()
assert.Error(t, err)
err = ReloadTLSCertificate()
assert.NoError(t, err)
certMgr = oldMgr
}
func TestServerGetSettings(t *testing.T) {
oldConfig := common.Config
binding := Binding{
Port: 2121,
ApplyProxyConfig: true,
}
c := &Configuration{
BindPort: 2121,
Bindings: []Binding{binding},
PassivePortRange: PortRange{
Start: 10000,
End: 11000,
},
}
server, err := NewServer(c, configDir)
assert.NoError(t, err)
assert.False(t, binding.HasProxy())
server := NewServer(c, configDir, binding, 0)
settings, err := server.GetSettings()
assert.NoError(t, err)
assert.Equal(t, 10000, settings.PassiveTransferPortRange.Start)
@ -158,12 +177,21 @@ func TestServerGetSettings(t *testing.T) {
common.Config.ProxyProtocol = 1
common.Config.ProxyAllowed = []string{"invalid"}
assert.True(t, binding.HasProxy())
_, err = server.GetSettings()
assert.Error(t, err)
server.config.BindPort = 8021
server.binding.Port = 8021
_, err = server.GetSettings()
assert.Error(t, err)
assert.Equal(t, "Plain and explicit", binding.GetTLSDescription())
binding.TLSMode = 1
assert.Equal(t, "Explicit required", binding.GetTLSDescription())
binding.TLSMode = 2
assert.Equal(t, "Implicit", binding.GetTLSDescription())
common.Config = oldConfig
}
@ -171,16 +199,18 @@ func TestUserInvalidParams(t *testing.T) {
u := dataprovider.User{
HomeDir: "invalid",
}
binding := Binding{
Port: 2121,
}
c := &Configuration{
BindPort: 2121,
Bindings: []Binding{binding},
PassivePortRange: PortRange{
Start: 10000,
End: 11000,
},
}
server, err := NewServer(c, configDir)
assert.NoError(t, err)
_, err = server.validateUser(u, mockFTPClientContext{})
server := NewServer(c, configDir, binding, 0)
_, err := server.validateUser(u, mockFTPClientContext{})
assert.Error(t, err)
u.Username = "a"

View file

@ -20,31 +20,23 @@ import (
// Server implements the ftpserverlib MainDriver interface
type Server struct {
ID int
config *Configuration
certMgr *common.CertManager
initialMsg string
statusBanner string
status ServiceStatus
binding Binding
}
// NewServer returns a new FTP server driver
func NewServer(config *Configuration, configDir string) (*Server, error) {
var err error
func NewServer(config *Configuration, configDir string, binding Binding, id int) *Server {
server := &Server{
config: config,
certMgr: nil,
initialMsg: config.Banner,
statusBanner: fmt.Sprintf("SFTPGo %v FTP Server", version.Get().Version),
binding: binding,
ID: id,
}
certificateFile := getConfigPath(config.CertificateFile, configDir)
certificateKeyFile := getConfigPath(config.CertificateKeyFile, configDir)
if certificateFile != "" && certificateKeyFile != "" {
server.certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, logSender)
if err != nil {
return server, err
}
}
if len(config.BannerFile) > 0 {
if config.BannerFile != "" {
bannerFilePath := config.BannerFile
if !filepath.IsAbs(bannerFilePath) {
bannerFilePath = filepath.Join(configDir, bannerFilePath)
@ -57,7 +49,7 @@ func NewServer(config *Configuration, configDir string) (*Server, error) {
logger.Warn(logSender, "", "unable to read banner file: %v", err)
}
}
return server, err
return server
}
// GetSettings returns FTP server settings
@ -70,10 +62,10 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
}
}
var ftpListener net.Listener
if common.Config.ProxyProtocol > 0 {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.BindPort))
if common.Config.ProxyProtocol > 0 && s.binding.ApplyProxyConfig {
listener, err := net.Listen("tcp", s.binding.GetAddress())
if err != nil {
logger.Warn(logSender, "", "error starting listener on address %s:%d: %v", s.config.BindAddress, s.config.BindPort, err)
logger.Warn(logSender, "", "error starting listener on address %v: %v", s.binding.GetAddress(), err)
return nil, err
}
ftpListener, err = common.Config.GetProxyListener(listener)
@ -83,16 +75,24 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
}
}
if s.binding.TLSMode < 0 || s.binding.TLSMode > 2 {
return nil, errors.New("unsupported TLS mode")
}
if s.binding.TLSMode > 0 && certMgr == nil {
return nil, errors.New("to enable TLS you need to provide a certificate")
}
return &ftpserver.Settings{
Listener: ftpListener,
ListenAddr: fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.BindPort),
PublicHost: s.config.ForcePassiveIP,
ListenAddr: s.binding.GetAddress(),
PublicHost: s.binding.ForcePassiveIP,
PassiveTransferPortRange: portRange,
ActiveTransferPortNon20: s.config.ActiveTransfersPortNon20,
IdleTimeout: -1,
ConnectionTimeout: 20,
Banner: s.statusBanner,
TLSRequired: ftpserver.TLSRequirement(s.config.TLSMode),
TLSRequired: ftpserver.TLSRequirement(s.binding.TLSMode),
}, nil
}
@ -105,7 +105,7 @@ func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
if err := common.Config.ExecutePostConnectHook(cc.RemoteAddr().String(), common.ProtocolFTP); err != nil {
return "", err
}
connID := fmt.Sprintf("%v", cc.ID())
connID := fmt.Sprintf("%v_%v", s.ID, cc.ID())
user := dataprovider.User{}
connection := &Connection{
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
@ -117,7 +117,7 @@ func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
// ClientDisconnected is called when the user disconnects, even if he never authenticated
func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
connID := fmt.Sprintf("%v_%v", common.ProtocolFTP, cc.ID())
connID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
common.Connections.Remove(connID)
}
@ -146,9 +146,9 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
// GetTLSConfig returns a TLS Certificate to use
func (s *Server) GetTLSConfig() (*tls.Config, error) {
if s.certMgr != nil {
if certMgr != nil {
return &tls.Config{
GetCertificate: s.certMgr.GetCertificateFunc(),
GetCertificate: certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12,
}, nil
}
@ -193,7 +193,7 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
return nil, err
}
connection := &Connection{
BaseConnection: common.NewBaseConnection(fmt.Sprintf("%v", cc.ID()), common.ProtocolFTP, user, fs),
BaseConnection: common.NewBaseConnection(fmt.Sprintf("%v_%v", s.ID, cc.ID()), common.ProtocolFTP, user, fs),
clientContext: cc,
}
err = common.Connections.Swap(connection)

10
go.mod
View file

@ -8,10 +8,10 @@ require (
github.com/Azure/azure-storage-blob-go v0.12.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b
github.com/aws/aws-sdk-go v1.36.10
github.com/aws/aws-sdk-go v1.36.13
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
github.com/fclairamb/ftpserverlib v0.10.1-0.20201218011054-66782562d4fd
github.com/fclairamb/ftpserverlib v0.11.0
github.com/frankban/quicktest v1.11.2 // indirect
github.com/go-chi/chi v1.5.1
github.com/go-chi/render v1.0.1
@ -45,11 +45,11 @@ require (
go.uber.org/automaxprocs v1.3.0
gocloud.dev v0.21.0
gocloud.dev/secrets/hashivault v0.21.0
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/tools v0.0.0-20201215192005-fa10ef0b8743 // indirect
golang.org/x/tools v0.0.0-20201221201019-196535612888 // indirect
google.golang.org/api v0.36.0
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d // indirect
gopkg.in/ini.v1 v1.62.0 // indirect

14
go.sum
View file

@ -105,8 +105,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.36.10 h1:JCBOoVIEJkNcio01MNbmelgOc6+uxANTC3TYYPlcsEE=
github.com/aws/aws-sdk-go v1.36.10/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.36.13 h1:RAyssUwg/yM7q874D2PQuIST6uhhyYFFPJtgVG/OujI=
github.com/aws/aws-sdk-go v1.36.13/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -178,9 +178,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fclairamb/ftpserverlib v0.10.1-0.20201217134934-fdb96115baf4/go.mod h1:lCDfM4WqDDh/wlVMZP185tEH93I0t5gsY2/lKfrLl3o=
github.com/fclairamb/ftpserverlib v0.10.1-0.20201218011054-66782562d4fd h1:HDZpKSw2Iztmj/fPI7vlCYvAYaB5W8OT0RglegRxOEE=
github.com/fclairamb/ftpserverlib v0.10.1-0.20201218011054-66782562d4fd/go.mod h1:lCDfM4WqDDh/wlVMZP185tEH93I0t5gsY2/lKfrLl3o=
github.com/fclairamb/ftpserverlib v0.11.0 h1:leyKdtf3Xk9POY/akxicXrrHF5804PVqnXlUBUYN4Tk=
github.com/fclairamb/ftpserverlib v0.11.0/go.mod h1:lCDfM4WqDDh/wlVMZP185tEH93I0t5gsY2/lKfrLl3o=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@ -746,8 +745,9 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo=
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -825,7 +825,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201215192005-fa10ef0b8743/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201221201019-196535612888/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -2,7 +2,7 @@
// REST API allows to manage users and quota and to get real time reports for the active connections
// with possibility of forcibly closing a connection.
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml
// https://github.com/drakkan/sftpgo/blob/master/httpd/schema/openapi.yaml
// A basic Web interface to manage users and connections is provided too
package httpd

View file

@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: SFTPGo
description: SFTPGo REST API
version: 2.2.2
version: 2.2.3
servers:
- url: /api/v1
@ -1371,26 +1371,70 @@ components:
type: string
fingerprint:
type: string
BaseServiceStatus:
SSHBinding:
type: object
properties:
address:
type: string
description: TCP address the server listen on
port:
type: integer
description: the port used for serving requests
apply_proxy_config:
type: boolean
description: apply the proxy configuration, if any
WebDAVBinding:
type: object
properties:
address:
type: string
description: TCP address the server listen on
port:
type: integer
description: the port used for serving requests
enable_https:
type: boolean
FTPDBinding:
type: object
properties:
address:
type: string
description: TCP address the server listen on
port:
type: integer
description: the port used for serving requests
apply_proxy_config:
type: boolean
description: apply the proxy configuration, if any
tls_mode:
type: integer
enum:
- 0
- 1
- 2
description: >
* `0` - clear or explicit TLS
* `1` - explicit TLS required
* `2` - implicit TLS
force_passive_ip:
type: string
description: External IP address to expose for passive connections
SSHServiceStatus:
type: object
properties:
is_active:
type: boolean
address:
bindings:
type: array
items:
$ref: '#/components/schemas/SSHBinding'
host_keys:
type: array
items:
$ref: '#/components/schemas/SSHHostKey'
ssh_commands:
type: string
description: TCP address the server listen on in the form "host:port"
SSHServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
host_keys:
type: array
items:
$ref: '#/components/schemas/SSHHostKey'
ssh_commands:
type: string
description: accepted SSH commands comma separated
description: accepted SSH commands comma separated
FTPPassivePortRange:
type: object
properties:
@ -1399,28 +1443,25 @@ components:
end:
type: integer
FTPServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
passive_port_range:
$ref: '#/components/schemas/FTPPassivePortRange'
ftpes:
type: string
enum:
- Disabled
- Enabled
- Required
type: object
properties:
is_active:
type: boolean
bindings:
type: array
items:
$ref: '#/components/schemas/FTPDBinding'
passive_port_range:
$ref: '#/components/schemas/FTPPassivePortRange'
WebDAVServiceStatus:
allOf:
- $ref: '#/components/schemas/BaseServiceStatus'
- type: object
properties:
protocol:
type: string
enum:
- HTTP
- HTTPS
type: object
properties:
is_active:
type: boolean
bindings:
type: array
items:
$ref: '#/components/schemas/WebDAVBinding'
DataProviderStatus:
type: object
properties:

View file

@ -129,7 +129,7 @@ func (s *Service) startServices() {
webDavDConf := config.GetWebDAVDConfig()
telemetryConf := config.GetTelemetryConfig()
if sftpdConf.BindPort > 0 {
if sftpdConf.ShouldBind() {
go func() {
logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf)
if err := sftpdConf.Initialize(s.ConfigDir); err != nil {
@ -158,7 +158,7 @@ func (s *Service) startServices() {
logger.DebugToConsole("HTTP server not started, disabled in config file")
}
}
if ftpdConf.BindPort > 0 {
if ftpdConf.ShouldBind() {
go func() {
if err := ftpdConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start FTP server: %v", err)
@ -170,7 +170,7 @@ func (s *Service) startServices() {
} else {
logger.Debug(logSender, "", "FTP server not started, disabled in config file")
}
if webDavDConf.BindPort > 0 {
if webDavDConf.ShouldBind() {
go func() {
if err := webDavDConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start WebDAV server: %v", err)

View file

@ -15,11 +15,13 @@ import (
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/ftpd"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/utils"
"github.com/drakkan/sftpgo/version"
"github.com/drakkan/sftpgo/webdavd"
)
// StartPortableMode starts the service in portable mode
@ -49,13 +51,17 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
config.SetHTTPDConfig(httpdConf)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.MaxAuthTries = 12
sftpdConf.BindPort = sftpdPort
sftpdConf.Bindings = []sftpd.Binding{
{
Port: sftpdPort,
},
}
if sftpdPort >= 0 {
if sftpdPort > 0 {
sftpdConf.BindPort = sftpdPort
sftpdConf.Bindings[0].Port = sftpdPort
} else {
// dynamic ports starts from 49152
sftpdConf.BindPort = 49152 + rand.Intn(15000)
sftpdConf.Bindings[0].Port = 49152 + rand.Intn(15000)
}
if utils.IsStringInSlice("*", enabledSSHCommands) {
sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
@ -67,11 +73,13 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
if ftpPort >= 0 {
ftpConf := config.GetFTPDConfig()
binding := ftpd.Binding{}
if ftpPort > 0 {
ftpConf.BindPort = ftpPort
binding.Port = ftpPort
} else {
ftpConf.BindPort = 49152 + rand.Intn(15000)
binding.Port = 49152 + rand.Intn(15000)
}
ftpConf.Bindings = []ftpd.Binding{binding}
ftpConf.Banner = fmt.Sprintf("SFTPGo portable %v ready", version.Get().Version)
ftpConf.CertificateFile = ftpsCert
ftpConf.CertificateKeyFile = ftpsKey
@ -80,10 +88,11 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
if webdavPort >= 0 {
webDavConf := config.GetWebDAVDConfig()
binding := webdavd.Binding{}
if webdavPort > 0 {
webDavConf.BindPort = webdavPort
binding.Port = webdavPort
} else {
webDavConf.BindPort = 49152 + rand.Intn(15000)
binding.Port = 49152 + rand.Intn(15000)
}
webDavConf.CertificateFile = webDavCert
webDavConf.CertificateKeyFile = webDavKey
@ -106,19 +115,19 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
func (s *Service) getServiceOptionalInfoString() string {
var info strings.Builder
if config.GetSFTPDConfig().BindPort > 0 {
info.WriteString(fmt.Sprintf("SFTP port: %v ", config.GetSFTPDConfig().BindPort))
if config.GetSFTPDConfig().Bindings[0].IsValid() {
info.WriteString(fmt.Sprintf("SFTP port: %v ", config.GetSFTPDConfig().Bindings[0].Port))
}
if config.GetFTPDConfig().BindPort > 0 {
info.WriteString(fmt.Sprintf("FTP port: %v ", config.GetFTPDConfig().BindPort))
if config.GetFTPDConfig().Bindings[0].IsValid() {
info.WriteString(fmt.Sprintf("FTP port: %v ", config.GetFTPDConfig().Bindings[0].Port))
}
if config.GetWebDAVDConfig().BindPort > 0 {
if config.GetWebDAVDConfig().Bindings[0].IsValid() {
scheme := "http"
if config.GetWebDAVDConfig().CertificateFile != "" && config.GetWebDAVDConfig().CertificateKeyFile != "" {
scheme = "https"
}
info.WriteString(fmt.Sprintf("WebDAV URL: %v://<your IP>:%v/%v",
scheme, config.GetWebDAVDConfig().BindPort, s.PortableUser.Username))
scheme, config.GetWebDAVDConfig().Bindings[0].Port, s.PortableUser.Username))
}
return info.String()
}
@ -143,14 +152,14 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
}
}
sftpdConf := config.GetSFTPDConfig()
if sftpdConf.BindPort > 0 {
if sftpdConf.Bindings[0].IsValid() {
mDNSServiceSFTP, err = zeroconf.Register(
fmt.Sprintf("SFTPGo portable %v", sftpdConf.BindPort), // service instance name
"_sftp-ssh._tcp", // service type and protocol
"local.", // service domain
sftpdConf.BindPort, // service port
meta, // service metadata
nil, // register on all network interfaces
fmt.Sprintf("SFTPGo portable %v", sftpdConf.Bindings[0].Port), // service instance name
"_sftp-ssh._tcp", // service type and protocol
"local.", // service domain
sftpdConf.Bindings[0].Port, // service port
meta, // service metadata
nil, // register on all network interfaces
)
if err != nil {
mDNSServiceSFTP = nil
@ -160,12 +169,13 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
}
}
ftpdConf := config.GetFTPDConfig()
if ftpdConf.BindPort > 0 {
if ftpdConf.Bindings[0].IsValid() {
port := ftpdConf.Bindings[0].Port
mDNSServiceFTP, err = zeroconf.Register(
fmt.Sprintf("SFTPGo portable %v", ftpdConf.BindPort),
fmt.Sprintf("SFTPGo portable %v", port),
"_ftp._tcp",
"local.",
ftpdConf.BindPort,
port,
meta,
nil,
)
@ -177,12 +187,12 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
}
}
webdavConf := config.GetWebDAVDConfig()
if webdavConf.BindPort > 0 {
if webdavConf.Bindings[0].IsValid() {
mDNSServiceDAV, err = zeroconf.Register(
fmt.Sprintf("SFTPGo portable %v", webdavConf.BindPort),
fmt.Sprintf("SFTPGo portable %v", webdavConf.Bindings[0].Port),
"_http._tcp",
"local.",
webdavConf.BindPort,
webdavConf.Bindings[0].Port,
meta,
nil,
)

View file

@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/pires/go-proxyproto"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
@ -36,13 +37,40 @@ var (
sftpExtensions = []string{"posix-rename@openssh.com"}
)
// Binding defines the configuration for a network listener
type Binding struct {
// The address to listen on. A blank value means listen on all available network interfaces.
Address string `json:"address" mapstructure:"address"`
// The port used for serving requests
Port int `json:"port" mapstructure:"port"`
// apply the proxy configuration, if any, for this binding
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
}
// GetAddress returns the binding address
func (b *Binding) GetAddress() string {
return fmt.Sprintf("%s:%d", b.Address, b.Port)
}
// IsValid returns true if the binding port is > 0
func (b *Binding) IsValid() bool {
return b.Port > 0
}
// HasProxy returns true if the proxy protocol is active for this binding
func (b *Binding) HasProxy() bool {
return b.ApplyProxyConfig && common.Config.ProxyProtocol > 0
}
// Configuration for the SFTP server
type Configuration struct {
// Identification string used by the server
Banner string `json:"banner" mapstructure:"banner"`
// The port used for serving SFTP requests
// Addresses and ports to bind to
Bindings []Binding `json:"bindings" mapstructure:"bindings"`
// Deprecated: please use Bindings
BindPort int `json:"bind_port" mapstructure:"bind_port"`
// The address to listen on. A blank value means listen on all available network interfaces.
// Deprecated: please use Bindings
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
// Deprecated: please use the same key in common configuration
IdleTimeout int `json:"idle_timeout" mapstructure:"idle_timeout"`
@ -127,6 +155,17 @@ func (e *authenticationError) Error() string {
return fmt.Sprintf("Authentication error: %s", e.err)
}
// ShouldBind returns true if there is at least a valid binding
func (c *Configuration) ShouldBind() bool {
for _, binding := range c.Bindings {
if binding.IsValid() {
return true
}
}
return false
}
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
func (c *Configuration) Initialize(configDir string) error {
serverConfig := &ssh.ServerConfig{
@ -165,6 +204,10 @@ func (c *Configuration) Initialize(configDir string) error {
}
}
if !c.ShouldBind() {
return common.ErrNoBinding
}
if err := c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
serviceStatus.HostKeys = nil
return err
@ -181,22 +224,43 @@ func (c *Configuration) Initialize(configDir string) error {
c.configureLoginBanner(serverConfig, configDir)
c.checkSSHCommands()
addr := fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort)
listener, err := net.Listen("tcp", addr)
if err != nil {
logger.Warn(logSender, "", "error starting listener on address %s:%d: %v", c.BindAddress, c.BindPort, err)
return err
exitChannel := make(chan error)
serviceStatus.Bindings = nil
for _, binding := range c.Bindings {
if !binding.IsValid() {
continue
}
serviceStatus.Bindings = append(serviceStatus.Bindings, binding)
go func(binding Binding) {
exitChannel <- c.listenAndServe(binding, serverConfig)
}(binding)
}
proxyListener, err := common.Config.GetProxyListener(listener)
if err != nil {
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
return err
}
logger.Info(logSender, "", "server listener registered address: %v", listener.Addr().String())
serviceStatus.Address = addr
serviceStatus.IsActive = true
serviceStatus.SSHCommands = strings.Join(c.EnabledSSHCommands, ", ")
return <-exitChannel
}
func (c *Configuration) listenAndServe(binding Binding, serverConfig *ssh.ServerConfig) error {
addr := binding.GetAddress()
listener, err := net.Listen("tcp", addr)
if err != nil {
logger.Warn(logSender, "", "error starting listener on address %v: %v", addr, err)
return err
}
var proxyListener *proxyproto.Listener
if binding.ApplyProxyConfig {
proxyListener, err = common.Config.GetProxyListener(listener)
if err != nil {
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
return err
}
}
logger.Info(logSender, "", "server listener registered, address: %v", listener.Addr().String())
for {
var conn net.Conn
if proxyListener != nil {

View file

@ -38,7 +38,7 @@ type HostKey struct {
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
Bindings []Binding `json:"bindings"`
SSHCommands string `json:"ssh_commands"`
HostKeys []HostKey `json:"host_keys"`
}

View file

@ -190,7 +190,12 @@ func TestMain(m *testing.M) {
sftpdConf := config.GetSFTPDConfig()
httpdConf := config.GetHTTPDConfig()
sftpdConf.BindPort = 2022
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 2022,
ApplyProxyConfig: true,
},
}
sftpdConf.KexAlgorithms = []string{"curve25519-sha256@libssh.org", "ecdh-sha2-nistp256",
"ecdh-sha2-nistp384"}
sftpdConf.Ciphers = []string{"chacha20-poly1305@openssh.com", "aes128-gcm@openssh.com",
@ -250,10 +255,15 @@ func TestMain(m *testing.M) {
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
sftpdConf.BindPort = 2222
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 2222,
ApplyProxyConfig: true,
},
}
sftpdConf.PasswordAuthentication = false
common.Config.ProxyProtocol = 1
go func() {
@ -265,9 +275,14 @@ func TestMain(m *testing.M) {
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
sftpdConf.BindPort = 2224
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 2224,
ApplyProxyConfig: true,
},
}
sftpdConf.PasswordAuthentication = true
common.Config.ProxyProtocol = 2
go func() {
@ -279,7 +294,7 @@ func TestMain(m *testing.M) {
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
getHostKeysFingerprints(sftpdConf.HostKeys)
exitCode := m.Run()
@ -301,7 +316,15 @@ func TestInitialization(t *testing.T) {
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
sftpdConf := config.GetSFTPDConfig()
sftpdConf.BindPort = 2022
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 2022,
ApplyProxyConfig: true,
},
{
Port: 0,
},
}
sftpdConf.LoginBannerFile = "invalid_file"
sftpdConf.EnabledSSHCommands = append(sftpdConf.EnabledSSHCommands, "ls")
err = sftpdConf.Initialize(configDir)
@ -312,9 +335,15 @@ func TestInitialization(t *testing.T) {
sftpdConf.KeyboardInteractiveHook = filepath.Join(homeBasePath, "invalid_file")
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
sftpdConf.BindPort = 4444
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 4444,
ApplyProxyConfig: true,
},
}
common.Config.ProxyProtocol = 1
common.Config.ProxyAllowed = []string{"1270.0.0.1"}
assert.True(t, sftpdConf.Bindings[0].HasProxy())
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
sftpdConf.HostKeys = []string{"missing key"}
@ -324,6 +353,9 @@ func TestInitialization(t *testing.T) {
sftpdConf.TrustedUserCAKeys = []string{"missing ca key"}
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
sftpdConf.Bindings = nil
err = sftpdConf.Initialize(configDir)
assert.EqualError(t, err, common.ErrNoBinding.Error())
}
func TestBasicSFTPHandling(t *testing.T) {

View file

@ -13,8 +13,13 @@
"max_total_connections": 0
},
"sftpd": {
"bind_port": 2022,
"bind_address": "",
"bindings": [
{
"port": 2022,
"address": "",
"apply_proxy_config": true
}
],
"max_auth_tries": 0,
"banner": "",
"host_keys": [],
@ -34,23 +39,33 @@
"password_authentication": true
},
"ftpd": {
"bind_port": 0,
"bind_address": "",
"bindings": [
{
"address": "",
"port": 0,
"apply_proxy_config": true,
"tls_mode": 0,
"force_passive_ip": ""
}
],
"banner": "",
"banner_file": "",
"active_transfers_port_non_20": false,
"force_passive_ip": "",
"active_transfers_port_non_20": true,
"passive_port_range": {
"start": 50000,
"end": 50100
},
"certificate_file": "",
"certificate_key_file": "",
"tls_mode": 0
"certificate_key_file": ""
},
"webdavd": {
"bind_port": 0,
"bind_address": "",
"bindings": [
{
"address": "",
"port": 0,
"enable_https": false
}
],
"certificate_file": "",
"certificate_key_file": "",
"cors": {

View file

@ -11,8 +11,11 @@
Status: {{ if .Status.SSH.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.SSH.IsActive}}
<br>
Address: "{{.Status.SSH.Address}}"
{{range .Status.SSH.Bindings}}
<br>
Address: "{{.GetAddress}}" {{if .HasProxy}}Proxy: ON{{end}}
<br>
{{end}}
Accepted commands: "{{.Status.SSH.SSHCommands}}"
<br>
{{range .Status.SSH.HostKeys}}
@ -34,11 +37,19 @@
Status: {{ if .Status.FTP.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.FTP.IsActive}}
<br>
Address: "{{.Status.FTP.Address}}"
{{range .Status.FTP.Bindings}}
<br>
Address: "{{.GetAddress}}" {{if .HasProxy}}Proxy: ON{{end}}
<br>
TLS: "{{.GetTLSDescription}}"
{{if .ForcePassiveIP}}
<br>
PassiveIP: {{.ForcePassiveIP}}
{{end}}
<br>
{{end}}
<br>
Passive port range: "{{.Status.FTP.PassivePortRange.Start}}-{{.Status.FTP.PassivePortRange.End}}"
<br>
TLS: "{{.Status.FTP.FTPES}}"
{{end}}
</p>
</div>
@ -51,9 +62,13 @@
Status: {{ if .Status.WebDAV.IsActive}}"Started"{{else}}"Stopped"{{end}}
{{if .Status.WebDAV.IsActive}}
<br>
Address: "{{.Status.WebDAV.Address}}"
{{range .Status.WebDAV.Bindings}}
<br>
Protocol: "{{.Status.WebDAV.Protocol}}"
Address: "{{.GetAddress}}"
<br>
Protocol: {{if .EnableHTTPS}} HTTPS {{else}} HTTP {{end}}
<br>
{{end}}
{{end}}
</p>
</div>

View file

@ -159,7 +159,11 @@ func TestUserInvalidParams(t *testing.T) {
HomeDir: "invalid",
}
c := &Configuration{
BindPort: 9000,
Bindings: []Binding{
{
Port: 9000,
},
},
}
server, err := newServer(c, configDir)
assert.NoError(t, err)
@ -670,7 +674,11 @@ func TestBasicUsersCache(t *testing.T) {
assert.NoError(t, err)
c := &Configuration{
BindPort: 9000,
Bindings: []Binding{
{
Port: 9000,
},
},
Cache: Cache{
Users: UsersCacheConfig{
MaxSize: 50,
@ -782,7 +790,11 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
assert.NoError(t, err)
c := &Configuration{
BindPort: 9000,
Bindings: []Binding{
{
Port: 9000,
},
},
Cache: Cache{
Users: UsersCacheConfig{
MaxSize: 3,
@ -926,7 +938,11 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
func TestRecoverer(t *testing.T) {
c := &Configuration{
BindPort: 9000,
Bindings: []Binding{
{
Port: 9000,
},
},
}
server, err := newServer(c, configDir)
assert.NoError(t, err)

View file

@ -53,13 +53,9 @@ func newServer(config *Configuration, configDir string) (*webDavServer, error) {
return server, nil
}
func (s *webDavServer) listenAndServe() error {
addr := fmt.Sprintf("%s:%d", s.config.BindAddress, s.config.BindPort)
s.status.IsActive = true
s.status.Address = addr
s.status.Protocol = "HTTP"
func (s *webDavServer) listenAndServe(binding Binding) error {
httpServer := &http.Server{
Addr: addr,
Addr: binding.GetAddress(),
Handler: server,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
@ -76,17 +72,17 @@ func (s *webDavServer) listenAndServe() error {
OptionsPassthrough: true,
})
httpServer.Handler = c.Handler(server)
} else {
httpServer.Handler = server
}
if s.certMgr != nil {
s.status.Protocol = "HTTPS"
if s.certMgr != nil && binding.EnableHTTPS {
server.status.Bindings = append(server.status.Bindings, binding)
httpServer.TLSConfig = &tls.Config{
GetCertificate: s.certMgr.GetCertificateFunc(),
MinVersion: tls.VersionTLS12,
}
return httpServer.ListenAndServeTLS("", "")
}
binding.EnableHTTPS = false
server.status.Bindings = append(server.status.Bindings, binding)
return httpServer.ListenAndServe()
}

View file

@ -2,8 +2,10 @@
package webdavd
import (
"fmt"
"path/filepath"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
@ -25,9 +27,8 @@ var (
// ServiceStatus defines the service status
type ServiceStatus struct {
IsActive bool `json:"is_active"`
Address string `json:"address"`
Protocol string `json:"protocol"`
IsActive bool `json:"is_active"`
Bindings []Binding `json:"bindings"`
}
// Cors configuration
@ -59,11 +60,33 @@ type Cache struct {
MimeTypes MimeCacheConfig `json:"mime_types" mapstructure:"mime_types"`
}
// Binding defines the configuration for a network listener
type Binding struct {
// The address to listen on. A blank value means listen on all available network interfaces.
Address string `json:"address" mapstructure:"address"`
// The port used for serving requests
Port int `json:"port" mapstructure:"port"`
// you also need to provide a certificate for enabling HTTPS
EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
}
// GetAddress returns the binding address
func (b *Binding) GetAddress() string {
return fmt.Sprintf("%s:%d", b.Address, b.Port)
}
// IsValid returns true if the binding port is > 0
func (b *Binding) IsValid() bool {
return b.Port > 0
}
// Configuration defines the configuration for the WevDAV server
type Configuration struct {
// The port used for serving FTP requests
// Addresses and ports to bind to
Bindings []Binding `json:"bindings" mapstructure:"bindings"`
// Deprecated: please use Bindings
BindPort int `json:"bind_port" mapstructure:"bind_port"`
// The address to listen on. A blank value means listen on all available network interfaces.
// Deprecated: please use Bindings
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
// If files containing a certificate and matching private key for the server are provided the server will expect
// HTTPS connections.
@ -85,6 +108,17 @@ func GetStatus() ServiceStatus {
return server.status
}
// ShouldBind returns true if there is at least a valid binding
func (c *Configuration) ShouldBind() bool {
for _, binding := range c.Bindings {
if binding.IsValid() {
return true
}
}
return false
}
// Initialize configures and starts the WebDAV server
func (c *Configuration) Initialize(configDir string) error {
var err error
@ -96,11 +130,31 @@ func (c *Configuration) Initialize(configDir string) error {
if !c.Cache.MimeTypes.Enabled {
mimeTypeCache.maxSize = 0
}
if !c.ShouldBind() {
return common.ErrNoBinding
}
server, err = newServer(c, configDir)
if err != nil {
return err
}
return server.listenAndServe()
server.status.Bindings = nil
exitChannel := make(chan error)
for _, binding := range c.Bindings {
if !binding.IsValid() {
continue
}
go func(binding Binding) {
exitChannel <- server.listenAndServe(binding)
}(binding)
}
server.status.IsActive = true
return <-exitChannel
}
// ReloadTLSCertificate reloads the TLS certificate and key from the configured paths

View file

@ -32,6 +32,7 @@ import (
"github.com/drakkan/sftpgo/httpd"
"github.com/drakkan/sftpgo/kms"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/drakkan/sftpgo/vfs"
"github.com/drakkan/sftpgo/webdavd"
)
@ -143,11 +144,19 @@ func TestMain(m *testing.M) {
// required to test sftpfs
sftpdConf := config.GetSFTPDConfig()
sftpdConf.BindPort = 9022
sftpdConf.Bindings = []sftpd.Binding{
{
Port: 9022,
},
}
sftpdConf.HostKeys = []string{filepath.Join(os.TempDir(), "id_ecdsa")}
webDavConf := config.GetWebDAVDConfig()
webDavConf.BindPort = webDavServerPort
webDavConf.Bindings = []webdavd.Binding{
{
Port: webDavServerPort,
},
}
webDavConf.Cors = webdavd.Cors{
Enabled: true,
AllowedOrigins: []string{"*"},
@ -196,9 +205,9 @@ func TestMain(m *testing.M) {
}
}()
waitTCPListening(fmt.Sprintf("%s:%d", webDavConf.BindAddress, webDavConf.BindPort))
waitTCPListening(webDavConf.Bindings[0].GetAddress())
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
waitTCPListening(fmt.Sprintf("%s:%d", sftpdConf.BindAddress, sftpdConf.BindPort))
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
webdavd.ReloadTLSCertificate() //nolint:errcheck
exitCode := m.Run()
@ -213,7 +222,15 @@ func TestMain(m *testing.M) {
func TestInitialization(t *testing.T) {
cfg := webdavd.Configuration{
BindPort: 1234,
Bindings: []webdavd.Binding{
{
Port: 1234,
EnableHTTPS: true,
},
{
Port: 0,
},
},
CertificateFile: "missing path",
CertificateKeyFile: "bad path",
}
@ -221,13 +238,21 @@ func TestInitialization(t *testing.T) {
assert.Error(t, err)
cfg.Cache = config.GetWebDAVDConfig().Cache
cfg.BindPort = webDavServerPort
cfg.Bindings[0].Port = webDavServerPort
cfg.CertificateFile = certPath
cfg.CertificateKeyFile = keyPath
err = cfg.Initialize(configDir)
assert.Error(t, err)
err = webdavd.ReloadTLSCertificate()
assert.NoError(t, err)
cfg.Bindings = []webdavd.Binding{
{
Port: 0,
},
}
err = cfg.Initialize(configDir)
assert.EqualError(t, err, common.ErrNoBinding.Error())
}
func TestBasicHandling(t *testing.T) {