Files
john-okeefe 1a769783dc Phase 2 Week 6: Device Authentication & Rate Limiting
Implement per-device authentication with rate limiting and permissions.

Device Rate Limiter (device_rate_limiter.go):
- DeviceRateLimiter: Track requests per device and request type
- CheckRateLimit: Verify device hasn't exceeded limits
- GetRemainingRequests: Return remaining request quota
- Reset: Clear rate limit data for specific device
- cleanupOldEntries: Remove stale entries automatically
- Request Types: sync, progress, metadata
- Rate Limits:
  * Sync requests: 60/minute
  * Progress updates: 120/minute (page turns)
  * Metadata requests: 30/minute

Device Auth Middleware Updates:
- Add rateLimiter to DeviceAuthMiddleware
- Check rate limits during authentication
- Return 429 Too Many Requests when limits exceeded
- Set rate limit headers:
  * X-RateLimit-Limit: Request limit
  * X-RateLimit-Remaining: Quota remaining
  * X-RateLimit-Reset: Reset time
- getRequestType: Determine request type from URL path

Request Type Detection:
- /progress endpoints → progress type (120/min)
- /metadata, /library endpoints → metadata type (30/min)
- All other sync endpoints → sync type (60/min)

Benefits:
- Prevent device abuse and DoS attacks
- Fair resource allocation across devices
- Higher limits for frequent operations (page turns)
- Lower limits for expensive operations (metadata)
- Automatic cleanup of stale data
- Per-device isolation (one device can't affect others)

Integration with Device Auth:
- Rate limit check happens after token validation
- Before processing actual sync request
- Returns standard HTTP 429 with retry info
- Works seamlessly with existing device middleware

Device revocation still available via:
- DELETE /api/devices/:id endpoint
- Sets auth_token to NULL
- Disables sync_enabled flag
2026-01-30 16:47:29 -05:00

178 lines
3.6 KiB
Go

package middleware
import (
"sync"
"time"
)
type DeviceRateLimiter struct {
requests map[string]*DeviceRequestInfo
mu sync.RWMutex
cleanup *time.Ticker
}
type DeviceRequestInfo struct {
Requests []time.Time
LastSeen time.Time
DeviceID string
}
type DeviceRateLimitConfig struct {
SyncRequestsPerMinute int
ProgressUpdatesPerMinute int
MetadataRequestsPerMinute int
}
const (
DefaultSyncRequestsPerMinute = 60
DefaultProgressUpdatesPerMinute = 120
DefaultMetadataRequestsPerMinute = 30
CleanupInterval = 5 * time.Minute
RequestWindow = 1 * time.Minute
)
func NewDeviceRateLimiter() *DeviceRateLimiter {
limiter := &DeviceRateLimiter{
requests: make(map[string]*DeviceRequestInfo),
cleanup: time.NewTicker(CleanupInterval),
}
go limiter.cleanupOldEntries()
return limiter
}
func (drl *DeviceRateLimiter) CheckRateLimit(deviceID string, requestType string, config DeviceRateLimitConfig) bool {
drl.mu.Lock()
defer drl.mu.Unlock()
now := time.Now()
key := deviceID + ":" + requestType
info, exists := drl.requests[key]
if !exists {
info = &DeviceRequestInfo{
Requests: []time.Time{},
DeviceID: deviceID,
LastSeen: now,
}
drl.requests[key] = info
}
var limit int
switch requestType {
case "sync":
limit = config.SyncRequestsPerMinute
case "progress":
limit = config.ProgressUpdatesPerMinute
case "metadata":
limit = config.MetadataRequestsPerMinute
default:
limit = DefaultSyncRequestsPerMinute
}
if limit == 0 {
limit = DefaultSyncRequestsPerMinute
}
info.Requests = append(info.Requests, now)
info.LastSeen = now
cutoff := now.Add(-RequestWindow)
validRequests := 0
for _, reqTime := range info.Requests {
if reqTime.After(cutoff) {
validRequests++
}
}
info.Requests = info.Requests[len(info.Requests)-validRequests:]
return validRequests <= limit
}
func (drl *DeviceRateLimiter) GetRemainingRequests(deviceID string, requestType string, config DeviceRateLimitConfig) int {
drl.mu.RLock()
defer drl.mu.RUnlock()
key := deviceID + ":" + requestType
info, exists := drl.requests[key]
if !exists {
var limit int
switch requestType {
case "sync":
limit = config.SyncRequestsPerMinute
case "progress":
limit = config.ProgressUpdatesPerMinute
case "metadata":
limit = config.MetadataRequestsPerMinute
default:
limit = DefaultSyncRequestsPerMinute
}
if limit == 0 {
limit = DefaultSyncRequestsPerMinute
}
return limit
}
now := time.Now()
cutoff := now.Add(-RequestWindow)
validRequests := 0
for _, reqTime := range info.Requests {
if reqTime.After(cutoff) {
validRequests++
}
}
var limit int
switch requestType {
case "sync":
limit = config.SyncRequestsPerMinute
case "progress":
limit = config.ProgressUpdatesPerMinute
case "metadata":
limit = config.MetadataRequestsPerMinute
default:
limit = DefaultSyncRequestsPerMinute
}
if limit == 0 {
limit = DefaultSyncRequestsPerMinute
}
remaining := limit - validRequests
if remaining < 0 {
remaining = 0
}
return remaining
}
func (drl *DeviceRateLimiter) Reset(deviceID string) {
drl.mu.Lock()
defer drl.mu.Unlock()
for key := range drl.requests {
if len(key) > len(deviceID) && key[:len(deviceID)] == deviceID {
delete(drl.requests, key)
}
}
}
func (drl *DeviceRateLimiter) cleanupOldEntries() {
for range drl.cleanup.C {
drl.mu.Lock()
cutoff := time.Now().Add(-10 * time.Minute)
for key, info := range drl.requests {
if info.LastSeen.Before(cutoff) {
delete(drl.requests, key)
}
}
drl.mu.Unlock()
}
}
func (drl *DeviceRateLimiter) Stop() {
drl.cleanup.Stop()
}