diff --git a/composer.json b/composer.json index ce376072..43829870 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "prefer-stable": true, "require": { "php": ">=8.1", + "ext-filter": "*", "ext-mbstring": "*", "psr/http-factory": "^1.0", "psr/http-message": "^2.0", diff --git a/src/http/Request.php b/src/http/Request.php index b5418d7d..35ac17db 100644 --- a/src/http/Request.php +++ b/src/http/Request.php @@ -15,6 +15,7 @@ use function base64_decode; use function count; use function explode; +use function filter_var; use function is_array; use function is_string; use function mb_check_encoding; @@ -476,14 +477,15 @@ public function getRemoteHost(): string|null /** * Retrieves the remote IP address for the current request, supporting PSR-7 and Yii2 fallback. * - * Returns the remote IP address as provided by the PSR-7 ServerRequestAdapter if the adapter is set. + * Returns the remote IP address as determined by the PSR-7 adapter if present, using the 'REMOTE_ADDR' server + * parameter. * - * If no adapter is present, falls back to the parent implementation. + * If no adapter is set, falls back to the parent implementation. * * This method enables seamless access to the remote IP address in both PSR-7 and Yii2 environments, supporting * interoperability with modern HTTP stacks and legacy workflows. * - * @return string|null Remote IP address for the current request, or `null` if not available. + * @return string|null Remote IP address for the current request, or `null` if not available or invalid. * * Usage example: * ```php @@ -495,7 +497,15 @@ public function getRemoteIP(): string|null if ($this->adapter !== null) { $remoteIP = $this->getServerParam('REMOTE_ADDR'); - return is_string($remoteIP) ? $remoteIP : null; + if (is_string($remoteIP) === false) { + return null; + } + + if (filter_var($remoteIP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) { + return null; + } + + return $remoteIP; } return parent::getRemoteIP(); diff --git a/tests/adapter/ServerParamsPsr7Test.php b/tests/adapter/ServerParamsPsr7Test.php index b2ac5ddf..b04f11bb 100644 --- a/tests/adapter/ServerParamsPsr7Test.php +++ b/tests/adapter/ServerParamsPsr7Test.php @@ -137,6 +137,53 @@ public function testReturnRemoteHostFromServerParamsCases(mixed $serverValue, st ); } + #[Group('remote-ip')] + public function testReturnRemoteIPFromPsr7ServerParamsOverridesGlobalServer(): void + { + $_SERVER['REMOTE_ADDR'] = '192.168.1.100'; + + $request = new Request(); + + $request->setPsr7Request( + FactoryHelper::createRequest( + 'GET', + 'https://old.example.com/api', + serverParams: ['REMOTE_ADDR' => '10.0.0.1'], + ), + ); + + self::assertSame( + '10.0.0.1', + $request->getRemoteIP(), + "'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams', not from global " . + '$_SERVER.', + ); + } + + #[DataProviderExternal(RequestProvider::class, 'remoteIPCases')] + #[Group('remote-ip')] + public function testReturnRemoteIPFromServerParamsCases(mixed $serverValue, string|null $expected): void + { + $request = new Request(); + + $request->setPsr7Request( + FactoryHelper::createRequest('GET', '/test', serverParams: ['REMOTE_ADDR' => $serverValue]), + ); + + $actual = $request->getRemoteIP(); + + self::assertSame( + $expected, + $actual, + sprintf( + "'getRemoteIP()' should return '%s' when 'REMOTE_ADDR' is '%s' in PSR-7 'serverParams'. Got: '%s'", + var_export($expected, true), + var_export($serverValue, true), + var_export($actual, true), + ), + ); + } + #[DataProviderExternal(RequestProvider::class, 'serverNameCases')] #[Group('server-name')] public function testReturnServerNameFromServerParamsCases(mixed $serverValue, string|null $expected): void diff --git a/tests/adapter/ServerRequestAdapterTest.php b/tests/adapter/ServerRequestAdapterTest.php index 5294ef9d..7cc13541 100644 --- a/tests/adapter/ServerRequestAdapterTest.php +++ b/tests/adapter/ServerRequestAdapterTest.php @@ -1179,47 +1179,6 @@ public function testReturnRawBodyWhenAdapterIsSetWithEmptyBody(): void ); } - public function testReturnRemoteIPFromPsr7ServerParams(): void - { - $request = new Request(); - - $request->setPsr7Request( - FactoryHelper::createRequest( - 'GET', - 'https://old.example.com/api', - serverParams: ['REMOTE_ADDR' => '192.168.1.100'], - ), - ); - - self::assertSame( - '192.168.1.100', - $request->getRemoteIP(), - "'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams'.", - ); - } - - public function testReturnRemoteIPFromPsr7ServerParamsOverridesGlobalServer(): void - { - $_SERVER['REMOTE_ADDR'] = '192.168.1.100'; - - $request = new Request(); - - $request->setPsr7Request( - FactoryHelper::createRequest( - 'GET', - 'https://old.example.com/api', - serverParams: ['REMOTE_ADDR' => '10.0.0.1'], - ), - ); - - self::assertSame( - '10.0.0.1', - $request->getRemoteIP(), - "'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams', not from global " . - '$_SERVER.', - ); - } - /** * @throws InvalidConfigException if the configuration is invalid or incomplete. */ diff --git a/tests/provider/RequestProvider.php b/tests/provider/RequestProvider.php index 830b5017..77d999b1 100644 --- a/tests/provider/RequestProvider.php +++ b/tests/provider/RequestProvider.php @@ -819,6 +819,103 @@ public static function remoteHostCases(): array ]; } + /** + * @phpstan-return array + */ + public static function remoteIPCases(): array + { + return [ + 'boolean-false' => [ + false, + null, + ], + 'boolean-true' => [ + true, + null, + ], + 'empty-array' => [ + [], + null, + ], + 'empty-string' => [ + '', + null, + ], + 'float' => [ + 123.45, + null, + ], + 'integer' => [ + 12345, + null, + ], + 'integer-zero' => [ + 0, + null, + ], + 'invalid-ip' => [ + '999.999.999.999', + null, + ], + 'IPv4' => [ + '192.168.1.100', + '192.168.1.100', + ], + 'ipv4-with-port' => [ + '10.0.0.1:8080', + null, + ], + 'IPv4-local' => [ + '127.0.0.1', + '127.0.0.1', + ], + 'IPv6' => [ + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + ], + 'ipv6-bracketed' => [ + '[::1]', + null, + ], + 'IPv6-compressed' => [ + '::1', + '::1', + ], + 'ipv6-with-port' => [ + '[::1]:443', + null, + ], + 'localhost' => [ + 'localhost', + null, + ], + 'null' => [ + null, + null, + ], + 'numeric string' => [ + '123', + null, + ], + 'object' => [ + (object) ['foo' => 'bar'], + null, + ], + 'spaces-around' => [ + ' 127.0.0.1 ', + null, + ], + 'string-zero' => [ + '0', + null, + ], + 'zero-address' => [ + '0.0.0.0', + '0.0.0.0', + ], + ]; + } + /** * @phpstan-return array */