verify( $request->string(self::TOKEN_FIELD)->toString(), $request->boolean(self::UNAVAILABLE_FIELD), $action, $request->ip(), ); } public function verify(?string $token, bool $clientUnavailable, string $action, ?string $ip): BotCheck { if (! self::enabled()) { return new BotCheck(BotCheckStatus::Skipped); } $token = trim((string) $token); if ($token === '') { return $clientUnavailable ? $this->unavailable('widget_not_loaded', $action, $ip) : $this->failed('missing_token', $action, $ip); } if (strlen($token) > self::MAX_TOKEN_LENGTH) { return $this->failed('token_too_long', $action, $ip); } try { $response = Http::asForm() ->timeout((int) config('services.turnstile.timeout', 3)) ->post(self::ENDPOINT, array_filter([ 'secret' => config('services.turnstile.secret_key'), 'response' => $token, 'remoteip' => $ip, ])); } catch (ConnectionException) { return $this->unavailable('siteverify_unreachable', $action, $ip); } if (! $response->successful()) { return $this->unavailable('siteverify_http_'.$response->status(), $action, $ip); } $errors = (array) $response->json('error-codes', []); if ($response->json('success') !== true) { if (in_array('internal-error', $errors, true)) { return $this->unavailable('siteverify_internal_error', $action, $ip); } if (array_intersect(['missing-input-secret', 'invalid-input-secret'], $errors) !== []) { // Unser Fehler, nicht der des Absenders — laut melden, nicht blockieren. Log::error('Turnstile lehnt den Secret-Key ab — TURNSTILE_SECRET_KEY prüfen.', ['errors' => $errors]); return $this->unavailable('invalid_secret', $action, $ip); } return $this->failed(implode(',', $errors) ?: 'rejected', $action, $ip); } $returnedAction = (string) $response->json('action', ''); // Die Cloudflare-Testkeys (1x…/2x…/3x…) liefern keine echte Aktion. if (! $this->usesTestSecret() && $returnedAction !== $action) { return $this->failed('action_mismatch', $action, $ip); } return new BotCheck(BotCheckStatus::Passed); } private function unavailable(string $reason, string $action, ?string $ip): BotCheck { $this->log('Bot-Prüfung nicht verfügbar — durchgelassen und markiert.', $reason, $action, $ip); if (! config('services.turnstile.fail_open', true)) { return new BotCheck(BotCheckStatus::Failed, $reason); } return new BotCheck(BotCheckStatus::Unavailable, $reason); } private function failed(string $reason, string $action, ?string $ip): BotCheck { $this->log('Bot-Prüfung nicht bestanden.', $reason, $action, $ip); return new BotCheck(BotCheckStatus::Failed, $reason); } private function log(string $message, string $reason, string $action, ?string $ip): void { Log::channel('spam')->info($message, [ 'form' => $action, 'reason' => $reason, 'ip_prefix' => SpamLog::ipPrefix($ip), ]); } private function usesTestSecret(): bool { return (bool) preg_match('/^[123]x0{20,}/', (string) config('services.turnstile.secret_key')); } }