From d1ea1363d35014788e04ac7d57d62ee9d9acdac1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:44:29 +0000 Subject: [PATCH 1/3] Initial plan From fbf1b1ef49ed85d2d70bdadbde9a8d85143a350b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:47:01 +0000 Subject: [PATCH 2/3] Fix HTTP middleware and legacy event bugs Co-authored-by: asika32764 <1639206+asika32764@users.noreply.github.com> --- .../src/Middleware/FileProcessMiddleware.php | 9 ++++-- packages/http/src/Server/HttpServerTrait.php | 2 +- packages/http/src/Server/ReactServer.php | 2 +- .../Middleware/FileProcessMiddlewareTest.php | 29 +++++++++++++++++++ packages/http/test/Server/HttpServerTest.php | 2 +- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/http/src/Middleware/FileProcessMiddleware.php b/packages/http/src/Middleware/FileProcessMiddleware.php index 3a5f96fcd..c71790716 100644 --- a/packages/http/src/Middleware/FileProcessMiddleware.php +++ b/packages/http/src/Middleware/FileProcessMiddleware.php @@ -38,9 +38,14 @@ public function __construct( */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $path = ltrim($request->getUri()->getPath(), '/'); + $path = rawurldecode($request->getUri()->getPath()); + $path = str_replace('\\', '/', $path); - $file = new SplFileInfo($this->publicPath . '/' . $path); + if (in_array('..', explode('/', $path), true)) { + return $handler->handle($request); + } + + $file = new SplFileInfo($this->publicPath . '/' . ltrim($path, '/')); if (is_file($file->getPathname())) { $segments = explode('.', $file->getPathname()); diff --git a/packages/http/src/Server/HttpServerTrait.php b/packages/http/src/Server/HttpServerTrait.php index 92e5384a1..a36a99a20 100644 --- a/packages/http/src/Server/HttpServerTrait.php +++ b/packages/http/src/Server/HttpServerTrait.php @@ -131,7 +131,7 @@ function (RequestEvent $event) { $this->on( ResponseEvent::class, function (ResponseEvent $event) { - $this->eventPassthrough('reponse', $event); + $this->eventPassthrough('response', $event); }, ); diff --git a/packages/http/src/Server/ReactServer.php b/packages/http/src/Server/ReactServer.php index 789681807..ea5238106 100644 --- a/packages/http/src/Server/ReactServer.php +++ b/packages/http/src/Server/ReactServer.php @@ -21,7 +21,7 @@ use Windwalker\Http\Request\ServerRequest; /** - * The ReactServerAdapter class. + * The ReactServerAdapter class. It dispatches events directly and does not run middleware. */ class ReactServer implements ServerInterface { diff --git a/packages/http/test/Middleware/FileProcessMiddlewareTest.php b/packages/http/test/Middleware/FileProcessMiddlewareTest.php index dc878d667..d202f5d04 100644 --- a/packages/http/test/Middleware/FileProcessMiddlewareTest.php +++ b/packages/http/test/Middleware/FileProcessMiddlewareTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; use Psr\Http\Server\RequestHandlerInterface; use SplFileInfo; use Symfony\Component\Mime\MimeTypes; @@ -114,6 +115,34 @@ public function processDelegatesForDirectories(): void self::assertSame(404, $res->getStatusCode()); } + #[Test] + public function processRejectsParentDirectorySegments(): void + { + $basePath = sys_get_temp_dir() . '/windwalker-http-' . bin2hex(random_bytes(8)); + mkdir($basePath . '/public', recursive: true); + file_put_contents($basePath . '/secret.txt', 'secret'); + + try { + foreach (['/../secret.txt', '/%2e%2e/secret.txt', '/..\\secret.txt', '/..%5csecret.txt'] as $path) { + $uri = $this->createStub(UriInterface::class); + $uri->method('getPath')->willReturn($path); + + $request = $this->createStub(ServerRequestInterface::class); + $request->method('getUri')->willReturn($uri); + + $response = (new FileProcessMiddleware($basePath . '/public')) + ->process($request, $this->handler()); + + self::assertSame(404, $response->getStatusCode()); + self::assertSame('next', (string) $response->getBody()); + } + } finally { + unlink($basePath . '/secret.txt'); + rmdir($basePath . '/public'); + rmdir($basePath); + } + } + #[Test] public function getMimeTypesReturnsInjectedInstanceOrDefault(): void { diff --git a/packages/http/test/Server/HttpServerTest.php b/packages/http/test/Server/HttpServerTest.php index e907a0f93..9716fba44 100644 --- a/packages/http/test/Server/HttpServerTest.php +++ b/packages/http/test/Server/HttpServerTest.php @@ -114,7 +114,7 @@ public function onRequestAndOnResponseListenToLegacyEventNames(): void $server->getAdapter()->handle($this->request()); - self::assertContains('request', $called); + self::assertSame(['request', 'response'], $called); } #[Test] From 85985a6955307640672b3d20165ed8d322fd6491 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:04:21 +0000 Subject: [PATCH 3/3] Add middleware support to ReactServer Co-authored-by: asika32764 <1639206+asika32764@users.noreply.github.com> --- packages/http/src/Server/ReactServer.php | 85 +++++++++++++++++-- packages/http/test/Server/ReactServerTest.php | 30 ++++++- 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/packages/http/src/Server/ReactServer.php b/packages/http/src/Server/ReactServer.php index ea5238106..93db799bf 100644 --- a/packages/http/src/Server/ReactServer.php +++ b/packages/http/src/Server/ReactServer.php @@ -4,6 +4,7 @@ namespace Windwalker\Http\Server; +use Closure; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; @@ -17,16 +18,25 @@ use Windwalker\Http\Event\RequestEvent; use Windwalker\Http\Helper\ResponseHelper; use Windwalker\Http\HttpFactory; +use Windwalker\Http\Middleware\RequestRunner; use Windwalker\Http\Output\Output; use Windwalker\Http\Request\ServerRequest; +use function Windwalker\DI\create; + /** - * The ReactServerAdapter class. It dispatches events directly and does not run middleware. + * The ReactServerAdapter class. */ class ReactServer implements ServerInterface { use EventAwareTrait; + protected array $middlewares = []; + + protected ?Closure $middlewareResolver = null; + + protected ?HttpFactory $httpFactory = null; + public ?LoopInterface $loop = null; /** @@ -142,17 +152,27 @@ protected function createServer(): HttpServer $this->getLoop(), function (ServerRequestInterface $req) use ($output) { try { - $event = $this->emit( - new RequestEvent( - request: $req, - output: $output, - )->setName('request'), - ); + $middlewares = $this->getMiddlewares(); + $middlewares[] = function (ServerRequestInterface $request) use ($output) { + $event = $this->emit( + new RequestEvent( + request: $request, + output: $output, + )->setName('request'), + ); + + return $event->response ?? $this->getHttpFactory()->createResponse(); + }; + + $response = (new RequestRunner( + $middlewares, + $this->getMiddlewareResolver(), + ))->handle($req); } catch (Throwable $e) { $code = $e->getCode(); $code = ResponseHelper::isClientError($code) ? $code : 500; - $res = new HttpFactory()->createResponse($code); + $res = $this->getHttpFactory()->createResponse($code); $res->getBody()->write((string) $e); return $res; @@ -162,7 +182,7 @@ function (ServerRequestInterface $req) use ($output) { new RequestEvent( request: $req, output: $output, - response: $event->response, + response: $response, )->setName('response'), ); @@ -186,6 +206,53 @@ function (Throwable $e) use ($output) { return $server; } + public function middleware(mixed $middleware, mixed ...$args): static + { + if (is_string($middleware)) { + $middleware = create($middleware, ...$args); + } + + $this->middlewares[] = $middleware; + + return $this; + } + + public function getMiddlewares(): array + { + return $this->middlewares; + } + + public function setMiddlewares(array $middlewares): static + { + $this->middlewares = $middlewares; + + return $this; + } + + public function getMiddlewareResolver(): Closure + { + return $this->middlewareResolver ?? static fn($entry) => $entry; + } + + public function setMiddlewareResolver(?Closure $middlewareResolver): static + { + $this->middlewareResolver = $middlewareResolver; + + return $this; + } + + public function getHttpFactory(): HttpFactory + { + return $this->httpFactory ??= new HttpFactory(); + } + + public function setHttpFactory(?HttpFactory $httpFactory): static + { + $this->httpFactory = $httpFactory; + + return $this; + } + /** * @param HttpServer|null $server * diff --git a/packages/http/test/Server/ReactServerTest.php b/packages/http/test/Server/ReactServerTest.php index f55812270..d1cb31a8b 100644 --- a/packages/http/test/Server/ReactServerTest.php +++ b/packages/http/test/Server/ReactServerTest.php @@ -8,15 +8,17 @@ use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Http\HttpServer; +use React\Socket\SocketServer; +use RuntimeException; use Windwalker\Http\Event\ErrorEvent; use Windwalker\Http\Event\RequestEvent; use Windwalker\Http\HttpFactory; -use React\Socket\SocketServer; -use RuntimeException; use Windwalker\Http\Server\ReactServer; /** @@ -184,6 +186,30 @@ function (RequestEvent $event) use (&$names) { self::assertSame(['request', 'response'], $names); } + #[Test] + public function handleRunsMiddlewaresBeforeRequestEvent(): void + { + $this->instance->middleware( + function (ServerRequestInterface $request, RequestHandlerInterface $handler) { + return $handler->handle($request->withAttribute('middleware', 'ran')); + }, + ); + $this->instance->on( + 'request', + function (RequestEvent $event) { + $event->response = $this->instance + ->getHttpFactory() + ->createResponse() + ->withHeader('X-Middleware', $event->request->getAttribute('middleware')); + }, + ); + + $raw = $this->requestRaw("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); + + self::assertStringStartsWith('HTTP/1.1 200', $raw); + self::assertStringContainsString('X-Middleware: ran', $raw); + } + #[Test] public function returnsServerErrorWhenRequestListenerThrows(): void {