Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-4046: Allow custom header name to be used in reverse proxy env #165

Merged
merged 4 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

class TrustedHeaderClientIpEventSubscriber implements EventSubscriberInterface
Nattfarinn marked this conversation as resolved.
Show resolved Hide resolved
{
public const PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP = 'X-Client-IP';
Nattfarinn marked this conversation as resolved.
Show resolved Hide resolved

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"

trusted_header_client_ip_name: ~
Nattfarinn marked this conversation as resolved.
Show resolved Hide resolved

###
# 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: '%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,173 @@
<?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 ?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',
[TrustedHeaderClientIpEventSubscriber::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',
[
TrustedHeaderClientIpEventSubscriber::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,
[
TrustedHeaderClientIpEventSubscriber::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());
}
}