Sfoglia il codice sorgente

replace utils.Contains with slices.Contains

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 anno fa
parent
commit
d94f80c8da
51 ha cambiato i file con 353 aggiunte e 322 eliminazioni
  1. 2 1
      internal/acme/acme.go
  2. 2 3
      internal/command/command.go
  3. 5 4
      internal/common/actions.go
  4. 4 3
      internal/common/common.go
  5. 4 3
      internal/common/common_test.go
  6. 2 2
      internal/common/connection.go
  7. 2 1
      internal/common/connection_test.go
  8. 5 4
      internal/common/eventmanager.go
  9. 35 34
      internal/common/protocol_test.go
  10. 2 1
      internal/common/ratelimiter.go
  11. 3 2
      internal/common/tlsutils.go
  12. 2 1
      internal/config/config.go
  13. 11 11
      internal/config/config_test.go
  14. 3 2
      internal/dataprovider/actions.go
  15. 7 6
      internal/dataprovider/admin.go
  16. 11 10
      internal/dataprovider/bolt.go
  17. 6 5
      internal/dataprovider/configs.go
  18. 33 32
      internal/dataprovider/dataprovider.go
  19. 14 13
      internal/dataprovider/eventrule.go
  20. 2 1
      internal/dataprovider/iplist.go
  21. 9 8
      internal/dataprovider/memory.go
  22. 2 1
      internal/dataprovider/pgsql.go
  23. 28 27
      internal/dataprovider/user.go
  24. 2 1
      internal/ftpd/server.go
  25. 2 1
      internal/httpd/api_mfa.go
  26. 6 5
      internal/httpd/api_shares.go
  27. 3 2
      internal/httpd/api_utils.go
  28. 12 11
      internal/httpd/auth_utils.go
  29. 26 25
      internal/httpd/httpd_test.go
  30. 6 5
      internal/httpd/middleware.go
  31. 2 1
      internal/httpd/oidc.go
  32. 6 5
      internal/httpd/server.go
  33. 8 7
      internal/httpd/webadmin.go
  34. 4 3
      internal/httpd/webclient.go
  35. 25 25
      internal/httpdtest/httpdtest.go
  36. 3 3
      internal/plugin/kms.go
  37. 4 4
      internal/plugin/notifier.go
  38. 2 1
      internal/plugin/plugin.go
  39. 2 1
      internal/service/service_portable.go
  40. 4 3
      internal/sftpd/internal_test.go
  41. 17 16
      internal/sftpd/server.go
  42. 3 2
      internal/sftpd/sftpd_test.go
  43. 6 5
      internal/sftpd/ssh_cmd.go
  44. 2 2
      internal/smtp/oauth2.go
  45. 0 10
      internal/util/util.go
  46. 2 2
      internal/vfs/osfs.go
  47. 2 1
      internal/vfs/s3fs.go
  48. 4 3
      internal/vfs/sftpfs.go
  49. 2 1
      internal/vfs/vfs.go
  50. 2 1
      internal/webdavd/file.go
  51. 2 1
      internal/webdavd/server.go

+ 2 - 1
internal/acme/acme.go

@@ -30,6 +30,7 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -249,7 +250,7 @@ func (c *Configuration) Initialize(configDir string) error {
 	if c.RenewDays < 1 {
 	if c.RenewDays < 1 {
 		return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
 		return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
 	}
 	}
-	if !util.Contains(supportedKeyTypes, c.KeyType) {
+	if !slices.Contains(supportedKeyTypes, c.KeyType) {
 		return fmt.Errorf("invalid key type %q", c.KeyType)
 		return fmt.Errorf("invalid key type %q", c.KeyType)
 	}
 	}
 	caURL, err := url.Parse(c.CAEndpoint)
 	caURL, err := url.Parse(c.CAEndpoint)

+ 2 - 3
internal/command/command.go

@@ -17,10 +17,9 @@ package command
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
-
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 )
 
 
 const (
 const (
@@ -117,7 +116,7 @@ func (c Config) Initialize() error {
 		}
 		}
 		// don't validate args, we allow to pass empty arguments
 		// don't validate args, we allow to pass empty arguments
 		if cmd.Hook != "" {
 		if cmd.Hook != "" {
-			if !util.Contains(supportedHooks, cmd.Hook) {
+			if !slices.Contains(supportedHooks, cmd.Hook) {
 				return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
 				return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
 			}
 			}
 		}
 		}

+ 5 - 4
internal/common/actions.go

@@ -25,6 +25,7 @@ import (
 	"os/exec"
 	"os/exec"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
@@ -86,7 +87,7 @@ func InitializeActionHandler(handler ActionHandler) {
 func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
 func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
 	var event *notifier.FsEvent
 	var event *notifier.FsEvent
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
-	hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
+	hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
 	hasRules := eventManager.hasFsRules()
 	hasRules := eventManager.hasFsRules()
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 		return 0, nil
 		return 0, nil
@@ -132,7 +133,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
 	fileSize int64, err error, elapsed int64, metadata map[string]string,
 	fileSize int64, err error, elapsed int64, metadata map[string]string,
 ) error {
 ) error {
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
-	hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
+	hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
 	hasRules := eventManager.hasFsRules()
 	hasRules := eventManager.hasFsRules()
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 		return nil
 		return nil
@@ -173,7 +174,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
 		}
 		}
 	}
 	}
 	if hasHook {
 	if hasHook {
-		if util.Contains(Config.Actions.ExecuteSync, operation) {
+		if slices.Contains(Config.Actions.ExecuteSync, operation) {
 			_, err := actionHandler.Handle(notification)
 			_, err := actionHandler.Handle(notification)
 			return err
 			return err
 		}
 		}
@@ -247,7 +248,7 @@ func newActionNotification(
 type defaultActionHandler struct{}
 type defaultActionHandler struct{}
 
 
 func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
 func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
-	if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
+	if !slices.Contains(Config.Actions.ExecuteOn, event.Action) {
 		return 0, nil
 		return 0, nil
 	}
 	}
 
 

+ 4 - 3
internal/common/common.go

@@ -25,6 +25,7 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -207,7 +208,7 @@ func Initialize(c Configuration, isShared int) error {
 		Config.rateLimitersList = rateLimitersList
 		Config.rateLimitersList = rateLimitersList
 	}
 	}
 	if c.DefenderConfig.Enabled {
 	if c.DefenderConfig.Enabled {
-		if !util.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
+		if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
 			return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
 			return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
 		}
 		}
 		var defender Defender
 		var defender Defender
@@ -777,7 +778,7 @@ func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username,
 	if c.PostDisconnectHook == "" {
 	if c.PostDisconnectHook == "" {
 		return
 		return
 	}
 	}
-	if !util.Contains(disconnHookProtocols, protocol) {
+	if !slices.Contains(disconnHookProtocols, protocol) {
 		return
 		return
 	}
 	}
 	go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
 	go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
@@ -1019,7 +1020,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
 		metric.UpdateActiveConnectionsSize(lastIdx)
 		metric.UpdateActiveConnectionsSize(lastIdx)
 		logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
 		logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
 			conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
 			conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
-		if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
+		if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {
 			ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
 			ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
 			logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
 			logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
 				dataprovider.ErrNoAuthTried.Error())
 				dataprovider.ErrNoAuthTried.Error())

+ 4 - 3
internal/common/common_test.go

@@ -23,6 +23,7 @@ import (
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"sync"
 	"sync"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -1226,8 +1227,8 @@ func TestFolderCopy(t *testing.T) {
 	folder.ID = 2
 	folder.ID = 2
 	folder.Users = []string{"user3"}
 	folder.Users = []string{"user3"}
 	require.Len(t, folderCopy.Users, 2)
 	require.Len(t, folderCopy.Users, 2)
-	require.True(t, util.Contains(folderCopy.Users, "user1"))
-	require.True(t, util.Contains(folderCopy.Users, "user2"))
+	require.True(t, slices.Contains(folderCopy.Users, "user1"))
+	require.True(t, slices.Contains(folderCopy.Users, "user2"))
 	require.Equal(t, int64(1), folderCopy.ID)
 	require.Equal(t, int64(1), folderCopy.ID)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
@@ -1243,7 +1244,7 @@ func TestFolderCopy(t *testing.T) {
 	folderCopy = folder.GetACopy()
 	folderCopy = folder.GetACopy()
 	folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
 	folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
 	require.Len(t, folderCopy.Users, 1)
 	require.Len(t, folderCopy.Users, 1)
-	require.True(t, util.Contains(folderCopy.Users, "user3"))
+	require.True(t, slices.Contains(folderCopy.Users, "user3"))
 	require.Equal(t, int64(2), folderCopy.ID)
 	require.Equal(t, int64(2), folderCopy.ID)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)

+ 2 - 2
internal/common/connection.go

@@ -63,7 +63,7 @@ type BaseConnection struct {
 // NewBaseConnection returns a new BaseConnection
 // NewBaseConnection returns a new BaseConnection
 func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
 func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
 	connID := id
 	connID := id
-	if util.Contains(supportedProtocols, protocol) {
+	if slices.Contains(supportedProtocols, protocol) {
 		connID = fmt.Sprintf("%s_%s", protocol, id)
 		connID = fmt.Sprintf("%s_%s", protocol, id)
 	}
 	}
 	user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
 	user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
@@ -132,7 +132,7 @@ func (c *BaseConnection) GetRemoteIP() string {
 // SetProtocol sets the protocol for this connection
 // SetProtocol sets the protocol for this connection
 func (c *BaseConnection) SetProtocol(protocol string) {
 func (c *BaseConnection) SetProtocol(protocol string) {
 	c.protocol = protocol
 	c.protocol = protocol
-	if util.Contains(supportedProtocols, c.protocol) {
+	if slices.Contains(supportedProtocols, c.protocol) {
 		c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
 		c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
 	}
 	}
 }
 }

+ 2 - 1
internal/common/connection_test.go

@@ -22,6 +22,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"strconv"
 	"strconv"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -389,7 +390,7 @@ func TestErrorsMapping(t *testing.T) {
 		err := conn.GetFsError(fs, os.ErrNotExist)
 		err := conn.GetFsError(fs, os.ErrNotExist)
 		if protocol == ProtocolSFTP {
 		if protocol == ProtocolSFTP {
 			assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
 			assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
-		} else if util.Contains(osErrorsProtocols, protocol) {
+		} else if slices.Contains(osErrorsProtocols, protocol) {
 			assert.EqualError(t, err, os.ErrNotExist.Error())
 			assert.EqualError(t, err, os.ErrNotExist.Error())
 		} else {
 		} else {
 			assert.EqualError(t, err, ErrNotExist.Error())
 			assert.EqualError(t, err, ErrNotExist.Error())

+ 5 - 4
internal/common/eventmanager.go

@@ -31,6 +31,7 @@ import (
 	"os/exec"
 	"os/exec"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -307,7 +308,7 @@ func (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.Eve
 }
 }
 
 
 func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
 func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
-	if !util.Contains(conditions.ProviderEvents, params.Event) {
+	if !slices.Contains(conditions.ProviderEvents, params.Event) {
 		return false
 		return false
 	}
 	}
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
@@ -316,14 +317,14 @@ func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.Eve
 	if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
 	if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
 		return false
 		return false
 	}
 	}
-	if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
+	if len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
 		return false
 		return false
 	}
 	}
 	return true
 	return true
 }
 }
 
 
 func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
 func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
-	if !util.Contains(conditions.FsEvents, params.Event) {
+	if !slices.Contains(conditions.FsEvents, params.Event) {
 		return false
 		return false
 	}
 	}
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
@@ -338,7 +339,7 @@ func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventCond
 	if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
 	if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
 		return false
 		return false
 	}
 	}
-	if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
+	if len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {
 		return false
 		return false
 	}
 	}
 	if params.Event == operationUpload || params.Event == operationDownload {
 	if params.Event == operationUpload || params.Event == operationDownload {

+ 35 - 34
internal/common/protocol_test.go

@@ -30,6 +30,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"testing"
 	"testing"
@@ -3978,9 +3979,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username))
 		// test the failure action, we download a file that exceeds the transfer quota limit
 		// test the failure action, we download a file that exceeds the transfer quota limit
 		err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client)
 		err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client)
@@ -3999,9 +4000,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username))
 		assert.Contains(t, email.Data, `"download" failed`)
 		assert.Contains(t, email.Data, `"download" failed`)
 		assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error())
 		assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error())
@@ -4019,7 +4020,7 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name))
 		assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name))
 		// now test the download rule
 		// now test the download rule
@@ -4036,9 +4037,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username))
 	}
 	}
 	// test upload action command with arguments
 	// test upload action command with arguments
@@ -4079,9 +4080,9 @@ func TestEventRule(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 3)
 	assert.Len(t, email.To, 3)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`)
 	assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`)
 	_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
 	_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -4236,7 +4237,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: New "update" from "admin"`)
 		assert.Contains(t, email.Data, `Subject: New "update" from "admin"`)
 	}
 	}
 	// now delete the script to generate an error
 	// now delete the script to generate an error
@@ -4251,7 +4252,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`)
 	assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`)
 	assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
 	assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
 	lastReceivedEmail.reset()
 	lastReceivedEmail.reset()
@@ -5306,7 +5307,7 @@ func TestBackupAsAttachment(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, `Domain: example.com`)
 	assert.Contains(t, email.Data, `Domain: example.com`)
 	assert.Contains(t, email.Data, "Content-Type: application/json")
 	assert.Contains(t, email.Data, "Content-Type: application/json")
@@ -5676,7 +5677,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		}, 3*time.Second, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		// update quota size so the user is already overquota
 		// update quota size so the user is already overquota
@@ -5691,7 +5692,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		}, 3*time.Second, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		// remove the path to compress to trigger an error for size estimation
 		// remove the path to compress to trigger an error for size estimation
@@ -5705,7 +5706,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		}, 3*time.Second, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, "unable to estimate archive size")
 		assert.Contains(t, email.Data, "unable to estimate archive size")
 	}
 	}
@@ -6041,7 +6042,7 @@ func TestEventActionEmailAttachments(t *testing.T) {
 			}, 1500*time.Millisecond, 100*time.Millisecond)
 			}, 1500*time.Millisecond, 100*time.Millisecond)
 			email := lastReceivedEmail.get()
 			email := lastReceivedEmail.get()
 			assert.Len(t, email.To, 1)
 			assert.Len(t, email.To, 1)
-			assert.True(t, util.Contains(email.To, "[email protected]"))
+			assert.True(t, slices.Contains(email.To, "[email protected]"))
 			assert.Contains(t, email.Data, `Subject: "upload" from`)
 			assert.Contains(t, email.Data, `Subject: "upload" from`)
 			assert.Contains(t, email.Data, "Content-Disposition: attachment")
 			assert.Contains(t, email.Data, "Content-Disposition: attachment")
 		}
 		}
@@ -6218,7 +6219,7 @@ func TestEventActionsRetentionReports(t *testing.T) {
 
 
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, "Content-Disposition: attachment")
 		assert.Contains(t, email.Data, "Content-Disposition: attachment")
 		_, err = client.Stat(testDir)
 		_, err = client.Stat(testDir)
@@ -6391,7 +6392,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username))
 		lastReceivedEmail.reset()
 		lastReceivedEmail.reset()
 		// a new upload will not produce a new notification
 		// a new upload will not produce a new notification
@@ -6414,7 +6415,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username))
 		// download again
 		// download again
 		lastReceivedEmail.reset()
 		lastReceivedEmail.reset()
@@ -6510,7 +6511,7 @@ func TestEventRuleRenameEvent(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, "Content-Type: text/html")
 		assert.Contains(t, email.Data, "Content-Type: text/html")
 		assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
 		assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
@@ -6644,7 +6645,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginUser))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginUser))
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, custom1)
 	assert.Contains(t, email.Data, custom1)
@@ -6708,7 +6709,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginAdmin))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginAdmin))
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, custom1)
 	assert.Contains(t, email.Data, custom1)
@@ -6900,7 +6901,7 @@ func TestEventRuleEmailField(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, user.Email))
+	assert.True(t, slices.Contains(email.To, user.Email))
 	assert.Contains(t, email.Data, `Subject: "add" from "admin"`)
 	assert.Contains(t, email.Data, `Subject: "add" from "admin"`)
 
 
 	// if we add a user without email the notification will fail
 	// if we add a user without email the notification will fail
@@ -6914,7 +6915,7 @@ func TestEventRuleEmailField(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `no recipient addresses set`)
 	assert.Contains(t, email.Data, `no recipient addresses set`)
 
 
 	conn, client, err := getSftpClient(user)
 	conn, client, err := getSftpClient(user)
@@ -6931,7 +6932,7 @@ func TestEventRuleEmailField(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, user.Email))
+		assert.True(t, slices.Contains(email.To, user.Email))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "mkdir" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "mkdir" from "%s"`, user.Username))
 	}
 	}
 
 
@@ -7038,7 +7039,7 @@ func TestEventRuleCertificate(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, "Content-Type: text/plain")
 	assert.Contains(t, email.Data, "Content-Type: text/plain")
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
@@ -7058,7 +7059,7 @@ func TestEventRuleCertificate(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent))
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
 	assert.Contains(t, email.Data, errRenew.Error())
 	assert.Contains(t, email.Data, errRenew.Error())
@@ -7184,8 +7185,8 @@ func TestEventRuleIPBlocked(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 2)
 	assert.Len(t, email.To, 2)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: New "IP Blocked"`)
 	assert.Contains(t, email.Data, `Subject: New "IP Blocked"`)
 
 
 	err = dataprovider.DeleteEventRule(rule1.Name, "", "", "")
 	err = dataprovider.DeleteEventRule(rule1.Name, "", "", "")
@@ -8357,7 +8358,7 @@ func TestSFTPLoopError(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: Failed action`)
 	assert.Contains(t, email.Data, `Subject: Failed action`)
 
 
 	user1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)
 	user1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)

+ 2 - 1
internal/common/ratelimiter.go

@@ -17,6 +17,7 @@ package common
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"slices"
 	"sort"
 	"sort"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
@@ -94,7 +95,7 @@ func (r *RateLimiterConfig) validate() error {
 	}
 	}
 	r.Protocols = util.RemoveDuplicates(r.Protocols, true)
 	r.Protocols = util.RemoveDuplicates(r.Protocols, true)
 	for _, protocol := range r.Protocols {
 	for _, protocol := range r.Protocols {
-		if !util.Contains(rateLimiterProtocolValues, protocol) {
+		if !slices.Contains(rateLimiterProtocolValues, protocol) {
 			return fmt.Errorf("invalid protocol %q", protocol)
 			return fmt.Errorf("invalid protocol %q", protocol)
 		}
 		}
 	}
 	}

+ 3 - 2
internal/common/tlsutils.go

@@ -25,6 +25,7 @@ import (
 	"math/rand"
 	"math/rand"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"sync"
 	"sync"
 
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
@@ -96,7 +97,7 @@ func (m *CertManager) loadCertificates() error {
 		}
 		}
 		logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
 		logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
 		certs[keyPair.ID] = &newCert
 		certs[keyPair.ID] = &newCert
-		if !util.Contains(m.monitorList, keyPair.Cert) {
+		if !slices.Contains(m.monitorList, keyPair.Cert) {
 			m.monitorList = append(m.monitorList, keyPair.Cert)
 			m.monitorList = append(m.monitorList, keyPair.Cert)
 		}
 		}
 	}
 	}
@@ -190,7 +191,7 @@ func (m *CertManager) LoadCRLs() error {
 
 
 		logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
 		logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
 		crls = append(crls, crl)
 		crls = append(crls, crl)
-		if !util.Contains(m.monitorList, revocationList) {
+		if !slices.Contains(m.monitorList, revocationList) {
 			m.monitorList = append(m.monitorList, revocationList)
 			m.monitorList = append(m.monitorList, revocationList)
 		}
 		}
 	}
 	}

+ 2 - 1
internal/config/config.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -716,7 +717,7 @@ func checkOverrideDefaultSettings() {
 		}
 		}
 	}
 	}
 
 
-	if util.Contains(viper.AllKeys(), "mfa.totp") {
+	if slices.Contains(viper.AllKeys(), "mfa.totp") {
 		globalConf.MFAConfig.TOTP = nil
 		globalConf.MFAConfig.TOTP = nil
 	}
 	}
 }
 }

+ 11 - 11
internal/config/config_test.go

@@ -19,6 +19,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"testing"
 	"testing"
 
 
 	"github.com/sftpgo/sdk/kms"
 	"github.com/sftpgo/sdk/kms"
@@ -36,7 +37,6 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/plugin"
 	"github.com/drakkan/sftpgo/v2/internal/plugin"
 	"github.com/drakkan/sftpgo/v2/internal/sftpd"
 	"github.com/drakkan/sftpgo/v2/internal/sftpd"
 	"github.com/drakkan/sftpgo/v2/internal/smtp"
 	"github.com/drakkan/sftpgo/v2/internal/smtp"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/webdavd"
 	"github.com/drakkan/sftpgo/v2/internal/webdavd"
 )
 )
 
 
@@ -679,8 +679,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	pluginConf := pluginsConf[0]
 	pluginConf := pluginsConf[0]
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@@ -729,8 +729,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	pluginConf = pluginsConf[0]
 	pluginConf = pluginsConf[0]
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@@ -787,8 +787,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
 	require.Equal(t, 2, limiters[0].Type)
 	require.Equal(t, 2, limiters[0].Type)
 	protocols := limiters[0].Protocols
 	protocols := limiters[0].Protocols
 	require.Len(t, protocols, 2)
 	require.Len(t, protocols, 2)
-	require.True(t, util.Contains(protocols, common.ProtocolFTP))
-	require.True(t, util.Contains(protocols, common.ProtocolSSH))
+	require.True(t, slices.Contains(protocols, common.ProtocolFTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolSSH))
 	require.True(t, limiters[0].GenerateDefenderEvents)
 	require.True(t, limiters[0].GenerateDefenderEvents)
 	require.Equal(t, 50, limiters[0].EntriesSoftLimit)
 	require.Equal(t, 50, limiters[0].EntriesSoftLimit)
 	require.Equal(t, 100, limiters[0].EntriesHardLimit)
 	require.Equal(t, 100, limiters[0].EntriesHardLimit)
@@ -799,10 +799,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
 	require.Equal(t, 2, limiters[1].Type)
 	require.Equal(t, 2, limiters[1].Type)
 	protocols = limiters[1].Protocols
 	protocols = limiters[1].Protocols
 	require.Len(t, protocols, 4)
 	require.Len(t, protocols, 4)
-	require.True(t, util.Contains(protocols, common.ProtocolFTP))
-	require.True(t, util.Contains(protocols, common.ProtocolSSH))
-	require.True(t, util.Contains(protocols, common.ProtocolWebDAV))
-	require.True(t, util.Contains(protocols, common.ProtocolHTTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolFTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolSSH))
+	require.True(t, slices.Contains(protocols, common.ProtocolWebDAV))
+	require.True(t, slices.Contains(protocols, common.ProtocolHTTP))
 	require.False(t, limiters[1].GenerateDefenderEvents)
 	require.False(t, limiters[1].GenerateDefenderEvents)
 	require.Equal(t, 100, limiters[1].EntriesSoftLimit)
 	require.Equal(t, 100, limiters[1].EntriesSoftLimit)
 	require.Equal(t, 150, limiters[1].EntriesHardLimit)
 	require.Equal(t, 150, limiters[1].EntriesHardLimit)

+ 3 - 2
internal/dataprovider/actions.go

@@ -21,6 +21,7 @@ import (
 	"net/url"
 	"net/url"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -78,8 +79,8 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
 	if config.Actions.Hook == "" {
 	if config.Actions.Hook == "" {
 		return
 		return
 	}
 	}
-	if !util.Contains(config.Actions.ExecuteOn, operation) ||
-		!util.Contains(config.Actions.ExecuteFor, objectType) {
+	if !slices.Contains(config.Actions.ExecuteOn, operation) ||
+		!slices.Contains(config.Actions.ExecuteFor, objectType) {
 		return
 		return
 	}
 	}
 
 

+ 7 - 6
internal/dataprovider/admin.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 	"os"
 	"os"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -96,7 +97,7 @@ func (c *AdminTOTPConfig) validate(username string) error {
 	if c.ConfigName == "" {
 	if c.ConfigName == "" {
 		return util.NewValidationError("totp: config name is mandatory")
 		return util.NewValidationError("totp: config name is mandatory")
 	}
 	}
-	if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
+	if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 	}
 	}
 	if c.Secret.IsEmpty() {
 	if c.Secret.IsEmpty() {
@@ -337,15 +338,15 @@ func (a *Admin) validatePermissions() error {
 			util.I18nErrorPermissionsRequired,
 			util.I18nErrorPermissionsRequired,
 		)
 		)
 	}
 	}
-	if util.Contains(a.Permissions, PermAdminAny) {
+	if slices.Contains(a.Permissions, PermAdminAny) {
 		a.Permissions = []string{PermAdminAny}
 		a.Permissions = []string{PermAdminAny}
 	}
 	}
 	for _, perm := range a.Permissions {
 	for _, perm := range a.Permissions {
-		if !util.Contains(validAdminPerms, perm) {
+		if !slices.Contains(validAdminPerms, perm) {
 			return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
 			return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
 		}
 		}
 		if a.Role != "" {
 		if a.Role != "" {
-			if util.Contains(forbiddenPermsForRoleAdmins, perm) {
+			if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
 				deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
 				deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
 				return util.NewI18nError(
 				return util.NewI18nError(
 					util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
 					util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
@@ -559,10 +560,10 @@ func (a *Admin) SetNilSecretsIfEmpty() {
 
 
 // HasPermission returns true if the admin has the specified permission
 // HasPermission returns true if the admin has the specified permission
 func (a *Admin) HasPermission(perm string) bool {
 func (a *Admin) HasPermission(perm string) bool {
-	if util.Contains(a.Permissions, PermAdminAny) {
+	if slices.Contains(a.Permissions, PermAdminAny) {
 		return true
 		return true
 	}
 	}
-	return util.Contains(a.Permissions, perm)
+	return slices.Contains(a.Permissions, perm)
 }
 }
 
 
 // GetAllowedIPAsString returns the allowed IP as comma separated string
 // GetAllowedIPAsString returns the allowed IP as comma separated string

+ 11 - 10
internal/dataprovider/bolt.go

@@ -25,6 +25,7 @@ import (
 	"fmt"
 	"fmt"
 	"net/netip"
 	"net/netip"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"sort"
 	"sort"
 	"time"
 	"time"
 
 
@@ -3320,7 +3321,7 @@ func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bu
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(role.Admins, username) {
+	if !slices.Contains(role.Admins, username) {
 		role.Admins = append(role.Admins, username)
 		role.Admins = append(role.Admins, username)
 		buf, err := json.Marshal(role)
 		buf, err := json.Marshal(role)
 		if err != nil {
 		if err != nil {
@@ -3345,7 +3346,7 @@ func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bo
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if util.Contains(role.Admins, username) {
+	if slices.Contains(role.Admins, username) {
 		var admins []string
 		var admins []string
 		for _, admin := range role.Admins {
 		for _, admin := range role.Admins {
 			if admin != username {
 			if admin != username {
@@ -3375,7 +3376,7 @@ func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Buc
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(role.Users, username) {
+	if !slices.Contains(role.Users, username) {
 		role.Users = append(role.Users, username)
 		role.Users = append(role.Users, username)
 		buf, err := json.Marshal(role)
 		buf, err := json.Marshal(role)
 		if err != nil {
 		if err != nil {
@@ -3400,7 +3401,7 @@ func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bol
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if util.Contains(role.Users, username) {
+	if slices.Contains(role.Users, username) {
 		var users []string
 		var users []string
 		for _, user := range role.Users {
 		for _, user := range role.Users {
 			if user != username {
 			if user != username {
@@ -3428,7 +3429,7 @@ func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucke
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(action.Rules, ruleName) {
+	if !slices.Contains(action.Rules, ruleName) {
 		action.Rules = append(action.Rules, ruleName)
 		action.Rules = append(action.Rules, ruleName)
 		buf, err := json.Marshal(action)
 		buf, err := json.Marshal(action)
 		if err != nil {
 		if err != nil {
@@ -3450,7 +3451,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if util.Contains(action.Rules, ruleName) {
+	if slices.Contains(action.Rules, ruleName) {
 		var rules []string
 		var rules []string
 		for _, r := range action.Rules {
 		for _, r := range action.Rules {
 			if r != ruleName {
 			if r != ruleName {
@@ -3477,7 +3478,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(group.Users, username) {
+	if !slices.Contains(group.Users, username) {
 		group.Users = append(group.Users, username)
 		group.Users = append(group.Users, username)
 		buf, err := json.Marshal(group)
 		buf, err := json.Marshal(group)
 		if err != nil {
 		if err != nil {
@@ -3522,7 +3523,7 @@ func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(group.Admins, username) {
+	if !slices.Contains(group.Admins, username) {
 		group.Admins = append(group.Admins, username)
 		group.Admins = append(group.Admins, username)
 		buf, err := json.Marshal(group)
 		buf, err := json.Marshal(group)
 		if err != nil {
 		if err != nil {
@@ -3593,11 +3594,11 @@ func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User,
 		return err
 		return err
 	}
 	}
 	updated := false
 	updated := false
-	if user != nil && !util.Contains(folder.Users, user.Username) {
+	if user != nil && !slices.Contains(folder.Users, user.Username) {
 		folder.Users = append(folder.Users, user.Username)
 		folder.Users = append(folder.Users, user.Username)
 		updated = true
 		updated = true
 	}
 	}
-	if group != nil && !util.Contains(folder.Groups, group.Name) {
+	if group != nil && !slices.Contains(folder.Groups, group.Name) {
 		folder.Groups = append(folder.Groups, group.Name)
 		folder.Groups = append(folder.Groups, group.Name)
 		updated = true
 		updated = true
 	}
 	}

+ 6 - 5
internal/dataprovider/configs.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"fmt"
 	"image/png"
 	"image/png"
 	"net/url"
 	"net/url"
+	"slices"
 
 
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/crypto/ssh"
 
 
@@ -105,7 +106,7 @@ func (c *SFTPDConfigs) validate() error {
 		if algo == ssh.CertAlgoRSAv01 {
 		if algo == ssh.CertAlgoRSAv01 {
 			continue
 			continue
 		}
 		}
-		if !util.Contains(supportedHostKeyAlgos, algo) {
+		if !slices.Contains(supportedHostKeyAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
 			return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
 		}
 		}
 		hostKeyAlgos = append(hostKeyAlgos, algo)
 		hostKeyAlgos = append(hostKeyAlgos, algo)
@@ -116,24 +117,24 @@ func (c *SFTPDConfigs) validate() error {
 		if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
 		if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
 			continue
 			continue
 		}
 		}
-		if !util.Contains(supportedKexAlgos, algo) {
+		if !slices.Contains(supportedKexAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
 			return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
 		}
 		}
 		kexAlgos = append(kexAlgos, algo)
 		kexAlgos = append(kexAlgos, algo)
 	}
 	}
 	c.KexAlgorithms = kexAlgos
 	c.KexAlgorithms = kexAlgos
 	for _, cipher := range c.Ciphers {
 	for _, cipher := range c.Ciphers {
-		if !util.Contains(supportedCiphers, cipher) {
+		if !slices.Contains(supportedCiphers, cipher) {
 			return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
 			return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
 		}
 		}
 	}
 	}
 	for _, mac := range c.MACs {
 	for _, mac := range c.MACs {
-		if !util.Contains(supportedMACs, mac) {
+		if !slices.Contains(supportedMACs, mac) {
 			return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
 			return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
 		}
 		}
 	}
 	}
 	for _, algo := range c.PublicKeyAlgos {
 	for _, algo := range c.PublicKeyAlgos {
-		if !util.Contains(supportedPublicKeyAlgos, algo) {
+		if !slices.Contains(supportedPublicKeyAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
 			return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
 		}
 		}
 	}
 	}

+ 33 - 32
internal/dataprovider/dataprovider.go

@@ -44,6 +44,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
 	"runtime"
 	"runtime"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -519,7 +520,7 @@ type Config struct {
 // GetShared returns the provider share mode.
 // GetShared returns the provider share mode.
 // This method is called before the provider is initialized
 // This method is called before the provider is initialized
 func (c *Config) GetShared() int {
 func (c *Config) GetShared() int {
-	if !util.Contains(sharedProviders, c.Driver) {
+	if !slices.Contains(sharedProviders, c.Driver) {
 		return 0
 		return 0
 	}
 	}
 	return c.IsShared
 	return c.IsShared
@@ -885,7 +886,7 @@ func SetTempPath(fsPath string) {
 }
 }
 
 
 func checkSharedMode() {
 func checkSharedMode() {
-	if !util.Contains(sharedProviders, config.Driver) {
+	if !slices.Contains(sharedProviders, config.Driver) {
 		config.IsShared = 0
 		config.IsShared = 0
 	}
 	}
 }
 }
@@ -1714,7 +1715,7 @@ func IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)
 
 
 // GetIPListEntries returns the IP list entries applying the specified criteria and search limit
 // GetIPListEntries returns the IP list entries applying the specified criteria and search limit
 func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
 func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
-	if !util.Contains(supportedIPListType, listType) {
+	if !slices.Contains(supportedIPListType, listType) {
 		return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
 		return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
 	}
 	}
 	return provider.getIPListEntries(listType, filter, from, order, limit)
 	return provider.getIPListEntries(listType, filter, from, order, limit)
@@ -2373,7 +2374,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
 }
 }
 
 
 func dumpUsers(data *BackupData, scopes []string) error {
 func dumpUsers(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {
 		users, err := provider.dumpUsers()
 		users, err := provider.dumpUsers()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2384,7 +2385,7 @@ func dumpUsers(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpFolders(data *BackupData, scopes []string) error {
 func dumpFolders(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {
 		folders, err := provider.dumpFolders()
 		folders, err := provider.dumpFolders()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2395,7 +2396,7 @@ func dumpFolders(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpGroups(data *BackupData, scopes []string) error {
 func dumpGroups(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {
 		groups, err := provider.dumpGroups()
 		groups, err := provider.dumpGroups()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2406,7 +2407,7 @@ func dumpGroups(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpAdmins(data *BackupData, scopes []string) error {
 func dumpAdmins(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {
 		admins, err := provider.dumpAdmins()
 		admins, err := provider.dumpAdmins()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2417,7 +2418,7 @@ func dumpAdmins(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpAPIKeys(data *BackupData, scopes []string) error {
 func dumpAPIKeys(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {
 		apiKeys, err := provider.dumpAPIKeys()
 		apiKeys, err := provider.dumpAPIKeys()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2428,7 +2429,7 @@ func dumpAPIKeys(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpShares(data *BackupData, scopes []string) error {
 func dumpShares(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {
 		shares, err := provider.dumpShares()
 		shares, err := provider.dumpShares()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2439,7 +2440,7 @@ func dumpShares(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpActions(data *BackupData, scopes []string) error {
 func dumpActions(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {
 		actions, err := provider.dumpEventActions()
 		actions, err := provider.dumpEventActions()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2450,7 +2451,7 @@ func dumpActions(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpRules(data *BackupData, scopes []string) error {
 func dumpRules(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {
 		rules, err := provider.dumpEventRules()
 		rules, err := provider.dumpEventRules()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2461,7 +2462,7 @@ func dumpRules(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpRoles(data *BackupData, scopes []string) error {
 func dumpRoles(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {
 		roles, err := provider.dumpRoles()
 		roles, err := provider.dumpRoles()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2472,7 +2473,7 @@ func dumpRoles(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpIPLists(data *BackupData, scopes []string) error {
 func dumpIPLists(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {
 		ipLists, err := provider.dumpIPListEntries()
 		ipLists, err := provider.dumpIPListEntries()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2483,7 +2484,7 @@ func dumpIPLists(data *BackupData, scopes []string) error {
 }
 }
 
 
 func dumpConfigs(data *BackupData, scopes []string) error {
 func dumpConfigs(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {
 		configs, err := provider.getConfigs()
 		configs, err := provider.getConfigs()
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -2787,7 +2788,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
 	if c.ConfigName == "" {
 	if c.ConfigName == "" {
 		return util.NewValidationError("totp: config name is mandatory")
 		return util.NewValidationError("totp: config name is mandatory")
 	}
 	}
-	if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
+	if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 	}
 	}
 	if c.Secret.IsEmpty() {
 	if c.Secret.IsEmpty() {
@@ -2803,7 +2804,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
 		return util.NewValidationError("totp: specify at least one protocol")
 		return util.NewValidationError("totp: specify at least one protocol")
 	}
 	}
 	for _, protocol := range c.Protocols {
 	for _, protocol := range c.Protocols {
-		if !util.Contains(MFAProtocols, protocol) {
+		if !slices.Contains(MFAProtocols, protocol) {
 			return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
 			return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
 		}
 		}
 	}
 	}
@@ -2836,7 +2837,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
 			return permissions, util.NewValidationError("invalid permissions")
 			return permissions, util.NewValidationError("invalid permissions")
 		}
 		}
 		for _, p := range perms {
 		for _, p := range perms {
-			if !util.Contains(ValidPerms, p) {
+			if !slices.Contains(ValidPerms, p) {
 				return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
 				return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
 			}
 			}
 		}
 		}
@@ -2850,7 +2851,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
 		if dir != cleanedDir && cleanedDir == "/" {
 		if dir != cleanedDir && cleanedDir == "/" {
 			return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
 			return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
 		}
 		}
-		if util.Contains(perms, PermAny) {
+		if slices.Contains(perms, PermAny) {
 			permissions[cleanedDir] = []string{PermAny}
 			permissions[cleanedDir] = []string{PermAny}
 		} else {
 		} else {
 			permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
 			permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
@@ -2926,7 +2927,7 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
 				util.I18nErrorFilePatternPathInvalid,
 				util.I18nErrorFilePatternPathInvalid,
 			)
 			)
 		}
 		}
-		if util.Contains(filteredPaths, cleanedPath) {
+		if slices.Contains(filteredPaths, cleanedPath) {
 			return util.NewI18nError(
 			return util.NewI18nError(
 				util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
 				util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
 				util.I18nErrorFilePatternDuplicated,
 				util.I18nErrorFilePatternDuplicated,
@@ -3045,13 +3046,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
 		return util.NewValidationError("invalid denied_protocols")
 		return util.NewValidationError("invalid denied_protocols")
 	}
 	}
 	for _, p := range filters.DeniedProtocols {
 	for _, p := range filters.DeniedProtocols {
-		if !util.Contains(ValidProtocols, p) {
+		if !slices.Contains(ValidProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
 			return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
 		}
 		}
 	}
 	}
 
 
 	for _, p := range filters.TwoFactorAuthProtocols {
 	for _, p := range filters.TwoFactorAuthProtocols {
-		if !util.Contains(MFAProtocols, p) {
+		if !slices.Contains(MFAProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
 			return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
 		}
 		}
 	}
 	}
@@ -3107,7 +3108,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 		return util.NewValidationError("invalid denied_login_methods")
 		return util.NewValidationError("invalid denied_login_methods")
 	}
 	}
 	for _, loginMethod := range filters.DeniedLoginMethods {
 	for _, loginMethod := range filters.DeniedLoginMethods {
-		if !util.Contains(ValidLoginMethods, loginMethod) {
+		if !slices.Contains(ValidLoginMethods, loginMethod) {
 			return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
 			return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
 		}
 		}
 	}
 	}
@@ -3115,7 +3116,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 		return err
 		return err
 	}
 	}
 	if filters.TLSUsername != "" {
 	if filters.TLSUsername != "" {
-		if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
+		if !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {
 			return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
 			return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
 		}
 		}
 	}
 	}
@@ -3125,7 +3126,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 	}
 	}
 	filters.TLSCerts = certs
 	filters.TLSCerts = certs
 	for _, opts := range filters.WebClient {
 	for _, opts := range filters.WebClient {
-		if !util.Contains(sdk.WebClientOptions, opts) {
+		if !slices.Contains(sdk.WebClientOptions, opts) {
 			return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
 			return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
 		}
 		}
 	}
 	}
@@ -3193,19 +3194,19 @@ func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
 }
 }
 
 
 func validateCombinedUserFilters(user *User) error {
 func validateCombinedUserFilters(user *User) error {
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return util.NewI18nError(
 		return util.NewI18nError(
 			util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
 			util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
 			util.I18nErrorDisableActive2FA,
 			util.I18nErrorDisableActive2FA,
 		)
 		)
 	}
 	}
-	if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
+	if user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
 		return util.NewI18nError(
 		return util.NewI18nError(
 			util.NewValidationError("you cannot require password change and at the same time disallow it"),
 			util.NewValidationError("you cannot require password change and at the same time disallow it"),
 			util.I18nErrorPwdChangeConflict,
 			util.I18nErrorPwdChangeConflict,
 		)
 		)
 	}
 	}
-	if len(user.Filters.TwoFactorAuthProtocols) > 0 && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return util.NewI18nError(
 		return util.NewI18nError(
 			util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
 			util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
 			util.I18nError2FAConflict,
 			util.I18nError2FAConflict,
@@ -3526,7 +3527,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
 	if user.Filters.TOTPConfig.Enabled {
 	if user.Filters.TOTPConfig.Enabled {
 		switch protocol {
 		switch protocol {
 		case protocolFTP:
 		case protocolFTP:
-			if util.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
+			if slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
 				// the TOTP passcode has six digits
 				// the TOTP passcode has six digits
 				pwdLen := len(password)
 				pwdLen := len(password)
 				if pwdLen < 7 {
 				if pwdLen < 7 {
@@ -3732,7 +3733,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
 	if err := user.LoadAndApplyGroupSettings(); err != nil {
 	if err := user.LoadAndApplyGroupSettings(); err != nil {
 		return 0, err
 		return 0, err
 	}
 	}
-	hasSecondFactor := user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
+	hasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
 	if !isPartialAuth || !hasSecondFactor {
 	if !isPartialAuth || !hasSecondFactor {
 		answers, err := client("", "", []string{"Password: "}, []bool{false})
 		answers, err := client("", "", []string{"Password: "}, []bool{false})
 		if err != nil {
 		if err != nil {
@@ -3750,7 +3751,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
 }
 }
 
 
 func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
 func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
-	if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
+	if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
 		return 1, nil
 		return 1, nil
 	}
 	}
 	err := user.Filters.TOTPConfig.Secret.TryDecrypt()
 	err := user.Filters.TOTPConfig.Secret.TryDecrypt()
@@ -3874,7 +3875,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
 	}
 	}
 	if len(answers) == 1 && response.CheckPwd > 0 {
 	if len(answers) == 1 && response.CheckPwd > 0 {
 		if response.CheckPwd == 2 {
 		if response.CheckPwd == 2 {
-			if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
+			if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
 				providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
 				providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
 					user.Username)
 					user.Username)
 				return answers, errors.New("TOTP not enabled for SSH protocol")
 				return answers, errors.New("TOTP not enabled for SSH protocol")
@@ -4640,7 +4641,7 @@ func getConfigPath(name, configDir string) string {
 }
 }
 
 
 func checkReservedUsernames(username string) error {
 func checkReservedUsernames(username string) error {
-	if util.Contains(reservedUsers, username) {
+	if slices.Contains(reservedUsers, username) {
 		return util.NewValidationError("this username is reserved")
 		return util.NewValidationError("this username is reserved")
 	}
 	}
 	return nil
 	return nil

+ 14 - 13
internal/dataprovider/eventrule.go

@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/http"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -60,7 +61,7 @@ var (
 )
 )
 
 
 func isActionTypeValid(action int) bool {
 func isActionTypeValid(action int) bool {
-	return util.Contains(supportedEventActions, action)
+	return slices.Contains(supportedEventActions, action)
 }
 }
 
 
 func getActionTypeAsString(action int) string {
 func getActionTypeAsString(action int) string {
@@ -115,7 +116,7 @@ var (
 )
 )
 
 
 func isEventTriggerValid(trigger int) bool {
 func isEventTriggerValid(trigger int) bool {
-	return util.Contains(supportedEventTriggers, trigger)
+	return slices.Contains(supportedEventTriggers, trigger)
 }
 }
 
 
 func getTriggerTypeAsString(trigger int) string {
 func getTriggerTypeAsString(trigger int) string {
@@ -169,7 +170,7 @@ var (
 )
 )
 
 
 func isFilesystemActionValid(value int) bool {
 func isFilesystemActionValid(value int) bool {
-	return util.Contains(supportedFsActions, value)
+	return slices.Contains(supportedFsActions, value)
 }
 }
 
 
 func getFsActionTypeAsString(value int) string {
 func getFsActionTypeAsString(value int) string {
@@ -380,7 +381,7 @@ func (c *EventActionHTTPConfig) validate(additionalData string) error {
 			return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
 			return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
 		}
 		}
 	}
 	}
-	if !util.Contains(SupportedHTTPActionMethods, c.Method) {
+	if !slices.Contains(SupportedHTTPActionMethods, c.Method) {
 		return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
 		return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
 	}
 	}
 	for _, kv := range c.QueryParameters {
 	for _, kv := range c.QueryParameters {
@@ -1280,7 +1281,7 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
 		}
 		}
 		if trigger == EventTriggerFsEvent {
 		if trigger == EventTriggerFsEvent {
 			for _, ev := range fsEvents {
 			for _, ev := range fsEvents {
-				if !util.Contains(allowedSyncFsEvents, ev) {
+				if !slices.Contains(allowedSyncFsEvents, ev) {
 					return util.NewI18nError(
 					return util.NewI18nError(
 						util.NewValidationError("sync execution is only supported for upload and pre-* events"),
 						util.NewValidationError("sync execution is only supported for upload and pre-* events"),
 						util.I18nErrorEvSyncUnsupportedFs,
 						util.I18nErrorEvSyncUnsupportedFs,
@@ -1361,12 +1362,12 @@ func (f *ConditionOptions) validate() error {
 	}
 	}
 
 
 	for _, p := range f.Protocols {
 	for _, p := range f.Protocols {
-		if !util.Contains(SupportedRuleConditionProtocols, p) {
+		if !slices.Contains(SupportedRuleConditionProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
 			return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
 		}
 		}
 	}
 	}
 	for _, p := range f.ProviderObjects {
 	for _, p := range f.ProviderObjects {
-		if !util.Contains(SupporteRuleConditionProviderObjects, p) {
+		if !slices.Contains(SupporteRuleConditionProviderObjects, p) {
 			return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
 			return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
 		}
 		}
 	}
 	}
@@ -1468,7 +1469,7 @@ func (c *EventConditions) validate(trigger int) error {
 			)
 			)
 		}
 		}
 		for _, ev := range c.FsEvents {
 		for _, ev := range c.FsEvents {
-			if !util.Contains(SupportedFsEvents, ev) {
+			if !slices.Contains(SupportedFsEvents, ev) {
 				return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
 				return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
 			}
 			}
 		}
 		}
@@ -1488,7 +1489,7 @@ func (c *EventConditions) validate(trigger int) error {
 			)
 			)
 		}
 		}
 		for _, ev := range c.ProviderEvents {
 		for _, ev := range c.ProviderEvents {
-			if !util.Contains(SupportedProviderEvents, ev) {
+			if !slices.Contains(SupportedProviderEvents, ev) {
 				return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
 				return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
 			}
 			}
 		}
 		}
@@ -1537,7 +1538,7 @@ func (c *EventConditions) validate(trigger int) error {
 		c.Options.MinFileSize = 0
 		c.Options.MinFileSize = 0
 		c.Options.MaxFileSize = 0
 		c.Options.MaxFileSize = 0
 		c.Schedules = nil
 		c.Schedules = nil
-		if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
+		if !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
 			return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
 			return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
 		}
 		}
 	default:
 	default:
@@ -1690,7 +1691,7 @@ func (r *EventRule) validateMandatorySyncActions() error {
 		return nil
 		return nil
 	}
 	}
 	for _, ev := range r.Conditions.FsEvents {
 	for _, ev := range r.Conditions.FsEvents {
-		if util.Contains(mandatorySyncFsEvents, ev) {
+		if slices.Contains(mandatorySyncFsEvents, ev) {
 			return util.NewI18nError(
 			return util.NewI18nError(
 				util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
 				util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
 				util.I18nErrorRuleSyncActionRequired,
 				util.I18nErrorRuleSyncActionRequired,
@@ -1708,7 +1709,7 @@ func (r *EventRule) checkIPBlockedAndCertificateActions() error {
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
 		ActionTypeUserExpirationCheck}
 		ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
 	for _, action := range r.Actions {
-		if util.Contains(unavailableActions, action.Type) {
+		if slices.Contains(unavailableActions, action.Type) {
 			return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
 			return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
 				action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
 				action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
 		}
 		}
@@ -1724,7 +1725,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem,
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem,
 		ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
 		ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
 	for _, action := range r.Actions {
-		if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
+		if slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
 			return fmt.Errorf("action %q, type %q is only supported for provider user events",
 			return fmt.Errorf("action %q, type %q is only supported for provider user events",
 				action.Name, getActionTypeAsString(action.Type))
 				action.Name, getActionTypeAsString(action.Type))
 		}
 		}

+ 2 - 1
internal/dataprovider/iplist.go

@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 	"net/netip"
 	"net/netip"
+	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
@@ -85,7 +86,7 @@ var (
 
 
 // CheckIPListType returns an error if the provided IP list type is not valid
 // CheckIPListType returns an error if the provided IP list type is not valid
 func CheckIPListType(t IPListType) error {
 func CheckIPListType(t IPListType) error {
-	if !util.Contains(supportedIPListType, t) {
+	if !slices.Contains(supportedIPListType, t) {
 		return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
 		return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
 	}
 	}
 	return nil
 	return nil

+ 9 - 8
internal/dataprovider/memory.go

@@ -22,6 +22,7 @@ import (
 	"net/netip"
 	"net/netip"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"sort"
 	"sort"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -1210,7 +1211,7 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
 	if err != nil {
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
 		return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
 	}
 	}
-	if !util.Contains(a.Rules, ruleName) {
+	if !slices.Contains(a.Rules, ruleName) {
 		a.Rules = append(a.Rules, ruleName)
 		a.Rules = append(a.Rules, ruleName)
 		p.dbHandle.actions[actionName] = a
 		p.dbHandle.actions[actionName] = a
 	}
 	}
@@ -1223,7 +1224,7 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
 		providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
 		providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
 		return
 		return
 	}
 	}
-	if util.Contains(a.Rules, ruleName) {
+	if slices.Contains(a.Rules, ruleName) {
 		var rules []string
 		var rules []string
 		for _, r := range a.Rules {
 		for _, r := range a.Rules {
 			if r != ruleName {
 			if r != ruleName {
@@ -1240,7 +1241,7 @@ func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) erro
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(g.Admins, username) {
+	if !slices.Contains(g.Admins, username) {
 		g.Admins = append(g.Admins, username)
 		g.Admins = append(g.Admins, username)
 		p.dbHandle.groups[groupname] = g
 		p.dbHandle.groups[groupname] = g
 	}
 	}
@@ -1283,7 +1284,7 @@ func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(g.Users, username) {
+	if !slices.Contains(g.Users, username) {
 		g.Users = append(g.Users, username)
 		g.Users = append(g.Users, username)
 		p.dbHandle.groups[groupname] = g
 		p.dbHandle.groups[groupname] = g
 	}
 	}
@@ -1313,7 +1314,7 @@ func (p *MemoryProvider) addAdminToRole(username, role string) error {
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 	}
 	}
-	if !util.Contains(r.Admins, username) {
+	if !slices.Contains(r.Admins, username) {
 		r.Admins = append(r.Admins, username)
 		r.Admins = append(r.Admins, username)
 		p.dbHandle.roles[role] = r
 		p.dbHandle.roles[role] = r
 	}
 	}
@@ -1347,7 +1348,7 @@ func (p *MemoryProvider) addUserToRole(username, role string) error {
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 	}
 	}
-	if !util.Contains(r.Users, username) {
+	if !slices.Contains(r.Users, username) {
 		r.Users = append(r.Users, username)
 		r.Users = append(r.Users, username)
 		p.dbHandle.roles[role] = r
 		p.dbHandle.roles[role] = r
 	}
 	}
@@ -1378,7 +1379,7 @@ func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) err
 	if err != nil {
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 	}
 	}
-	if !util.Contains(f.Users, username) {
+	if !slices.Contains(f.Users, username) {
 		f.Users = append(f.Users, username)
 		f.Users = append(f.Users, username)
 		p.dbHandle.vfolders[foldername] = f
 		p.dbHandle.vfolders[foldername] = f
 	}
 	}
@@ -1390,7 +1391,7 @@ func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error
 	if err != nil {
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 	}
 	}
-	if !util.Contains(f.Groups, name) {
+	if !slices.Contains(f.Groups, name) {
 		f.Groups = append(f.Groups, name)
 		f.Groups = append(f.Groups, name)
 		p.dbHandle.vfolders[foldername] = f
 		p.dbHandle.vfolders[foldername] = f
 	}
 	}

+ 2 - 1
internal/dataprovider/pgsql.go

@@ -24,6 +24,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net"
 	"net"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -305,7 +306,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
 		if config.DisableSNI {
 		if config.DisableSNI {
 			connectionString += " sslsni=0"
 			connectionString += " sslsni=0"
 		}
 		}
-		if util.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
+		if slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
 			connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
 			connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
 		}
 		}
 	} else {
 	} else {

+ 28 - 27
internal/dataprovider/user.go

@@ -22,6 +22,7 @@ import (
 	"net"
 	"net"
 	"os"
 	"os"
 	"path"
 	"path"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -844,20 +845,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
 // HasPerm returns true if the user has the given permission or any permission
 // HasPerm returns true if the user has the given permission or any permission
 func (u *User) HasPerm(permission, path string) bool {
 func (u *User) HasPerm(permission, path string) bool {
 	perms := u.GetPermissionsForPath(path)
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 		return true
 	}
 	}
-	return util.Contains(perms, permission)
+	return slices.Contains(perms, permission)
 }
 }
 
 
 // HasAnyPerm returns true if the user has at least one of the given permissions
 // HasAnyPerm returns true if the user has at least one of the given permissions
 func (u *User) HasAnyPerm(permissions []string, path string) bool {
 func (u *User) HasAnyPerm(permissions []string, path string) bool {
 	perms := u.GetPermissionsForPath(path)
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 		return true
 	}
 	}
 	for _, permission := range permissions {
 	for _, permission := range permissions {
-		if util.Contains(perms, permission) {
+		if slices.Contains(perms, permission) {
 			return true
 			return true
 		}
 		}
 	}
 	}
@@ -867,11 +868,11 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
 // HasPerms returns true if the user has all the given permissions
 // HasPerms returns true if the user has all the given permissions
 func (u *User) HasPerms(permissions []string, path string) bool {
 func (u *User) HasPerms(permissions []string, path string) bool {
 	perms := u.GetPermissionsForPath(path)
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 		return true
 	}
 	}
 	for _, permission := range permissions {
 	for _, permission := range permissions {
-		if !util.Contains(perms, permission) {
+		if !slices.Contains(perms, permission) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -931,11 +932,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {
 	if len(u.Filters.DeniedLoginMethods) == 0 {
 	if len(u.Filters.DeniedLoginMethods) == 0 {
 		return true
 		return true
 	}
 	}
-	if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
+	if slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
 		return false
 		return false
 	}
 	}
 	if protocol == protocolSSH && loginMethod == LoginMethodPassword {
 	if protocol == protocolSSH && loginMethod == LoginMethodPassword {
-		if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
+		if slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -969,10 +970,10 @@ func (u *User) IsPartialAuth() bool {
 			method == SSHLoginMethodPassword {
 			method == SSHLoginMethodPassword {
 			continue
 			continue
 		}
 		}
-		if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
+		if method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
 			continue
 			continue
 		}
 		}
-		if !util.Contains(SSHMultiStepsLoginMethods, method) {
+		if !slices.Contains(SSHMultiStepsLoginMethods, method) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -986,7 +987,7 @@ func (u *User) GetAllowedLoginMethods() []string {
 		if method == SSHLoginMethodPassword {
 		if method == SSHLoginMethodPassword {
 			continue
 			continue
 		}
 		}
-		if !util.Contains(u.Filters.DeniedLoginMethods, method) {
+		if !slices.Contains(u.Filters.DeniedLoginMethods, method) {
 			allowedMethods = append(allowedMethods, method)
 			allowedMethods = append(allowedMethods, method)
 		}
 		}
 	}
 	}
@@ -1056,7 +1057,7 @@ func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
 
 
 // CanManageMFA returns true if the user can add a multi-factor authentication configuration
 // CanManageMFA returns true if the user can add a multi-factor authentication configuration
 func (u *User) CanManageMFA() bool {
 func (u *User) CanManageMFA() bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return false
 		return false
 	}
 	}
 	return len(mfa.GetAvailableTOTPConfigs()) > 0
 	return len(mfa.GetAvailableTOTPConfigs()) > 0
@@ -1077,39 +1078,39 @@ func (u *User) skipExternalAuth() bool {
 
 
 // CanManageShares returns true if the user can add, update and list shares
 // CanManageShares returns true if the user can add, update and list shares
 func (u *User) CanManageShares() bool {
 func (u *User) CanManageShares() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
 }
 }
 
 
 // CanResetPassword returns true if this user is allowed to reset its password
 // CanResetPassword returns true if this user is allowed to reset its password
 func (u *User) CanResetPassword() bool {
 func (u *User) CanResetPassword() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
 }
 }
 
 
 // CanChangePassword returns true if this user is allowed to change its password
 // CanChangePassword returns true if this user is allowed to change its password
 func (u *User) CanChangePassword() bool {
 func (u *User) CanChangePassword() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
 }
 }
 
 
 // CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
 // CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
 func (u *User) CanChangeAPIKeyAuth() bool {
 func (u *User) CanChangeAPIKeyAuth() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
 }
 }
 
 
 // CanChangeInfo returns true if this user is allowed to change its info such as email and description
 // CanChangeInfo returns true if this user is allowed to change its info such as email and description
 func (u *User) CanChangeInfo() bool {
 func (u *User) CanChangeInfo() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
 }
 }
 
 
 // CanManagePublicKeys returns true if this user is allowed to manage public keys
 // CanManagePublicKeys returns true if this user is allowed to manage public keys
 // from the WebClient. Used in WebClient UI
 // from the WebClient. Used in WebClient UI
 func (u *User) CanManagePublicKeys() bool {
 func (u *User) CanManagePublicKeys() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
 }
 }
 
 
 // CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
 // CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
 // from the WebClient. Used in WebClient UI
 // from the WebClient. Used in WebClient UI
 func (u *User) CanManageTLSCerts() bool {
 func (u *User) CanManageTLSCerts() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
 }
 }
 
 
 // CanUpdateProfile returns true if the user is allowed to update the profile.
 // CanUpdateProfile returns true if the user is allowed to update the profile.
@@ -1121,7 +1122,7 @@ func (u *User) CanUpdateProfile() bool {
 // CanAddFilesFromWeb returns true if the client can add files from the web UI.
 // CanAddFilesFromWeb returns true if the client can add files from the web UI.
 // The specified target is the directory where the files must be uploaded
 // The specified target is the directory where the files must be uploaded
 func (u *User) CanAddFilesFromWeb(target string) bool {
 func (u *User) CanAddFilesFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 		return false
 	}
 	}
 	return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
 	return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
@@ -1130,7 +1131,7 @@ func (u *User) CanAddFilesFromWeb(target string) bool {
 // CanAddDirsFromWeb returns true if the client can add directories from the web UI.
 // CanAddDirsFromWeb returns true if the client can add directories from the web UI.
 // The specified target is the directory where the new directory must be created
 // The specified target is the directory where the new directory must be created
 func (u *User) CanAddDirsFromWeb(target string) bool {
 func (u *User) CanAddDirsFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 		return false
 	}
 	}
 	return u.HasPerm(PermCreateDirs, target)
 	return u.HasPerm(PermCreateDirs, target)
@@ -1139,7 +1140,7 @@ func (u *User) CanAddDirsFromWeb(target string) bool {
 // CanRenameFromWeb returns true if the client can rename objects from the web UI.
 // CanRenameFromWeb returns true if the client can rename objects from the web UI.
 // The specified src and dest are the source and target directories for the rename.
 // The specified src and dest are the source and target directories for the rename.
 func (u *User) CanRenameFromWeb(src, dest string) bool {
 func (u *User) CanRenameFromWeb(src, dest string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 		return false
 	}
 	}
 	return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
 	return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
@@ -1148,7 +1149,7 @@ func (u *User) CanRenameFromWeb(src, dest string) bool {
 // CanDeleteFromWeb returns true if the client can delete objects from the web UI.
 // CanDeleteFromWeb returns true if the client can delete objects from the web UI.
 // The specified target is the parent directory for the object to delete
 // The specified target is the parent directory for the object to delete
 func (u *User) CanDeleteFromWeb(target string) bool {
 func (u *User) CanDeleteFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 		return false
 	}
 	}
 	return u.HasAnyPerm(permsDeleteAny, target)
 	return u.HasAnyPerm(permsDeleteAny, target)
@@ -1157,7 +1158,7 @@ func (u *User) CanDeleteFromWeb(target string) bool {
 // CanCopyFromWeb returns true if the client can copy objects from the web UI.
 // CanCopyFromWeb returns true if the client can copy objects from the web UI.
 // The specified src and dest are the source and target directories for the copy.
 // The specified src and dest are the source and target directories for the copy.
 func (u *User) CanCopyFromWeb(src, dest string) bool {
 func (u *User) CanCopyFromWeb(src, dest string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 		return false
 	}
 	}
 	if !u.HasPerm(PermListItems, src) {
 	if !u.HasPerm(PermListItems, src) {
@@ -1217,7 +1218,7 @@ func (u *User) MustSetSecondFactor() bool {
 			return true
 			return true
 		}
 		}
 		for _, p := range u.Filters.TwoFactorAuthProtocols {
 		for _, p := range u.Filters.TwoFactorAuthProtocols {
-			if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
+			if !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {
 				return true
 				return true
 			}
 			}
 		}
 		}
@@ -1228,11 +1229,11 @@ func (u *User) MustSetSecondFactor() bool {
 // MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
 // MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
 // for the specified protocol
 // for the specified protocol
 func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
 func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
-	if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
+	if slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
 		if !u.Filters.TOTPConfig.Enabled {
 		if !u.Filters.TOTPConfig.Enabled {
 			return true
 			return true
 		}
 		}
-		if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
+		if !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
 			return true
 			return true
 		}
 		}
 	}
 	}

+ 2 - 1
internal/ftpd/server.go

@@ -22,6 +22,7 @@ import (
 	"net"
 	"net"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 
 
 	ftpserver "github.com/fclairamb/ftpserverlib"
 	ftpserver "github.com/fclairamb/ftpserverlib"
 	"github.com/sftpgo/sdk/plugin/notifier"
 	"github.com/sftpgo/sdk/plugin/notifier"
@@ -361,7 +362,7 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
 			user.Username, user.HomeDir)
 			user.Username, user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol FTP is not allowed", user.Username)
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol FTP is not allowed", user.Username)
 		return nil, fmt.Errorf("protocol FTP is not allowed for user %q", user.Username)
 		return nil, fmt.Errorf("protocol FTP is not allowed for user %q", user.Username)
 	}
 	}

+ 2 - 1
internal/httpd/api_mfa.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -275,7 +276,7 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []datapr
 		return util.NewValidationError("two-factor authentication must be enabled")
 		return util.NewValidationError("two-factor authentication must be enabled")
 	}
 	}
 	for _, p := range userMerged.Filters.TwoFactorAuthProtocols {
 	for _, p := range userMerged.Filters.TwoFactorAuthProtocols {
-		if !util.Contains(user.Filters.TOTPConfig.Protocols, p) {
+		if !slices.Contains(user.Filters.TOTPConfig.Protocols, p) {
 			return util.NewValidationError(fmt.Sprintf("totp: the following protocols are required: %q",
 			return util.NewValidationError(fmt.Sprintf("totp: the following protocols are required: %q",
 				strings.Join(userMerged.Filters.TwoFactorAuthProtocols, ", ")))
 				strings.Join(userMerged.Filters.TwoFactorAuthProtocols, ", ")))
 		}
 		}

+ 6 - 5
internal/httpd/api_shares.go

@@ -22,6 +22,7 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path"
 	"path"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -107,7 +108,7 @@ func addShare(w http.ResponseWriter, r *http.Request) {
 		share.Name = share.ShareID
 		share.Name = share.ShareID
 	}
 	}
 	if share.Password == "" {
 	if share.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 				http.StatusForbidden)
 				http.StatusForbidden)
 			return
 			return
@@ -155,7 +156,7 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
 		updatedShare.Password = share.Password
 		updatedShare.Password = share.Password
 	}
 	}
 	if updatedShare.Password == "" {
 	if updatedShare.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 				http.StatusForbidden)
 				http.StatusForbidden)
 			return
 			return
@@ -434,7 +435,7 @@ func (s *httpdServer) getShareClaims(r *http.Request, shareID string) (*jwtToken
 	if tokenString == "" || invalidatedJWTTokens.Get(tokenString) {
 	if tokenString == "" || invalidatedJWTTokens.Get(tokenString) {
 		return nil, errInvalidToken
 		return nil, errInvalidToken
 	}
 	}
-	if !util.Contains(token.Audience(), tokenAudienceWebShare) {
+	if !slices.Contains(token.Audience(), tokenAudienceWebShare) {
 		logger.Debug(logSender, "", "invalid token audience for share %q", shareID)
 		logger.Debug(logSender, "", "invalid token audience for share %q", shareID)
 		return nil, errInvalidToken
 		return nil, errInvalidToken
 	}
 	}
@@ -486,7 +487,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
 		renderError(err, "", statusCode)
 		renderError(err, "", statusCode)
 		return share, nil, err
 		return share, nil, err
 	}
 	}
-	if !util.Contains(validScopes, share.Scope) {
+	if !slices.Contains(validScopes, share.Scope) {
 		err := errors.New("invalid share scope")
 		err := errors.New("invalid share scope")
 		renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden)
 		renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden)
 		return share, nil, err
 		return share, nil, err
@@ -543,7 +544,7 @@ func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
 	if !user.CanManageShares() {
 	if !user.CanManageShares() {
 		return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message)
 		return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message)
 	}
 	}
-	if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
+	if share.Password == "" && slices.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
 		return user, util.NewI18nError(
 		return user, util.NewI18nError(
 			fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission),
 			fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission),
 			util.I18nError403Message,
 			util.I18nError403Message,

+ 3 - 2
internal/httpd/api_utils.go

@@ -27,6 +27,7 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path"
 	"path"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -727,7 +728,7 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
 }
 }
 
 
 func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error {
 func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error {
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username)
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username)
 		return util.NewI18nError(
 		return util.NewI18nError(
 			fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username),
 			fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username),
@@ -912,7 +913,7 @@ func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool
 	if !user.CanResetPassword() {
 	if !user.CanResetPassword() {
 		return false
 		return false
 	}
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
 		return false
 		return false
 	}
 	}
 	if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {
 	if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {

+ 12 - 11
internal/httpd/auth_utils.go

@@ -18,6 +18,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"slices"
 	"time"
 	"time"
 
 
 	"github.com/go-chi/jwtauth/v5"
 	"github.com/go-chi/jwtauth/v5"
@@ -227,24 +228,24 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
 }
 }
 
 
 func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
 func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
-	if util.Contains(permissions, dataprovider.PermAdminAny) {
+	if slices.Contains(permissions, dataprovider.PermAdminAny) {
 		return false
 		return false
 	}
 	}
-	if (util.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
-		util.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
-		!util.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
-		!util.Contains(permissions, dataprovider.PermAdminAny) {
+	if (slices.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
+		slices.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
+		!slices.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
+		!slices.Contains(permissions, dataprovider.PermAdminAny) {
 		return true
 		return true
 	}
 	}
 	return false
 	return false
 }
 }
 
 
 func (c *jwtTokenClaims) hasPerm(perm string) bool {
 func (c *jwtTokenClaims) hasPerm(perm string) bool {
-	if util.Contains(c.Permissions, dataprovider.PermAdminAny) {
+	if slices.Contains(c.Permissions, dataprovider.PermAdminAny) {
 		return true
 		return true
 	}
 	}
 
 
-	return util.Contains(c.Permissions, perm)
+	return slices.Contains(c.Permissions, perm)
 }
 }
 
 
 func (c *jwtTokenClaims) createToken(tokenAuth *jwtauth.JWTAuth, audience tokenAudience, ip string) (jwt.Token, string, error) {
 func (c *jwtTokenClaims) createToken(tokenAuth *jwtauth.JWTAuth, audience tokenAudience, ip string) (jwt.Token, string, error) {
@@ -458,7 +459,7 @@ func verifyCSRFToken(r *http.Request, csrfTokenAuth *jwtauth.JWTAuth) error {
 		return fmt.Errorf("unable to verify form token: %v", err)
 		return fmt.Errorf("unable to verify form token: %v", err)
 	}
 	}
 
 
-	if !util.Contains(token.Audience(), tokenAudienceCSRF) {
+	if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
 		logger.Debug(logSender, "", "error validating CSRF token audience")
 		logger.Debug(logSender, "", "error validating CSRF token audience")
 		return errors.New("the form token is not valid")
 		return errors.New("the form token is not valid")
 	}
 	}
@@ -495,7 +496,7 @@ func verifyLoginCookie(r *http.Request) error {
 		logger.Debug(logSender, "", "the login token has been invalidated")
 		logger.Debug(logSender, "", "the login token has been invalidated")
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}
-	if !util.Contains(token.Audience(), tokenAudienceWebLogin) {
+	if !slices.Contains(token.Audience(), tokenAudienceWebLogin) {
 		logger.Debug(logSender, "", "the token with id %q is not valid for audience %q", token.JwtID(), tokenAudienceWebLogin)
 		logger.Debug(logSender, "", "the token with id %q is not valid for audience %q", token.JwtID(), tokenAudienceWebLogin)
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}
@@ -543,7 +544,7 @@ func verifyOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, tokenString, ip string) (
 		)
 		)
 	}
 	}
 
 
-	if !util.Contains(token.Audience(), tokenAudienceOAuth2) {
+	if !slices.Contains(token.Audience(), tokenAudienceOAuth2) {
 		logger.Debug(logSender, "", "error validating OAuth2 token audience")
 		logger.Debug(logSender, "", "error validating OAuth2 token audience")
 		return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
 		return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
 	}
 	}
@@ -563,7 +564,7 @@ func verifyOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, tokenString, ip string) (
 
 
 func validateIPForToken(token jwt.Token, ip string) error {
 func validateIPForToken(token jwt.Token, ip string) error {
 	if tokenValidationMode != tokenValidationNoIPMatch {
 	if tokenValidationMode != tokenValidationNoIPMatch {
-		if !util.Contains(token.Audience(), ip) {
+		if !slices.Contains(token.Audience(), ip) {
 			return errInvalidToken
 			return errInvalidToken
 		}
 		}
 	}
 	}

+ 26 - 25
internal/httpd/httpd_test.go

@@ -36,6 +36,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
 	"runtime"
 	"runtime"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -1340,7 +1341,7 @@ func TestGroupSettingsOverride(t *testing.T) {
 	var folderNames []string
 	var folderNames []string
 	if assert.Len(t, user.VirtualFolders, 4) {
 	if assert.Len(t, user.VirtualFolders, 4) {
 		for _, f := range user.VirtualFolders {
 		for _, f := range user.VirtualFolders {
-			if !util.Contains(folderNames, f.Name) {
+			if !slices.Contains(folderNames, f.Name) {
 				folderNames = append(folderNames, f.Name)
 				folderNames = append(folderNames, f.Name)
 			}
 			}
 			switch f.Name {
 			switch f.Name {
@@ -1348,7 +1349,7 @@ func TestGroupSettingsOverride(t *testing.T) {
 				assert.Equal(t, mappedPath1, f.MappedPath)
 				assert.Equal(t, mappedPath1, f.MappedPath)
 				assert.Equal(t, 3, f.BaseVirtualFolder.FsConfig.OSConfig.ReadBufferSize)
 				assert.Equal(t, 3, f.BaseVirtualFolder.FsConfig.OSConfig.ReadBufferSize)
 				assert.Equal(t, 5, f.BaseVirtualFolder.FsConfig.OSConfig.WriteBufferSize)
 				assert.Equal(t, 5, f.BaseVirtualFolder.FsConfig.OSConfig.WriteBufferSize)
-				assert.True(t, util.Contains([]string{"/vdir1", "/vdir2"}, f.VirtualPath))
+				assert.True(t, slices.Contains([]string{"/vdir1", "/vdir2"}, f.VirtualPath))
 			case folderName2:
 			case folderName2:
 				assert.Equal(t, mappedPath2, f.MappedPath)
 				assert.Equal(t, mappedPath2, f.MappedPath)
 				assert.Equal(t, "/vdir3", f.VirtualPath)
 				assert.Equal(t, "/vdir3", f.VirtualPath)
@@ -2103,16 +2104,16 @@ func TestActionRuleRelations(t *testing.T) {
 	action1, _, err = httpdtest.GetEventActionByName(action1.Name, http.StatusOK)
 	action1, _, err = httpdtest.GetEventActionByName(action1.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, action1.Rules, 1)
 	assert.Len(t, action1.Rules, 1)
-	assert.True(t, util.Contains(action1.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action1.Rules, rule1.Name))
 	action2, _, err = httpdtest.GetEventActionByName(action2.Name, http.StatusOK)
 	action2, _, err = httpdtest.GetEventActionByName(action2.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, action2.Rules, 1)
 	assert.Len(t, action2.Rules, 1)
-	assert.True(t, util.Contains(action2.Rules, rule2.Name))
+	assert.True(t, slices.Contains(action2.Rules, rule2.Name))
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, action3.Rules, 2)
 	assert.Len(t, action3.Rules, 2)
-	assert.True(t, util.Contains(action3.Rules, rule1.Name))
-	assert.True(t, util.Contains(action3.Rules, rule2.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule2.Name))
 	// referenced actions cannot be removed
 	// referenced actions cannot be removed
 	_, err = httpdtest.RemoveEventAction(action1, http.StatusBadRequest)
 	_, err = httpdtest.RemoveEventAction(action1, http.StatusBadRequest)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -2140,7 +2141,7 @@ func TestActionRuleRelations(t *testing.T) {
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, action3.Rules, 1)
 	assert.Len(t, action3.Rules, 1)
-	assert.True(t, util.Contains(action3.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule1.Name))
 
 
 	_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
 	_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -8912,7 +8913,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
 	assert.Equal(t, user.MaxSessions, updatedUser.MaxSessions)
 	assert.Equal(t, user.MaxSessions, updatedUser.MaxSessions)
 	assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth)
 	assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth)
 	assert.Equal(t, 1, len(updatedUser.Permissions["/"]))
 	assert.Equal(t, 1, len(updatedUser.Permissions["/"]))
-	assert.True(t, util.Contains(updatedUser.Permissions["/"], dataprovider.PermAny))
+	assert.True(t, slices.Contains(updatedUser.Permissions["/"], dataprovider.PermAny))
 	req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil)
 	req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil)
 	setBearerForReq(req, token)
 	setBearerForReq(req, token)
 	rr = executeRequest(req)
 	rr = executeRequest(req)
@@ -12140,7 +12141,7 @@ func TestUserPermissionsMock(t *testing.T) {
 	err = render.DecodeJSON(rr.Body, &updatedUser)
 	err = render.DecodeJSON(rr.Body, &updatedUser)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	if val, ok := updatedUser.Permissions["/otherdir"]; ok {
 	if val, ok := updatedUser.Permissions["/otherdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
 		assert.Equal(t, 1, len(val))
 		assert.Equal(t, 1, len(val))
 	} else {
 	} else {
 		assert.Fail(t, "expected dir not found in permissions")
 		assert.Fail(t, "expected dir not found in permissions")
@@ -21552,10 +21553,10 @@ func TestWebUserAddMock(t *testing.T) {
 	assert.Equal(t, 60, newUser.Filters.PasswordStrength)
 	assert.Equal(t, 60, newUser.Filters.PasswordStrength)
 	assert.Greater(t, newUser.LastPasswordChange, int64(0))
 	assert.Greater(t, newUser.LastPasswordChange, int64(0))
 	assert.True(t, newUser.Filters.RequirePasswordChange)
 	assert.True(t, newUser.Filters.RequirePasswordChange)
-	assert.True(t, util.Contains(newUser.PublicKeys, testPubKey))
+	assert.True(t, slices.Contains(newUser.PublicKeys, testPubKey))
 	if val, ok := newUser.Permissions["/subdir"]; ok {
 	if val, ok := newUser.Permissions["/subdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
-		assert.True(t, util.Contains(val, dataprovider.PermDownload))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermDownload))
 	} else {
 	} else {
 		assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions)
 		assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions)
 	}
 	}
@@ -21574,20 +21575,20 @@ func TestWebUserAddMock(t *testing.T) {
 		case "/dir1":
 		case "/dir1":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 1)
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.png"))
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.zip"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.png"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.zip"))
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 		case "/dir2":
 		case "/dir2":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 2)
 			assert.Len(t, filter.AllowedPatterns, 2)
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.jpg"))
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.png"))
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.mkv"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.jpg"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.png"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.mkv"))
 			assert.Equal(t, sdk.DenyPolicyHide, filter.DenyPolicy)
 			assert.Equal(t, sdk.DenyPolicyHide, filter.DenyPolicy)
 		case "/dir3":
 		case "/dir3":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 0)
 			assert.Len(t, filter.AllowedPatterns, 0)
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.rar"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.rar"))
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 		}
 		}
 	}
 	}
@@ -21828,16 +21829,16 @@ func TestWebUserUpdateMock(t *testing.T) {
 	assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
 	assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
 	assert.True(t, updateUser.Filters.RequirePasswordChange)
 	assert.True(t, updateUser.Filters.RequirePasswordChange)
 	if val, ok := updateUser.Permissions["/otherdir"]; ok {
 	if val, ok := updateUser.Permissions["/otherdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
-		assert.True(t, util.Contains(val, dataprovider.PermUpload))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermUpload))
 	} else {
 	} else {
 		assert.Fail(t, "user permissions must contains /otherdir", "actual: %v", updateUser.Permissions)
 		assert.Fail(t, "user permissions must contains /otherdir", "actual: %v", updateUser.Permissions)
 	}
 	}
-	assert.True(t, util.Contains(updateUser.Filters.AllowedIP, "192.168.1.3/32"))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedIP, "10.0.0.2/32"))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedLoginMethods, dataprovider.SSHLoginMethodKeyboardInteractive))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedProtocols, common.ProtocolFTP))
-	assert.True(t, util.Contains(updateUser.Filters.FilePatterns[0].DeniedPatterns, "*.zip"))
+	assert.True(t, slices.Contains(updateUser.Filters.AllowedIP, "192.168.1.3/32"))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedIP, "10.0.0.2/32"))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedLoginMethods, dataprovider.SSHLoginMethodKeyboardInteractive))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedProtocols, common.ProtocolFTP))
+	assert.True(t, slices.Contains(updateUser.Filters.FilePatterns[0].DeniedPatterns, "*.zip"))
 	assert.Len(t, updateUser.Filters.BandwidthLimits, 0)
 	assert.Len(t, updateUser.Filters.BandwidthLimits, 0)
 	assert.Len(t, updateUser.Filters.TLSCerts, 1)
 	assert.Len(t, updateUser.Filters.TLSCerts, 1)
 	req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
 	req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)

+ 6 - 5
internal/httpd/middleware.go

@@ -20,6 +20,7 @@ import (
 	"io/fs"
 	"io/fs"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"slices"
 	"strings"
 	"strings"
 
 
 	"github.com/go-chi/jwtauth/v5"
 	"github.com/go-chi/jwtauth/v5"
@@ -83,7 +84,7 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
 	if err := checkPartialAuth(w, r, audience, token.Audience()); err != nil {
 	if err := checkPartialAuth(w, r, audience, token.Audience()); err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(token.Audience(), audience) {
+	if !slices.Contains(token.Audience(), audience) {
 		logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
 		logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
 		doRedirect("Your token audience is not valid", nil)
 		doRedirect("Your token audience is not valid", nil)
 		return errInvalidToken
 		return errInvalidToken
@@ -113,7 +114,7 @@ func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Req
 		notFoundFunc(w, r, nil)
 		notFoundFunc(w, r, nil)
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}
-	if !util.Contains(token.Audience(), audience) {
+	if !slices.Contains(token.Audience(), audience) {
 		logger.Debug(logSender, "", "the partial token with id %q is not valid for audience %q", token.JwtID(), audience)
 		logger.Debug(logSender, "", "the partial token with id %q is not valid for audience %q", token.JwtID(), audience)
 		notFoundFunc(w, r, nil)
 		notFoundFunc(w, r, nil)
 		return errInvalidToken
 		return errInvalidToken
@@ -331,7 +332,7 @@ func (s *httpdServer) verifyCSRFHeader(next http.Handler) http.Handler {
 			return
 			return
 		}
 		}
 
 
-		if !util.Contains(token.Audience(), tokenAudienceCSRF) {
+		if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
 			logger.Debug(logSender, "", "error validating CSRF header token audience")
 			logger.Debug(logSender, "", "error validating CSRF header token audience")
 			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
 			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
 			return
 			return
@@ -571,11 +572,11 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
 }
 }
 
 
 func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {
 func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {
-	if audience == tokenAudienceWebAdmin && util.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
+	if audience == tokenAudienceWebAdmin && slices.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
 		http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
 		http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}
-	if audience == tokenAudienceWebClient && util.Contains(tokenAudience, tokenAudienceWebClientPartial) {
+	if audience == tokenAudienceWebClient && slices.Contains(tokenAudience, tokenAudienceWebClientPartial) {
 		http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
 		http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
 		return errInvalidToken
 		return errInvalidToken
 	}
 	}

+ 2 - 1
internal/httpd/oidc.go

@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -143,7 +144,7 @@ func (o *OIDC) initialize() error {
 	if o.RedirectBaseURL == "" {
 	if o.RedirectBaseURL == "" {
 		return errors.New("oidc: redirect base URL cannot be empty")
 		return errors.New("oidc: redirect base URL cannot be empty")
 	}
 	}
-	if !util.Contains(o.Scopes, oidc.ScopeOpenID) {
+	if !slices.Contains(o.Scopes, oidc.ScopeOpenID) {
 		return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID)
 		return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID)
 	}
 	}
 	if o.ClientSecretFile != "" {
 	if o.ClientSecretFile != "" {

+ 6 - 5
internal/httpd/server.go

@@ -26,6 +26,7 @@ import (
 	"net/url"
 	"net/url"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -355,7 +356,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
 			util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
 			util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
 		return
 		return
 	}
 	}
-	if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if !userMerged.Filters.TOTPConfig.Enabled || !slices.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(
 			util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled))
 			util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled))
 		return
 		return
@@ -423,7 +424,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
 		return
 		return
 	}
 	}
-	if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))
 		return
 		return
@@ -743,7 +744,7 @@ func (s *httpdServer) loginUser(
 	}
 	}
 
 
 	audience := tokenAudienceWebClient
 	audience := tokenAudienceWebClient
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
 		user.CanManageMFA() && !isSecondFactorAuth {
 		user.CanManageMFA() && !isSecondFactorAuth {
 		audience = tokenAudienceWebClientPartial
 		audience = tokenAudienceWebClientPartial
 	}
 	}
@@ -863,7 +864,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		passcode := r.Header.Get(otpHeaderCode)
 		passcode := r.Header.Get(otpHeaderCode)
 		if passcode == "" {
 		if passcode == "" {
 			logger.Debug(logSender, "", "TOTP enabled for user %q and not passcode provided, authentication refused", user.Username)
 			logger.Debug(logSender, "", "TOTP enabled for user %q and not passcode provided, authentication refused", user.Username)
@@ -1009,7 +1010,7 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
 	if time.Until(token.Expiration()) > tokenRefreshThreshold {
 	if time.Until(token.Expiration()) > tokenRefreshThreshold {
 		return
 		return
 	}
 	}
-	if util.Contains(token.Audience(), tokenAudienceWebClient) {
+	if slices.Contains(token.Audience(), tokenAudienceWebClient) {
 		s.refreshClientToken(w, r, &tokenClaims)
 		s.refreshClientToken(w, r, &tokenClaims)
 	} else {
 	} else {
 		s.refreshAdminToken(w, r, &tokenClaims)
 		s.refreshAdminToken(w, r, &tokenClaims)

+ 8 - 7
internal/httpd/webadmin.go

@@ -25,6 +25,7 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"sort"
 	"sort"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
@@ -1488,13 +1489,13 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
 	filters.PasswordStrength = passwordStrength
 	filters.PasswordStrength = passwordStrength
 	filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
 	filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
 	hooks := r.Form["hooks"]
 	hooks := r.Form["hooks"]
-	if util.Contains(hooks, "external_auth_disabled") {
+	if slices.Contains(hooks, "external_auth_disabled") {
 		filters.Hooks.ExternalAuthDisabled = true
 		filters.Hooks.ExternalAuthDisabled = true
 	}
 	}
-	if util.Contains(hooks, "pre_login_disabled") {
+	if slices.Contains(hooks, "pre_login_disabled") {
 		filters.Hooks.PreLoginDisabled = true
 		filters.Hooks.PreLoginDisabled = true
 	}
 	}
-	if util.Contains(hooks, "check_password_disabled") {
+	if slices.Contains(hooks, "check_password_disabled") {
 		filters.Hooks.CheckPasswordDisabled = true
 		filters.Hooks.CheckPasswordDisabled = true
 	}
 	}
 	filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
 	filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
@@ -2215,7 +2216,7 @@ func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRe
 			res = append(res, dataprovider.FolderRetention{
 			res = append(res, dataprovider.FolderRetention{
 				Path:            p,
 				Path:            p,
 				Retention:       retention,
 				Retention:       retention,
-				DeleteEmptyDirs: util.Contains(opts, "1"),
+				DeleteEmptyDirs: slices.Contains(opts, "1"),
 			})
 			})
 		}
 		}
 	}
 	}
@@ -2557,9 +2558,9 @@ func getEventRuleActionsFromPostFields(r *http.Request) []dataprovider.EventActi
 					},
 					},
 					Order: order + 1,
 					Order: order + 1,
 					Options: dataprovider.EventActionOptions{
 					Options: dataprovider.EventActionOptions{
-						IsFailureAction: util.Contains(options, "1"),
-						StopOnFailure:   util.Contains(options, "2"),
-						ExecuteSync:     util.Contains(options, "3"),
+						IsFailureAction: slices.Contains(options, "1"),
+						StopOnFailure:   slices.Contains(options, "2"),
+						ExecuteSync:     slices.Contains(options, "3"),
 					},
 					},
 				})
 				})
 			}
 			}

+ 4 - 3
internal/httpd/webclient.go

@@ -27,6 +27,7 @@ import (
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -1463,7 +1464,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
 	share.LastUseAt = 0
 	share.LastUseAt = 0
 	share.Username = claims.Username
 	share.Username = claims.Username
 	if share.Password == "" {
 	if share.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			s.renderAddUpdateSharePage(w, r, share,
 			s.renderAddUpdateSharePage(w, r, share,
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				true)
 				true)
@@ -1532,7 +1533,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
 		updatedShare.Password = share.Password
 		updatedShare.Password = share.Password
 	}
 	}
 	if updatedShare.Password == "" {
 	if updatedShare.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			s.renderAddUpdateSharePage(w, r, updatedShare,
 			s.renderAddUpdateSharePage(w, r, updatedShare,
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				false)
 				false)
@@ -2015,7 +2016,7 @@ func doCheckExist(w http.ResponseWriter, r *http.Request, connection *Connection
 		}
 		}
 		existing := make([]map[string]any, 0)
 		existing := make([]map[string]any, 0)
 		for _, info := range contents {
 		for _, info := range contents {
-			if util.Contains(filesList.Files, info.Name()) {
+			if slices.Contains(filesList.Files, info.Name()) {
 				res := make(map[string]any)
 				res := make(map[string]any)
 				res["name"] = info.Name()
 				res["name"] = info.Name()
 				if info.IsDir() {
 				if info.IsDir() {

+ 25 - 25
internal/httpdtest/httpdtest.go

@@ -25,6 +25,7 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
@@ -36,7 +37,6 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/httpclient"
 	"github.com/drakkan/sftpgo/v2/internal/httpclient"
 	"github.com/drakkan/sftpgo/v2/internal/httpd"
 	"github.com/drakkan/sftpgo/v2/internal/httpd"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/version"
 	"github.com/drakkan/sftpgo/v2/internal/version"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
 )
@@ -1679,7 +1679,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
 		return errors.New("condition protocols mismatch")
 		return errors.New("condition protocols mismatch")
 	}
 	}
 	for _, v := range expected.Protocols {
 	for _, v := range expected.Protocols {
-		if !util.Contains(actual.Protocols, v) {
+		if !slices.Contains(actual.Protocols, v) {
 			return errors.New("condition protocols content mismatch")
 			return errors.New("condition protocols content mismatch")
 		}
 		}
 	}
 	}
@@ -1687,7 +1687,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
 		return errors.New("condition provider objects mismatch")
 		return errors.New("condition provider objects mismatch")
 	}
 	}
 	for _, v := range expected.ProviderObjects {
 	for _, v := range expected.ProviderObjects {
-		if !util.Contains(actual.ProviderObjects, v) {
+		if !slices.Contains(actual.ProviderObjects, v) {
 			return errors.New("condition provider objects content mismatch")
 			return errors.New("condition provider objects content mismatch")
 		}
 		}
 	}
 	}
@@ -1705,7 +1705,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
 		return errors.New("fs events mismatch")
 		return errors.New("fs events mismatch")
 	}
 	}
 	for _, v := range expected.FsEvents {
 	for _, v := range expected.FsEvents {
-		if !util.Contains(actual.FsEvents, v) {
+		if !slices.Contains(actual.FsEvents, v) {
 			return errors.New("fs events content mismatch")
 			return errors.New("fs events content mismatch")
 		}
 		}
 	}
 	}
@@ -1713,7 +1713,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
 		return errors.New("provider events mismatch")
 		return errors.New("provider events mismatch")
 	}
 	}
 	for _, v := range expected.ProviderEvents {
 	for _, v := range expected.ProviderEvents {
-		if !util.Contains(actual.ProviderEvents, v) {
+		if !slices.Contains(actual.ProviderEvents, v) {
 			return errors.New("provider events content mismatch")
 			return errors.New("provider events content mismatch")
 		}
 		}
 	}
 	}
@@ -1948,7 +1948,7 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
 		return errors.New("permissions mismatch")
 		return errors.New("permissions mismatch")
 	}
 	}
 	for _, p := range expected.Permissions {
 	for _, p := range expected.Permissions {
-		if !util.Contains(actual.Permissions, p) {
+		if !slices.Contains(actual.Permissions, p) {
 			return errors.New("permissions content mismatch")
 			return errors.New("permissions content mismatch")
 		}
 		}
 	}
 	}
@@ -1966,7 +1966,7 @@ func compareAdminFilters(expected, actual dataprovider.AdminFilters) error {
 		return errors.New("allow list mismatch")
 		return errors.New("allow list mismatch")
 	}
 	}
 	for _, v := range expected.AllowList {
 	for _, v := range expected.AllowList {
-		if !util.Contains(actual.AllowList, v) {
+		if !slices.Contains(actual.AllowList, v) {
 			return errors.New("allow list content mismatch")
 			return errors.New("allow list content mismatch")
 		}
 		}
 	}
 	}
@@ -2057,7 +2057,7 @@ func compareUserPermissions(expected map[string][]string, actual map[string][]st
 	for dir, perms := range expected {
 	for dir, perms := range expected {
 		if actualPerms, ok := actual[dir]; ok {
 		if actualPerms, ok := actual[dir]; ok {
 			for _, v := range actualPerms {
 			for _, v := range actualPerms {
-				if !util.Contains(perms, v) {
+				if !slices.Contains(perms, v) {
 					return errors.New("permissions contents mismatch")
 					return errors.New("permissions contents mismatch")
 				}
 				}
 			}
 			}
@@ -2310,7 +2310,7 @@ func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
 		return errors.New("SFTPFs fingerprints mismatch")
 		return errors.New("SFTPFs fingerprints mismatch")
 	}
 	}
 	for _, value := range actual.SFTPConfig.Fingerprints {
 	for _, value := range actual.SFTPConfig.Fingerprints {
-		if !util.Contains(expected.SFTPConfig.Fingerprints, value) {
+		if !slices.Contains(expected.SFTPConfig.Fingerprints, value) {
 			return errors.New("SFTPFs fingerprints mismatch")
 			return errors.New("SFTPFs fingerprints mismatch")
 		}
 		}
 	}
 	}
@@ -2401,27 +2401,27 @@ func checkEncryptedSecret(expected, actual *kms.Secret) error {
 
 
 func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
 func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
 	for _, IPMask := range expected.AllowedIP {
 	for _, IPMask := range expected.AllowedIP {
-		if !util.Contains(actual.AllowedIP, IPMask) {
+		if !slices.Contains(actual.AllowedIP, IPMask) {
 			return errors.New("allowed IP contents mismatch")
 			return errors.New("allowed IP contents mismatch")
 		}
 		}
 	}
 	}
 	for _, IPMask := range expected.DeniedIP {
 	for _, IPMask := range expected.DeniedIP {
-		if !util.Contains(actual.DeniedIP, IPMask) {
+		if !slices.Contains(actual.DeniedIP, IPMask) {
 			return errors.New("denied IP contents mismatch")
 			return errors.New("denied IP contents mismatch")
 		}
 		}
 	}
 	}
 	for _, method := range expected.DeniedLoginMethods {
 	for _, method := range expected.DeniedLoginMethods {
-		if !util.Contains(actual.DeniedLoginMethods, method) {
+		if !slices.Contains(actual.DeniedLoginMethods, method) {
 			return errors.New("denied login methods contents mismatch")
 			return errors.New("denied login methods contents mismatch")
 		}
 		}
 	}
 	}
 	for _, protocol := range expected.DeniedProtocols {
 	for _, protocol := range expected.DeniedProtocols {
-		if !util.Contains(actual.DeniedProtocols, protocol) {
+		if !slices.Contains(actual.DeniedProtocols, protocol) {
 			return errors.New("denied protocols contents mismatch")
 			return errors.New("denied protocols contents mismatch")
 		}
 		}
 	}
 	}
 	for _, options := range expected.WebClient {
 	for _, options := range expected.WebClient {
-		if !util.Contains(actual.WebClient, options) {
+		if !slices.Contains(actual.WebClient, options) {
 			return errors.New("web client options contents mismatch")
 			return errors.New("web client options contents mismatch")
 		}
 		}
 	}
 	}
@@ -2430,7 +2430,7 @@ func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUs
 		return errors.New("TLS certs mismatch")
 		return errors.New("TLS certs mismatch")
 	}
 	}
 	for _, cert := range expected.TLSCerts {
 	for _, cert := range expected.TLSCerts {
-		if !util.Contains(actual.TLSCerts, cert) {
+		if !slices.Contains(actual.TLSCerts, cert) {
 			return errors.New("TLS certs content mismatch")
 			return errors.New("TLS certs content mismatch")
 		}
 		}
 	}
 	}
@@ -2527,7 +2527,7 @@ func checkFilterMatch(expected []string, actual []string) bool {
 		return false
 		return false
 	}
 	}
 	for _, e := range expected {
 	for _, e := range expected {
-		if !util.Contains(actual, strings.ToLower(e)) {
+		if !slices.Contains(actual, strings.ToLower(e)) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -2570,7 +2570,7 @@ func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.B
 			return errors.New("bandwidth filters sources mismatch")
 			return errors.New("bandwidth filters sources mismatch")
 		}
 		}
 		for _, source := range actual.BandwidthLimits[idx].Sources {
 		for _, source := range actual.BandwidthLimits[idx].Sources {
-			if !util.Contains(l.Sources, source) {
+			if !slices.Contains(l.Sources, source) {
 				return errors.New("bandwidth filters source mismatch")
 				return errors.New("bandwidth filters source mismatch")
 			}
 			}
 		}
 		}
@@ -2680,7 +2680,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email recipients mismatch")
 		return errors.New("email recipients mismatch")
 	}
 	}
 	for _, v := range expected.Recipients {
 	for _, v := range expected.Recipients {
-		if !util.Contains(actual.Recipients, v) {
+		if !slices.Contains(actual.Recipients, v) {
 			return errors.New("email recipients content mismatch")
 			return errors.New("email recipients content mismatch")
 		}
 		}
 	}
 	}
@@ -2688,7 +2688,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email bcc mismatch")
 		return errors.New("email bcc mismatch")
 	}
 	}
 	for _, v := range expected.Bcc {
 	for _, v := range expected.Bcc {
-		if !util.Contains(actual.Bcc, v) {
+		if !slices.Contains(actual.Bcc, v) {
 			return errors.New("email bcc content mismatch")
 			return errors.New("email bcc content mismatch")
 		}
 		}
 	}
 	}
@@ -2705,7 +2705,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email attachments mismatch")
 		return errors.New("email attachments mismatch")
 	}
 	}
 	for _, v := range expected.Attachments {
 	for _, v := range expected.Attachments {
-		if !util.Contains(actual.Attachments, v) {
+		if !slices.Contains(actual.Attachments, v) {
 			return errors.New("email attachments content mismatch")
 			return errors.New("email attachments content mismatch")
 		}
 		}
 	}
 	}
@@ -2720,7 +2720,7 @@ func compareEventActionFsCompressFields(expected, actual dataprovider.EventActio
 		return errors.New("fs compress paths mismatch")
 		return errors.New("fs compress paths mismatch")
 	}
 	}
 	for _, v := range expected.Paths {
 	for _, v := range expected.Paths {
-		if !util.Contains(actual.Paths, v) {
+		if !slices.Contains(actual.Paths, v) {
 			return errors.New("fs compress paths content mismatch")
 			return errors.New("fs compress paths content mismatch")
 		}
 		}
 	}
 	}
@@ -2741,7 +2741,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs deletes mismatch")
 		return errors.New("fs deletes mismatch")
 	}
 	}
 	for _, v := range expected.Deletes {
 	for _, v := range expected.Deletes {
-		if !util.Contains(actual.Deletes, v) {
+		if !slices.Contains(actual.Deletes, v) {
 			return errors.New("fs deletes content mismatch")
 			return errors.New("fs deletes content mismatch")
 		}
 		}
 	}
 	}
@@ -2749,7 +2749,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs mkdirs mismatch")
 		return errors.New("fs mkdirs mismatch")
 	}
 	}
 	for _, v := range expected.MkDirs {
 	for _, v := range expected.MkDirs {
-		if !util.Contains(actual.MkDirs, v) {
+		if !slices.Contains(actual.MkDirs, v) {
 			return errors.New("fs mkdir content mismatch")
 			return errors.New("fs mkdir content mismatch")
 		}
 		}
 	}
 	}
@@ -2757,7 +2757,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs exist mismatch")
 		return errors.New("fs exist mismatch")
 	}
 	}
 	for _, v := range expected.Exist {
 	for _, v := range expected.Exist {
-		if !util.Contains(actual.Exist, v) {
+		if !slices.Contains(actual.Exist, v) {
 			return errors.New("fs exist content mismatch")
 			return errors.New("fs exist content mismatch")
 		}
 		}
 	}
 	}
@@ -2788,7 +2788,7 @@ func compareEventActionCmdConfigFields(expected, actual dataprovider.EventAction
 		return errors.New("cmd args mismatch")
 		return errors.New("cmd args mismatch")
 	}
 	}
 	for _, v := range expected.Args {
 	for _, v := range expected.Args {
-		if !util.Contains(actual.Args, v) {
+		if !slices.Contains(actual.Args, v) {
 			return errors.New("cmd args content mismatch")
 			return errors.New("cmd args content mismatch")
 		}
 		}
 	}
 	}

+ 3 - 3
internal/plugin/kms.go

@@ -17,6 +17,7 @@ package plugin
 import (
 import (
 	"fmt"
 	"fmt"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 
 
 	"github.com/hashicorp/go-hclog"
 	"github.com/hashicorp/go-hclog"
 	"github.com/hashicorp/go-plugin"
 	"github.com/hashicorp/go-plugin"
@@ -25,7 +26,6 @@ import (
 
 
 	"github.com/drakkan/sftpgo/v2/internal/kms"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 )
 
 
 var (
 var (
@@ -41,10 +41,10 @@ type KMSConfig struct {
 }
 }
 
 
 func (c *KMSConfig) validate() error {
 func (c *KMSConfig) validate() error {
-	if !util.Contains(validKMSSchemes, c.Scheme) {
+	if !slices.Contains(validKMSSchemes, c.Scheme) {
 		return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
 		return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
 	}
 	}
-	if !util.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
+	if !slices.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
 		return fmt.Errorf("invalid kms encrypted status: %v", c.EncryptedStatus)
 		return fmt.Errorf("invalid kms encrypted status: %v", c.EncryptedStatus)
 	}
 	}
 	return nil
 	return nil

+ 4 - 4
internal/plugin/notifier.go

@@ -16,6 +16,7 @@ package plugin
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"slices"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
@@ -24,7 +25,6 @@ import (
 	"github.com/sftpgo/sdk/plugin/notifier"
 	"github.com/sftpgo/sdk/plugin/notifier"
 
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 )
 
 
 // NotifierConfig defines configuration parameters for notifiers plugins
 // NotifierConfig defines configuration parameters for notifiers plugins
@@ -220,7 +220,7 @@ func (p *notifierPlugin) canQueueEvent(timestamp int64) bool {
 }
 }
 
 
 func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
 func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
-	if !util.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
+	if !slices.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
 		return
 		return
 	}
 	}
 
 
@@ -233,8 +233,8 @@ func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
 }
 }
 
 
 func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, object Renderer) {
 func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, object Renderer) {
-	if !util.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
-		!util.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
+	if !slices.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
+		!slices.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
 		return
 		return
 	}
 	}
 
 

+ 2 - 1
internal/plugin/plugin.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
@@ -336,7 +337,7 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username
 	var e *notifier.LogEvent
 	var e *notifier.LogEvent
 
 
 	for _, n := range m.notifiers {
 	for _, n := range m.notifiers {
-		if util.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
+		if slices.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
 			if e == nil {
 			if e == nil {
 				message := ""
 				message := ""
 				if err != nil {
 				if err != nil {

+ 2 - 1
internal/service/service_portable.go

@@ -20,6 +20,7 @@ package service
 import (
 import (
 	"fmt"
 	"fmt"
 	"math/rand"
 	"math/rand"
+	"slices"
 	"strings"
 	"strings"
 
 
 	"github.com/sftpgo/sdk"
 	"github.com/sftpgo/sdk"
@@ -211,7 +212,7 @@ func configurePortableSFTPService(port int, enabledSSHCommands []string) {
 	} else {
 	} else {
 		sftpdConf.Bindings[0].Port = 0
 		sftpdConf.Bindings[0].Port = 0
 	}
 	}
-	if util.Contains(enabledSSHCommands, "*") {
+	if slices.Contains(enabledSSHCommands, "*") {
 		sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
 		sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
 	} else {
 	} else {
 		sftpdConf.EnabledSSHCommands = enabledSSHCommands
 		sftpdConf.EnabledSSHCommands = enabledSSHCommands

+ 4 - 3
internal/sftpd/internal_test.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -418,7 +419,7 @@ func TestSupportedSSHCommands(t *testing.T) {
 	assert.Equal(t, len(supportedSSHCommands), len(cmds))
 	assert.Equal(t, len(supportedSSHCommands), len(cmds))
 
 
 	for _, c := range cmds {
 	for _, c := range cmds {
-		assert.True(t, util.Contains(supportedSSHCommands, c))
+		assert.True(t, slices.Contains(supportedSSHCommands, c))
 	}
 	}
 }
 }
 
 
@@ -842,7 +843,7 @@ func TestRsyncOptions(t *testing.T) {
 	}
 	}
 	cmd, err := sshCmd.getSystemCommand()
 	cmd, err := sshCmd.getSystemCommand()
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	assert.True(t, util.Contains(cmd.cmd.Args, "--safe-links"),
+	assert.True(t, slices.Contains(cmd.cmd.Args, "--safe-links"),
 		"--safe-links must be added if the user has the create symlinks permission")
 		"--safe-links must be added if the user has the create symlinks permission")
 
 
 	permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
 	permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
@@ -859,7 +860,7 @@ func TestRsyncOptions(t *testing.T) {
 	}
 	}
 	cmd, err = sshCmd.getSystemCommand()
 	cmd, err = sshCmd.getSystemCommand()
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	assert.True(t, util.Contains(cmd.cmd.Args, "--munge-links"),
+	assert.True(t, slices.Contains(cmd.cmd.Args, "--munge-links"),
 		"--munge-links must be added if the user has the create symlinks permission")
 		"--munge-links must be added if the user has the create symlinks permission")
 
 
 	sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{
 	sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{

+ 17 - 16
internal/sftpd/server.go

@@ -26,6 +26,7 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"runtime/debug"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -263,13 +264,13 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
 func (c *Configuration) updateSupportedAuthentications() {
 func (c *Configuration) updateSupportedAuthentications() {
 	serviceStatus.Authentications = util.RemoveDuplicates(serviceStatus.Authentications, false)
 	serviceStatus.Authentications = util.RemoveDuplicates(serviceStatus.Authentications, false)
 
 
-	if util.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
-		util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
+	if slices.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
+		slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndPassword)
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndPassword)
 	}
 	}
 
 
-	if util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
-		util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
+	if slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
+		slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndKeyboardInt)
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndKeyboardInt)
 	}
 	}
 }
 }
@@ -422,7 +423,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
 		c.HostKeyAlgorithms = util.RemoveDuplicates(c.HostKeyAlgorithms, true)
 		c.HostKeyAlgorithms = util.RemoveDuplicates(c.HostKeyAlgorithms, true)
 	}
 	}
 	for _, hostKeyAlgo := range c.HostKeyAlgorithms {
 	for _, hostKeyAlgo := range c.HostKeyAlgorithms {
-		if !util.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
+		if !slices.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
 			return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
 			return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
 		}
 		}
 	}
 	}
@@ -430,7 +431,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
 	if len(c.PublicKeyAlgorithms) > 0 {
 	if len(c.PublicKeyAlgorithms) > 0 {
 		c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
 		c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
 		for _, algo := range c.PublicKeyAlgorithms {
 		for _, algo := range c.PublicKeyAlgorithms {
-			if !util.Contains(supportedPublicKeyAlgos, algo) {
+			if !slices.Contains(supportedPublicKeyAlgos, algo) {
 				return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
 				return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
 			}
 			}
 		}
 		}
@@ -472,7 +473,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 			if kex == keyExchangeCurve25519SHA256LibSSH {
 			if kex == keyExchangeCurve25519SHA256LibSSH {
 				continue
 				continue
 			}
 			}
-			if !util.Contains(supportedKexAlgos, kex) {
+			if !slices.Contains(supportedKexAlgos, kex) {
 				return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
 				return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
 			}
 			}
 		}
 		}
@@ -486,7 +487,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 	if len(c.Ciphers) > 0 {
 	if len(c.Ciphers) > 0 {
 		c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
 		c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
 		for _, cipher := range c.Ciphers {
 		for _, cipher := range c.Ciphers {
-			if !util.Contains(supportedCiphers, cipher) {
+			if !slices.Contains(supportedCiphers, cipher) {
 				return fmt.Errorf("unsupported cipher %q", cipher)
 				return fmt.Errorf("unsupported cipher %q", cipher)
 			}
 			}
 		}
 		}
@@ -499,7 +500,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 	if len(c.MACs) > 0 {
 	if len(c.MACs) > 0 {
 		c.MACs = util.RemoveDuplicates(c.MACs, true)
 		c.MACs = util.RemoveDuplicates(c.MACs, true)
 		for _, mac := range c.MACs {
 		for _, mac := range c.MACs {
-			if !util.Contains(supportedMACs, mac) {
+			if !slices.Contains(supportedMACs, mac) {
 				return fmt.Errorf("unsupported MAC algorithm %q", mac)
 				return fmt.Errorf("unsupported MAC algorithm %q", mac)
 			}
 			}
 		}
 		}
@@ -785,7 +786,7 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
 			user.Username, user.HomeDir)
 			user.Username, user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol SSH is not allowed", user.Username)
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol SSH is not allowed", user.Username)
 		return nil, fmt.Errorf("protocol SSH is not allowed for user %q", user.Username)
 		return nil, fmt.Errorf("protocol SSH is not allowed for user %q", user.Username)
 	}
 	}
@@ -830,14 +831,14 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
 }
 }
 
 
 func (c *Configuration) checkSSHCommands() {
 func (c *Configuration) checkSSHCommands() {
-	if util.Contains(c.EnabledSSHCommands, "*") {
+	if slices.Contains(c.EnabledSSHCommands, "*") {
 		c.EnabledSSHCommands = GetSupportedSSHCommands()
 		c.EnabledSSHCommands = GetSupportedSSHCommands()
 		return
 		return
 	}
 	}
 	sshCommands := []string{}
 	sshCommands := []string{}
 	for _, command := range c.EnabledSSHCommands {
 	for _, command := range c.EnabledSSHCommands {
 		command = strings.TrimSpace(command)
 		command = strings.TrimSpace(command)
-		if util.Contains(supportedSSHCommands, command) {
+		if slices.Contains(supportedSSHCommands, command) {
 			sshCommands = append(sshCommands, command)
 			sshCommands = append(sshCommands, command)
 		} else {
 		} else {
 			logger.Warn(logSender, "", "unsupported ssh command: %q ignored", command)
 			logger.Warn(logSender, "", "unsupported ssh command: %q ignored", command)
@@ -927,7 +928,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
 func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
 func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
 	var algos []string
 	var algos []string
 	for _, algo := range algorithmsForKeyFormat(keyFormat) {
 	for _, algo := range algorithmsForKeyFormat(keyFormat) {
-		if util.Contains(c.HostKeyAlgorithms, algo) {
+		if slices.Contains(c.HostKeyAlgorithms, algo) {
 			algos = append(algos, algo)
 			algos = append(algos, algo)
 		}
 		}
 	}
 	}
@@ -986,7 +987,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
 				var algos []string
 				var algos []string
 				for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
 				for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
 					if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
 					if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
-						if util.Contains(mas.Algorithms(), underlyingAlgo) {
+						if slices.Contains(mas.Algorithms(), underlyingAlgo) {
 							algos = append(algos, algo)
 							algos = append(algos, algo)
 						}
 						}
 					}
 					}
@@ -1098,12 +1099,12 @@ func (c *Configuration) initializeCertChecker(configDir string) error {
 
 
 func (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {
 func (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {
 	err := &ssh.PartialSuccessError{}
 	err := &ssh.PartialSuccessError{}
-	if c.PasswordAuthentication && util.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
+	if c.PasswordAuthentication && slices.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
 		err.Next.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
 		err.Next.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
 			return c.validatePasswordCredentials(conn, password, dataprovider.SSHLoginMethodKeyAndPassword)
 			return c.validatePasswordCredentials(conn, password, dataprovider.SSHLoginMethodKeyAndPassword)
 		}
 		}
 	}
 	}
-	if c.KeyboardInteractiveAuthentication && util.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
+	if c.KeyboardInteractiveAuthentication && slices.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
 		err.Next.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
 		err.Next.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
 			return c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyAndKeyboardInt, true)
 			return c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyAndKeyboardInt, true)
 		}
 		}

+ 3 - 2
internal/sftpd/sftpd_test.go

@@ -38,6 +38,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -8639,8 +8640,8 @@ func TestUserAllowedLoginMethods(t *testing.T) {
 	allowedMethods = user.GetAllowedLoginMethods()
 	allowedMethods = user.GetAllowedLoginMethods()
 	assert.Equal(t, 4, len(allowedMethods))
 	assert.Equal(t, 4, len(allowedMethods))
 
 
-	assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
-	assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
+	assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
+	assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
 }
 }
 
 
 func TestUserPartialAuth(t *testing.T) {
 func TestUserPartialAuth(t *testing.T) {

+ 6 - 5
internal/sftpd/ssh_cmd.go

@@ -27,6 +27,7 @@ import (
 	"os/exec"
 	"os/exec"
 	"path"
 	"path"
 	"runtime/debug"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -91,7 +92,7 @@ func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommand
 		name, args, err := parseCommandPayload(msg.Command)
 		name, args, err := parseCommandPayload(msg.Command)
 		connection.Log(logger.LevelDebug, "new ssh command: %q args: %v num args: %d user: %s, error: %v",
 		connection.Log(logger.LevelDebug, "new ssh command: %q args: %v num args: %d user: %s, error: %v",
 			name, args, len(args), connection.User.Username, err)
 			name, args, len(args), connection.User.Username, err)
-		if err == nil && util.Contains(enabledSSHCommands, name) {
+		if err == nil && slices.Contains(enabledSSHCommands, name) {
 			connection.command = msg.Command
 			connection.command = msg.Command
 			if name == scpCmdName && len(args) >= 2 {
 			if name == scpCmdName && len(args) >= 2 {
 				connection.SetProtocol(common.ProtocolSCP)
 				connection.SetProtocol(common.ProtocolSCP)
@@ -139,9 +140,9 @@ func (c *sshCommand) handle() (err error) {
 	defer common.Connections.Remove(c.connection.GetID())
 	defer common.Connections.Remove(c.connection.GetID())
 
 
 	c.connection.UpdateLastActivity()
 	c.connection.UpdateLastActivity()
-	if util.Contains(sshHashCommands, c.command) {
+	if slices.Contains(sshHashCommands, c.command) {
 		return c.handleHashCommands()
 		return c.handleHashCommands()
-	} else if util.Contains(systemCommands, c.command) {
+	} else if slices.Contains(systemCommands, c.command) {
 		command, err := c.getSystemCommand()
 		command, err := c.getSystemCommand()
 		if err != nil {
 		if err != nil {
 			return c.sendErrorResponse(err)
 			return c.sendErrorResponse(err)
@@ -429,11 +430,11 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
 		// If the user cannot create symlinks we add the option --munge-links, if it is not
 		// If the user cannot create symlinks we add the option --munge-links, if it is not
 		// already set. This should make symlinks unusable (but manually recoverable)
 		// already set. This should make symlinks unusable (but manually recoverable)
 		if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
 		if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
-			if !util.Contains(args, "--safe-links") {
+			if !slices.Contains(args, "--safe-links") {
 				args = append([]string{"--safe-links"}, args...)
 				args = append([]string{"--safe-links"}, args...)
 			}
 			}
 		} else {
 		} else {
-			if !util.Contains(args, "--munge-links") {
+			if !slices.Contains(args, "--munge-links") {
 				args = append([]string{"--munge-links"}, args...)
 				args = append([]string{"--munge-links"}, args...)
 			}
 			}
 		}
 		}

+ 2 - 2
internal/smtp/oauth2.go

@@ -19,6 +19,7 @@ import (
 	"context"
 	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"slices"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
@@ -27,7 +28,6 @@ import (
 	"golang.org/x/oauth2/microsoft"
 	"golang.org/x/oauth2/microsoft"
 
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 )
 
 
 // Supported OAuth2 providers
 // Supported OAuth2 providers
@@ -56,7 +56,7 @@ type OAuth2Config struct {
 
 
 // Validate validates and initializes the configuration
 // Validate validates and initializes the configuration
 func (c *OAuth2Config) Validate() error {
 func (c *OAuth2Config) Validate() error {
-	if !util.Contains(supportedOAuth2Providers, c.Provider) {
+	if !slices.Contains(supportedOAuth2Providers, c.Provider) {
 		return fmt.Errorf("smtp oauth2: unsupported provider %d", c.Provider)
 		return fmt.Errorf("smtp oauth2: unsupported provider %d", c.Provider)
 	}
 	}
 	if c.ClientID == "" {
 	if c.ClientID == "" {

+ 0 - 10
internal/util/util.go

@@ -128,16 +128,6 @@ var bytesSizeTable = map[string]uint64{
 	"e":  eByte,
 	"e":  eByte,
 }
 }
 
 
-// Contains reports whether v is present in elems.
-func Contains[T comparable](elems []T, v T) bool {
-	for _, s := range elems {
-		if v == s {
-			return true
-		}
-	}
-	return false
-}
-
 // Remove removes an element from a string slice and
 // Remove removes an element from a string slice and
 // returns the modified slice
 // returns the modified slice
 func Remove(elems []string, val string) []string {
 func Remove(elems []string, val string) []string {

+ 2 - 2
internal/vfs/osfs.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -34,7 +35,6 @@ import (
 	"github.com/sftpgo/sdk"
 	"github.com/sftpgo/sdk"
 
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 )
 
 
 const (
 const (
@@ -475,7 +475,7 @@ func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
 	for fs.IsNotExist(err) {
 	for fs.IsNotExist(err) {
 		results = append(results, parent)
 		results = append(results, parent)
 		parent = filepath.Dir(parent)
 		parent = filepath.Dir(parent)
-		if util.Contains(results, parent) {
+		if slices.Contains(results, parent) {
 			break
 			break
 		}
 		}
 		_, err = os.Stat(parent)
 		_, err = os.Stat(parent)

+ 2 - 1
internal/vfs/s3fs.go

@@ -30,6 +30,7 @@ import (
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"sort"
 	"sort"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -161,7 +162,7 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
 	if err == nil {
 	if err == nil {
 		// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
 		// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
 		// So we check some common content types to detect if this is a "directory".
 		// So we check some common content types to detect if this is a "directory".
-		isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
+		isDir := slices.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
 		if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
 		if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
 			_, err = fs.headObject(name + "/")
 			_, err = fs.headObject(name + "/")
 			isDir = err == nil
 			isDir = err == nil

+ 4 - 3
internal/vfs/sftpfs.go

@@ -28,6 +28,7 @@ import (
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -125,7 +126,7 @@ func (c *SFTPFsConfig) isEqual(other SFTPFsConfig) bool {
 		return false
 		return false
 	}
 	}
 	for _, fp := range c.Fingerprints {
 	for _, fp := range c.Fingerprints {
-		if !util.Contains(other.Fingerprints, fp) {
+		if !slices.Contains(other.Fingerprints, fp) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -954,12 +955,12 @@ func (c *sftpConnection) openConnNoLock() error {
 		User: c.config.Username,
 		User: c.config.Username,
 		HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
 		HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
 			fp := ssh.FingerprintSHA256(key)
 			fp := ssh.FingerprintSHA256(key)
-			if util.Contains(sftpFingerprints, fp) {
+			if slices.Contains(sftpFingerprints, fp) {
 				if allowSelfConnections == 0 {
 				if allowSelfConnections == 0 {
 					logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
 					logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
 					return ErrSFTPLoop
 					return ErrSFTPLoop
 				}
 				}
-				if util.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
+				if slices.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
 					logger.Log(logger.LevelError, c.logSender, "",
 					logger.Log(logger.LevelError, c.logSender, "",
 						"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
 						"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
 						c.config.Username, c.config.forbiddenSelfUsernames)
 						c.config.Username, c.config.forbiddenSelfUsernames)

+ 2 - 1
internal/vfs/vfs.go

@@ -24,6 +24,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"slices"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -764,7 +765,7 @@ func (c *AzBlobFsConfig) validate() error {
 	if err := c.checkPartSizeAndConcurrency(); err != nil {
 	if err := c.checkPartSizeAndConcurrency(); err != nil {
 		return err
 		return err
 	}
 	}
-	if !util.Contains(validAzAccessTier, c.AccessTier) {
+	if !slices.Contains(validAzAccessTier, c.AccessTier) {
 		return fmt.Errorf("invalid access tier %q, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
 		return fmt.Errorf("invalid access tier %q, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
 	}
 	}
 	return nil
 	return nil

+ 2 - 1
internal/webdavd/file.go

@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"path"
 	"path"
+	"slices"
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
 
 
@@ -447,7 +448,7 @@ func (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error
 		pstat := webdav.Propstat{}
 		pstat := webdav.Propstat{}
 		for _, p := range patch.Props {
 		for _, p := range patch.Props {
 			if status == http.StatusForbidden && !hasError {
 			if status == http.StatusForbidden && !hasError {
-				if !patch.Remove && util.Contains(lastModifiedProps, p.XMLName.Local) {
+				if !patch.Remove && slices.Contains(lastModifiedProps, p.XMLName.Local) {
 					parsed, err := parseTime(util.BytesToString(p.InnerXML))
 					parsed, err := parseTime(util.BytesToString(p.InnerXML))
 					if err != nil {
 					if err != nil {
 						f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",
 						f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",

+ 2 - 1
internal/webdavd/server.go

@@ -26,6 +26,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"runtime/debug"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -346,7 +347,7 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
 			user.Username, user.HomeDir)
 			user.Username, user.HomeDir)
 		return connID, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 		return connID, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol DAV is not allowed", user.Username)
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol DAV is not allowed", user.Username)
 		return connID, fmt.Errorf("protocol DAV is not allowed for user %q", user.Username)
 		return connID, fmt.Errorf("protocol DAV is not allowed for user %q", user.Username)
 	}
 	}