Merge branch '2.7' into 2.8
* 2.7: [HttpFoundation] Warning when request has both Forwarded and X-Forwarded-For [Console] Decouple SymfonyStyle from TableCell
This commit is contained in:
commit
0a7aa63ece
@ -46,5 +46,9 @@
|
|||||||
<argument type="service" id="request_stack" />
|
<argument type="service" id="request_stack" />
|
||||||
<tag name="kernel.event_subscriber" />
|
<tag name="kernel.event_subscriber" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="validate_request_listener" class="Symfony\Component\HttpKernel\EventListener\ValidateRequestListener">
|
||||||
|
<tag name="kernel.event_subscriber" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
@ -18,7 +18,6 @@ use Symfony\Component\Console\Helper\Helper;
|
|||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
||||||
use Symfony\Component\Console\Helper\Table;
|
use Symfony\Component\Console\Helper\Table;
|
||||||
use Symfony\Component\Console\Helper\TableCell;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\BufferedOutput;
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@ -185,21 +184,13 @@ class SymfonyStyle extends OutputStyle
|
|||||||
*/
|
*/
|
||||||
public function table(array $headers, array $rows)
|
public function table(array $headers, array $rows)
|
||||||
{
|
{
|
||||||
array_walk_recursive($headers, function (&$value) {
|
$style = clone Table::getStyleDefinition('symfony-style-guide');
|
||||||
if ($value instanceof TableCell) {
|
$style->setCellHeaderFormat('<info>%s</info>');
|
||||||
$value = new TableCell(sprintf('<info>%s</>', $value), array(
|
|
||||||
'colspan' => $value->getColspan(),
|
|
||||||
'rowspan' => $value->getRowspan(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
$value = sprintf('<info>%s</>', $value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$table = new Table($this);
|
$table = new Table($this);
|
||||||
$table->setHeaders($headers);
|
$table->setHeaders($headers);
|
||||||
$table->setRows($rows);
|
$table->setRows($rows);
|
||||||
$table->setStyle('symfony-style-guide');
|
$table->setStyle($style);
|
||||||
|
|
||||||
$table->render();
|
$table->render();
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpFoundation\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP request contains headers with conflicting information.
|
||||||
|
*
|
||||||
|
* This exception should trigger an HTTP 400 response in your application code.
|
||||||
|
*
|
||||||
|
* @author Magnus Nordlander <magnus@fervo.se>
|
||||||
|
*/
|
||||||
|
class ConflictingHeadersException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\HttpFoundation;
|
namespace Symfony\Component\HttpFoundation;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
|
||||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -811,41 +812,34 @@ class Request
|
|||||||
return array($ip);
|
return array($ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
|
$hasTrustedForwardedHeader = self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED]);
|
||||||
|
$hasTrustedClientIpHeader = self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP]);
|
||||||
|
|
||||||
|
if ($hasTrustedForwardedHeader) {
|
||||||
$forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
|
$forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
|
||||||
preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
|
preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
|
||||||
$clientIps = $matches[3];
|
$forwardedClientIps = $matches[3];
|
||||||
} elseif (self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
|
|
||||||
$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
|
$forwardedClientIps = $this->normalizeAndFilterClientIps($forwardedClientIps, $ip);
|
||||||
|
$clientIps = $forwardedClientIps;
|
||||||
}
|
}
|
||||||
|
|
||||||
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
|
if ($hasTrustedClientIpHeader) {
|
||||||
$firstTrustedIp = null;
|
$xForwardedForClientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
|
||||||
|
|
||||||
foreach ($clientIps as $key => $clientIp) {
|
$xForwardedForClientIps = $this->normalizeAndFilterClientIps($xForwardedForClientIps, $ip);
|
||||||
// Remove port (unfortunately, it does happen)
|
$clientIps = $xForwardedForClientIps;
|
||||||
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
|
|
||||||
$clientIps[$key] = $clientIp = $match[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
|
|
||||||
unset($clientIps[$key]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
|
|
||||||
unset($clientIps[$key]);
|
|
||||||
|
|
||||||
// Fallback to this when the client IP falls into the range of trusted proxies
|
|
||||||
if (null === $firstTrustedIp) {
|
|
||||||
$firstTrustedIp = $clientIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now the IP chain contains only untrusted proxies and the client IP
|
if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) {
|
||||||
return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
|
throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure Symfony to distrust one of them.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) {
|
||||||
|
return $this->normalizeAndFilterClientIps(array(), $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clientIps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1936,4 +1930,35 @@ class Request
|
|||||||
{
|
{
|
||||||
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
|
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeAndFilterClientIps(array $clientIps, $ip)
|
||||||
|
{
|
||||||
|
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
|
||||||
|
$firstTrustedIp = null;
|
||||||
|
|
||||||
|
foreach ($clientIps as $key => $clientIp) {
|
||||||
|
// Remove port (unfortunately, it does happen)
|
||||||
|
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
|
||||||
|
$clientIps[$key] = $clientIp = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
|
||||||
|
unset($clientIps[$key]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
|
||||||
|
unset($clientIps[$key]);
|
||||||
|
|
||||||
|
// Fallback to this when the client IP falls into the range of trusted proxies
|
||||||
|
if (null === $firstTrustedIp) {
|
||||||
|
$firstTrustedIp = $clientIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now the IP chain contains only untrusted proxies and the client IP
|
||||||
|
return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -923,6 +923,74 @@ class RequestTest extends \PHPUnit_Framework_TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException
|
||||||
|
* @dataProvider testGetClientIpsWithConflictingHeadersProvider
|
||||||
|
*/
|
||||||
|
public function testGetClientIpsWithConflictingHeaders($httpForwarded, $httpXForwardedFor)
|
||||||
|
{
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
$server = array(
|
||||||
|
'REMOTE_ADDR' => '88.88.88.88',
|
||||||
|
'HTTP_FORWARDED' => $httpForwarded,
|
||||||
|
'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor,
|
||||||
|
);
|
||||||
|
|
||||||
|
Request::setTrustedProxies(array('88.88.88.88'));
|
||||||
|
|
||||||
|
$request->initialize(array(), array(), array(), array(), array(), $server);
|
||||||
|
|
||||||
|
$request->getClientIps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetClientIpsWithConflictingHeadersProvider()
|
||||||
|
{
|
||||||
|
// $httpForwarded $httpXForwardedFor
|
||||||
|
return array(
|
||||||
|
array('for=87.65.43.21', '192.0.2.60'),
|
||||||
|
array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60'),
|
||||||
|
array('for=192.0.2.60', '192.0.2.60,87.65.43.21'),
|
||||||
|
array('for="::face", for=192.0.2.60', '192.0.2.60,192.0.2.43'),
|
||||||
|
array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60,87.65.43.21'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider testGetClientIpsWithAgreeingHeadersProvider
|
||||||
|
*/
|
||||||
|
public function testGetClientIpsWithAgreeingHeaders($httpForwarded, $httpXForwardedFor)
|
||||||
|
{
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
$server = array(
|
||||||
|
'REMOTE_ADDR' => '88.88.88.88',
|
||||||
|
'HTTP_FORWARDED' => $httpForwarded,
|
||||||
|
'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor,
|
||||||
|
);
|
||||||
|
|
||||||
|
Request::setTrustedProxies(array('88.88.88.88'));
|
||||||
|
|
||||||
|
$request->initialize(array(), array(), array(), array(), array(), $server);
|
||||||
|
|
||||||
|
$request->getClientIps();
|
||||||
|
|
||||||
|
Request::setTrustedProxies(array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetClientIpsWithAgreeingHeadersProvider()
|
||||||
|
{
|
||||||
|
// $httpForwarded $httpXForwardedFor
|
||||||
|
return array(
|
||||||
|
array('for="192.0.2.60"', '192.0.2.60'),
|
||||||
|
array('for=192.0.2.60, for=87.65.43.21', '192.0.2.60,87.65.43.21'),
|
||||||
|
array('for="[::face]", for=192.0.2.60', '::face,192.0.2.60'),
|
||||||
|
array('for="192.0.2.60:80"', '192.0.2.60'),
|
||||||
|
array('for=192.0.2.60;proto=http;by=203.0.113.43', '192.0.2.60'),
|
||||||
|
array('for="[2001:db8:cafe::17]:4711"', '2001:db8:cafe::17'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetContentWorksTwiceInDefaultMode()
|
public function testGetContentWorksTwiceInDefaultMode()
|
||||||
{
|
{
|
||||||
$req = new Request();
|
$req = new Request();
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
|
||||||
|
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the headers and other information indicating the
|
||||||
|
* client IP address of a request are consistent.
|
||||||
|
*
|
||||||
|
* @author Magnus Nordlander <magnus@fervo.se>
|
||||||
|
*/
|
||||||
|
class ValidateRequestListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Performs the validation.
|
||||||
|
*
|
||||||
|
* @param GetResponseEvent $event
|
||||||
|
*/
|
||||||
|
public function onKernelRequest(GetResponseEvent $event)
|
||||||
|
{
|
||||||
|
if ($event->isMasterRequest()) {
|
||||||
|
try {
|
||||||
|
// This will throw an exception if the headers are inconsistent.
|
||||||
|
$event->getRequest()->getClientIps();
|
||||||
|
} catch (ConflictingHeadersException $e) {
|
||||||
|
throw new BadRequestHttpException('The request headers contain conflicting information regarding the origin of this request.', $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
KernelEvents::REQUEST => array(
|
||||||
|
array('onKernelRequest', 256),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\HttpKernel\Profiler;
|
namespace Symfony\Component\HttpKernel\Profiler;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
|
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
|
||||||
@ -208,9 +209,13 @@ class Profiler
|
|||||||
$profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
|
$profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
|
||||||
$profile->setTime(time());
|
$profile->setTime(time());
|
||||||
$profile->setUrl($request->getUri());
|
$profile->setUrl($request->getUri());
|
||||||
$profile->setIp($request->getClientIp());
|
|
||||||
$profile->setMethod($request->getMethod());
|
$profile->setMethod($request->getMethod());
|
||||||
$profile->setStatusCode($response->getStatusCode());
|
$profile->setStatusCode($response->getStatusCode());
|
||||||
|
try {
|
||||||
|
$profile->setIp($request->getClientIp());
|
||||||
|
} catch (ConflictingHeadersException $e) {
|
||||||
|
$profile->setIp('Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
$response->headers->set('X-Debug-Token', $profile->getToken());
|
$response->headers->set('X-Debug-Token', $profile->getToken());
|
||||||
|
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\Tests\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
|
||||||
|
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||||
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|
||||||
|
class ValidateRequestListenerTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps()
|
||||||
|
{
|
||||||
|
$dispatcher = new EventDispatcher();
|
||||||
|
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
|
||||||
|
$listener = new ValidateRequestListener();
|
||||||
|
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
|
||||||
|
$request->method('getClientIps')
|
||||||
|
->will($this->throwException(new ConflictingHeadersException()));
|
||||||
|
|
||||||
|
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
|
||||||
|
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
|
||||||
|
|
||||||
|
$this->setExpectedException('Symfony\Component\HttpKernel\Exception\BadRequestHttpException');
|
||||||
|
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListenerDoesNothingOnValidRequests()
|
||||||
|
{
|
||||||
|
$dispatcher = new EventDispatcher();
|
||||||
|
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
|
||||||
|
$listener = new ValidateRequestListener();
|
||||||
|
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
|
||||||
|
$request->method('getClientIps')
|
||||||
|
->willReturn(array('127.0.0.1'));
|
||||||
|
|
||||||
|
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
|
||||||
|
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
|
||||||
|
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListenerDoesNothingOnSubrequests()
|
||||||
|
{
|
||||||
|
$dispatcher = new EventDispatcher();
|
||||||
|
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
|
||||||
|
$listener = new ValidateRequestListener();
|
||||||
|
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
|
||||||
|
$request->method('getClientIps')
|
||||||
|
->will($this->throwException(new ConflictingHeadersException()));
|
||||||
|
|
||||||
|
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
|
||||||
|
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST);
|
||||||
|
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user