8000 [HttpFoundation] Move IP check methods to a HttpUtils class for reuse by vicb · Pull Request #6005 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpFoundation] Move IP check methods to a HttpUtils class for reuse #6005

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
2.2.0
-----

* added a IpUtils class to check if an IP belongs to a CIDR
* added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method)
* disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to enable it)
* Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3
Expand Down
111 changes: 111 additions & 0 deletions src/Symfony/Component/HttpFoundation/IpUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?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;

/**
* Http utility functions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IpUtils
{
/**
* This class should not be instantiated
*/
private function __construct() {}

/**
* Validates an IPv4 or IPv6 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*/
public static function checkIp($requestIp, $ip)
{
if (false !== strpos($requestIp, ':')) {
return self::checkIp6($requestIp, $ip);
}

return self::checkIp4($requestIp, $ip);
}

/**
* Validates an IPv4 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*/
public static function checkIp4($requestIp, $ip)
{
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);

if ($netmask < 1 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}

return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}

/**
* Validates an IPv6 address.
*
* @author David Soria Parra <dsp at php dot net>
* @see https://github.com/dsp/v6tools
*
* @param string $requestIp
* @param string $ip
*
* @return boolean Whether the IP is valid
*
* @throws \RuntimeException When IPV6 support is not enabled
*/
public static function checkIp6($requestIp, $ip)
{
if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) {
throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
}

if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);

if ($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}

$bytesAddr = unpack("n*", inet_pton($address));
$bytesTest = unpack("n*", inet_pton($requestIp));

for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; $i++) {
$left = $netmask - 16 * ($i-1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}

return true;
}
}
87 changes: 1 addition & 86 deletions src/Symfony/Component/HttpFoundation/RequestMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,96 +143,11 @@ public function matches(Request $request)
return false;
}

if (null !== $this->ip && !$this->checkIp($request->getClientIp(), $this->ip)) {
if (null !== $this->ip && !IpUtils::checkIp($request->getClientIp(), $this->ip)) {
return false;
}

return true;
}

/**
* Validates an IP address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp($requestIp, $ip)
{
// IPv6 address
if (false !== strpos($requestIp, ':')) {
return $this->checkIp6($requestIp, $ip);
} else {
return $this->checkIp4($requestIp, $ip);
}
}

/**
* Validates an IPv4 address.
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp4($requestIp, $ip)
{
if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);

if ($netmask < 1 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}

return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}

/**
* Validates an IPv6 address.
*
* @author David Soria Parra <dsp at php dot net>
* @see https://github.com/dsp/v6tools
*
* @param string $requestIp
* @param string $ip
*
* @return boolean True valid, false if not.
*/
protected function checkIp6($requestIp, $ip)
{
if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) {
throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
}

if (false !== strpos($ip, '/')) {
list($address, $netmask) = explode('/', $ip, 2);

if ($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}

$bytesAddr = unpack("n*", inet_pton($address));
$bytesTest = unpack("n*", inet_pton($requestIp));

for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; $i++) {
$left = $netmask - 16 * ($i-1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}

return true;
}
}

71 changes: 71 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\Tests;

use Symfony\Component\HttpFoundation\IpUtils;

class IpUtilsTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider testIpv4Provider
*/
public function testIpv4($matches, $remoteAddr, $cidr)
{
$this->assertSame($matches, IpUtils::checkIp($remoteAddr, $cidr));
}

public function testIpv4Provider()
{
return array(
array(true, '192.168.1.1', '192.168.1.1'),
array(true, '192.168.1.1', '192.168.1.1/1'),
array(true, '192.168.1.1', '192.168.1.0/24'),
array(false, '192.168.1.1', '1.2.3.4/1'),
array(false, '192.168.1.1', '192.168.1/33'),
);
}

/**
* @dataProvider testIpv6Provider
*/
public function testIpv6($matches, $remoteAddr, $cidr)
{
if (!defined('AF_INET6')) {
$this->markTestSkipped('Only works when PHP is compiled without the option "disable-ipv6".');
}

$this->assertSame($matches, IpUtils::checkIp($remoteAddr, $cidr));
}

public function testIpv6Provider()
{
return array(
array(true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'),
array(false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'),
array(false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'),
array(true, '0:0:0:0:0:0:0:1', '::1'),
array(false, '0:0:603:0:396e:4789:8e99:0001', '::1'),
);
}

/**
* @expectedException \RuntimeException
*/
public function testAnIpv6WithOptionDisabledIpv6()
{
if (defined('AF_INET6')) {
$this->markTestSkipped 9E19 ('Only works when PHP is compiled with the option "disable-ipv6".');
}

IpUtils::checkIp('2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65');
}
}
73 changes: 0 additions & 73 deletions src/Symfony/Component/HttpFoundation/Tests/RequestMatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,78 +16,6 @@

class RequestMatcherTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider testIpv4Provider
*/
public function testIpv4($matches, $remoteAddr, $cidr)
{
$request = Request::create('', 'get', array(), array(), array(), array('REMOTE_ADDR' => $remoteAddr));

$matcher = new RequestMatcher();
$matcher->matchIp($cidr);

$this->assertEquals($matches, $matcher->matches($request));
}

public function testIpv4Provider()
{
return array(
array(true, '192.168.1.1', '192.168.1.1'),
array(true, '192.168.1.1', '192.168.1.1/1'),
array(true, '192.168.1.1', '192.168.1.0/24'),
array(false, '192.168.1.1', '1.2.3.4/1'),
array(false, '192.168.1.1', '192.168.1/33'),
);
}

/**
* @dataProvider testIpv6Provider
*/
public function testIpv6($matches, $remoteAddr, $cidr)
{
if (!defined('AF_INET6')) {
$this->markTestSkipped('Only works when PHP is compiled without the option "disable-ipv6".');
}

$request = Request::create('', 'get', array(), array(), array(), array('REMOTE_ADDR' => $remoteAddr));

$matcher = new RequestMatcher();
$matcher->matchIp($cidr);

$this->assertEquals($matches, $matcher->matches($request));
}

public function testIpv6Provider()
{
return array(
array(true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'),
array(false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'),
array(false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'),
array(true, '0:0:0:0:0:0:0:1', '::1'),
array(false, '0:0:603:0:396e:4789:8e99:0001', '::1'),
);
}

public function testAnIpv6WithOptionDisabledIpv6()
{
if (defined('AF_INET6')) {
$this->markTestSkipped('Only works when PHP is compiled with the option "disable-ipv6".');
}

$request = Request::create('', 'get', array(), array(), array(), array('REMOTE_ADDR' => '2a01:198:603:0:396e:4789:8e99:890f'));

$matcher = new RequestMatcher();
$matcher->matchIp('2a01:198:603:0::/65');

try {
$matcher->matches($request);

$this->fail('An expected RuntimeException has not been raised.');
} catch (\Exception $e) {
$this->assertInstanceOf('\RuntimeException', $e);
}
}

/**
* @dataProvider testMethodFixtures
*/
Expand Down Expand Up @@ -200,4 +128,3 @@ public function testAttributes()
$this->assertFalse($matcher->matches($request));
}
}

0