The codebase that powers boop.cat
boop.cat
1// Copyright 2025 boop.cat
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5package middleware
6
7import (
8 "net/http"
9 "sync"
10 "time"
11)
12
13type visitor struct {
14 limiter *rateLimiter
15 lastSeen time.Time
16}
17
18type rateLimiter struct {
19 rate int
20 per time.Duration
21 tokens int
22 last time.Time
23 mu sync.Mutex
24}
25
26func newRateLimiter(r int, d time.Duration) *rateLimiter {
27 return &rateLimiter{
28 rate: r,
29 per: d,
30 tokens: r,
31 last: time.Now(),
32 }
33}
34
35func (l *rateLimiter) Allow() bool {
36 l.mu.Lock()
37 defer l.mu.Unlock()
38
39 now := time.Now()
40
41 elapsed := now.Sub(l.last)
42 if elapsed > l.per {
43 l.tokens = l.rate
44 l.last = now
45 }
46
47 if l.tokens > 0 {
48 l.tokens--
49 return true
50 }
51 return false
52}
53
54var (
55 visitors = make(map[string]*rateLimiter)
56 mu sync.Mutex
57)
58
59func RateLimit(requests int, window time.Duration) func(http.Handler) http.Handler {
60 return func(next http.Handler) http.Handler {
61
62 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63 ip := r.RemoteAddr
64
65 mu.Lock()
66 limiter, exists := visitors[ip]
67 if !exists {
68 limiter = newRateLimiter(requests, window)
69 visitors[ip] = limiter
70 }
71 mu.Unlock()
72
73 if !limiter.Allow() {
74 http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
75 return
76 }
77 next.ServeHTTP(w, r)
78 })
79 }
80}