diff --git a/cmd/startsubsys.go b/cmd/startsubsys.go index 28199d84..d45f0242 100644 --- a/cmd/startsubsys.go +++ b/cmd/startsubsys.go @@ -15,7 +15,7 @@ import ( "github.com/drakkan/sftpgo/v2/config" "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/version" ) diff --git a/common/actions.go b/common/actions.go index e0d42a9c..71724993 100644 --- a/common/actions.go +++ b/common/actions.go @@ -17,8 +17,8 @@ import ( "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" "github.com/drakkan/sftpgo/v2/util" ) diff --git a/common/actions_test.go b/common/actions_test.go index f9adb019..9f83d9eb 100644 --- a/common/actions_test.go +++ b/common/actions_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/drakkan/sftpgo/v2/dataprovider" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" "github.com/drakkan/sftpgo/v2/vfs" ) diff --git a/config/config.go b/config/config.go index 60ef2cc7..6cc84998 100644 --- a/config/config.go +++ b/config/config.go @@ -18,8 +18,8 @@ import ( "github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/mfa" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/smtp" "github.com/drakkan/sftpgo/v2/telemetry" diff --git a/config/config_test.go b/config/config_test.go index a6313c2c..b7c7f0b5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -18,8 +18,8 @@ import ( "github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/mfa" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/smtp" "github.com/drakkan/sftpgo/v2/util" diff --git a/dataprovider/actions.go b/dataprovider/actions.go index 308c644a..55c41ef2 100644 --- a/dataprovider/actions.go +++ b/dataprovider/actions.go @@ -13,7 +13,7 @@ import ( "github.com/drakkan/sftpgo/v2/httpclient" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" "github.com/drakkan/sftpgo/v2/util" ) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 5febc61a..ea215d34 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -49,10 +49,9 @@ import ( "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" "github.com/drakkan/sftpgo/v2/mfa" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" - sdkutil "github.com/drakkan/sftpgo/v2/sdk/util" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/vfs" ) @@ -2890,7 +2889,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip, var tlsCert string if cert != nil { var err error - tlsCert, err = sdkutil.EncodeTLSCertToPem(cert) + tlsCert, err = util.EncodeTLSCertToPem(cert) if err != nil { return nil, err } diff --git a/httpd/api_events.go b/httpd/api_events.go index 4dfdd39c..960e3c07 100644 --- a/httpd/api_events.go +++ b/httpd/api_events.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/drakkan/sftpgo/v2/dataprovider" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk/plugin/eventsearcher" "github.com/drakkan/sftpgo/v2/util" ) diff --git a/httpd/api_utils.go b/httpd/api_utils.go index e64eb236..cfab52a2 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -24,7 +24,7 @@ import ( "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/smtp" "github.com/drakkan/sftpgo/v2/util" ) diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 8c5edbf3..d5173a8e 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -47,9 +47,9 @@ import ( "github.com/drakkan/sftpgo/v2/httpdtest" "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/mfa" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sftpd" "github.com/drakkan/sftpgo/v2/smtp" "github.com/drakkan/sftpgo/v2/util" diff --git a/httpd/internal_test.go b/httpd/internal_test.go index 9ac631ad..29667e86 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -33,9 +33,9 @@ import ( "github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/dataprovider" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/vfs" ) diff --git a/httpd/server.go b/httpd/server.go index 0d8dca45..337d1f08 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -25,7 +25,6 @@ import ( "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/mfa" "github.com/drakkan/sftpgo/v2/sdk" - sdklogger "github.com/drakkan/sftpgo/v2/sdk/logger" "github.com/drakkan/sftpgo/v2/smtp" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/version" @@ -76,7 +75,7 @@ func (s *httpdServer) listenAndServe() error { WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16, // 64KB - ErrorLog: log.New(&sdklogger.StdLoggerWrapper{Sender: logSender}, "", 0), + ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), } if certMgr != nil && s.binding.EnableHTTPS { config := &tls.Config{ diff --git a/sdk/logger/hclog_adapter.go b/logger/hclog_adapter.go similarity index 79% rename from sdk/logger/hclog_adapter.go rename to logger/hclog_adapter.go index 9805dfb1..4dc15208 100644 --- a/sdk/logger/hclog_adapter.go +++ b/logger/hclog_adapter.go @@ -5,6 +5,7 @@ import ( "log" "github.com/hashicorp/go-hclog" + "github.com/rs/zerolog" ) // HCLogAdapter is an adapter for hclog.Logger @@ -14,7 +15,20 @@ type HCLogAdapter struct { // Log emits a message and key/value pairs at a provided log level func (l *HCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) { - logger.LogWithKeyVals(level, l.Name(), msg, args...) + var ev *zerolog.Event + switch level { + case hclog.Info: + ev = logger.Info() + case hclog.Warn: + ev = logger.Warn() + case hclog.Error: + ev = logger.Error() + default: + ev = logger.Debug() + } + ev.Timestamp().Str("sender", l.Name()) + addKeysAndValues(ev, args...) + ev.Msg(msg) } // Trace emits a message and key/value pairs at the TRACE level @@ -61,21 +75,3 @@ func (l *HCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Lo func (l *HCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer { return &StdLoggerWrapper{Sender: l.Name()} } - -// StdLoggerWrapper is a wrapper for standard logger compatibility -type StdLoggerWrapper struct { - Sender string -} - -// Write implements the io.Writer interface. This is useful to set as a writer -// for the standard library log. -func (l *StdLoggerWrapper) Write(p []byte) (n int, err error) { - n = len(p) - if n > 0 && p[n-1] == '\n' { - // Trim CR added by stdlog. - p = p[0 : n-1] - } - - logger.Log(hclog.Error, l.Sender, "", string(p)) - return -} diff --git a/logger/logger.go b/logger/logger.go index 316d902e..f191a355 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -16,7 +16,6 @@ import ( "time" ftpserverlog "github.com/fclairamb/go-log" - "github.com/hashicorp/go-hclog" "github.com/rs/zerolog" lumberjack "gopkg.in/natefinch/lumberjack.v2" @@ -46,25 +45,38 @@ var ( type logWrapper struct{} -// LogWithKeyVals logs at the specified level for the specified sender adding the specified key vals -func (l *logWrapper) LogWithKeyVals(level hclog.Level, sender, msg string, args ...interface{}) { - LogWithKeyVals(level, sender, msg, args...) -} - // Log logs at the specified level for the specified sender -func (l *logWrapper) Log(level hclog.Level, sender, format string, v ...interface{}) { +func (l *logWrapper) Log(level int, sender, format string, v ...interface{}) { switch level { - case hclog.Info: + case 1: Log(LevelInfo, sender, "", format, v...) - case hclog.Warn: + case 2: Log(LevelWarn, sender, "", format, v...) - case hclog.Error: + case 3: Log(LevelError, sender, "", format, v...) default: Log(LevelDebug, sender, "", format, v...) } } +// StdLoggerWrapper is a wrapper for standard logger compatibility +type StdLoggerWrapper struct { + Sender string +} + +// Write implements the io.Writer interface. This is useful to set as a writer +// for the standard library log. +func (l *StdLoggerWrapper) Write(p []byte) (n int, err error) { + n = len(p) + if n > 0 && p[n-1] == '\n' { + // Trim CR added by stdlog. + p = p[0 : n-1] + } + + Log(LevelError, l.Sender, "", string(p)) + return +} + // LeveledLogger is a logger that accepts a message string and a variadic number of key-value pairs type LeveledLogger struct { Sender string @@ -219,24 +231,6 @@ func RotateLogFile() error { return errors.New("logging to file is disabled") } -// LogWithKeyVals logs at the specified level for the specified sender adding the specified key vals -func LogWithKeyVals(level hclog.Level, sender, msg string, args ...interface{}) { - var ev *zerolog.Event - switch level { - case hclog.Info: - ev = logger.Info() - case hclog.Warn: - ev = logger.Warn() - case hclog.Error: - ev = logger.Error() - default: - ev = logger.Debug() - } - ev.Timestamp().Str("sender", sender) - addKeysAndValues(ev, args...) - ev.Msg(msg) -} - // Log logs at the specified level for the specified sender func Log(level LogLevel, sender string, connectionID string, format string, v ...interface{}) { var ev *zerolog.Event diff --git a/sdk/plugin/auth.go b/plugin/auth.go similarity index 99% rename from sdk/plugin/auth.go rename to plugin/auth.go index 4e05bbac..4cf408ff 100644 --- a/sdk/plugin/auth.go +++ b/plugin/auth.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" - "github.com/drakkan/sftpgo/v2/sdk/logger" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/sdk/plugin/auth" ) diff --git a/sdk/plugin/kms.go b/plugin/kms.go similarity index 98% rename from sdk/plugin/kms.go rename to plugin/kms.go index d74c734c..6e80a51b 100644 --- a/sdk/plugin/kms.go +++ b/plugin/kms.go @@ -9,10 +9,10 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/logger" kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" - "github.com/drakkan/sftpgo/v2/sdk/util" + "github.com/drakkan/sftpgo/v2/util" ) var ( diff --git a/sdk/plugin/metadata.go b/plugin/metadata.go similarity index 97% rename from sdk/plugin/metadata.go rename to plugin/metadata.go index cecebaeb..dae88724 100644 --- a/sdk/plugin/metadata.go +++ b/plugin/metadata.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" - "github.com/drakkan/sftpgo/v2/sdk/logger" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/sdk/plugin/metadata" ) diff --git a/sdk/plugin/notifier.go b/plugin/notifier.go similarity index 98% rename from sdk/plugin/notifier.go rename to plugin/notifier.go index ffb1a727..c770c463 100644 --- a/sdk/plugin/notifier.go +++ b/plugin/notifier.go @@ -10,9 +10,9 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" - "github.com/drakkan/sftpgo/v2/sdk/logger" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" - "github.com/drakkan/sftpgo/v2/sdk/util" + "github.com/drakkan/sftpgo/v2/util" ) // NotifierConfig defines configuration parameters for notifiers plugins diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 00000000..29c564ad --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,666 @@ +// Package plugin provides support for the SFTPGo plugin system +package plugin + +import ( + "crypto/x509" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/hashicorp/go-hclog" + + "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/sdk/kms" + "github.com/drakkan/sftpgo/v2/sdk/plugin/auth" + "github.com/drakkan/sftpgo/v2/sdk/plugin/eventsearcher" + kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" + "github.com/drakkan/sftpgo/v2/sdk/plugin/metadata" + "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" + "github.com/drakkan/sftpgo/v2/util" +) + +const ( + logSender = "plugins" +) + +var ( + // Handler defines the plugins manager + Handler Manager + pluginsLogLevel = hclog.Debug + // ErrNoSearcher defines the error to return for events searches if no plugin is configured + ErrNoSearcher = errors.New("no events searcher plugin defined") + // ErrNoMetadater returns the error to return for metadata methods if no plugin is configured + ErrNoMetadater = errors.New("no metadata plugin defined") +) + +// Renderer defines the interface for generic objects rendering +type Renderer interface { + RenderAsJSON(reload bool) ([]byte, error) +} + +// Config defines a plugin configuration +type Config struct { + // Plugin type + Type string `json:"type" mapstructure:"type"` + // NotifierOptions defines options for notifiers plugins + NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"` + // KMSOptions defines options for a KMS plugin + KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"` + // AuthOptions defines options for authentication plugins + AuthOptions AuthConfig `json:"auth_options" mapstructure:"auth_options"` + // Path to the plugin executable + Cmd string `json:"cmd" mapstructure:"cmd"` + // Args to pass to the plugin executable + Args []string `json:"args" mapstructure:"args"` + // SHA256 checksum for the plugin executable. + // If not empty it will be used to verify the integrity of the executable + SHA256Sum string `json:"sha256sum" mapstructure:"sha256sum"` + // If enabled the client and the server automatically negotiate mTLS for + // transport authentication. This ensures that only the original client will + // be allowed to connect to the server, and all other connections will be + // rejected. The client will also refuse to connect to any server that isn't + // the original instance started by the client. + AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"` + // unique identifier for kms plugins + kmsID int +} + +func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { + return &kmsPluginSecretProvider{ + BaseSecret: base, + URL: url, + MasterKey: masterKey, + config: c, + } +} + +// Manager handles enabled plugins +type Manager struct { + closed int32 + done chan bool + // List of configured plugins + Configs []Config `json:"plugins" mapstructure:"plugins"` + notifLock sync.RWMutex + notifiers []*notifierPlugin + kmsLock sync.RWMutex + kms []*kmsPlugin + authLock sync.RWMutex + auths []*authPlugin + searcherLock sync.RWMutex + searcher *searcherPlugin + metadaterLock sync.RWMutex + metadater *metadataPlugin + authScopes int + hasSearcher bool + hasMetadater bool + hasNotifiers bool +} + +// Initialize initializes the configured plugins +func Initialize(configs []Config, logVerbose bool) error { + logger.Debug(logSender, "", "initialize") + Handler = Manager{ + Configs: configs, + done: make(chan bool), + closed: 0, + authScopes: -1, + } + setLogLevel(logVerbose) + if len(configs) == 0 { + return nil + } + + if err := Handler.validateConfigs(); err != nil { + return err + } + + kmsID := 0 + for idx, config := range Handler.Configs { + switch config.Type { + case notifier.PluginName: + plugin, err := newNotifierPlugin(config) + if err != nil { + return err + } + Handler.notifiers = append(Handler.notifiers, plugin) + case kmsplugin.PluginName: + plugin, err := newKMSPlugin(config) + if err != nil { + return err + } + Handler.kms = append(Handler.kms, plugin) + Handler.Configs[idx].kmsID = kmsID + kmsID++ + kms.RegisterSecretProvider(config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus, + Handler.Configs[idx].newKMSPluginSecretProvider) + logger.Debug(logSender, "", "registered secret provider for scheme: %v, encrypted status: %v", + config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus) + case auth.PluginName: + plugin, err := newAuthPlugin(config) + if err != nil { + return err + } + Handler.auths = append(Handler.auths, plugin) + if Handler.authScopes == -1 { + Handler.authScopes = config.AuthOptions.Scope + } else { + Handler.authScopes |= config.AuthOptions.Scope + } + case eventsearcher.PluginName: + plugin, err := newSearcherPlugin(config) + if err != nil { + return err + } + Handler.searcher = plugin + case metadata.PluginName: + plugin, err := newMetadaterPlugin(config) + if err != nil { + return err + } + Handler.metadater = plugin + default: + return fmt.Errorf("unsupported plugin type: %v", config.Type) + } + } + startCheckTicker() + return nil +} + +func (m *Manager) validateConfigs() error { + kmsSchemes := make(map[string]bool) + kmsEncryptions := make(map[string]bool) + m.hasSearcher = false + m.hasMetadater = false + m.hasNotifiers = false + + for _, config := range m.Configs { + if config.Type == kmsplugin.PluginName { + if _, ok := kmsSchemes[config.KMSOptions.Scheme]; ok { + return fmt.Errorf("invalid KMS configuration, duplicated scheme %#v", config.KMSOptions.Scheme) + } + if _, ok := kmsEncryptions[config.KMSOptions.EncryptedStatus]; ok { + return fmt.Errorf("invalid KMS configuration, duplicated encrypted status %#v", config.KMSOptions.EncryptedStatus) + } + kmsSchemes[config.KMSOptions.Scheme] = true + kmsEncryptions[config.KMSOptions.EncryptedStatus] = true + } + if config.Type == eventsearcher.PluginName { + if m.hasSearcher { + return errors.New("only one eventsearcher plugin can be defined") + } + m.hasSearcher = true + } + if config.Type == metadata.PluginName { + if m.hasMetadater { + return errors.New("only one metadata plugin can be defined") + } + m.hasMetadater = true + } + if config.Type == notifier.PluginName { + m.hasNotifiers = true + } + } + return nil +} + +// HasNotifiers returns true if there is at least a notifier plugin +func (m *Manager) HasNotifiers() bool { + return m.hasNotifiers +} + +// NotifyFsEvent sends the fs event notifications using any defined notifier plugins +func (m *Manager) NotifyFsEvent(event *notifier.FsEvent) { + m.notifLock.RLock() + defer m.notifLock.RUnlock() + + for _, n := range m.notifiers { + n.notifyFsAction(event) + } +} + +// NotifyProviderEvent sends the provider event notifications using any defined notifier plugins +func (m *Manager) NotifyProviderEvent(event *notifier.ProviderEvent, object Renderer) { + m.notifLock.RLock() + defer m.notifLock.RUnlock() + + for _, n := range m.notifiers { + n.notifyProviderAction(event, object) + } +} + +// SearchFsEvents returns the filesystem events matching the specified filters +func (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]byte, []string, []string, error) { + if !m.hasSearcher { + return nil, nil, nil, ErrNoSearcher + } + m.searcherLock.RLock() + plugin := m.searcher + m.searcherLock.RUnlock() + + return plugin.searchear.SearchFsEvents(searchFilters) +} + +// SearchProviderEvents returns the provider events matching the specified filters +func (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEventSearch) ([]byte, []string, []string, error) { + if !m.hasSearcher { + return nil, nil, nil, ErrNoSearcher + } + m.searcherLock.RLock() + plugin := m.searcher + m.searcherLock.RUnlock() + + return plugin.searchear.SearchProviderEvents(searchFilters) +} + +// HasMetadater returns true if a metadata plugin is defined +func (m *Manager) HasMetadater() bool { + return m.hasMetadater +} + +// SetModificationTime sets the modification time for the specified object +func (m *Manager) SetModificationTime(storageID, objectPath string, mTime int64) error { + if !m.hasMetadater { + return ErrNoMetadater + } + m.metadaterLock.RLock() + plugin := m.metadater + m.metadaterLock.RUnlock() + + return plugin.metadater.SetModificationTime(storageID, objectPath, mTime) +} + +// GetModificationTime returns the modification time for the specified path +func (m *Manager) GetModificationTime(storageID, objectPath string, isDir bool) (int64, error) { + if !m.hasMetadater { + return 0, ErrNoMetadater + } + m.metadaterLock.RLock() + plugin := m.metadater + m.metadaterLock.RUnlock() + + return plugin.metadater.GetModificationTime(storageID, objectPath) +} + +// GetModificationTimes returns the modification times for all the files within the specified folder +func (m *Manager) GetModificationTimes(storageID, objectPath string) (map[string]int64, error) { + if !m.hasMetadater { + return nil, ErrNoMetadater + } + m.metadaterLock.RLock() + plugin := m.metadater + m.metadaterLock.RUnlock() + + return plugin.metadater.GetModificationTimes(storageID, objectPath) +} + +// RemoveMetadata deletes the metadata stored for the specified object +func (m *Manager) RemoveMetadata(storageID, objectPath string) error { + if !m.hasMetadater { + return ErrNoMetadater + } + m.metadaterLock.RLock() + plugin := m.metadater + m.metadaterLock.RUnlock() + + return plugin.metadater.RemoveMetadata(storageID, objectPath) +} + +// GetMetadataFolders returns the folders that metadata is associated with +func (m *Manager) GetMetadataFolders(storageID, from string, limit int) ([]string, error) { + if !m.hasMetadater { + return nil, ErrNoMetadater + } + m.metadaterLock.RLock() + plugin := m.metadater + m.metadaterLock.RUnlock() + + return plugin.metadater.GetFolders(storageID, limit, from) +} + +func (m *Manager) kmsEncrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, string, int32, error) { + m.kmsLock.RLock() + plugin := m.kms[kmsID] + m.kmsLock.RUnlock() + + return plugin.Encrypt(secret, url, masterKey) +} + +func (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, error) { + m.kmsLock.RLock() + plugin := m.kms[kmsID] + m.kmsLock.RUnlock() + + return plugin.Decrypt(secret, url, masterKey) +} + +// HasAuthScope returns true if there is an auth plugin that support the specified scope +func (m *Manager) HasAuthScope(scope int) bool { + if m.authScopes == -1 { + return false + } + return m.authScopes&scope != 0 +} + +// Authenticate tries to authenticate the specified user using an external plugin +func (m *Manager) Authenticate(username, password, ip, protocol string, pkey string, + tlsCert *x509.Certificate, authScope int, userAsJSON []byte, +) ([]byte, error) { + switch authScope { + case AuthScopePassword: + return m.checkUserAndPass(username, password, ip, protocol, userAsJSON) + case AuthScopePublicKey: + return m.checkUserAndPublicKey(username, pkey, ip, protocol, userAsJSON) + case AuthScopeKeyboardInteractive: + return m.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON) + case AuthScopeTLSCertificate: + cert, err := util.EncodeTLSCertToPem(tlsCert) + if err != nil { + logger.Error(logSender, "", "unable to encode tls certificate to pem: %v", err) + return nil, fmt.Errorf("unable to encode tls cert to pem: %w", err) + } + return m.checkUserAndTLSCert(username, cert, ip, protocol, userAsJSON) + default: + return nil, fmt.Errorf("unsupported auth scope: %v", authScope) + } +} + +// ExecuteKeyboardInteractiveStep executes a keyboard interactive step +func (m *Manager) ExecuteKeyboardInteractiveStep(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) { + var plugin *authPlugin + + m.authLock.Lock() + for _, p := range m.auths { + if p.config.AuthOptions.Scope&AuthScopePassword != 0 { + plugin = p + break + } + } + m.authLock.Unlock() + + if plugin == nil { + return nil, errors.New("no auth plugin configured for keyaboard interactive authentication step") + } + + return plugin.sendKeyboardIteractiveRequest(req) +} + +func (m *Manager) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) { + var plugin *authPlugin + + m.authLock.Lock() + for _, p := range m.auths { + if p.config.AuthOptions.Scope&AuthScopePassword != 0 { + plugin = p + break + } + } + m.authLock.Unlock() + + if plugin == nil { + return nil, errors.New("no auth plugin configured for password checking") + } + + return plugin.checkUserAndPass(username, password, ip, protocol, userAsJSON) +} + +func (m *Manager) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) { + var plugin *authPlugin + + m.authLock.Lock() + for _, p := range m.auths { + if p.config.AuthOptions.Scope&AuthScopePublicKey != 0 { + plugin = p + break + } + } + m.authLock.Unlock() + + if plugin == nil { + return nil, errors.New("no auth plugin configured for public key checking") + } + + return plugin.checkUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON) +} + +func (m *Manager) checkUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) { + var plugin *authPlugin + + m.authLock.Lock() + for _, p := range m.auths { + if p.config.AuthOptions.Scope&AuthScopeTLSCertificate != 0 { + plugin = p + break + } + } + m.authLock.Unlock() + + if plugin == nil { + return nil, errors.New("no auth plugin configured for TLS certificate checking") + } + + return plugin.checkUserAndTLSCertificate(username, tlsCert, ip, protocol, userAsJSON) +} + +func (m *Manager) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) { + var plugin *authPlugin + + m.authLock.Lock() + for _, p := range m.auths { + if p.config.AuthOptions.Scope&AuthScopeKeyboardInteractive != 0 { + plugin = p + break + } + } + m.authLock.Unlock() + + if plugin == nil { + return nil, errors.New("no auth plugin configured for keyboard interactive checking") + } + + return plugin.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON) +} + +func (m *Manager) checkCrashedPlugins() { + m.notifLock.RLock() + for idx, n := range m.notifiers { + if n.exited() { + defer func(cfg Config, index int) { + Handler.restartNotifierPlugin(cfg, index) + }(n.config, idx) + } else { + n.sendQueuedEvents() + } + } + m.notifLock.RUnlock() + + m.kmsLock.RLock() + for idx, k := range m.kms { + if k.exited() { + defer func(cfg Config, index int) { + Handler.restartKMSPlugin(cfg, index) + }(k.config, idx) + } + } + m.kmsLock.RUnlock() + + m.authLock.RLock() + for idx, a := range m.auths { + if a.exited() { + defer func(cfg Config, index int) { + Handler.restartAuthPlugin(cfg, index) + }(a.config, idx) + } + } + m.authLock.RUnlock() + + if m.hasSearcher { + m.searcherLock.RLock() + if m.searcher.exited() { + defer func(cfg Config) { + Handler.restartSearcherPlugin(cfg) + }(m.searcher.config) + } + m.searcherLock.RUnlock() + } + + if m.hasMetadater { + m.metadaterLock.RLock() + if m.metadater.exited() { + defer func(cfg Config) { + Handler.restartMetadaterPlugin(cfg) + }(m.metadater.config) + } + m.metadaterLock.RUnlock() + } +} + +func (m *Manager) restartNotifierPlugin(config Config, idx int) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed notifier plugin %#v, idx: %v", config.Cmd, idx) + plugin, err := newNotifierPlugin(config) + if err != nil { + logger.Error(logSender, "", "unable to restart notifier plugin %#v, err: %v", config.Cmd, err) + return + } + + m.notifLock.Lock() + plugin.queue = m.notifiers[idx].queue + m.notifiers[idx] = plugin + m.notifLock.Unlock() + plugin.sendQueuedEvents() +} + +func (m *Manager) restartKMSPlugin(config Config, idx int) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed kms plugin %#v, idx: %v", config.Cmd, idx) + plugin, err := newKMSPlugin(config) + if err != nil { + logger.Error(logSender, "", "unable to restart kms plugin %#v, err: %v", config.Cmd, err) + return + } + + m.kmsLock.Lock() + m.kms[idx] = plugin + m.kmsLock.Unlock() +} + +func (m *Manager) restartAuthPlugin(config Config, idx int) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed auth plugin %#v, idx: %v", config.Cmd, idx) + plugin, err := newAuthPlugin(config) + if err != nil { + logger.Error(logSender, "", "unable to restart auth plugin %#v, err: %v", config.Cmd, err) + return + } + + m.authLock.Lock() + m.auths[idx] = plugin + m.authLock.Unlock() +} + +func (m *Manager) restartSearcherPlugin(config Config) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed searcher plugin %#v", config.Cmd) + plugin, err := newSearcherPlugin(config) + if err != nil { + logger.Error(logSender, "", "unable to restart searcher plugin %#v, err: %v", config.Cmd, err) + return + } + + m.searcherLock.Lock() + m.searcher = plugin + m.searcherLock.Unlock() +} + +func (m *Manager) restartMetadaterPlugin(config Config) { + if atomic.LoadInt32(&m.closed) == 1 { + return + } + logger.Info(logSender, "", "try to restart crashed metadater plugin %#v", config.Cmd) + plugin, err := newMetadaterPlugin(config) + if err != nil { + logger.Error(logSender, "", "unable to restart metadater plugin %#v, err: %v", config.Cmd, err) + return + } + + m.metadaterLock.Lock() + m.metadater = plugin + m.metadaterLock.Unlock() +} + +// Cleanup releases all the active plugins +func (m *Manager) Cleanup() { + logger.Debug(logSender, "", "cleanup") + atomic.StoreInt32(&m.closed, 1) + close(m.done) + m.notifLock.Lock() + for _, n := range m.notifiers { + logger.Debug(logSender, "", "cleanup notifier plugin %v", n.config.Cmd) + n.cleanup() + } + m.notifLock.Unlock() + + m.kmsLock.Lock() + for _, k := range m.kms { + logger.Debug(logSender, "", "cleanup kms plugin %v", k.config.Cmd) + k.cleanup() + } + m.kmsLock.Unlock() + + m.authLock.Lock() + for _, a := range m.auths { + logger.Debug(logSender, "", "cleanup auth plugin %v", a.config.Cmd) + a.cleanup() + } + m.authLock.Unlock() + + if m.hasSearcher { + m.searcherLock.Lock() + logger.Debug(logSender, "", "cleanup searcher plugin %v", m.searcher.config.Cmd) + m.searcher.cleanup() + m.searcherLock.Unlock() + } + + if m.hasMetadater { + m.metadaterLock.Lock() + logger.Debug(logSender, "", "cleanup metadater plugin %v", m.metadater.config.Cmd) + m.metadater.cleanup() + m.metadaterLock.Unlock() + } +} + +func setLogLevel(logVerbose bool) { + if logVerbose { + pluginsLogLevel = hclog.Debug + } else { + pluginsLogLevel = hclog.Info + } +} + +func startCheckTicker() { + logger.Debug(logSender, "", "start plugins checker") + checker := time.NewTicker(30 * time.Second) + + go func() { + for { + select { + case <-Handler.done: + logger.Debug(logSender, "", "handler done, stop plugins checker") + checker.Stop() + return + case <-checker.C: + Handler.checkCrashedPlugins() + } + } + }() +} diff --git a/sdk/plugin/searcher.go b/plugin/searcher.go similarity index 97% rename from sdk/plugin/searcher.go rename to plugin/searcher.go index 574bb006..c515193b 100644 --- a/sdk/plugin/searcher.go +++ b/plugin/searcher.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" - "github.com/drakkan/sftpgo/v2/sdk/logger" + "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/sdk/plugin/eventsearcher" ) diff --git a/sdk/plugin/util.go b/plugin/util.go similarity index 91% rename from sdk/plugin/util.go rename to plugin/util.go index 09e7ee1f..5ef8c79f 100644 --- a/sdk/plugin/util.go +++ b/plugin/util.go @@ -3,7 +3,7 @@ package plugin import ( "github.com/shirou/gopsutil/v3/process" - "github.com/drakkan/sftpgo/v2/sdk/logger" + "github.com/drakkan/sftpgo/v2/logger" ) func killProcess(processPath string) { diff --git a/sdk/kms/kms.go b/sdk/kms/kms.go index 4f66ad5d..7d57ac36 100644 --- a/sdk/kms/kms.go +++ b/sdk/kms/kms.go @@ -9,7 +9,6 @@ import ( "sync" "github.com/drakkan/sftpgo/v2/sdk/logger" - "github.com/drakkan/sftpgo/v2/sdk/util" ) // SecretProvider defines the interface for a KMS secrets provider @@ -141,7 +140,7 @@ func (c *Configuration) Initialize() error { config.Secrets.URL = SchemeLocal + "://" } for k, v := range secretProviders { - logger.Debug(logSender, "secret provider registered for scheme: %#v, encrypted status: %#v", + logger.Info(logSender, "secret provider registered for scheme: %#v, encrypted status: %#v", k, v.encryptedStatus) } return nil @@ -217,8 +216,7 @@ func (s *Secret) UnmarshalJSON(data []byte) error { return nil } } - logger.Debug(logSender, "no provider registered for status %#v", baseSecret.Status) - + logger.Error(logSender, "no provider registered for status %#v", baseSecret.Status) return ErrInvalidSecret } @@ -399,7 +397,7 @@ func (s *Secret) IsValidInput() bool { s.RLock() defer s.RUnlock() - if !util.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) { + if !isSecretStatusValid(s.provider.GetStatus()) { return false } if s.provider.GetPayload() == "" { @@ -444,3 +442,12 @@ func (s *Secret) TryDecrypt() error { } return nil } + +func isSecretStatusValid(status string) bool { + for i := 0; i < len(validSecretStatuses); i++ { + if validSecretStatuses[i] == status { + return true + } + } + return false +} diff --git a/sdk/logger/logger.go b/sdk/logger/logger.go index a41dc946..883cb5f0 100644 --- a/sdk/logger/logger.go +++ b/sdk/logger/logger.go @@ -1,7 +1,12 @@ // Package logger provides logging capabilities. package logger -import "github.com/hashicorp/go-hclog" +const ( + levelDebug = iota + levelInfo + levelWarn + levelError +) var ( logger Logger @@ -13,10 +18,8 @@ func init() { // Logger interface type Logger interface { - // LogWithKeyVals logs at the specified level for the specified sender adding the specified key vals - LogWithKeyVals(level hclog.Level, sender, msg string, args ...interface{}) // Log logs at the specified level for the specified sender - Log(level hclog.Level, sender, format string, v ...interface{}) + Log(level int, sender, format string, v ...interface{}) } // SetLogger sets the specified logger @@ -31,26 +34,24 @@ func DisableLogger() { type noLogger struct{} -func (*noLogger) LogWithKeyVals(level hclog.Level, sender, msg string, args ...interface{}) {} - -func (*noLogger) Log(level hclog.Level, sender, format string, v ...interface{}) {} +func (*noLogger) Log(level int, sender, format string, v ...interface{}) {} // Debug logs at debug level for the specified sender func Debug(sender, format string, v ...interface{}) { - logger.Log(hclog.Debug, sender, format, v...) + logger.Log(levelDebug, sender, format, v...) } // Info logs at info level for the specified sender func Info(sender, format string, v ...interface{}) { - logger.Log(hclog.Info, sender, format, v...) + logger.Log(levelInfo, sender, format, v...) } // Warn logs at warn level for the specified sender func Warn(sender, format string, v ...interface{}) { - logger.Log(hclog.Warn, sender, format, v...) + logger.Log(levelWarn, sender, format, v...) } // Error logs at error level for the specified sender func Error(sender, format string, v ...interface{}) { - logger.Log(hclog.Error, sender, format, v...) + logger.Log(levelError, sender, format, v...) } diff --git a/sdk/plugin/plugin.go b/sdk/plugin/plugin.go index abef2c8c..b0736c3a 100644 --- a/sdk/plugin/plugin.go +++ b/sdk/plugin/plugin.go @@ -1,666 +1 @@ -// Package plugin provides support for the SFTPGo plugin system package plugin - -import ( - "crypto/x509" - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/hashicorp/go-hclog" - - "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin/auth" - "github.com/drakkan/sftpgo/v2/sdk/plugin/eventsearcher" - kmsplugin "github.com/drakkan/sftpgo/v2/sdk/plugin/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin/metadata" - "github.com/drakkan/sftpgo/v2/sdk/plugin/notifier" - "github.com/drakkan/sftpgo/v2/sdk/util" -) - -const ( - logSender = "plugins" -) - -var ( - // Handler defines the plugins manager - Handler Manager - pluginsLogLevel = hclog.Debug - // ErrNoSearcher defines the error to return for events searches if no plugin is configured - ErrNoSearcher = errors.New("no events searcher plugin defined") - // ErrNoMetadater returns the error to return for metadata methods if no plugin is configured - ErrNoMetadater = errors.New("no metadata plugin defined") -) - -// Renderer defines the interface for generic objects rendering -type Renderer interface { - RenderAsJSON(reload bool) ([]byte, error) -} - -// Config defines a plugin configuration -type Config struct { - // Plugin type - Type string `json:"type" mapstructure:"type"` - // NotifierOptions defines options for notifiers plugins - NotifierOptions NotifierConfig `json:"notifier_options" mapstructure:"notifier_options"` - // KMSOptions defines options for a KMS plugin - KMSOptions KMSConfig `json:"kms_options" mapstructure:"kms_options"` - // AuthOptions defines options for authentication plugins - AuthOptions AuthConfig `json:"auth_options" mapstructure:"auth_options"` - // Path to the plugin executable - Cmd string `json:"cmd" mapstructure:"cmd"` - // Args to pass to the plugin executable - Args []string `json:"args" mapstructure:"args"` - // SHA256 checksum for the plugin executable. - // If not empty it will be used to verify the integrity of the executable - SHA256Sum string `json:"sha256sum" mapstructure:"sha256sum"` - // If enabled the client and the server automatically negotiate mTLS for - // transport authentication. This ensures that only the original client will - // be allowed to connect to the server, and all other connections will be - // rejected. The client will also refuse to connect to any server that isn't - // the original instance started by the client. - AutoMTLS bool `json:"auto_mtls" mapstructure:"auto_mtls"` - // unique identifier for kms plugins - kmsID int -} - -func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider { - return &kmsPluginSecretProvider{ - BaseSecret: base, - URL: url, - MasterKey: masterKey, - config: c, - } -} - -// Manager handles enabled plugins -type Manager struct { - closed int32 - done chan bool - // List of configured plugins - Configs []Config `json:"plugins" mapstructure:"plugins"` - notifLock sync.RWMutex - notifiers []*notifierPlugin - kmsLock sync.RWMutex - kms []*kmsPlugin - authLock sync.RWMutex - auths []*authPlugin - searcherLock sync.RWMutex - searcher *searcherPlugin - metadaterLock sync.RWMutex - metadater *metadataPlugin - authScopes int - hasSearcher bool - hasMetadater bool - hasNotifiers bool -} - -// Initialize initializes the configured plugins -func Initialize(configs []Config, logVerbose bool) error { - logger.Debug(logSender, "initialize") - Handler = Manager{ - Configs: configs, - done: make(chan bool), - closed: 0, - authScopes: -1, - } - setLogLevel(logVerbose) - if len(configs) == 0 { - return nil - } - - if err := Handler.validateConfigs(); err != nil { - return err - } - - kmsID := 0 - for idx, config := range Handler.Configs { - switch config.Type { - case notifier.PluginName: - plugin, err := newNotifierPlugin(config) - if err != nil { - return err - } - Handler.notifiers = append(Handler.notifiers, plugin) - case kmsplugin.PluginName: - plugin, err := newKMSPlugin(config) - if err != nil { - return err - } - Handler.kms = append(Handler.kms, plugin) - Handler.Configs[idx].kmsID = kmsID - kmsID++ - kms.RegisterSecretProvider(config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus, - Handler.Configs[idx].newKMSPluginSecretProvider) - logger.Debug(logSender, "registered secret provider for scheme: %v, encrypted status: %v", - config.KMSOptions.Scheme, config.KMSOptions.EncryptedStatus) - case auth.PluginName: - plugin, err := newAuthPlugin(config) - if err != nil { - return err - } - Handler.auths = append(Handler.auths, plugin) - if Handler.authScopes == -1 { - Handler.authScopes = config.AuthOptions.Scope - } else { - Handler.authScopes |= config.AuthOptions.Scope - } - case eventsearcher.PluginName: - plugin, err := newSearcherPlugin(config) - if err != nil { - return err - } - Handler.searcher = plugin - case metadata.PluginName: - plugin, err := newMetadaterPlugin(config) - if err != nil { - return err - } - Handler.metadater = plugin - default: - return fmt.Errorf("unsupported plugin type: %v", config.Type) - } - } - startCheckTicker() - return nil -} - -func (m *Manager) validateConfigs() error { - kmsSchemes := make(map[string]bool) - kmsEncryptions := make(map[string]bool) - m.hasSearcher = false - m.hasMetadater = false - m.hasNotifiers = false - - for _, config := range m.Configs { - if config.Type == kmsplugin.PluginName { - if _, ok := kmsSchemes[config.KMSOptions.Scheme]; ok { - return fmt.Errorf("invalid KMS configuration, duplicated scheme %#v", config.KMSOptions.Scheme) - } - if _, ok := kmsEncryptions[config.KMSOptions.EncryptedStatus]; ok { - return fmt.Errorf("invalid KMS configuration, duplicated encrypted status %#v", config.KMSOptions.EncryptedStatus) - } - kmsSchemes[config.KMSOptions.Scheme] = true - kmsEncryptions[config.KMSOptions.EncryptedStatus] = true - } - if config.Type == eventsearcher.PluginName { - if m.hasSearcher { - return errors.New("only one eventsearcher plugin can be defined") - } - m.hasSearcher = true - } - if config.Type == metadata.PluginName { - if m.hasMetadater { - return errors.New("only one metadata plugin can be defined") - } - m.hasMetadater = true - } - if config.Type == notifier.PluginName { - m.hasNotifiers = true - } - } - return nil -} - -// HasNotifiers returns true if there is at least a notifier plugin -func (m *Manager) HasNotifiers() bool { - return m.hasNotifiers -} - -// NotifyFsEvent sends the fs event notifications using any defined notifier plugins -func (m *Manager) NotifyFsEvent(event *notifier.FsEvent) { - m.notifLock.RLock() - defer m.notifLock.RUnlock() - - for _, n := range m.notifiers { - n.notifyFsAction(event) - } -} - -// NotifyProviderEvent sends the provider event notifications using any defined notifier plugins -func (m *Manager) NotifyProviderEvent(event *notifier.ProviderEvent, object Renderer) { - m.notifLock.RLock() - defer m.notifLock.RUnlock() - - for _, n := range m.notifiers { - n.notifyProviderAction(event, object) - } -} - -// SearchFsEvents returns the filesystem events matching the specified filters -func (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]byte, []string, []string, error) { - if !m.hasSearcher { - return nil, nil, nil, ErrNoSearcher - } - m.searcherLock.RLock() - plugin := m.searcher - m.searcherLock.RUnlock() - - return plugin.searchear.SearchFsEvents(searchFilters) -} - -// SearchProviderEvents returns the provider events matching the specified filters -func (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEventSearch) ([]byte, []string, []string, error) { - if !m.hasSearcher { - return nil, nil, nil, ErrNoSearcher - } - m.searcherLock.RLock() - plugin := m.searcher - m.searcherLock.RUnlock() - - return plugin.searchear.SearchProviderEvents(searchFilters) -} - -// HasMetadater returns true if a metadata plugin is defined -func (m *Manager) HasMetadater() bool { - return m.hasMetadater -} - -// SetModificationTime sets the modification time for the specified object -func (m *Manager) SetModificationTime(storageID, objectPath string, mTime int64) error { - if !m.hasMetadater { - return ErrNoMetadater - } - m.metadaterLock.RLock() - plugin := m.metadater - m.metadaterLock.RUnlock() - - return plugin.metadater.SetModificationTime(storageID, objectPath, mTime) -} - -// GetModificationTime returns the modification time for the specified path -func (m *Manager) GetModificationTime(storageID, objectPath string, isDir bool) (int64, error) { - if !m.hasMetadater { - return 0, ErrNoMetadater - } - m.metadaterLock.RLock() - plugin := m.metadater - m.metadaterLock.RUnlock() - - return plugin.metadater.GetModificationTime(storageID, objectPath) -} - -// GetModificationTimes returns the modification times for all the files within the specified folder -func (m *Manager) GetModificationTimes(storageID, objectPath string) (map[string]int64, error) { - if !m.hasMetadater { - return nil, ErrNoMetadater - } - m.metadaterLock.RLock() - plugin := m.metadater - m.metadaterLock.RUnlock() - - return plugin.metadater.GetModificationTimes(storageID, objectPath) -} - -// RemoveMetadata deletes the metadata stored for the specified object -func (m *Manager) RemoveMetadata(storageID, objectPath string) error { - if !m.hasMetadater { - return ErrNoMetadater - } - m.metadaterLock.RLock() - plugin := m.metadater - m.metadaterLock.RUnlock() - - return plugin.metadater.RemoveMetadata(storageID, objectPath) -} - -// GetMetadataFolders returns the folders that metadata is associated with -func (m *Manager) GetMetadataFolders(storageID, from string, limit int) ([]string, error) { - if !m.hasMetadater { - return nil, ErrNoMetadater - } - m.metadaterLock.RLock() - plugin := m.metadater - m.metadaterLock.RUnlock() - - return plugin.metadater.GetFolders(storageID, limit, from) -} - -func (m *Manager) kmsEncrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, string, int32, error) { - m.kmsLock.RLock() - plugin := m.kms[kmsID] - m.kmsLock.RUnlock() - - return plugin.Encrypt(secret, url, masterKey) -} - -func (m *Manager) kmsDecrypt(secret kms.BaseSecret, url string, masterKey string, kmsID int) (string, error) { - m.kmsLock.RLock() - plugin := m.kms[kmsID] - m.kmsLock.RUnlock() - - return plugin.Decrypt(secret, url, masterKey) -} - -// HasAuthScope returns true if there is an auth plugin that support the specified scope -func (m *Manager) HasAuthScope(scope int) bool { - if m.authScopes == -1 { - return false - } - return m.authScopes&scope != 0 -} - -// Authenticate tries to authenticate the specified user using an external plugin -func (m *Manager) Authenticate(username, password, ip, protocol string, pkey string, - tlsCert *x509.Certificate, authScope int, userAsJSON []byte, -) ([]byte, error) { - switch authScope { - case AuthScopePassword: - return m.checkUserAndPass(username, password, ip, protocol, userAsJSON) - case AuthScopePublicKey: - return m.checkUserAndPublicKey(username, pkey, ip, protocol, userAsJSON) - case AuthScopeKeyboardInteractive: - return m.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON) - case AuthScopeTLSCertificate: - cert, err := util.EncodeTLSCertToPem(tlsCert) - if err != nil { - logger.Error(logSender, "unable to encode tls certificate to pem: %v", err) - return nil, fmt.Errorf("unable to encode tls cert to pem: %w", err) - } - return m.checkUserAndTLSCert(username, cert, ip, protocol, userAsJSON) - default: - return nil, fmt.Errorf("unsupported auth scope: %v", authScope) - } -} - -// ExecuteKeyboardInteractiveStep executes a keyboard interactive step -func (m *Manager) ExecuteKeyboardInteractiveStep(req *KeyboardAuthRequest) (*KeyboardAuthResponse, error) { - var plugin *authPlugin - - m.authLock.Lock() - for _, p := range m.auths { - if p.config.AuthOptions.Scope&AuthScopePassword != 0 { - plugin = p - break - } - } - m.authLock.Unlock() - - if plugin == nil { - return nil, errors.New("no auth plugin configured for keyaboard interactive authentication step") - } - - return plugin.sendKeyboardIteractiveRequest(req) -} - -func (m *Manager) checkUserAndPass(username, password, ip, protocol string, userAsJSON []byte) ([]byte, error) { - var plugin *authPlugin - - m.authLock.Lock() - for _, p := range m.auths { - if p.config.AuthOptions.Scope&AuthScopePassword != 0 { - plugin = p - break - } - } - m.authLock.Unlock() - - if plugin == nil { - return nil, errors.New("no auth plugin configured for password checking") - } - - return plugin.checkUserAndPass(username, password, ip, protocol, userAsJSON) -} - -func (m *Manager) checkUserAndPublicKey(username, pubKey, ip, protocol string, userAsJSON []byte) ([]byte, error) { - var plugin *authPlugin - - m.authLock.Lock() - for _, p := range m.auths { - if p.config.AuthOptions.Scope&AuthScopePublicKey != 0 { - plugin = p - break - } - } - m.authLock.Unlock() - - if plugin == nil { - return nil, errors.New("no auth plugin configured for public key checking") - } - - return plugin.checkUserAndPublicKey(username, pubKey, ip, protocol, userAsJSON) -} - -func (m *Manager) checkUserAndTLSCert(username, tlsCert, ip, protocol string, userAsJSON []byte) ([]byte, error) { - var plugin *authPlugin - - m.authLock.Lock() - for _, p := range m.auths { - if p.config.AuthOptions.Scope&AuthScopeTLSCertificate != 0 { - plugin = p - break - } - } - m.authLock.Unlock() - - if plugin == nil { - return nil, errors.New("no auth plugin configured for TLS certificate checking") - } - - return plugin.checkUserAndTLSCertificate(username, tlsCert, ip, protocol, userAsJSON) -} - -func (m *Manager) checkUserAndKeyboardInteractive(username, ip, protocol string, userAsJSON []byte) ([]byte, error) { - var plugin *authPlugin - - m.authLock.Lock() - for _, p := range m.auths { - if p.config.AuthOptions.Scope&AuthScopeKeyboardInteractive != 0 { - plugin = p - break - } - } - m.authLock.Unlock() - - if plugin == nil { - return nil, errors.New("no auth plugin configured for keyboard interactive checking") - } - - return plugin.checkUserAndKeyboardInteractive(username, ip, protocol, userAsJSON) -} - -func (m *Manager) checkCrashedPlugins() { - m.notifLock.RLock() - for idx, n := range m.notifiers { - if n.exited() { - defer func(cfg Config, index int) { - Handler.restartNotifierPlugin(cfg, index) - }(n.config, idx) - } else { - n.sendQueuedEvents() - } - } - m.notifLock.RUnlock() - - m.kmsLock.RLock() - for idx, k := range m.kms { - if k.exited() { - defer func(cfg Config, index int) { - Handler.restartKMSPlugin(cfg, index) - }(k.config, idx) - } - } - m.kmsLock.RUnlock() - - m.authLock.RLock() - for idx, a := range m.auths { - if a.exited() { - defer func(cfg Config, index int) { - Handler.restartAuthPlugin(cfg, index) - }(a.config, idx) - } - } - m.authLock.RUnlock() - - if m.hasSearcher { - m.searcherLock.RLock() - if m.searcher.exited() { - defer func(cfg Config) { - Handler.restartSearcherPlugin(cfg) - }(m.searcher.config) - } - m.searcherLock.RUnlock() - } - - if m.hasMetadater { - m.metadaterLock.RLock() - if m.metadater.exited() { - defer func(cfg Config) { - Handler.restartMetadaterPlugin(cfg) - }(m.metadater.config) - } - m.metadaterLock.RUnlock() - } -} - -func (m *Manager) restartNotifierPlugin(config Config, idx int) { - if atomic.LoadInt32(&m.closed) == 1 { - return - } - logger.Info(logSender, "try to restart crashed notifier plugin %#v, idx: %v", config.Cmd, idx) - plugin, err := newNotifierPlugin(config) - if err != nil { - logger.Error(logSender, "unable to restart notifier plugin %#v, err: %v", config.Cmd, err) - return - } - - m.notifLock.Lock() - plugin.queue = m.notifiers[idx].queue - m.notifiers[idx] = plugin - m.notifLock.Unlock() - plugin.sendQueuedEvents() -} - -func (m *Manager) restartKMSPlugin(config Config, idx int) { - if atomic.LoadInt32(&m.closed) == 1 { - return - } - logger.Info(logSender, "try to restart crashed kms plugin %#v, idx: %v", config.Cmd, idx) - plugin, err := newKMSPlugin(config) - if err != nil { - logger.Error(logSender, "unable to restart kms plugin %#v, err: %v", config.Cmd, err) - return - } - - m.kmsLock.Lock() - m.kms[idx] = plugin - m.kmsLock.Unlock() -} - -func (m *Manager) restartAuthPlugin(config Config, idx int) { - if atomic.LoadInt32(&m.closed) == 1 { - return - } - logger.Info(logSender, "try to restart crashed auth plugin %#v, idx: %v", config.Cmd, idx) - plugin, err := newAuthPlugin(config) - if err != nil { - logger.Error(logSender, "unable to restart auth plugin %#v, err: %v", config.Cmd, err) - return - } - - m.authLock.Lock() - m.auths[idx] = plugin - m.authLock.Unlock() -} - -func (m *Manager) restartSearcherPlugin(config Config) { - if atomic.LoadInt32(&m.closed) == 1 { - return - } - logger.Info(logSender, "try to restart crashed searcher plugin %#v", config.Cmd) - plugin, err := newSearcherPlugin(config) - if err != nil { - logger.Error(logSender, "unable to restart searcher plugin %#v, err: %v", config.Cmd, err) - return - } - - m.searcherLock.Lock() - m.searcher = plugin - m.searcherLock.Unlock() -} - -func (m *Manager) restartMetadaterPlugin(config Config) { - if atomic.LoadInt32(&m.closed) == 1 { - return - } - logger.Info(logSender, "try to restart crashed metadater plugin %#v", config.Cmd) - plugin, err := newMetadaterPlugin(config) - if err != nil { - logger.Error(logSender, "unable to restart metadater plugin %#v, err: %v", config.Cmd, err) - return - } - - m.metadaterLock.Lock() - m.metadater = plugin - m.metadaterLock.Unlock() -} - -// Cleanup releases all the active plugins -func (m *Manager) Cleanup() { - logger.Debug(logSender, "cleanup") - atomic.StoreInt32(&m.closed, 1) - close(m.done) - m.notifLock.Lock() - for _, n := range m.notifiers { - logger.Debug(logSender, "cleanup notifier plugin %v", n.config.Cmd) - n.cleanup() - } - m.notifLock.Unlock() - - m.kmsLock.Lock() - for _, k := range m.kms { - logger.Debug(logSender, "cleanup kms plugin %v", k.config.Cmd) - k.cleanup() - } - m.kmsLock.Unlock() - - m.authLock.Lock() - for _, a := range m.auths { - logger.Debug(logSender, "cleanup auth plugin %v", a.config.Cmd) - a.cleanup() - } - m.authLock.Unlock() - - if m.hasSearcher { - m.searcherLock.Lock() - logger.Debug(logSender, "cleanup searcher plugin %v", m.searcher.config.Cmd) - m.searcher.cleanup() - m.searcherLock.Unlock() - } - - if m.hasMetadater { - m.metadaterLock.Lock() - logger.Debug(logSender, "cleanup metadater plugin %v", m.metadater.config.Cmd) - m.metadater.cleanup() - m.metadaterLock.Unlock() - } -} - -func setLogLevel(logVerbose bool) { - if logVerbose { - pluginsLogLevel = hclog.Debug - } else { - pluginsLogLevel = hclog.Info - } -} - -func startCheckTicker() { - logger.Debug(logSender, "start plugins checker") - checker := time.NewTicker(30 * time.Second) - - go func() { - for { - select { - case <-Handler.done: - logger.Debug(logSender, "handler done, stop plugins checker") - checker.Stop() - return - case <-checker.C: - Handler.checkCrashedPlugins() - } - } - }() -} diff --git a/sdk/user.go b/sdk/user.go index 0ea600d1..12f31abc 100644 --- a/sdk/user.go +++ b/sdk/user.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/util" ) // Web Client/user REST API restrictions @@ -53,11 +52,6 @@ type DirectoryPermissions struct { Permissions []string } -// HasPerm returns true if the directory has the specified permissions -func (d *DirectoryPermissions) HasPerm(perm string) bool { - return util.IsStringInSlice(perm, d.Permissions) -} - // PatternsFilter defines filters based on shell like patterns. // These restrictions do not apply to files listing for performance reasons, so // a denied file cannot be downloaded/overwritten/renamed but will still be diff --git a/sdk/util/util.go b/sdk/util/util.go deleted file mode 100644 index 51186389..00000000 --- a/sdk/util/util.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package util provides some common utility methods -package util - -import ( - "crypto/x509" - "encoding/pem" - "errors" -) - -// IsStringInSlice searches a string in a slice and returns true if the string is found -func IsStringInSlice(obj string, list []string) bool { - for i := 0; i < len(list); i++ { - if list[i] == obj { - return true - } - } - return false -} - -// EncodeTLSCertToPem returns the specified certificate PEM encoded. -// This can be verified using openssl x509 -in cert.crt -text -noout -func EncodeTLSCertToPem(tlsCert *x509.Certificate) (string, error) { - if len(tlsCert.Raw) == 0 { - return "", errors.New("invalid x509 certificate, no der contents") - } - publicKeyBlock := pem.Block{ - Type: "CERTIFICATE", - Bytes: tlsCert.Raw, - } - return string(pem.EncodeToMemory(&publicKeyBlock)), nil -} diff --git a/service/service.go b/service/service.go index 0e222283..214aab26 100644 --- a/service/service.go +++ b/service/service.go @@ -14,7 +14,7 @@ import ( "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/version" ) diff --git a/service/service_windows.go b/service/service_windows.go index 310de562..7b64c0a1 100644 --- a/service/service_windows.go +++ b/service/service_windows.go @@ -16,7 +16,7 @@ import ( "github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/telemetry" "github.com/drakkan/sftpgo/v2/webdavd" ) diff --git a/service/signals_unix.go b/service/signals_unix.go index 5f43e60b..5eb7c8a4 100644 --- a/service/signals_unix.go +++ b/service/signals_unix.go @@ -13,7 +13,7 @@ import ( "github.com/drakkan/sftpgo/v2/ftpd" "github.com/drakkan/sftpgo/v2/httpd" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/telemetry" "github.com/drakkan/sftpgo/v2/webdavd" ) diff --git a/service/signals_windows.go b/service/signals_windows.go index 154a1075..23b7cbdf 100644 --- a/service/signals_windows.go +++ b/service/signals_windows.go @@ -5,7 +5,7 @@ import ( "os/signal" "github.com/drakkan/sftpgo/v2/logger" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" ) func registerSignals() { diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index 048b4b31..9fc45e7a 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -16,7 +16,6 @@ import ( "github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/logger" - sdklogger "github.com/drakkan/sftpgo/v2/sdk/logger" "github.com/drakkan/sftpgo/v2/util" ) @@ -96,7 +95,7 @@ func (c Conf) Initialize(configDir string) error { WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 14, // 16KB - ErrorLog: log.New(&sdklogger.StdLoggerWrapper{Sender: logSender}, "", 0), + ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), } if certificateFile != "" && certificateKeyFile != "" { certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender) diff --git a/util/util.go b/util/util.go index 935aaa79..dfaa9f21 100644 --- a/util/util.go +++ b/util/util.go @@ -11,6 +11,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "errors" "fmt" "html/template" "io" @@ -422,6 +423,19 @@ func GetTLSCiphersFromNames(cipherNames []string) []uint16 { return ciphers } +// EncodeTLSCertToPem returns the specified certificate PEM encoded. +// This can be verified using openssl x509 -in cert.crt -text -noout +func EncodeTLSCertToPem(tlsCert *x509.Certificate) (string, error) { + if len(tlsCert.Raw) == 0 { + return "", errors.New("invalid x509 certificate, no der contents") + } + publicKeyBlock := pem.Block{ + Type: "CERTIFICATE", + Bytes: tlsCert.Raw, + } + return string(pem.EncodeToMemory(&publicKeyBlock)), nil +} + // CheckTCP4Port quits the app if bind on the given IPv4 port fails. // This is a ugly hack to avoid to bind on an already used port. // It is required on Windows only. Upstream does not consider this diff --git a/vfs/azblobfs.go b/vfs/azblobfs.go index 81f5411f..08fd5b95 100644 --- a/vfs/azblobfs.go +++ b/vfs/azblobfs.go @@ -26,7 +26,7 @@ import ( "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/version" ) diff --git a/vfs/gcsfs.go b/vfs/gcsfs.go index 6af87892..627c3d39 100644 --- a/vfs/gcsfs.go +++ b/vfs/gcsfs.go @@ -25,8 +25,8 @@ import ( "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/version" ) diff --git a/vfs/s3fs.go b/vfs/s3fs.go index e9c09f1a..6a85cda3 100644 --- a/vfs/s3fs.go +++ b/vfs/s3fs.go @@ -26,7 +26,7 @@ import ( "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" - "github.com/drakkan/sftpgo/v2/sdk/plugin" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/version" ) diff --git a/vfs/vfs.go b/vfs/vfs.go index 73807f04..6f2ec171 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -17,9 +17,9 @@ import ( "github.com/pkg/sftp" "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/plugin" "github.com/drakkan/sftpgo/v2/sdk" "github.com/drakkan/sftpgo/v2/sdk/kms" - "github.com/drakkan/sftpgo/v2/sdk/plugin" "github.com/drakkan/sftpgo/v2/sdk/plugin/metadata" "github.com/drakkan/sftpgo/v2/util" ) diff --git a/webdavd/server.go b/webdavd/server.go index 841bc5a5..1b333aee 100644 --- a/webdavd/server.go +++ b/webdavd/server.go @@ -23,7 +23,6 @@ import ( "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/metric" - sdklogger "github.com/drakkan/sftpgo/v2/sdk/logger" "github.com/drakkan/sftpgo/v2/util" ) @@ -40,7 +39,7 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error { WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16, // 64KB - ErrorLog: log.New(&sdklogger.StdLoggerWrapper{Sender: logSender}, "", 0), + ErrorLog: log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0), } if s.config.Cors.Enabled { c := cors.New(cors.Options{