From f5ced3b7175e2e8dc5272ebfc9e256b1f471a346 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 13 Jun 2026 16:41:00 +0300 Subject: [PATCH 1/3] refactor: use cache store from tinyauth --- cache.go | 85 --------------------- cache_store.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 3 +- rate_limiter.go | 82 ++++++++++---------- 4 files changed, 241 insertions(+), 126 deletions(-) delete mode 100644 cache.go create mode 100644 cache_store.go diff --git a/cache.go b/cache.go deleted file mode 100644 index 8e1864e..0000000 --- a/cache.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "sync" - "time" -) - -type cacheField struct { - value any - expire int64 -} - -type Cache struct { - cache map[string]cacheField - mutex sync.RWMutex -} - -func NewCache() *Cache { - cache := &Cache{ - cache: make(map[string]cacheField), - } - cache.cleanup() - return cache -} - -func (c *Cache) Set(key string, value any, ttl int64) { - c.mutex.Lock() - defer c.mutex.Unlock() - - expire := time.Now().Add(time.Duration(ttl) * time.Second).Unix() - - c.cache[key] = cacheField{ - value: value, - expire: expire, - } -} - -func (c *Cache) Get(key string) (any, bool) { - c.mutex.RLock() - - field, ok := c.cache[key] - - if !ok { - c.mutex.RUnlock() - return nil, false - } - - if time.Now().Unix() > field.expire { - c.mutex.RUnlock() - c.Delete(key) - return nil, false - } - - c.mutex.RUnlock() - return field.value, true -} - -func (c *Cache) Delete(key string) { - c.mutex.Lock() - defer c.mutex.Unlock() - delete(c.cache, key) -} - -func (c *Cache) Flush() { - c.mutex.Lock() - defer c.mutex.Unlock() - c.cache = make(map[string]cacheField, 0) -} - -func (c *Cache) cleanup() { - go func() { - ticker := time.NewTicker(24 * time.Hour) - defer ticker.Stop() - - for range ticker.C { - c.mutex.Lock() - for key, field := range c.cache { - if time.Now().Unix() > field.expire { - delete(c.cache, key) - } - } - c.mutex.Unlock() - } - }() -} diff --git a/cache_store.go b/cache_store.go new file mode 100644 index 0000000..bf16239 --- /dev/null +++ b/cache_store.go @@ -0,0 +1,197 @@ +package main + +import ( + "slices" + "sync" + "time" +) + +type CacheStoreActions[T any] struct { + Set func(key string, value T, ttl time.Duration) + Get func(key string) (T, bool) + Delete func(key string) + Update func(key string, value T, ttl time.Duration) bool +} + +type cacheEntry[T any] struct { + value T + expiresAt *time.Time +} + +type CacheStore[T any] struct { + cache map[string]cacheEntry[T] + order []string + mu sync.RWMutex + maxSize int +} + +func NewCacheStore[T any](maxSize int) *CacheStore[T] { + return &CacheStore[T]{ + cache: make(map[string]cacheEntry[T]), + order: make([]string, 0), + maxSize: maxSize, + } +} + +// With lock allows performing multiple operations on the cache store atomically. +// The provided mutate function receives a set of actions (Set, Get, Delete) that +// can be used to manipulate the cache store within the locked context. +func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) { + cs.mu.Lock() + defer cs.mu.Unlock() + actions := CacheStoreActions[T]{ + Set: cs.setCallback, + Get: cs.getCallback, + Delete: cs.deleteCallback, + Update: cs.updateCallback, + } + mutate(actions) +} + +func (cs *CacheStore[T]) updateCallback(key string, value T, ttl time.Duration) bool { + if currentEntry, exists := cs.cache[key]; exists { + if currentEntry.expiresAt != nil && time.Now().After(*currentEntry.expiresAt) { + return false + } + + entry := cacheEntry[T]{ + value: value, + expiresAt: currentEntry.expiresAt, + } + + if ttl > 0 { + expiration := time.Now().Add(ttl) + entry.expiresAt = &expiration + } + + cs.cache[key] = entry + + return true + } + + return false +} + +func (cs *CacheStore[T]) Update(key string, value T, ttl time.Duration) bool { + cs.mu.Lock() + defer cs.mu.Unlock() + return cs.updateCallback(key, value, ttl) +} + +func (cs *CacheStore[T]) setCallback(key string, value T, ttl time.Duration) { + if cs.maxSize > 0 { + if _, exists := cs.cache[key]; !exists && len(cs.cache) >= cs.maxSize { + cs.evictOne() + } + } + + var expiresAt *time.Time + + if ttl > 0 { + expiration := time.Now().Add(ttl) + expiresAt = &expiration + } + + cs.cache[key] = cacheEntry[T]{ + value: value, + expiresAt: expiresAt, + } + + if !slices.Contains(cs.order, key) { + cs.order = append(cs.order, key) + } +} + +func (cs *CacheStore[T]) Set(key string, value T, ttl time.Duration) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.setCallback(key, value, ttl) +} + +func (cs *CacheStore[T]) getCallback(key string) (T, bool) { + entry, exists := cs.cache[key] + + if !exists { + var zero T + return zero, false + } + + if entry.expiresAt != nil && time.Now().After(*entry.expiresAt) { + var zero T + return zero, false + } + + return entry.value, true +} + +func (cs *CacheStore[T]) Get(key string) (T, bool) { + cs.mu.RLock() + defer cs.mu.RUnlock() + return cs.getCallback(key) +} + +func (cs *CacheStore[T]) deleteCallback(key string) { + delete(cs.cache, key) + keyIdx := slices.Index(cs.order, key) + if keyIdx != -1 { + cs.order = append(cs.order[:keyIdx], cs.order[keyIdx+1:]...) + } +} + +func (cs *CacheStore[T]) Delete(key string) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.deleteCallback(key) +} + +func (cs *CacheStore[T]) Sweep() { + cs.mu.Lock() + for key, entry := range cs.cache { + if entry.expiresAt != nil && time.Now().After(*entry.expiresAt) { + cs.deleteCallback(key) + } + } + cs.mu.Unlock() +} + +func (cs *CacheStore[T]) evictOne() bool { + now := time.Now() + var oldestKey string + var oldestExp *time.Time + + for k, e := range cs.cache { + if e.expiresAt != nil && now.After(*e.expiresAt) { + cs.deleteCallback(k) + return true + } + if e.expiresAt != nil && (oldestExp == nil || e.expiresAt.Before(*oldestExp)) { + oldestKey, oldestExp = k, e.expiresAt + } + } + + // If we found an oldest key, evict it else we delete the first key in the order list + if oldestKey != "" { + cs.deleteCallback(oldestKey) + return true + } else { + if len(cs.order) > 0 { + cs.deleteCallback(cs.order[0]) + return true + } + } + + return false +} + +func (cs *CacheStore[T]) Size() int { + cs.mu.RLock() + defer cs.mu.RUnlock() + return len(cs.cache) +} + +func (cs *CacheStore[T]) Clear() { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.cache = make(map[string]cacheEntry[T]) + cs.order = make([]string, 0) +} diff --git a/main.go b/main.go index edee0d4..04785eb 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,6 @@ func main() { );`) queries := queries.New(sqlDb) - cache := NewCache() router := chi.NewRouter() router.Use(middleware.Logger) router.Use(middleware.Recoverer) @@ -87,7 +86,7 @@ func main() { rateLimiter := NewRateLimiter(RateLimitConfig{ RateLimitCount: config.RateLimitCount, TrustedProxies: config.TrustedProxies, - }, cache) + }) instancesHandler := NewInstancesHandler(queries) healthHandler := NewHealthHandler() diff --git a/rate_limiter.go b/rate_limiter.go index 68a3e00..aac6efa 100644 --- a/rate_limiter.go +++ b/rate_limiter.go @@ -2,11 +2,10 @@ package main import ( "fmt" - "log/slog" "net" "net/http" "slices" - "sync" + "strings" "time" ) @@ -17,63 +16,65 @@ type RateLimitConfig struct { type RateLimiter struct { config RateLimitConfig - cache *Cache - mutex sync.RWMutex + caches struct { + ratelimit *CacheStore[int] + } } -func NewRateLimiter(config RateLimitConfig, cache *Cache) *RateLimiter { - return &RateLimiter{ +func NewRateLimiter(config RateLimitConfig) *RateLimiter { + rl := &RateLimiter{ config: config, - cache: cache, } + + ratelimitCache := NewCacheStore[int](0) + rl.caches.ratelimit = ratelimitCache + + go func() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + rl.caches.ratelimit.Sweep() + } + }() + + return rl } func (rl *RateLimiter) limit(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rl.mutex.Lock() - defer rl.mutex.Unlock() - clientIP := rl.getClientIP(r) - if clientIP == "" { http.Error(w, "failed to determine client ip", http.StatusInternalServerError) return } - value, exists := rl.cache.Get(clientIP) + var used int + rl.caches.ratelimit.WithLock(func(actions CacheStoreActions[int]) { + current, exists := actions.Get(clientIP) + if !exists { + actions.Set(clientIP, 1, 12*time.Hour) + used = 1 + return + } + current++ + used = current + if current > rl.config.RateLimitCount { + return + } + actions.Update(clientIP, current, 0) + }) w.Header().Set("x-ratelimit-limit", fmt.Sprint(rl.config.RateLimitCount)) - w.Header().Set("x-ratelimit-reset", fmt.Sprint(time.Now().Add(12*time.Hour).Unix())) - - if !exists { - rl.cache.Set(clientIP, 1, 43200) // 12 hours TTL - w.Header().Set("x-ratelimit-remaining", fmt.Sprint(rl.config.RateLimitCount-1)) - w.Header().Set("x-ratelimit-used", fmt.Sprint(1)) - next.ServeHTTP(w, r) - return - } - - used, ok := value.(int) - - if !ok { - slog.Error("failed to assert rate limit cache value type") - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - used++ + w.Header().Set("x-ratelimit-used", fmt.Sprint(used)) if used > rl.config.RateLimitCount { - w.Header().Set("x-ratelimit-remaining", fmt.Sprint(0)) - w.Header().Set("x-ratelimit-used", fmt.Sprint(used)) + w.Header().Set("x-ratelimit-remaining", "0") http.Error(w, "rate limit exceeded", http.StatusTooManyRequests) return } - rl.cache.Set(clientIP, used, 43200) // 12 hours TTL - w.Header().Set("x-ratelimit-remaining", fmt.Sprint(rl.config.RateLimitCount-used)) - w.Header().Set("x-ratelimit-used", fmt.Sprint(used)) next.ServeHTTP(w, r) }) } @@ -92,10 +93,13 @@ func (rl *RateLimiter) getClientIP(r *http.Request) string { } if slices.Contains(rl.config.TrustedProxies, ip) { - xForwardedFor := r.Header.Values("x-forwarded-for") + xForwardedFor := r.Header.Get("x-forwarded-for") - if len(xForwardedFor) > 0 { - return xForwardedFor[0] + if xForwardedFor != "" { + firstIp := strings.SplitN(xForwardedFor, ",", 2)[0] + if firstIp != "" { + return firstIp + } } } From 313a0cf132180e0e996525139cf4be109b298e7c Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 15 Jun 2026 20:31:15 +0300 Subject: [PATCH 2/3] chore: review comments --- rate_limiter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rate_limiter.go b/rate_limiter.go index aac6efa..a6198e2 100644 --- a/rate_limiter.go +++ b/rate_limiter.go @@ -59,10 +59,10 @@ func (rl *RateLimiter) limit(next http.Handler) http.Handler { } current++ used = current + actions.Update(clientIP, current, 0) if current > rl.config.RateLimitCount { return } - actions.Update(clientIP, current, 0) }) w.Header().Set("x-ratelimit-limit", fmt.Sprint(rl.config.RateLimitCount)) @@ -97,6 +97,7 @@ func (rl *RateLimiter) getClientIP(r *http.Request) string { if xForwardedFor != "" { firstIp := strings.SplitN(xForwardedFor, ",", 2)[0] + firstIp = strings.TrimSpace(firstIp) if firstIp != "" { return firstIp } From 3f46f6a2de420f16c328387d002d2e4614a47af8 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 17 Jul 2026 02:13:06 +0300 Subject: [PATCH 3/3] refactor: pull cache store from tinyauth repo --- .gitignore | 3 + cache_store.go | 197 ------------------------------------------------ go.mod | 11 +-- go.sum | 19 +++++ rate_limiter.go | 8 +- 5 files changed, 33 insertions(+), 205 deletions(-) delete mode 100644 cache_store.go diff --git a/.gitignore b/.gitignore index d7071db..7ce37cd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ __debug_* # data /analytics.db* /data + +# jetbrains +/.idea/ \ No newline at end of file diff --git a/cache_store.go b/cache_store.go deleted file mode 100644 index bf16239..0000000 --- a/cache_store.go +++ /dev/null @@ -1,197 +0,0 @@ -package main - -import ( - "slices" - "sync" - "time" -) - -type CacheStoreActions[T any] struct { - Set func(key string, value T, ttl time.Duration) - Get func(key string) (T, bool) - Delete func(key string) - Update func(key string, value T, ttl time.Duration) bool -} - -type cacheEntry[T any] struct { - value T - expiresAt *time.Time -} - -type CacheStore[T any] struct { - cache map[string]cacheEntry[T] - order []string - mu sync.RWMutex - maxSize int -} - -func NewCacheStore[T any](maxSize int) *CacheStore[T] { - return &CacheStore[T]{ - cache: make(map[string]cacheEntry[T]), - order: make([]string, 0), - maxSize: maxSize, - } -} - -// With lock allows performing multiple operations on the cache store atomically. -// The provided mutate function receives a set of actions (Set, Get, Delete) that -// can be used to manipulate the cache store within the locked context. -func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) { - cs.mu.Lock() - defer cs.mu.Unlock() - actions := CacheStoreActions[T]{ - Set: cs.setCallback, - Get: cs.getCallback, - Delete: cs.deleteCallback, - Update: cs.updateCallback, - } - mutate(actions) -} - -func (cs *CacheStore[T]) updateCallback(key string, value T, ttl time.Duration) bool { - if currentEntry, exists := cs.cache[key]; exists { - if currentEntry.expiresAt != nil && time.Now().After(*currentEntry.expiresAt) { - return false - } - - entry := cacheEntry[T]{ - value: value, - expiresAt: currentEntry.expiresAt, - } - - if ttl > 0 { - expiration := time.Now().Add(ttl) - entry.expiresAt = &expiration - } - - cs.cache[key] = entry - - return true - } - - return false -} - -func (cs *CacheStore[T]) Update(key string, value T, ttl time.Duration) bool { - cs.mu.Lock() - defer cs.mu.Unlock() - return cs.updateCallback(key, value, ttl) -} - -func (cs *CacheStore[T]) setCallback(key string, value T, ttl time.Duration) { - if cs.maxSize > 0 { - if _, exists := cs.cache[key]; !exists && len(cs.cache) >= cs.maxSize { - cs.evictOne() - } - } - - var expiresAt *time.Time - - if ttl > 0 { - expiration := time.Now().Add(ttl) - expiresAt = &expiration - } - - cs.cache[key] = cacheEntry[T]{ - value: value, - expiresAt: expiresAt, - } - - if !slices.Contains(cs.order, key) { - cs.order = append(cs.order, key) - } -} - -func (cs *CacheStore[T]) Set(key string, value T, ttl time.Duration) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.setCallback(key, value, ttl) -} - -func (cs *CacheStore[T]) getCallback(key string) (T, bool) { - entry, exists := cs.cache[key] - - if !exists { - var zero T - return zero, false - } - - if entry.expiresAt != nil && time.Now().After(*entry.expiresAt) { - var zero T - return zero, false - } - - return entry.value, true -} - -func (cs *CacheStore[T]) Get(key string) (T, bool) { - cs.mu.RLock() - defer cs.mu.RUnlock() - return cs.getCallback(key) -} - -func (cs *CacheStore[T]) deleteCallback(key string) { - delete(cs.cache, key) - keyIdx := slices.Index(cs.order, key) - if keyIdx != -1 { - cs.order = append(cs.order[:keyIdx], cs.order[keyIdx+1:]...) - } -} - -func (cs *CacheStore[T]) Delete(key string) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.deleteCallback(key) -} - -func (cs *CacheStore[T]) Sweep() { - cs.mu.Lock() - for key, entry := range cs.cache { - if entry.expiresAt != nil && time.Now().After(*entry.expiresAt) { - cs.deleteCallback(key) - } - } - cs.mu.Unlock() -} - -func (cs *CacheStore[T]) evictOne() bool { - now := time.Now() - var oldestKey string - var oldestExp *time.Time - - for k, e := range cs.cache { - if e.expiresAt != nil && now.After(*e.expiresAt) { - cs.deleteCallback(k) - return true - } - if e.expiresAt != nil && (oldestExp == nil || e.expiresAt.Before(*oldestExp)) { - oldestKey, oldestExp = k, e.expiresAt - } - } - - // If we found an oldest key, evict it else we delete the first key in the order list - if oldestKey != "" { - cs.deleteCallback(oldestKey) - return true - } else { - if len(cs.order) > 0 { - cs.deleteCallback(cs.order[0]) - return true - } - } - - return false -} - -func (cs *CacheStore[T]) Size() int { - cs.mu.RLock() - defer cs.mu.RUnlock() - return len(cs.cache) -} - -func (cs *CacheStore[T]) Clear() { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.cache = make(map[string]cacheEntry[T]) - cs.order = make([]string, 0) -} diff --git a/go.mod b/go.mod index 7a5d941..528d222 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/tinyauthapp/analytics -go 1.25.0 +go 1.26.4 require ( github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/cors v1.2.2 github.com/go-chi/render v1.0.3 github.com/spf13/viper v1.21.0 - modernc.org/sqlite v1.49.1 + modernc.org/sqlite v1.53.0 ) require ( @@ -26,10 +26,11 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinyauthapp/tinyauth v1.0.1-0.20260716230434-a7eba59a4243 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.28.0 // indirect - modernc.org/libc v1.72.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index d8570ec..e67bff7 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -36,6 +37,7 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -56,19 +58,28 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinyauthapp/tinyauth v1.0.1-0.20260716230434-a7eba59a4243 h1:upiRms1+AoOtPfda8NUwzrgKNIqLdZyMCOswkiU/COs= +github.com/tinyauthapp/tinyauth v1.0.1-0.20260716230434-a7eba59a4243/go.mod h1:EZtN6Y9y0IqUF98QLRv0uDH2nz1zPiEUs3KuuGRjQks= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -76,28 +87,36 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/rate_limiter.go b/rate_limiter.go index a6198e2..b6b38ae 100644 --- a/rate_limiter.go +++ b/rate_limiter.go @@ -7,6 +7,8 @@ import ( "slices" "strings" "time" + + "github.com/tinyauthapp/tinyauth/pkg/cache" ) type RateLimitConfig struct { @@ -17,7 +19,7 @@ type RateLimitConfig struct { type RateLimiter struct { config RateLimitConfig caches struct { - ratelimit *CacheStore[int] + ratelimit *cache.CacheStore[int] } } @@ -26,7 +28,7 @@ func NewRateLimiter(config RateLimitConfig) *RateLimiter { config: config, } - ratelimitCache := NewCacheStore[int](0) + ratelimitCache := cache.NewCacheStore[int](0) rl.caches.ratelimit = ratelimitCache go func() { @@ -50,7 +52,7 @@ func (rl *RateLimiter) limit(next http.Handler) http.Handler { } var used int - rl.caches.ratelimit.WithLock(func(actions CacheStoreActions[int]) { + rl.caches.ratelimit.WithLock(func(actions cache.CacheStoreActions[int]) { current, exists := actions.Get(clientIP) if !exists { actions.Set(clientIP, 1, 12*time.Hour)