From dc46af5ea14a9ba0487c7e524477a6ab42e775d3 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 3 Jan 2022 19:19:47 +0100 Subject: [PATCH] Automatically return allowed HTTP methods for OPTIONS per specific endpoint --- src/App/Router.php | 65 ++++++++++--------- src/Module/Special/Options.php | 34 +++++++++- src/Util/Router/FriendicaGroupCountBased.php | 62 ++++++++++++++++++ tests/src/Module/Special/OptionsTest.php | 20 +++++- .../Router/FriendicaGroupCountBasedTest.php | 28 ++++++++ 5 files changed, 176 insertions(+), 33 deletions(-) create mode 100644 src/Util/Router/FriendicaGroupCountBased.php create mode 100644 tests/src/Util/Router/FriendicaGroupCountBasedTest.php diff --git a/src/App/Router.php b/src/App/Router.php index 2984a8acab..fd1cec362f 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -41,6 +41,7 @@ use Friendica\Module\Special\Options; use Friendica\Network\HTTPException; use Friendica\Network\HTTPException\MethodNotAllowedException; use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Util\Router\FriendicaGroupCountBased; use Psr\Log\LoggerInterface; /** @@ -123,20 +124,18 @@ class Router */ public function __construct(array $server, string $baseRoutesFilepath, L10n $l10n, ICanCache $cache, ICanLock $lock, IManageConfigValues $config, Arguments $args, LoggerInterface $logger, Dice $dice, RouteCollector $routeCollector = null) { - $this->baseRoutesFilepath = $baseRoutesFilepath; - $this->l10n = $l10n; - $this->cache = $cache; - $this->lock = $lock; - $this->args = $args; - $this->config = $config; - $this->dice = $dice; - $this->server = $server; - $this->logger = $logger; + $this->baseRoutesFilepath = $baseRoutesFilepath; + $this->l10n = $l10n; + $this->cache = $cache; + $this->lock = $lock; + $this->args = $args; + $this->config = $config; + $this->dice = $dice; + $this->server = $server; + $this->logger = $logger; $this->dice_profiler_threshold = $config->get('system', 'dice_profiler_threshold', 0); - $this->routeCollector = isset($routeCollector) ? - $routeCollector : - new RouteCollector(new Std(), new GroupCountBased()); + $this->routeCollector = $routeCollector ?? new RouteCollector(new Std(), new GroupCountBased()); if ($this->baseRoutesFilepath && !file_exists($this->baseRoutesFilepath)) { throw new HTTPException\InternalServerErrorException('Routes file path does\'n exist.'); @@ -155,9 +154,7 @@ class Router */ public function loadRoutes(array $routes) { - $routeCollector = (isset($this->routeCollector) ? - $this->routeCollector : - new RouteCollector(new Std(), new GroupCountBased())); + $routeCollector = ($this->routeCollector ?? new RouteCollector(new Std(), new GroupCountBased())); $this->addRoutes($routeCollector, $routes); @@ -175,7 +172,10 @@ class Router if ($this->isGroup($config)) { $this->addGroup($route, $config, $routeCollector); } elseif ($this->isRoute($config)) { - $routeCollector->addRoute($config[1], $route, $config[0]); + // Always add the OPTIONS endpoint to a route + $httpMethods = (array) $config[1]; + $httpMethods[] = Router::OPTIONS; + $routeCollector->addRoute($httpMethods, $route, $config[0]); } else { throw new HTTPException\InternalServerErrorException("Wrong route config for route '" . print_r($route, true) . "'"); } @@ -258,23 +258,26 @@ class Router $cmd = $this->args->getCommand(); $cmd = '/' . ltrim($cmd, '/'); - $dispatcher = new Dispatcher\GroupCountBased($this->getCachedDispatchData()); + $dispatcher = new FriendicaGroupCountBased($this->getCachedDispatchData()); $this->parameters = []; - $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); - if ($routeInfo[0] === Dispatcher::FOUND) { - $moduleClass = $routeInfo[1]; - $this->parameters = $routeInfo[2]; - } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - if ($this->args->getMethod() === static::OPTIONS) { - // Default response for HTTP OPTIONS requests in case there is no special treatment - $moduleClass = Options::class; - } else { + // Check if the HTTP method ist OPTIONS and return the special Options Module with the possible HTTP methods + if ($this->args->getMethod() === static::OPTIONS) { + $routeOptions = $dispatcher->getOptions($cmd); + + $moduleClass = Options::class; + $this->parameters = ['allowedMethods' => $routeOptions]; + } else { + $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); + if ($routeInfo[0] === Dispatcher::FOUND) { + $moduleClass = $routeInfo[1]; + $this->parameters = $routeInfo[2]; + } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); + } else { + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); } - } else { - throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); } return $moduleClass; @@ -374,13 +377,13 @@ class Router */ private function getCachedDispatchData() { - $routerDispatchData = $this->cache->get('routerDispatchData'); + $routerDispatchData = $this->cache->get('routerDispatchData'); $lastRoutesFileModifiedTime = $this->cache->get('lastRoutesFileModifiedTime'); - $forceRecompute = false; + $forceRecompute = false; if ($this->baseRoutesFilepath) { $routesFileModifiedTime = filemtime($this->baseRoutesFilepath); - $forceRecompute = $lastRoutesFileModifiedTime != $routesFileModifiedTime; + $forceRecompute = $lastRoutesFileModifiedTime != $routesFileModifiedTime; } if (!$forceRecompute && $routerDispatchData) { diff --git a/src/Module/Special/Options.php b/src/Module/Special/Options.php index 327f746b0c..ecc9e2e0c7 100644 --- a/src/Module/Special/Options.php +++ b/src/Module/Special/Options.php @@ -1,16 +1,48 @@ . + * + */ namespace Friendica\Module\Special; use Friendica\App\Router; use Friendica\BaseModule; +/** + * Returns the allowed HTTP methods based on the route information + * + * It's a special class which shouldn't be called directly + * + * @see Router::getModuleClass() + */ class Options extends BaseModule { protected function options(array $request = []) { + $allowedMethods = $this->parameters['AllowedMethods'] ?? []; + + if (empty($allowedMethods)) { + $allowedMethods = Router::ALLOWED_METHODS; + } + // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS - $this->response->setHeader(implode(',', Router::ALLOWED_METHODS), 'Allow'); + $this->response->setHeader(implode(',', $allowedMethods), 'Allow'); $this->response->setStatus(204); } } diff --git a/src/Util/Router/FriendicaGroupCountBased.php b/src/Util/Router/FriendicaGroupCountBased.php new file mode 100644 index 0000000000..46e718eff4 --- /dev/null +++ b/src/Util/Router/FriendicaGroupCountBased.php @@ -0,0 +1,62 @@ +. + * + */ + +namespace Friendica\Util\Router; + +use FastRoute\Dispatcher\GroupCountBased; + +/** + * Extends the Fast-Router collector for getting the possible HTTP method options for a given URI + */ +class FriendicaGroupCountBased extends GroupCountBased +{ + /** + * Returns all possible HTTP methods for a given URI + * + * @param $uri + * + * @return array + * + * @todo Distinguish between an invalid route and the asterisk (*) default route + */ + public function getOptions($uri): array + { + $varRouteData = $this->variableRouteData; + + // Find allowed methods for this URI by matching against all other HTTP methods as well + $allowedMethods = []; + + foreach ($this->staticRouteMap as $method => $uriMap) { + if (isset($uriMap[$uri])) { + $allowedMethods[] = $method; + } + } + + foreach ($varRouteData as $method => $routeData) { + $result = $this->dispatchVariableRoute($routeData, $uri); + if ($result[0] === self::FOUND) { + $allowedMethods[] = $method; + } + } + + return $allowedMethods; + } +} diff --git a/tests/src/Module/Special/OptionsTest.php b/tests/src/Module/Special/OptionsTest.php index e6f798de17..98010cd18f 100644 --- a/tests/src/Module/Special/OptionsTest.php +++ b/tests/src/Module/Special/OptionsTest.php @@ -10,7 +10,7 @@ use Friendica\Test\FixtureTest; class OptionsTest extends FixtureTest { - public function testOptions() + public function testOptionsAll() { $this->useHttpMethod(Router::OPTIONS); @@ -25,4 +25,22 @@ class OptionsTest extends FixtureTest ], $response->getHeaders()); self::assertEquals(implode(',', Router::ALLOWED_METHODS), $response->getHeaderLine('Allow')); } + + public function testOptionsSpecific() + { + $this->useHttpMethod(Router::OPTIONS); + + $response = (new Options(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + 'AllowedMethods' => [Router::GET, Router::POST], + ]))->run(); + + self::assertEmpty((string)$response->getBody()); + self::assertEquals(204, $response->getStatusCode()); + self::assertEquals('No Content', $response->getReasonPhrase()); + self::assertEquals([ + 'Allow' => [implode(',', [Router::GET, Router::POST])], + ICanCreateResponses::X_HEADER => ['html'], + ], $response->getHeaders()); + self::assertEquals(implode(',', [Router::GET, Router::POST]), $response->getHeaderLine('Allow')); + } } diff --git a/tests/src/Util/Router/FriendicaGroupCountBasedTest.php b/tests/src/Util/Router/FriendicaGroupCountBasedTest.php new file mode 100644 index 0000000000..62bf0d243a --- /dev/null +++ b/tests/src/Util/Router/FriendicaGroupCountBasedTest.php @@ -0,0 +1,28 @@ +addRoute('GET', '/get', Options::class); + $collector->addRoute('POST', '/post', Options::class); + $collector->addRoute('GET', '/multi', Options::class); + $collector->addRoute('POST', '/multi', Options::class); + + $dispatcher = new FriendicaGroupCountBased($collector->getData()); + + self::assertEquals(['GET'], $dispatcher->getOptions('/get')); + self::assertEquals(['POST'], $dispatcher->getOptions('/post')); + self::assertEquals(['GET', 'POST'], $dispatcher->getOptions('/multi')); + } +} -- 2.39.2