Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Util/Ip.php
<?php

namespace XF\Util;

use function
chr, count, is_array, is_string, ord, strlen;

class
Ip
{
   
/**
     * Converts a string based IP (v4 or v6) to a 4 or 16 byte string.
     * This tries to identify not only 192.168.1.1 and 2001::1:2:3:4 style IPs,
     * but integer encoded IPv4 and already binary encoded IPs. IPv4
     * embedded in IPv6 via things like ::ffff:192.168.1.1 is also detected.
     *
     * @param string|int $ip
     *
     * @return bool|string False on failure, binary data otherwise
     */
   
public static function convertIpStringToBinary($ip)
    {
       
$originalIp = $ip;

        if (
strlen($ip) == 4)
        {
           
// already encoded IPv4
           
return $ip;
        }

        if (
strlen($ip) == 16 && preg_match('/[^0-9a-f.:]/i', $ip))
        {
           
// already encoded IPv6
           
return $ip;
        }

       
$ip = trim($ip, " \t");

        if (
strpos($ip, ':') !== false)
        {
           
// IPv6
           
if (preg_match('#:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$#i', $ip, $match))
            {
               
// embedded IPv4, just treat as IPv4
               
$long = ip2long($match[1]);
                if (!
$long)
                {
                    return
false;
                }

                return
self::convertHexToBin(
                   
str_pad(dechex($long), 8, '0', STR_PAD_LEFT)
                );
            }

            if (
strpos($ip, '::') !== false)
            {
                if (
substr_count($ip, '::') > 1)
                {
                   
// ambiguous
                   
return false;
                }

               
$delims = substr_count($ip, ':');
                if (
$delims > 7)
                {
                    return
false;
                }

               
$ip = str_replace('::', str_repeat(':0', 8 - $delims) . ':', $ip);
                if (
$ip[0] == ':')
                {
                   
$ip = '0' . $ip;
                }
            }

           
$ip = strtolower($ip);

           
$parts = explode(':', $ip);
            if (
count($parts) != 8)
            {
                return
false;
            }

            foreach (
$parts AS &$part)
            {
               
$len = strlen($part);
                if (
$len > 4 || preg_match('/[^0-9a-f]/', $part))
                {
                    return
false;
                }

                if (
$len < 4)
                {
                   
$part = str_repeat('0', 4 - $len) . $part;
                }
            }

           
$hex = implode('', $parts);
            if (
strlen($hex) != 32)
            {
                return
false;
            }

            if (
preg_match('/^00000000000000000000ffff([0-9a-f]{8})$/', $hex, $match))
            {
               
// ::ffff:IPv4 address that was written in pure IPv6 form, treat as an IPv4 address
               
return self::convertHexToBin($match[1]);
            }

            return
self::convertHexToBin($hex);
        }

        if (
strpos($ip, '.'))
        {
           
// IPv4
           
if (!preg_match('#(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})#', $ip, $match))
            {
                return
false;
            }

           
$long = ip2long($match[1]);
            if (
$long === false)
            {
                return
false;
            }

            return
self::convertHexToBin(
               
str_pad(dechex($long), 8, '0', STR_PAD_LEFT)
            );
        }

        if (
strlen($ip) == 4 || strlen($ip) == 16)
        {
           
// already binary encoded
           
return $ip;
        }

        if (
is_numeric($originalIp) && $originalIp < pow(2, 32))
        {
           
// IPv4 as integer
           
return self::convertHexToBin(
               
str_pad(dechex($originalIp), 8, '0', STR_PAD_LEFT)
            );
        }

        return
false;
    }

   
/**
     * Converts a hex string to binary
     *
     * @param string $hex
     *
     * @return string
     */
   
public static function convertHexToBin($hex)
    {
        if (
function_exists('hex2bin'))
        {
            return
hex2bin($hex);
        }

       
$len = strlen($hex);

        if (
$len % 2)
        {
           
trigger_error('Hexadecimal input string must have an even length', E_USER_WARNING);
        }

        if (
strspn($hex, '0123456789abcdefABCDEF') != $len)
        {
           
trigger_error('Input string must be hexadecimal string', E_USER_WARNING);
        }

        return
pack('H*', $hex);
    }

   
/**
     * Converts a binary string containing IPv4 or v6 data to a printable/human
     * readable version. If shortening is enabled, IPv6 data will be collapsed
     * as much as possible.
     *
     * @param string $ip Binary IP data
     * @param bool $shorten
     *
     * @return bool|string
     */
   
public static function convertIpBinaryToString($ip, $shorten = true)
    {
        if (
strlen($ip) == 4)
        {
           
// IPv4
           
$parts = [];
            foreach (
str_split($ip) AS $char)
            {
               
$parts[] = ord($char);
            }

            return
implode('.', $parts);
        }

        if (
strlen($ip) == 16)
        {
           
// IPv6
           
$parts = [];
           
$chunks = str_split($ip);
            for (
$i = 0; $i < 16; $i += 2)
            {
               
$char1 = $chunks[$i];
               
$char2 = $chunks[$i + 1];

               
$part = sprintf('%02x%02x', ord($char1), ord($char2));
                if (
$shorten)
                {
                   
// reduce this to the shortest length possible, but keep 1 zero if needed
                   
$part = ltrim($part, '0');
                    if (!
strlen($part))
                    {
                       
$part = '0';
                    }
                }
               
$parts[] = $part;
            }

           
$output = implode(':', $parts);
            if (
$shorten)
            {
               
$output = preg_replace_callback(
                   
'/((^0|:0){2,})(.*)$/',
                    function(
$matches)
                    {
                        return
':' . (strlen($matches[3]) ? $matches[3] : ':');
                    },
                   
$output
               
);

                if (
$output == ':')
                {
                   
// correct way of writing an IPv6 address of all zeroes
                   
$output = '::';
                }

                if (
preg_match('/^::ffff:([0-9a-f]{2})([0-9a-f]{2}):([0-9a-f]{2})([0-9a-f]{2})$/i', $output, $match))
                {
                   
// IPv4-mapped IPv6
                   
$output = '::ffff:' . hexdec($match[1]) . '.' . hexdec($match[2]) . '.'
                       
. hexdec($match[3]) . '.' . hexdec($match[4]);
                }
            }

            return
strtolower($output);
        }

        if (
preg_match('/^[0-9]+$/', $ip))
        {
            return
long2ip($ip + 0);
        }

        return
false;
    }

   
/**
     * Gets the range of binary-encoded IPs that match the given
     * CIDR block size. Supports IPv4 and v6. Ranges can be checked
     * via >= lower and <= upper.
     *
     * @param string $ip Binary IP
     * @param integer $cidr CIDR range
     *
     * @return array|string If no CIDR is specified or if the CIDR specifies only one address, a string with that address.
     *         Otherwise, an array with a lower and upper bound.
     */
   
public static function getIpCidrMatchRange($ip, $cidr)
    {
        if (!
$cidr)
        {
            return
$ip;
        }

       
$bytes = strlen($ip);
       
$bits = $bytes * 8;
        if (
$cidr >= $bits)
        {
            return
$ip; // exact match
       
}

       
$prefixBytes = (int)floor($cidr / 8);
       
$remainingBits = ($cidr - $prefixBytes * 8);

       
$prefix = substr($ip, 0, $prefixBytes);
        if (
$remainingBits)
        {
           
$partialByteOrd = ord($ip[$prefixBytes]); // first character after full prefix bytes
           
$mask = (1 << 8 - $remainingBits) - 1;

           
$upperBound = chr($partialByteOrd | $mask);
           
$lowerBound = chr($partialByteOrd & ~$mask);
           
$boundLength = 1;
        }
        else
        {
           
$upperBound = '';
           
$lowerBound = '';
           
$boundLength = 0;
        }

       
$suffixBytes = $bytes - $prefixBytes - $boundLength;
        if (
$suffixBytes)
        {
           
$lowerSuffix = str_repeat(chr(0), $suffixBytes);
           
$upperSuffix = str_repeat(chr(255), $suffixBytes);
        }
        else
        {
           
$lowerSuffix = '';
           
$upperSuffix = '';
        }

        return [
$prefix . $lowerBound . $lowerSuffix, $prefix . $upperBound . $upperSuffix];
    }

   
/**
     * Determines if a particular IP matches a IP CIDR range.
     * Both IPs must be binary encoded
     *
     * @param string $testIp Binary encoded IP to test if within range
     * @param string $rangeIp Binary encoded IP to make the range from
     * @param integer $cidr CIDR block size
     *
     * @return bool
     */
   
public static function ipMatchesCidrRange($testIp, $rangeIp, $cidr)
    {
       
$range = self::getIpCidrMatchRange($rangeIp, $cidr);
        if (
is_string($range))
        {
            return (
$testIp == $range);
        }
        else
        {
            return
self::ipMatchesRange($testIp, $range[0], $range[1]);
        }
    }

   
/**
     * Simplifies checking if an IP is within a range.
     * All IPs and ranges must be binary encoded.
     *
     * @param string $testIp
     * @param string $lowerBound
     * @param string $upperBound
     *
     * @return bool
     */
   
public static function ipMatchesRange($testIp, $lowerBound, $upperBound)
    {
        return (
$testIp >= $lowerBound AND $testIp <= $upperBound AND strlen($testIp) == strlen($lowerBound));
    }

   
/**
     * @param array|string $checkIps List of IPs to check for a match
     * @param array $firstByteRangeList List of binary IP ranges [start, end] keyed by [binary first byte]
     *
     * @return bool
     */
   
public static function checkIpsAgainstBinaryRangeList($checkIps, array $firstByteRangeList)
    {
        if (!
is_array($checkIps))
        {
           
$checkIps = [$checkIps];
        }

        foreach (
$checkIps AS $ip)
        {
           
$binary = self::convertIpStringToBinary($ip);
            if (!
$binary)
            {
                continue;
            }

           
$firstByte = $binary[0];

            if (empty(
$firstByteRangeList[$firstByte]))
            {
                continue;
            }

            foreach (
$firstByteRangeList[$firstByte] AS $range)
            {
                if (
self::ipMatchesRange($binary, $range[0], $range[1]))
                {
                    return
$range;
                }
            }
        }

        return
null;
    }

   
/**
     * Parses a human readable IP range string into a machine processable version.
     * IPv4 can be specified with CIDR or 192.168.* style ranges. IPv6 supports CIDR only.
     *
     * @param string $ip Human readable IPv4 or v6 IP
     *
     * @return array|bool False on failure, Otherwise array with following keys:
     *         - printable: human readable version of the IP range. May be adjusted to standardize display slightly
     *         - binary: binary version of provided IP. IPv4 missing octets are considered to be 0.
     *         - cidr: the final CIDR range found/used. If a IPv4 partial is provided, this will be determined from number of missing octets. 0 for exact.
     *         - isRange: true if the provided IP actually spans a range
     *        - startRange: binary version of lower bound IP
     *        - endRange: binary version of upper bound IP
     */
   
public static function parseIpRangeString($ip)
    {
       
$ip = trim($ip);
       
$niceIp = $ip;

        if (
preg_match('#/(\d+)$#', $ip, $match))
        {
           
$ip = substr($ip, 0, -strlen($match[0]));
           
$cidr = $match[1];
            if (
$cidr && $cidr < 8)
            {
               
$cidr = 8;
               
$niceIp = $ip . "/$cidr";
            }
        }
        else
        {
           
$cidr = 0;
        }

        if (
strpos($ip, ':') !== false)
        {
           
// IPv6 -- no partials, only CIDR
           
$binary = self::convertIpStringToBinary($ip);
            if (
$binary === false)
            {
                return
false;
            }
        }
        else
        {
           
$ip = preg_replace('/\.+$/', '', $ip);
            if (!
preg_match('/^\d+(\.\d+){0,2}(\.\d+|\.\*)?$/', $ip))
            {
                return
false;
            }

            if (
substr($ip, -2) == '.*')
            {
               
$ip = substr($ip, 0, -2);
            }

           
$ipParts = explode('.', $ip);
            foreach (
$ipParts AS $part)
            {
                if (
$part < 0 || $part > 255)
                {
                    return
false;
                }
            }

           
$localCidr = 32;
            while (
count($ipParts) < 4)
            {
               
$ipParts[] = 0;
               
$localCidr -= 8;
            }

            if (!
$cidr && $localCidr != 32)
            {
               
$cidr = $localCidr;
               
$niceIp = $ip . '.*';
            }

           
$binary = self::convertIpStringToBinary(implode('.', $ipParts));
            if (!
$binary)
            {
                return
false;
            }
        }

       
$range = self::getIpCidrMatchRange($binary, $cidr);

        return [
           
'printable' => $niceIp,
           
'binary' => $binary,
           
'cidr' => $cidr,
           
'isRange' => is_array($range),
           
'startRange' => is_string($range) ? $range : $range[0],
           
'endRange' => is_string($range) ? $range : $range[1]
        ];
    }

    protected static
$lookupCache = [];

   
/**
     * Resolves the host name of an IP address
     *
     * @param string $ip
     *
     * @return string
     */
   
public static function getHost($ip)
    {
        if (isset(
self::$lookupCache[$ip]))
        {
            return
self::$lookupCache[$ip];
        }

       
$decompressed = self::convertIpBinaryToString($ip, false);
        if (
$decompressed !== false)
        {
           
$ip = $decompressed;
        }

        if (
strpos($ip, ':') !== false)
        {
           
// we need to uncompress the hex address to split this up
           
$binary = self::convertIpStringToBinary($ip);
            if (!
$binary)
            {
                return
'';
            }
           
$checkIp = self::convertIpBinaryToString($binary, false);
            if (!
$checkIp)
            {
                return
false;
            }

           
$checkIp = str_replace(':', '', $checkIp);
           
$parts = str_split($checkIp);
           
$dnsRecord = implode('.', array_reverse($parts)) . '.ip6.arpa';
        }
        else
        {
           
$parts = explode('.', $ip);
            if (
count($parts) != 4)
            {
                return
'';
            }

           
$dnsRecord = implode('.', array_reverse($parts)) . '.in-addr.arpa';
        }

       
$lookup = false;

        try
        {
            if (
function_exists('dns_get_record'))
            {
               
$host = @dns_get_record($dnsRecord, DNS_PTR);
                if (isset(
$host[0]['target']))
                {
                   
$lookup = $host[0]['target'];
                }
            }
            else
            {
               
$lookup = gethostbyaddr($ip);
            }
        }
        catch (\
Exception $e) { } // bad lookup

       
if (!$lookup)
        {
           
$lookup = $ip;
        }

       
self::$lookupCache[$ip] = $lookup;
        return
$lookup;
    }
}