diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f410e..95d819c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated -- **`KeyExtractors::clientIp(TrustedProxyResolver)`** - the resolved client IP is now the default discriminator for every rule via the `Config` IP resolver, so a dedicated key extractor is no longer needed. Configure proxy trust once with `$config->setIpResolver($trustedProxyResolver->resolve(...))` and omit the rule's key (or use `PortableConfig::keyIp()`); both resolve the client IP. For the raw connecting peer address, `KeyExtractors::ip()` (REMOTE_ADDR) remains the explicit escape hatch. +- **`KeyExtractors::clientIp(TrustedProxyResolver)`** - the resolved client IP is now the default discriminator for every rule via the `Config` IP resolver, so a dedicated key extractor is no longer needed. Configure proxy trust once with `$config->setIpResolver($trustedProxyResolver->resolve(...))` and omit the rule's key (or use `PortableConfig::keyIp()`); both resolve the client IP. +- **`KeyExtractors::ip()`** - the name is ambiguous (it reads like "the client IP" but returns the raw `REMOTE_ADDR` peer, which behind a proxy is the proxy itself) and it bypasses the `Config` IP resolver. To key on the client IP, omit the rule's key (or use `PortableConfig::keyIp()`) so it resolves through the resolver. For the raw connecting peer, read `$request->getServerParams()['REMOTE_ADDR']` directly. ## 0.6.0 - 2026-06-17 diff --git a/README.md b/README.md index 6f3f490..871f8ed 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ composer require flowd/phirewall ```php use Flowd\Phirewall\Config; use Flowd\Phirewall\Middleware; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; // Create the firewall @@ -38,14 +37,13 @@ $config->safelists->add('health', fn($req) => $req->getUri()->getPath() === '/he $config->blocklists->add('scanners', fn($req) => str_starts_with($req->getUri()->getPath(), '/wp-admin')); // Rate limit: 100 requests per minute per IP -$config->throttles->add('api', limit: 100, period: 60 /* seconds */, key: KeyExtractors::ip()); +$config->throttles->add('api', limit: 100, period: 60 /* seconds */); // Ban IP after 5 failed logins in 5 minutes. The filter never matches at // request time — failures are signaled from the handler via RequestContext // (see "Login Protection" below for the handler-side snippet). $config->fail2ban->add('login', threshold: 5, period: 300 /* seconds */, ban: 3600 /* seconds */, filter: fn($req) => false, - key: KeyExtractors::ip() ); // Add to your middleware stack @@ -221,7 +219,8 @@ $config->throttles->add('api', limit: 100, period: 60); ``` When no resolver is set the client IP is `REMOTE_ADDR`. For the raw connecting peer -address regardless of proxy configuration, use `KeyExtractors::ip()`. +address regardless of proxy configuration, read `$request->getServerParams()['REMOTE_ADDR']` +directly. ## Custom Responses @@ -412,13 +411,13 @@ See [31-presets.php](examples/31-presets.php) for standalone use, portable inspe use Flowd\Phirewall\KeyExtractors; // Global limit -$config->throttles->add('global', limit: 1000, period: 60, key: KeyExtractors::ip()); +$config->throttles->add('global', limit: 1000, period: 60); // Burst + sustained rate limiting with multiThrottle $config->throttles->multi('api', [ 1 => 5, // 5 req/s burst 60 => 200, // 200 req/min sustained -], KeyExtractors::ip()); +]); // Dynamic limits based on user role // Note: a header-keyed rule is skipped when the header is absent, so a client can avoid the @@ -431,8 +430,6 @@ $config->throttles->add('user', fn($req) => $req->getHeaderLine('X-Plan') === 'p ### Login Protection ```php -use Flowd\Phirewall\KeyExtractors; - // Throttle login attempts $config->throttles->add('login', limit: 10, period: 60, key: function($req) { return $req->getUri()->getPath() === '/login' @@ -443,7 +440,6 @@ $config->throttles->add('login', limit: 10, period: 60, key: function($req) { // Ban after failures — signaled via RequestContext from your handler $config->fail2ban->add('login-ban', threshold: 5, period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip() ); ``` diff --git a/examples/01-basic-setup.php b/examples/01-basic-setup.php index b4e5059..2faa156 100644 --- a/examples/01-basic-setup.php +++ b/examples/01-basic-setup.php @@ -18,7 +18,6 @@ require __DIR__ . '/../vendor/autoload.php'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -71,8 +70,7 @@ $config->throttles->add( name: 'ip-limit', limit: 5, - period: 60, - key: KeyExtractors::ip() + period: 60 ); echo "3c. Throttle rule added: 5 requests/minute per IP\n"; diff --git a/examples/02-brute-force-protection.php b/examples/02-brute-force-protection.php index 221cf98..3b16eda 100644 --- a/examples/02-brute-force-protection.php +++ b/examples/02-brute-force-protection.php @@ -19,7 +19,6 @@ use Flowd\Phirewall\Config\DiagnosticsCounters; use Flowd\Phirewall\Config\DiagnosticsDispatcher; use Flowd\Phirewall\Context\RequestContext; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -60,8 +59,7 @@ threshold: 5, // Number of failures before ban period: 300, // Time window in seconds (5 minutes) ban: 3600, // Ban duration in seconds (1 hour) - filter: fn(ServerRequestInterface $serverRequest): bool => false, - key: KeyExtractors::ip() + filter: fn(ServerRequestInterface $serverRequest): bool => false ); echo "1. Fail2Ban configured: 5 failures in 5 min = 1 hour ban (handler signals failures)\n"; diff --git a/examples/03-api-rate-limiting.php b/examples/03-api-rate-limiting.php index 8691236..f70647c 100644 --- a/examples/03-api-rate-limiting.php +++ b/examples/03-api-rate-limiting.php @@ -48,8 +48,7 @@ $config->throttles->add( name: 'global-ip', limit: 100, // 100 requests - period: 60, // per minute - key: KeyExtractors::ip() + period: 60 // per minute ); echo "1. Global limit: 100 req/min per IP\n"; @@ -59,8 +58,7 @@ $config->throttles->add( name: 'burst', limit: 20, // 20 requests - period: 5, // in 5 seconds - key: KeyExtractors::ip() + period: 5 // in 5 seconds ); echo "2. Burst limit: 20 req/5s per IP\n"; diff --git a/examples/06-bot-detection.php b/examples/06-bot-detection.php index 562ee8a..8838f87 100644 --- a/examples/06-bot-detection.php +++ b/examples/06-bot-detection.php @@ -19,7 +19,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\DiagnosticsCounters; use Flowd\Phirewall\Config\DiagnosticsDispatcher; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -225,8 +224,7 @@ filter: fn(ServerRequestInterface $serverRequest): bool => // This filter matches requests that hit our blocklist rules // In practice, you'd track this via events - $serverRequest->getHeaderLine('X-Scanner-Detected') === '1', - key: KeyExtractors::ip() + $serverRequest->getHeaderLine('X-Scanner-Detected') === '1' ); echo "5. Fail2Ban for persistent scanners configured\n"; @@ -237,8 +235,7 @@ $config->throttles->add( name: 'rapid-requests', limit: 30, // 30 requests - period: 10, // In 10 seconds - key: KeyExtractors::ip() + period: 10 // In 10 seconds ); echo "6. Rapid request throttling configured (30/10s)\n\n"; diff --git a/examples/09-observability-monolog.php b/examples/09-observability-monolog.php index 4ff556f..65c4671 100644 --- a/examples/09-observability-monolog.php +++ b/examples/09-observability-monolog.php @@ -22,7 +22,6 @@ require __DIR__ . '/../vendor/autoload.php'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -140,8 +139,7 @@ public function clear(): void $config->throttles->add( name: 'ip-limit', limit: 3, - period: 60, - key: KeyExtractors::ip() + period: 60 ); echo "Throttle rule configured: 3 requests/min per IP\n"; diff --git a/examples/10-observability-opentelemetry.php b/examples/10-observability-opentelemetry.php index 829e1b9..0e5e8fc 100644 --- a/examples/10-observability-opentelemetry.php +++ b/examples/10-observability-opentelemetry.php @@ -22,7 +22,6 @@ require __DIR__ . '/../vendor/autoload.php'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -157,8 +156,7 @@ public function dispatch(object $event): object $config->throttles->add( name: 'api-limit', limit: 3, - period: 60, - key: KeyExtractors::ip() + period: 60 ); echo "Throttle rule: 3 requests/min per IP\n"; diff --git a/examples/11-redis-storage.php b/examples/11-redis-storage.php index 1500a33..b46ea0e 100644 --- a/examples/11-redis-storage.php +++ b/examples/11-redis-storage.php @@ -27,7 +27,6 @@ require __DIR__ . '/../vendor/autoload.php'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\RedisCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -95,8 +94,7 @@ $config->throttles->add( name: 'ip-limit', limit: 3, // Only 3 requests - period: 30, // Per 30 seconds - key: KeyExtractors::ip() + period: 30 // Per 30 seconds ); echo "Throttle rule: 3 requests per 30 seconds per IP\n"; @@ -106,8 +104,7 @@ threshold: 2, // 2 blocked requests period: 60, // In 1 minute ban: 300, // 5 minute ban - filter: fn($req): bool => $req->getHeaderLine('X-Abuse') === '1', - key: KeyExtractors::ip() + filter: fn($req): bool => $req->getHeaderLine('X-Abuse') === '1' ); echo "Fail2Ban rule: 2 abuse markers = 5 minute ban\n\n"; diff --git a/examples/16-allow2ban.php b/examples/16-allow2ban.php index 01e2887..994c36a 100644 --- a/examples/16-allow2ban.php +++ b/examples/16-allow2ban.php @@ -31,7 +31,6 @@ threshold: 100, period: 60, banSeconds: 3600, - key: KeyExtractors::ip(), ); // Multiple rules can coexist. E.g., also ban by API key for authenticated routes diff --git a/examples/21-sliding-window.php b/examples/21-sliding-window.php index 01fdb24..666f135 100644 --- a/examples/21-sliding-window.php +++ b/examples/21-sliding-window.php @@ -21,7 +21,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Http\Firewall; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\ServerRequest; @@ -35,7 +34,6 @@ name: 'api-sliding', limit: 10, period: 60, - key: KeyExtractors::ip(), ); echo "Sliding throttle configured: 10 req/60s per IP\n\n"; diff --git a/examples/22-multi-throttle.php b/examples/22-multi-throttle.php index bc94b20..5085f4c 100644 --- a/examples/22-multi-throttle.php +++ b/examples/22-multi-throttle.php @@ -15,7 +15,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Http\Firewall; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\ServerRequest; @@ -32,7 +31,7 @@ $config->throttles->multi('api', [ 1 => 3, // 3 req/s burst limit 60 => 60, // 60 req/min sustained limit -], KeyExtractors::ip()); +]); echo "Rules registered:\n"; foreach ($config->throttles->rules() as $name => $rule) { diff --git a/examples/24-pdo-storage.php b/examples/24-pdo-storage.php index 0fd1028..c5ec1e9 100644 --- a/examples/24-pdo-storage.php +++ b/examples/24-pdo-storage.php @@ -17,7 +17,6 @@ use Flowd\Phirewall\BanType; use Flowd\Phirewall\Config; use Flowd\Phirewall\Http\Firewall; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\PdoCache; use Nyholm\Psr7\ServerRequest; @@ -34,11 +33,10 @@ // --- Configure rate limiting --- -$config->throttles->add('api', limit: 5, period: 60, key: KeyExtractors::ip()); +$config->throttles->add('api', limit: 5, period: 60); $config->fail2ban->add('login', threshold: 3, period: 300, ban: 600, - filter: fn($req): bool => $req->getHeaderLine('X-Login-Failed') === '1', - key: KeyExtractors::ip() + filter: fn($req): bool => $req->getHeaderLine('X-Login-Failed') === '1' ); $firewall = new Firewall($config); diff --git a/examples/26-psr17-factories.php b/examples/26-psr17-factories.php index 5fff20e..5c987fe 100644 --- a/examples/26-psr17-factories.php +++ b/examples/26-psr17-factories.php @@ -22,7 +22,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Response\Psr17BlocklistedResponseFactory; use Flowd\Phirewall\Config\Response\Psr17ThrottledResponseFactory; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -75,7 +74,7 @@ public function handle(ServerRequestInterface $serverRequest): ResponseInterface $config->enableResponseHeaders(); $config->usePsr17Responses($psr17Factory, $psr17Factory); $config->blocklists->add('admin', fn(ServerRequestInterface $serverRequest): bool => str_starts_with($serverRequest->getUri()->getPath(), '/admin')); -$config->throttles->add('ip', 2, 60, KeyExtractors::ip()); +$config->throttles->add('ip', 2, 60); $middleware = new Middleware($config, $psr17Factory); @@ -120,7 +119,7 @@ public function handle(ServerRequestInterface $serverRequest): ResponseInterface 'Rate limit exceeded. Please slow down.', ); $config2->blocklists->add('blocked', fn(ServerRequestInterface $serverRequest): bool => $serverRequest->getUri()->getPath() === '/secret'); -$config2->throttles->add('ip', 1, 30, KeyExtractors::ip()); +$config2->throttles->add('ip', 1, 30); $middleware2 = new Middleware($config2, $psr17Factory); diff --git a/examples/27-request-context.php b/examples/27-request-context.php index acee566..09cb505 100644 --- a/examples/27-request-context.php +++ b/examples/27-request-context.php @@ -23,7 +23,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Context\RequestContext; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -54,7 +53,6 @@ period: 300, ban: 3600, filter: fn(ServerRequestInterface $serverRequest): bool => false, - key: KeyExtractors::ip(), ); echo " Fail2Ban rule 'login-failures': 3 failures in 5 min = 1 hour ban\n"; diff --git a/src/Config.php b/src/Config.php index 7525e51..dee8bea 100644 --- a/src/Config.php +++ b/src/Config.php @@ -365,8 +365,8 @@ public function resolveKey(?KeyExtractorInterface $keyExtractor, ServerRequestIn } /** - * This Config's client-IP resolver, falling back to {@see KeyExtractors::ip()} - * (REMOTE_ADDR) when none is set. Supplied to {@see Matchers\ClientIpResolverAware} + * This Config's client-IP resolver, falling back to the raw REMOTE_ADDR peer + * address when none is set. Supplied to {@see Matchers\ClientIpResolverAware} * matchers at evaluation time so an IP rule added without an explicit resolver * reads the client IP through the Config it actually runs under. * @@ -374,7 +374,10 @@ public function resolveKey(?KeyExtractorInterface $keyExtractor, ServerRequestIn */ public function clientIpResolver(): callable { - return $this->ipResolver ?? KeyExtractors::ip(); + return $this->ipResolver ?? static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }; } // ── Discriminator normalizer ──────────────────────────────────────── diff --git a/src/Config/FileIpBlocklistMatcher.php b/src/Config/FileIpBlocklistMatcher.php index 7419ce1..b55aad7 100644 --- a/src/Config/FileIpBlocklistMatcher.php +++ b/src/Config/FileIpBlocklistMatcher.php @@ -4,7 +4,6 @@ namespace Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\ClientIpResolverAware; use Flowd\Phirewall\Matchers\Support\CidrMatcher; use Psr\Http\Message\ServerRequestInterface; @@ -37,8 +36,8 @@ final class FileIpBlocklistMatcher implements RequestMatcherInterface, ClientIpR * How to extract the client IP from a request. When omitted * ({@see ClientIpResolverAware}), the matcher late-binds to the * resolver of the {@see \Flowd\Phirewall\Config} it is evaluated - * under, falling back to {@see KeyExtractors::ip()} (`REMOTE_ADDR` - * verbatim, no proxy headers) when used standalone or when that + * under, falling back to `REMOTE_ADDR` (the raw peer address, + * no proxy headers) when used standalone or when that * Config sets none. Deployments behind a CDN, load balancer, or * reverse proxy must configure a trusted client-IP resolver - set * it on the Config via `setIpResolver((new TrustedProxyResolver([...]))->resolve(...))`, @@ -65,7 +64,10 @@ public function __construct( public function match(ServerRequestInterface $serverRequest): MatchResult { - return $this->matchWithResolver($serverRequest, KeyExtractors::ip()); + return $this->matchWithResolver($serverRequest, static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }); } public function matchWithResolver(ServerRequestInterface $serverRequest, callable $defaultResolver): MatchResult diff --git a/src/Infrastructure/InfrastructureBanListener.php b/src/Infrastructure/InfrastructureBanListener.php index 982f3d8..a474672 100644 --- a/src/Infrastructure/InfrastructureBanListener.php +++ b/src/Infrastructure/InfrastructureBanListener.php @@ -6,7 +6,6 @@ use Flowd\Phirewall\Events\BlocklistMatched; use Flowd\Phirewall\Events\Fail2BanBanned; -use Flowd\Phirewall\KeyExtractors; use Psr\Http\Message\ServerRequestInterface; /** @@ -38,8 +37,8 @@ public function __construct( */ ?callable $keyToIp = null, /** - * Extract IP address from a ServerRequestInterface. Defaults to - * {@see KeyExtractors::ip()} (REMOTE_ADDR). Deployments behind a + * Extract IP address from a ServerRequestInterface. Defaults to the + * raw `REMOTE_ADDR` peer address. Deployments behind a * CDN, load balancer, or reverse proxy should pass * `(new TrustedProxyResolver([...]))->resolve(...)` so the * infrastructure ban targets the originating client rather than the @@ -50,7 +49,10 @@ public function __construct( ?callable $requestToIp = null, ) { $this->keyToIp = $keyToIp ?? static fn(string $key): string => $key; - $this->requestToIp = $requestToIp ?? KeyExtractors::ip(); + $this->requestToIp = $requestToIp ?? static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }; } /** Listener for Fail2Ban bans. */ diff --git a/src/KeyExtractors.php b/src/KeyExtractors.php index 1d4959c..0573428 100644 --- a/src/KeyExtractors.php +++ b/src/KeyExtractors.php @@ -12,7 +12,7 @@ * Common key extractor helpers for counter rules (throttle / fail2ban / allow2ban / track). * * The client IP is the default discriminator: omit a rule's key (or use - * PortableConfig::keyIp()) and the firewall resolves it through the Config's IP resolver, + * \Flowd\Phirewall\Portable\PortableConfig::keyIp()) and the firewall resolves it through the Config's IP resolver, * falling back to REMOTE_ADDR when none is set. Configure proxy trust once with * $config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...)). Reach for * {@see ip()} only when you deliberately want the raw REMOTE_ADDR peer address. @@ -20,10 +20,12 @@ final class KeyExtractors { /** - * The raw REMOTE_ADDR peer address - the explicit escape hatch for when you - * deliberately want the connecting peer rather than the resolved client IP. Does - * NOT consult the Config IP resolver. The default client IP comes from that resolver - * (omit a rule key / PortableConfig::keyIp()); use this only for the raw peer address. + * The raw REMOTE_ADDR peer address. + * + * @deprecated The name is ambiguous and it bypasses the Config IP resolver. To key on + * the client IP, omit the rule's key (or use \Flowd\Phirewall\Portable\PortableConfig::keyIp()) so it resolves + * through the Config's IP resolver (else REMOTE_ADDR). For the raw connecting peer, + * read $request->getServerParams()['REMOTE_ADDR'] directly. * @return Closure(ServerRequestInterface): ?string */ public static function ip(): Closure @@ -125,7 +127,7 @@ public static function userAgent(): Closure * @deprecated The client IP is now the default for every rule via the Config IP * resolver, so a dedicated extractor is no longer needed. Configure proxy trust * once with $config->setIpResolver($trustedProxyResolver->resolve(...)) and omit - * the rule key (or use PortableConfig::keyIp()); both resolve the client IP. + * the rule key (or use \Flowd\Phirewall\Portable\PortableConfig::keyIp()); both resolve the client IP. * @return Closure(ServerRequestInterface): ?string */ public static function clientIp(TrustedProxyResolver $trustedProxyResolver): Closure diff --git a/src/Matchers/IpMatcher.php b/src/Matchers/IpMatcher.php index 87e29fe..798e14f 100644 --- a/src/Matchers/IpMatcher.php +++ b/src/Matchers/IpMatcher.php @@ -6,7 +6,6 @@ use Flowd\Phirewall\Config\MatchResult; use Flowd\Phirewall\Config\RequestMatcherInterface; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\Support\CidrMatcher; use Psr\Http\Message\ServerRequestInterface; @@ -18,7 +17,7 @@ * * When constructed without an explicit resolver the client IP is read through * the evaluating Config's resolver at match time ({@see ClientIpResolverAware}); - * standalone use falls back to {@see KeyExtractors::ip()} (REMOTE_ADDR). + * standalone use falls back to the raw REMOTE_ADDR peer address. */ final class IpMatcher implements RequestMatcherInterface, ClientIpResolverAware { @@ -33,7 +32,7 @@ final class IpMatcher implements RequestMatcherInterface, ClientIpResolverAware /** * @param list $ipsOrCidrs List of IPs and/or CIDR ranges (e.g. '10.0.0.1', '192.168.0.0/16', '::1') - * @param (callable(ServerRequestInterface): ?string)|null $ipResolver Explicit IP resolver. When omitted, the evaluating Config's resolver is used (falling back to KeyExtractors::ip()). + * @param (callable(ServerRequestInterface): ?string)|null $ipResolver Explicit IP resolver. When omitted, the evaluating Config's resolver is used (falling back to REMOTE_ADDR). */ public function __construct(array $ipsOrCidrs, ?callable $ipResolver = null) { @@ -62,7 +61,10 @@ public function __construct(array $ipsOrCidrs, ?callable $ipResolver = null) public function match(ServerRequestInterface $serverRequest): MatchResult { - return $this->matchWithResolver($serverRequest, KeyExtractors::ip()); + return $this->matchWithResolver($serverRequest, static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }); } public function matchWithResolver(ServerRequestInterface $serverRequest, callable $defaultResolver): MatchResult diff --git a/src/Matchers/TrustedBotMatcher.php b/src/Matchers/TrustedBotMatcher.php index 5783b50..08e61e5 100644 --- a/src/Matchers/TrustedBotMatcher.php +++ b/src/Matchers/TrustedBotMatcher.php @@ -6,7 +6,6 @@ use Flowd\Phirewall\Config\MatchResult; use Flowd\Phirewall\Config\RequestMatcherInterface; -use Flowd\Phirewall\KeyExtractors; use Psr\Http\Message\ServerRequestInterface; use Psr\SimpleCache\CacheInterface; @@ -72,7 +71,7 @@ final class TrustedBotMatcher implements RequestMatcherInterface, ClientIpResolv * @param list $additionalBots * @param (callable(string): string)|null $reverseResolve Override for gethostbyaddr() (for testing). * @param (callable(string): list)|null $forwardResolve Override for gethostbynamel() (for testing). - * @param (callable(ServerRequestInterface): ?string)|null $ipResolver Explicit IP resolver. When omitted, the evaluating Config's resolver is used (falling back to KeyExtractors::ip()). + * @param (callable(ServerRequestInterface): ?string)|null $ipResolver Explicit IP resolver. When omitted, the evaluating Config's resolver is used (falling back to REMOTE_ADDR). * @param CacheInterface|null $cache PSR-16 cache for DNS results. Avoids repeated lookups for the same IP. * @param positive-int $cacheTtl Cache TTL in seconds. Default: 86400 (24 hours). */ @@ -129,7 +128,10 @@ public function __construct( public function match(ServerRequestInterface $serverRequest): MatchResult { - return $this->matchWithResolver($serverRequest, KeyExtractors::ip()); + return $this->matchWithResolver($serverRequest, static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }); } public function matchWithResolver(ServerRequestInterface $serverRequest, callable $defaultResolver): MatchResult diff --git a/src/Pattern/SnapshotBlocklistMatcher.php b/src/Pattern/SnapshotBlocklistMatcher.php index 014dba2..b2cb910 100644 --- a/src/Pattern/SnapshotBlocklistMatcher.php +++ b/src/Pattern/SnapshotBlocklistMatcher.php @@ -6,7 +6,6 @@ use Flowd\Phirewall\Config\MatchResult; use Flowd\Phirewall\Config\RequestMatcherInterface; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\ClientIpResolverAware; use Flowd\Phirewall\Matchers\Support\CidrMatcher; use Flowd\Phirewall\Matchers\Support\RegexMatcher; @@ -60,7 +59,10 @@ public function withBackend(PatternBackendInterface $patternBackend): self public function match(ServerRequestInterface $serverRequest): MatchResult { - return $this->matchWithResolver($serverRequest, KeyExtractors::ip()); + return $this->matchWithResolver($serverRequest, static function (ServerRequestInterface $serverRequest): ?string { + $remoteAddr = $serverRequest->getServerParams()['REMOTE_ADDR'] ?? null; + return is_string($remoteAddr) && $remoteAddr !== '' ? $remoteAddr : null; + }); } public function matchWithResolver(ServerRequestInterface $serverRequest, callable $defaultResolver): MatchResult diff --git a/src/Portable/PortableConfig.php b/src/Portable/PortableConfig.php index 27cde0d..a3153d1 100644 --- a/src/Portable/PortableConfig.php +++ b/src/Portable/PortableConfig.php @@ -1005,7 +1005,6 @@ private function compileKey(array $key): \Closure { $type = (string)($key['type'] ?? ''); return match ($type) { - 'ip' => KeyExtractors::ip(), 'method' => KeyExtractors::method(), 'path' => KeyExtractors::path(), 'header' => (static fn(string $name): \Closure => KeyExtractors::header($name))((string)($key['name'] ?? '')), diff --git a/tests/Unit/Config/ConfigCompositionTest.php b/tests/Unit/Config/ConfigCompositionTest.php index cf1d428..9d1acfa 100644 --- a/tests/Unit/Config/ConfigCompositionTest.php +++ b/tests/Unit/Config/ConfigCompositionTest.php @@ -5,7 +5,6 @@ namespace Flowd\Phirewall\Tests\Config; use Flowd\Phirewall\Config; -use Flowd\Phirewall\Config\ClosureKeyExtractor; use Flowd\Phirewall\Config\ClosureRequestMatcher; use Flowd\Phirewall\Config\Response\BlocklistedResponseFactoryInterface; use Flowd\Phirewall\Config\Response\ThrottledResponseFactoryInterface; @@ -17,7 +16,6 @@ use Flowd\Phirewall\Config\Rule\TrackRule; use Flowd\Phirewall\Http\Firewall; use Flowd\Phirewall\Http\Outcome; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Pattern\InMemoryPatternBackend; use Flowd\Phirewall\Pattern\PatternEntry; use Flowd\Phirewall\Pattern\PatternKind; @@ -375,10 +373,10 @@ public function testTypedRulesAreCarriedAcrossEverySection(): void { $base = new Config(new InMemoryCache()); $base->safelists->addRule(new SafelistRule('s', new ClosureRequestMatcher(static fn(): bool => false))); - $base->throttles->addRule(new ThrottleRule('t', 1, 60, new ClosureKeyExtractor(KeyExtractors::ip()))); - $base->fail2ban->addRule(new Fail2BanRule('f', 1, 60, 60, new ClosureRequestMatcher(static fn(): bool => false), new ClosureKeyExtractor(KeyExtractors::ip()))); - $base->allow2ban->addRule(new Allow2BanRule('a', 1, 60, 60, new ClosureKeyExtractor(KeyExtractors::ip()))); - $base->tracks->addRule(new TrackRule('tr', 60, new ClosureRequestMatcher(static fn(): bool => false), new ClosureKeyExtractor(KeyExtractors::ip()))); + $base->throttles->addRule(new ThrottleRule('t', 1, 60, null)); + $base->fail2ban->addRule(new Fail2BanRule('f', 1, 60, 60, new ClosureRequestMatcher(static fn(): bool => false), null)); + $base->allow2ban->addRule(new Allow2BanRule('a', 1, 60, 60, null)); + $base->tracks->addRule(new TrackRule('tr', 60, new ClosureRequestMatcher(static fn(): bool => false), null)); $composed = $base->with(new Config(new InMemoryCache())); diff --git a/tests/Unit/RequestAttributeIntegrationTest.php b/tests/Unit/RequestAttributeIntegrationTest.php index 77d0ce0..30c07f3 100644 --- a/tests/Unit/RequestAttributeIntegrationTest.php +++ b/tests/Unit/RequestAttributeIntegrationTest.php @@ -8,7 +8,6 @@ use Flowd\Phirewall\Context\RequestContext; use Flowd\Phirewall\Events\Fail2BanBanned; use Flowd\Phirewall\Events\FirewallError; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -89,7 +88,6 @@ public function testMiddlewareAttachesContextAndHandlerRecordsFailure(): void period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -115,7 +113,6 @@ public function testBanTriggersOnThreshold(): void period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -211,7 +208,6 @@ public function has(string $key): bool period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -297,7 +293,6 @@ public function has(string $key): bool period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -379,7 +374,6 @@ public function testUnknownRuleNameIsIgnored(): void period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -406,7 +400,6 @@ public function testDiscriminatorNormalizerAppliedToRecordedFailures(): void period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); @@ -439,7 +432,6 @@ public function testRecordFailureWithoutKeyUsesRuleDiscriminator(): void period: 300, ban: 3600, filter: fn($request): bool => false, - key: KeyExtractors::ip(), ); $middleware = new Middleware($config, new Psr17Factory()); diff --git a/tests/Unit/Store/PdoCacheFunctionalTest.php b/tests/Unit/Store/PdoCacheFunctionalTest.php index 759586d..e75fb67 100644 --- a/tests/Unit/Store/PdoCacheFunctionalTest.php +++ b/tests/Unit/Store/PdoCacheFunctionalTest.php @@ -6,7 +6,6 @@ use Flowd\Phirewall\Config; use Flowd\Phirewall\Http\Firewall; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\PdoCache; use Nyholm\Psr7\ServerRequest; use PDO; @@ -263,7 +262,7 @@ public function testWorksAsFirewallBackend(): void $cache = $this->createCache(); $config = new Config($cache); - $config->throttles->add('api', limit: 3, period: 60, key: KeyExtractors::ip()); + $config->throttles->add('api', limit: 3, period: 60); $firewall = new Firewall($config); $request = new ServerRequest('GET', '/', [], null, '1.1', ['REMOTE_ADDR' => '10.0.0.1']); @@ -285,7 +284,6 @@ public function testFail2BanWithRealDatabase(): void period: 60, ban: 300, filter: fn($request): bool => $request->getHeaderLine('X-Failed') === '1', - key: KeyExtractors::ip() ); $firewall = new Firewall($config); diff --git a/tests/phpt/allow2ban/allow2ban_threshold_then_ban.phpt b/tests/phpt/allow2ban/allow2ban_threshold_then_ban.phpt index a70f75d..64fb570 100644 --- a/tests/phpt/allow2ban/allow2ban_threshold_then_ban.phpt +++ b/tests/phpt/allow2ban/allow2ban_threshold_then_ban.phpt @@ -7,7 +7,6 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; @@ -18,7 +17,6 @@ $config->allow2ban->add( threshold: 3, period: 60, banSeconds: 3600, - key: KeyExtractors::ip(), ); $middleware = phpt_middleware($config); diff --git a/tests/phpt/context/request_context_fail2ban_signal.phpt b/tests/phpt/context/request_context_fail2ban_signal.phpt index b6b2673..8a63385 100644 --- a/tests/phpt/context/request_context_fail2ban_signal.phpt +++ b/tests/phpt/context/request_context_fail2ban_signal.phpt @@ -8,7 +8,6 @@ require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; use Flowd\Phirewall\Context\RequestContext; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; use Nyholm\Psr7\Response; @@ -25,7 +24,6 @@ $config->fail2ban->add( period: 300, ban: 3600, filter: fn() => false, - key: KeyExtractors::ip(), ); $middleware = phpt_middleware($config); diff --git a/tests/phpt/fail2ban/fail2ban_threshold_then_ban.phpt b/tests/phpt/fail2ban/fail2ban_threshold_then_ban.phpt index 6d7ee62..def3c16 100644 --- a/tests/phpt/fail2ban/fail2ban_threshold_then_ban.phpt +++ b/tests/phpt/fail2ban/fail2ban_threshold_then_ban.phpt @@ -7,7 +7,6 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; @@ -19,7 +18,6 @@ $config->fail2ban->add( period: 300, ban: 3600, filter: fn($request) => $request->getHeaderLine('X-Auth-Failed') === '1', - key: KeyExtractors::ip(), ); $middleware = phpt_middleware($config); diff --git a/tests/phpt/throttle/throttle_dynamic_limit_by_header.phpt b/tests/phpt/throttle/throttle_dynamic_limit_by_header.phpt index 5511045..7336c78 100644 --- a/tests/phpt/throttle/throttle_dynamic_limit_by_header.phpt +++ b/tests/phpt/throttle/throttle_dynamic_limit_by_header.phpt @@ -7,7 +7,6 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; @@ -17,8 +16,7 @@ $config = new Config($cache, clock: $fakeClock); $config->throttles->add( 'role-limit', fn($r) => $r->getHeaderLine('X-User-Role') === 'admin' ? 5 : 2, - 60, - KeyExtractors::ip() + 60 ); $middleware = phpt_middleware($config); diff --git a/tests/phpt/throttle/throttle_fixed_window_allows_then_429.phpt b/tests/phpt/throttle/throttle_fixed_window_allows_then_429.phpt index d60d2e8..72e340e 100644 --- a/tests/phpt/throttle/throttle_fixed_window_allows_then_429.phpt +++ b/tests/phpt/throttle/throttle_fixed_window_allows_then_429.phpt @@ -7,14 +7,13 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; $fakeClock = new FakeClock(1_200_000_000.0); $cache = new InMemoryCache($fakeClock); $config = new Config($cache, clock: $fakeClock); -$config->throttles->add('limit', 3, 60, KeyExtractors::ip()); +$config->throttles->add('limit', 3, 60); $middleware = phpt_middleware($config); $handler = phpt_handler(); diff --git a/tests/phpt/throttle/throttle_multi_window_burst_blocks.phpt b/tests/phpt/throttle/throttle_multi_window_burst_blocks.phpt index 233849f..8b72854 100644 --- a/tests/phpt/throttle/throttle_multi_window_burst_blocks.phpt +++ b/tests/phpt/throttle/throttle_multi_window_burst_blocks.phpt @@ -7,7 +7,6 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; @@ -15,7 +14,7 @@ $fakeClock = new FakeClock(1_200_000_000.0); $cache = new InMemoryCache($fakeClock); $config = new Config($cache, clock: $fakeClock); // Burst limit: 2 per 10s, sustained: 100 per 60s -$config->throttles->multi('api', [10 => 2, 60 => 100], KeyExtractors::ip()); +$config->throttles->multi('api', [10 => 2, 60 => 100]); $middleware = phpt_middleware($config); $handler = phpt_handler(); diff --git a/tests/phpt/throttle/throttle_sliding_window_allows_then_429.phpt b/tests/phpt/throttle/throttle_sliding_window_allows_then_429.phpt index 40d73cc..bfd6623 100644 --- a/tests/phpt/throttle/throttle_sliding_window_allows_then_429.phpt +++ b/tests/phpt/throttle/throttle_sliding_window_allows_then_429.phpt @@ -7,14 +7,13 @@ declare(strict_types=1); require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; $fakeClock = new FakeClock(1_200_000_000.0); $cache = new InMemoryCache($fakeClock); $config = new Config($cache, clock: $fakeClock); -$config->throttles->sliding('limit', 3, 60, KeyExtractors::ip()); +$config->throttles->sliding('limit', 3, 60); $middleware = phpt_middleware($config); $handler = phpt_handler(); diff --git a/tests/phpt/track/track_threshold_flag.phpt b/tests/phpt/track/track_threshold_flag.phpt index 9301ed2..2120a24 100644 --- a/tests/phpt/track/track_threshold_flag.phpt +++ b/tests/phpt/track/track_threshold_flag.phpt @@ -8,7 +8,6 @@ require __DIR__ . '/../_bootstrap.inc'; use Flowd\Phirewall\Config; use Flowd\Phirewall\Events\TrackHit; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Flowd\Phirewall\Tests\Support\FakeClock; use Psr\EventDispatcher\EventDispatcherInterface; @@ -32,7 +31,6 @@ $config->tracks->add( 'api', period: 60, filter: fn() => true, - key: KeyExtractors::ip(), limit: 3, );