throttleKey($request, $gateway); $maxAttempts = $this->getMaxAttempts($request, $gateway); return $this->limiter->tooManyAttempts($key, $maxAttempts); } /** * Increment the webhook attempt counter. */ public function increment(Request $request, string $gateway): void { $key = $this->throttleKey($request, $gateway); $this->limiter->hit($key, self::DECAY_SECONDS); } /** * Get the number of attempts made. */ public function attempts(Request $request, string $gateway): int { return $this->limiter->attempts($this->throttleKey($request, $gateway)); } /** * Get seconds until rate limit resets. */ public function availableIn(Request $request, string $gateway): int { return $this->limiter->availableIn($this->throttleKey($request, $gateway)); } /** * Get remaining attempts before rate limit is hit. */ public function remainingAttempts(Request $request, string $gateway): int { $maxAttempts = $this->getMaxAttempts($request, $gateway); $attempts = $this->attempts($request, $gateway); return max(0, $maxAttempts - $attempts); } /** * Check if the request IP is from a trusted gateway. */ public function isTrustedGatewayIp(Request $request, string $gateway): bool { $ip = $request->ip(); if (! $ip) { return false; } $trustedIps = $this->getTrustedIps($gateway); foreach ($trustedIps as $trustedIp) { if ($this->ipMatches($ip, $trustedIp)) { return true; } } return false; } /** * Get max attempts based on whether IP is trusted. */ protected function getMaxAttempts(Request $request, string $gateway): int { // Check for gateway-specific config first $configKey = "commerce.webhooks.rate_limits.{$gateway}"; $gatewayConfig = config($configKey); if ($gatewayConfig) { if ($this->isTrustedGatewayIp($request, $gateway)) { return (int) ($gatewayConfig['trusted'] ?? self::TRUSTED_MAX_ATTEMPTS); } return (int) ($gatewayConfig['default'] ?? self::DEFAULT_MAX_ATTEMPTS); } // Fall back to global webhook config if ($this->isTrustedGatewayIp($request, $gateway)) { return (int) config('commerce.webhooks.rate_limits.trusted', self::TRUSTED_MAX_ATTEMPTS); } return (int) config('commerce.webhooks.rate_limits.default', self::DEFAULT_MAX_ATTEMPTS); } /** * Get trusted IPs for a gateway. * * Returns IP addresses or CIDR ranges that are known to belong to * the payment gateway. * * @return array */ protected function getTrustedIps(string $gateway): array { // Check gateway-specific trusted IPs first $gatewayIps = config("commerce.webhooks.trusted_ips.{$gateway}", []); // Merge with global trusted IPs $globalIps = config('commerce.webhooks.trusted_ips.global', []); return array_merge($gatewayIps, $globalIps); } /** * Check if an IP matches a trusted IP or CIDR range. */ protected function ipMatches(string $ip, string $trustedIp): bool { // Direct match if ($ip === $trustedIp) { return true; } // CIDR range match if (str_contains($trustedIp, '/')) { return $this->ipInCidr($ip, $trustedIp); } return false; } /** * Check if IP is within a CIDR range. */ protected function ipInCidr(string $ip, string $cidr): bool { [$subnet, $mask] = explode('/', $cidr, 2); $mask = (int) $mask; // Handle IPv4 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $ipLong = ip2long($ip); $subnetLong = ip2long($subnet); if ($ipLong === false || $subnetLong === false) { return false; } $maskLong = -1 << (32 - $mask); return ($ipLong & $maskLong) === ($subnetLong & $maskLong); } // Handle IPv6 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ipBinary = inet_pton($ip); $subnetBinary = inet_pton($subnet); if ($ipBinary === false || $subnetBinary === false) { return false; } // Build a bitmask from the prefix length $maskBinary = str_repeat("\xff", (int) ($mask / 8)); if ($mask % 8) { $maskBinary .= chr(0xff << (8 - ($mask % 8))); } $maskBinary = str_pad($maskBinary, 16, "\x00"); return ($ipBinary & $maskBinary) === ($subnetBinary & $maskBinary); } return false; } /** * Generate throttle key from gateway and IP. * * Each gateway has separate rate limits per IP to prevent * cross-gateway interference. */ protected function throttleKey(Request $request, string $gateway): string { $ip = $request->ip() ?? 'unknown'; return "webhook:{$gateway}:ip:{$ip}"; } }