Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,175 @@
|
|||
// Package middleware provides HTTP middleware for the MapleFile backend.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware provides rate limiting functionality for HTTP endpoints
|
||||
type RateLimitMiddleware struct {
|
||||
logger *zap.Logger
|
||||
loginRateLimiter ratelimit.LoginRateLimiter
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware creates a new rate limit middleware
|
||||
func NewRateLimitMiddleware(logger *zap.Logger, loginRateLimiter ratelimit.LoginRateLimiter) *RateLimitMiddleware {
|
||||
return &RateLimitMiddleware{
|
||||
logger: logger.Named("RateLimitMiddleware"),
|
||||
loginRateLimiter: loginRateLimiter,
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRateLimit applies login-specific rate limiting to auth endpoints
|
||||
// CWE-307: Protects against brute force attacks on authentication endpoints
|
||||
func (m *RateLimitMiddleware) LoginRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract client IP
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Extract email from request body (need to buffer and restore)
|
||||
email := m.extractEmailFromRequest(r)
|
||||
|
||||
// Check rate limit
|
||||
allowed, isLocked, remainingAttempts, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, email, clientIP)
|
||||
if err != nil {
|
||||
// Log error but allow request (fail open for availability)
|
||||
m.logger.Warn("Rate limiter error, allowing request",
|
||||
zap.Error(err),
|
||||
zap.String("ip", validation.MaskIP(clientIP)))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if isLocked {
|
||||
m.logger.Warn("Login attempt on locked account",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Account temporarily locked due to too many failed attempts. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if IP rate limit exceeded
|
||||
if !allowed {
|
||||
m.logger.Warn("Rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("remaining_attempts", remainingAttempts))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests. Please slow down and try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Add remaining attempts to response header for client awareness
|
||||
if remainingAttempts > 0 && remainingAttempts <= 3 {
|
||||
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remainingAttempts))
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRateLimit applies general rate limiting to auth endpoints
|
||||
// For endpoints like registration, email verification, etc.
|
||||
func (m *RateLimitMiddleware) AuthRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract client IP for rate limiting key
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Use the login rate limiter for IP-based checking only
|
||||
// This provides basic protection against automated attacks
|
||||
ctx := r.Context()
|
||||
allowed, _, _, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, "", clientIP)
|
||||
if err != nil {
|
||||
// Fail open
|
||||
m.logger.Warn("Rate limiter error, allowing request", zap.Error(err))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
m.logger.Warn("Auth rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests from this IP. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// extractClientIP extracts the real client IP from the request
|
||||
func (m *RateLimitMiddleware) extractClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header first (for reverse proxies)
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// Take the first IP in the chain
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
// Remove port if present
|
||||
ip := r.RemoteAddr
|
||||
if idx := strings.LastIndex(ip, ":"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// extractEmailFromRequest extracts email from JSON request body
|
||||
// It buffers the body so it can be read again by the handler
|
||||
func (m *RateLimitMiddleware) extractEmailFromRequest(r *http.Request) string {
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Restore body for handler
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
// Parse JSON to extract email
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(strings.TrimSpace(req.Email))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue