diff --git a/src/http/Request.php b/src/http/Request.php index c32d683f..43a0a2ff 100644 --- a/src/http/Request.php +++ b/src/http/Request.php @@ -17,6 +17,7 @@ use function explode; use function filter_var; use function is_array; +use function is_numeric; use function is_string; use function mb_check_encoding; use function mb_substr; @@ -624,6 +625,56 @@ public function getServerParams(): array return $_SERVER; } + /** + * Retrieves the server port number for the current request, supporting PSR-7 and Yii2 fallback. + * + * Returns the port number as determined by the PSR-7 adapter if present, checking configured port headers and + * falling back to the 'SERVER_PORT' server parameter if no header is found. + * + * If no adapter is set, this method falls back to the parent implementation. + * + * This enables seamless access to the server port in both PSR-7 and Yii2 environments, supporting interoperability + * with modern HTTP stacks and legacy workflows. + * + * @return int|null Server port number, or `null` if unavailable. + * + * Usage example: + * ```php + * $port = $request->getServerPort(); + * ``` + */ + public function getServerPort(): int|null + { + if ($this->adapter !== null) { + $headers = $this->getHeaders(); + + foreach ($this->portHeaders as $portHeader) { + if ($headers->has($portHeader)) { + $headerPort = $headers->get($portHeader); + + if (is_string($headerPort)) { + $ports = explode(',', $headerPort); + $firstPort = trim($ports[0]); + + if (is_numeric($firstPort)) { + $port = (int) $firstPort; + + if ($port >= 1 && $port <= 65535) { + return $port; + } + } + } + } + } + + $port = $this->getServerParam('SERVER_PORT'); + + return is_numeric($port) ? (int) $port : null; + } + + return parent::getServerPort(); + } + /** * Retrieves uploaded files from the current request, supporting PSR-7 and Yii2 fallback. * diff --git a/tests/adapter/ServerParamsPsr7Test.php b/tests/adapter/ServerParamsPsr7Test.php index f2b24e31..28fa8572 100644 --- a/tests/adapter/ServerParamsPsr7Test.php +++ b/tests/adapter/ServerParamsPsr7Test.php @@ -8,6 +8,7 @@ use yii\base\InvalidConfigException; use yii2\extensions\psrbridge\http\Request; use yii2\extensions\psrbridge\tests\provider\RequestProvider; +use yii2\extensions\psrbridge\tests\provider\ServerParamsPsr7Provider; use yii2\extensions\psrbridge\tests\support\FactoryHelper; use yii2\extensions\psrbridge\tests\TestCase; @@ -495,4 +496,110 @@ public function testServerNameIndependentRequestsWithDifferentServerNames(): voi 'Independent request instances should return different server names when configured with different values.', ); } + + #[Group('server-port')] + public function testServerPortAfterRequestReset(): void + { + $initialPort = 8080; + $newPort = 9090; + + $request = new Request(); + + $request->setPsr7Request( + FactoryHelper::createRequest('GET', '/test', serverParams: ['SERVER_PORT' => $initialPort]), + ); + + $result1 = $request->getServerPort(); + + self::assertSame( + $initialPort, + $result1, + "'SERVER_PORT' should return '{$initialPort}' from initial PSR-7 request.", + ); + + $request->reset(); + + $request->setPsr7Request( + FactoryHelper::createRequest('GET', '/test', serverParams: ['SERVER_PORT' => $newPort]), + ); + + $result2 = $request->getServerPort(); + + self::assertSame( + $newPort, + $result2, + "'SERVER_PORT' should return '{$newPort}' from new PSR-7 request after 'reset' method.", + ); + self::assertNotSame( + $result1, + $result2, + "'SERVER_PORT' should change after request 'reset' method and new PSR-7 request assignment.", + ); + } + + /** + * @phpstan-param array $requestConfig + * @phpstan-param array $serverGlobal + * @phpstan-param array|int|string> $headers + * @phpstan-param array $serverParams + */ + #[DataProviderExternal(ServerParamsPsr7Provider::class, 'serverPortCases')] + #[Group('server-port')] + public function testServerPortCases( + array $requestConfig, + array $serverGlobal, + array $headers, + array $serverParams, + int|null $expected, + string $message, + ): void { + $_SERVER = $serverGlobal; + + $request = new Request($requestConfig); + + $request->setPsr7Request( + FactoryHelper::createRequest('GET', '/test', $headers, serverParams: $serverParams), + ); + + self::assertSame($expected, $request->getServerPort(), $message); + } + + #[Group('server-port')] + public function testServerPortIndependentRequestsWithDifferentPorts(): void + { + $port1 = 8080; + $port2 = 443; + + $request1 = new Request(); + + $request1->setPsr7Request( + FactoryHelper::createRequest('GET', '/test1', serverParams: ['SERVER_PORT' => $port1]), + ); + + $request2 = new Request(); + + $request2->setPsr7Request( + FactoryHelper::createRequest('GET', '/test2', serverParams: ['SERVER_PORT' => $port2]), + ); + + $result1 = $request1->getServerPort(); + $result2 = $request2->getServerPort(); + + self::assertSame( + $port1, + $result1, + "First request should return '{$port1}' from its PSR-7 'serverParams'.", + ); + self::assertSame( + $port2, + $result2, + "Second request should return '{$port2}' from its PSR-7 'serverParams'.", + ); + self::assertNotSame( + $result1, + $result2, + "Independent request instances should return different 'SERVER_PORT' when configured with different " . + 'values.', + ); + } } diff --git a/tests/provider/ServerParamsPsr7Provider.php b/tests/provider/ServerParamsPsr7Provider.php new file mode 100644 index 00000000..89268ce3 --- /dev/null +++ b/tests/provider/ServerParamsPsr7Provider.php @@ -0,0 +1,182 @@ +, + * array, + * array|int|string>, + * array, + * int|null, + * string, + * } + * > + */ + public static function serverPortCases(): array + { + return [ + 'Forwarded port when request from trusted proxy' => [ + [ + 'portHeaders' => ['X-Forwarded-Port'], + 'trustedHosts' => ['10.0.0.0/24'], // trust this subnet + ], + ['REMOTE_ADDR' => '10.0.0.1'], + ['X-Forwarded-Port' => '443'], + [ + 'SERVER_PORT' => '8080', + 'REMOTE_ADDR' => '10.0.0.1', + ], + 443, + "'getServerPort()' should return forwarded port when request comes from trusted proxy.", + ], + 'Ignore forwarded port when request from untrusted host' => [ + [ + 'portHeaders' => ['X-Forwarded-Port'], + 'secureHeaders' => ['X-Forwarded-Port'], + 'trustedHosts' => ['10.0.0.0/24'], // only trust this subnet + ], + ['REMOTE_ADDR' => '192.168.1.100'], + ['X-Forwarded-Port' => '443'], + [ + 'REMOTE_ADDR' => '192.168.1.100', + 'SERVER_PORT' => '8080', + ], + 8080, + "'getServerPort()' should ignore forwarded port header from untrusted hosts and use 'SERVER_PORT'.", + ], + 'Null when PSR-7 request server port is empty array' => [ + [], + [], + [], + ['SERVER_PORT' => []], + null, + "'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " . + 'is an empty array.', + ], + 'Null when PSR-7 request server port is null' => [ + [], + [], + [], + ['SERVER_PORT' => null], + null, + "'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " . + "is 'null'.", + ], + 'Null when PSR-7 request server port is not present' => [ + [], + [], + [], + ['HTTP_HOST' => 'example.com'], + null, + "'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " . + 'is not present.', + ], + 'Null when PSR-7 request server port is not string' => [ + [], + [], + [], + ['SERVER_PORT' => ['invalid' => 'array']], + null, + "'SERVER_PORT' should return 'null' from PSR-7 'serverParams' when adapter is set but 'SERVER_PORT' " . + 'is not a string.', + ], + 'Server port as integer when PSR-7 server port.' => [ + [], + [], + [], + ['SERVER_PORT' => '443'], + 443, + "'getServerPort()' should return integer value when 'SERVER_PORT' is a numeric string.", + ], + 'Server port as integer when PSR-7 server port is numeric string' => [ + [], + [], + [], + ['SERVER_PORT' => '443'], + 443, + "'getServerPort()' should return integer value when 'SERVER_PORT' is a numeric string.", + ], + 'Server port from comma separated forwarded header' => [ + [ + 'portHeaders' => ['X-Forwarded-Port'], + 'secureHeaders' => ['X-Forwarded-Port'], + 'trustedHosts' => ['127.0.0.1'], + ], + ['REMOTE_ADDR' => '127.0.0.1'], + ['X-Forwarded-Port' => '9443, 7443'], + [ + 'REMOTE_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + ], + 9443, + "'getServerPort()' should return the first port from a comma-separated 'X-Forwarded-Port' header.", + ], + 'Server port from first valid forwarded header when multiple configured' => [ + [ + 'portHeaders' => [ + 'X-Custom-Port', + 'X-Forwarded-Port', + 'X-Real-Port', + ], + 'secureHeaders' => [ + 'X-Custom-Port', + 'X-Forwarded-For', + 'X-Forwarded-Host', + 'X-Forwarded-Port', + 'X-Forwarded-Proto', + 'X-Real-Port', + ], + 'trustedHosts' => ['127.0.0.1'], + ], + ['REMOTE_ADDR' => '127.0.0.1'], + [ + 'X-Custom-Port' => '', + 'X-Forwarded-Port' => '9443', + 'X-Real-Port' => '7443', + ], + [ + 'REMOTE_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + ], + 9443, + "'getServerPort()' should return the port from the first valid forwarded header in the configured " . + 'list.', + ], + 'Server port from forwarded header when adapter is set' => [ + [ + 'portHeaders' => ['X-Forwarded-Port'], + 'trustedHosts' => ['127.0.0.1'], + ], + ['REMOTE_ADDR' => '127.0.0.1'], + ['X-Forwarded-Port' => '443'], + [ + 'REMOTE_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '8080', + ], + 443, + "'getServerPort()' should return the port from 'X-Forwarded-Port' header when present, ignoring " . + "'SERVER_PORT' from PSR-7 'serverParams'.", + ], + 'Server port from PSR-7 request when adapter is set and server port present' => [ + [ + 'portHeaders' => [ + 'X-Custom-Port', + 'X-Forwarded-Port', + ], + ], + [], + ['X-Custom-Port' => ''], + ['SERVER_PORT' => '3000'], + 3000, + "'getServerPort()' should fallback to 'SERVER_PORT' when all forwarded headers are 'null' or missing.", + ], + ]; + } +}