userAgent(); $botName = HoneypotHit::detectBot($userAgent); $path = $request->path(); $severity = HoneypotHit::severityForPath($path); $ip = $request->ip(); // Rate limit honeypot logging to prevent DoS via log flooding. // Each IP gets limited to N log entries per time window. $rateLimitKey = 'honeypot:log:'.$ip; $maxAttempts = (int) config('core.bouncer.honeypot.rate_limit_max', 10); $decaySeconds = (int) config('core.bouncer.honeypot.rate_limit_window', 60); if (! RateLimiter::tooManyAttempts($rateLimitKey, $maxAttempts)) { RateLimiter::hit($rateLimitKey, $decaySeconds); // Optional services - use app() since route skips web middleware $geoIp = app(DetectLocation::class); HoneypotHit::create([ 'ip_address' => $ip, 'user_agent' => substr($userAgent ?? '', 0, 1000), 'referer' => substr($request->header('Referer', ''), 0, 2000), 'path' => $path, 'method' => $request->method(), 'headers' => $this->sanitizeHeaders($request->headers->all()), 'country' => $geoIp?->getCountryCode($ip), 'city' => $geoIp?->getCity($ip), 'is_bot' => $botName !== null, 'bot_name' => $botName, 'severity' => $severity, ]); } // Auto-block critical hits (active probing) if enabled in config. // Skip localhost in dev to avoid blocking yourself. $autoBlockEnabled = config('core.bouncer.honeypot.auto_block_critical', true); $isLocalhost = in_array($ip, ['127.0.0.1', '::1'], true); $isCritical = $severity === HoneypotHit::getSeverityCritical(); if ($autoBlockEnabled && $isCritical && ! $isLocalhost) { app(BlocklistService::class)->block($ip, 'honeypot_critical'); } // Return the 418 I'm a teapot response return response($this->teapotBody(), 418, [ 'Content-Type' => 'text/html; charset=utf-8', 'X-Powered-By' => 'Earl Grey', 'X-Severity' => $severity, ]); } /** * Remove sensitive headers before storing. */ protected function sanitizeHeaders(array $headers): array { $sensitive = ['cookie', 'authorization', 'x-csrf-token', 'x-xsrf-token']; foreach ($sensitive as $key) { unset($headers[$key]); } return $headers; } /** * The teapot response body. */ protected function teapotBody(): string { return <<<'HTML' 418 I'm a Teapot
🫖

418 I'm a Teapot

The server refuses to brew coffee because it is, permanently, a teapot.

RFC 2324 · RFC 7168

HTML; } }