130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// CORS allows requests from the frontend.
|
|
// Supports both development (Vite dev server) and production (nginx/Docker) environments.
|
|
func CORS(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
origin := r.Header.Get("Origin")
|
|
|
|
// Allow requests from common development and production origins
|
|
allowedOrigins := []string{
|
|
"http://localhost:5173", // Vite dev server
|
|
"http://localhost", // Docker nginx
|
|
"http://localhost:80", // Docker nginx with port
|
|
}
|
|
|
|
// Check if the origin is allowed
|
|
allowed := slices.Contains(allowedOrigins, origin)
|
|
|
|
// Set CORS headers if origin is allowed
|
|
if allowed {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// requestHistory stores timestamps of requests for each IP address
|
|
type requestHistory struct {
|
|
mu sync.Mutex
|
|
timestamps map[string][]time.Time
|
|
}
|
|
|
|
var rateLimiter = &requestHistory{
|
|
timestamps: make(map[string][]time.Time),
|
|
}
|
|
|
|
// RateLimit restricts requests to 10 per hour per IP address
|
|
// Trace: SRD_FuncReq_0014 - Prevent users from grading more than 10 resumes per hour
|
|
// Trace: SRD_SecReq_0004 - Implement rate limit of 10 requests per hour
|
|
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
|
|
// Trace: SDD_LLD_0011 - Restrict request frequency to 10 calls per hour per unique source IP
|
|
// Trace: SDD_HLD_0006 - Function only within call rate limit of 10 calls per hour
|
|
func RateLimit(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Extract client IP address
|
|
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
|
|
ip := getClientIP(r)
|
|
|
|
rateLimiter.mu.Lock()
|
|
defer rateLimiter.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
oneHourAgo := now.Add(-1 * time.Hour)
|
|
|
|
// Get request history for this IP
|
|
history := rateLimiter.timestamps[ip]
|
|
|
|
// Filter out requests older than 1 hour
|
|
var recentRequests []time.Time
|
|
for _, timestamp := range history {
|
|
if timestamp.After(oneHourAgo) {
|
|
recentRequests = append(recentRequests, timestamp)
|
|
}
|
|
}
|
|
|
|
// Check if rate limit exceeded
|
|
// Trace: SDD_LLD_0011 - Restrict to 10 calls per hour per IP
|
|
if len(recentRequests) >= 10 {
|
|
// Trace: SRD_UseCase_0005, SRD_UseCase_0006 - Rate limit error handling
|
|
// Trace: SRD_QualAssurReq_0006 - Return error when rate limit reached
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
w.Write([]byte(`{"error": "Rate limit exceeded. You can make up to 10 requests per hour. Please try again later."}`))
|
|
return
|
|
}
|
|
|
|
// Add current request to history
|
|
recentRequests = append(recentRequests, now)
|
|
rateLimiter.timestamps[ip] = recentRequests
|
|
|
|
// Allow request to proceed
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// getClientIP extracts the real client IP address from the request
|
|
// Trace: SRD_SecReq_0006 - Use device sending IP to impose rate limit
|
|
func getClientIP(r *http.Request) string {
|
|
// Check X-Forwarded-For header (for requests behind proxies/load balancers)
|
|
xff := r.Header.Get("X-Forwarded-For")
|
|
if xff != "" {
|
|
// X-Forwarded-For can contain multiple IPs, take the first one
|
|
ips := strings.Split(xff, ",")
|
|
if len(ips) > 0 {
|
|
return strings.TrimSpace(ips[0])
|
|
}
|
|
}
|
|
|
|
// Check X-Real-IP header (alternative proxy header)
|
|
xri := r.Header.Get("X-Real-IP")
|
|
if xri != "" {
|
|
return xri
|
|
}
|
|
|
|
// Fall back to RemoteAddr (direct connection)
|
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return ip
|
|
}
|