Files
WP-Security-Pack/wp-security-pack/includes/class-wpsp-ip2location.php
T
2026-01-26 11:23:23 +01:00

385 lines
7.7 KiB
PHP

<?php
/**
* IP2Location database reader for WP Security Pack.
*
* Reads IP2Location BIN format databases.
*
* @package WP_Security_Pack
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* IP2Location BIN database reader.
*
* Based on IP2Location PHP Module but simplified for our needs.
*/
class WPSP_IP2Location {
/**
* Database file handle.
*
* @var resource|null
*/
private $handle = null;
/**
* Database type.
*
* @var int
*/
private $db_type = 0;
/**
* Database column count.
*
* @var int
*/
private $db_column = 0;
/**
* Database year.
*
* @var int
*/
private $db_year = 0;
/**
* Database month.
*
* @var int
*/
private $db_month = 0;
/**
* Database day.
*
* @var int
*/
private $db_day = 0;
/**
* IPv4 database count.
*
* @var int
*/
private $ipv4_count = 0;
/**
* IPv4 database address.
*
* @var int
*/
private $ipv4_addr = 0;
/**
* IPv6 database count.
*
* @var int
*/
private $ipv6_count = 0;
/**
* IPv6 database address.
*
* @var int
*/
private $ipv6_addr = 0;
/**
* IPv4 index base address.
*
* @var int
*/
private $ipv4_index_addr = 0;
/**
* IPv6 index base address.
*
* @var int
*/
private $ipv6_index_addr = 0;
/**
* Constructor.
*
* @param string $db_path Path to database file.
*/
public function __construct( $db_path ) {
if ( ! file_exists( $db_path ) ) {
return;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$this->handle = fopen( $db_path, 'rb' );
if ( ! $this->handle ) {
return;
}
// Read database header.
$this->db_type = $this->read_byte( 1 );
$this->db_column = $this->read_byte( 2 );
$this->db_year = $this->read_byte( 3 );
$this->db_month = $this->read_byte( 4 );
$this->db_day = $this->read_byte( 5 );
$this->ipv4_count = $this->read_word( 6 );
$this->ipv4_addr = $this->read_word( 10 );
$this->ipv6_count = $this->read_word( 14 );
$this->ipv6_addr = $this->read_word( 18 );
$this->ipv4_index_addr = $this->read_word( 22 );
$this->ipv6_index_addr = $this->read_word( 26 );
}
/**
* Destructor.
*/
public function __destruct() {
if ( $this->handle ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
fclose( $this->handle );
}
}
/**
* Lookup an IP address.
*
* @param string $ip IP address.
* @return array|null
*/
public function lookup( $ip ) {
if ( ! $this->handle ) {
return null;
}
// Determine IP version.
$ip_version = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ? 6 : 4;
if ( 4 === $ip_version ) {
return $this->lookup_ipv4( $ip );
}
return $this->lookup_ipv6( $ip );
}
/**
* Lookup IPv4 address.
*
* @param string $ip IPv4 address.
* @return array|null
*/
private function lookup_ipv4( $ip ) {
if ( $this->ipv4_count <= 0 ) {
return null;
}
$ip_num = sprintf( '%u', ip2long( $ip ) );
// Use index for faster lookup.
$index = ( $ip_num >> 16 );
$low = 0;
$high = $this->ipv4_count;
if ( $this->ipv4_index_addr > 0 ) {
$low = $this->read_word( $this->ipv4_index_addr + ( $index * 8 ) );
$high = $this->read_word( $this->ipv4_index_addr + ( $index * 8 ) + 4 );
}
// Binary search.
while ( $low <= $high ) {
$mid = (int) ( ( $low + $high ) / 2 );
$ip_from = $this->read_word( $this->ipv4_addr + $mid * $this->db_column * 4 );
$ip_to = $this->read_word( $this->ipv4_addr + ( $mid + 1 ) * $this->db_column * 4 );
if ( $ip_num >= $ip_from && $ip_num < $ip_to ) {
return $this->read_record( $this->ipv4_addr + $mid * $this->db_column * 4, 4 );
}
if ( $ip_num < $ip_from ) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}
return null;
}
/**
* Lookup IPv6 address.
*
* @param string $ip IPv6 address.
* @return array|null
*/
private function lookup_ipv6( $ip ) {
if ( $this->ipv6_count <= 0 ) {
return null;
}
$ip_num = $this->ipv6_to_number( $ip );
$low = 0;
$high = $this->ipv6_count;
if ( $this->ipv6_index_addr > 0 ) {
$index_value = bcdiv( $ip_num, bcpow( '2', '112' ) );
$index = bcmod( $index_value, '65536' );
$low = $this->read_word( $this->ipv6_index_addr + (int) $index * 8 );
$high = $this->read_word( $this->ipv6_index_addr + (int) $index * 8 + 4 );
}
// Binary search.
while ( $low <= $high ) {
$mid = (int) ( ( $low + $high ) / 2 );
$ip_from = $this->read_ipv6( $this->ipv6_addr + $mid * ( $this->db_column * 4 + 12 ) );
$ip_to = $this->read_ipv6( $this->ipv6_addr + ( $mid + 1 ) * ( $this->db_column * 4 + 12 ) );
if ( bccomp( $ip_num, $ip_from ) >= 0 && bccomp( $ip_num, $ip_to ) < 0 ) {
return $this->read_record( $this->ipv6_addr + $mid * ( $this->db_column * 4 + 12 ) + 12, 6 );
}
if ( bccomp( $ip_num, $ip_from ) < 0 ) {
$high = $mid - 1;
} else {
$low = $mid + 1;
}
}
return null;
}
/**
* Read record from database.
*
* @param int $row_addr Row address.
* @param int $ip_version IP version.
* @return array
*/
private function read_record( $row_addr, $ip_version ) {
$record = array(
'country_code' => '-',
'country_name' => '-',
);
// Country is always at column 0 for DB1-DB24.
$country_pos = $row_addr + 4;
if ( 6 === $ip_version ) {
$country_pos = $row_addr;
}
$country_offset = $this->read_word( $country_pos );
if ( $country_offset > 0 ) {
$record['country_code'] = $this->read_string( $country_offset );
$record['country_name'] = $this->read_string( $country_offset + 3 );
}
return $record;
}
/**
* Read a byte from database.
*
* @param int $pos Position.
* @return int
*/
private function read_byte( $pos ) {
fseek( $this->handle, $pos - 1, SEEK_SET );
$data = fread( $this->handle, 1 );
return unpack( 'C', $data )[1];
}
/**
* Read a 32-bit word from database.
*
* @param int $pos Position.
* @return int
*/
private function read_word( $pos ) {
fseek( $this->handle, $pos - 1, SEEK_SET );
$data = fread( $this->handle, 4 );
return unpack( 'V', $data )[1];
}
/**
* Read a string from database.
*
* @param int $pos Position.
* @return string
*/
private function read_string( $pos ) {
fseek( $this->handle, $pos, SEEK_SET );
$length = unpack( 'C', fread( $this->handle, 1 ) )[1];
return fread( $this->handle, $length );
}
/**
* Read IPv6 address from database.
*
* @param int $pos Position.
* @return string
*/
private function read_ipv6( $pos ) {
fseek( $this->handle, $pos - 1, SEEK_SET );
$data = fread( $this->handle, 16 );
$int = unpack( 'V4', $data );
$result = bcadd( bcadd( bcmul( $int[4], bcpow( '4294967296', '3' ) ), bcmul( $int[3], bcpow( '4294967296', '2' ) ) ), bcadd( bcmul( $int[2], '4294967296' ), $int[1] ) );
return $result;
}
/**
* Convert IPv6 to number.
*
* @param string $ip IPv6 address.
* @return string
*/
private function ipv6_to_number( $ip ) {
$bin = inet_pton( $ip );
$hex = bin2hex( $bin );
$result = '0';
for ( $i = 0; $i < strlen( $hex ); $i++ ) {
$result = bcadd( bcmul( $result, '16' ), hexdec( $hex[ $i ] ) );
}
return $result;
}
/**
* Get database type string.
*
* @return string
*/
public function get_database_type() {
if ( ! $this->handle ) {
return '';
}
$types = array(
1 => 'DB1 (Country)',
2 => 'DB2 (Country + ISP)',
3 => 'DB3 (Country + Region + City)',
4 => 'DB4 (Country + Region + City + ISP)',
5 => 'DB5 (Country + Region + City + Lat/Long)',
11 => 'DB11 (Full)',
24 => 'DB24 (Full)',
);
$date = sprintf( '%04d-%02d-%02d', 2000 + $this->db_year, $this->db_month, $this->db_day );
$type = isset( $types[ $this->db_type ] ) ? $types[ $this->db_type ] : 'DB' . $this->db_type;
return $type . ' (' . $date . ')';
}
}