feature #37734 [HttpFoundation] add support for X_FORWARDED_PREFIX header (jeff1985)

This PR was squashed before being merged into the 5.2-dev branch.

Discussion
----------

[HttpFoundation] add support for X_FORWARDED_PREFIX header

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #36809
| License       | MIT

Add support for `X-Forwarded-Prefix` header added by the popular Traefik HTTP LoadBalancer and Reverse Proxy. This ensures that the links rendered by symfony application deployed behind LB are valid even if the application is deployed via prefix URL.

Example routing setup:
route `/admin/(.*)` => symfony backend `/$1`
in this case links rendered by symfony backend must start with `/admin/`

To accept traefik prefix in your symfony app, you must modify index.php to allow accepting this header:

    Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_TRAEFIK ^ Request::HEADER_X_FORWARDED_HOST );`

Commits
-------

109e0a9f1a [HttpFoundation] add support for X_FORWARDED_PREFIX header
This commit is contained in:
Fabien Potencier 2020-08-22 08:37:35 +02:00
commit c281867227
3 changed files with 81 additions and 9 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
5.3.0
-----
* added support for `X-Forwarded-Prefix` header
5.2.0
-----

View File

@ -39,13 +39,16 @@ class_exists(ServerBag::class);
*/
class Request
{
const HEADER_FORWARDED = 0b00001; // When using RFC 7239
const HEADER_X_FORWARDED_FOR = 0b00010;
const HEADER_X_FORWARDED_HOST = 0b00100;
const HEADER_X_FORWARDED_PROTO = 0b01000;
const HEADER_X_FORWARDED_PORT = 0b10000;
const HEADER_X_FORWARDED_ALL = 0b11110; // All "X-Forwarded-*" headers
const HEADER_X_FORWARDED_AWS_ELB = 0b11010; // AWS ELB doesn't send X-Forwarded-Host
const HEADER_FORWARDED = 0b000001; // When using RFC 7239
const HEADER_X_FORWARDED_FOR = 0b000010;
const HEADER_X_FORWARDED_HOST = 0b000100;
const HEADER_X_FORWARDED_PROTO = 0b001000;
const HEADER_X_FORWARDED_PORT = 0b010000;
const HEADER_X_FORWARDED_PREFIX = 0b100000;
const HEADER_X_FORWARDED_ALL = 0b011110; // All "X-Forwarded-*" headers sent by "usual" reverse proxy
const HEADER_X_FORWARDED_AWS_ELB = 0b011010; // AWS ELB doesn't send X-Forwarded-Host
const HEADER_X_FORWARDED_TRAEFIK = 0b111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy
const METHOD_HEAD = 'HEAD';
const METHOD_GET = 'GET';
@ -237,6 +240,7 @@ class Request
self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
];
/**
@ -894,6 +898,24 @@ class Request
* @return string The raw URL (i.e. not urldecoded)
*/
public function getBaseUrl()
{
$trustedPrefix = '';
// the proxy prefix must be prepended to any prefix being needed at the webserver level
if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) {
$trustedPrefix = rtrim($trustedPrefixValues[0], '/');
}
return $trustedPrefix.$this->getBaseUrlReal();
}
/**
* Returns the real base URL received by the webserver from which this request is executed.
* The URL does not include trusted reverse proxy prefix.
*
* @return string The raw URL (i.e. not urldecoded)
*/
private function getBaseUrlReal()
{
if (null === $this->baseUrl) {
$this->baseUrl = $this->prepareBaseUrl();
@ -1910,7 +1932,7 @@ class Request
$requestUri = '/'.$requestUri;
}
if (null === ($baseUrl = $this->getBaseUrl())) {
if (null === ($baseUrl = $this->getBaseUrlReal())) {
return $requestUri;
}
@ -2014,7 +2036,7 @@ class Request
}
}
if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::$forwardedParams[$type])) && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
$forwarded = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
$parts = HeaderUtils::split($forwarded, ',;=');
$forwardedValues = [];

View File

@ -2278,6 +2278,51 @@ class RequestTest extends TestCase
$this->assertSame(443, $request->getPort());
}
public function testTrustedPrefix()
{
Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_TRAEFIK);
//test with index deployed under root
$request = Request::create('/method');
$request->server->set('REMOTE_ADDR', '1.1.1.1');
$request->headers->set('X-Forwarded-Prefix', '/myprefix');
$request->headers->set('Forwarded', 'host=localhost:8080');
$this->assertSame('/myprefix', $request->getBaseUrl());
$this->assertSame('/myprefix', $request->getBasePath());
$this->assertSame('/method', $request->getPathInfo());
}
public function testTrustedPrefixWithSubdir()
{
Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_TRAEFIK);
$server = [
'SCRIPT_FILENAME' => '/var/hidden/app/public/public/index.php',
'SCRIPT_NAME' => '/public/index.php',
'PHP_SELF' => '/public/index.php',
];
//test with index file deployed in subdir, i.e. local dev server (insecure!!)
$request = Request::create('/public/method', 'GET', [], [], [], $server);
$request->server->set('REMOTE_ADDR', '1.1.1.1');
$request->headers->set('X-Forwarded-Prefix', '/prefix');
$request->headers->set('Forwarded', 'host=localhost:8080');
$this->assertSame('/prefix/public', $request->getBaseUrl());
$this->assertSame('/prefix/public', $request->getBasePath());
$this->assertSame('/method', $request->getPathInfo());
}
public function testTrustedPrefixEmpty()
{
//check that there is no error, if no prefix is provided
Request::setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_TRAEFIK);
$request = Request::create('/method');
$request->server->set('REMOTE_ADDR', '1.1.1.1');
$this->assertSame('', $request->getBaseUrl());
}
public function testTrustedPort()
{
Request::setTrustedProxies(['1.1.1.1'], -1);