-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IBX-4046: Allowed custom header name to be used in reverse proxy env (#…
…165) For more details see https://issues.ibexa.co/browse/IBX-4046
- Loading branch information
1 parent
1cf713a
commit 7a117a7
Showing
4 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
src/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
declare(strict_types=1); | ||
|
||
namespace Ibexa\Bundle\Core\EventSubscriber; | ||
|
||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Event\RequestEvent; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
|
||
final class TrustedHeaderClientIpEventSubscriber implements EventSubscriberInterface | ||
{ | ||
private const PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP = 'X-Client-IP'; | ||
|
||
private ?string $trustedHeaderName; | ||
|
||
public function __construct( | ||
?string $trustedHeaderName | ||
) { | ||
$this->trustedHeaderName = $trustedHeaderName; | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
return [ | ||
KernelEvents::REQUEST => ['onKernelRequest', PHP_INT_MAX], | ||
]; | ||
} | ||
|
||
public function onKernelRequest(RequestEvent $event): void | ||
{ | ||
$request = $event->getRequest(); | ||
|
||
$trustedProxies = Request::getTrustedProxies(); | ||
$trustedHeaderSet = Request::getTrustedHeaderSet(); | ||
|
||
$trustedHeaderName = $this->trustedHeaderName; | ||
if (null === $trustedHeaderName && $this->isPlatformShProxy($request)) { | ||
$trustedHeaderName = self::PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP; | ||
} | ||
|
||
if (null === $trustedHeaderName) { | ||
return; | ||
} | ||
|
||
$trustedClientIp = $request->headers->get($trustedHeaderName); | ||
|
||
if (null !== $trustedClientIp) { | ||
if ($trustedHeaderSet !== -1) { | ||
$trustedHeaderSet |= Request::HEADER_X_FORWARDED_FOR; | ||
} | ||
$request->headers->set('X_FORWARDED_FOR', $trustedClientIp); | ||
} | ||
|
||
Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); | ||
} | ||
|
||
private function isPlatformShProxy(Request $request): bool | ||
{ | ||
return null !== $request->server->get('PLATFORM_RELATIONSHIPS'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
175 changes: 175 additions & 0 deletions
175
tests/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriberTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
<?php | ||
|
||
/** | ||
* @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
* @license For full copyright and license information view LICENSE file distributed with this source code. | ||
*/ | ||
declare(strict_types=1); | ||
|
||
namespace Ibexa\Tests\Bundle\Core\EventSubscriber; | ||
|
||
use Ibexa\Bundle\Core\EventSubscriber\TrustedHeaderClientIpEventSubscriber; | ||
use PHPUnit\Framework\TestCase; | ||
use Symfony\Component\EventDispatcher\EventDispatcher; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Event\RequestEvent; | ||
use Symfony\Component\HttpKernel\HttpKernelInterface; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
use Symfony\Component\HttpKernel\KernelInterface; | ||
|
||
final class TrustedHeaderClientIpEventSubscriberTest extends TestCase | ||
{ | ||
private const PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP = 'X-Client-IP'; | ||
|
||
private ?string $originalRemoteAddr; | ||
|
||
private const PROXY_IP = '127.100.100.1'; | ||
|
||
private const REAL_CLIENT_IP = '98.76.123.234'; | ||
|
||
private const CUSTOM_CLIENT_IP = '234.123.78.98'; | ||
|
||
public function __construct($name = null, array $data = [], $dataName = '') | ||
{ | ||
parent::__construct($name, $data, $dataName); | ||
|
||
$this->originalRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? null; | ||
} | ||
|
||
protected function setUp(): void | ||
{ | ||
$_SERVER['REMOTE_ADDR'] = null; | ||
Request::setTrustedProxies([], -1); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
$_SERVER['REMOTE_ADDR'] = $this->originalRemoteAddr; | ||
} | ||
|
||
public function getTrustedHeaderEventSubscriberTestData(): array | ||
{ | ||
return [ | ||
'default behaviour' => [ | ||
self::REAL_CLIENT_IP, | ||
self::REAL_CLIENT_IP, | ||
], | ||
'use custom header name with valid value' => [ | ||
self::REAL_CLIENT_IP, | ||
self::PROXY_IP, | ||
'X-Custom-Header', | ||
['X-Custom-Header' => self::REAL_CLIENT_IP], | ||
], | ||
'use custom header name without valid value' => [ | ||
self::PROXY_IP, | ||
self::PROXY_IP, | ||
'X-Custom-Header', | ||
], | ||
'use custom header value without custom header name' => [ | ||
self::PROXY_IP, | ||
self::PROXY_IP, | ||
null, | ||
['X-Custom-Header' => self::REAL_CLIENT_IP], | ||
], | ||
'default platform.sh behaviour' => [ | ||
self::REAL_CLIENT_IP, | ||
self::PROXY_IP, | ||
null, | ||
['X-Client-IP' => self::REAL_CLIENT_IP], | ||
['PLATFORM_RELATIONSHIPS' => true], | ||
], | ||
'use custom header name without valid value on platform.sh' => [ | ||
self::PROXY_IP, | ||
self::PROXY_IP, | ||
'X-Custom-Header', | ||
[self::PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP => self::REAL_CLIENT_IP], | ||
['PLATFORM_RELATIONSHIPS' => true], | ||
], | ||
'use custom header with valid value on platform.sh' => [ | ||
self::CUSTOM_CLIENT_IP, | ||
self::PROXY_IP, | ||
'X-Custom-Header', | ||
[ | ||
self::PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP => self::REAL_CLIENT_IP, | ||
'X-Custom-Header' => self::CUSTOM_CLIENT_IP, | ||
], | ||
['PLATFORM_RELATIONSHIPS' => true], | ||
], | ||
'use valid value without custom header name on platform.sh' => [ | ||
self::REAL_CLIENT_IP, | ||
self::PROXY_IP, | ||
null, | ||
[ | ||
self::PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP => self::REAL_CLIENT_IP, | ||
'X-Custom-Header' => self::CUSTOM_CLIENT_IP, | ||
], | ||
['PLATFORM_RELATIONSHIPS' => true], | ||
], | ||
]; | ||
} | ||
|
||
public function testTrustedHeaderEventSubscriberWithoutTrustedProxy(): void | ||
{ | ||
$_SERVER['REMOTE_ADDR'] = self::PROXY_IP; | ||
|
||
$eventDispatcher = new EventDispatcher(); | ||
$eventDispatcher->addSubscriber( | ||
new TrustedHeaderClientIpEventSubscriber('X-Custom-Header') | ||
); | ||
|
||
$request = Request::create('/', 'GET', [], [], [], array_merge( | ||
$_SERVER, | ||
['PLATFORM_RELATIONSHIPS' => true], | ||
)); | ||
$request->headers->add([ | ||
'X-Custom-Header' => self::REAL_CLIENT_IP, | ||
]); | ||
|
||
$event = $eventDispatcher->dispatch(new RequestEvent( | ||
self::createMock(KernelInterface::class), | ||
$request, | ||
HttpKernelInterface::MAIN_REQUEST | ||
), KernelEvents::REQUEST); | ||
|
||
/** @var \Symfony\Component\HttpFoundation\Request $request */ | ||
$request = $event->getRequest(); | ||
|
||
self::assertEquals(self::PROXY_IP, $request->getClientIp()); | ||
} | ||
|
||
/** | ||
* @dataProvider getTrustedHeaderEventSubscriberTestData | ||
*/ | ||
public function testTrustedHeaderEventSubscriberWithTrustedProxy( | ||
string $expectedIp, | ||
string $remoteAddrIp, | ||
?string $trustedHeaderName = null, | ||
array $headers = [], | ||
array $server = [] | ||
): void { | ||
$_SERVER['REMOTE_ADDR'] = $remoteAddrIp; | ||
Request::setTrustedProxies(['REMOTE_ADDR'], Request::getTrustedHeaderSet()); | ||
|
||
$eventDispatcher = new EventDispatcher(); | ||
$eventDispatcher->addSubscriber( | ||
new TrustedHeaderClientIpEventSubscriber($trustedHeaderName) | ||
); | ||
|
||
$request = Request::create('/', 'GET', [], [], [], array_merge( | ||
$server, | ||
['REMOTE_ADDR' => $remoteAddrIp], | ||
)); | ||
$request->headers->add($headers); | ||
|
||
$event = $eventDispatcher->dispatch(new RequestEvent( | ||
self::createMock(KernelInterface::class), | ||
$request, | ||
HttpKernelInterface::MAIN_REQUEST | ||
), KernelEvents::REQUEST); | ||
|
||
/** @var \Symfony\Component\HttpFoundation\Request $request */ | ||
$request = $event->getRequest(); | ||
|
||
self::assertEquals($expectedIp, $request->getClientIp()); | ||
} | ||
} |