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, trustedProxies []netip.Prefix, 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, trustedProxies) 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, trustedProxies []netip.Prefix) 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, trustedProxies) 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 AccessLogMiddleware(logger *log.Logger, trustProxyHeaders bool, trustedProxies []netip.Prefix) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { if logger == nil { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !shouldLogAccessRequest(r.Method, r.URL.Path) { next.ServeHTTP(w, r) return } start := time.Now() recorder := &accessLogResponseWriter{ResponseWriter: w} next.ServeHTTP(recorder, r) status := recorder.status if status == 0 { status = http.StatusOK } clientIP := "-" if ip, ok := extractClientIP(r, trustProxyHeaders, trustedProxies); ok { clientIP = ip.String() } logger.Printf( "method=%s path=%s status=%d bytes=%d duration_ms=%d ip=%s ua=%q", r.Method, r.URL.RequestURI(), status, recorder.bytes, time.Since(start).Milliseconds(), clientIP, r.UserAgent(), ) }) } } func shouldLogAccessRequest(method string, path string) bool { if method == http.MethodPost && path == "/api/scratch" { return true } if method == http.MethodGet && strings.HasPrefix(path, "/api/raw/") && len(path) > len("/api/raw/") { return true } return false } 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 } type accessLogResponseWriter struct { http.ResponseWriter status int bytes int } func (w *accessLogResponseWriter) WriteHeader(statusCode int) { w.status = statusCode w.ResponseWriter.WriteHeader(statusCode) } func (w *accessLogResponseWriter) Write(p []byte) (int, error) { if w.status == 0 { w.status = http.StatusOK } n, err := w.ResponseWriter.Write(p) w.bytes += n return n, err } func extractClientIP(r *http.Request, trustProxyHeaders bool, trustedProxies []netip.Prefix) (netip.Addr, bool) { remoteIP, ok := remoteIPFromRequest(r) if !ok { return netip.Addr{}, false } if trustProxyHeaders && isTrustedProxy(remoteIP, trustedProxies) { 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 } } } return remoteIP, true } func remoteIPFromRequest(r *http.Request) (netip.Addr, bool) { 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 isTrustedProxy(remoteIP netip.Addr, trusted []netip.Prefix) bool { for _, prefix := range trusted { if prefix.Contains(remoteIP) { return true } } return false } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) }