From ea11d4674c6781f23a15549006c84b562f63d18d Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 19 Sep 2024 10:53:48 +0800 Subject: [PATCH] MDL-82814 router: Handle ssl and reverse proxies in router When a terminating SSL proxy is in use, the scheme and port received by the web server may be different to the wwwroot. When a reverse proxy is in use, the scheme, port, and host name received by the web server may be different to the wwwroot. In both of these cases the incoming request URI must be updated to use the correct host scheme, and port. In the case of the reverse proxy, the host name must also be updated. In both cases the actual path is expected to be correct, and neither the path nor query parameters should be modified. --- .../moodle_bootstrap_middleware.php | 49 ++++- .../moodle_bootstrap_middleware_test.php | 197 +++++++++++++++++- 2 files changed, 235 insertions(+), 11 deletions(-) diff --git a/lib/classes/router/middleware/moodle_bootstrap_middleware.php b/lib/classes/router/middleware/moodle_bootstrap_middleware.php index 004cfc5f784..c6872d252bb 100644 --- a/lib/classes/router/middleware/moodle_bootstrap_middleware.php +++ b/lib/classes/router/middleware/moodle_bootstrap_middleware.php @@ -33,7 +33,7 @@ use Psr\Http\Server\RequestHandlerInterface; class moodle_bootstrap_middleware implements MiddlewareInterface { #[\Override] public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - global $PAGE; + global $CFG, $PAGE; if (str_contains($request->getUri(), route_loader_interface::ROUTE_GROUP_API)) { // @codeCoverageIgnoreStart @@ -58,7 +58,52 @@ class moodle_bootstrap_middleware implements MiddlewareInterface { $this->load_full_moodle(); } - $PAGE->set_url((string) $request->getUri()); + // Set the URL for the page. + // Normally in Moodle this is a largely hard-coded value with only the query string changing dynamically in the page. + // However, in this instance, we are generating the URL dynamically because we are the request terminator + // for a large number of requests at different endpoints. + + // In basic cases this will just work, but there are some edge cases to consider - specficially were the site + // is behind a reverse proxy and/or an SSL terminator. + // In these cases the URL we generate from the ServerRequestInterface may be the _terminated_ URL, + // and not the URL that was requested by the client. + + // We need to generate the URL that the client requested, not the URL that the server received. + $url = $request->getUri(); + + if (!empty($CFG->reverseproxy)) { + // This site is behind a reverse proxy. The requested URI may have a different: + // - scheme + // - host + // - port + // to the URL that the client requested. + + $url = $url + // Start by setting the scheme and host to the wwwroot. + ->withScheme(parse_url($CFG->wwwroot, PHP_URL_SCHEME)) + ->withHost(parse_url($CFG->wwwroot, PHP_URL_HOST)) + + // Update the URL to match the port of the wwwroot. + // While it is highly unlikely that a wwwroot includes an explicit port, we should still handle it. + ->withPort(parse_url($CFG->wwwroot, PHP_URL_PORT)); + + } + + if (!empty($CFG->sslproxy)) { + // This site is behind an ssl terminating proxy. The requested URI may have a different: + // - scheme + // - port + // to the URL that the client requested. + $url = $url + // The wwwroot must use the https scheme, but the terminating request may have been received using http. + ->withScheme('https') + + // Update the URL to match the port of the wwwroot. + // While it is highly unlikely that a wwwroot includes an explicit port, we should still handle it. + ->withPort(parse_url($CFG->wwwroot, PHP_URL_PORT)); + } + + $PAGE->set_url((string) $url); return $handler->handle($request); } diff --git a/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php b/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php index 078a5a295ba..323a3b9e911 100644 --- a/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php +++ b/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php @@ -17,8 +17,10 @@ namespace core\router\middleware; use core\di; +use core\url; use core\tests\route_testcase; use GuzzleHttp\Psr7\ServerRequest; +use Slim\Exception\HttpNotFoundException; /** * Tests for the Moodle Bootstrap middleware. @@ -30,21 +32,198 @@ use GuzzleHttp\Psr7\ServerRequest; * @covers \core\router\middleware\moodle_bootstrap_middleware */ final class moodle_bootstrap_middleware_test extends route_testcase { - public function test_set_page_to_uri(): void { - global $PAGE; + /** + * Test setting of the page URI based on the request URI. + * + * @dataProvider page_url_provider + * + * @param string $pattern The pattern to register with the app. + * @param string $basepath The basepath of the wwwroot. + * @param string $uri The URI to test. + * @param array $cfg The configuration to use. + * @param string|false $expected The expected page URI, or false if no page URI is expected. + */ + public function test_set_page_to_uri( + string $pattern, + string $basepath, + string $uri, + array $cfg, + string|false $expected + ): void { + global $CFG, $PAGE; + + $this->resetAfterTest(); + + foreach ($cfg as $key => $value) { + $CFG->{$key} = $value; + } + $app = $this->get_simple_app(); + $app->setBasePath($basepath); $app->add(di::get(moodle_bootstrap_middleware::class)); $app->addRoutingMiddleware(); - $app->map(['GET'], '/example', function ($request, $response) { - return $response; - }); + $app->map( + methods: ['GET'], + pattern: $pattern, + callable: fn ($request, $response) => $response, + ); // Handle the request. - $request = new ServerRequest('GET', '/example'); - $app->handle($request); + $request = new ServerRequest('GET', $uri); - $expect = new \moodle_url('/example'); - $this->assertEquals($expect->out(), $PAGE->url->out()); + if ($expected) { + $app->handle($request); + $expect = new url($expected); + $this->assertEquals($expect->out(), $PAGE->url->out()); + } else { + $this->expectException(HttpNotFoundException::class); + $app->handle($request); + } + } + + /** + * Data provider for test_set_page_to_uri. + * + * @return array + */ + public static function page_url_provider(): array { + return [ + 'A basic URI' => [ + 'pattern' => '/example', + 'basepath' => '', + 'uri' => '/example', + 'cfg' => [], + 'expected' => '/example', + ], + 'A basic URI including wwwroot' => [ + 'pattern' => '/example', + 'basepath' => '/example/path', + 'uri' => "https://example.com/example/path/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/example/path', + ], + 'expected' => '/example', + ], + 'A basic URI with query parameters' => [ + 'pattern' => '/example', + 'basepath' => '', + 'uri' => "https://example.com/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com', + ], + 'expected' => "https://example.com/example?foo=bar&baz=qux", + ], + 'A request behind a terminating SSL proxy' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a terminating SSL proxy with query parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a terminating SSL proxy and random port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a terminating SSL proxy and random port where the wwwroot has an explicit port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example', + ], + 'A request behind a terminating SSL proxy where the wwwroot has an explicity port and query parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a SSL proxy proxy with different path is invalid' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => false, + ], + 'A request behind a reverse proxy' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a reverse proxy with query params' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a reverse proxy with different path is invalid' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => false, + ], + 'A request behind a reverse proxy where the wwwroot has an explicit port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example', + ], + 'A request behind a reverse proxy where the wwwroot has an explicit port and parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example?foo=bar&baz=qux', + ], + ]; } }