diff --git a/composer.json b/composer.json index 10b034a..2459f2d 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "jetbrains/phpstorm-attributes": "^1.0", "nyholm/psr7": "^1.3", "phpunit/phpunit": "^10.0", + "spiral/dumper": "^3.3", "symfony/process": "^6.2 || ^7.0", "vimeo/psalm": "^5.9" }, diff --git a/src/GlobalState.php b/src/GlobalState.php new file mode 100644 index 0000000..04f87ed --- /dev/null +++ b/src/GlobalState.php @@ -0,0 +1,60 @@ + Cached state of the $_SERVER superglobal. + */ + private static array $cachedServer = []; + + /** + * Cache superglobal $_SERVER to avoid state leaks between requests. + */ + public static function cacheServerVars(): void + { + self::$cachedServer = $_SERVER; + } + + /** + * Enrich cached $_SERVER with data from the request. + * + * @return non-empty-array Cached $_SERVER data enriched with request data. + */ + public static function enrichServerVars(Request $request): array + { + $server = self::$cachedServer; + + $server['REQUEST_URI'] = $request->uri; + $server['REQUEST_TIME'] = time(); + $server['REQUEST_TIME_FLOAT'] = microtime(true); + $server['REMOTE_ADDR'] = $request->getRemoteAddr(); + $server['REQUEST_METHOD'] = $request->method; + $server['HTTP_USER_AGENT'] = ''; + + foreach ($request->headers as $key => $value) { + $key = strtoupper(str_replace('-', '_', $key)); + + if ($key == 'CONTENT_TYPE' || $key == 'CONTENT_LENGTH') { + $server[$key] = implode(', ', $value); + + continue; + } + + $server['HTTP_' . $key] = implode(', ', $value); + } + + return $server; + } +} + +GlobalState::cacheServerVars(); diff --git a/src/PSR7Worker.php b/src/PSR7Worker.php index 209beea..43569b6 100644 --- a/src/PSR7Worker.php +++ b/src/PSR7Worker.php @@ -32,7 +32,6 @@ class PSR7Worker implements PSR7WorkerInterface private readonly HttpWorker $httpWorker; - private readonly array $originalServer; /** * @var string[] Valid values for HTTP protocol version @@ -46,7 +45,6 @@ public function __construct( private readonly UploadedFileFactoryInterface $uploadsFactory, ) { $this->httpWorker = new HttpWorker($worker); - $this->originalServer = $_SERVER; } public function getWorker(): WorkerInterface @@ -60,18 +58,26 @@ public function getHttpWorker(): HttpWorker } /** + * @psalm-suppress DeprecatedMethod + * + * @param bool $populateServer Whether to populate $_SERVER superglobal. + * * @throws \JsonException */ - public function waitRequest(): ?ServerRequestInterface + public function waitRequest(bool $populateServer = true): ?ServerRequestInterface { $httpRequest = $this->httpWorker->waitRequest(); if ($httpRequest === null) { return null; } - $_SERVER = $this->configureServer($httpRequest); + $vars = $this->configureServer($httpRequest); + + if ($populateServer) { + $_SERVER = $vars; + } - return $this->mapRequest($httpRequest, $_SERVER); + return $this->mapRequest($httpRequest, $vars); } /** @@ -92,7 +98,7 @@ public function respond(ResponseInterface $response): void /** * @return Generator Compatible - * with {@see \Spiral\RoadRunner\Http\HttpWorker::respondStream()}. + * with {@see HttpWorker::respondStream}. */ private function streamToGenerator(StreamInterface $stream): Generator { @@ -125,32 +131,20 @@ private function streamToGenerator(StreamInterface $stream): Generator */ protected function configureServer(Request $request): array { - $server = $this->originalServer; - - $server['REQUEST_URI'] = $request->uri; - $server['REQUEST_TIME'] = $this->timeInt(); - $server['REQUEST_TIME_FLOAT'] = $this->timeFloat(); - $server['REMOTE_ADDR'] = $request->getRemoteAddr(); - $server['REQUEST_METHOD'] = $request->method; - - $server['HTTP_USER_AGENT'] = ''; - foreach ($request->headers as $key => $value) { - $key = \strtoupper(\str_replace('-', '_', $key)); - if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) { - $server[$key] = \implode(', ', $value); - } else { - $server['HTTP_' . $key] = \implode(', ', $value); - } - } - - return $server; + return GlobalState::enrichServerVars($request); } + /** + * @deprecated + */ protected function timeInt(): int { return \time(); } + /** + * @deprecated + */ protected function timeFloat(): float { return \microtime(true); diff --git a/tests/Unit/PSR7WorkerTest.php b/tests/Unit/PSR7WorkerTest.php index c79d52b..2cbf17e 100644 --- a/tests/Unit/PSR7WorkerTest.php +++ b/tests/Unit/PSR7WorkerTest.php @@ -5,27 +5,89 @@ namespace Spiral\RoadRunner\Tests\Http\Unit; use Nyholm\Psr7\Factory\Psr17Factory; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\RunClassInSeparateProcess; use PHPUnit\Framework\TestCase; -use Spiral\RoadRunner\Http\HttpWorker; +use Spiral\Goridge\Frame; +use Spiral\RoadRunner\Http\GlobalState; use Spiral\RoadRunner\Http\PSR7Worker; +use Spiral\RoadRunner\Tests\Http\Unit\Stub\TestRelay; use Spiral\RoadRunner\Worker; + +#[CoversClass(PSR7Worker::class)] +#[CoversClass(GlobalState::class)] +#[RunClassInSeparateProcess] final class PSR7WorkerTest extends TestCase { - public function testHttpWorkerIsAvailable(): void + public function testStateServerLeak(): void { $psrFactory = new Psr17Factory(); - - $psrWorker = new PSR7Worker( - Worker::create(), + $relay = new TestRelay(); + $psrWorker = new PSR7Worker( + new Worker($relay), $psrFactory, $psrFactory, $psrFactory, ); - self::assertInstanceOf(HttpWorker::class, $psrWorker->getHttpWorker()); + //dataProvider is always random and we need to keep the order + $fixtures = [ + [ + [ + 'Content-Type' => ['application/html'], + 'Connection' => ['keep-alive'] + ], + [ + 'REQUEST_URI' => 'http://localhost', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => '', + 'CONTENT_TYPE' => 'application/html', + 'HTTP_CONNECTION' => 'keep-alive', + ], + ], + [ + [ + 'Content-Type' => ['application/json'] + ], + [ + 'REQUEST_URI' => 'http://localhost', + 'REMOTE_ADDR' => '127.0.0.1', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => '', + 'CONTENT_TYPE' => 'application/json' + ], + ], + ]; + + $_SERVER = []; + foreach ($fixtures as [$headers, $expectedServer]) { + $body = [ + 'headers' => $headers, + 'rawQuery' => '', + 'remoteAddr' => '127.0.0.1', + 'protocol' => 'HTTP/1.1', + 'method' => 'GET', + 'uri' => 'http://localhost', + 'parsed' => false, + ]; + + $head = (string)\json_encode($body, \JSON_THROW_ON_ERROR); + $frame = new Frame($head .'test', [\strlen($head)]); + + $relay->addFrames($frame); + + $psrWorker->waitRequest(); + + unset($_SERVER['REQUEST_TIME']); + unset($_SERVER['REQUEST_TIME_FLOAT']); + + self::assertEquals($expectedServer, $_SERVER); + } } + protected function tearDown(): void { // Clean all extra output buffers