refactor: use 'c' alias for constants package
- Update all imports from 'constants' to 'c' for brevity - Replace all 'constants.' references with 'c.' - Fix remaining hardcoded content-type headers in httputil - Fix remaining hardcoded User-Agent and Accept headers - Rename CSRF receiver from 'c' to 'csrf' to avoid conflict - Add ContentTypePlainSimple constant for Accept header matching - Fix JSONCached to use proper integer formatting
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/constants"
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
func BrowserOnly(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check 1: User-Agent validation
|
||||
userAgent := r.Header.Get(constants.HeaderUserAgent)
|
||||
userAgent := r.Header.Get(c.HeaderUserAgent)
|
||||
if userAgent == "" || isBotUserAgent(userAgent) {
|
||||
log.Printf("SECURITY: Blocked non-browser User-Agent from IP %s: %s", getRequestIP(r), userAgent)
|
||||
http.Error(w, "Forbidden: Browser access only", http.StatusForbidden)
|
||||
@@ -31,8 +31,8 @@ func BrowserOnly(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// Check 2: Require Referer or Origin header
|
||||
referer := r.Header.Get(constants.HeaderReferer)
|
||||
origin := r.Header.Get(constants.HeaderOrigin)
|
||||
referer := r.Header.Get(c.HeaderReferer)
|
||||
origin := r.Header.Get(c.HeaderOrigin)
|
||||
|
||||
if referer == "" && origin == "" {
|
||||
log.Printf("SECURITY: Blocked request without Referer/Origin from IP %s", getRequestIP(r))
|
||||
@@ -43,7 +43,7 @@ func BrowserOnly(next http.Handler) http.Handler {
|
||||
// Check 3: Custom header validation (set by JavaScript)
|
||||
// For HTMX requests, check HX-Request header
|
||||
// For fetch/XMLHttpRequest, check X-Requested-With header
|
||||
hasHTMXHeader := r.Header.Get(constants.HeaderHXRequest) == "true"
|
||||
hasHTMXHeader := r.Header.Get(c.HeaderHXRequest) == "true"
|
||||
hasXMLHTTPHeader := r.Header.Get(browserHeaderName) == browserHeaderValue
|
||||
hasCustomBrowserHeader := r.Header.Get("X-Browser-Request") == "true"
|
||||
|
||||
@@ -98,7 +98,7 @@ func isBotUserAgent(ua string) bool {
|
||||
// getRequestIP extracts the client IP from the request
|
||||
func getRequestIP(r *http.Request) string {
|
||||
// Try X-Forwarded-For first (for proxies/load balancers)
|
||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
||||
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||
if ip != "" {
|
||||
// Take first IP if multiple
|
||||
ips := strings.Split(ip, ",")
|
||||
@@ -106,7 +106,7 @@ func getRequestIP(r *http.Request) string {
|
||||
}
|
||||
|
||||
// Try X-Real-IP
|
||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
||||
ip = r.Header.Get(c.HeaderXRealIP)
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/constants"
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
// contactRateLimitEntry tracks rate limiting for contact form per IP
|
||||
@@ -39,9 +39,9 @@ func NewContactRateLimiter() *ContactRateLimiter {
|
||||
func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get client IP (handle X-Forwarded-For for proxies)
|
||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
||||
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||
if ip == "" {
|
||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
||||
ip = r.Header.Get(c.HeaderXRealIP)
|
||||
}
|
||||
if ip == "" {
|
||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||
@@ -54,18 +54,18 @@ func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler {
|
||||
|
||||
if !rl.allow(ip) {
|
||||
// Check if HTMX request
|
||||
isHTMX := r.Header.Get(constants.HeaderHXRequest) != ""
|
||||
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
||||
|
||||
if isHTMX {
|
||||
// Return HTMX-friendly error
|
||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
||||
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
||||
<h3>Too Many Requests</h3>
|
||||
<p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
|
||||
</div>`))
|
||||
} else {
|
||||
w.Header().Set(constants.HeaderRetryAfter, "3600") // 1 hour
|
||||
w.Header().Set(c.HeaderRetryAfter, "3600") // 1 hour
|
||||
http.Error(w, "Too many contact form submissions. Please try again in an hour.", http.StatusTooManyRequests)
|
||||
}
|
||||
return
|
||||
|
||||
+24
-22
@@ -8,6 +8,8 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,18 +46,18 @@ func NewCSRFProtection() *CSRFProtection {
|
||||
// Middleware provides CSRF protection for state-changing operations
|
||||
// GET requests: Generate and set CSRF token
|
||||
// POST/PUT/DELETE: Validate CSRF token
|
||||
func (c *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
||||
func (csrf *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only validate on state-changing methods
|
||||
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete {
|
||||
if !c.validateToken(r) {
|
||||
if !csrf.validateToken(r) {
|
||||
log.Printf("SECURITY: CSRF validation failed from IP %s", getClientIP(r))
|
||||
|
||||
// Check if HTMX request
|
||||
isHTMX := r.Header.Get("HX-Request") != ""
|
||||
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
||||
|
||||
if isHTMX {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
||||
<h3>Security Error</h3>
|
||||
@@ -73,7 +75,7 @@ func (c *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// generateToken creates a new CSRF token
|
||||
func (c *CSRFProtection) generateToken() (string, error) {
|
||||
func (csrf *CSRFProtection) generateToken() (string, error) {
|
||||
bytes := make([]byte, csrfTokenLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
@@ -82,26 +84,26 @@ func (c *CSRFProtection) generateToken() (string, error) {
|
||||
token := base64.URLEncoding.EncodeToString(bytes)
|
||||
|
||||
// Store token with expiration
|
||||
c.mu.Lock()
|
||||
c.tokens[token] = &csrfTokenEntry{
|
||||
csrf.mu.Lock()
|
||||
csrf.tokens[token] = &csrfTokenEntry{
|
||||
token: token,
|
||||
expiresAt: time.Now().Add(csrfTokenTTL),
|
||||
}
|
||||
c.mu.Unlock()
|
||||
csrf.mu.Unlock()
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// GetToken retrieves or generates a CSRF token for the request
|
||||
// This should be called when rendering forms
|
||||
func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
func (csrf *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
// Check if token exists in cookie
|
||||
cookie, err := r.Cookie(csrfCookieName)
|
||||
if err == nil && cookie.Value != "" {
|
||||
// Validate existing token
|
||||
c.mu.RLock()
|
||||
entry, exists := c.tokens[cookie.Value]
|
||||
c.mu.RUnlock()
|
||||
csrf.mu.RLock()
|
||||
entry, exists := csrf.tokens[cookie.Value]
|
||||
csrf.mu.RUnlock()
|
||||
|
||||
if exists && time.Now().Before(entry.expiresAt) {
|
||||
// Token is valid, return it
|
||||
@@ -110,7 +112,7 @@ func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (strin
|
||||
}
|
||||
|
||||
// Generate new token
|
||||
token, err := c.generateToken()
|
||||
token, err := csrf.generateToken()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate CSRF token: %w", err)
|
||||
}
|
||||
@@ -130,7 +132,7 @@ func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (strin
|
||||
}
|
||||
|
||||
// validateToken validates the CSRF token from the request
|
||||
func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
||||
func (csrf *CSRFProtection) validateToken(r *http.Request) bool {
|
||||
// Get token from form
|
||||
var formToken string
|
||||
|
||||
@@ -163,9 +165,9 @@ func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// Validate token exists and is not expired
|
||||
c.mu.RLock()
|
||||
entry, exists := c.tokens[formToken]
|
||||
c.mu.RUnlock()
|
||||
csrf.mu.RLock()
|
||||
entry, exists := csrf.tokens[formToken]
|
||||
csrf.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
log.Printf("CSRF: Token not found in store")
|
||||
@@ -181,19 +183,19 @@ func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// cleanup removes expired tokens periodically
|
||||
func (c *CSRFProtection) cleanup() {
|
||||
func (csrf *CSRFProtection) cleanup() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
c.mu.Lock()
|
||||
csrf.mu.Lock()
|
||||
now := time.Now()
|
||||
for token, entry := range c.tokens {
|
||||
for token, entry := range csrf.tokens {
|
||||
if now.After(entry.expiresAt) {
|
||||
delete(c.tokens, token)
|
||||
delete(csrf.tokens, token)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
csrf.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/constants"
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
// contextKey is a private type for context keys to avoid collisions
|
||||
@@ -30,22 +30,22 @@ type Preferences struct {
|
||||
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prefs := &Preferences{
|
||||
CVLength: getPreferenceCookie(r, constants.CookieCVLength, constants.CVLengthShort),
|
||||
CVIcons: getPreferenceCookie(r, constants.CookieCVIcons, constants.CVIconsShow),
|
||||
CVLanguage: getPreferenceCookie(r, constants.CookieCVLanguage, constants.LangEnglish),
|
||||
CVTheme: getPreferenceCookie(r, constants.CookieCVTheme, constants.CVThemeDefault),
|
||||
ColorTheme: getPreferenceCookie(r, constants.CookieColorTheme, constants.ColorThemeLight),
|
||||
CVLength: getPreferenceCookie(r, c.CookieCVLength, c.CVLengthShort),
|
||||
CVIcons: getPreferenceCookie(r, c.CookieCVIcons, c.CVIconsShow),
|
||||
CVLanguage: getPreferenceCookie(r, c.CookieCVLanguage, c.LangEnglish),
|
||||
CVTheme: getPreferenceCookie(r, c.CookieCVTheme, c.CVThemeDefault),
|
||||
ColorTheme: getPreferenceCookie(r, c.CookieColorTheme, c.ColorThemeLight),
|
||||
}
|
||||
|
||||
// Migrate old preference values (one-time auto-migration)
|
||||
if prefs.CVLength == "extended" {
|
||||
prefs.CVLength = constants.CVLengthLong
|
||||
prefs.CVLength = c.CVLengthLong
|
||||
}
|
||||
switch prefs.CVIcons {
|
||||
case "true":
|
||||
prefs.CVIcons = constants.CVIconsShow
|
||||
prefs.CVIcons = c.CVIconsShow
|
||||
case "false":
|
||||
prefs.CVIcons = constants.CVIconsHide
|
||||
prefs.CVIcons = c.CVIconsHide
|
||||
}
|
||||
|
||||
// Store preferences in context
|
||||
@@ -60,11 +60,11 @@ func GetPreferences(r *http.Request) *Preferences {
|
||||
if !ok {
|
||||
// Return default preferences if not found
|
||||
return &Preferences{
|
||||
CVLength: constants.CVLengthShort,
|
||||
CVIcons: constants.CVIconsShow,
|
||||
CVLanguage: constants.LangEnglish,
|
||||
CVTheme: constants.CVThemeDefault,
|
||||
ColorTheme: constants.ColorThemeLight,
|
||||
CVLength: c.CVLengthShort,
|
||||
CVIcons: c.CVIconsShow,
|
||||
CVLanguage: c.LangEnglish,
|
||||
CVTheme: c.CVThemeDefault,
|
||||
ColorTheme: c.ColorThemeLight,
|
||||
}
|
||||
}
|
||||
return prefs
|
||||
@@ -102,42 +102,42 @@ func GetColorTheme(r *http.Request) string {
|
||||
|
||||
// IsLongCV returns true if the user prefers long CV format
|
||||
func IsLongCV(r *http.Request) bool {
|
||||
return GetCVLength(r) == constants.CVLengthLong
|
||||
return GetCVLength(r) == c.CVLengthLong
|
||||
}
|
||||
|
||||
// IsShortCV returns true if the user prefers short CV format
|
||||
func IsShortCV(r *http.Request) bool {
|
||||
return GetCVLength(r) == constants.CVLengthShort
|
||||
return GetCVLength(r) == c.CVLengthShort
|
||||
}
|
||||
|
||||
// ShowIcons returns true if icons should be visible
|
||||
func ShowIcons(r *http.Request) bool {
|
||||
return GetCVIcons(r) == constants.CVIconsShow
|
||||
return GetCVIcons(r) == c.CVIconsShow
|
||||
}
|
||||
|
||||
// HideIcons returns true if icons should be hidden
|
||||
func HideIcons(r *http.Request) bool {
|
||||
return GetCVIcons(r) == constants.CVIconsHide
|
||||
return GetCVIcons(r) == c.CVIconsHide
|
||||
}
|
||||
|
||||
// IsCleanTheme returns true if clean theme is selected
|
||||
func IsCleanTheme(r *http.Request) bool {
|
||||
return GetCVTheme(r) == constants.CVThemeClean
|
||||
return GetCVTheme(r) == c.CVThemeClean
|
||||
}
|
||||
|
||||
// IsDefaultTheme returns true if default theme is selected
|
||||
func IsDefaultTheme(r *http.Request) bool {
|
||||
return GetCVTheme(r) == constants.CVThemeDefault
|
||||
return GetCVTheme(r) == c.CVThemeDefault
|
||||
}
|
||||
|
||||
// IsDarkMode returns true if dark mode is enabled
|
||||
func IsDarkMode(r *http.Request) bool {
|
||||
return GetColorTheme(r) == constants.ColorThemeDark
|
||||
return GetColorTheme(r) == c.ColorThemeDark
|
||||
}
|
||||
|
||||
// IsLightMode returns true if light mode is enabled
|
||||
func IsLightMode(r *http.Request) bool {
|
||||
return GetColorTheme(r) == constants.ColorThemeLight
|
||||
return GetColorTheme(r) == c.ColorThemeLight
|
||||
}
|
||||
|
||||
// SetPreferenceCookie sets a preference cookie (1 year expiry)
|
||||
@@ -145,8 +145,8 @@ func SetPreferenceCookie(w http.ResponseWriter, name string, value string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: constants.CookiePath,
|
||||
MaxAge: constants.CookieMaxAge,
|
||||
Path: c.CookiePath,
|
||||
MaxAge: c.CookieMaxAge,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: isProductionMode(), // Secure in production with HTTPS
|
||||
@@ -155,8 +155,8 @@ func SetPreferenceCookie(w http.ResponseWriter, name string, value string) {
|
||||
|
||||
// isProductionMode checks if the application is running in production
|
||||
func isProductionMode() bool {
|
||||
env := os.Getenv(constants.EnvVarGOEnv)
|
||||
return env == constants.EnvProduction || env == "prod"
|
||||
env := os.Getenv(c.EnvVarGOEnv)
|
||||
return env == c.EnvProduction || env == "prod"
|
||||
}
|
||||
|
||||
// getPreferenceCookie gets a preference cookie value, returns default if not found
|
||||
|
||||
@@ -7,26 +7,26 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/constants"
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
// SecurityHeaders adds production-grade security headers to responses
|
||||
func SecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent clickjacking
|
||||
w.Header().Set(constants.HeaderXFrameOptions, constants.FrameOptionsSameOrigin)
|
||||
w.Header().Set(c.HeaderXFrameOptions, c.FrameOptionsSameOrigin)
|
||||
|
||||
// Prevent MIME type sniffing
|
||||
w.Header().Set(constants.HeaderXContentTypeOpts, constants.NoSniff)
|
||||
w.Header().Set(c.HeaderXContentTypeOpts, c.NoSniff)
|
||||
|
||||
// XSS Protection (legacy but still useful for older browsers)
|
||||
w.Header().Set(constants.HeaderXXSSProtection, constants.XSSProtection)
|
||||
w.Header().Set(c.HeaderXXSSProtection, c.XSSProtection)
|
||||
|
||||
// Referrer policy - strict privacy
|
||||
w.Header().Set(constants.HeaderReferrerPolicy, constants.ReferrerPolicy)
|
||||
w.Header().Set(c.HeaderReferrerPolicy, c.ReferrerPolicy)
|
||||
|
||||
// Permissions Policy - disable unnecessary features
|
||||
w.Header().Set(constants.HeaderPermissionsPolicy,
|
||||
w.Header().Set(c.HeaderPermissionsPolicy,
|
||||
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), "+
|
||||
"magnetometer=(), gyroscope=(), accelerometer=()")
|
||||
|
||||
@@ -40,12 +40,12 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
"frame-ancestors 'self'; " +
|
||||
"base-uri 'self'; " +
|
||||
"form-action 'self'"
|
||||
w.Header().Set(constants.HeaderCSP, csp)
|
||||
w.Header().Set(c.HeaderCSP, csp)
|
||||
|
||||
// HSTS - only in production with HTTPS
|
||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
||||
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||
// 1 year max-age, include subdomains
|
||||
w.Header().Set(constants.HeaderHSTS, constants.HSTSMaxAge)
|
||||
w.Header().Set(c.HeaderHSTS, c.HSTSMaxAge)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
@@ -76,7 +76,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// Check Origin header (for CORS requests)
|
||||
origin := r.Header.Get(constants.HeaderOrigin)
|
||||
origin := r.Header.Get(c.HeaderOrigin)
|
||||
if origin != "" {
|
||||
if !isAllowedOrigin(origin, allowedOrigins) {
|
||||
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
||||
@@ -85,7 +85,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// Check Referer header (for direct requests)
|
||||
referer := r.Header.Get(constants.HeaderReferer)
|
||||
referer := r.Header.Get(c.HeaderReferer)
|
||||
if referer != "" {
|
||||
if !isAllowedOrigin(referer, allowedOrigins) {
|
||||
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
||||
@@ -98,7 +98,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
||||
if origin == "" && referer == "" {
|
||||
// For production, you might want to be stricter here
|
||||
// For now, allow it (users can bookmark /export/pdf directly)
|
||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction && r.URL.Path == constants.RouteExportPDF {
|
||||
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction && r.URL.Path == c.RouteExportPDF {
|
||||
// In production, require at least a referer for PDF endpoint
|
||||
http.Error(w, "Forbidden: Direct access not allowed", http.StatusForbidden)
|
||||
return
|
||||
@@ -163,16 +163,16 @@ func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
||||
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get client IP (handle X-Forwarded-For for proxies)
|
||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
||||
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||
if ip == "" {
|
||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
||||
ip = r.Header.Get(c.HeaderXRealIP)
|
||||
}
|
||||
if ip == "" {
|
||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
|
||||
if !rl.allow(ip) {
|
||||
w.Header().Set(constants.HeaderRetryAfter, "60")
|
||||
w.Header().Set(c.HeaderRetryAfter, "60")
|
||||
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
@@ -227,12 +227,12 @@ func (rl *RateLimiter) cleanup() {
|
||||
// 1 hour in development, 1 day in production
|
||||
func CacheControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cacheValue := constants.CachePublic1Hour
|
||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
||||
cacheValue = constants.CachePublic1Day
|
||||
cacheValue := c.CachePublic1Hour
|
||||
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||
cacheValue = c.CachePublic1Day
|
||||
}
|
||||
|
||||
w.Header().Set(constants.HeaderCacheControl, cacheValue)
|
||||
w.Header().Set(c.HeaderCacheControl, cacheValue)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -243,12 +243,12 @@ func DynamicCacheControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// For dynamic HTML pages: short cache, must revalidate
|
||||
// This improves performance while ensuring fresh content
|
||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
||||
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||
// Production: 5 minutes cache, but must revalidate
|
||||
w.Header().Set(constants.HeaderCacheControl, constants.CachePublic5Min)
|
||||
w.Header().Set(c.HeaderCacheControl, c.CachePublic5Min)
|
||||
} else {
|
||||
// Development: no cache for easier testing
|
||||
w.Header().Set(constants.HeaderCacheControl, constants.CacheNoStore)
|
||||
w.Header().Set(c.HeaderCacheControl, c.CacheNoStore)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/constants"
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
// SecurityEvent represents a security-related event
|
||||
@@ -57,7 +57,7 @@ func LogSecurityEvent(eventType string, r *http.Request, details string) {
|
||||
EventType: eventType,
|
||||
Severity: severity,
|
||||
IP: getClientIP(r),
|
||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
||||
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
Details: details,
|
||||
@@ -74,7 +74,7 @@ func LogSecurityEvent(eventType string, r *http.Request, details string) {
|
||||
log.Printf("[SECURITY] %s", eventJSON)
|
||||
|
||||
// Also log to separate security log file in production
|
||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
||||
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||
logToSecurityFile(eventJSON)
|
||||
}
|
||||
}
|
||||
@@ -99,14 +99,14 @@ func getSeverity(eventType string) string {
|
||||
// getClientIP extracts the real client IP from request headers
|
||||
func getClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header (proxy/load balancer)
|
||||
if xff := r.Header.Get(constants.HeaderXForwardedFor); xff != "" {
|
||||
if xff := r.Header.Get(c.HeaderXForwardedFor); xff != "" {
|
||||
// Take first IP from comma-separated list
|
||||
ips := strings.Split(xff, ",")
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get(constants.HeaderXRealIP); xri != "" {
|
||||
if xri := r.Header.Get(c.HeaderXRealIP); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func SecurityLogger(next http.Handler) http.Handler {
|
||||
EventType: "REQUEST",
|
||||
Severity: SeverityInfo,
|
||||
IP: getClientIP(r),
|
||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
||||
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
Details: string(detailsJSON),
|
||||
@@ -203,7 +203,7 @@ func SecurityLogger(next http.Handler) http.Handler {
|
||||
EventType: "HTTP_ERROR",
|
||||
Severity: severity,
|
||||
IP: getClientIP(r),
|
||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
||||
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
Details: http.StatusText(wrapped.status),
|
||||
|
||||
Reference in New Issue
Block a user