mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
56 lines
1.5 KiB
Go
56 lines
1.5 KiB
Go
package pds
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// jtiReplayCache tracks JWT IDs of recently-seen appview tokens so the same
|
|
// token can't be replayed within its TTL. Entries auto-expire at the token's
|
|
// claimed expiration time (no background sweeper needed — eviction happens
|
|
// lazily on Seen()).
|
|
type jtiReplayCache struct {
|
|
mu sync.Mutex
|
|
entries map[string]time.Time
|
|
clock func() time.Time
|
|
}
|
|
|
|
func newJTIReplayCache() *jtiReplayCache {
|
|
return &jtiReplayCache{entries: make(map[string]time.Time), clock: time.Now}
|
|
}
|
|
|
|
// Seen records `jti` with the given expiration and returns true if the jti was
|
|
// already present (i.e., this is a replay). It opportunistically evicts any
|
|
// already-expired entries it encounters.
|
|
func (c *jtiReplayCache) Seen(jti string, exp time.Time) bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
now := c.clock()
|
|
// lazy eviction: drop the entry we're about to look at if it's expired
|
|
if prev, ok := c.entries[jti]; ok {
|
|
if !prev.After(now) {
|
|
delete(c.entries, jti)
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
// opportunistic sweep — keeps the map bounded under steady-state load
|
|
// without needing a goroutine. O(n) but n is small in practice.
|
|
if len(c.entries) > 0 && len(c.entries)%64 == 0 {
|
|
for k, e := range c.entries {
|
|
if !e.After(now) {
|
|
delete(c.entries, k)
|
|
}
|
|
}
|
|
}
|
|
c.entries[jti] = exp
|
|
return false
|
|
}
|
|
|
|
// size returns the number of tracked entries (test-only helper).
|
|
func (c *jtiReplayCache) size() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return len(c.entries)
|
|
}
|