2024-01-01 10:31:45 +00:00
|
|
|
// Copyright (C) 2019 Nicola Murino
|
2022-07-17 18:16:00 +00:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
|
|
// by the Free Software Foundation, version 3.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
2023-01-03 09:18:30 +00:00
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2022-07-17 18:16:00 +00:00
|
|
|
|
2021-04-20 07:39:36 +00:00
|
|
|
package dataprovider
|
|
|
|
|
2021-06-05 14:07:09 +00:00
|
|
|
import (
|
2023-04-13 16:23:42 +00:00
|
|
|
"sort"
|
2021-06-05 14:07:09 +00:00
|
|
|
"sync"
|
2023-04-13 16:23:42 +00:00
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
2021-06-05 14:07:09 +00:00
|
|
|
)
|
2021-04-20 07:39:36 +00:00
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
var (
|
|
|
|
cachedUserPasswords credentialsCache
|
|
|
|
cachedAdminPasswords credentialsCache
|
|
|
|
cachedAPIKeys credentialsCache
|
|
|
|
)
|
2021-04-20 07:39:36 +00:00
|
|
|
|
|
|
|
func init() {
|
2023-04-13 16:23:42 +00:00
|
|
|
cachedUserPasswords = credentialsCache{
|
|
|
|
name: "users",
|
|
|
|
sizeLimit: 500,
|
|
|
|
cache: make(map[string]credentialObject),
|
|
|
|
}
|
|
|
|
cachedAdminPasswords = credentialsCache{
|
|
|
|
name: "admins",
|
|
|
|
sizeLimit: 100,
|
|
|
|
cache: make(map[string]credentialObject),
|
|
|
|
}
|
|
|
|
cachedAPIKeys = credentialsCache{
|
|
|
|
name: "API keys",
|
|
|
|
sizeLimit: 500,
|
|
|
|
cache: make(map[string]credentialObject),
|
2021-04-20 07:39:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
// CheckCachedUserPassword is an utility method used only in test cases
|
|
|
|
func CheckCachedUserPassword(username, password, hash string) (bool, bool) {
|
|
|
|
return cachedUserPasswords.Check(username, password, hash)
|
|
|
|
}
|
|
|
|
|
|
|
|
type credentialObject struct {
|
|
|
|
key string
|
|
|
|
hash string
|
|
|
|
password string
|
|
|
|
usedAt *atomic.Int64
|
|
|
|
}
|
|
|
|
|
|
|
|
type credentialsCache struct {
|
|
|
|
name string
|
|
|
|
sizeLimit int
|
2021-04-20 07:39:36 +00:00
|
|
|
sync.RWMutex
|
2023-04-13 16:23:42 +00:00
|
|
|
cache map[string]credentialObject
|
2021-04-20 07:39:36 +00:00
|
|
|
}
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
func (c *credentialsCache) Add(username, password, hash string) {
|
|
|
|
if !config.PasswordCaching || username == "" || password == "" || hash == "" {
|
2021-04-20 07:39:36 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
obj := credentialObject{
|
|
|
|
key: username,
|
|
|
|
hash: hash,
|
|
|
|
password: password,
|
|
|
|
usedAt: &atomic.Int64{},
|
|
|
|
}
|
|
|
|
obj.usedAt.Store(util.GetTimeAsMsSinceEpoch(time.Now()))
|
|
|
|
|
|
|
|
c.cache[username] = obj
|
2021-04-20 07:39:36 +00:00
|
|
|
}
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
func (c *credentialsCache) Remove(username string) {
|
2021-04-20 07:39:36 +00:00
|
|
|
if !config.PasswordCaching {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
|
|
|
|
delete(c.cache, username)
|
|
|
|
}
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
// Check returns if the username is found and if the password match
|
|
|
|
func (c *credentialsCache) Check(username, password, hash string) (bool, bool) {
|
|
|
|
if username == "" || password == "" || hash == "" {
|
2021-04-20 07:39:36 +00:00
|
|
|
return false, false
|
|
|
|
}
|
|
|
|
|
|
|
|
c.RLock()
|
|
|
|
defer c.RUnlock()
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
creds, ok := c.cache[username]
|
2021-04-20 07:39:36 +00:00
|
|
|
if !ok {
|
|
|
|
return false, false
|
|
|
|
}
|
2023-04-13 16:23:42 +00:00
|
|
|
if creds.hash != hash {
|
|
|
|
creds.usedAt.Store(0)
|
|
|
|
return false, false
|
|
|
|
}
|
|
|
|
match := creds.password == password
|
|
|
|
if match {
|
|
|
|
creds.usedAt.Store(util.GetTimeAsMsSinceEpoch(time.Now()))
|
|
|
|
}
|
|
|
|
return true, match
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *credentialsCache) count() int {
|
|
|
|
c.RLock()
|
|
|
|
defer c.RUnlock()
|
2021-04-20 07:39:36 +00:00
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
return len(c.cache)
|
2021-04-20 07:39:36 +00:00
|
|
|
}
|
|
|
|
|
2023-04-13 16:23:42 +00:00
|
|
|
func (c *credentialsCache) cleanup() {
|
|
|
|
if !config.PasswordCaching {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if c.count() <= c.sizeLimit {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
|
|
|
|
for k, v := range c.cache {
|
|
|
|
if v.usedAt.Load() < util.GetTimeAsMsSinceEpoch(time.Now().Add(-60*time.Minute)) {
|
|
|
|
delete(c.cache, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
providerLog(logger.LevelDebug, "size for credentials %q after cleanup: %d", c.name, len(c.cache))
|
|
|
|
|
|
|
|
if len(c.cache) < c.sizeLimit*5 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
numToRemove := len(c.cache) - c.sizeLimit
|
|
|
|
providerLog(logger.LevelDebug, "additional item to remove from credentials %q: %d", c.name, numToRemove)
|
|
|
|
credentials := make([]credentialObject, 0, len(c.cache))
|
|
|
|
for _, v := range c.cache {
|
|
|
|
credentials = append(credentials, v)
|
|
|
|
}
|
|
|
|
sort.Slice(credentials, func(i, j int) bool {
|
|
|
|
return credentials[i].usedAt.Load() < credentials[j].usedAt.Load()
|
|
|
|
})
|
|
|
|
|
|
|
|
for idx := range credentials {
|
|
|
|
if idx >= numToRemove {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
delete(c.cache, credentials[idx].key)
|
|
|
|
}
|
|
|
|
providerLog(logger.LevelDebug, "size for credentials %q after additional cleanup: %d", c.name, len(c.cache))
|
2021-04-20 07:39:36 +00:00
|
|
|
}
|