Skip to content

Commit

Permalink
IBX-4046: Allowed custom header name to be used in reverse proxy env (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Nattfarinn authored Oct 28, 2022
1 parent 1cf713a commit 7a117a7
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
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');
}
}
2 changes: 2 additions & 0 deletions src/bundle/Core/Resources/config/default_settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ parameters:
# Kernel related params
webroot_dir: "%kernel.project_dir%/public"

ibexa.trusted_header_client_ip_name: ~

###
# ibexa.site_access.config namespace, default scope
###
Expand Down
6 changes: 6 additions & 0 deletions src/bundle/Core/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ services:
tags:
- {name: kernel.event_subscriber}

Ibexa\Bundle\Core\EventSubscriber\TrustedHeaderClientIpEventSubscriber:
arguments:
$trustedHeaderName: '%ibexa.trusted_header_client_ip_name%'
tags:
- {name: kernel.event_subscriber}

Ibexa\Bundle\Core\Command\DeleteContentTranslationCommand:
class: Ibexa\Bundle\Core\Command\DeleteContentTranslationCommand
arguments:
Expand Down
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());
}
}

0 comments on commit 7a117a7

Please sign in to comment.