Page MenuHomePhabricator

D11043.id26520.diff
No OneTemporary

D11043.id26520.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -110,6 +110,8 @@
'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
+ 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
+ 'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
@@ -177,6 +179,8 @@
'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
+ 'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
+ 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
'PhutilInfrastructureTestCase' => '__tests__/PhutilInfrastructureTestCase.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
@@ -554,6 +558,8 @@
'PhutilBufferedIterator' => 'Iterator',
'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
+ 'PhutilCIDRBlock' => 'Phobject',
+ 'PhutilCIDRList' => 'Phobject',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilChannelChannel' => 'PhutilChannel',
@@ -597,6 +603,8 @@
'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
+ 'PhutilIPAddress' => 'Phobject',
+ 'PhutilIPAddressTestCase' => 'PhutilTestCase',
'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
'PhutilInfrastructureTestCase' => 'PhutilTestCase',
'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
diff --git a/src/ip/PhutilCIDRBlock.php b/src/ip/PhutilCIDRBlock.php
new file mode 100644
--- /dev/null
+++ b/src/ip/PhutilCIDRBlock.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * CIDR notation IP block, like "172.30.0.0/16".
+ *
+ * See @{class:PhutilCIDRList} for discussion.
+ */
+final class PhutilCIDRBlock extends Phobject {
+
+ private $ip;
+ private $bits;
+
+ private function __construct() {
+ // <private>
+ }
+
+ public static function newBlock($in) {
+ if ($in instanceof PhutilCIDRBlock) {
+ return $in;
+ }
+
+ return self::newFromString($in);
+ }
+
+ private static function newFromString($str) {
+ if (!preg_match('(^[\d.]+/[\d]+\z)', $str)) {
+ throw new Exception(
+ pht(
+ 'CIDR block "%s" is not formatted correctly. Expected an IP block '.
+ 'in CIDR notation, like "172.30.0.0/16".',
+ $str));
+ }
+
+ list($ip, $mask) = explode('/', $str);
+
+ $ip = PhutilIPAddress::newAddress($ip);
+
+ // These rules are for IPv4; some day we'll handle IPv6 too.
+
+ if (preg_match('/^0\d/', $mask)) {
+ throw new Exception(
+ pht(
+ 'CIDR block "%s" is not formatted correctly. The IP block mask '.
+ '("%s") must not have leading zeroes.',
+ $str,
+ $mask));
+ }
+
+ $bits = (int)$mask;
+ if ($bits < 0 || $bits > 32) {
+ throw new Exception(
+ pht(
+ 'CIDR block "%s" is not formatted correctly. The IP block mask '.
+ '("%s") must mask between 0 and 32 bits, inclusive.',
+ $str,
+ $mask));
+ }
+
+ $obj = new PhutilCIDRBlock();
+ $obj->ip = $ip;
+ $obj->bits = $bits;
+
+ return $obj;
+ }
+
+ public function containsAddress($address) {
+ $address = PhutilIPAddress::newAddress($address);
+
+ $block_bits = $this->ip->toBits();
+ $address_bits = $address->toBits();
+
+ return (strncmp($block_bits, $address_bits, $this->bits) === 0);
+ }
+
+}
diff --git a/src/ip/PhutilCIDRList.php b/src/ip/PhutilCIDRList.php
new file mode 100644
--- /dev/null
+++ b/src/ip/PhutilCIDRList.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * List of CIDR notation IP blocks, like "172.30.0.0/16".
+ *
+ * This class is primarily useful for managing IP whitelists or blacklists.
+ * For example, you can check if an address is on a subnet like this:
+ *
+ * $whitelist = PhutilCIDRList::newList(array('172.30.0.0/16'));
+ * $ok = $whitelist->containsAddrsss('172.30.0.1');
+ */
+final class PhutilCIDRList extends Phobject {
+
+ private $blocks;
+
+ private function __construct() {
+ // <private>
+ }
+
+ public static function newList(array $blocks) {
+ foreach ($blocks as $key => $block) {
+ $blocks[$key] = PhutilCIDRBlock::newBlock($block);
+ }
+
+ $obj = new PhutilCIDRList();
+ $obj->blocks = $blocks;
+ return $obj;
+ }
+
+ public function containsAddress($address) {
+ foreach ($this->blocks as $block) {
+ if ($block->containsAddress($address)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/ip/PhutilIPAddress.php b/src/ip/PhutilIPAddress.php
new file mode 100644
--- /dev/null
+++ b/src/ip/PhutilIPAddress.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * Represent and manipulate IP addresses.
+ *
+ * NOTE: This class only supports IPv4 for now.
+ */
+final class PhutilIPAddress extends Phobject {
+
+ private $ip;
+ private $bits;
+
+ private function __construct() {
+ // <private>
+ }
+
+ public static function newAddress($in) {
+ if ($in instanceof PhutilIPAddress) {
+ return $in;
+ }
+
+ return self::newFromString($in);
+ }
+
+ private static function newFromString($str) {
+ $matches = null;
+ $ok = preg_match('(^(\d+)\.(\d+)\.(\d+).(\d+)\z)', $str, $matches);
+ if (!$ok) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted. Expected an IP '.
+ 'address like "23.45.67.89".',
+ $str));
+ }
+
+ $parts = array_slice($matches, 1);
+ foreach ($parts as $part) {
+ if (preg_match('/^0\d/', $part)) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted. Address segments '.
+ 'should have no leading zeroes, but segment "%s" has a leading '.
+ 'zero.',
+ $str,
+ $part));
+ }
+
+ $value = (int)$part;
+ if ($value < 0 || $value > 255) {
+ throw new Exception(
+ pht(
+ 'IP address "%s" is not properly formatted. Address segments '.
+ 'should be between 0 and 255, inclusive, but segment "%s" has '.
+ 'a value outside of this range.',
+ $str,
+ $part));
+ }
+ }
+
+ $obj = new PhutilIPAddress();
+ $obj->ip = $str;
+
+ return $obj;
+ }
+
+ public function toBits() {
+ if ($this->bits === null) {
+ $bits = '';
+ foreach (explode('.', $this->ip) as $part) {
+ $value = (int)$part;
+ for ($ii = 7; $ii >= 0; $ii--) {
+ $mask = (1 << $ii);
+ if (($value & $mask) === $mask) {
+ $bits .= '1';
+ } else {
+ $bits .= '0';
+ }
+ }
+ }
+
+ $this->bits = $bits;
+ }
+
+ return $this->bits;
+ }
+
+}
diff --git a/src/ip/__tests__/PhutilIPAddressTestCase.php b/src/ip/__tests__/PhutilIPAddressTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/ip/__tests__/PhutilIPAddressTestCase.php
@@ -0,0 +1,152 @@
+<?php
+
+final class PhutilIPAddressTestCase extends PhutilTestCase {
+
+ public function testValidIPAddresses() {
+ $cases = array(
+ // Valid IP.
+ '1.2.3.4' => true,
+
+ // No nonsense.
+ '1.2.3' => false,
+ 'duck' => false,
+ '' => false,
+ '1 2 3 4' => false,
+ '.' => false,
+ '1.2.3.4.' => false,
+ '1..3.4' => false,
+
+ // No leading zeroes.
+ '0.0.0.0' => true,
+ '0.0.0.01' => false,
+
+ // No segments > 255.
+ '255.255.255.255' => true,
+ '255.255.255.256' => false,
+ );
+
+ foreach ($cases as $input => $expect) {
+ $caught = null;
+ try {
+ PhutilIPAddress::newAddress($input);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ $expect,
+ !($caught instanceof Exception),
+ 'PhutilIPAddress['.$input.']');
+ }
+ }
+
+ public function testIPAddressToBits() {
+ $cases = array(
+ '0.0.0.0' => '00000000000000000000000000000000',
+ '255.255.255.255' => '11111111111111111111111111111111',
+ '255.0.0.0' => '11111111000000000000000000000000',
+ '0.0.0.1' => '00000000000000000000000000000001',
+ '0.0.0.2' => '00000000000000000000000000000010',
+ '0.0.0.3' => '00000000000000000000000000000011',
+ );
+
+ foreach ($cases as $input => $expect) {
+ $actual = PhutilIPAddress::newAddress($input)->toBits();
+ $this->assertEqual(
+ $expect,
+ $actual,
+ 'PhutilIPAddress['.$input.']->toBits()');
+ }
+ }
+
+ public function testValidCIDRBlocks() {
+ $cases = array(
+ // Valid block.
+ '1.0.0.0/16' => true,
+
+ // No nonsense.
+ 'duck' => false,
+ '1/2/3' => false,
+ '23/0.0.0.0' => false,
+ '0.0.0.0/0.0.0.0' => false,
+
+ // No leading zeroes.
+ '1.0.0.0/4' => true,
+ '1.0.0.0/04' => false,
+
+ // No out-of-range masks.
+ '1.0.0.0/32' => true,
+ '1.0.0.0/33' => false,
+ );
+
+ foreach ($cases as $input => $expect) {
+ $caught = null;
+ try {
+ PhutilCIDRBlock::newBlock($input);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ $expect,
+ !($caught instanceof Exception),
+ 'PhutilCIDRBlock['.$input.']');
+ }
+ }
+
+ public function testCIDRBlockContains() {
+ $cases = array(
+ '0.0.0.0/0' => array(
+ '0.0.0.0' => true,
+ '1.1.1.1' => true,
+ '2.3.4.5' => true,
+ ),
+ '0.0.0.2/32' => array(
+ '0.0.0.1' => false,
+ '0.0.0.2' => true,
+ '0.0.0.3' => false,
+ ),
+ '172.30.0.0/16' => array(
+ '172.29.255.255' => false,
+ '172.30.0.0' => true,
+ '172.30.255.255' => true,
+ '172.31.0.0' => false,
+ ),
+ );
+
+ foreach ($cases as $input_block => $tests) {
+ $block = PhutilCIDRBlock::newBlock($input_block);
+ foreach ($tests as $input => $expect) {
+ $this->assertEqual(
+ $expect,
+ $block->containsAddress($input),
+ 'PhutilCIDRBlock['.$input_block.']->containsAddress('.$input.')');
+ }
+ }
+ }
+
+ public function testCIDRList() {
+ $list = array(
+ '172.30.0.0/16',
+ '127.0.0.3/32',
+ );
+
+ $cases = array(
+ '0.0.0.0' => false,
+ '172.30.0.5' => true,
+ '127.0.0.2' => false,
+ '127.0.0.3' => true,
+ );
+
+ $list = PhutilCIDRList::newList($list);
+
+ foreach ($cases as $input => $expect) {
+ $this->assertEqual(
+ $expect,
+ $list->containsAddress($input),
+ 'PhutilCIDRList->containsAddress('.$input.')');
+ }
+ }
+
+
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 8, 3:13 PM (2 w, 1 m ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7229628
Default Alt Text
D11043.id26520.diff (11 KB)

Event Timeline