Merge branch '2.0' into 2.1

* 2.0:
  replaced magic strings by proper constants
  refactored tests for Request
  fixed the logic in Request::isSecure() (if the information comes from a source that we trust, don't check other ones)
  added a way to configure the X-Forwarded-XXX header names and a way to disable trusting them
  fixed algorithm used to determine the trusted client IP
  removed the non-standard Client-IP HTTP header

Conflicts:
	src/Symfony/Component/HttpFoundation/Request.php
	src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
This commit is contained in:
Fabien Potencier 2012-11-29 12:27:48 +01:00
commit 6c67476ef0
2 changed files with 209 additions and 93 deletions

View File

@ -30,7 +30,28 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface;
*/
class Request
{
protected static $trustProxy = false;
const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto';
const HEADER_CLIENT_PORT = 'client_port';
protected static $trustProxyData = false;
protected static $trustedProxies = array();
/**
* Names for headers that can be trusted when
* using trusted proxies.
*
* The default names are non-standard, but widely used
* by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
*/
protected static $trustedHeaders = array(
self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
);
/**
* @var \Symfony\Component\HttpFoundation\ParameterBag
@ -439,14 +460,50 @@ class Request
/**
* Trusts $_SERVER entries coming from proxies.
*
* You should only call this method if your application
* is hosted behind a reverse proxy that you manage.
*
* @api
* @deprecated Deprecated since version 2.0, to be removed in 2.3. Use setTrustedProxies instead.
*/
public static function trustProxyData()
{
self::$trustProxy = true;
self::$trustProxyData = true;
}
/**
* Sets a list of trusted proxies.
*
* You should only list the reverse proxies that you manage directly.
*
* @param array $proxies A list of trusted proxies
*
* @api
*/
public static function setTrustedProxies(array $proxies)
{
self::$trustedProxies = $proxies;
self::$trustProxyData = $proxies ? true : false;
}
/**
* Sets the name for trusted headers.
*
* The following header keys are supported:
*
* * Request::HEADER_CLIENT_IP: defaults to X-Forwarded-For (see getClientIp())
* * Request::HEADER_CLIENT_HOST: defaults to X-Forwarded-Host (see getClientHost())
* * Request::HEADER_CLIENT_PORT: defaults to X-Forwarded-Port (see getClientPort())
* * Request::HEADER_CLIENT_PROTO: defaults to X-Forwarded-Proto (see getScheme() and isSecure())
*
* Setting an empty value allows to disable the trusted header for the given key.
*
* @param string $key The header key
* @param string $value The header name
*/
public static function setTrustedHeaderName($key, $value)
{
if (!array_key_exists($key, self::$trustedHeaders)) {
throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key));
}
self::$trustedHeaders[$key] = $value;
}
/**
@ -582,31 +639,43 @@ class Request
/**
* Returns the client IP address.
*
* This method can read the client IP address from the "X-Forwarded-For" header
* when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For"
* header value is a comma+space separated list of IP addresses, the left-most
* being the original client, and each successive proxy that passed the request
* adding the IP address where it received the request from.
*
* If your reverse proxy uses a different header name than "X-Forwarded-For",
* ("Client-Ip" for instance), configure it via "setTrustedHeaderName()" with
* the "client-ip" key.
*
* @return string The client IP address
*
* @see http://en.wikipedia.org/wiki/X-Forwarded-For
*
* @deprecated The proxy argument is deprecated since version 2.0 and will be removed in 2.3. Use setTrustedProxies instead.
*
* @api
*/
public function getClientIp()
{
if (self::$trustProxy) {
if ($this->server->has('HTTP_CLIENT_IP')) {
return $this->server->get('HTTP_CLIENT_IP');
} elseif ($this->server->has('HTTP_X_FORWARDED_FOR')) {
$clientIp = explode(',', $this->server->get('HTTP_X_FORWARDED_FOR'));
$ip = $this->server->get('REMOTE_ADDR');
foreach ($clientIp as $ipAddress) {
$cleanIpAddress = trim($ipAddress);
if (false !== filter_var($cleanIpAddress, FILTER_VALIDATE_IP)) {
return $cleanIpAddress;
}
}
return '';
}
if (!self::$trustProxyData) {
return $ip;
}
return $this->server->get('REMOTE_ADDR');
if (!self::$trustedHeaders[self::HEADER_CLIENT_IP] || !$this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
return $ip;
}
$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
$clientIps[] = $ip;
$trustedProxies = self::$trustProxyData && !self::$trustedProxies ? array($ip) : self::$trustedProxies;
$clientIps = array_diff($clientIps, $trustedProxies);
return array_pop($clientIps);
}
/**
@ -705,14 +774,22 @@ class Request
/**
* Returns the port on which the request is made.
*
* This method can read the client port from the "X-Forwarded-Port" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Port" header must contain the client port.
*
* If your reverse proxy uses a different header name than "X-Forwarded-Port",
* configure it via "setTrustedHeaderName()" with the "client-port" key.
*
* @return string
*
* @api
*/
public function getPort()
{
if (self::$trustProxy && $this->headers->has('X-Forwarded-Port')) {
return $this->headers->get('X-Forwarded-Port');
if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) {
return $port;
}
return $this->server->get('SERVER_PORT');
@ -858,31 +935,46 @@ class Request
/**
* Checks whether the request is secure or not.
*
* This method can read the client port from the "X-Forwarded-Proto" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
*
* If your reverse proxy uses a different header name than "X-Forwarded-Proto"
* ("SSL_HTTPS" for instance), configure it via "setTrustedHeaderName()" with
* the "client-proto" key.
*
* @return Boolean
*
* @api
*/
public function isSecure()
{
return (
(strtolower($this->server->get('HTTPS')) == 'on' || $this->server->get('HTTPS') == 1)
||
(self::$trustProxy && strtolower($this->headers->get('SSL_HTTPS')) == 'on' || $this->headers->get('SSL_HTTPS') == 1)
||
(self::$trustProxy && strtolower($this->headers->get('X_FORWARDED_PROTO')) == 'https')
);
if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) {
return in_array(strtolower($proto), array('https', 'on', '1'));
}
return 'on' == strtolower($this->server->get('HTTPS')) || 1 == $this->server->get('HTTPS');
}
/**
* Returns the host name.
*
* This method can read the client port from the "X-Forwarded-Host" header
* when trusted proxies were set via "setTrustedProxies()".
*
* The "X-Forwarded-Host" header must contain the client host name.
*
* If your reverse proxy uses a different header name than "X-Forwarded-Host",
* configure it via "setTrustedHeaderName()" with the "client-host" key.
*
* @return string
*
* @api
*/
public function getHost()
{
if (self::$trustProxy && $host = $this->headers->get('X_FORWARDED_HOST')) {
if (self::$trustProxyData && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) {
$elements = explode(',', $host);
$host = trim($elements[count($elements) - 1]);

View File

@ -573,33 +573,14 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com'));
$this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from Host Header');
// Host header with port number.
// Host header with port number
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com:8080'));
$this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from Host Header with port number');
// Server values.
// Server values
$request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com'));
$this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from server name');
$this->startTrustingProxyData();
// X_FORWARDED_HOST.
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com'));
$this->assertEquals('www.exemple.com', $request->getHost(), '->getHost() from X_FORWARDED_HOST');
// X_FORWARDED_HOST
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com, www.second.com'));
$this->assertEquals('www.second.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST use last value');
// X_FORWARDED_HOST with port number
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_X_FORWARDED_HOST' => 'www.exemple.com, www.second.com:8080'));
$this->assertEquals('www.second.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST with port number');
$request->initialize(array(), array(), array(), array(), array(), array('HTTP_HOST' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com'));
$this->assertEquals('www.forward.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over Host');
$request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_X_FORWARDED_HOST' => 'www.forward.com'));
$this->assertEquals('www.forward.com', $request->getHost(), '->getHost() value from X_FORWARDED_HOST has priority over SERVER_NAME ');
$request->initialize(array(), array(), array(), array(), array(), array('SERVER_NAME' => 'www.exemple.com', 'HTTP_HOST' => 'www.host.com'));
$this->assertEquals('www.host.com', $request->getHost(), '->getHost() value from Host header has priority over SERVER_NAME ');
$this->stopTrustingProxyData();
@ -646,42 +627,40 @@ class RequestTest extends \PHPUnit_Framework_TestCase
/**
* @dataProvider testGetClientIpProvider
*/
public function testGetClientIp($expected, $proxy, $remoteAddr, $httpClientIp, $httpForwardedFor)
public function testGetClientIp($expected, $proxy, $remoteAddr, $httpForwardedFor, $trustedProxies)
{
$request = new Request();
$this->assertEquals('', $request->getClientIp());
$server = array('REMOTE_ADDR' => $remoteAddr);
if (null !== $httpClientIp) {
$server['HTTP_CLIENT_IP'] = $httpClientIp;
}
if (null !== $httpForwardedFor) {
$server['HTTP_X_FORWARDED_FOR'] = $httpForwardedFor;
}
if ($proxy || $trustedProxies) {
Request::setTrustedProxies(null === $trustedProxies ? array($remoteAddr) : $trustedProxies);
}
$request->initialize(array(), array(), array(), array(), array(), $server);
if ($proxy) {
$this->startTrustingProxyData();
}
$this->assertEquals($expected, $request->getClientIp($proxy));
if ($proxy) {
$this->stopTrustingProxyData();
}
Request::setTrustedProxies(array());
}
public function testGetClientIpProvider()
{
return array(
array('88.88.88.88', false, '88.88.88.88', null, null),
array('127.0.0.1', false, '127.0.0.1', '88.88.88.88', null),
array('88.88.88.88', true, '127.0.0.1', '88.88.88.88', null),
array('127.0.0.1', false, '127.0.0.1', null, '88.88.88.88'),
array('88.88.88.88', true, '127.0.0.1', null, '88.88.88.88'),
array('::1', false, '::1', null, null),
array('2620:0:1cfe:face:b00c::3', true, '::1', '2620:0:1cfe:face:b00c::3', null),
array('2620:0:1cfe:face:b00c::3', true, '::1', null, '2620:0:1cfe:face:b00c::3, ::1'),
array('88.88.88.88', true, '123.45.67.89', null, '88.88.88.88, 87.65.43.21, 127.0.0.1'),
array('88.88.88.88', true, '123.45.67.89', null, 'unknown, 88.88.88.88'),
array('88.88.88.88', false, '88.88.88.88', null, null),
array('127.0.0.1', false, '127.0.0.1', null, null),
array('::1', false, '::1', null, null),
array('127.0.0.1', false, '127.0.0.1', '88.88.88.88', null),
array('88.88.88.88', true, '127.0.0.1', '88.88.88.88', null),
array('2620:0:1cfe:face:b00c::3', true, '::1', '2620:0:1cfe:face:b00c::3', null),
array('88.88.88.88', true, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', null),
array('87.65.43.21', true, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '88.88.88.88')),
array('87.65.43.21', false, '123.45.67.89', '127.0.0.1, 87.65.43.21, 88.88.88.88', array('123.45.67.89', '88.88.88.88')),
);
}
@ -790,8 +769,9 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$this->startTrustingProxyData();
$request->headers->set('X_FORWARDED_PROTO', 'https');
Request::setTrustedProxies(array('1.1.1.1'));
$this->assertTrue($request->isSecure());
$this->stopTrustingProxyData();
Request::setTrustedProxies(array());
$request->overrideGlobals();
@ -997,18 +977,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $request->getRequestFormat(null));
}
public function testForwardedSecure()
{
$request = new Request();
$request->headers->set('X-Forwarded-Proto', 'https');
$request->headers->set('X-Forwarded-Port', 443);
$this->startTrustingProxyData();
$this->assertTrue($request->isSecure());
$this->assertEquals(443, $request->getPort());
$this->stopTrustingProxyData();
}
public function testHasSession()
{
$request = new Request();
@ -1062,14 +1030,6 @@ class RequestTest extends \PHPUnit_Framework_TestCase
);
}
public function testIsProxyTrusted()
{
$this->startTrustingProxyData();
$this->assertTrue(Request::isProxyTrusted());
$this->stopTrustingProxyData();
$this->assertFalse(Request::isProxyTrusted());
}
public function testIsMethod()
{
$request = new Request();
@ -1188,10 +1148,74 @@ class RequestTest extends \PHPUnit_Framework_TestCase
private function stopTrustingProxyData()
{
$class = new \ReflectionClass('Symfony\\Component\\HttpFoundation\\Request');
$property = $class->getProperty('trustProxy');
$property = $class->getProperty('trustProxyData');
$property->setAccessible(true);
$property->setValue(false);
}
public function testTrustedProxies()
{
$request = Request::create('http://example.com/');
$request->server->set('REMOTE_ADDR', '3.3.3.3');
$request->headers->set('X_FORWARDED_FOR', '1.1.1.1, 2.2.2.2');
$request->headers->set('X_FORWARDED_HOST', 'foo.example.com, real.example.com:8080');
$request->headers->set('X_FORWARDED_PROTO', 'https');
$request->headers->set('X_FORWARDED_PORT', 443);
$request->headers->set('X_MY_FOR', '3.3.3.3, 4.4.4.4');
$request->headers->set('X_MY_HOST', 'my.example.com');
$request->headers->set('X_MY_PROTO', 'http');
$request->headers->set('X_MY_PORT', 81);
// no trusted proxies
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// trusted proxy via deprecated trustProxyData()
Request::trustProxyData();
$this->assertEquals('2.2.2.2', $request->getClientIp());
$this->assertEquals('real.example.com', $request->getHost());
$this->assertEquals(443, $request->getPort());
$this->assertTrue($request->isSecure());
// disabling proxy trusting
Request::setTrustedProxies(array());
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// trusted proxy via setTrustedProxies()
Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'));
$this->assertEquals('1.1.1.1', $request->getClientIp());
$this->assertEquals('real.example.com', $request->getHost());
$this->assertEquals(443, $request->getPort());
$this->assertTrue($request->isSecure());
// custom header names
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X_MY_FOR');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, 'X_MY_HOST');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, 'X_MY_PORT');
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_MY_PROTO');
$this->assertEquals('4.4.4.4', $request->getClientIp());
$this->assertEquals('my.example.com', $request->getHost());
$this->assertEquals(81, $request->getPort());
$this->assertFalse($request->isSecure());
// disabling via empty header names
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, null);
Request::setTrustedHeaderName(Request::HEADER_CLIENT_HOST, null);
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PORT, null);
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, null);
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// reset
Request::setTrustedProxies(array());
}
}
class RequestContentProxy extends Request