mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
parent
3c16a19269
commit
141ca6777c
9 changed files with 116 additions and 7 deletions
|
@ -2,9 +2,14 @@ package common
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
// CertManager defines a TLS certificate manager
|
||||
|
@ -12,7 +17,8 @@ type CertManager struct {
|
|||
certPath string
|
||||
keyPath string
|
||||
sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
cert *tls.Certificate
|
||||
rootCAs *x509.CertPool
|
||||
}
|
||||
|
||||
// LoadCertificate loads the configured x509 key pair
|
||||
|
@ -39,6 +45,44 @@ func (m *CertManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Cert
|
|||
}
|
||||
}
|
||||
|
||||
// GetRootCAs returns the set of root certificate authorities that servers
|
||||
// use if required to verify a client certificate
|
||||
func (m *CertManager) GetRootCAs() *x509.CertPool {
|
||||
return m.rootCAs
|
||||
}
|
||||
|
||||
// LoadRootCAs tries to load root CA certificate authorities from the given paths
|
||||
func (m *CertManager) LoadRootCAs(caCertificates []string, configDir string) error {
|
||||
if len(caCertificates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rootCAs := x509.NewCertPool()
|
||||
|
||||
for _, rootCA := range caCertificates {
|
||||
if !utils.IsFileInputValid(rootCA) {
|
||||
return fmt.Errorf("invalid root CA certificate %#v", rootCA)
|
||||
}
|
||||
if rootCA != "" && !filepath.IsAbs(rootCA) {
|
||||
rootCA = filepath.Join(configDir, rootCA)
|
||||
}
|
||||
crt, err := ioutil.ReadFile(rootCA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rootCAs.AppendCertsFromPEM(crt) {
|
||||
logger.Debug(logSender, "", "TLS certificate authority %#v successfully loaded", rootCA)
|
||||
} else {
|
||||
err := fmt.Errorf("unable to load TLS certificate authority %#v", rootCA)
|
||||
logger.Debug(logSender, "", "%v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
m.rootCAs = rootCAs
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCertManager creates a new certificate manager
|
||||
func NewCertManager(certificateFile, certificateKeyFile, logSender string) (*CertManager, error) {
|
||||
manager := &CertManager{
|
||||
|
|
|
@ -56,6 +56,25 @@ func TestLoadCertificate(t *testing.T) {
|
|||
assert.Equal(t, certManager.cert, cert)
|
||||
}
|
||||
|
||||
err = certManager.LoadRootCAs(nil, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = certManager.LoadRootCAs([]string{""}, "")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = certManager.LoadRootCAs([]string{"invalid"}, "")
|
||||
assert.Error(t, err)
|
||||
|
||||
// laoding the key as root CA must fail
|
||||
err = certManager.LoadRootCAs([]string{keyPath}, "")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = certManager.LoadRootCAs([]string{certPath}, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
rootCa := certManager.GetRootCAs()
|
||||
assert.NotNil(t, rootCa)
|
||||
|
||||
err = os.Remove(certPath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(keyPath)
|
||||
|
|
|
@ -49,9 +49,10 @@ var (
|
|||
ApplyProxyConfig: true,
|
||||
}
|
||||
defaultWebDAVDBinding = webdavd.Binding{
|
||||
Address: "",
|
||||
Port: 0,
|
||||
EnableHTTPS: false,
|
||||
Address: "",
|
||||
Port: 0,
|
||||
EnableHTTPS: false,
|
||||
ClientAuthType: 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -124,6 +125,7 @@ func Init() {
|
|||
Bindings: []webdavd.Binding{defaultWebDAVDBinding},
|
||||
CertificateFile: "",
|
||||
CertificateKeyFile: "",
|
||||
CACertificates: []string{},
|
||||
Cors: webdavd.Cors{
|
||||
Enabled: false,
|
||||
AllowedOrigins: []string{},
|
||||
|
@ -629,6 +631,12 @@ func getWebDAVDBindingFromEnv(idx int) {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx))
|
||||
if ok {
|
||||
binding.ClientAuthType = clientAuthType
|
||||
isSet = true
|
||||
}
|
||||
|
||||
if isSet {
|
||||
if len(globalConf.WebDAVD.Bindings) > idx {
|
||||
globalConf.WebDAVD.Bindings[idx] = binding
|
||||
|
@ -672,6 +680,7 @@ func setViperDefaults() {
|
|||
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
|
||||
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
|
||||
viper.SetDefault("webdavd.certificate_key_file", globalConf.WebDAVD.CertificateKeyFile)
|
||||
viper.SetDefault("webdavd.ca_certificates", globalConf.WebDAVD.CACertificates)
|
||||
viper.SetDefault("webdavd.cors.enabled", globalConf.WebDAVD.Cors.Enabled)
|
||||
viper.SetDefault("webdavd.cors.allowed_origins", globalConf.WebDAVD.Cors.AllowedOrigins)
|
||||
viper.SetDefault("webdavd.cors.allowed_methods", globalConf.WebDAVD.Cors.AllowedMethods)
|
||||
|
|
|
@ -589,6 +589,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
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")
|
||||
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
|
||||
|
@ -596,6 +597,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
|
||||
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE")
|
||||
})
|
||||
|
||||
configDir := ".."
|
||||
|
@ -612,6 +614,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 9000, bindings[2].Port)
|
||||
require.Equal(t, "127.0.1.1", bindings[2].Address)
|
||||
require.True(t, bindings[2].EnableHTTPS)
|
||||
require.Equal(t, 1, bindings[2].ClientAuthType)
|
||||
}
|
||||
|
||||
func TestConfigFromEnv(t *testing.T) {
|
||||
|
|
|
@ -116,10 +116,12 @@ The configuration file contains the following sections:
|
|||
- `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`
|
||||
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to basic auth. You need to define at least a certificate authority for this to work. Default: 0.
|
||||
- `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. 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.
|
||||
- `ca_certificates`, list of strings. Set of root certificate authorities to use to verify client certificates.
|
||||
- `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.
|
||||
|
|
|
@ -67,11 +67,13 @@
|
|||
{
|
||||
"address": "",
|
||||
"port": 0,
|
||||
"enable_https": false
|
||||
"enable_https": false,
|
||||
"client_auth_type": 0
|
||||
}
|
||||
],
|
||||
"certificate_file": "",
|
||||
"certificate_key_file": "",
|
||||
"ca_certificates": [],
|
||||
"cors": {
|
||||
"enabled": false,
|
||||
"allowed_origins": [],
|
||||
|
|
|
@ -44,11 +44,14 @@ func newServer(config *Configuration, configDir string) (*webDavServer, error) {
|
|||
}
|
||||
certificateFile := getConfigPath(config.CertificateFile, configDir)
|
||||
certificateKeyFile := getConfigPath(config.CertificateKeyFile, configDir)
|
||||
if len(certificateFile) > 0 && len(certificateKeyFile) > 0 {
|
||||
if certificateFile != "" && certificateKeyFile != "" {
|
||||
server.certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, logSender)
|
||||
if err != nil {
|
||||
return server, err
|
||||
}
|
||||
if err := server.certMgr.LoadRootCAs(config.CACertificates, configDir); err != nil {
|
||||
return server, err
|
||||
}
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
@ -79,6 +82,10 @@ func (s *webDavServer) listenAndServe(binding Binding) error {
|
|||
GetCertificate: s.certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
if binding.ClientAuthType == 1 {
|
||||
httpServer.TLSConfig.ClientCAs = s.certMgr.GetRootCAs()
|
||||
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
return httpServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
binding.EnableHTTPS = false
|
||||
|
|
|
@ -68,6 +68,9 @@ type Binding struct {
|
|||
Port int `json:"port" mapstructure:"port"`
|
||||
// you also need to provide a certificate for enabling HTTPS
|
||||
EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
|
||||
// set to 1 to require client certificate authentication in addition to basic auth.
|
||||
// You need to define at least a certificate authority for this to work
|
||||
ClientAuthType int `json:"client_auth_type" mapstructure:"client_auth_type"`
|
||||
}
|
||||
|
||||
// GetAddress returns the binding address
|
||||
|
@ -94,6 +97,8 @@ type Configuration struct {
|
|||
// "paramchange" request to the running service on Windows.
|
||||
CertificateFile string `json:"certificate_file" mapstructure:"certificate_file"`
|
||||
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
|
||||
// CACertificates defines the set of root certificate authorities to use to verify client certificates.
|
||||
CACertificates []string `json:"ca_certificates" mapstructure:"ca_certificates"`
|
||||
// CORS configuration
|
||||
Cors Cors `json:"cors" mapstructure:"cors"`
|
||||
// Cache configuration
|
||||
|
@ -169,7 +174,7 @@ 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
|
||||
|
|
|
@ -256,6 +256,24 @@ func TestInitialization(t *testing.T) {
|
|||
}
|
||||
err = cfg.Initialize(configDir)
|
||||
assert.EqualError(t, err, common.ErrNoBinding.Error())
|
||||
|
||||
cfg.CertificateFile = certPath
|
||||
cfg.CertificateKeyFile = keyPath
|
||||
cfg.CACertificates = []string{""}
|
||||
|
||||
cfg.Bindings = []webdavd.Binding{
|
||||
{
|
||||
Port: 9022,
|
||||
ClientAuthType: 1,
|
||||
EnableHTTPS: true,
|
||||
},
|
||||
}
|
||||
err = cfg.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
|
||||
cfg.CACertificates = nil
|
||||
err = cfg.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBasicHandling(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue