Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -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))
}