feat: Add BlockDirectIP configuration and middleware for IP access control
- Introduced BlockDirectIP field in ReverseProxyConfig to control direct IP access. - Implemented middleware to block requests made directly to the server's IP, allowing exceptions for ACME challenges. - Enhanced configuration loading to set BlockDirectIP based on environment variables and defaults. - Updated logging to provide insights on BlockDirectIP settings and access attempts.
- commit
- 8791634418c0ffe566f343a21c37fa8ed7c296d8
- author
- bamen · · 2025-03-30 16:11 UTC
- parent
- 4af5e86
- signature
- unsigned
3 files · +157 -123
M internal/common/init_config.go +29 -0
| @@ -85,6 +85,7 @@ type ReverseProxyConfig struct { | |||
| 85 | 85 | EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging | |
| 86 | 86 | EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware | |
| 87 | 87 | DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies | |
| 88 | + | BlockDirectIP bool `yaml:"blockDirectIP"` // Whether to block requests made directly to the server's IP (allows ACME) | |
| 88 | 89 | DefaultChallengeType string `yaml:"defaultChallengeType"` // http-01 or dns-01 | |
| 89 | 90 | DefaultHttpChallengePort string `yaml:"defaultHttpChallengePort"` | |
| 90 | 91 | DefaultDnsChallengePropagationTimeout int `yaml:"defaultDnsChallengePropagationTimeout"` | |
| @@ -115,6 +116,7 @@ var ( | |||
| 115 | 116 | defaultDnsChallengePollingInterval = 5 // seconds | |
| 116 | 117 | defaultAdminPath = "/admin" | |
| 117 | 118 | defaultNetwork = "gordon" | |
| 119 | + | blockDirectIP = true // Default to enabled | |
| 118 | 120 | ) | |
| 119 | 121 | ||
| 120 | 122 | // applyDefaultsToConfig applies default values to any fields that have zero values | |
| @@ -262,6 +264,33 @@ func applyDefaultsToConfig(config *Config) bool { | |||
| 262 | 264 | defaultsApplied = true | |
| 263 | 265 | } | |
| 264 | 266 | ||
| 267 | + | // Set BlockDirectIP to true by default if not explicitly set to false | |
| 268 | + | if !config.ReverseProxy.BlockDirectIP { | |
| 269 | + | // Check if the field is explicitly set to false in the config file | |
| 270 | + | configFilePath := filepath.Join(getConfigDirMustExist(), "config.yml") | |
| 271 | + | isExplicitlyFalse := false | |
| 272 | + | if fileExists(configFilePath) { | |
| 273 | + | yamlContent, err := os.ReadFile(configFilePath) | |
| 274 | + | if err == nil { | |
| 275 | + | // Simple check for 'blockDirectIP: false' | |
| 276 | + | // Note: This doesn't handle comments or complex YAML structures perfectly | |
| 277 | + | if strings.Contains(string(yamlContent), "blockDirectIP: false") { | |
| 278 | + | isExplicitlyFalse = true | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // Apply default 'true' only if it wasn't explicitly set to 'false' | |
| 284 | + | if !isExplicitlyFalse { | |
| 285 | + | config.ReverseProxy.BlockDirectIP = blockDirectIP | |
| 286 | + | logger.Debug("Applied default value for ReverseProxy.BlockDirectIP", "value", blockDirectIP) | |
| 287 | + | defaultsApplied = true | |
| 288 | + | } else { | |
| 289 | + | // It was explicitly set to false in the file, keep it as false (env var can still override) | |
| 290 | + | logger.Debug("Keeping explicitly set 'false' for ReverseProxy.BlockDirectIP", "value", config.ReverseProxy.BlockDirectIP) | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | ||
| 265 | 294 | // Apply defaults for Let's Encrypt challenge settings | |
| 266 | 295 | if config.ReverseProxy.DefaultChallengeType == "" { | |
| 267 | 296 | config.ReverseProxy.DefaultChallengeType = defaultChallengeType | |
| @@ -85,6 +85,7 @@ type ReverseProxyConfig struct { | |||||
| 85 | EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging | 85 | EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging | ||
| 86 | EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware | 86 | EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware | ||
| 87 | DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies | 87 | DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies | ||
| 88 | + | BlockDirectIP bool `yaml:"blockDirectIP"` // Whether to block requests made directly to the server's IP (allows ACME) | |||
| 88 | DefaultChallengeType string `yaml:"defaultChallengeType"` // http-01 or dns-01 | 89 | DefaultChallengeType string `yaml:"defaultChallengeType"` // http-01 or dns-01 | ||
| 89 | DefaultHttpChallengePort string `yaml:"defaultHttpChallengePort"` | 90 | DefaultHttpChallengePort string `yaml:"defaultHttpChallengePort"` | ||
| 90 | DefaultDnsChallengePropagationTimeout int `yaml:"defaultDnsChallengePropagationTimeout"` | 91 | DefaultDnsChallengePropagationTimeout int `yaml:"defaultDnsChallengePropagationTimeout"` | ||
| @@ -115,6 +116,7 @@ var ( | |||||
| 115 | defaultDnsChallengePollingInterval = 5 // seconds | 116 | defaultDnsChallengePollingInterval = 5 // seconds | ||
| 116 | defaultAdminPath = "/admin" | 117 | defaultAdminPath = "/admin" | ||
| 117 | defaultNetwork = "gordon" | 118 | defaultNetwork = "gordon" | ||
| 119 | + | blockDirectIP = true // Default to enabled | |||
| 118 | ) | 120 | ) | ||
| 119 | 121 | ||||
| 120 | // applyDefaultsToConfig applies default values to any fields that have zero values | 122 | // applyDefaultsToConfig applies default values to any fields that have zero values | ||
| @@ -262,6 +264,33 @@ func applyDefaultsToConfig(config *Config) bool { | |||||
| 262 | defaultsApplied = true | 264 | defaultsApplied = true | ||
| 263 | } | 265 | } | ||
| 264 | 266 | ||||
| 267 | + | // Set BlockDirectIP to true by default if not explicitly set to false | |||
| 268 | + | if !config.ReverseProxy.BlockDirectIP { | |||
| 269 | + | // Check if the field is explicitly set to false in the config file | |||
| 270 | + | configFilePath := filepath.Join(getConfigDirMustExist(), "config.yml") | |||
| 271 | + | isExplicitlyFalse := false | |||
| 272 | + | if fileExists(configFilePath) { | |||
| 273 | + | yamlContent, err := os.ReadFile(configFilePath) | |||
| 274 | + | if err == nil { | |||
| 275 | + | // Simple check for 'blockDirectIP: false' | |||
| 276 | + | // Note: This doesn't handle comments or complex YAML structures perfectly | |||
| 277 | + | if strings.Contains(string(yamlContent), "blockDirectIP: false") { | |||
| 278 | + | isExplicitlyFalse = true | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // Apply default 'true' only if it wasn't explicitly set to 'false' | |||
| 284 | + | if !isExplicitlyFalse { | |||
| 285 | + | config.ReverseProxy.BlockDirectIP = blockDirectIP | |||
| 286 | + | logger.Debug("Applied default value for ReverseProxy.BlockDirectIP", "value", blockDirectIP) | |||
| 287 | + | defaultsApplied = true | |||
| 288 | + | } else { | |||
| 289 | + | // It was explicitly set to false in the file, keep it as false (env var can still override) | |||
| 290 | + | logger.Debug("Keeping explicitly set 'false' for ReverseProxy.BlockDirectIP", "value", config.ReverseProxy.BlockDirectIP) | |||
| 291 | + | } | |||
| 292 | + | } | |||
| 293 | + | ||||
| 265 | // Apply defaults for Let's Encrypt challenge settings | 294 | // Apply defaults for Let's Encrypt challenge settings | ||
| 266 | if config.ReverseProxy.DefaultChallengeType == "" { | 295 | if config.ReverseProxy.DefaultChallengeType == "" { | ||
| 267 | config.ReverseProxy.DefaultChallengeType = defaultChallengeType | 296 | config.ReverseProxy.DefaultChallengeType = defaultChallengeType | ||
M internal/common/init_env.go +11 -0
| @@ -269,4 +269,15 @@ func loadConfigFromEnv(config *Config, printLogs bool) { | |||
| 269 | 269 | logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates) | |
| 270 | 270 | } | |
| 271 | 271 | } | |
| 272 | + | ||
| 273 | + | // Handle GORDON_PROXY_BLOCK_DIRECT_IP environment variable | |
| 274 | + | if val := os.Getenv("GORDON_PROXY_BLOCK_DIRECT_IP"); val != "" { | |
| 275 | + | blockDirectIP, err := strconv.ParseBool(val) | |
| 276 | + | if err == nil { | |
| 277 | + | config.ReverseProxy.BlockDirectIP = blockDirectIP | |
| 278 | + | } | |
| 279 | + | if printLogs { | |
| 280 | + | logger.Info("Using environment variable GORDON_PROXY_BLOCK_DIRECT_IP", "value", config.ReverseProxy.BlockDirectIP) | |
| 281 | + | } | |
| 282 | + | } | |
| 272 | 283 | } | |
| @@ -269,4 +269,15 @@ func loadConfigFromEnv(config *Config, printLogs bool) { | |||||
| 269 | logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates) | 269 | logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates) | ||
| 270 | } | 270 | } | ||
| 271 | } | 271 | } | ||
| 272 | + | ||||
| 273 | + | // Handle GORDON_PROXY_BLOCK_DIRECT_IP environment variable | |||
| 274 | + | if val := os.Getenv("GORDON_PROXY_BLOCK_DIRECT_IP"); val != "" { | |||
| 275 | + | blockDirectIP, err := strconv.ParseBool(val) | |||
| 276 | + | if err == nil { | |||
| 277 | + | config.ReverseProxy.BlockDirectIP = blockDirectIP | |||
| 278 | + | } | |||
| 279 | + | if printLogs { | |||
| 280 | + | logger.Info("Using environment variable GORDON_PROXY_BLOCK_DIRECT_IP", "value", config.ReverseProxy.BlockDirectIP) | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 272 | } | 283 | } | ||
M internal/proxy/middleware.go +117 -123
| @@ -2,6 +2,7 @@ package proxy | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "net" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "time" | |
| @@ -21,185 +22,178 @@ var rateLimiterCleanup []func() | |||
| 21 | 22 | // RequestIDKey is the key used to store the request ID in the context | |
| 22 | 23 | const RequestIDKey = "request_id" | |
| 23 | 24 | ||
| 25 | + | // isHostIPAddress checks if the host part of a string (like "1.2.3.4:80" or "1.2.3.4") is a valid IP address. | |
| 26 | + | func isHostIPAddress(host string) bool { | |
| 27 | + | // Split host and port | |
| 28 | + | h, _, err := net.SplitHostPort(host) | |
| 29 | + | if err != nil { | |
| 30 | + | // If splitting fails, assume it might be just an IP without a port | |
| 31 | + | h = host | |
| 32 | + | } | |
| 33 | + | // Try parsing the host part as an IP address | |
| 34 | + | ip := net.ParseIP(h) | |
| 35 | + | return ip != nil | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // blockDirectIPMiddleware rejects requests where the Host header is an IP address, | |
| 39 | + | // unless it's an ACME challenge request. | |
| 40 | + | func blockDirectIPMiddleware() echo.MiddlewareFunc { | |
| 41 | + | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 42 | + | return func(c echo.Context) error { | |
| 43 | + | host := c.Request().Host | |
| 44 | + | path := c.Request().URL.Path | |
| 45 | + | ||
| 46 | + | if isHostIPAddress(host) { | |
| 47 | + | // Allow ACME HTTP-01 challenges to proceed even via IP | |
| 48 | + | if strings.HasPrefix(path, "/.well-known/acme-challenge/") { | |
| 49 | + | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", c.RealIP()) | |
| 50 | + | return next(c) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Block other direct IP access attempts | |
| 54 | + | logger.Warn("Blocking direct IP access attempt", "host", host, "path", path, "ip", c.RealIP(), "user_agent", c.Request().UserAgent()) | |
| 55 | + | return echo.NewHTTPError(http.StatusForbidden, "Direct IP access is not permitted.") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // If host is not an IP, proceed normally | |
| 59 | + | return next(c) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 24 | 64 | // setupMiddleware configures middleware for both HTTP and HTTPS servers | |
| 25 | 65 | func (p *Proxy) setupMiddleware() { | |
| 26 | 66 | // Configure Echo IP extraction. Since the proxy is edge-facing (directly exposed), | |
| 27 | 67 | // ignore X-Forwarded-For and use the direct remote address. | |
| 28 | 68 | p.httpsServer.IPExtractor = echo.ExtractIPDirect() | |
| 29 | 69 | ||
| 30 | - | // Add UUID generator middleware for both HTTP and HTTPS servers | |
| 70 | + | // Add UUID generator middleware | |
| 31 | 71 | p.httpsServer.Use(p.createRequestIDMiddleware()) | |
| 32 | 72 | ||
| 73 | + | // --- Block Direct IP Access Middleware (Conditional) --- | |
| 74 | + | if p.config.BlockDirectIP { | |
| 75 | + | // This should run early, before logging or rate limiting potentially. | |
| 76 | + | p.httpsServer.Use(blockDirectIPMiddleware()) | |
| 77 | + | logger.Info("Direct IP blocking middleware is ENABLED (allows ACME)") | |
| 78 | + | } else { | |
| 79 | + | logger.Info("Direct IP blocking middleware is DISABLED") | |
| 80 | + | } | |
| 81 | + | ||
| 33 | 82 | // Add middleware to log incoming headers (for debugging Cloudflare detection) | |
| 34 | 83 | p.httpsServer.Use(logHeadersMiddleware()) | |
| 35 | 84 | ||
| 36 | - | // Check if rate limiting is disabled via configuration | |
| 85 | + | // --- Rate Limiting Setup (Domain Only Now) --- | |
| 37 | 86 | if p.config.EnableRateLimit { | |
| 38 | - | // Create a new Starskey rate limiter store for HTTPS server | |
| 39 | - | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 40 | - | "./data/ratelimiter/https", // Path to store rate limit data | |
| 41 | - | 10, // Rate: 10 requests per second | |
| 42 | - | 30, // Burst: 30 requests | |
| 43 | - | 3*time.Minute, // Expires in 3 minutes | |
| 87 | + | logger.Debug("Rate limiting enabled. Setting up limiter for domain requests only.") | |
| 88 | + | ||
| 89 | + | // --- REMOVED Rate Limiter for Direct IP Access --- | |
| 90 | + | ||
| 91 | + | // --- Rate Limiter for Domain Access (Existing Logic) --- | |
| 92 | + | // Use Starskey or fallback for requests coming through a domain name. | |
| 93 | + | domainRateLimiterStore, err := kv.NewStarskeyRateLimiterStore( | |
| 94 | + | "./data/ratelimiter/domain", // Path for domain limiter data | |
| 95 | + | 10, // Rate: 10 requests per second (default) | |
| 96 | + | 30, // Burst: 30 requests (default) | |
| 97 | + | 3*time.Minute, // Expires in 3 minutes | |
| 44 | 98 | ) | |
| 45 | - | if err != nil { | |
| 46 | - | logger.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err) | |
| 47 | 99 | ||
| 48 | - | // Fallback to memory store if Starskey store creation fails | |
| 100 | + | if err != nil { | |
| 101 | + | logger.Error("Failed to create Domain rate limiter store (Starskey), falling back to memory store", "error", err) | |
| 102 | + | // Fallback to memory store | |
| 49 | 103 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 50 | 104 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 51 | 105 | middleware.RateLimiterMemoryStoreConfig{ | |
| 52 | - | Rate: rate.Limit(10), // 10 requests per second | |
| 53 | - | Burst: 30, // Burst of 30 requests | |
| 54 | - | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 106 | + | Rate: rate.Limit(10), | |
| 107 | + | Burst: 30, | |
| 108 | + | ExpiresIn: 3 * time.Minute, | |
| 55 | 109 | }, | |
| 56 | 110 | ), | |
| 57 | - | DenyHandler: func(context echo.Context, identifier string, err error) error { | |
| 58 | - | logger.Info("[RateLimit Debug] DenyHandler invoked for Memory Fallback", "identifier", identifier, "error", err.Error()) | |
| 59 | - | ||
| 60 | - | logger.Warn("Rate limit exceeded", | |
| 61 | - | "ip", identifier, | |
| 62 | - | "path", context.Request().URL.Path, | |
| 63 | - | "method", context.Request().Method) | |
| 64 | - | return context.String(http.StatusTooManyRequests, "Too many requests") | |
| 111 | + | // Apply this limiter ONLY if the host is NOT an IP address (redundant now but safe) | |
| 112 | + | Skipper: func(c echo.Context) bool { | |
| 113 | + | host := c.Request().Host | |
| 114 | + | isIP := isHostIPAddress(host) | |
| 115 | + | if !isIP { | |
| 116 | + | // Log only if applying the limiter | |
| 117 | + | logger.Debug("Applying domain rate limiter (Memory Fallback)", "host", host, "ip", c.RealIP()) | |
| 118 | + | } | |
| 119 | + | return isIP // Skip if it IS an IP | |
| 65 | 120 | }, | |
| 66 | - | })) | |
| 67 | - | } else { | |
| 68 | - | // Use the Starskey store for HTTPS rate limiting | |
| 69 | - | logger.Debug("Using Starskey rate limiter for HTTPS server") | |
| 70 | - | ||
| 71 | - | // Add custom Starskey rate limiter middleware | |
| 72 | - | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 73 | - | Store: httpsRateLimiter, | |
| 74 | 121 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | |
| 75 | - | id := ctx.RealIP() | |
| 76 | - | logger.Info("[RateLimit Debug] IdentifierExtractor called", "ip_identified", id, "remote_addr", ctx.Request().RemoteAddr) | |
| 77 | - | ||
| 78 | - | logger.Debug("Rate limiting HTTPS request", | |
| 79 | - | "ip", id, | |
| 80 | - | "remote_addr", ctx.Request().RemoteAddr) | |
| 81 | - | ||
| 82 | - | // Add a response header to show the detected IP (for debugging) | |
| 83 | - | ctx.Response().Header().Set("X-Rate-Limit-IP", id) | |
| 84 | - | ||
| 85 | - | return id, nil | |
| 86 | - | }, | |
| 87 | - | DenyHandler: func(context echo.Context, identifier string, err error) error { | |
| 88 | - | logger.Info("[RateLimit Debug] DenyHandler invoked for Starskey", "identifier", identifier, "error", err.Error()) | |
| 89 | - | ||
| 90 | - | logger.Warn("HTTPS rate limit exceeded", | |
| 91 | - | "ip", identifier, | |
| 92 | - | "path", context.Request().URL.Path, | |
| 93 | - | "method", context.Request().Method) | |
| 94 | - | return context.String(http.StatusTooManyRequests, "Too many requests") | |
| 122 | + | return ctx.RealIP(), nil | |
| 95 | 123 | }, | |
| 96 | - | })) | |
| 97 | - | ||
| 98 | - | // Add cleanup function to global slice | |
| 99 | - | rateLimiterCleanup = append(rateLimiterCleanup, func() { | |
| 100 | - | if err := httpsRateLimiter.Close(); err != nil { | |
| 101 | - | logger.Error("Failed to close HTTPS rate limiter store", "error", err) | |
| 102 | - | } | |
| 103 | - | }) | |
| 104 | - | } | |
| 105 | - | } else { | |
| 106 | - | logger.Info("Rate limiting is disabled for HTTPS server via configuration") | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | // Add custom logger middleware with request ID if enabled in config | |
| 110 | - | if p.config.EnableHttpLogs { | |
| 111 | - | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 112 | - | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", | |
| 113 | - | CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00", | |
| 114 | - | Skipper: func(c echo.Context) bool { | |
| 115 | - | return !p.config.EnableHttpLogs | |
| 116 | - | }, | |
| 117 | - | })) | |
| 118 | - | logger.Debug("HTTP request logging enabled for HTTPS server") | |
| 119 | - | } else { | |
| 120 | - | logger.Debug("HTTP request logging disabled for HTTPS server") | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | // Check if rate limiting is disabled via configuration | |
| 124 | - | if p.config.EnableRateLimit { | |
| 125 | - | // Create a new Starskey rate limiter store for HTTP server | |
| 126 | - | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 127 | - | "./data/ratelimiter/http", // Path to store rate limit data | |
| 128 | - | 2, // Rate: 2 requests per second (lowered from 10) | |
| 129 | - | 10, // Burst: 10 requests (lowered from 30) | |
| 130 | - | 3*time.Minute, // Expires in 3 minutes | |
| 131 | - | ) | |
| 132 | - | if err != nil { | |
| 133 | - | logger.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err) | |
| 134 | - | ||
| 135 | - | // Fallback to memory store if Starskey store creation fails | |
| 136 | - | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 137 | - | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 138 | - | middleware.RateLimiterMemoryStoreConfig{ | |
| 139 | - | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) | |
| 140 | - | Burst: 10, // Burst of 10 requests (lowered from 30) | |
| 141 | - | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 142 | - | }, | |
| 143 | - | ), | |
| 144 | 124 | DenyHandler: func(context echo.Context, identifier string, err error) error { | |
| 145 | - | logger.Warn("Rate limit exceeded", | |
| 125 | + | logger.Warn("Domain rate limit exceeded (Memory Fallback)", | |
| 146 | 126 | "ip", identifier, | |
| 127 | + | "host", context.Request().Host, | |
| 147 | 128 | "path", context.Request().URL.Path, | |
| 148 | - | "method", context.Request().Method) | |
| 129 | + | "method", context.Request().Method, | |
| 130 | + | "error", err.Error()) | |
| 149 | 131 | return context.String(http.StatusTooManyRequests, "Too many requests") | |
| 150 | 132 | }, | |
| 151 | 133 | })) | |
| 152 | 134 | } else { | |
| 153 | - | // Use the Starskey store for HTTP rate limiting | |
| 154 | - | logger.Debug("Using Starskey rate limiter for HTTP server") | |
| 155 | - | ||
| 156 | - | // Add custom Starskey rate limiter middleware | |
| 135 | + | logger.Debug("Using Starskey rate limiter for domain requests") | |
| 136 | + | // Use the Starskey store | |
| 157 | 137 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 158 | - | Store: httpRateLimiter, | |
| 138 | + | Store: domainRateLimiterStore, | |
| 139 | + | // Apply this limiter ONLY if the host is NOT an IP address (redundant now but safe) | |
| 140 | + | Skipper: func(c echo.Context) bool { | |
| 141 | + | host := c.Request().Host | |
| 142 | + | isIP := isHostIPAddress(host) | |
| 143 | + | if !isIP { | |
| 144 | + | // Log only if applying the limiter | |
| 145 | + | logger.Debug("Applying domain rate limiter (Starskey)", "host", host, "ip", c.RealIP()) | |
| 146 | + | } | |
| 147 | + | return isIP // Skip if it IS an IP | |
| 148 | + | }, | |
| 159 | 149 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | |
| 160 | 150 | id := ctx.RealIP() | |
| 161 | - | logger.Debug("Rate limiting HTTP request", | |
| 162 | - | "ip", id, | |
| 163 | - | "remote_addr", ctx.Request().RemoteAddr) | |
| 164 | - | ||
| 165 | - | // Add a response header to show the detected IP (for debugging) | |
| 166 | - | ctx.Response().Header().Set("X-Rate-Limit-IP", id) | |
| 167 | - | ||
| 151 | + | ctx.Response().Header().Set("X-Rate-Limit-Domain-IP", id) // Keep debug header | |
| 168 | 152 | return id, nil | |
| 169 | 153 | }, | |
| 170 | 154 | DenyHandler: func(context echo.Context, identifier string, err error) error { | |
| 171 | - | logger.Warn("HTTP rate limit exceeded", | |
| 155 | + | logger.Warn("Domain rate limit exceeded (Starskey)", | |
| 172 | 156 | "ip", identifier, | |
| 157 | + | "host", context.Request().Host, | |
| 173 | 158 | "path", context.Request().URL.Path, | |
| 174 | - | "method", context.Request().Method) | |
| 159 | + | "method", context.Request().Method, | |
| 160 | + | "error", err.Error()) | |
| 175 | 161 | return context.String(http.StatusTooManyRequests, "Too many requests") | |
| 176 | 162 | }, | |
| 177 | 163 | })) | |
| 178 | 164 | ||
| 179 | - | // Add cleanup function to global slice | |
| 165 | + | // Add cleanup function for the Starskey domain limiter | |
| 180 | 166 | rateLimiterCleanup = append(rateLimiterCleanup, func() { | |
| 181 | - | if err := httpRateLimiter.Close(); err != nil { | |
| 182 | - | logger.Error("Failed to close HTTP rate limiter store", "error", err) | |
| 167 | + | if err := domainRateLimiterStore.Close(); err != nil { | |
| 168 | + | logger.Error("Failed to close Domain rate limiter store (Starskey)", "error", err) | |
| 183 | 169 | } | |
| 184 | 170 | }) | |
| 185 | 171 | } | |
| 172 | + | logger.Debug("Configured rate limiter for domain requests") | |
| 173 | + | ||
| 186 | 174 | } else { | |
| 187 | - | logger.Info("Rate limiting is disabled for HTTP server via configuration") | |
| 175 | + | logger.Info("Rate limiting is disabled globally via configuration") | |
| 188 | 176 | } | |
| 189 | 177 | ||
| 190 | - | // Add custom logger middleware with request ID for HTTP server if enabled in config | |
| 178 | + | // --- Logging Middleware (Existing) --- | |
| 191 | 179 | if p.config.EnableHttpLogs { | |
| 192 | 180 | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 193 | - | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", | |
| 181 | + | // Use a standard string with escaped quotes for the JSON format | |
| 182 | + | Format: "{\"time\":\"${time_rfc3339_nano}\",\"id\":\"${id}\",\"remote_ip\":\"${remote_ip}\",\"host\":\"${host}\",\"method\":\"${method}\",\"uri\":\"${uri}\",\"user_agent\":\"${user_agent}\",\"status\":${status},\"error\":\"${error}\",\"latency\":${latency},\"latency_human\":\"${latency_human}\",\"bytes_in\":${bytes_in},\"bytes_out\":${bytes_out}}\n", | |
| 194 | 183 | CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00", | |
| 195 | 184 | Skipper: func(c echo.Context) bool { | |
| 196 | - | return !p.config.EnableHttpLogs | |
| 185 | + | // Skip logging if disabled in config OR if it's a successful ACME challenge | |
| 186 | + | // (to avoid polluting logs during certificate issuance) | |
| 187 | + | isAcme := strings.HasPrefix(c.Request().URL.Path, "/.well-known/acme-challenge/") | |
| 188 | + | return !p.config.EnableHttpLogs || (isAcme && c.Response().Status == http.StatusOK) | |
| 197 | 189 | }, | |
| 198 | 190 | })) | |
| 199 | - | logger.Debug("HTTP request logging enabled for HTTPS server") | |
| 191 | + | logger.Debug("HTTP request logging enabled for proxy server") | |
| 200 | 192 | } else { | |
| 201 | - | logger.Debug("HTTP request logging disabled for HTTPS server") | |
| 193 | + | logger.Debug("HTTP request logging disabled for proxy server") | |
| 202 | 194 | } | |
| 195 | + | ||
| 196 | + | // REMOVED the duplicated rate limiter and logger blocks that seemed intended for a separate HTTP server | |
| 203 | 197 | } | |
| 204 | 198 | ||
| 205 | 199 | // createRequestIDMiddleware returns a middleware function that generates a UUID for each request | |
| @@ -2,6 +2,7 @@ package proxy | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "net" | |||
| 5 | "net/http" | 6 | "net/http" | ||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "time" | 8 | "time" | ||
| @@ -21,185 +22,178 @@ var rateLimiterCleanup []func() | |||||
| 21 | // RequestIDKey is the key used to store the request ID in the context | 22 | // RequestIDKey is the key used to store the request ID in the context | ||
| 22 | const RequestIDKey = "request_id" | 23 | const RequestIDKey = "request_id" | ||
| 23 | 24 | ||||
| 25 | + | // isHostIPAddress checks if the host part of a string (like "1.2.3.4:80" or "1.2.3.4") is a valid IP address. | |||
| 26 | + | func isHostIPAddress(host string) bool { | |||
| 27 | + | // Split host and port | |||
| 28 | + | h, _, err := net.SplitHostPort(host) | |||
| 29 | + | if err != nil { | |||
| 30 | + | // If splitting fails, assume it might be just an IP without a port | |||
| 31 | + | h = host | |||
| 32 | + | } | |||
| 33 | + | // Try parsing the host part as an IP address | |||
| 34 | + | ip := net.ParseIP(h) | |||
| 35 | + | return ip != nil | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // blockDirectIPMiddleware rejects requests where the Host header is an IP address, | |||
| 39 | + | // unless it's an ACME challenge request. | |||
| 40 | + | func blockDirectIPMiddleware() echo.MiddlewareFunc { | |||
| 41 | + | return func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 42 | + | return func(c echo.Context) error { | |||
| 43 | + | host := c.Request().Host | |||
| 44 | + | path := c.Request().URL.Path | |||
| 45 | + | ||||
| 46 | + | if isHostIPAddress(host) { | |||
| 47 | + | // Allow ACME HTTP-01 challenges to proceed even via IP | |||
| 48 | + | if strings.HasPrefix(path, "/.well-known/acme-challenge/") { | |||
| 49 | + | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", c.RealIP()) | |||
| 50 | + | return next(c) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Block other direct IP access attempts | |||
| 54 | + | logger.Warn("Blocking direct IP access attempt", "host", host, "path", path, "ip", c.RealIP(), "user_agent", c.Request().UserAgent()) | |||
| 55 | + | return echo.NewHTTPError(http.StatusForbidden, "Direct IP access is not permitted.") | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // If host is not an IP, proceed normally | |||
| 59 | + | return next(c) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 24 | // setupMiddleware configures middleware for both HTTP and HTTPS servers | 64 | // setupMiddleware configures middleware for both HTTP and HTTPS servers | ||
| 25 | func (p *Proxy) setupMiddleware() { | 65 | func (p *Proxy) setupMiddleware() { | ||
| 26 | // Configure Echo IP extraction. Since the proxy is edge-facing (directly exposed), | 66 | // Configure Echo IP extraction. Since the proxy is edge-facing (directly exposed), | ||
| 27 | // ignore X-Forwarded-For and use the direct remote address. | 67 | // ignore X-Forwarded-For and use the direct remote address. | ||
| 28 | p.httpsServer.IPExtractor = echo.ExtractIPDirect() | 68 | p.httpsServer.IPExtractor = echo.ExtractIPDirect() | ||
| 29 | 69 | ||||
| 30 | - | // Add UUID generator middleware for both HTTP and HTTPS servers | 70 | + | // Add UUID generator middleware |
| 31 | p.httpsServer.Use(p.createRequestIDMiddleware()) | 71 | p.httpsServer.Use(p.createRequestIDMiddleware()) | ||
| 32 | 72 | ||||
| 73 | + | // --- Block Direct IP Access Middleware (Conditional) --- | |||
| 74 | + | if p.config.BlockDirectIP { | |||
| 75 | + | // This should run early, before logging or rate limiting potentially. | |||
| 76 | + | p.httpsServer.Use(blockDirectIPMiddleware()) | |||
| 77 | + | logger.Info("Direct IP blocking middleware is ENABLED (allows ACME)") | |||
| 78 | + | } else { | |||
| 79 | + | logger.Info("Direct IP blocking middleware is DISABLED") | |||
| 80 | + | } | |||
| 81 | + | ||||
| 33 | // Add middleware to log incoming headers (for debugging Cloudflare detection) | 82 | // Add middleware to log incoming headers (for debugging Cloudflare detection) | ||
| 34 | p.httpsServer.Use(logHeadersMiddleware()) | 83 | p.httpsServer.Use(logHeadersMiddleware()) | ||
| 35 | 84 | ||||
| 36 | - | // Check if rate limiting is disabled via configuration | 85 | + | // --- Rate Limiting Setup (Domain Only Now) --- |
| 37 | if p.config.EnableRateLimit { | 86 | if p.config.EnableRateLimit { | ||
| 38 | - | // Create a new Starskey rate limiter store for HTTPS server | 87 | + | logger.Debug("Rate limiting enabled. Setting up limiter for domain requests only.") |
| 39 | - | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | 88 | + | |
| 40 | - | "./data/ratelimiter/https", // Path to store rate limit data | 89 | + | // --- REMOVED Rate Limiter for Direct IP Access --- |
| 41 | - | 10, // Rate: 10 requests per second | 90 | + | |
| 42 | - | 30, // Burst: 30 requests | 91 | + | // --- Rate Limiter for Domain Access (Existing Logic) --- |
| 43 | - | 3*time.Minute, // Expires in 3 minutes | 92 | + | // Use Starskey or fallback for requests coming through a domain name. |
| 93 | + | domainRateLimiterStore, err := kv.NewStarskeyRateLimiterStore( | |||
| 94 | + | "./data/ratelimiter/domain", // Path for domain limiter data | |||
| 95 | + | 10, // Rate: 10 requests per second (default) | |||
| 96 | + | 30, // Burst: 30 requests (default) | |||
| 97 | + | 3*time.Minute, // Expires in 3 minutes | |||
| 44 | ) | 98 | ) | ||
| 45 | - | if err != nil { | |||
| 46 | - | logger.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err) | |||
| 47 | 99 | ||||
| 48 | - | // Fallback to memory store if Starskey store creation fails | 100 | + | if err != nil { |
| 101 | + | logger.Error("Failed to create Domain rate limiter store (Starskey), falling back to memory store", "error", err) | |||
| 102 | + | // Fallback to memory store | |||
| 49 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | 103 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | ||
| 50 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | 104 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | ||
| 51 | middleware.RateLimiterMemoryStoreConfig{ | 105 | middleware.RateLimiterMemoryStoreConfig{ | ||
| 52 | - | Rate: rate.Limit(10), // 10 requests per second | 106 | + | Rate: rate.Limit(10), |
| 53 | - | Burst: 30, // Burst of 30 requests | 107 | + | Burst: 30, |
| 54 | - | ExpiresIn: 3 * time.Minute, // Store expiration | 108 | + | ExpiresIn: 3 * time.Minute, |
| 55 | }, | 109 | }, | ||
| 56 | ), | 110 | ), | ||
| 57 | - | DenyHandler: func(context echo.Context, identifier string, err error) error { | 111 | + | // Apply this limiter ONLY if the host is NOT an IP address (redundant now but safe) |
| 58 | - | logger.Info("[RateLimit Debug] DenyHandler invoked for Memory Fallback", "identifier", identifier, "error", err.Error()) | 112 | + | Skipper: func(c echo.Context) bool { |
| 59 | - | 113 | + | host := c.Request().Host | |
| 60 | - | logger.Warn("Rate limit exceeded", | 114 | + | isIP := isHostIPAddress(host) |
| 61 | - | "ip", identifier, | 115 | + | if !isIP { |
| 62 | - | "path", context.Request().URL.Path, | 116 | + | // Log only if applying the limiter |
| 63 | - | "method", context.Request().Method) | 117 | + | logger.Debug("Applying domain rate limiter (Memory Fallback)", "host", host, "ip", c.RealIP()) |
| 64 | - | return context.String(http.StatusTooManyRequests, "Too many requests") | 118 | + | } |
| 119 | + | return isIP // Skip if it IS an IP | |||
| 65 | }, | 120 | }, | ||
| 66 | - | })) | |||
| 67 | - | } else { | |||
| 68 | - | // Use the Starskey store for HTTPS rate limiting | |||
| 69 | - | logger.Debug("Using Starskey rate limiter for HTTPS server") | |||
| 70 | - | ||||
| 71 | - | // Add custom Starskey rate limiter middleware | |||
| 72 | - | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |||
| 73 | - | Store: httpsRateLimiter, | |||
| 74 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | 121 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | ||
| 75 | - | id := ctx.RealIP() | 122 | + | return ctx.RealIP(), nil |
| 76 | - | logger.Info("[RateLimit Debug] IdentifierExtractor called", "ip_identified", id, "remote_addr", ctx.Request().RemoteAddr) | |||
| 77 | - | ||||
| 78 | - | logger.Debug("Rate limiting HTTPS request", | |||
| 79 | - | "ip", id, | |||
| 80 | - | "remote_addr", ctx.Request().RemoteAddr) | |||
| 81 | - | ||||
| 82 | - | // Add a response header to show the detected IP (for debugging) | |||
| 83 | - | ctx.Response().Header().Set("X-Rate-Limit-IP", id) | |||
| 84 | - | ||||
| 85 | - | return id, nil | |||
| 86 | - | }, | |||
| 87 | - | DenyHandler: func(context echo.Context, identifier string, err error) error { | |||
| 88 | - | logger.Info("[RateLimit Debug] DenyHandler invoked for Starskey", "identifier", identifier, "error", err.Error()) | |||
| 89 | - | ||||
| 90 | - | logger.Warn("HTTPS rate limit exceeded", | |||
| 91 | - | "ip", identifier, | |||
| 92 | - | "path", context.Request().URL.Path, | |||
| 93 | - | "method", context.Request().Method) | |||
| 94 | - | return context.String(http.StatusTooManyRequests, "Too many requests") | |||
| 95 | }, | 123 | }, | ||
| 96 | - | })) | |||
| 97 | - | ||||
| 98 | - | // Add cleanup function to global slice | |||
| 99 | - | rateLimiterCleanup = append(rateLimiterCleanup, func() { | |||
| 100 | - | if err := httpsRateLimiter.Close(); err != nil { | |||
| 101 | - | logger.Error("Failed to close HTTPS rate limiter store", "error", err) | |||
| 102 | - | } | |||
| 103 | - | }) | |||
| 104 | - | } | |||
| 105 | - | } else { | |||
| 106 | - | logger.Info("Rate limiting is disabled for HTTPS server via configuration") | |||
| 107 | - | } | |||
| 108 | - | ||||
| 109 | - | // Add custom logger middleware with request ID if enabled in config | |||
| 110 | - | if p.config.EnableHttpLogs { | |||
| 111 | - | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |||
| 112 | - | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", | |||
| 113 | - | CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00", | |||
| 114 | - | Skipper: func(c echo.Context) bool { | |||
| 115 | - | return !p.config.EnableHttpLogs | |||
| 116 | - | }, | |||
| 117 | - | })) | |||
| 118 | - | logger.Debug("HTTP request logging enabled for HTTPS server") | |||
| 119 | - | } else { | |||
| 120 | - | logger.Debug("HTTP request logging disabled for HTTPS server") | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | - | // Check if rate limiting is disabled via configuration | |||
| 124 | - | if p.config.EnableRateLimit { | |||
| 125 | - | // Create a new Starskey rate limiter store for HTTP server | |||
| 126 | - | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |||
| 127 | - | "./data/ratelimiter/http", // Path to store rate limit data | |||
| 128 | - | 2, // Rate: 2 requests per second (lowered from 10) | |||
| 129 | - | 10, // Burst: 10 requests (lowered from 30) | |||
| 130 | - | 3*time.Minute, // Expires in 3 minutes | |||
| 131 | - | ) | |||
| 132 | - | if err != nil { | |||
| 133 | - | logger.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err) | |||
| 134 | - | ||||
| 135 | - | // Fallback to memory store if Starskey store creation fails | |||
| 136 | - | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |||
| 137 | - | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |||
| 138 | - | middleware.RateLimiterMemoryStoreConfig{ | |||
| 139 | - | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) | |||
| 140 | - | Burst: 10, // Burst of 10 requests (lowered from 30) | |||
| 141 | - | ExpiresIn: 3 * time.Minute, // Store expiration | |||
| 142 | - | }, | |||
| 143 | - | ), | |||
| 144 | DenyHandler: func(context echo.Context, identifier string, err error) error { | 124 | DenyHandler: func(context echo.Context, identifier string, err error) error { | ||
| 145 | - | logger.Warn("Rate limit exceeded", | 125 | + | logger.Warn("Domain rate limit exceeded (Memory Fallback)", |
| 146 | "ip", identifier, | 126 | "ip", identifier, | ||
| 127 | + | "host", context.Request().Host, | |||
| 147 | "path", context.Request().URL.Path, | 128 | "path", context.Request().URL.Path, | ||
| 148 | - | "method", context.Request().Method) | 129 | + | "method", context.Request().Method, |
| 130 | + | "error", err.Error()) | |||
| 149 | return context.String(http.StatusTooManyRequests, "Too many requests") | 131 | return context.String(http.StatusTooManyRequests, "Too many requests") | ||
| 150 | }, | 132 | }, | ||
| 151 | })) | 133 | })) | ||
| 152 | } else { | 134 | } else { | ||
| 153 | - | // Use the Starskey store for HTTP rate limiting | 135 | + | logger.Debug("Using Starskey rate limiter for domain requests") |
| 154 | - | logger.Debug("Using Starskey rate limiter for HTTP server") | 136 | + | // Use the Starskey store |
| 155 | - | ||||
| 156 | - | // Add custom Starskey rate limiter middleware | |||
| 157 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | 137 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | ||
| 158 | - | Store: httpRateLimiter, | 138 | + | Store: domainRateLimiterStore, |
| 139 | + | // Apply this limiter ONLY if the host is NOT an IP address (redundant now but safe) | |||
| 140 | + | Skipper: func(c echo.Context) bool { | |||
| 141 | + | host := c.Request().Host | |||
| 142 | + | isIP := isHostIPAddress(host) | |||
| 143 | + | if !isIP { | |||
| 144 | + | // Log only if applying the limiter | |||
| 145 | + | logger.Debug("Applying domain rate limiter (Starskey)", "host", host, "ip", c.RealIP()) | |||
| 146 | + | } | |||
| 147 | + | return isIP // Skip if it IS an IP | |||
| 148 | + | }, | |||
| 159 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | 149 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | ||
| 160 | id := ctx.RealIP() | 150 | id := ctx.RealIP() | ||
| 161 | - | logger.Debug("Rate limiting HTTP request", | 151 | + | ctx.Response().Header().Set("X-Rate-Limit-Domain-IP", id) // Keep debug header |
| 162 | - | "ip", id, | |||
| 163 | - | "remote_addr", ctx.Request().RemoteAddr) | |||
| 164 | - | ||||
| 165 | - | // Add a response header to show the detected IP (for debugging) | |||
| 166 | - | ctx.Response().Header().Set("X-Rate-Limit-IP", id) | |||
| 167 | - | ||||
| 168 | return id, nil | 152 | return id, nil | ||
| 169 | }, | 153 | }, | ||
| 170 | DenyHandler: func(context echo.Context, identifier string, err error) error { | 154 | DenyHandler: func(context echo.Context, identifier string, err error) error { | ||
| 171 | - | logger.Warn("HTTP rate limit exceeded", | 155 | + | logger.Warn("Domain rate limit exceeded (Starskey)", |
| 172 | "ip", identifier, | 156 | "ip", identifier, | ||
| 157 | + | "host", context.Request().Host, | |||
| 173 | "path", context.Request().URL.Path, | 158 | "path", context.Request().URL.Path, | ||
| 174 | - | "method", context.Request().Method) | 159 | + | "method", context.Request().Method, |
| 160 | + | "error", err.Error()) | |||
| 175 | return context.String(http.StatusTooManyRequests, "Too many requests") | 161 | return context.String(http.StatusTooManyRequests, "Too many requests") | ||
| 176 | }, | 162 | }, | ||
| 177 | })) | 163 | })) | ||
| 178 | 164 | ||||
| 179 | - | // Add cleanup function to global slice | 165 | + | // Add cleanup function for the Starskey domain limiter |
| 180 | rateLimiterCleanup = append(rateLimiterCleanup, func() { | 166 | rateLimiterCleanup = append(rateLimiterCleanup, func() { | ||
| 181 | - | if err := httpRateLimiter.Close(); err != nil { | 167 | + | if err := domainRateLimiterStore.Close(); err != nil { |
| 182 | - | logger.Error("Failed to close HTTP rate limiter store", "error", err) | 168 | + | logger.Error("Failed to close Domain rate limiter store (Starskey)", "error", err) |
| 183 | } | 169 | } | ||
| 184 | }) | 170 | }) | ||
| 185 | } | 171 | } | ||
| 172 | + | logger.Debug("Configured rate limiter for domain requests") | |||
| 173 | + | ||||
| 186 | } else { | 174 | } else { | ||
| 187 | - | logger.Info("Rate limiting is disabled for HTTP server via configuration") | 175 | + | logger.Info("Rate limiting is disabled globally via configuration") |
| 188 | } | 176 | } | ||
| 189 | 177 | ||||
| 190 | - | // Add custom logger middleware with request ID for HTTP server if enabled in config | 178 | + | // --- Logging Middleware (Existing) --- |
| 191 | if p.config.EnableHttpLogs { | 179 | if p.config.EnableHttpLogs { | ||
| 192 | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | 180 | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | ||
| 193 | - | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", | 181 | + | // Use a standard string with escaped quotes for the JSON format |
| 182 | + | Format: "{\"time\":\"${time_rfc3339_nano}\",\"id\":\"${id}\",\"remote_ip\":\"${remote_ip}\",\"host\":\"${host}\",\"method\":\"${method}\",\"uri\":\"${uri}\",\"user_agent\":\"${user_agent}\",\"status\":${status},\"error\":\"${error}\",\"latency\":${latency},\"latency_human\":\"${latency_human}\",\"bytes_in\":${bytes_in},\"bytes_out\":${bytes_out}}\n", | |||
| 194 | CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00", | 183 | CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00", | ||
| 195 | Skipper: func(c echo.Context) bool { | 184 | Skipper: func(c echo.Context) bool { | ||
| 196 | - | return !p.config.EnableHttpLogs | 185 | + | // Skip logging if disabled in config OR if it's a successful ACME challenge |
| 186 | + | // (to avoid polluting logs during certificate issuance) | |||
| 187 | + | isAcme := strings.HasPrefix(c.Request().URL.Path, "/.well-known/acme-challenge/") | |||
| 188 | + | return !p.config.EnableHttpLogs || (isAcme && c.Response().Status == http.StatusOK) | |||
| 197 | }, | 189 | }, | ||
| 198 | })) | 190 | })) | ||
| 199 | - | logger.Debug("HTTP request logging enabled for HTTPS server") | 191 | + | logger.Debug("HTTP request logging enabled for proxy server") |
| 200 | } else { | 192 | } else { | ||
| 201 | - | logger.Debug("HTTP request logging disabled for HTTPS server") | 193 | + | logger.Debug("HTTP request logging disabled for proxy server") |
| 202 | } | 194 | } | ||
| 195 | + | ||||
| 196 | + | // REMOVED the duplicated rate limiter and logger blocks that seemed intended for a separate HTTP server | |||
| 203 | } | 197 | } | ||
| 204 | 198 | ||||
| 205 | // createRequestIDMiddleware returns a middleware function that generates a UUID for each request | 199 | // createRequestIDMiddleware returns a middleware function that generates a UUID for each request | ||