IP.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <?php
  2. /**
  3. * Functions and constants to play with IP addresses and ranges
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @author Antoine Musso "<hashar at free dot fr>"
  22. */
  23. use Wikimedia\IPSet;
  24. // Some regex definition to "play" with IP address and IP address ranges
  25. // An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255
  26. define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' );
  27. define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
  28. // An IPv4 range is an IP address and a prefix (d1 to d32)
  29. define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' );
  30. define( 'RE_IP_RANGE', RE_IP_ADD . '\/' . RE_IP_PREFIX );
  31. // An IPv6 address is made up of 8 words (each x0000 to xFFFF).
  32. // However, the "::" abbreviation can be used on consecutive x0000 words.
  33. define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
  34. define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' );
  35. define( 'RE_IPV6_ADD',
  36. '(?:' . // starts with "::" (including "::")
  37. ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' .
  38. '|' . // ends with "::" (except "::")
  39. RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' .
  40. '|' . // contains one "::" in the middle (the ^ makes the test fail if none found)
  41. RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' .
  42. '|' . // contains no "::"
  43. RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' .
  44. ')'
  45. );
  46. // An IPv6 range is an IP address and a prefix (d1 to d128)
  47. define( 'RE_IPV6_RANGE', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
  48. // For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
  49. define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
  50. define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
  51. // This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network
  52. define( 'IP_ADDRESS_STRING',
  53. '(?:' .
  54. RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4
  55. '|' .
  56. RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6
  57. ')'
  58. );
  59. /**
  60. * A collection of public static functions to play with IP address
  61. * and IP ranges.
  62. */
  63. class IP {
  64. /**
  65. * Determine if a string is as valid IP address or network (CIDR prefix).
  66. * SIIT IPv4-translated addresses are rejected.
  67. * @note canonicalize() tries to convert translated addresses to IPv4.
  68. *
  69. * @param string $ip Possible IP address
  70. * @return bool
  71. */
  72. public static function isIPAddress( $ip ) {
  73. return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip );
  74. }
  75. /**
  76. * Given a string, determine if it as valid IP in IPv6 only.
  77. * @note Unlike isValid(), this looks for networks too.
  78. *
  79. * @param string $ip Possible IP address
  80. * @return bool
  81. */
  82. public static function isIPv6( $ip ) {
  83. return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip );
  84. }
  85. /**
  86. * Given a string, determine if it as valid IP in IPv4 only.
  87. * @note Unlike isValid(), this looks for networks too.
  88. *
  89. * @param string $ip Possible IP address
  90. * @return bool
  91. */
  92. public static function isIPv4( $ip ) {
  93. return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip );
  94. }
  95. /**
  96. * Validate an IP address. Ranges are NOT considered valid.
  97. * SIIT IPv4-translated addresses are rejected.
  98. * @note canonicalize() tries to convert translated addresses to IPv4.
  99. *
  100. * @param string $ip
  101. * @return bool True if it is valid
  102. */
  103. public static function isValid( $ip ) {
  104. return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip )
  105. || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) );
  106. }
  107. /**
  108. * Validate an IP range (valid address with a valid CIDR prefix).
  109. * SIIT IPv4-translated addresses are rejected.
  110. * @note canonicalize() tries to convert translated addresses to IPv4.
  111. *
  112. * @param string $ipRange
  113. * @return bool True if it is valid
  114. * @since 1.30
  115. */
  116. public static function isValidRange( $ipRange ) {
  117. return ( preg_match( '/^' . RE_IPV6_RANGE . '$/', $ipRange )
  118. || preg_match( '/^' . RE_IP_RANGE . '$/', $ipRange ) );
  119. }
  120. /**
  121. * Convert an IP into a verbose, uppercase, normalized form.
  122. * Both IPv4 and IPv6 addresses are trimmed. Additionally,
  123. * IPv6 addresses in octet notation are expanded to 8 words;
  124. * IPv4 addresses have leading zeros, in each octet, removed.
  125. *
  126. * @param string $ip IP address in quad or octet form (CIDR or not).
  127. * @return string
  128. */
  129. public static function sanitizeIP( $ip ) {
  130. $ip = trim( $ip );
  131. if ( $ip === '' ) {
  132. return null;
  133. }
  134. /* If not an IP, just return trimmed value, since sanitizeIP() is called
  135. * in a number of contexts where usernames are supplied as input.
  136. */
  137. if ( !self::isIPAddress( $ip ) ) {
  138. return $ip;
  139. }
  140. if ( self::isIPv4( $ip ) ) {
  141. // Remove leading 0's from octet representation of IPv4 address
  142. $ip = preg_replace( '!(?:^|(?<=\.))0+(?=[1-9]|0[./]|0$)!', '', $ip );
  143. return $ip;
  144. }
  145. // Remove any whitespaces, convert to upper case
  146. $ip = strtoupper( $ip );
  147. // Expand zero abbreviations
  148. $abbrevPos = strpos( $ip, '::' );
  149. if ( $abbrevPos !== false ) {
  150. // We know this is valid IPv6. Find the last index of the
  151. // address before any CIDR number (e.g. "a:b:c::/24").
  152. $CIDRStart = strpos( $ip, "/" );
  153. $addressEnd = ( $CIDRStart !== false )
  154. ? $CIDRStart - 1
  155. : strlen( $ip ) - 1;
  156. // If the '::' is at the beginning...
  157. if ( $abbrevPos == 0 ) {
  158. $repeat = '0:';
  159. $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::'
  160. $pad = 9; // 7+2 (due to '::')
  161. // If the '::' is at the end...
  162. } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) {
  163. $repeat = ':0';
  164. $extra = '';
  165. $pad = 9; // 7+2 (due to '::')
  166. // If the '::' is in the middle...
  167. } else {
  168. $repeat = ':0';
  169. $extra = ':';
  170. $pad = 8; // 6+2 (due to '::')
  171. }
  172. $ip = str_replace( '::',
  173. str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra,
  174. $ip
  175. );
  176. }
  177. // Remove leading zeros from each bloc as needed
  178. $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip );
  179. return $ip;
  180. }
  181. /**
  182. * Prettify an IP for display to end users.
  183. * This will make it more compact and lower-case.
  184. *
  185. * @param string $ip
  186. * @return string
  187. */
  188. public static function prettifyIP( $ip ) {
  189. $ip = self::sanitizeIP( $ip ); // normalize (removes '::')
  190. if ( self::isIPv6( $ip ) ) {
  191. // Split IP into an address and a CIDR
  192. if ( strpos( $ip, '/' ) !== false ) {
  193. list( $ip, $cidr ) = explode( '/', $ip, 2 );
  194. } else {
  195. list( $ip, $cidr ) = [ $ip, '' ];
  196. }
  197. // Get the largest slice of words with multiple zeros
  198. $offset = 0;
  199. $longest = $longestPos = false;
  200. while ( preg_match(
  201. '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset
  202. ) ) {
  203. list( $match, $pos ) = $m[0]; // full match
  204. if ( strlen( $match ) > strlen( $longest ) ) {
  205. $longest = $match;
  206. $longestPos = $pos;
  207. }
  208. $offset = ( $pos + strlen( $match ) ); // advance
  209. }
  210. if ( $longest !== false ) {
  211. // Replace this portion of the string with the '::' abbreviation
  212. $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) );
  213. }
  214. // Add any CIDR back on
  215. if ( $cidr !== '' ) {
  216. $ip = "{$ip}/{$cidr}";
  217. }
  218. // Convert to lower case to make it more readable
  219. $ip = strtolower( $ip );
  220. }
  221. return $ip;
  222. }
  223. /**
  224. * Given a host/port string, like one might find in the host part of a URL
  225. * per RFC 2732, split the hostname part and the port part and return an
  226. * array with an element for each. If there is no port part, the array will
  227. * have false in place of the port. If the string was invalid in some way,
  228. * false is returned.
  229. *
  230. * This was easy with IPv4 and was generally done in an ad-hoc way, but
  231. * with IPv6 it's somewhat more complicated due to the need to parse the
  232. * square brackets and colons.
  233. *
  234. * A bare IPv6 address is accepted despite the lack of square brackets.
  235. *
  236. * @param string $both The string with the host and port
  237. * @return array|false Array normally, false on certain failures
  238. */
  239. public static function splitHostAndPort( $both ) {
  240. if ( substr( $both, 0, 1 ) === '[' ) {
  241. if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) {
  242. if ( isset( $m['port'] ) ) {
  243. return [ $m[1], intval( $m['port'] ) ];
  244. } else {
  245. return [ $m[1], false ];
  246. }
  247. } else {
  248. // Square bracket found but no IPv6
  249. return false;
  250. }
  251. }
  252. $numColons = substr_count( $both, ':' );
  253. if ( $numColons >= 2 ) {
  254. // Is it a bare IPv6 address?
  255. if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) {
  256. return [ $both, false ];
  257. } else {
  258. // Not valid IPv6, but too many colons for anything else
  259. return false;
  260. }
  261. }
  262. if ( $numColons >= 1 ) {
  263. // Host:port?
  264. $bits = explode( ':', $both );
  265. if ( preg_match( '/^\d+/', $bits[1] ) ) {
  266. return [ $bits[0], intval( $bits[1] ) ];
  267. } else {
  268. // Not a valid port
  269. return false;
  270. }
  271. }
  272. // Plain hostname
  273. return [ $both, false ];
  274. }
  275. /**
  276. * Given a host name and a port, combine them into host/port string like
  277. * you might find in a URL. If the host contains a colon, wrap it in square
  278. * brackets like in RFC 2732. If the port matches the default port, omit
  279. * the port specification
  280. *
  281. * @param string $host
  282. * @param int $port
  283. * @param bool|int $defaultPort
  284. * @return string
  285. */
  286. public static function combineHostAndPort( $host, $port, $defaultPort = false ) {
  287. if ( strpos( $host, ':' ) !== false ) {
  288. $host = "[$host]";
  289. }
  290. if ( $defaultPort !== false && $port == $defaultPort ) {
  291. return $host;
  292. } else {
  293. return "$host:$port";
  294. }
  295. }
  296. /**
  297. * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
  298. *
  299. * @param string $hex Number, with "v6-" prefix if it is IPv6
  300. * @return string Quad-dotted (IPv4) or octet notation (IPv6)
  301. */
  302. public static function formatHex( $hex ) {
  303. if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6
  304. return self::hexToOctet( substr( $hex, 3 ) );
  305. } else { // IPv4
  306. return self::hexToQuad( $hex );
  307. }
  308. }
  309. /**
  310. * Converts a hexadecimal number to an IPv6 address in octet notation
  311. *
  312. * @param string $ip_hex Pure hex (no v6- prefix)
  313. * @return string (of format a:b:c:d:e:f:g:h)
  314. */
  315. public static function hexToOctet( $ip_hex ) {
  316. // Pad hex to 32 chars (128 bits)
  317. $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT );
  318. // Separate into 8 words
  319. $ip_oct = substr( $ip_hex, 0, 4 );
  320. for ( $n = 1; $n < 8; $n++ ) {
  321. $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 );
  322. }
  323. // NO leading zeroes
  324. $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct );
  325. return $ip_oct;
  326. }
  327. /**
  328. * Converts a hexadecimal number to an IPv4 address in quad-dotted notation
  329. *
  330. * @param string $ip_hex Pure hex
  331. * @return string (of format a.b.c.d)
  332. */
  333. public static function hexToQuad( $ip_hex ) {
  334. // Pad hex to 8 chars (32 bits)
  335. $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT );
  336. // Separate into four quads
  337. $s = '';
  338. for ( $i = 0; $i < 4; $i++ ) {
  339. if ( $s !== '' ) {
  340. $s .= '.';
  341. }
  342. $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 );
  343. }
  344. return $s;
  345. }
  346. /**
  347. * Determine if an IP address really is an IP address, and if it is public,
  348. * i.e. not RFC 1918 or similar
  349. *
  350. * @param string $ip
  351. * @return bool
  352. */
  353. public static function isPublic( $ip ) {
  354. static $privateSet = null;
  355. if ( !$privateSet ) {
  356. $privateSet = new IPSet( [
  357. '10.0.0.0/8', # RFC 1918 (private)
  358. '172.16.0.0/12', # RFC 1918 (private)
  359. '192.168.0.0/16', # RFC 1918 (private)
  360. '0.0.0.0/8', # this network
  361. '127.0.0.0/8', # loopback
  362. 'fc00::/7', # RFC 4193 (local)
  363. '0:0:0:0:0:0:0:1', # loopback
  364. '169.254.0.0/16', # link-local
  365. 'fe80::/10', # link-local
  366. ] );
  367. }
  368. return !$privateSet->match( $ip );
  369. }
  370. /**
  371. * Return a zero-padded upper case hexadecimal representation of an IP address.
  372. *
  373. * Hexadecimal addresses are used because they can easily be extended to
  374. * IPv6 support. To separate the ranges, the return value from this
  375. * function for an IPv6 address will be prefixed with "v6-", a non-
  376. * hexadecimal string which sorts after the IPv4 addresses.
  377. *
  378. * @param string $ip Quad dotted/octet IP address.
  379. * @return string|bool False on failure
  380. */
  381. public static function toHex( $ip ) {
  382. if ( self::isIPv6( $ip ) ) {
  383. $n = 'v6-' . self::IPv6ToRawHex( $ip );
  384. } elseif ( self::isIPv4( $ip ) ) {
  385. // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08),
  386. // also double/triple 0 needs to be changed to just a single 0 for ip2long.
  387. $ip = self::sanitizeIP( $ip );
  388. $n = ip2long( $ip );
  389. if ( $n < 0 ) {
  390. $n += 2 ** 32;
  391. # On 32-bit platforms (and on Windows), 2^32 does not fit into an int,
  392. # so $n becomes a float. We convert it to string instead.
  393. if ( is_float( $n ) ) {
  394. $n = (string)$n;
  395. }
  396. }
  397. if ( $n !== false ) {
  398. # Floating points can handle the conversion; faster than Wikimedia\base_convert()
  399. $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) );
  400. }
  401. } else {
  402. $n = false;
  403. }
  404. return $n;
  405. }
  406. /**
  407. * Given an IPv6 address in octet notation, returns a pure hex string.
  408. *
  409. * @param string $ip Octet ipv6 IP address.
  410. * @return string|bool Pure hex (uppercase); false on failure
  411. */
  412. private static function IPv6ToRawHex( $ip ) {
  413. $ip = self::sanitizeIP( $ip );
  414. if ( !$ip ) {
  415. return false;
  416. }
  417. $r_ip = '';
  418. foreach ( explode( ':', $ip ) as $v ) {
  419. $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
  420. }
  421. return $r_ip;
  422. }
  423. /**
  424. * Convert a network specification in CIDR notation
  425. * to an integer network and a number of bits
  426. *
  427. * @param string $range IP with CIDR prefix
  428. * @return array [int or string, int]
  429. */
  430. public static function parseCIDR( $range ) {
  431. if ( self::isIPv6( $range ) ) {
  432. return self::parseCIDR6( $range );
  433. }
  434. $parts = explode( '/', $range, 2 );
  435. if ( count( $parts ) != 2 ) {
  436. return [ false, false ];
  437. }
  438. list( $network, $bits ) = $parts;
  439. $network = ip2long( $network );
  440. if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) {
  441. if ( $bits == 0 ) {
  442. $network = 0;
  443. } else {
  444. $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 );
  445. }
  446. # Convert to unsigned
  447. if ( $network < 0 ) {
  448. $network += 2 ** 32;
  449. }
  450. } else {
  451. $network = false;
  452. $bits = false;
  453. }
  454. return [ $network, $bits ];
  455. }
  456. /**
  457. * Given a string range in a number of formats,
  458. * return the start and end of the range in hexadecimal.
  459. *
  460. * Formats are:
  461. * 1.2.3.4/24 CIDR
  462. * 1.2.3.4 - 1.2.3.5 Explicit range
  463. * 1.2.3.4 Single IP
  464. *
  465. * 2001:0db8:85a3::7344/96 CIDR
  466. * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
  467. * 2001:0db8:85a3::7344 Single IP
  468. * @param string $range IP range
  469. * @return array [ string, string ]
  470. */
  471. public static function parseRange( $range ) {
  472. // CIDR notation
  473. if ( strpos( $range, '/' ) !== false ) {
  474. if ( self::isIPv6( $range ) ) {
  475. return self::parseRange6( $range );
  476. }
  477. list( $network, $bits ) = self::parseCIDR( $range );
  478. if ( $network === false ) {
  479. $start = $end = false;
  480. } else {
  481. $start = sprintf( '%08X', $network );
  482. $end = sprintf( '%08X', $network + 2 ** ( 32 - $bits ) - 1 );
  483. }
  484. // Explicit range
  485. } elseif ( strpos( $range, '-' ) !== false ) {
  486. list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
  487. if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) {
  488. return self::parseRange6( $range );
  489. }
  490. if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) {
  491. $start = self::toHex( $start );
  492. $end = self::toHex( $end );
  493. if ( $start > $end ) {
  494. $start = $end = false;
  495. }
  496. } else {
  497. $start = $end = false;
  498. }
  499. } else {
  500. # Single IP
  501. $start = $end = self::toHex( $range );
  502. }
  503. if ( $start === false || $end === false ) {
  504. return [ false, false ];
  505. } else {
  506. return [ $start, $end ];
  507. }
  508. }
  509. /**
  510. * Convert a network specification in IPv6 CIDR notation to an
  511. * integer network and a number of bits
  512. *
  513. * @param string $range
  514. *
  515. * @return array [string, int]
  516. */
  517. private static function parseCIDR6( $range ) {
  518. # Explode into <expanded IP,range>
  519. $parts = explode( '/', self::sanitizeIP( $range ), 2 );
  520. if ( count( $parts ) != 2 ) {
  521. return [ false, false ];
  522. }
  523. list( $network, $bits ) = $parts;
  524. $network = self::IPv6ToRawHex( $network );
  525. if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) {
  526. if ( $bits == 0 ) {
  527. $network = "0";
  528. } else {
  529. # Native 32 bit functions WONT work here!!!
  530. # Convert to a padded binary number
  531. $network = Wikimedia\base_convert( $network, 16, 2, 128 );
  532. # Truncate the last (128-$bits) bits and replace them with zeros
  533. $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
  534. # Convert back to an integer
  535. $network = Wikimedia\base_convert( $network, 2, 10 );
  536. }
  537. } else {
  538. $network = false;
  539. $bits = false;
  540. }
  541. return [ $network, (int)$bits ];
  542. }
  543. /**
  544. * Given a string range in a number of formats, return the
  545. * start and end of the range in hexadecimal. For IPv6.
  546. *
  547. * Formats are:
  548. * 2001:0db8:85a3::7344/96 CIDR
  549. * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
  550. * 2001:0db8:85a3::7344/96 Single IP
  551. *
  552. * @param string $range
  553. *
  554. * @return array [string, string]
  555. */
  556. private static function parseRange6( $range ) {
  557. # Expand any IPv6 IP
  558. $range = self::sanitizeIP( $range );
  559. // CIDR notation...
  560. if ( strpos( $range, '/' ) !== false ) {
  561. list( $network, $bits ) = self::parseCIDR6( $range );
  562. if ( $network === false ) {
  563. $start = $end = false;
  564. } else {
  565. $start = Wikimedia\base_convert( $network, 10, 16, 32, false );
  566. # Turn network to binary (again)
  567. $end = Wikimedia\base_convert( $network, 10, 2, 128 );
  568. # Truncate the last (128-$bits) bits and replace them with ones
  569. $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
  570. # Convert to hex
  571. $end = Wikimedia\base_convert( $end, 2, 16, 32, false );
  572. # see toHex() comment
  573. $start = "v6-$start";
  574. $end = "v6-$end";
  575. }
  576. // Explicit range notation...
  577. } elseif ( strpos( $range, '-' ) !== false ) {
  578. list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
  579. $start = self::toHex( $start );
  580. $end = self::toHex( $end );
  581. if ( $start > $end ) {
  582. $start = $end = false;
  583. }
  584. } else {
  585. # Single IP
  586. $start = $end = self::toHex( $range );
  587. }
  588. if ( $start === false || $end === false ) {
  589. return [ false, false ];
  590. } else {
  591. return [ $start, $end ];
  592. }
  593. }
  594. /**
  595. * Determine if a given IPv4/IPv6 address is in a given CIDR network
  596. *
  597. * @param string $addr The address to check against the given range.
  598. * @param string $range The range to check the given address against.
  599. * @return bool Whether or not the given address is in the given range.
  600. *
  601. * @note This can return unexpected results for invalid arguments!
  602. * Make sure you pass a valid IP address and IP range.
  603. */
  604. public static function isInRange( $addr, $range ) {
  605. $hexIP = self::toHex( $addr );
  606. list( $start, $end ) = self::parseRange( $range );
  607. return ( strcmp( $hexIP, $start ) >= 0 &&
  608. strcmp( $hexIP, $end ) <= 0 );
  609. }
  610. /**
  611. * Determines if an IP address is a list of CIDR a.b.c.d/n ranges.
  612. *
  613. * @since 1.25
  614. *
  615. * @param string $ip the IP to check
  616. * @param array $ranges the IP ranges, each element a range
  617. *
  618. * @return bool true if the specified adress belongs to the specified range; otherwise, false.
  619. */
  620. public static function isInRanges( $ip, $ranges ) {
  621. foreach ( $ranges as $range ) {
  622. if ( self::isInRange( $ip, $range ) ) {
  623. return true;
  624. }
  625. }
  626. return false;
  627. }
  628. /**
  629. * Convert some unusual representations of IPv4 addresses to their
  630. * canonical dotted quad representation.
  631. *
  632. * This currently only checks a few IPV4-to-IPv6 related cases. More
  633. * unusual representations may be added later.
  634. *
  635. * @param string $addr Something that might be an IP address
  636. * @return string|null Valid dotted quad IPv4 address or null
  637. */
  638. public static function canonicalize( $addr ) {
  639. // remove zone info (T37738)
  640. $addr = preg_replace( '/\%.*/', '', $addr );
  641. if ( self::isValid( $addr ) ) {
  642. return $addr;
  643. }
  644. // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
  645. if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) {
  646. $addr = substr( $addr, strrpos( $addr, ':' ) + 1 );
  647. if ( self::isIPv4( $addr ) ) {
  648. return $addr;
  649. }
  650. }
  651. // IPv6 loopback address
  652. $m = [];
  653. if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) {
  654. return '127.0.0.1';
  655. }
  656. // IPv4-mapped and IPv4-compatible IPv6 addresses
  657. if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) {
  658. return $m[1];
  659. }
  660. if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD .
  661. ':' . RE_IPV6_WORD . '$/i', $addr, $m )
  662. ) {
  663. return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
  664. }
  665. return null; // give up
  666. }
  667. /**
  668. * Gets rid of unneeded numbers in quad-dotted/octet IP strings
  669. * For example, 127.111.113.151/24 -> 127.111.113.0/24
  670. * @param string $range IP address to normalize
  671. * @return string
  672. */
  673. public static function sanitizeRange( $range ) {
  674. list( /*...*/, $bits ) = self::parseCIDR( $range );
  675. list( $start, /*...*/ ) = self::parseRange( $range );
  676. $start = self::formatHex( $start );
  677. if ( $bits === false ) {
  678. return $start; // wasn't actually a range
  679. }
  680. return "$start/$bits";
  681. }
  682. /**
  683. * Returns the subnet of a given IP
  684. *
  685. * @param string $ip
  686. * @return string|false
  687. */
  688. public static function getSubnet( $ip ) {
  689. $matches = [];
  690. $subnet = false;
  691. if ( self::isIPv6( $ip ) ) {
  692. $parts = self::parseRange( "$ip/64" );
  693. $subnet = $parts[0];
  694. } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
  695. // IPv4
  696. $subnet = $matches[1];
  697. }
  698. return $subnet;
  699. }
  700. }