├── .github └── workflows │ └── phpunit.yml ├── LICENSE ├── README.md ├── composer.json ├── src ├── IP.php ├── IPUtils.php ├── IPv4.php └── IPv6.php └── tests ├── IPTest.php ├── IPUtilsTest.php ├── IPv4Test.php └── IPv6Test.php /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | name: PHPUnit 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-version }} 23 | - name: Composer install 24 | run: composer install 25 | - name: PHPUnit / PHP ${{ matrix.php-version }} 26 | run: ./vendor/bin/phpunit 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matomo/Network 2 | 3 | Component providing Network tools. 4 | 5 | [![Build Status](https://travis-ci.com/matomo-org/component-network.svg?branch=master)](https://travis-ci.com/matomo-org/component-network) 6 | 7 | ## Installation 8 | 9 | With Composer: 10 | 11 | ```json 12 | { 13 | "require": { 14 | "matomo/network": "*" 15 | } 16 | } 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### IP 22 | 23 | To manipulate an IP address, you can use the `Matomo\Network\IP` class: 24 | 25 | ```php 26 | $ip = IP::fromStringIP('127.0.0.1'); 27 | // IPv6 28 | $ip = IP::fromStringIP('::1'); 29 | // In binary format: 30 | $ip = IP::fromBinaryIP("\x7F\x00\x00\x01"); 31 | 32 | echo $ip->toString(); // 127.0.0.1 33 | echo $ip->toBinary(); 34 | 35 | // IPv4 & IPv6 36 | if ($ip instanceof IPv4) {} 37 | if ($ip instanceof IPv6) {} 38 | 39 | // Hostname reverse lookup 40 | echo $ip->getHostname(); 41 | 42 | if ($ip->isInRange('192.168.1.1/32')) {} 43 | if ($ip->isInRange('192.168.*.*')) {} 44 | 45 | // Anonymize an IP by setting X bytes to null bytes 46 | $ip->anonymize(2); 47 | ``` 48 | 49 | The `Matomo\Network\IPUtils` class provides utility methods: 50 | 51 | ```php 52 | echo IPUtils::binaryToStringIP("\x7F\x00\x00\x01"); 53 | echo IPUtils::stringToBinaryIP('127.0.0.1'); 54 | 55 | // Sanitization methods 56 | $sanitizedIp = IPUtils::sanitizeIp($_GET['ip']); 57 | $sanitizedIpRange = IPUtils::sanitizeIpRange($_GET['ipRange']); 58 | 59 | // IP range 60 | $bounds = IPUtils::getIPRangeBounds('192.168.1.*'); 61 | echo $bounds[0]; // 192.168.1.0 62 | echo $bounds[1]; // 192.168.1.255 63 | ``` 64 | 65 | ## License 66 | 67 | The Network component is released under the [LGPL v3.0](http://choosealicense.com/licenses/lgpl-3.0/). 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matomo/network", 3 | "type": "library", 4 | "license": "LGPL-3.0", 5 | "autoload": { 6 | "psr-4": { 7 | "Matomo\\Network\\": "src/" 8 | } 9 | }, 10 | "autoload-dev": { 11 | "psr-4": { 12 | "Tests\\Matomo\\Network\\": "tests/" 13 | } 14 | }, 15 | "require": { 16 | "php": ">=7.2" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^8 || ^9 || ^10 || ^11" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/IP.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 35 | } 36 | 37 | /** 38 | * Factory method to create an IP instance from an IP in binary format. 39 | * 40 | * @see fromStringIP 41 | * 42 | * @param string $ip IP address in a binary format. 43 | * @return IP 44 | */ 45 | public static function fromBinaryIP($ip) 46 | { 47 | if ($ip === null || $ip === '') { 48 | return new IPv4("\x00\x00\x00\x00"); 49 | } 50 | 51 | if (self::isIPv4($ip)) { 52 | return new IPv4($ip); 53 | } 54 | 55 | return new IPv6($ip); 56 | } 57 | 58 | /** 59 | * Factory method to create an IP instance from an IP represented as string. 60 | * 61 | * @see fromBinaryIP 62 | * 63 | * @param string $ip IP address in a string format (X.X.X.X). 64 | * @return IP 65 | */ 66 | public static function fromStringIP($ip) 67 | { 68 | return self::fromBinaryIP(IPUtils::stringToBinaryIP($ip)); 69 | } 70 | 71 | /** 72 | * Returns the IP address in a binary format. 73 | * 74 | * @return string 75 | */ 76 | public function toBinary() 77 | { 78 | return $this->ip; 79 | } 80 | 81 | /** 82 | * Returns the IP address in a string format (X.X.X.X). 83 | * 84 | * @return string 85 | */ 86 | public function toString() 87 | { 88 | return IPUtils::binaryToStringIP($this->ip); 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | public function __toString() 95 | { 96 | return $this->toString(); 97 | } 98 | 99 | /** 100 | * Tries to return the hostname associated to the IP. 101 | * 102 | * @return string|null The hostname or null if the hostname can't be resolved. 103 | */ 104 | public function getHostname() 105 | { 106 | $stringIp = $this->toString(); 107 | 108 | $host = strtolower(@gethostbyaddr($stringIp)); 109 | 110 | if ($host === '' || $host === $stringIp) { 111 | return null; 112 | } 113 | 114 | return $host; 115 | } 116 | 117 | /** 118 | * Determines if the IP address is in a specified IP address range. 119 | * 120 | * An IPv4-mapped address should be range checked with an IPv4-mapped address range. 121 | * 122 | * @param array|string $ipRange IP address range (string or array containing min and max IP addresses) 123 | * @return bool 124 | */ 125 | public function isInRange($ipRange) 126 | { 127 | $ipLen = strlen($this->ip); 128 | if (empty($this->ip) || empty($ipRange) || ($ipLen != 4 && $ipLen != 16)) { 129 | return false; 130 | } 131 | 132 | if (is_array($ipRange)) { 133 | // already split into low/high IP addresses 134 | $ipRange[0] = IPUtils::stringToBinaryIP($ipRange[0]); 135 | $ipRange[1] = IPUtils::stringToBinaryIP($ipRange[1]); 136 | } else { 137 | // expect CIDR format but handle some variations 138 | $ipRange = IPUtils::getIPRangeBounds($ipRange); 139 | } 140 | if ($ipRange === null) { 141 | return false; 142 | } 143 | 144 | $low = $ipRange[0]; 145 | $high = $ipRange[1]; 146 | if (strlen($low) != $ipLen) { 147 | return false; 148 | } 149 | 150 | // binary-safe string comparison 151 | if ($this->ip >= $low && $this->ip <= $high) { 152 | return true; 153 | } 154 | 155 | return false; 156 | } 157 | 158 | /** 159 | * Determines if the IP address is in a specified IP address range. 160 | * 161 | * An IPv4-mapped address should be range checked with IPv4-mapped address ranges. 162 | * 163 | * @param array $ipRanges List of IP address ranges (strings or arrays containing min and max IP addresses). 164 | * @return bool True if in any of the specified IP address ranges; false otherwise. 165 | */ 166 | public function isInRanges(array $ipRanges) 167 | { 168 | $ipLen = strlen($this->ip); 169 | if (empty($this->ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16)) { 170 | return false; 171 | } 172 | 173 | foreach ($ipRanges as $ipRange) { 174 | if ($this->isInRange($ipRange)) { 175 | return true; 176 | } 177 | } 178 | 179 | return false; 180 | } 181 | 182 | /** 183 | * Returns the IP address as an IPv4 string when possible. 184 | * 185 | * Some IPv6 can be transformed to IPv4 addresses, for example 186 | * IPv4-mapped IPv6 addresses: `::ffff:192.168.0.1` will return `192.168.0.1`. 187 | * 188 | * @return string|null IPv4 string address e.g. `'192.0.2.128'` or null if this is not an IPv4 address. 189 | */ 190 | public abstract function toIPv4String(); 191 | 192 | /** 193 | * Anonymize X bytes of the IP address by setting them to a null byte. 194 | * 195 | * This method returns a new IP instance, it does not modify the current object. 196 | * 197 | * @param int $byteCount Number of bytes to set to "\0". 198 | * 199 | * @return IP Returns a new modified instance. 200 | */ 201 | public abstract function anonymize($byteCount); 202 | 203 | /** 204 | * Returns true if this is an IPv4, IPv4-compat, or IPv4-mapped address, false otherwise. 205 | * 206 | * @param string $binaryIp 207 | * @return bool 208 | */ 209 | private static function isIPv4($binaryIp) 210 | { 211 | // in case mbstring overloads strlen function 212 | $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; 213 | 214 | return $strlen($binaryIp) == 4; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/IPUtils.php: -------------------------------------------------------------------------------- 1 | $posDot) { 47 | $ipString = substr($ipString, 0, $posColon); 48 | } 49 | // else: Dotted quad IPv6 address, A:B:C:D:E:F:G.H.I.J 50 | } else if (strpos($ipString, ':') === $posColon) { 51 | $ipString = substr($ipString, 0, $posColon); 52 | } 53 | // else: IPv6 address, A:B:C:D:E:F:G:H 54 | } 55 | // else: IPv4 address, A.B.C.D 56 | 57 | return $ipString; 58 | } 59 | 60 | /** 61 | * Sanitize human-readable (user-supplied) IP address range. 62 | * 63 | * Accepts the following formats for $ipRange: 64 | * - single IPv4 address, e.g., 127.0.0.1 65 | * - single IPv6 address, e.g., ::1/128 66 | * - IPv4 block using CIDR notation, e.g., 192.168.0.0/22 represents the IPv4 addresses from 192.168.0.0 to 192.168.3.255 67 | * - IPv6 block using CIDR notation, e.g., 2001:DB8::/48 represents the IPv6 addresses from 2001:DB8:0:0:0:0:0:0 to 2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF 68 | * - wildcards, e.g., 192.168.0.* or 2001:DB8:*:*:*:*:*:* 69 | * 70 | * @param string $ipRangeString IP address range 71 | * @return string|null IP address range in CIDR notation OR null on failure 72 | */ 73 | public static function sanitizeIpRange($ipRangeString) 74 | { 75 | $ipRangeString = trim($ipRangeString); 76 | if (empty($ipRangeString)) { 77 | return null; 78 | } 79 | 80 | // IP address with wildcards '*' 81 | if (strpos($ipRangeString, '*') !== false) { 82 | // Disallow prefixed wildcards and anything other than wildcards 83 | // and separators (including IPv6 zero groups) after first wildcard 84 | if (preg_match('/[^.:]\*|\*.*([^.:*]|::)/', $ipRangeString)) { 85 | return null; 86 | } 87 | 88 | $numWildcards = substr_count($ipRangeString, '*'); 89 | $ipRangeString = str_replace('*', '0', $ipRangeString); 90 | 91 | // CIDR 92 | } elseif (($pos = strpos($ipRangeString, '/')) !== false) { 93 | $bits = substr($ipRangeString, $pos + 1); 94 | $ipRangeString = substr($ipRangeString, 0, $pos); 95 | 96 | if (!is_numeric($bits)) { 97 | return null; 98 | } 99 | } 100 | 101 | // single IP 102 | if (($ip = @inet_pton($ipRangeString)) === false) 103 | return null; 104 | 105 | $maxbits = strlen($ip) * 8; 106 | if (!isset($bits)) { 107 | $bits = $maxbits; 108 | 109 | if (isset($numWildcards)) { 110 | $bits -= ($maxbits === 32 ? 8 : 16) * $numWildcards; 111 | } 112 | } 113 | 114 | if ($bits < 0 || $bits > $maxbits) { 115 | return null; 116 | } 117 | 118 | return "$ipRangeString/$bits"; 119 | } 120 | 121 | /** 122 | * Converts an IP address in string/presentation format to binary/network address format. 123 | * 124 | * @param string $ipString IP address, either IPv4 or IPv6, e.g. `'127.0.0.1'`. 125 | * @return string Binary-safe string, e.g. `"\x7F\x00\x00\x01"`. 126 | */ 127 | public static function stringToBinaryIP($ipString) 128 | { 129 | // use @inet_pton() because it throws an exception and E_WARNING on invalid input 130 | $ip = @inet_pton($ipString); 131 | return $ip === false ? "\x00\x00\x00\x00" : $ip; 132 | } 133 | 134 | /** 135 | * Convert binary/network address format to string/presentation format. 136 | * 137 | * @param string $ip IP address in binary/network address format, e.g. `"\x7F\x00\x00\x01"`. 138 | * @return string IP address in string format, e.g. `'127.0.0.1'`. 139 | */ 140 | public static function binaryToStringIP($ip) 141 | { 142 | // use @inet_ntop() because it throws an exception and E_WARNING on invalid input 143 | $ipStr = @inet_ntop($ip); 144 | return $ipStr === false ? '0.0.0.0' : $ipStr; 145 | } 146 | 147 | /** 148 | * Get low and high IP addresses for a specified IP range. 149 | * 150 | * @param string $ipRange An IP address range in string format, e.g. `'192.168.1.1/24'`. 151 | * @return array|null Array `array($lowIp, $highIp)` in binary format, or null on failure. 152 | */ 153 | public static function getIPRangeBounds($ipRange) 154 | { 155 | $ipRange = self::sanitizeIpRange($ipRange); 156 | 157 | if ($ipRange === null || 158 | (($pos = strpos($ipRange, '/')) === false) || 159 | ($pos + 1 === strlen($ipRange)) 160 | ) { 161 | return null; 162 | } 163 | 164 | $range = substr($ipRange, 0, $pos); 165 | $high = $low = @inet_pton($range); 166 | 167 | if ($low === false) { 168 | return null; 169 | } 170 | 171 | $addrLen = strlen($low); 172 | $bits = (int) substr($ipRange, $pos + 1); 173 | 174 | if ($bits < 0 || $bits > $addrLen * 8) { 175 | return null; 176 | } 177 | 178 | $octet = (int) (($bits + 7) / 8); 179 | 180 | for ($i = $octet; $i < $addrLen; $i++) { 181 | $low[$i] = chr(0); 182 | $high[$i] = chr(255); 183 | } 184 | 185 | if (($n = $bits % 8)) { 186 | $mask = (1 << (8 - $n)) - 1; 187 | $value = ord($low[--$octet]) & ~$mask; 188 | 189 | $low[$octet] = chr($value); 190 | $high[$octet] = chr($value | $mask); 191 | } 192 | 193 | return array($low, $high); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/IPv4.php: -------------------------------------------------------------------------------- 1 | toString(); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function anonymize($byteCount) 31 | { 32 | $newBinaryIp = $this->ip; 33 | 34 | $i = strlen($newBinaryIp); 35 | if ($byteCount > $i) { 36 | $byteCount = $i; 37 | } 38 | 39 | while ($byteCount-- > 0) { 40 | $newBinaryIp[--$i] = chr(0); 41 | } 42 | 43 | return self::fromBinaryIP($newBinaryIp); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/IPv6.php: -------------------------------------------------------------------------------- 1 | ip; 27 | 28 | if ($this->isMappedIPv4()) { 29 | $i = strlen($newBinaryIp); 30 | if ($byteCount > $i) { 31 | $byteCount = $i; 32 | } 33 | 34 | while ($byteCount-- > 0) { 35 | $newBinaryIp[--$i] = chr(0); 36 | } 37 | 38 | return self::fromBinaryIP($newBinaryIp); 39 | } 40 | 41 | $masks = array( 42 | 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 43 | 'ffff:ffff:ffff:ffff::', 44 | 'ffff:ffff:ffff:0000::', 45 | 'ffff:ff00:0000:0000::', 46 | '0000::' 47 | ); 48 | 49 | $newBinaryIp = $newBinaryIp & pack('a16', inet_pton($masks[$byteCount])); 50 | 51 | return self::fromBinaryIP($newBinaryIp); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function toIPv4String() 58 | { 59 | $str = $this->toString(); 60 | 61 | if ($this->isMappedIPv4()) { 62 | return substr($str, strlen(self::MAPPED_IPv4_START)); 63 | } 64 | 65 | return null; 66 | } 67 | 68 | /** 69 | * Returns true if this is a IPv4 mapped address, false otherwise. 70 | * 71 | * @return bool 72 | */ 73 | public function isMappedIPv4() 74 | { 75 | return substr_compare($this->ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 76 | || substr_compare($this->ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/IPTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Matomo\Network\\' . $class, $ip); 54 | 55 | $this->assertEquals($binary, $ip->toBinary()); 56 | $this->assertEquals($str, $ip->toString()); 57 | $this->assertEquals($str, (string) $ip); 58 | } 59 | 60 | /** 61 | * @dataProvider emptyNullIpData 62 | */ 63 | public function testFromBinaryIPOnEmptyAndNull($ipAddress, $expectedBinary, $expectedStr) 64 | { 65 | $ip = IP::fromBinaryIP($ipAddress); 66 | 67 | $this->assertInstanceOf('Matomo\Network\\IPv4', $ip); 68 | 69 | $this->assertEquals($expectedBinary, $ip->toBinary()); 70 | $this->assertEquals($expectedStr, $ip->toString()); 71 | $this->assertEquals($expectedStr, (string) $ip); 72 | } 73 | 74 | /** 75 | * @dataProvider ipData 76 | */ 77 | public function testFromStringIP($str, $binary) 78 | { 79 | $ip = IP::fromStringIP($str); 80 | 81 | $this->assertEquals($binary, $ip->toBinary()); 82 | $this->assertEquals($str, $ip->toString()); 83 | $this->assertEquals($str, (string) $ip); 84 | } 85 | 86 | /** 87 | * @dataProvider emptyNullIpData 88 | */ 89 | public function testFromStringIPOnEmptyAndNull($ipAddress, $expectedBinary, $expectedStr) 90 | { 91 | $ip = IP::fromStringIP($ipAddress); 92 | 93 | $this->assertInstanceOf('Matomo\Network\\IPv4', $ip); 94 | 95 | $this->assertEquals($expectedBinary, $ip->toBinary()); 96 | $this->assertEquals($expectedStr, $ip->toString()); 97 | $this->assertEquals($expectedStr, (string) $ip); 98 | } 99 | 100 | public function testGetHostnameIPv4() 101 | { 102 | $hosts = array('localhost', 'localhost.localdomain', strtolower(@php_uname('n')), '127.0.0.1'); 103 | 104 | $ip = IP::fromStringIP('127.0.0.1'); 105 | $this->assertContains($ip->getHostname(), $hosts, '127.0.0.1 -> localhost'); 106 | } 107 | 108 | public function testGetHostnameIPv6() 109 | { 110 | $hosts = array('ip6-localhost', 'localhost', 'localhost.localdomain', strtolower(@php_uname('n')), '::1'); 111 | 112 | if(self::isTravisCI()) { 113 | // Reverse lookup does not work on Travis for ::1 ipv6 address 114 | $hosts[] = null; 115 | } 116 | 117 | $ip = IP::fromStringIP('::1'); 118 | $this->assertContains($ip->getHostname(), $hosts, '::1 -> ip6-localhost'); 119 | } 120 | 121 | /** 122 | * Returns true if continuous integration running this request 123 | * Useful to exclude tests which may fail only on this setup 124 | */ 125 | public static function isTravisCI() 126 | { 127 | $travis = getenv('TRAVIS'); 128 | return !empty($travis); 129 | } 130 | 131 | public function testGetHostnameFailure() 132 | { 133 | $ip = IP::fromStringIP('0.1.2.3'); 134 | $this->assertNull($ip->getHostname()); 135 | } 136 | 137 | public static function getIpsInRangeData() 138 | { 139 | return array( 140 | array('192.168.1.10', array( 141 | '192.168.1.9' => false, 142 | '192.168.1.10' => true, 143 | '192.168.1.11' => false, 144 | 145 | // IPv6 addresses (including IPv4 mapped) have to be compared against IPv6 address ranges 146 | '::ffff:192.168.1.10' => false, 147 | )), 148 | 149 | array('::ffff:192.168.1.10', array( 150 | '::ffff:192.168.1.9' => false, 151 | '::ffff:192.168.1.10' => true, 152 | '::ffff:c0a8:010a' => true, 153 | '0000:0000:0000:0000:0000:ffff:c0a8:010a' => true, 154 | '::ffff:192.168.1.11' => false, 155 | 156 | // conversely, IPv4 addresses have to be compared against IPv4 address ranges 157 | '192.168.1.10' => false, 158 | )), 159 | 160 | array('192.168.1.10/32', array( 161 | '192.168.1.9' => false, 162 | '192.168.1.10' => true, 163 | '192.168.1.11' => false, 164 | )), 165 | 166 | array('192.168.1.10/31', array( 167 | '192.168.1.9' => false, 168 | '192.168.1.10' => true, 169 | '192.168.1.11' => true, 170 | '192.168.1.12' => false, 171 | )), 172 | 173 | array('192.168.1.128/25', array( 174 | '192.168.1.127' => false, 175 | '192.168.1.128' => true, 176 | '192.168.1.255' => true, 177 | '192.168.2.0' => false, 178 | )), 179 | 180 | array('192.168.1.10/24', array( 181 | '192.168.0.255' => false, 182 | '192.168.1.0' => true, 183 | '192.168.1.1' => true, 184 | '192.168.1.2' => true, 185 | '192.168.1.3' => true, 186 | '192.168.1.4' => true, 187 | '192.168.1.7' => true, 188 | '192.168.1.8' => true, 189 | '192.168.1.15' => true, 190 | '192.168.1.16' => true, 191 | '192.168.1.31' => true, 192 | '192.168.1.32' => true, 193 | '192.168.1.63' => true, 194 | '192.168.1.64' => true, 195 | '192.168.1.127' => true, 196 | '192.168.1.128' => true, 197 | '192.168.1.255' => true, 198 | '192.168.2.0' => false, 199 | )), 200 | 201 | array('192.168.1.*', array( 202 | '192.168.0.255' => false, 203 | '192.168.1.0' => true, 204 | '192.168.1.1' => true, 205 | '192.168.1.2' => true, 206 | '192.168.1.3' => true, 207 | '192.168.1.4' => true, 208 | '192.168.1.7' => true, 209 | '192.168.1.8' => true, 210 | '192.168.1.15' => true, 211 | '192.168.1.16' => true, 212 | '192.168.1.31' => true, 213 | '192.168.1.32' => true, 214 | '192.168.1.63' => true, 215 | '192.168.1.64' => true, 216 | '192.168.1.127' => true, 217 | '192.168.1.128' => true, 218 | '192.168.1.255' => true, 219 | '192.168.2.0' => false, 220 | )), 221 | ); 222 | } 223 | 224 | public static function getEmptyIpRangeData() 225 | { 226 | return array( 227 | array(''), 228 | array(null) 229 | ); 230 | } 231 | 232 | /** 233 | * @dataProvider getIpsInRangeData 234 | */ 235 | public function testIsInRange($range, $test) 236 | { 237 | foreach ($test as $stringIp => $expected) { 238 | $ip = IP::fromStringIP($stringIp); 239 | 240 | // range as a string 241 | $this->assertEquals($expected, $ip->isInRange($range), "$ip in $range"); 242 | 243 | // range as an array(low, high) 244 | $arrayRange = IPUtils::getIPRangeBounds($range); 245 | $arrayRange[0] = IPUtils::binaryToStringIP($arrayRange[0]); 246 | $arrayRange[1] = IPUtils::binaryToStringIP($arrayRange[1]); 247 | $this->assertEquals($expected, $ip->isInRange($arrayRange), "$ip in $range"); 248 | } 249 | } 250 | 251 | /** 252 | * @dataProvider getEmptyIpRangeData 253 | */ 254 | public function testIsInRangeOnEmptyIPRange($emptyRange) 255 | { 256 | $ip = IP::fromStringIP('127.0.0.1'); 257 | 258 | $this->assertFalse($ip->isInRange($emptyRange)); 259 | } 260 | 261 | public function testIsInRangesOnEmptyIPRange() 262 | { 263 | $ip = IP::fromStringIP('127.0.0.1'); 264 | 265 | $this->assertFalse($ip->isInRanges(array())); 266 | } 267 | 268 | public function testIsInRangeWithInvalidRange() 269 | { 270 | $ip = IP::fromStringIP('127.0.0.1'); 271 | 272 | $this->assertFalse($ip->isInRange('foo-bar')); 273 | } 274 | 275 | /** 276 | * @dataProvider getIpsInRangeData 277 | */ 278 | public function testIsInRanges($range, $test) 279 | { 280 | foreach ($test as $stringIp => $expected) { 281 | $ip = IP::fromStringIP($stringIp); 282 | 283 | // range as a string 284 | $this->assertEquals($expected, $ip->isInRanges(array($range)), "$ip in $range"); 285 | 286 | // range as an array(low, high) 287 | $arrayRange = IPUtils::getIPRangeBounds($range); 288 | $arrayRange[0] = IPUtils::binaryToStringIP($arrayRange[0]); 289 | $arrayRange[1] = IPUtils::binaryToStringIP($arrayRange[1]); 290 | $this->assertEquals($expected, $ip->isInRanges(array($arrayRange)), "$ip in $range"); 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /tests/IPUtilsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, IPUtils::sanitizeIp($ip)); 67 | } 68 | 69 | public static function getIPRangeSanitizationData() 70 | { 71 | return array( 72 | array('', null), 73 | array(' 127.0.0.1 ', '127.0.0.1/32'), 74 | array('192.168.1.0', '192.168.1.0/32'), 75 | array('192.168.1.1/24', '192.168.1.1/24'), 76 | array('192.168.1.2/16', '192.168.1.2/16'), 77 | array('192.168.1.3/8', '192.168.1.3/8'), 78 | array('192.168.1.4/0', '192.168.1.4/0'), 79 | array('192.168.1.5/42', null), 80 | array('192.168.1.6/', null), 81 | array('192.168.1.7/a', null), 82 | array('192.168.2.*', '192.168.2.0/24'), 83 | array('192.169.*.*', '192.169.0.0/16'), 84 | array('193.*.*.*', '193.0.0.0/8'), 85 | array('*.*.*.*', '0.0.0.0/0'), 86 | array('*.*.*.1', null), 87 | array('*.*.1.1', null), 88 | array('*.1.1.1', null), 89 | array('1.*.1.1', null), 90 | array('1.1.*.1', null), 91 | array('1.*.*.1', null), 92 | array('1.1.1.**', null), 93 | array('1.1.1.1*', null), 94 | array('1.1.1.*1', null), 95 | array('1.1.1.1**', null), 96 | array('1.1.1.1*2', null), 97 | array('1.1.1.*/24', null), 98 | array('::1', '::1/128'), 99 | array('::ffff:127.0.0.1', '::ffff:127.0.0.1/128'), 100 | array('2001:5c0:1000:b::90f8', '2001:5c0:1000:b::90f8/128'), 101 | array('::1/64', '::1/64'), 102 | array('::1/129', null), 103 | array('::1/', null), 104 | array('::1/a', null), 105 | array('::ffff:127.0.0.1/64', '::ffff:127.0.0.1/64'), 106 | array('2001:5c0:1000:b::90f8/64', '2001:5c0:1000:b::90f8/64'), 107 | array('1:2:3:4:5:6:7:8', '1:2:3:4:5:6:7:8/128'), 108 | array('1:2:3:4:5:6:7:*', '1:2:3:4:5:6:7:0/112'), 109 | array('1:2:3:4:5:6:*:*', '1:2:3:4:5:6:0:0/96'), 110 | array('1:2:3:4:5:*:*:*', '1:2:3:4:5:0:0:0/80'), 111 | array('1:2:3:4:*:*:*:*', '1:2:3:4:0:0:0:0/64'), 112 | array('1:2:3:*:*:*:*:*', '1:2:3:0:0:0:0:0/48'), 113 | array('1:2:*:*:*:*:*:*', '1:2:0:0:0:0:0:0/32'), 114 | array('1:*:*:*:*:*:*:*', '1:0:0:0:0:0:0:0/16'), 115 | array('*:*:*:*:*:*:*:*', '0:0:0:0:0:0:0:0/0'), 116 | array('*:2:3:4:5:6:7:8', null), 117 | array('::*', '::0/112'), 118 | array('::*/112', null), 119 | array('::7:*', '::7:0/112'), 120 | array('1::*', '1::0/112'), 121 | array('::**', null), 122 | array('*::', null), 123 | array('*::8', null), 124 | array('*:2::8', null), 125 | array(':*:8', null), 126 | array('::*:8', null), 127 | array('::*8', null), 128 | ); 129 | } 130 | 131 | /** 132 | * @dataProvider getIPRangeSanitizationData 133 | */ 134 | public function testSanitizeIpRange($ip, $expected) 135 | { 136 | $this->assertSame($expected, IPUtils::sanitizeIpRange($ip)); 137 | } 138 | 139 | public static function getIPData() 140 | { 141 | return array( 142 | // IPv4 143 | array('0.0.0.0', "\x00\x00\x00\x00"), 144 | array('127.0.0.1', "\x7F\x00\x00\x01"), 145 | array('192.168.1.12', "\xc0\xa8\x01\x0c"), 146 | array('255.255.255.255', "\xff\xff\xff\xff"), 147 | 148 | // IPv6 149 | array('::', "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 150 | array('::1', "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), 151 | array('::fffe:7f00:1', "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x7f\x00\x00\x01"), 152 | array('::ffff:127.0.0.1', "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x01"), 153 | array('2001:5c0:1000:b::90f8', "\x20\x01\x05\xc0\x10\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x90\xf8"), 154 | ); 155 | } 156 | 157 | /** 158 | * @dataProvider getIPData 159 | */ 160 | public function testStringToBinaryIP($string, $binary) 161 | { 162 | $this->assertEquals($binary, IPUtils::stringToBinaryIP($string)); 163 | } 164 | 165 | public static function getInvalidIPData() 166 | { 167 | return array( 168 | // not a series of dotted numbers 169 | array(null), 170 | array(''), 171 | array('alpha'), 172 | array('...'), 173 | 174 | // missing an octet 175 | array('.0.0.0'), 176 | array('0..0.0'), 177 | array('0.0..0'), 178 | array('0.0.0.'), 179 | 180 | // octets must be 0-255 181 | array('-1.0.0.0'), 182 | array('1.1.1.256'), 183 | 184 | // leading zeros not supported (i.e., can be ambiguous, e.g., octal) 185 | // array('07.07.07.07'), 186 | ); 187 | } 188 | 189 | /** 190 | * @dataProvider getInvalidIPData 191 | */ 192 | public function testStringToBinaryInvalidIP($stringIp) 193 | { 194 | $this->assertEquals("\x00\x00\x00\x00", IPUtils::stringToBinaryIP($stringIp)); 195 | } 196 | 197 | public static function getBinaryIPData() 198 | { 199 | // a valid network address is either 4 or 16 bytes; those lines are intentionally left blank ;) 200 | return array( 201 | array(null), 202 | array(''), 203 | array("\x01"), 204 | array("\x01\x00"), 205 | array("\x01\x00\x00"), 206 | 207 | array("\x01\x00\x00\x00\x00"), 208 | array("\x01\x00\x00\x00\x00\x00"), 209 | array("\x01\x00\x00\x00\x00\x00\x00"), 210 | array("\x01\x00\x00\x00\x00\x00\x00\x00"), 211 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00"), 212 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 213 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 214 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 215 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 216 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 217 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 218 | 219 | array("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), 220 | ); 221 | } 222 | 223 | /** 224 | * @dataProvider getIPData 225 | */ 226 | public function testBinaryToStringIP($string, $binary) 227 | { 228 | $this->assertEquals($string, IPUtils::binaryToStringIP($binary)); 229 | } 230 | 231 | /** 232 | * @dataProvider getBinaryIPData 233 | */ 234 | public function testBinaryToStringInvalidIP($binary) 235 | { 236 | $this->assertEquals('0.0.0.0', IPUtils::binaryToStringIP($binary), bin2hex($binary)); 237 | } 238 | 239 | public static function getBoundsForIPRangeTest() 240 | { 241 | return array( 242 | 243 | // invalid ranges 244 | array(null, null), 245 | array('', null), 246 | array('0', null), 247 | array('192.168.255.255/33', null), 248 | array('192.168.255.255/-1', null), 249 | array('192.168.1.1/', null), 250 | 251 | // single IPv4 252 | array('127.0.0.1', array("\x7f\x00\x00\x01", "\x7f\x00\x00\x01")), 253 | 254 | // IPv4 with wildcards 255 | array('192.168.1.*', array("\xc0\xa8\x01\x00", "\xc0\xa8\x01\xff")), 256 | array('192.168.*.*', array("\xc0\xa8\x00\x00", "\xc0\xa8\xff\xff")), 257 | array('192.*.*.*', array("\xc0\x00\x00\x00", "\xc0\xff\xff\xff")), 258 | array('*.*.*.*', array("\x00\x00\x00\x00", "\xff\xff\xff\xff")), 259 | 260 | // single IPv4 in expected CIDR notation 261 | array('192.168.1.1/24', array("\xc0\xa8\x01\x00", "\xc0\xa8\x01\xff")), 262 | 263 | array('192.168.1.127/32', array("\xc0\xa8\x01\x7f", "\xc0\xa8\x01\x7f")), 264 | array('192.168.1.127/31', array("\xc0\xa8\x01\x7e", "\xc0\xa8\x01\x7f")), 265 | array('192.168.1.127/30', array("\xc0\xa8\x01\x7c", "\xc0\xa8\x01\x7f")), 266 | array('192.168.1.127/29', array("\xc0\xa8\x01\x78", "\xc0\xa8\x01\x7f")), 267 | array('192.168.1.127/28', array("\xc0\xa8\x01\x70", "\xc0\xa8\x01\x7f")), 268 | array('192.168.1.127/27', array("\xc0\xa8\x01\x60", "\xc0\xa8\x01\x7f")), 269 | array('192.168.1.127/26', array("\xc0\xa8\x01\x40", "\xc0\xa8\x01\x7f")), 270 | array('192.168.1.127/25', array("\xc0\xa8\x01\x00", "\xc0\xa8\x01\x7f")), 271 | 272 | array('192.168.1.255/32', array("\xc0\xa8\x01\xff", "\xc0\xa8\x01\xff")), 273 | array('192.168.1.255/31', array("\xc0\xa8\x01\xfe", "\xc0\xa8\x01\xff")), 274 | array('192.168.1.255/30', array("\xc0\xa8\x01\xfc", "\xc0\xa8\x01\xff")), 275 | array('192.168.1.255/29', array("\xc0\xa8\x01\xf8", "\xc0\xa8\x01\xff")), 276 | array('192.168.1.255/28', array("\xc0\xa8\x01\xf0", "\xc0\xa8\x01\xff")), 277 | array('192.168.1.255/27', array("\xc0\xa8\x01\xe0", "\xc0\xa8\x01\xff")), 278 | array('192.168.1.255/26', array("\xc0\xa8\x01\xc0", "\xc0\xa8\x01\xff")), 279 | array('192.168.1.255/25', array("\xc0\xa8\x01\x80", "\xc0\xa8\x01\xff")), 280 | 281 | array('192.168.255.255/24', array("\xc0\xa8\xff\x00", "\xc0\xa8\xff\xff")), 282 | array('192.168.255.255/23', array("\xc0\xa8\xfe\x00", "\xc0\xa8\xff\xff")), 283 | array('192.168.255.255/22', array("\xc0\xa8\xfc\x00", "\xc0\xa8\xff\xff")), 284 | array('192.168.255.255/21', array("\xc0\xa8\xf8\x00", "\xc0\xa8\xff\xff")), 285 | array('192.168.255.255/20', array("\xc0\xa8\xf0\x00", "\xc0\xa8\xff\xff")), 286 | array('192.168.255.255/19', array("\xc0\xa8\xe0\x00", "\xc0\xa8\xff\xff")), 287 | array('192.168.255.255/18', array("\xc0\xa8\xc0\x00", "\xc0\xa8\xff\xff")), 288 | array('192.168.255.255/17', array("\xc0\xa8\x80\x00", "\xc0\xa8\xff\xff")), 289 | array('192.168.255.255/16', array("\xc0\xa8\x00\x00", "\xc0\xa8\xff\xff")), 290 | array('192.168.255.255/15', array("\xc0\xa8\x00\x00", "\xc0\xa9\xff\xff")), 291 | array('192.168.255.255/14', array("\xc0\xa8\x00\x00", "\xc0\xab\xff\xff")), 292 | array('192.168.255.255/13', array("\xc0\xa8\x00\x00", "\xc0\xaf\xff\xff")), 293 | array('192.168.255.255/12', array("\xc0\xa0\x00\x00", "\xc0\xaf\xff\xff")), 294 | array('192.168.255.255/11', array("\xc0\xa0\x00\x00", "\xc0\xbf\xff\xff")), 295 | array('192.168.255.255/10', array("\xc0\x80\x00\x00", "\xc0\xbf\xff\xff")), 296 | array('192.168.255.255/9', array("\xc0\x80\x00\x00", "\xc0\xff\xff\xff")), 297 | array('192.168.255.255/8', array("\xc0\x00\x00\x00", "\xc0\xff\xff\xff")), 298 | array('192.168.255.255/7', array("\xc0\x00\x00\x00", "\xc1\xff\xff\xff")), 299 | array('192.168.255.255/6', array("\xc0\x00\x00\x00", "\xc3\xff\xff\xff")), 300 | array('192.168.255.255/5', array("\xc0\x00\x00\x00", "\xc7\xff\xff\xff")), 301 | array('192.168.255.255/4', array("\xc0\x00\x00\x00", "\xcf\xff\xff\xff")), 302 | array('192.168.255.255/3', array("\xc0\x00\x00\x00", "\xdf\xff\xff\xff")), 303 | array('192.168.255.255/2', array("\xc0\x00\x00\x00", "\xff\xff\xff\xff")), 304 | array('192.168.255.255/1', array("\x80\x00\x00\x00", "\xff\xff\xff\xff")), 305 | array('0.0.0.0/0', array("\x00\x00\x00\x00", "\xff\xff\xff\xff")), 306 | 307 | // single IPv6 308 | array('::1', array("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01")), 309 | 310 | // single IPv6 in expected CIDR notation 311 | array('::1/128', array("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01")), 312 | array('::1/127', array("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01")), 313 | array('::fffe:7f00:1/120', array("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x7f\x00\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x7f\x00\x00\xff")), 314 | array('::ffff:127.0.0.1/120', array("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\xff")), 315 | 316 | array('2001:ca11:911::b0b:15:dead/128', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xad", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xad")), 317 | array('2001:ca11:911::b0b:15:dead/127', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xac", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xad")), 318 | array('2001:ca11:911::b0b:15:dead/126', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xac", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xaf")), 319 | array('2001:ca11:911::b0b:15:dead/125', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xa8", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xaf")), 320 | array('2001:ca11:911::b0b:15:dead/124', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xa0", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xaf")), 321 | array('2001:ca11:911::b0b:15:dead/123', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xa0", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xbf")), 322 | array('2001:ca11:911::b0b:15:dead/122', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\x80", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xbf")), 323 | array('2001:ca11:911::b0b:15:dead/121', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\x80", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xff")), 324 | array('2001:ca11:911::b0b:15:dead/120', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\xff")), 325 | array('2001:ca11:911::b0b:15:dead/119', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xde\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdf\xff")), 326 | array('2001:ca11:911::b0b:15:dead/118', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdc\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdf\xff")), 327 | array('2001:ca11:911::b0b:15:dead/117', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xd8\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdf\xff")), 328 | array('2001:ca11:911::b0b:15:dead/116', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xd0\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdf\xff")), 329 | array('2001:ca11:911::b0b:15:dead/115', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xc0\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xdf\xff")), 330 | array('2001:ca11:911::b0b:15:dead/114', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xc0\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xff\xff")), 331 | array('2001:ca11:911::b0b:15:dead/113', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\x80\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xff\xff")), 332 | array('2001:ca11:911::b0b:15:dead/112', array("\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\x00\x00", "\x20\x01\xca\x11\x09\x11\x00\x00\x00\x00\x0b\x0b\x00\x15\xff\xff")), 333 | ); 334 | } 335 | 336 | /** 337 | * @dataProvider getBoundsForIPRangeTest 338 | */ 339 | public function testGetIPRangeBounds($range, $expected) 340 | { 341 | $this->assertSame($expected, IPUtils::getIPRangeBounds($range)); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /tests/IPv4Test.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Matomo\Network\IPv4', $ip); 36 | 37 | $this->assertEquals($expected, $ip->toIPv4String()); 38 | } 39 | 40 | public static function getAddressesToAnonymize() 41 | { 42 | return array( 43 | // ip, array( expected0, expected1, expected2, expected3, expected4 ), 44 | array('0.0.0.0', array("\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 45 | array('0.0.0.1', array("\x00\x00\x00\x01", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 46 | array('0.0.0.255', array("\x00\x00\x00\xff", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 47 | array('0.0.1.0', array("\x00\x00\x01\x00", "\x00\x00\x01\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 48 | array('0.0.1.1', array("\x00\x00\x01\x01", "\x00\x00\x01\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 49 | array('0.0.255.255', array("\x00\x00\xff\xff", "\x00\x00\xff\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 50 | array('0.1.0.0', array("\x00\x01\x00\x00", "\x00\x01\x00\x00", "\x00\x01\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 51 | array('0.1.1.1', array("\x00\x01\x01\x01", "\x00\x01\x01\x00", "\x00\x01\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 52 | array('0.255.255.255', array("\x00\xff\xff\xff", "\x00\xff\xff\x00", "\x00\xff\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 53 | array('1.0.0.0', array("\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x00\x00\x00\x00")), 54 | array('127.255.255.255', array("\x7f\xff\xff\xff", "\x7f\xff\xff\x00", "\x7f\xff\x00\x00", "\x7f\x00\x00\x00", "\x00\x00\x00\x00")), 55 | array('128.0.0.0', array("\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x00\x00\x00\x00")), 56 | array('255.255.255.255', array("\xff\xff\xff\xff", "\xff\xff\xff\x00", "\xff\xff\x00\x00", "\xff\x00\x00\x00", "\x00\x00\x00\x00")), 57 | ); 58 | } 59 | 60 | /** 61 | * @dataProvider getAddressesToAnonymize 62 | */ 63 | public function testAnonymize($ipString, $expected) 64 | { 65 | $ip = IP::fromStringIP($ipString); 66 | 67 | $this->assertInstanceOf('Matomo\Network\IPv4', $ip); 68 | 69 | // each IP is tested with 0 to 4 octets masked 70 | for ($byteCount = 0; $byteCount <= 4; $byteCount++) { 71 | $result = $ip->anonymize($byteCount); 72 | $this->assertEquals($expected[$byteCount], $result->toBinary(), "Got $result, Expected " . bin2hex($expected[$byteCount])); 73 | } 74 | 75 | // edge case (bounds check) 76 | $this->assertEquals("\x00\x00\x00\x00", $ip->anonymize(5)->toBinary()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/IPv6Test.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Matomo\Network\IPv6', $ip); 36 | 37 | $this->assertEquals($expected, $ip->toIPv4String(), $stringIp); 38 | } 39 | 40 | public static function getMappedIPv4Data() 41 | { 42 | return array( 43 | array(IP::fromStringIP('::ffff:192.168.0.1'), true), 44 | array(IP::fromStringIP('2001:5c0:1000:b::90f8'), false), 45 | 46 | // IPv4-mapped (RFC 4291, 2.5.5.2) 47 | array(IP::fromBinaryIP("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xc0\xa8\x01\x02"), true), 48 | // IPv4-compatible (this transitional format is deprecated in RFC 4291, section 2.5.5.1) 49 | array(IP::fromBinaryIP("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xa8\x01\x01"), true), 50 | 51 | // other IPv6 address 52 | array(IP::fromBinaryIP("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xc0\xa8\x01\x03"), false), 53 | array(IP::fromBinaryIP("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xc0\xa8\x01\x04"), false), 54 | array(IP::fromBinaryIP("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xa8\x01\x05"), false), 55 | ); 56 | } 57 | 58 | /** 59 | * @dataProvider getMappedIPv4Data 60 | */ 61 | public function testIsMappedIPv4(IPv6 $ip, $isMapped) 62 | { 63 | $this->assertEquals($isMapped, $ip->isMappedIPv4(), $ip); 64 | } 65 | 66 | public static function getAddressesToAnonymize() 67 | { 68 | return array( 69 | array('2001:db8:0:8d3:0:8a2e:70:7344', array( 70 | "\x20\x01\x0d\xb8\x00\x00\x08\xd3\x00\x00\x8a\x2e\x00\x70\x73\x44", 71 | "\x20\x01\x0d\xb8\x00\x00\x08\xd3\x00\x00\x00\x00\x00\x00\x00\x00", // mask 64 bits 72 | "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // mask 80 bits 73 | "\x20\x01\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // mask 104 bits 74 | "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" // mask all bits 75 | )), 76 | array('2001:6f8:900:724::2', array( 77 | "\x20\x01\x06\xf8\x09\x00\x07\x24\x00\x00\x00\x00\x00\x00\x00\x02", 78 | "\x20\x01\x06\xf8\x09\x00\x07\x24\x00\x00\x00\x00\x00\x00\x00\x00", 79 | "\x20\x01\x06\xf8\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 80 | "\x20\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 81 | "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 82 | )) 83 | ); 84 | } 85 | 86 | /** 87 | * @dataProvider getAddressesToAnonymize 88 | */ 89 | public function testAnonymize($ipString, $expected) 90 | { 91 | $ip = IP::fromStringIP($ipString); 92 | 93 | $this->assertInstanceOf('Matomo\Network\IPv6', $ip); 94 | 95 | // each IP is tested with 0 to 4 octets masked 96 | for ($byteCount = 0; $byteCount <= 4; $byteCount++) { 97 | $result = $ip->anonymize($byteCount); 98 | $this->assertEquals($expected[$byteCount], $result->toBinary(), "Got $result, Expected " . bin2hex($expected[$byteCount]) . ", Mask: " . $byteCount); 99 | } 100 | } 101 | 102 | 103 | public static function getIPv4AddressesToAnonymize() 104 | { 105 | return array( 106 | // ip, array( expected0, expected1, expected2, expected3, expected4 ), 107 | array('0.0.0.0', array("\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 108 | array('0.0.0.1', array("\x00\x00\x00\x01", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 109 | array('0.0.0.255', array("\x00\x00\x00\xff", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 110 | array('0.0.1.0', array("\x00\x00\x01\x00", "\x00\x00\x01\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 111 | array('0.0.1.1', array("\x00\x00\x01\x01", "\x00\x00\x01\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 112 | array('0.0.255.255', array("\x00\x00\xff\xff", "\x00\x00\xff\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 113 | array('0.1.0.0', array("\x00\x01\x00\x00", "\x00\x01\x00\x00", "\x00\x01\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 114 | array('0.1.1.1', array("\x00\x01\x01\x01", "\x00\x01\x01\x00", "\x00\x01\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 115 | array('0.255.255.255', array("\x00\xff\xff\xff", "\x00\xff\xff\x00", "\x00\xff\x00\x00", "\x00\x00\x00\x00", "\x00\x00\x00\x00")), 116 | array('1.0.0.0', array("\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x01\x00\x00\x00", "\x00\x00\x00\x00")), 117 | array('127.255.255.255', array("\x7f\xff\xff\xff", "\x7f\xff\xff\x00", "\x7f\xff\x00\x00", "\x7f\x00\x00\x00", "\x00\x00\x00\x00")), 118 | array('128.0.0.0', array("\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x80\x00\x00\x00", "\x00\x00\x00\x00")), 119 | array('255.255.255.255', array("\xff\xff\xff\xff", "\xff\xff\xff\x00", "\xff\xff\x00\x00", "\xff\x00\x00\x00", "\x00\x00\x00\x00")), 120 | ); 121 | } 122 | 123 | /** 124 | * @dataProvider getIPv4AddressesToAnonymize 125 | */ 126 | public function testAnonymizeIPv4MappedAdresses($ipString, $expected) 127 | { 128 | $ip = IP::fromStringIP('::ffff:' . $ipString); 129 | 130 | $this->assertInstanceOf('Matomo\Network\IPv6', $ip); 131 | 132 | // mask IPv4 mapped addresses 133 | for ($byteCount = 0; $byteCount <= 4; $byteCount++) { 134 | $result = $ip->anonymize($byteCount); 135 | $expectedIp = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $expected[$byteCount]; 136 | $this->assertEquals($expectedIp, $result->toBinary(), "Got $result, Expected " . bin2hex($expectedIp)); 137 | } 138 | 139 | $this->assertEquals("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00", $ip->anonymize(5)->toBinary()); 140 | } 141 | } 142 | --------------------------------------------------------------------------------