diff options
Diffstat (limited to 'lib/api/tokenmanager.go')
-rw-r--r-- | lib/api/tokenmanager.go | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/lib/api/tokenmanager.go b/lib/api/tokenmanager.go new file mode 100644 index 000000000..d03c75923 --- /dev/null +++ b/lib/api/tokenmanager.go @@ -0,0 +1,137 @@ +// Copyright (C) 2024 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package api + +import ( + "time" + + "github.com/syncthing/syncthing/lib/db" + "github.com/syncthing/syncthing/lib/rand" + "github.com/syncthing/syncthing/lib/sync" + "golang.org/x/exp/slices" +) + +type tokenManager struct { + key string + miscDB *db.NamespacedKV + lifetime time.Duration + maxItems int + + timeNow func() time.Time // can be overridden for testing + + mut sync.Mutex + tokens *TokenSet + saveTimer *time.Timer +} + +func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager { + tokens := &TokenSet{ + Tokens: make(map[string]int64), + } + if bs, ok, _ := miscDB.Bytes(key); ok { + _ = tokens.Unmarshal(bs) // best effort + } + return &tokenManager{ + key: key, + miscDB: miscDB, + lifetime: lifetime, + maxItems: maxItems, + timeNow: time.Now, + mut: sync.NewMutex(), + tokens: tokens, + } +} + +// Check returns true if the token is valid, and updates the token's expiry +// time. The token is removed if it is expired. +func (m *tokenManager) Check(token string) bool { + m.mut.Lock() + defer m.mut.Unlock() + + expires, ok := m.tokens.Tokens[token] + if ok { + if expires < m.timeNow().UnixNano() { + // The token is expired. + m.saveLocked() // removes expired tokens + return false + } + + // Give the token further life. + m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano() + m.saveLocked() + } + return ok +} + +// New creates a new token and returns it. +func (m *tokenManager) New() string { + token := rand.String(randomTokenLength) + + m.mut.Lock() + defer m.mut.Unlock() + + m.tokens.Tokens[token] = m.timeNow().Add(m.lifetime).UnixNano() + m.saveLocked() + + return token +} + +// Delete removes a token. +func (m *tokenManager) Delete(token string) { + m.mut.Lock() + defer m.mut.Unlock() + + delete(m.tokens.Tokens, token) + m.saveLocked() +} + +func (m *tokenManager) saveLocked() { + // Remove expired tokens. + now := m.timeNow().UnixNano() + for token, expiry := range m.tokens.Tokens { + if expiry < now { + delete(m.tokens.Tokens, token) + } + } + + // If we have a limit on the number of tokens, remove the oldest ones. + if m.maxItems > 0 && len(m.tokens.Tokens) > m.maxItems { + // Sort the tokens by expiry time, oldest first. + type tokenExpiry struct { + token string + expiry int64 + } + var tokens []tokenExpiry + for token, expiry := range m.tokens.Tokens { + tokens = append(tokens, tokenExpiry{token, expiry}) + } + slices.SortFunc(tokens, func(i, j tokenExpiry) int { + return int(i.expiry - j.expiry) + }) + // Remove the oldest tokens. + for _, token := range tokens[:len(tokens)-m.maxItems] { + delete(m.tokens.Tokens, token.token) + } + } + + // Postpone saving until one second of inactivity. + if m.saveTimer == nil { + m.saveTimer = time.AfterFunc(time.Second, m.scheduledSave) + } else { + m.saveTimer.Reset(time.Second) + } +} + +func (m *tokenManager) scheduledSave() { + m.mut.Lock() + defer m.mut.Unlock() + + m.saveTimer = nil + + bs, _ := m.tokens.Marshal() // can't fail + _ = m.miscDB.PutBytes(m.key, bs) // can fail, but what are we going to do? +} |