4) { $ipBin = substr($ipBin, 0, self::$ipv6Length); $countryPos = self::BinarySearch($ipBin, $file, $ip6TablePos, $ip6TableLength, 2); } else { $countryPos = self::BinarySearch($ipBin, $file, $ip4TablePos, $ip4TableLength, 2); } if ($countryPos === false) return null; // Nothing found if ($countryPos == 0xFFFF) return null; // Nothing found (gap filling record) // Read country data if ($countryPos >= $countriesTableLength) throw new Exception('Invalid country pointer: ' . $countryPos . ' >= ' . $countriesTableLength); fseek($file, $countriesTablePos + $countryPos); $countryCode = fread($file, 2); $countryNameLength = self::BinToNum(fread($file, 1)); if ($countryNameLength > 0) { $countryName = trim(fread($file, $countryNameLength)); } else { $countryName = ''; } return array( 'country' => $countryName, 'cc' => $countryCode ); } // Performs an IP address lookup in the city-level database file. // // ipBin = (array) IP address as byte array. // file = (resource) Opened database file handle. // private static function LookupIPCity($ipBin, $file) { // Read section pointers $countriesTablePos = self::$headerLength + 4 * 4; // Skip 4 pointers fseek($file, self::$headerLength); $regionsTablePos = self::BinToNum(fread($file, 4)); $citiesTablePos = self::BinToNum(fread($file, 4)); $ip4TablePos = self::BinToNum(fread($file, 4)); $ip6TablePos = self::BinToNum(fread($file, 4)); $stat = fstat($file); $countriesTableLength = $regionsTablePos - $countriesTablePos; $regionsTableLength = $citiesTablePos - $regionsTablePos; $citiesTableLength = $ip4TablePos - $citiesTablePos; $ip4TableLength = $ip6TablePos - $ip4TablePos; $ip6TableLength = intval($stat['size']) - $ip6TablePos; // Find IP address range if (strlen($ipBin) > 4) { $ipBin = substr($ipBin, 0, self::$ipv6Length); $cityPos = self::BinarySearch($ipBin, $file, $ip6TablePos, $ip6TableLength, 3); } else { $cityPos = self::BinarySearch($ipBin, $file, $ip4TablePos, $ip4TableLength, 3); } if ($cityPos === false) return null; // Nothing found if ($cityPos == 0xFFFFFF) return null; // Nothing found (gap filling record) // Read city data #echo 'Reading city data at 0x' . dechex($citiesTablePos + $cityPos) . ' (0x' . dechex($citiesTablePos) . ' + 0x' . dechex($cityPos) . ")\n"; if ($cityPos >= $citiesTableLength) throw new Exception('Invalid city pointer: ' . $cityPos . ' >= ' . $citiesTableLength); fseek($file, $citiesTablePos + $cityPos); $lat = self::BinToNum(fread($file, 3)) / 10000 - 90; $lng = self::BinToNum(fread($file, 3)) / 10000 - 180; $regionPos = self::BinToNum(fread($file, 2)); $cityLength = self::BinToNum(fread($file, 1)); if ($cityLength > 0) { $city = trim(fread($file, $cityLength)); } else { $city = ''; } // Read region data if (($regionPos & 0x8000) == 0) { // This is a region pointer, expand it to full resolution $regionPos *= 2; #echo 'Reading region data at 0x' . dechex($regionsTablePos + $regionPos) . ' (0x' . dechex($regionsTablePos) . ' + 0x' . dechex($regionPos) . ")\n"; if ($regionPos >= $regionsTableLength) throw new Exception('Invalid region pointer: ' . $regionPos . ' >= ' . $regionsTableLength); fseek($file, $regionsTablePos + $regionPos); $countryPos = self::BinToNum(fread($file, 2)); $regionLength = self::BinToNum(fread($file, 1)); if ($regionLength > 0) { $region = trim(fread($file, $regionLength)); } else { $region = ''; } } else { // This is already a country pointer because there is no region set, // unset the type bit and continue with the country $countryPos = $regionPos & 0x7FFF; $region = ''; } // Read country data #echo 'Reading country data at 0x' . dechex($countriesTablePos + $countryPos) . ' (0x' . dechex($countriesTablePos) . ' + 0x' . dechex($countryPos) . ")\n"; if ($countryPos >= $countriesTableLength) throw new Exception('Invalid country pointer: ' . $countryPos . ' >= ' . $countriesTableLength); fseek($file, $countriesTablePos + $countryPos); $countryCode = fread($file, 2); $countryNameLength = self::BinToNum(fread($file, 1)); if ($countryNameLength > 0) { $countryName = trim(fread($file, $countryNameLength)); } else { $countryName = ''; } return array( 'lat' => $lat, 'lng' => $lng, 'city' => $city, 'region' => $region, 'country' => $countryName, 'cc' => $countryCode ); } // Performs a binary search for an IP address in the database file. // // ipBin = (array) IP address as byte array. Must be the same length as the IP fields in the database table. // file = (resource) Opened database file handle. // start = (int) Start position of the IP table range in the file. // length = (int) Length of the IP table range in the file. // posSize = (int) Size of a position reference in bytes. // // Returns the position reference stored for the matching IP address range. // private static function BinarySearch($ipBin, $file, $start, $length, $posSize) { if ($length <= 0) return false; // Empty table, nothing to find // Compute the number of records in the given file range $ipSize = strlen($ipBin); $recordLength = $ipSize + $posSize; $recordCount = $length / $recordLength; // Initialisation $startIdx = 0; $endIdx = $recordCount - 1; // Do binary search #echo 'Searching for IP ' . bin2hex($ipBin) . "\n"; do { $idx = round(($startIdx + $endIdx) / 2); fseek($file, $start + $idx * $recordLength); // Read current range $ipStart = fread($file, $ipSize); $posData = fread($file, $posSize); // Skip position data (keep it for returning) if ($idx < $recordCount - 1) { // There is a valid record after the current index, read its IP $ipEnd = fread($file, $ipSize); } else { // This is the last record, the end IP is the maximum value $ipEnd = str_repeat(chr(255), $ipSize); } #echo ' Range: ' . $startIdx . '..' . $endIdx . ', reading: ' . $idx . # ' (file position 0x' . dechex($start + $idx * $recordLength) . # '), IP: ' . bin2hex($ipStart) . ' - ' . bin2hex($ipEnd) . "\n"; if ($ipStart <= $ipBin && $ipBin < $ipEnd) // IP address is in current range { return self::BinToNum($posData); } else if ($ipBin < $ipStart) // IP address is smaller than current range { $endIdx = $idx - 1; } else if ($ipEnd < $ipBin) // IP address is greater than current range { $startIdx = $idx + 1; } } while ($startIdx <= $endIdx); // Nothing found return false; } // Converts an IP address to a binary string. // // ip = (string) IPv4 or IPv6 address to convert. // // Returns (string) Binary string, or {{false}} for invalid input. // IPv4 addresses are 4 bytes long, IPv6 addresses 8 bytes (interface part is stripped off). // private static function IpToBin($ip) { // Try IPv4 address if (preg_match('_^(\d+)\.(\d+)\.(\d+)\.(\d+)$_', $ip, $m)) { // TODO: Check for byte range return chr($m[1]) . chr($m[2]) . chr($m[3]) . chr($m[4]); } // Try IPv6 address $ipv6 = self::ExpandIPv6($ip); if ($ipv6 !== false) { // 1234:1234:1234:1234:1234:1234:1234:1234 $bin = ''; for ($i = 0; $i < 8; $i++) { $chunk1 = substr($ipv6, $i * 5 + 0, 2); $chunk2 = substr($ipv6, $i * 5 + 2, 2); $dec1 = hexdec($chunk1); $dec2 = hexdec($chunk2); $bin .= chr($dec1) . chr($dec2); } return $bin; } throw new Exception('Invalid IP address format'); } // Expands an IPv6 address into its unshortened form. // // ip_addr = (string) IPv6 address to expand. // // Returns (string) Unshortened IPv6 address, or {{false}} for invalid input. // private static function ExpandIPv6($ip_addr) { $ip_addr = strtolower($ip_addr); // Zuerst auf gültige Zeichen prüfen if (!preg_match('/^[0-9a-f:]+$/', $ip_addr)) return false; // "::" darf nur einmal vorkommen if (substr_count($ip_addr, '::') > 1) return false; // "::" auflösen if ($ip_addr == '::') $ip_addr = '0:0:0:0:0:0:0:0'; $ip_addr = preg_replace('_^::_', '*:', $ip_addr); $ip_addr = preg_replace('_::$_', ':*', $ip_addr); $ip_addr = str_replace('::', ':*:', $ip_addr); $cnt = count(explode(':', $ip_addr)); if ($cnt <= 8) { if (strpos($ip_addr, ':*') !== false) $ip_addr = str_replace(':*', str_repeat(':0', 8 - ($cnt - 1)), $ip_addr); else if (strpos($ip_addr, '*:') !== false) $ip_addr = str_replace('*:', str_repeat('0:', 8 - ($cnt - 1)), $ip_addr); } // Blöcke zerlegen und prüfen $expanded = ''; $chunks = explode(':', $ip_addr); if (count($chunks) != 8) return false; foreach ($chunks as $chunk) { if (!preg_match('/^[0-9a-f]{1,4}$/', $chunk)) return false; $expanded .= (strlen($expanded) > 0 ? ':' : '') . str_pad($chunk, 4, '0', STR_PAD_LEFT); } return $expanded; } // Converts a large decimal number string into a fixed-length binary number. // // num = (string) Decimal number string. // length = (int) Minimum length of the binary string to return. // // Returns (string) Binary string of the decimal number. // private static function NumToBin($num, $length = 0) { $bin = ''; do { $byte = bcmod($num, 256); $bin = chr($byte) . $bin; $num = bcsub($num, $byte); $num = bcdiv($num, 256); } while (bccomp($num, 0) != 0); while (strlen($bin) < $length) { $bin = chr(0) . $bin; } return $bin; } // Converts a binary string to a decimal number. // // bin = (string) Binary string. // // Returns (int) Decimal number value. // private static function BinToNum($bin) { $num = 0; for ($i = 0; $i < strlen($bin); $i++) { $num *= 256; $num += ord($bin{$i}); } return $num; } } // class GeoIP ?>