Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15331908
D11043.id26520.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
11 KB
Referenced Files
None
Subscribers
None
D11043.id26520.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D11043: Add IP range / CIDR utilities to libphutil
Attached
Detach File
Event Timeline
Log In to Comment