├── .gitignore
├── composer.json
├── phpunit.xml.dist
├── LICENSE
├── README.md
├── tests
└── Lifo
│ └── IP
│ └── IPTest.php
└── src
├── IP.php
├── BC.php
└── CIDR.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lifo/ip",
3 | "description": "IP address helper PHP library for working with IPv4 and IPv6 addresses",
4 | "keywords": ["ip address", "ip", "ipv4", "ipv6"],
5 | "type": "library",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Jason Morriss",
10 | "email": "lifo2013@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "php": ">=5.3.0",
15 | "ext-bcmath": "*"
16 | },
17 | "autoload": {
18 | "psr-4": { "Lifo\\IP\\": "src/" }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | tests
12 |
13 |
14 |
15 |
16 |
17 | ./src
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Jason Morriss
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## IP Address Helper Library
2 |
3 | IP address helper PHP library for working with IPv4 and IPv6 addresses. Convert any IP address into decimal, hex or binary and back again.
4 |
5 | ### Notes
6 |
7 | This library is not complete and is missing certain CIDR, Subnet and other miscellaneous IP features.
8 | Most of the IP conversion routines use `BCMATH` to do calculations which means this library is not the fastest it
9 | could be. Once the library is in a more stable state I may start attempting to optimize certain bits.
10 |
11 | Feel free to send pull requests with missing functionality.
12 |
13 | ### Examples
14 |
15 | The translation routines are IP agnostic, meaning they don't care if you pass in an IPv4 or IPv6 address.
16 | All IP calculations are done in `Decimal` which is perfect for storing in databases.
17 |
18 | ```php
19 | use Lifo\IP\IP;
20 | use Lifo\IP\CIDR:
21 |
22 | // IPv4
23 | echo '127.0.0.1 = ', IP::inet_ptod('127.0.0.1'), "\n";
24 | echo IP::inet_dtop('2130706433'), " = 2130706433\n";
25 | echo '127.0.0.1 = ', IP::inet_ptoh('127.0.0.1'), " (hex)\n";
26 |
27 | // IPv6
28 | echo '2001:4056::1 = ', IP::inet_ptod('2001:4056::1'), "\n";
29 | echo IP::inet_dtop('42541793049812452694190522094162280449'), " = 42541793049812452694190522094162280449\n";
30 | echo '2001:4056::1 = ', IP::inet_ptoh('2001:4056::1'), " (hex)\n";
31 |
32 | // CIDR
33 |
34 | // note: the true CIDR block is calculated from the prefix (the ::1 is ignored)
35 | $ip = new CIDR('2001:4056::1/96');
36 |
37 | echo "$ip\n", implode(' - ', $ip->getRange()), " (" . number_format($ip->getTotal()) . " hosts)\n";
38 | ```
39 |
40 | ```
41 | // expected output:
42 | 127.0.0.1 = 2130706433
43 | 127.0.0.1 = 2130706433
44 | 127.0.0.1 = 7f000001 (hex)
45 |
46 | 2001:4056::1 = 42541793049812452694190522094162280449
47 | 2001:4056::1 = 42541793049812452694190522094162280449
48 | 2001:4056::1 = 20014056000000000000000000000001 (hex)
49 |
50 | 2001:4056::1/96
51 | 2001:4056:: - 2001:4056::ffff:ffff (4,294,967,296 hosts)
52 | ```
53 |
--------------------------------------------------------------------------------
/tests/Lifo/IP/IPTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(IP::isIPv4('10.0.0.1'));
12 | $this->assertFalse(IP::isIPv4('2007::1'));
13 | $this->assertEquals(IP::inet_ptod('10.0.0.1'), '167772161', 'IPv4 inet_ptod (decimal)');
14 | $this->assertEquals(IP::inet_ptod('10.0.0.1/24'), '167772161', 'IPv4 inet_ptod with cidr (decimal)');
15 | $this->assertEquals(IP::inet_ptoh('10.0.0.1'), '0a000001', 'IPv4 inet_ptoh (hex)');
16 | $this->assertEquals(IP::inet_ptob('10.0.0.1'), '00001010000000000000000000000001', 'IPv4 inet_ptob (binary)');
17 | $this->assertEquals(IP::inet_dtop('167772161'), '10.0.0.1', 'IPv4 inet_dtop (presentational)');
18 | $this->assertEquals(IP::inet_htop('0a000001'), '10.0.0.1', 'IPv4 inet_htop (hex)');
19 | $this->assertEquals(IP::inet_btop('00001010000000000000000000000001'), '10.0.0.1', 'IPv4 inet_btop (binary)');
20 | $this->assertEquals(IP::inet_expand('10.0.0.1'), '10.0.0.1', 'IPv4 expand');
21 | $this->assertEquals(IP::inet_expand('10.0.0.1/24'), '10.0.0.1', 'IPv4 expand with cidr');
22 | $this->assertEquals(IP::to_ipv6('10.0.0.1'), 'a00:1', 'IPv6 to_ipv6');
23 | $this->assertEquals(IP::to_ipv6('10.0.0.1', true), '0:0:0:0:0:ffff:a00:1', 'IPv6 to_ipv6 mapped');
24 |
25 | // IPv6
26 | $this->assertTrue(IP::isIPv6('2007:1234:5678::1'));
27 | $this->assertFalse(IP::isIPv6('10.0.0.1'));
28 | $this->assertEquals(IP::inet_ptod('2007:1234:5678::1'), '42572011173125150141124156729380044801', 'IPv6 inet_ptod (decimal)');
29 | $this->assertEquals(IP::inet_ptod('2007:1234:5678::1/64'), '42572011173125150141124156729380044801', 'IPv6 inet_ptod with cidr (decimal)');
30 | $this->assertEquals(IP::inet_ptoh('2007:1234:5678::1'), '20071234567800000000000000000001', 'IPv6 inet_ptoh (hex)');
31 | $this->assertEquals(IP::inet_ptob('2007:1234:5678::1'), '00100000000001110001001000110100010101100111100000000000000000000000000000000000000000000000000000000000000000000000000000000001', 'IPv6 inet_ptob (binary)');
32 | $this->assertEquals(IP::inet_dtop('42572011173125150141124156729380044801'), '2007:1234:5678::1', 'IPv6 inet_dtop (presentational)');
33 | $this->assertEquals(IP::inet_htop('20071234567800000000000000000001'), '2007:1234:5678::1', 'IPv6 inet_htop (hex)');
34 | $this->assertEquals(IP::inet_btop('00100000000001110001001000110100010101100111100000000000000000000000000000000000000000000000000000000000000000000000000000000001'), '2007:1234:5678::1', 'IPv6 inet_btop (binary)');
35 | $this->assertEquals(IP::inet_expand('2007:1234:5678::1'), '2007:1234:5678:0000:0000:0000:0000:0001', 'IPv6 expand');
36 | $this->assertEquals(IP::inet_expand('2007:1234:5678::1/64'), '2007:1234:5678:0000:0000:0000:0000:0001', 'IPv6 expand with cidr');
37 |
38 | // Misc tests for coverage
39 | $this->assertEquals(IP::inet_dtop('167772161'), '10.0.0.1', 'IPv6 inet_dtop low-end no version (decimal)');
40 | $this->assertEquals(IP::inet_dtop('167772161', 6), '::10.0.0.1', 'IPv6 inet_dtop low-end forced v6 (decimal)');
41 | $this->assertEquals(IP::inet_dtop('167772161', 4), '10.0.0.1', 'IPv6 inet_dtop low-end forced v4 (decimal)');
42 | }
43 |
44 | public function testIPv4cmp()
45 | {
46 | $lo = '10.0.0.1';
47 | $hi = '192.168.1.2';
48 |
49 | $this->assertTrue(IP::cmp($lo, $lo) == 0);
50 | $this->assertTrue(IP::cmp($lo, $hi) == -1);
51 | $this->assertTrue(IP::cmp($hi, $lo) == 1);
52 | }
53 |
54 | public function testIPv6cmp()
55 | {
56 | $lo = '2001::1';
57 | $hi = '9504::2';
58 |
59 | $this->assertTrue(IP::cmp($lo, $lo) == 0);
60 | $this->assertTrue(IP::cmp($lo, $hi) == -1);
61 | $this->assertTrue(IP::cmp($hi, $lo) == 1);
62 | }
63 |
64 | /**
65 | * @expectedException InvalidArgumentException
66 | */
67 | public function testIPException()
68 | {
69 | // param 1 must be an IPv4 address
70 | IP::to_ipv6('2007::1');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/IP.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Lifo\IP;
11 |
12 | /**
13 | * IP Address helper class.
14 | *
15 | * Provides routines to translate IPv4 and IPv6 addresses between human readable
16 | * strings, decimal, hexidecimal and binary.
17 | *
18 | * Requires BCmath extension and IPv6 PHP support
19 | */
20 | abstract class IP
21 | {
22 | /**
23 | * Convert a human readable (presentational) IP address string into a decimal string.
24 | */
25 | public static function inet_ptod($ip)
26 | {
27 | // shortcut for IPv4 addresses
28 | if (strpos($ip, ':') === false && strpos($ip, '.') !== false) {
29 | // remove any cidr block notation
30 | if (($o = strpos($ip, '/')) !== false) {
31 | $ip = substr($ip, 0, $o);
32 | }
33 | return sprintf('%u', ip2long($ip));
34 | }
35 |
36 | // remove any cidr block notation
37 | if (($o = strpos($ip, '/')) !== false) {
38 | $ip = substr($ip, 0, $o);
39 | }
40 |
41 | // unpack into 4 32bit integers
42 | $parts = unpack('N*', inet_pton($ip));
43 | foreach ($parts as &$part) {
44 | if ($part < 0) {
45 | // convert signed int into unsigned
46 | $part = sprintf('%u', $part);
47 | //$part = bcadd($part, '4294967296');
48 | }
49 | }
50 |
51 | // add each 32bit integer to the proper bit location in our big decimal
52 | $decimal = $parts[4]; // << 0
53 | $decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32
54 | $decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64
55 | $decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96
56 |
57 | return $decimal;
58 | }
59 |
60 | /**
61 | * Convert a decimal string into a human readable IP address.
62 | *
63 | * @param string $decimal Decimal number to convert into presentational IP string.
64 | * @param integer $version Force IP version to 4 or 6. Leave null for automatic.
65 | */
66 | public static function inet_dtop($decimal, $version = null)
67 | {
68 | $parts = array();
69 | $parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96
70 | $decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336'));
71 | $parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64
72 | $decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616'));
73 | $parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32
74 | $decimal = bcsub($decimal, bcmul($parts[3], '4294967296'));
75 | $parts[4] = $decimal; // >> 0
76 |
77 | foreach ($parts as &$part) {
78 | if (bccomp($part, '2147483647') == 1) {
79 | $part = bcsub($part, '4294967296');
80 | }
81 | $part = (int) $part;
82 | }
83 |
84 | if (!$version) {
85 | // if the first 96bits is all zeros then we can safely assume we
86 | // actually have an IPv4 address. Even though it's technically possible
87 | // you're not really ever going to see an IPv6 address in the range:
88 | // ::0 - ::ffff
89 | // It's feasible to see an IPv6 address of "::", in which case the
90 | // caller is going to have to account for that on their own (or
91 | // pass $version to this function).
92 | if (($parts[1] | $parts[2] | $parts[3]) == 0) {
93 | $version = 4;
94 | }
95 | }
96 |
97 | if ($version == 4) {
98 | $ip = long2ip($parts[4]);
99 | } else {
100 | $packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]);
101 | $ip = inet_ntop($packed);
102 | }
103 |
104 | return $ip;
105 | }
106 |
107 | /**
108 | * Convert a human readable (presentational) IP address into a HEX string.
109 | */
110 | public static function inet_ptoh($ip)
111 | {
112 | return bin2hex(inet_pton($ip));
113 | //return BC::bcdechex(self::inet_ptod($ip));
114 | }
115 |
116 | /**
117 | * Convert a human readable (presentational) IP address into a BINARY string.
118 | */
119 | public static function inet_ptob($ip, $bits = null)
120 | {
121 | if ($bits === null) {
122 | $bits = self::isIPv4($ip) ? 32 : 128;
123 | }
124 | return BC::bcdecbin(self::inet_ptod($ip), $bits);
125 | }
126 |
127 | /**
128 | * Convert a binary string into an IP address (presentational) string.
129 | */
130 | public static function inet_btop($bin)
131 | {
132 | return self::inet_dtop(BC::bcbindec($bin));
133 | }
134 |
135 | /**
136 | * Convert a HEX string into a human readable (presentational) IP address
137 | */
138 | public static function inet_htop($hex)
139 | {
140 | return self::inet_dtop(BC::bchexdec($hex));
141 | }
142 |
143 | /**
144 | * Expand an IP address. IPv4 addresses are returned as-is.
145 | *
146 | * Example:
147 | * 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001
148 | * ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001
149 | * 127.0.0.1 expands to 127.0.0.1
150 | */
151 | public static function inet_expand($ip)
152 | {
153 | // strip possible cidr notation off
154 | if (($pos = strpos($ip, '/')) !== false) {
155 | $ip = substr($ip, 0, $pos);
156 | }
157 | $bytes = unpack('n*', inet_pton($ip));
158 | if (count($bytes) > 2) {
159 | return implode(':', array_map(function ($b) {
160 | return sprintf("%04x", $b);
161 | }, $bytes));
162 | }
163 | return $ip;
164 | }
165 |
166 | /**
167 | * Convert an IPv4 address into an IPv6 address.
168 | *
169 | * One use-case for this is IP 6to4 tunnels used in networking.
170 | *
171 | * @example
172 | * to_ipv6("10.10.10.10") == a0a:a0a
173 | *
174 | * @param string $ip IPv4 address.
175 | * @param bool $mapped If true a Full IPv6 address is returned within the
176 | * official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x"
177 | */
178 | public static function to_ipv6($ip, $mapped = false)
179 | {
180 | if (!self::isIPv4($ip)) {
181 | throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\"");
182 | }
183 |
184 | $num = IP::inet_ptod($ip);
185 | $o1 = dechex($num >> 16);
186 | $o2 = dechex($num & 0x0000FFFF);
187 |
188 | return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2";
189 | }
190 |
191 | /**
192 | * Returns true if the IP address is a valid IPv4 address
193 | */
194 | public static function isIPv4($ip)
195 | {
196 | return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
197 | }
198 |
199 | /**
200 | * Returns true if the IP address is a valid IPv6 address
201 | */
202 | public static function isIPv6($ip)
203 | {
204 | return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
205 | }
206 |
207 | /**
208 | * Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = >
209 | * the second.
210 | *
211 | * @param string $ip1 IP address
212 | * @param string $ip2 IP address to compare against
213 | * @return integer Return -1,0,1 depending if $ip1 is <=> $ip2
214 | */
215 | public static function cmp($ip1, $ip2)
216 | {
217 | return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0);
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/BC.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Lifo\IP;
12 |
13 | /**
14 | * BCMath helper class.
15 | *
16 | * Provides a handful of BCMath routines that are not included in the native
17 | * PHP library.
18 | *
19 | * Note: The Bitwise functions operate on fixed byte boundaries. For example,
20 | * comparing the following numbers uses X number of bits:
21 | * 0xFFFF and 0xFF will result in comparison of 16 bits.
22 | * 0xFFFFFFFF and 0xF will result in comparison of 32 bits.
23 | * etc...
24 | *
25 | */
26 | abstract class BC
27 | {
28 | // Some common (maybe useless) constants
29 | const MAX_INT_32 = '2147483647'; // 7FFFFFFF
30 | const MAX_UINT_32 = '4294967295'; // FFFFFFFF
31 | const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF
32 | const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF
33 | const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF
34 | const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF
35 | const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
36 | const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
37 |
38 | /**
39 | * BC Math function to convert a HEX string into a DECIMAL
40 | *
41 | * @param string $hex
42 | * @return float|int|string
43 | */
44 | public static function bchexdec($hex)
45 | {
46 | if (strlen($hex) == 1) {
47 | return hexdec($hex);
48 | }
49 |
50 | $remain = substr($hex, 0, -1);
51 | $last = substr($hex, -1);
52 | return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0);
53 | }
54 |
55 | /**
56 | * BC Math function to convert a DECIMAL string into a BINARY string
57 | *
58 | * @param string $dec
59 | * @param int $pad
60 | * @return string
61 | */
62 | public static function bcdecbin($dec, $pad = null)
63 | {
64 | $bin = '';
65 | while ($dec) {
66 | $m = bcmod($dec, 2);
67 | $dec = bcdiv($dec, 2, 0);
68 | $bin = abs($m) . $bin;
69 | }
70 | return $pad ? sprintf("%0{$pad}s", $bin) : $bin;
71 | }
72 |
73 | /**
74 | * BC Math function to convert a BINARY string into a DECIMAL string
75 | *
76 | * @param string $bin
77 | * @return string
78 | */
79 | public static function bcbindec($bin)
80 | {
81 | $dec = '0';
82 | for ($i = 0, $j = strlen($bin); $i < $j; $i++) {
83 | $dec = bcmul($dec, '2', 0);
84 | $dec = bcadd($dec, $bin[$i], 0);
85 | }
86 | return $dec;
87 | }
88 |
89 | /**
90 | * BC Math function to convert a BINARY string into a HEX string
91 | *
92 | * @param string $bin
93 | * @return string
94 | */
95 | public static function bcbinhex($bin)
96 | {
97 | return self::bcdechex(self::bcbindec($bin));
98 | }
99 |
100 | /**
101 | * BC Math function to convert a DECIMAL into a HEX string
102 | *
103 | * @param string $dec
104 | * @return string
105 | */
106 | public static function bcdechex($dec)
107 | {
108 | $last = bcmod($dec, 16);
109 | $remain = bcdiv(bcsub($dec, $last, 0), 16, 0);
110 | return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last);
111 | }
112 |
113 | /**
114 | * Bitwise AND two arbitrarily large numbers together.
115 | *
116 | * @param string $left
117 | * @param string $right
118 | * @return string
119 | */
120 | public static function bcand($left, $right)
121 | {
122 | $len = self::_bitwise($left, $right);
123 |
124 | $value = '';
125 | for ($i = 0; $i < $len; $i++) {
126 | $value .= (($left[$i] + 0) & ($right[$i] + 0)) ? '1' : '0';
127 | }
128 | return self::bcbindec($value != '' ? $value : '0');
129 | }
130 |
131 | /**
132 | * Bitwise OR two arbitrarily large numbers together.
133 | *
134 | * @param string $left
135 | * @param string $right
136 | * @return string
137 | */
138 | public static function bcor($left, $right)
139 | {
140 | $len = self::_bitwise($left, $right);
141 |
142 | $value = '';
143 | for ($i = 0; $i < $len; $i++) {
144 | $value .= (($left[$i] + 0) | ($right[$i] + 0)) ? '1' : '0';
145 | }
146 | return self::bcbindec($value != '' ? $value : '0');
147 | }
148 |
149 | /**
150 | * Bitwise XOR two arbitrarily large numbers together.
151 | *
152 | * @param string $left
153 | * @param string $right
154 | * @return string
155 | */
156 | public static function bcxor($left, $right)
157 | {
158 | $len = self::_bitwise($left, $right);
159 |
160 | $value = '';
161 | for ($i = 0; $i < $len; $i++) {
162 | $value .= (($left[$i] + 0) ^ ($right[$i] + 0)) ? '1' : '0';
163 | }
164 | return self::bcbindec($value != '' ? $value : '0');
165 | }
166 |
167 | /**
168 | * Bitwise NOT two arbitrarily large numbers together.
169 | *
170 | * @param string $left
171 | * @param int $bits
172 | * @return string
173 | */
174 | public static function bcnot($left, $bits = null)
175 | {
176 | $right = 0;
177 | $len = self::_bitwise($left, $right, $bits);
178 | $value = '';
179 | for ($i = 0; $i < $len; $i++) {
180 | $value .= $left[$i] == '1' ? '0' : '1';
181 | }
182 | return self::bcbindec($value);
183 | }
184 |
185 | /**
186 | * Shift number to the left
187 | *
188 | * @param string $num
189 | * @param integer $bits Total bits to shift
190 | * @return string
191 | */
192 | public static function bcleft($num, $bits)
193 | {
194 | return bcmul($num, bcpow('2', $bits));
195 | }
196 |
197 | /**
198 | * Shift number to the right
199 | *
200 | * @param string $num
201 | * @param integer $bits Total bits to shift
202 | * @return string|null
203 | */
204 | public static function bcright($num, $bits)
205 | {
206 | return bcdiv($num, bcpow('2', $bits));
207 | }
208 |
209 | /**
210 | * Determine how many bits are needed to store the number rounded to the
211 | * nearest bit boundary.
212 | *
213 | * @param string $num
214 | * @param int $boundary
215 | * @return float|int
216 | */
217 | public static function bits_needed($num, $boundary = 4)
218 | {
219 | $bits = 0;
220 | while ($num > 0) {
221 | $num = bcdiv($num, '2', 0);
222 | $bits++;
223 | }
224 | // round to nearest boundrary
225 | return $boundary ? ceil($bits / $boundary) * $boundary : $bits;
226 | }
227 |
228 | /**
229 | * BC Math function to return an arbitrarily large random number.
230 | *
231 | * @param string $min
232 | * @param string $max
233 | * @return string
234 | */
235 | public static function bcrand($min, $max = null)
236 | {
237 | if ($max === null) {
238 | $max = $min;
239 | $min = 0;
240 | }
241 |
242 | // swap values if $min > $max
243 | if (bccomp($min, $max) == 1) {
244 | list($min, $max) = array($max, $min);
245 | }
246 |
247 | return bcadd(
248 | bcmul(
249 | bcdiv(
250 | mt_rand(0, mt_getrandmax()),
251 | mt_getrandmax(),
252 | strlen($max)
253 | ),
254 | bcsub(
255 | bcadd($max, '1'),
256 | $min
257 | )
258 | ),
259 | $min
260 | );
261 | }
262 |
263 | /**
264 | * Computes the natural logarithm using a series.
265 | *
266 | * @param string $num
267 | * @param int $iter
268 | * @param int $scale
269 | * @return string
270 | * @author Thomas Oldbury.
271 | * @license Public domain.
272 | */
273 | public static function bclog($num, $iter = 10, $scale = 100)
274 | {
275 | $log = "0.0";
276 | for ($i = 0; $i < $iter; $i++) {
277 | $pow = 1 + (2 * $i);
278 | $mul = bcdiv("1.0", $pow, $scale);
279 | $fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale);
280 | $log = bcadd($fraction, $log, $scale);
281 | }
282 | return bcmul("2.0", $log, $scale);
283 | }
284 |
285 | /**
286 | * Computes the base2 log using baseN log.
287 | *
288 | * @param string $num
289 | * @param int $iter
290 | * @param int $scale
291 | * @return string|null
292 | */
293 | public static function bclog2($num, $iter = 10, $scale = 100)
294 | {
295 | return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale);
296 | }
297 |
298 | /**
299 | * Rounds fractions down
300 | *
301 | * @param string $num
302 | * @return string
303 | */
304 | public static function bcfloor($num)
305 | {
306 | if (substr($num, 0, 1) == '-') {
307 | return bcsub($num, 1, 0);
308 | }
309 | return bcadd($num, 0, 0);
310 | }
311 |
312 | /**
313 | * Rounds fractions up
314 | *
315 | * @param string $num
316 | * @return string
317 | */
318 | public static function bcceil($num)
319 | {
320 | if (substr($num, 0, 1) == '-') {
321 | return bcsub($num, 0, 0);
322 | }
323 | return bcadd($num, 1, 0);
324 | }
325 |
326 | /**
327 | * Compare two numbers and return -1, 0, 1 depending if the LEFT number is
328 | * < = > the RIGHT.
329 | *
330 | * @param string|integer $left Left side operand
331 | * @param string|integer $right Right side operand
332 | * @return integer Return -1,0,1 for <=> comparison
333 | */
334 | public static function cmp($left, $right)
335 | {
336 | // @todo could an optimization be done to determine if a normal 32bit
337 | // comparison could be done instead of using bccomp? But would
338 | // the number verification cause too much overhead to be useful?
339 | return bccomp($left, $right, 0);
340 | }
341 |
342 | /**
343 | * Internal function to prepare for bitwise operations
344 | *
345 | * @param string $left
346 | * @param string $right
347 | * @param int $bits
348 | * @return mixed
349 | */
350 | private static function _bitwise(&$left, &$right, $bits = null)
351 | {
352 | if ($bits === null) {
353 | $bits = max(self::bits_needed($left), self::bits_needed($right));
354 | }
355 |
356 | $left = self::bcdecbin($left);
357 | $right = self::bcdecbin($right);
358 |
359 | $len = max(strlen($left), strlen($right), (int)$bits);
360 |
361 | $left = sprintf("%0{$len}s", $left);
362 | $right = sprintf("%0{$len}s", $right);
363 |
364 | return $len;
365 | }
366 |
367 | }
368 |
--------------------------------------------------------------------------------
/src/CIDR.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Lifo\IP;
11 |
12 | /**
13 | * CIDR Block helper class.
14 | *
15 | * Most routines can be used statically or by instantiating an object and
16 | * calling its methods.
17 | *
18 | * Provides routines to do various calculations on IP addresses and ranges.
19 | * Convert to/from CIDR to ranges, etc.
20 | */
21 | class CIDR
22 | {
23 | const INTERSECT_NO = 0;
24 | const INTERSECT_YES = 1;
25 | const INTERSECT_LOW = 2;
26 | const INTERSECT_HIGH = 3;
27 |
28 | protected $start;
29 | protected $end;
30 | protected $prefix;
31 | protected $version;
32 | protected $istart;
33 | protected $iend;
34 |
35 | private $cache;
36 |
37 | /**
38 | * Create a new CIDR object.
39 | *
40 | * The IP range can be arbitrary and does not have to fall on a valid CIDR
41 | * range. Some methods will return different values depending if you ignore
42 | * the prefix or not. By default all prefix sensitive methods will assume
43 | * the prefix is used.
44 | *
45 | * @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24),
46 | * or range "1.2.3.4-1.2.3.10"
47 | * @param string $end Ending IP in range if no cidr/prefix is given
48 | */
49 | public function __construct($cidr, $end = null)
50 | {
51 | if ($end !== null) {
52 | $this->setRange($cidr, $end);
53 | } else {
54 | $this->setCidr($cidr);
55 | }
56 | }
57 |
58 | /**
59 | * Returns the string representation of the CIDR block.
60 | */
61 | public function __toString()
62 | {
63 | // do not include the prefix if its a single IP
64 | try {
65 | if ($this->isTrueCidr() && (
66 | ($this->version == 4 and $this->prefix != 32) ||
67 | ($this->version == 6 and $this->prefix != 128)
68 | )
69 | ) {
70 | return $this->start . '/' . $this->prefix;
71 | }
72 | } catch (\Exception $e) {
73 | // isTrueCidr() calls getRange which can throw an exception
74 | }
75 | if (strcmp($this->start, $this->end) == 0) {
76 | return $this->start;
77 | }
78 | return $this->start . ' - ' . $this->end;
79 | }
80 |
81 | public function __clone()
82 | {
83 | // do not clone the cache. No real reason why. I just want to keep the
84 | // memory foot print as low as possible, even though this is trivial.
85 | $this->cache = array();
86 | }
87 |
88 | /**
89 | * Set an arbitrary IP range.
90 | * The closest matching prefix will be calculated but the actual range
91 | * stored in the object can be arbitrary.
92 | * @param string $start Starting IP or combination "start-end" string.
93 | * @param string $end Ending IP or null.
94 | */
95 | public function setRange($ip, $end = null)
96 | {
97 | if (strpos($ip, '-') !== false) {
98 | list($ip, $end) = array_map('trim', explode('-', $ip, 2));
99 | }
100 |
101 | if (false === filter_var($ip, FILTER_VALIDATE_IP) ||
102 | false === filter_var($end, FILTER_VALIDATE_IP)) {
103 | throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\"");
104 | }
105 |
106 | // determine version (4 or 6)
107 | $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
108 |
109 | $this->istart = IP::inet_ptod($ip);
110 | $this->iend = IP::inet_ptod($end);
111 |
112 | // fix order
113 | if (bccomp($this->istart, $this->iend) == 1) {
114 | list($this->istart, $this->iend) = array($this->iend, $this->istart);
115 | list($ip, $end) = array($end, $ip);
116 | }
117 |
118 | $this->start = $ip;
119 | $this->end = $end;
120 |
121 | // calculate real prefix
122 | $len = $this->version == 4 ? 32 : 128;
123 | $this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend)));
124 | }
125 |
126 | /**
127 | * Returns true if the current IP is a true cidr block
128 | */
129 | public function isTrueCidr()
130 | {
131 | return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast();
132 | }
133 |
134 | /**
135 | * Set the CIDR block.
136 | *
137 | * The prefix length is optional and will default to 32 ot 128 depending on
138 | * the version detected.
139 | *
140 | * @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64"
141 | * @throws \InvalidArgumentException If the CIDR block is invalid
142 | */
143 | public function setCidr($cidr)
144 | {
145 | if (strpos($cidr, '-') !== false) {
146 | return $this->setRange($cidr);
147 | }
148 |
149 | list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null);
150 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
151 | throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
152 | }
153 |
154 | // determine version (4 or 6)
155 | $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
156 |
157 | $this->start = $ip;
158 | $this->istart = IP::inet_ptod($ip);
159 |
160 | if ($bits !== null and $bits !== '') {
161 | $this->prefix = $bits;
162 | } else {
163 | $this->prefix = $this->version == 4 ? 32 : 128;
164 | }
165 |
166 | if (($this->prefix < 0)
167 | || ($this->prefix > 32 and $this->version == 4)
168 | || ($this->prefix > 128 and $this->version == 6)) {
169 | throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
170 | }
171 |
172 | $this->end = $this->getBroadcast();
173 | $this->iend = IP::inet_ptod($this->end);
174 |
175 | $this->cache = array();
176 | }
177 |
178 | /**
179 | * Get the IP version. 4 or 6.
180 | *
181 | * @return integer
182 | */
183 | public function getVersion()
184 | {
185 | return $this->version;
186 | }
187 |
188 | /**
189 | * Get the prefix.
190 | *
191 | * Always returns the "proper" prefix, even if the IP range is arbitrary.
192 | *
193 | * @return integer
194 | */
195 | public function getPrefix()
196 | {
197 | return $this->prefix;
198 | }
199 |
200 | /**
201 | * Return the starting presentational IP or Decimal value.
202 | *
203 | * Ignores prefix
204 | */
205 | public function getStart($decimal = false)
206 | {
207 | return $decimal ? $this->istart : $this->start;
208 | }
209 |
210 | /**
211 | * Return the ending presentational IP or Decimal value.
212 | *
213 | * Ignores prefix
214 | */
215 | public function getEnd($decimal = false)
216 | {
217 | return $decimal ? $this->iend : $this->end;
218 | }
219 |
220 | /**
221 | * Return the next presentational IP or Decimal value (following the
222 | * broadcast address of the current CIDR block).
223 | */
224 | public function getNext($decimal = false)
225 | {
226 | $next = bcadd($this->getEnd(true), '1');
227 | return $decimal ? $next : new self(IP::inet_dtop($next));
228 | }
229 |
230 | /**
231 | * Returns true if the IP is an IPv4
232 | *
233 | * @return bool
234 | */
235 | public function isIPv4()
236 | {
237 | return $this->version == 4;
238 | }
239 |
240 | /**
241 | * Returns true if the IP is an IPv6
242 | *
243 | * @return bool
244 | */
245 | public function isIPv6()
246 | {
247 | return $this->version == 6;
248 | }
249 |
250 | /**
251 | * Get the cidr notation for the subnet block.
252 | *
253 | * This is useful for when you want a string representation of the IP/prefix
254 | * and the starting IP is not on a valid network boundrary (eg: Displaying
255 | * an IP from an interface).
256 | *
257 | * Note: The CIDR block returned is NOT always bit aligned.
258 | *
259 | * @return string IP in CIDR notation "start_ip/prefix"
260 | */
261 | public function getCidr()
262 | {
263 | return $this->start . '/' . $this->prefix;
264 | }
265 |
266 | /**
267 | * Get the TRUE cidr notation for the subnet block.
268 | *
269 | * This is useful for when you want a string representation of the IP/prefix
270 | * and the starting IP is not on a valid network boundrary (eg: Displaying
271 | * an IP from an interface).
272 | *
273 | * Note: The CIDR block returned is ALWAYS bit aligned.
274 | *
275 | * @return string IP in CIDR notation "network/prefix"
276 | */
277 | public function getTrueCidr()
278 | {
279 | return $this->getNetwork() . '/' . $this->prefix;
280 | }
281 |
282 | /**
283 | * Get the [low,high] range of the CIDR block
284 | *
285 | * Prefix sensitive.
286 | *
287 | * @param bool $ignorePrefix If true the arbitrary start-end range is
288 | * returned. default=false.
289 | */
290 | public function getRange($ignorePrefix = false)
291 | {
292 | $range = $ignorePrefix
293 | ? array($this->start, $this->end)
294 | : self::cidr_to_range($this->start, $this->prefix);
295 | // watch out for IP '0' being converted to IPv6 '::'
296 | if ($range[0] == '::' and strpos($range[1], ':') == false) {
297 | $range[0] = '0.0.0.0';
298 | }
299 | return $range;
300 | }
301 |
302 | /**
303 | * Return the IP in its fully expanded form.
304 | *
305 | * For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001
306 | *
307 | * @see IP::inet_expand
308 | */
309 | public function getExpanded()
310 | {
311 | return IP::inet_expand($this->start);
312 | }
313 |
314 | /**
315 | * Get network IP of the CIDR block
316 | *
317 | * Prefix sensitive.
318 | *
319 | * @param bool $ignorePrefix If true the arbitrary start-end range is
320 | * returned. default=false.
321 | */
322 | public function getNetwork($ignorePrefix = false)
323 | {
324 | // micro-optimization to prevent calling getRange repeatedly
325 | $k = $ignorePrefix ? 1 : 0;
326 | if (!isset($this->cache['range'][$k])) {
327 | $this->cache['range'][$k] = $this->getRange($ignorePrefix);
328 | }
329 | return $this->cache['range'][$k][0];
330 | }
331 |
332 | /**
333 | * Get broadcast IP of the CIDR block
334 | *
335 | * Prefix sensitive.
336 | *
337 | * @param bool $ignorePrefix If true the arbitrary start-end range is
338 | * returned. default=false.
339 | */
340 | public function getBroadcast($ignorePrefix = false)
341 | {
342 | // micro-optimization to prevent calling getRange repeatedly
343 | $k = $ignorePrefix ? 1 : 0;
344 | if (!isset($this->cache['range'][$k])) {
345 | $this->cache['range'][$k] = $this->getRange($ignorePrefix);
346 | }
347 | return $this->cache['range'][$k][1];
348 | }
349 |
350 | /**
351 | * Get the network mask based on the prefix.
352 | *
353 | */
354 | public function getMask()
355 | {
356 | return self::prefix_to_mask($this->prefix, $this->version);
357 | }
358 |
359 | /**
360 | * Get total hosts within CIDR range
361 | *
362 | * Prefix sensitive.
363 | *
364 | * @param bool $ignorePrefix If true the arbitrary start-end range is
365 | * returned. default=false.
366 | */
367 | public function getTotal($ignorePrefix = false)
368 | {
369 | // micro-optimization to prevent calling getRange repeatedly
370 | $k = $ignorePrefix ? 1 : 0;
371 | if (!isset($this->cache['range'][$k])) {
372 | $this->cache['range'][$k] = $this->getRange($ignorePrefix);
373 | }
374 | return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]),
375 | IP::inet_ptod($this->cache['range'][$k][0])), '1');
376 | }
377 |
378 | public function intersects($cidr)
379 | {
380 | return self::cidr_intersect((string)$this, $cidr);
381 | }
382 |
383 | /**
384 | * Determines the intersection between an IP (with optional prefix) and a
385 | * CIDR block.
386 | *
387 | * The IP will be checked against the CIDR block given and will either be
388 | * inside or outside the CIDR completely, or partially.
389 | *
390 | * NOTE: The caller should explicitly check against the INTERSECT_*
391 | * constants because this method will return a value > 1 even for partial
392 | * matches.
393 | *
394 | * @param mixed $ip The IP/cidr to match
395 | * @param mixed $cidr The CIDR block to match within
396 | * @return integer Returns an INTERSECT_* constant
397 | * @throws \InvalidArgumentException if either $ip or $cidr is invalid
398 | */
399 | public static function cidr_intersect($ip, $cidr)
400 | {
401 | // use fixed length HEX strings so we can easily do STRING comparisons
402 | // instead of using slower bccomp() math.
403 | $map = function($v){ return sprintf("%032s", IP::inet_ptoh($v)); };
404 | list($lo,$hi) = array_map($map, CIDR::cidr_to_range($ip));
405 | list($min,$max) = array_map($map, CIDR::cidr_to_range($cidr));
406 |
407 | /** visualization of logic used below
408 | lo-hi = $ip to check
409 | min-max = $cidr block being checked against
410 | --- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check
411 | --- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match
412 | --- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match
413 | --- --- --- --- min max --- --- --- --- --- --- No match "NO"
414 | --- --- --- --- --- --- --- --- min --- max --- No match "NO"
415 | min --- max --- --- --- --- --- --- --- --- --- No match "NO"
416 | --- --- min --- --- --- --- max --- --- --- --- Full match "YES"
417 | */
418 |
419 | // IP is exact match or completely inside the CIDR block
420 | if ($lo >= $min and $hi <= $max) {
421 | return self::INTERSECT_YES;
422 | }
423 |
424 | // IP is completely outside the CIDR block
425 | if ($max < $lo or $min > $hi) {
426 | return self::INTERSECT_NO;
427 | }
428 |
429 | // @todo is it useful to return LOW/HIGH partial matches?
430 |
431 | // IP matches the lower end
432 | if ($max <= $hi and $min <= $lo) {
433 | return self::INTERSECT_LOW;
434 | }
435 |
436 | // IP matches the higher end
437 | if ($min >= $lo and $max >= $hi) {
438 | return self::INTERSECT_HIGH;
439 | }
440 |
441 | return self::INTERSECT_NO;
442 | }
443 |
444 | /**
445 | * Converts an IPv4 or IPv6 CIDR block into its range.
446 | *
447 | * @todo May not be the fastest way to do this.
448 | *
449 | * @static
450 | * @param string $cidr CIDR block or IP address string.
451 | * @param integer|null $bits If /bits is not specified on string they can be
452 | * passed via this parameter instead.
453 | * @return array A 2 element array with the low, high range
454 | */
455 | public static function cidr_to_range($cidr, $bits = null)
456 | {
457 | if (strpos($cidr, '/') !== false) {
458 | list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null);
459 | } else {
460 | $ip = $cidr;
461 | $_bits = $bits;
462 | }
463 |
464 | if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
465 | throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
466 | }
467 |
468 | // force bit length to 32 or 128 depending on type of IP
469 | $version = IP::isIPv4($ip) ? 4 : 6;
470 | $bitlen = $version == 4 ? 32 : 128;
471 |
472 | if ($bits === null) {
473 | // if no prefix is given use the length of the binary string which
474 | // will give us 32 or 128 and result in a single IP being returned.
475 | $bits = $_bits !== null ? $_bits : $bitlen;
476 | }
477 |
478 | if ($bits > $bitlen) {
479 | throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
480 | }
481 |
482 | $ipdec = IP::inet_ptod($ip);
483 | $ipbin = BC::bcdecbin($ipdec, $bitlen);
484 |
485 | // calculate network
486 | $netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0'));
487 | $ip1 = BC::bcand($ipdec, $netmask);
488 |
489 | // calculate "broadcast" (not technically a broadcast in IPv6)
490 | $ip2 = BC::bcor($ip1, BC::bcnot($netmask));
491 |
492 | return array(IP::inet_dtop($ip1, $version), IP::inet_dtop($ip2, $version));
493 | }
494 |
495 | /**
496 | * Return the CIDR string from the range given
497 | */
498 | public static function range_to_cidr($start, $end)
499 | {
500 | $cidr = new CIDR($start, $end);
501 | return (string)$cidr;
502 | }
503 |
504 | /**
505 | * Return the maximum prefix length that would fit the IP address given.
506 | *
507 | * This is useful to determine how my bit would be needed to store the IP
508 | * address when you don't already have a prefix for the IP.
509 | *
510 | * @example 216.240.32.0 would return 27
511 | *
512 | * @param string $ip IP address without prefix
513 | * @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6
514 | */
515 | public static function max_prefix($ip, $bits = null)
516 | {
517 | static $mask = array();
518 |
519 | $ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
520 | $max = $ver == 6 ? 128 : 32;
521 | if ($bits === null) {
522 | $bits = $max;
523 |
524 | }
525 |
526 | $int = IP::inet_ptod($ip);
527 | while ($bits > 0) {
528 | // micro-optimization; calculate mask once ...
529 | if (!isset($mask[$ver][$bits-1])) {
530 | // 2^$max - 2^($max - $bits);
531 | if ($ver == 4) {
532 | $mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1));
533 | } else {
534 | $mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1)));
535 | }
536 | }
537 |
538 | $m = $mask[$ver][$bits-1];
539 | //printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m)));
540 | //echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n";
541 | if (bccomp(BC::bcand($int, $m), $int) != 0) {
542 | return $bits;
543 | }
544 | $bits--;
545 | }
546 | return $bits;
547 | }
548 |
549 | /**
550 | * Return a contiguous list of true CIDR blocks that span the range given.
551 | *
552 | * Note: It's not a good idea to call this with IPv6 addresses. While it may
553 | * work for certain ranges this can be very slow. Also an IPv6 list won't be
554 | * as accurate as an IPv4 list.
555 | *
556 | * @example
557 | * range_to_cidrlist(192.168.0.0, 192.168.0.15) ==
558 | * 192.168.0.0/28
559 | * range_to_cidrlist(192.168.0.0, 192.168.0.20) ==
560 | * 192.168.0.0/28
561 | * 192.168.0.16/30
562 | * 192.168.0.20/32
563 | */
564 | public static function range_to_cidrlist($start, $end)
565 | {
566 | $ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
567 | $start = IP::inet_ptod($start);
568 | $end = IP::inet_ptod($end);
569 |
570 | $len = $ver == 4 ? 32 : 128;
571 | $log2 = $ver == 4 ? log(2) : BC::bclog(2);
572 |
573 | $list = array();
574 | while (BC::cmp($end, $start) >= 0) { // $end >= $start
575 | $prefix = self::max_prefix(IP::inet_dtop($start), $len);
576 | if ($ver == 4) {
577 | $diff = $len - floor( log($end - $start + 1) / $log2 );
578 | } else {
579 | // this is not as accurate due to the bclog function
580 | $diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2)));
581 | }
582 |
583 | if ($prefix < $diff) {
584 | $prefix = $diff;
585 | }
586 |
587 | $list[] = IP::inet_dtop($start) . "/" . $prefix;
588 |
589 | if ($ver == 4) {
590 | $start += pow(2, $len - $prefix);
591 | } else {
592 | $start = bcadd($start, bcpow(2, $len - $prefix));
593 | }
594 | }
595 | return $list;
596 | }
597 |
598 | /**
599 | * Return an list of optimized CIDR blocks by collapsing adjacent CIDR
600 | * blocks into larger blocks.
601 | *
602 | * @param array $cidrs List of CIDR block strings or objects
603 | * @param integer $maxPrefix Maximum prefix to allow
604 | * @return array Optimized list of CIDR objects
605 | */
606 | public static function optimize_cidrlist($cidrs, $maxPrefix = 32)
607 | {
608 | // all indexes must be a CIDR object
609 | $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
610 | // sort CIDR blocks in proper order so we can easily loop over them
611 | $cidrs = self::cidr_sort($cidrs);
612 |
613 | $list = array();
614 | while ($cidrs) {
615 | $c = array_shift($cidrs);
616 | $start = $c->getStart();
617 |
618 | $max = bcadd($c->getStart(true), $c->getTotal());
619 |
620 | // loop through each cidr block until its ending range is more than
621 | // the current maximum.
622 | while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) {
623 | $b = array_shift($cidrs);
624 | $newmax = bcadd($b->getStart(true), $b->getTotal());
625 | if ($newmax > $max) {
626 | $max = $newmax;
627 | }
628 | }
629 |
630 | // add the new cidr range to the optimized list
631 | $list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1'))));
632 | }
633 |
634 | return $list;
635 | }
636 |
637 | /**
638 | * Sort the list of CIDR blocks, optionally with a custom callback function.
639 | *
640 | * @param array $cidrs A list of CIDR blocks (strings or objects)
641 | * @param Closure $callback Optional callback to perform the sorting.
642 | * See PHP usort documentation for more details.
643 | */
644 | public static function cidr_sort($cidrs, $callback = null)
645 | {
646 | // all indexes must be a CIDR object
647 | $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
648 |
649 | if ($callback === null) {
650 | $callback = function($a, $b) {
651 | if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) {
652 | return $o; // < or >
653 | }
654 | if ($a->getPrefix() == $b->getPrefix()) {
655 | return 0;
656 | }
657 | return $a->getPrefix() < $b->getPrefix() ? -1 : 1;
658 | };
659 | } elseif (!($callback instanceof \Closure) or !is_callable($callback)) {
660 | throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback));
661 | }
662 |
663 | usort($cidrs, $callback);
664 | return $cidrs;
665 | }
666 |
667 | /**
668 | * Return the Prefix bits from the IPv4 mask given.
669 | *
670 | * This is only valid for IPv4 addresses since IPv6 addressing does not
671 | * have a concept of network masks.
672 | *
673 | * Example: 255.255.255.0 == 24
674 | *
675 | * @param string $mask IPv4 network mask.
676 | */
677 | public static function mask_to_prefix($mask)
678 | {
679 | if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
680 | throw new \InvalidArgumentException("Invalid IP netmask \"$mask\"");
681 | }
682 | return strrpos(IP::inet_ptob($mask, 32), '1') + 1;
683 | }
684 |
685 | /**
686 | * Return the network mask for the prefix given.
687 | *
688 | * Normally this is only useful for IPv4 addresses but you can generate a
689 | * mask for IPv6 addresses as well, only because its mathematically
690 | * possible.
691 | *
692 | * @param integer $prefix CIDR prefix bits (0-128)
693 | * @param integer $version IP version. If null the version will be detected
694 | * based on the prefix length given.
695 | */
696 | public static function prefix_to_mask($prefix, $version = null)
697 | {
698 | if ($version === null) {
699 | $version = $prefix > 32 ? 6 : 4;
700 | }
701 | if ($prefix < 0 or $prefix > 128) {
702 | throw new \InvalidArgumentException("Invalid prefix length \"$prefix\"");
703 | }
704 | if ($version != 4 and $version != 6) {
705 | throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6");
706 | }
707 |
708 | if ($version == 4) {
709 | return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix));
710 | } else {
711 | return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix));
712 | }
713 | }
714 |
715 | /**
716 | * Return true if the $ip given is a true CIDR block.
717 | *
718 | * A true CIDR block is one where the $ip given is the actual Network
719 | * address and broadcast matches the prefix appropriately.
720 | */
721 | public static function cidr_is_true($ip)
722 | {
723 | $ip = new CIDR($ip);
724 | return $ip->isTrueCidr();
725 | }
726 | }
727 |
--------------------------------------------------------------------------------