initial commit
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type visitor struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
visitors map[string]*visitor
|
||||
rate rate.Limit
|
||||
burst int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func NewRateLimiter(requestsPerMinute int, burst int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
visitors: map[string]*visitor{},
|
||||
rate: rate.Limit(float64(requestsPerMinute) / 60.0),
|
||||
burst: burst,
|
||||
ttl: 10 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RateLimiter) Allow(ip string) bool {
|
||||
now := time.Now()
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
for key, v := range r.visitors {
|
||||
if now.Sub(v.lastSeen) > r.ttl {
|
||||
delete(r.visitors, key)
|
||||
}
|
||||
}
|
||||
|
||||
v, ok := r.visitors[ip]
|
||||
if !ok {
|
||||
v = &visitor{
|
||||
limiter: rate.NewLimiter(r.rate, r.burst),
|
||||
}
|
||||
r.visitors[ip] = v
|
||||
}
|
||||
|
||||
v.lastSeen = now
|
||||
return v.limiter.Allow()
|
||||
}
|
||||
|
||||
func AllowlistMiddleware(allowed []netip.Prefix, trustProxyHeaders bool, logger *log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
if len(allowed) == 0 {
|
||||
return next
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP, ok := extractClientIP(r, trustProxyHeaders)
|
||||
if !ok {
|
||||
http.Error(w, "unable to determine client ip", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
for _, prefix := range allowed {
|
||||
if prefix.Contains(clientIP) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("allowlist denied request method=%s path=%s ip=%s", r.Method, r.URL.Path, clientIP.String())
|
||||
http.Error(w, "ip not allowed", http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RateLimitMiddleware(limiter *RateLimiter, trustProxyHeaders bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP, ok := extractClientIP(r, trustProxyHeaders)
|
||||
if !ok {
|
||||
http.Error(w, "unable to determine client ip", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow(clientIP.String()) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Retry-After", "60")
|
||||
writeJSON(w, http.StatusTooManyRequests, map[string]string{
|
||||
"error": "too many requests from this IP, retry later",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
|
||||
wrapped := handler
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
wrapped = middlewares[i](wrapped)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func extractClientIP(r *http.Request, trustProxyHeaders bool) (netip.Addr, bool) {
|
||||
if trustProxyHeaders {
|
||||
if xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); xff != "" {
|
||||
parts := strings.Split(xff, ",")
|
||||
if len(parts) > 0 {
|
||||
if ip, err := netip.ParseAddr(strings.TrimSpace(parts[0])); err == nil {
|
||||
return ip, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if xrip := strings.TrimSpace(r.Header.Get("X-Real-IP")); xrip != "" {
|
||||
if ip, err := netip.ParseAddr(xrip); err == nil {
|
||||
return ip, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
||||
if err != nil {
|
||||
if ip, parseErr := netip.ParseAddr(strings.TrimSpace(r.RemoteAddr)); parseErr == nil {
|
||||
return ip, true
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return ip, true
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
Reference in New Issue
Block a user