mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 15:40:23 +00:00
a6e36e7cad
For each user you can now configure: - TLS certificate auth - TLS certificate auth and password - Password auth For TLS auth, the certificate common name must match the name provided using the "USER" FTP command
350 lines
12 KiB
Go
350 lines
12 KiB
Go
package ftpd
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
ftpserver "github.com/fclairamb/ftpserverlib"
|
|
|
|
"github.com/drakkan/sftpgo/common"
|
|
"github.com/drakkan/sftpgo/dataprovider"
|
|
"github.com/drakkan/sftpgo/logger"
|
|
"github.com/drakkan/sftpgo/metrics"
|
|
"github.com/drakkan/sftpgo/utils"
|
|
"github.com/drakkan/sftpgo/version"
|
|
)
|
|
|
|
// Server implements the ftpserverlib MainDriver interface
|
|
type Server struct {
|
|
ID int
|
|
config *Configuration
|
|
initialMsg string
|
|
statusBanner string
|
|
binding Binding
|
|
mu sync.RWMutex
|
|
verifiedTLSConns map[uint32]bool
|
|
}
|
|
|
|
// NewServer returns a new FTP server driver
|
|
func NewServer(config *Configuration, configDir string, binding Binding, id int) *Server {
|
|
binding.setCiphers()
|
|
server := &Server{
|
|
config: config,
|
|
initialMsg: config.Banner,
|
|
statusBanner: fmt.Sprintf("SFTPGo %v FTP Server", version.Get().Version),
|
|
binding: binding,
|
|
ID: id,
|
|
verifiedTLSConns: make(map[uint32]bool),
|
|
}
|
|
if config.BannerFile != "" {
|
|
bannerFilePath := config.BannerFile
|
|
if !filepath.IsAbs(bannerFilePath) {
|
|
bannerFilePath = filepath.Join(configDir, bannerFilePath)
|
|
}
|
|
bannerContent, err := os.ReadFile(bannerFilePath)
|
|
if err == nil {
|
|
server.initialMsg = string(bannerContent)
|
|
} else {
|
|
logger.WarnToConsole("unable to read FTPD banner file: %v", err)
|
|
logger.Warn(logSender, "", "unable to read banner file: %v", err)
|
|
}
|
|
}
|
|
return server
|
|
}
|
|
|
|
func (s *Server) isTLSConnVerified(id uint32) bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
return s.verifiedTLSConns[id]
|
|
}
|
|
|
|
func (s *Server) setTLSConnVerified(id uint32, value bool) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.verifiedTLSConns[id] = value
|
|
}
|
|
|
|
func (s *Server) cleanTLSConnVerification(id uint32) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
delete(s.verifiedTLSConns, id)
|
|
}
|
|
|
|
// GetSettings returns FTP server settings
|
|
func (s *Server) GetSettings() (*ftpserver.Settings, error) {
|
|
var portRange *ftpserver.PortRange
|
|
if s.config.PassivePortRange.Start > 0 && s.config.PassivePortRange.End > s.config.PassivePortRange.Start {
|
|
portRange = &ftpserver.PortRange{
|
|
Start: s.config.PassivePortRange.Start,
|
|
End: s.config.PassivePortRange.End,
|
|
}
|
|
}
|
|
var ftpListener net.Listener
|
|
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 %v: %v", s.binding.GetAddress(), err)
|
|
return nil, err
|
|
}
|
|
ftpListener, err = common.Config.GetProxyListener(listener)
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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: s.binding.GetAddress(),
|
|
PublicHost: s.binding.ForcePassiveIP,
|
|
PassiveTransferPortRange: portRange,
|
|
ActiveTransferPortNon20: s.config.ActiveTransfersPortNon20,
|
|
IdleTimeout: -1,
|
|
ConnectionTimeout: 20,
|
|
Banner: s.statusBanner,
|
|
TLSRequired: ftpserver.TLSRequirement(s.binding.TLSMode),
|
|
DisableSite: !s.config.EnableSite,
|
|
DisableActiveMode: s.config.DisableActiveMode,
|
|
EnableHASH: s.config.HASHSupport > 0,
|
|
EnableCOMB: s.config.CombineSupport > 0,
|
|
DefaultTransferType: ftpserver.TransferTypeBinary,
|
|
}, nil
|
|
}
|
|
|
|
// ClientConnected is called to send the very first welcome message
|
|
func (s *Server) ClientConnected(cc ftpserver.ClientContext) (string, error) {
|
|
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())
|
|
if common.IsBanned(ipAddr) {
|
|
logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, ip %#v is banned", ipAddr)
|
|
return "Access denied, banned client IP", common.ErrConnectionDenied
|
|
}
|
|
if !common.Connections.IsNewConnectionAllowed() {
|
|
logger.Log(logger.LevelDebug, common.ProtocolFTP, "", "connection refused, configured limit reached")
|
|
return "", common.ErrConnectionDenied
|
|
}
|
|
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolFTP); err != nil {
|
|
return "", err
|
|
}
|
|
connID := fmt.Sprintf("%v_%v", s.ID, cc.ID())
|
|
user := dataprovider.User{}
|
|
connection := &Connection{
|
|
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
|
|
clientContext: cc,
|
|
}
|
|
common.Connections.Add(connection)
|
|
return s.initialMsg, nil
|
|
}
|
|
|
|
// ClientDisconnected is called when the user disconnects, even if he never authenticated
|
|
func (s *Server) ClientDisconnected(cc ftpserver.ClientContext) {
|
|
s.cleanTLSConnVerification(cc.ID())
|
|
connID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
|
|
common.Connections.Remove(connID)
|
|
}
|
|
|
|
// AuthUser authenticates the user and selects an handling driver
|
|
func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string) (ftpserver.ClientDriver, error) {
|
|
loginMethod := dataprovider.LoginMethodPassword
|
|
if s.isTLSConnVerified(cc.ID()) {
|
|
loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd
|
|
}
|
|
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())
|
|
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolFTP)
|
|
if err != nil {
|
|
user.Username = username
|
|
updateLoginMetrics(&user, ipAddr, loginMethod, err)
|
|
return nil, err
|
|
}
|
|
|
|
connection, err := s.validateUser(user, cc, loginMethod)
|
|
|
|
defer updateLoginMetrics(&user, ipAddr, loginMethod, err)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connection.Fs.CheckRootPath(connection.GetUsername(), user.GetUID(), user.GetGID())
|
|
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP, username: %#v, home_dir: %#v remote addr: %#v",
|
|
user.ID, user.Username, user.HomeDir, ipAddr)
|
|
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
|
|
return connection, nil
|
|
}
|
|
|
|
// VerifyConnection checks whether a user should be authenticated using a client certificate without prompting for a password
|
|
func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsConn *tls.Conn) (ftpserver.ClientDriver, error) {
|
|
if !s.binding.isMutualTLSEnabled() {
|
|
return nil, nil
|
|
}
|
|
s.setTLSConnVerified(cc.ID(), false)
|
|
if tlsConn != nil {
|
|
state := tlsConn.ConnectionState()
|
|
if len(state.PeerCertificates) > 0 {
|
|
ipAddr := utils.GetIPFromRemoteAddress(cc.RemoteAddr().String())
|
|
dbUser, err := dataprovider.CheckUserBeforeTLSAuth(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])
|
|
if err != nil {
|
|
dbUser.Username = user
|
|
updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err)
|
|
return nil, err
|
|
}
|
|
if dbUser.IsTLSUsernameVerificationEnabled() {
|
|
dbUser, err = dataprovider.CheckUserAndTLSCert(user, ipAddr, common.ProtocolFTP, state.PeerCertificates[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.setTLSConnVerified(cc.ID(), true)
|
|
|
|
if dbUser.IsLoginMethodAllowed(dataprovider.LoginMethodTLSCertificate, nil) {
|
|
connection, err := s.validateUser(dbUser, cc, dataprovider.LoginMethodTLSCertificate)
|
|
|
|
defer updateLoginMetrics(&dbUser, ipAddr, dataprovider.LoginMethodTLSCertificate, err)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connection.Fs.CheckRootPath(connection.GetUsername(), dbUser.GetUID(), dbUser.GetGID())
|
|
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP using a TLS certificate, username: %#v, home_dir: %#v remote addr: %#v",
|
|
dbUser.ID, dbUser.Username, dbUser.HomeDir, ipAddr)
|
|
dataprovider.UpdateLastLogin(&dbUser) //nolint:errcheck
|
|
return connection, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// GetTLSConfig returns a TLS Certificate to use
|
|
func (s *Server) GetTLSConfig() (*tls.Config, error) {
|
|
if certMgr != nil {
|
|
tlsConfig := &tls.Config{
|
|
GetCertificate: certMgr.GetCertificateFunc(),
|
|
MinVersion: tls.VersionTLS12,
|
|
CipherSuites: s.binding.ciphers,
|
|
PreferServerCipherSuites: true,
|
|
}
|
|
if s.binding.isMutualTLSEnabled() {
|
|
tlsConfig.ClientCAs = certMgr.GetRootCAs()
|
|
tlsConfig.VerifyConnection = s.verifyTLSConnection
|
|
switch s.binding.ClientAuthType {
|
|
case 1:
|
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
|
case 2:
|
|
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
|
}
|
|
}
|
|
return tlsConfig, nil
|
|
}
|
|
return nil, errors.New("no TLS certificate configured")
|
|
}
|
|
|
|
func (s *Server) verifyTLSConnection(state tls.ConnectionState) error {
|
|
if certMgr != nil {
|
|
var clientCrt *x509.Certificate
|
|
var clientCrtName string
|
|
if len(state.PeerCertificates) > 0 {
|
|
clientCrt = state.PeerCertificates[0]
|
|
clientCrtName = clientCrt.Subject.String()
|
|
}
|
|
if len(state.VerifiedChains) == 0 {
|
|
if s.binding.ClientAuthType == 2 {
|
|
return nil
|
|
}
|
|
logger.Warn(logSender, "", "TLS connection cannot be verified: unable to get verification chain")
|
|
return errors.New("TLS connection cannot be verified: unable to get verification chain")
|
|
}
|
|
for _, verifiedChain := range state.VerifiedChains {
|
|
var caCrt *x509.Certificate
|
|
if len(verifiedChain) > 0 {
|
|
caCrt = verifiedChain[len(verifiedChain)-1]
|
|
}
|
|
if certMgr.IsRevoked(clientCrt, caCrt) {
|
|
logger.Debug(logSender, "", "tls handshake error, client certificate %#v has beed revoked", clientCrtName)
|
|
return common.ErrCrtRevoked
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext, loginMethod string) (*Connection, error) {
|
|
connectionID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
|
|
if !filepath.IsAbs(user.HomeDir) {
|
|
logger.Warn(logSender, connectionID, "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
|
|
user.Username, user.HomeDir)
|
|
return nil, fmt.Errorf("cannot login user with invalid home dir: %#v", user.HomeDir)
|
|
}
|
|
if utils.IsStringInSlice(common.ProtocolFTP, user.Filters.DeniedProtocols) {
|
|
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol FTP is not allowed", user.Username)
|
|
return nil, fmt.Errorf("Protocol FTP is not allowed for user %#v", user.Username)
|
|
}
|
|
if !user.IsLoginMethodAllowed(loginMethod, nil) {
|
|
logger.Debug(logSender, connectionID, "cannot login user %#v, %v login method is not allowed", user.Username, loginMethod)
|
|
return nil, fmt.Errorf("Login method %v is not allowed for user %#v", loginMethod, user.Username)
|
|
}
|
|
if user.MaxSessions > 0 {
|
|
activeSessions := common.Connections.GetActiveSessions(user.Username)
|
|
if activeSessions >= user.MaxSessions {
|
|
logger.Debug(logSender, connectionID, "authentication refused for user: %#v, too many open sessions: %v/%v", user.Username,
|
|
activeSessions, user.MaxSessions)
|
|
return nil, fmt.Errorf("too many open sessions: %v", activeSessions)
|
|
}
|
|
}
|
|
if dataprovider.GetQuotaTracking() > 0 && user.HasOverlappedMappedPaths() {
|
|
logger.Debug(logSender, connectionID, "cannot login user %#v, overlapping mapped folders are allowed only with quota tracking disabled",
|
|
user.Username)
|
|
return nil, errors.New("overlapping mapped folders are allowed only with quota tracking disabled")
|
|
}
|
|
remoteAddr := cc.RemoteAddr().String()
|
|
if !user.IsLoginFromAddrAllowed(remoteAddr) {
|
|
logger.Debug(logSender, connectionID, "cannot login user %#v, remote address is not allowed: %v", user.Username, remoteAddr)
|
|
return nil, fmt.Errorf("Login for user %#v is not allowed from this address: %v", user.Username, remoteAddr)
|
|
}
|
|
fs, err := user.GetFilesystem(connectionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connection := &Connection{
|
|
BaseConnection: common.NewBaseConnection(fmt.Sprintf("%v_%v", s.ID, cc.ID()), common.ProtocolFTP, user, fs),
|
|
clientContext: cc,
|
|
}
|
|
err = common.Connections.Swap(connection)
|
|
if err != nil {
|
|
return nil, errors.New("Internal authentication error")
|
|
}
|
|
return connection, nil
|
|
}
|
|
|
|
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
|
|
metrics.AddLoginAttempt(loginMethod)
|
|
if err != nil {
|
|
logger.ConnectionFailedLog(user.Username, ip, loginMethod,
|
|
common.ProtocolFTP, err.Error())
|
|
event := common.HostEventLoginFailed
|
|
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
|
event = common.HostEventUserNotFound
|
|
}
|
|
common.AddDefenderEvent(ip, event)
|
|
}
|
|
metrics.AddLoginResult(loginMethod, err)
|
|
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)
|
|
}
|