| Code Coverage | ||||||||||
| Lines | Functions and Methods | Classes and Traits | ||||||||
| Total |  | 100.00% | 65 / 65 |  | 100.00% | 6 / 6 | CRAP |  | 100.00% | 1 / 1 | 
| HostMatch |  | 100.00% | 65 / 65 |  | 100.00% | 6 / 6 | 34 |  | 100.00% | 1 / 1 | 
| matchList |  | 100.00% | 9 / 9 |  | 100.00% | 1 / 1 | 5 | |||
| matchEntry |  | 100.00% | 15 / 15 |  | 100.00% | 1 / 1 | 10 | |||
| matchExactHostname |  | 100.00% | 14 / 14 |  | 100.00% | 1 / 1 | 11 | |||
| matchIpv4 |  | 100.00% | 12 / 12 |  | 100.00% | 1 / 1 | 3 | |||
| matchIpv6 |  | 100.00% | 8 / 8 |  | 100.00% | 1 / 1 | 3 | |||
| ipv6ToBits |  | 100.00% | 7 / 7 |  | 100.00% | 1 / 1 | 2 | |||
| 1 | <?php | 
| 2 | |
| 3 | namespace Miniframe\Core; | 
| 4 | |
| 5 | class HostMatch | 
| 6 | { | 
| 7 | /** | 
| 8 | * Check if we're allowed | 
| 9 | * | 
| 10 | * @param Config $config Reference to the config. | 
| 11 | * @param string $section The configuration section (accesslist or forwarded-for). | 
| 12 | * @param string $key The configuration key (proxyhost, allow or deny). | 
| 13 | * @param string $matchAddress IP to validate. | 
| 14 | * | 
| 15 | * @return boolean|null Null when the section doesn't exist. | 
| 16 | */ | 
| 17 | public function matchList(Config $config, string $section, string $key, string $matchAddress): ?bool | 
| 18 | { | 
| 19 | if (!$config->has($section, $key)) { | 
| 20 | return null; | 
| 21 | } | 
| 22 | |
| 23 | // Fetch validated entries | 
| 24 | $validatedEntries = $config->get($section, $key); | 
| 25 | if (!is_array($validatedEntries)) { | 
| 26 | throw new \RuntimeException(ucfirst($section . ':' . $key) . ' list must be an array'); | 
| 27 | } | 
| 28 | |
| 29 | // Validate each entry | 
| 30 | foreach ($validatedEntries as $requiredEntry) { | 
| 31 | if ($this->matchEntry($requiredEntry, $matchAddress)) { | 
| 32 | return true; | 
| 33 | } | 
| 34 | } | 
| 35 | |
| 36 | return false; | 
| 37 | } | 
| 38 | |
| 39 | /** | 
| 40 | * Validates if an entry matches | 
| 41 | * | 
| 42 | * @param string $hostMatch IP address containing wildcards (as configured). | 
| 43 | * @param string $matchAddress The client IP. | 
| 44 | * | 
| 45 | * @return boolean | 
| 46 | */ | 
| 47 | private function matchEntry(string $hostMatch, string $matchAddress): bool | 
| 48 | { | 
| 49 | // Match all | 
| 50 | if (strtolower($hostMatch) == 'all') { | 
| 51 | return true; | 
| 52 | } | 
| 53 | // Exact match, that's easy | 
| 54 | if ($hostMatch === $matchAddress) { | 
| 55 | return true; | 
| 56 | } | 
| 57 | |
| 58 | // Match IPv4 or IPv6 address, or a full hostname match | 
| 59 | if ( | 
| 60 | preg_match('/([0-9]{1,3}|\*)\.([0-9]{1,3}|\*)/', $hostMatch) // Could include wildcards and CIDR header | 
| 61 | && filter_var($matchAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) | 
| 62 | ) { | 
| 63 | return $this->matchIpv4($hostMatch, $matchAddress); | 
| 64 | } elseif ( | 
| 65 | preg_match('/([0-9a-f]{1,5}\:|\:[0-9a-f]{1,5})/i', $hostMatch) // Could include CIDR header | 
| 66 | && filter_var($matchAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) | 
| 67 | ) { | 
| 68 | return $this->matchIpv6($hostMatch, $matchAddress); | 
| 69 | } elseif ( | 
| 70 | // At least one letter and one dot, so it's no IPv4 (numeric only) and no IPv6 (no dots), but no wildcards | 
| 71 | strpos($hostMatch, '.') !== false | 
| 72 | && strpos($hostMatch, '*') === false | 
| 73 | && preg_match('/[a-z]+/i', $hostMatch) | 
| 74 | ) { | 
| 75 | return $this->matchExactHostname($hostMatch, $matchAddress); | 
| 76 | } | 
| 77 | |
| 78 | throw new \RuntimeException('Invalid IP address format: ' . $hostMatch); | 
| 79 | } | 
| 80 | |
| 81 | /** | 
| 82 | * Resolves a hostname and matches against the IP address(es) we get back | 
| 83 | * | 
| 84 | * @param string $hostname The hostname that should be resolved. | 
| 85 | * @param string $matchAddress Client IP address to match to. | 
| 86 | * | 
| 87 | * @return boolean | 
| 88 | */ | 
| 89 | private function matchExactHostname(string $hostname, string $matchAddress): bool | 
| 90 | { | 
| 91 | if (filter_var($matchAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { | 
| 92 | $dnsRecords = dns_get_record($hostname, DNS_A); | 
| 93 | if (is_array($dnsRecords)) { | 
| 94 | foreach ($dnsRecords as $dnsRecord) { | 
| 95 | if ($dnsRecord['type'] === 'A' && $dnsRecord['ip'] == $matchAddress) { | 
| 96 | return true; | 
| 97 | } | 
| 98 | } | 
| 99 | } | 
| 100 | } | 
| 101 | if (filter_var($matchAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { | 
| 102 | $dnsRecords = dns_get_record($hostname, DNS_AAAA); | 
| 103 | if (is_array($dnsRecords)) { | 
| 104 | foreach ($dnsRecords as $dnsRecord) { | 
| 105 | if ( | 
| 106 | $dnsRecord['type'] === 'AAAA' | 
| 107 | && $this->ipv6ToBits($dnsRecord['ipv6']) == $this->ipv6ToBits($matchAddress) | 
| 108 | ) { | 
| 109 | return true; | 
| 110 | } | 
| 111 | } | 
| 112 | } | 
| 113 | } | 
| 114 | |
| 115 | return false; | 
| 116 | } | 
| 117 | |
| 118 | /** | 
| 119 | * Validates if an IPv4 address matches | 
| 120 | * | 
| 121 | * @param string $hostMatch IP address containing wildcards (as configured). | 
| 122 | * @param string $matchAddress The client IP. | 
| 123 | * | 
| 124 | * @return boolean | 
| 125 | */ | 
| 126 | private function matchIpv4(string $hostMatch, string $matchAddress): bool | 
| 127 | { | 
| 128 | // When the IP is notated as 192.168.2.*, we'll match | 
| 129 | if (strpos($hostMatch, '*') !== false) { | 
| 130 | $regex = preg_quote($hostMatch, '/'); | 
| 131 | $regex = '/^' . str_replace('\\*', '[0-9]{1,3}', $regex) . '$/'; | 
| 132 | return preg_match($regex, $matchAddress) > 0; | 
| 133 | } | 
| 134 | // When the IP is notated as 192.168.2.1/24, we'll match | 
| 135 | if (strpos($hostMatch, '/') !== false) { | 
| 136 | list($range, $netmask) = explode('/', $hostMatch, 2); | 
| 137 | $range_decimal = ip2long($range); | 
| 138 | $ip_decimal = ip2long($matchAddress); | 
| 139 | $wildcard_decimal = pow(2, ( 32 - (int)$netmask )) - 1; | 
| 140 | $netmask_decimal = ~ $wildcard_decimal; | 
| 141 | return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); | 
| 142 | } | 
| 143 | |
| 144 | // No match | 
| 145 | return false; | 
| 146 | } | 
| 147 | |
| 148 | /** | 
| 149 | * Validates if an IPv6 address matches | 
| 150 | * | 
| 151 | * @param string $hostMatch IP address containing wildcards (as configured). | 
| 152 | * @param string $matchAddress The client IP. | 
| 153 | * | 
| 154 | * @return boolean | 
| 155 | */ | 
| 156 | private function matchIpv6(string $hostMatch, string $matchAddress): bool | 
| 157 | { | 
| 158 | // Exact match | 
| 159 | if ($this->ipv6ToBits($matchAddress) === $this->ipv6ToBits($hostMatch)) { | 
| 160 | return true; | 
| 161 | } | 
| 162 | |
| 163 | // When the IP is notated as 21DA:00D3:0000:2F3B::/64, we'll match | 
| 164 | if (strpos($hostMatch, '/') !== false) { | 
| 165 | list($net, $maskBits) = explode('/', $this->ipv6ToBits($hostMatch)); | 
| 166 | $ip_net_bits = substr($this->ipv6ToBits($matchAddress), 0, (int)$maskBits); | 
| 167 | $net_bits = substr($net, 0, (int)$maskBits); | 
| 168 | return ($ip_net_bits == $net_bits); | 
| 169 | } | 
| 170 | |
| 171 | return false; | 
| 172 | } | 
| 173 | |
| 174 | /** | 
| 175 | * Converts an IPv6 address to bits | 
| 176 | * | 
| 177 | * @param string $ipv6Address The Ipv6 notated address. | 
| 178 | * | 
| 179 | * @return string | 
| 180 | */ | 
| 181 | private function ipv6ToBits(string $ipv6Address): string | 
| 182 | { | 
| 183 | $parts = explode('/', $ipv6Address, 2); // CIDR header will remain untouched | 
| 184 | |
| 185 | $ip = inet_pton($parts[0]); | 
| 186 | $binary = ''; | 
| 187 | foreach (str_split((string)$ip) as $char) { | 
| 188 | $binary .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT); | 
| 189 | } | 
| 190 | |
| 191 | $parts[0] = $binary; | 
| 192 | |
| 193 | return implode('/', $parts); | 
| 194 | } | 
| 195 | } |