diff --git a/src/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php b/src/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php new file mode 100644 index 0000000000..cff112d523 --- /dev/null +++ b/src/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php @@ -0,0 +1,67 @@ +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'); + } +} diff --git a/src/bundle/Core/Resources/config/default_settings.yml b/src/bundle/Core/Resources/config/default_settings.yml index 4ad50ca8db..cf1718dbe7 100644 --- a/src/bundle/Core/Resources/config/default_settings.yml +++ b/src/bundle/Core/Resources/config/default_settings.yml @@ -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 ### diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index edb5e9f974..bba5a01dc8 100644 --- a/src/bundle/Core/Resources/config/services.yml +++ b/src/bundle/Core/Resources/config/services.yml @@ -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: diff --git a/tests/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriberTest.php b/tests/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriberTest.php new file mode 100644 index 0000000000..db84d93207 --- /dev/null +++ b/tests/bundle/Core/EventSubscriber/TrustedHeaderClientIpEventSubscriberTest.php @@ -0,0 +1,175 @@ +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()); + } +}