Phase 6: AIOS security plugin with conservative login lockdown config (10 attempts)

This commit is contained in:
Hanson.xyz Dev
2025-11-28 17:19:54 -06:00
parent 78a744ef06
commit abbd3502e8
430 changed files with 137111 additions and 7 deletions
@@ -0,0 +1,185 @@
<?php
namespace IPLib\Address;
use IPLib\Range\RangeInterface;
/**
* Interface of all the IP address types.
*/
interface AddressInterface
{
/**
* Get the short string representation of this address.
*
* @return string
*/
public function __toString();
/**
* Get the number of bits representing this address type.
*
* @return int
*
* @since 1.14.0
*
* @example 32 for IPv4
* @example 128 for IPv6
*/
public static function getNumberOfBits();
/**
* Get the string representation of this address.
*
* @param bool $long set to true to have a long/full representation, false otherwise
*
* @return string
*
* @example If $long is true, you'll get '0000:0000:0000:0000:0000:0000:0000:0001', '::1' otherwise.
*/
public function toString($long = false);
/**
* Get the byte list of the IP address.
*
* @return int[]
*
* @example For localhost: for IPv4 you'll get array(127, 0, 0, 1), for IPv6 array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
*/
public function getBytes();
/**
* Get the full bit list the IP address.
*
* @return string
*
* @since 1.14.0
*
* @example For localhost: For IPv4 you'll get '01111111000000000000000000000001' (32 digits), for IPv6 '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' (128 digits)
*/
public function getBits();
/**
* Get the type of the IP address.
*
* @return int One of the \IPLib\Address\Type::T_... constants
*
* @phpstan-return \IPLib\Address\Type::T_IPv4|\IPLib\Address\Type::T_IPv6
*/
public function getAddressType();
/**
* Get the default RFC reserved range type.
*
* @return int One of the \IPLib\Range\Type::T_... constants
*
* @since 1.5.0
*/
public static function getDefaultReservedRangeType();
/**
* Get the RFC reserved ranges (except the ones of type getDefaultReservedRangeType).
*
* @return \IPLib\Address\AssignedRange[] ranges are sorted
*
* @since 1.5.0
*/
public static function getReservedRanges();
/**
* Get the type of range of the IP address.
*
* @return int One of the \IPLib\Range\Type::T_... constants
*/
public function getRangeType();
/**
* Get a string representation of this address than can be used when comparing addresses and ranges.
*
* @return string
*/
public function getComparableString();
/**
* Check if this address is contained in an range.
*
* @param \IPLib\Range\RangeInterface $range
*
* @return bool
*/
public function matches(RangeInterface $range);
/**
* Get the address at a certain distance from this address.
*
* @param int|numeric-string|mixed $n the distance of the address (can be negative)
*
* @return \IPLib\Address\AddressInterface|null return NULL if $n is not an integer or NULL if $n is neither an integer nor a string containing a valid integer, or if the final address would be invalid
*
* @since 1.15.0
* @since 1.21.0 $n can also be a numeric string
*
* @example passing 1 to the address 127.0.0.1 will result in 127.0.0.2
* @example passing -1 to the address 127.0.0.1 will result in 127.0.0.0
* @example passing -1 to the address 0.0.0.0 will result in NULL
*/
public function getAddressAtOffset($n);
/**
* Get the address right after this IP address (if available).
*
* @return \IPLib\Address\AddressInterface|null
*
* @see \IPLib\Address\AddressInterface::getAddressAtOffset()
* @since 1.4.0
*/
public function getNextAddress();
/**
* Get the address right before this IP address (if available).
*
* @return \IPLib\Address\AddressInterface|null
*
* @see \IPLib\Address\AddressInterface::getAddressAtOffset()
* @since 1.4.0
*/
public function getPreviousAddress();
/**
* Get the Reverse DNS Lookup Address of this IP address.
*
* @return string
*
* @since 1.12.0
*
* @example for IPv4 it returns something like x.x.x.x.in-addr.arpa
* @example for IPv6 it returns something like x.x.x.x..x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa
*/
public function getReverseDNSLookupName();
/**
* Shift the bits of the address, padding with zeroes.
*
* @param int $bits If negative the bits will be shifted left, if positive the bits will be shifted right
*
* @return self
*
* @since 1.20.0
*
* @example shifting by 1 127.0.0.1 you'll have 63.128.0.0
* @example shifting by -1 127.0.0.1 you'll have 254.0.0.2
*/
public function shift($bits);
/**
* Create a new IP address by adding to this address another address.
*
* @return self|null returns NULL if $other is not compatible with this address, or if it generates an invalid address
*
* @since 1.20.0
*
* @example adding 0.0.0.10 to 127.0.0.1 generates the IP 127.0.0.11
* @example adding 255.0.0.10 to 127.0.0.1 generates NULL
*/
public function add(AddressInterface $other);
}
@@ -0,0 +1,140 @@
<?php
namespace IPLib\Address;
use IPLib\Range\RangeInterface;
/**
* Represents an IP address range with an assigned range type.
*
* @since 1.5.0
*/
class AssignedRange
{
/**
* The range definition.
*
* @var \IPLib\Range\RangeInterface
*/
protected $range;
/**
* The range type.
*
* @var int one of the \IPLib\Range\Type::T_ constants
*/
protected $type;
/**
* The list of exceptions for this range type.
*
* @var \IPLib\Address\AssignedRange[]
*/
protected $exceptions;
/**
* Initialize the instance.
*
* @param \IPLib\Range\RangeInterface $range the range definition
* @param int $type The range type (one of the \IPLib\Range\Type::T_ constants)
* @param \IPLib\Address\AssignedRange[] $exceptions the list of exceptions for this range type
*/
public function __construct(RangeInterface $range, $type, array $exceptions = array())
{
$this->range = $range;
$this->type = $type;
$this->exceptions = $exceptions;
}
/**
* Get the range definition.
*
* @return \IPLib\Range\RangeInterface
*/
public function getRange()
{
return $this->range;
}
/**
* Get the range type.
*
* @return int one of the \IPLib\Range\Type::T_ constants
*/
public function getType()
{
return $this->type;
}
/**
* Get the list of exceptions for this range type.
*
* @return \IPLib\Address\AssignedRange[]
*/
public function getExceptions()
{
return $this->exceptions;
}
/**
* Get the assigned type for a specific address.
*
* @param \IPLib\Address\AddressInterface $address
*
* @return int|null return NULL of the address is outside this address; a \IPLib\Range\Type::T_ constant otherwise
*/
public function getAddressType(AddressInterface $address)
{
$result = null;
if ($this->range->contains($address)) {
foreach ($this->exceptions as $exception) {
$result = $exception->getAddressType($address);
if ($result !== null) {
break;
}
}
if ($result === null) {
$result = $this->type;
}
}
return $result;
}
/**
* Get the assigned type for a specific address range.
*
* @param \IPLib\Range\RangeInterface $range
*
* @return int|false|null return NULL of the range is fully outside this range; false if it's partly crosses this range (or it contains mixed types); a \IPLib\Range\Type::T_ constant otherwise
*/
public function getRangeType(RangeInterface $range)
{
$myStart = $this->range->getComparableStartString();
$rangeEnd = $range->getComparableEndString();
if ($myStart > $rangeEnd) {
$result = null;
} else {
$myEnd = $this->range->getComparableEndString();
$rangeStart = $range->getComparableStartString();
if ($myEnd < $rangeStart) {
$result = null;
} elseif ($rangeStart < $myStart || $rangeEnd > $myEnd) {
$result = false;
} else {
$result = null;
foreach ($this->exceptions as $exception) {
$result = $exception->getRangeType($range);
if ($result !== null) {
break;
}
}
if ($result === null) {
$result = $this->getType();
}
}
}
return $result;
}
}
@@ -0,0 +1,582 @@
<?php
namespace IPLib\Address;
use IPLib\ParseStringFlag;
use IPLib\Range\RangeInterface;
use IPLib\Range\Subnet;
use IPLib\Range\Type as RangeType;
use IPLib\Service\BinaryMath;
use IPLib\Service\NumberInChunks;
/**
* An IPv4 address.
*
* @phpstan-consistent-constructor
*/
class IPv4 implements AddressInterface
{
/**
* The string representation of the address.
*
* @var string
*
* @example '127.0.0.1'
*/
protected $address;
/**
* The byte list of the IP address.
*
* @var int[]|null
*/
protected $bytes;
/**
* The type of the range of this IP address.
*
* @var int|null
*/
protected $rangeType;
/**
* A string representation of this address than can be used when comparing addresses and ranges.
*
* @var string|null
*/
protected $comparableString;
/**
* An array containing RFC designated address ranges.
*
* @var \IPLib\Address\AssignedRange[]|null
*/
private static $reservedRanges;
/**
* Initializes the instance.
*
* @param string $address
*/
protected function __construct($address)
{
$this->address = $address;
$this->bytes = null;
$this->rangeType = null;
$this->comparableString = null;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::__toString()
*/
public function __toString()
{
return $this->address;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getNumberOfBits()
*/
public static function getNumberOfBits()
{
return 32;
}
/**
* @deprecated since 1.17.0: use the parseString() method instead.
* For upgrading:
* - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $address the address to parse
* @param bool $mayIncludePort
* @param bool $supportNonDecimalIPv4
*
* @return static|null
*
* @see \IPLib\Address\IPv4::parseString()
* @since 1.1.0 added the $mayIncludePort argument
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function fromString($address, $mayIncludePort = true, $supportNonDecimalIPv4 = false)
{
return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Parse a string and returns an IPv4 instance if the string is valid, or null otherwise.
*
* @param string|mixed $address the address to parse
* @param int $flags A combination or zero or more flags
*
* @return static|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseString($address, $flags = 0)
{
if (!is_string($address)) {
return null;
}
$flags = (int) $flags;
$matches = null;
if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) {
if (preg_match('/^([12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2})\.in-addr\.arpa\.?$/i', $address, $matches)) {
$address = implode('.', array_reverse(explode('.', $matches[1])));
$flags = $flags & ~(ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED);
}
}
if ($flags & ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED) {
if (strpos($address, '.') === 0) {
return null;
}
$lengthNonHex = '{1,11}';
$lengthHex = '{1,8}';
$chunk234Optional = true;
} else {
if (!strpos($address, '.')) {
return null;
}
$lengthNonHex = '{1,3}';
$lengthHex = '{1,2}';
$chunk234Optional = false;
}
$rxChunk1 = "0?[0-9]{$lengthNonHex}";
if ($flags & ParseStringFlag::IPV4_MAYBE_NON_DECIMAL) {
$rxChunk1 = "(?:0[Xx]0*[0-9A-Fa-f]{$lengthHex})|(?:{$rxChunk1})";
$onlyDecimal = false;
} else {
$onlyDecimal = true;
}
$rxChunk1 = "0*?({$rxChunk1})";
$rxChunk234 = "\.{$rxChunk1}";
if ($chunk234Optional) {
$rxChunk234 = "(?:{$rxChunk234})?";
}
$rx = "{$rxChunk1}{$rxChunk234}{$rxChunk234}{$rxChunk234}";
if ($flags & ParseStringFlag::MAY_INCLUDE_PORT) {
$rx .= '(?::\d+)?';
}
if (!preg_match('/^' . $rx . '$/', $address, $matches)) {
return null;
}
$math = new \IPLib\Service\UnsignedIntegerMath();
$nums = array();
$maxChunkIndex = count($matches) - 1;
for ($i = 1; $i <= $maxChunkIndex; $i++) {
$numBytes = $i === $maxChunkIndex ? 5 - $i : 1;
$chunkBytes = $math->getBytes($matches[$i], $numBytes, $onlyDecimal);
if ($chunkBytes === null) {
return null;
}
$nums = array_merge($nums, $chunkBytes);
}
return new static(implode('.', $nums));
}
/**
* Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise.
*
* @param array<int|mixed> $bytes
*
* @return static|null
*/
public static function fromBytes(array $bytes)
{
$result = null;
if (count($bytes) === 4) {
$chunks = array_map(
function ($byte) {
return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false;
},
$bytes
);
if (in_array(false, $chunks, true) === false) {
$result = new static(implode('.', $chunks));
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::toString()
*/
public function toString($long = false)
{
if ($long) {
return $this->getComparableString();
}
return $this->address;
}
/**
* Get the octal representation of this IP address.
*
* @param bool $long
*
* @return string
*
* @since 1.10.0
*
* @example if $long == false: if the decimal representation is '0.7.8.255': '0.7.010.0377'
* @example if $long == true: if the decimal representation is '0.7.8.255': '0000.0007.0010.0377'
*/
public function toOctal($long = false)
{
$chunks = array();
foreach ($this->getBytes() as $byte) {
if ($long) {
$chunks[] = sprintf('%04o', $byte);
} else {
$chunks[] = '0' . decoct($byte);
}
}
return implode('.', $chunks);
}
/**
* Get the hexadecimal representation of this IP address.
*
* @param bool $long
*
* @return string
*
* @since 1.10.0
*
* @example if $long == false: if the decimal representation is '0.9.10.255': '0.9.0xa.0xff'
* @example if $long == true: if the decimal representation is '0.9.10.255': '0x00.0x09.0x0a.0xff'
*/
public function toHexadecimal($long = false)
{
$chunks = array();
foreach ($this->getBytes() as $byte) {
if ($long) {
$chunks[] = sprintf('0x%02x', $byte);
} else {
$chunks[] = '0x' . dechex($byte);
}
}
return implode('.', $chunks);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getBytes()
*/
public function getBytes()
{
if ($this->bytes === null) {
$this->bytes = array_map(
function ($chunk) {
return (int) $chunk;
},
explode('.', $this->address)
);
}
return $this->bytes;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getBits()
*/
public function getBits()
{
$parts = array();
foreach ($this->getBytes() as $byte) {
$parts[] = sprintf('%08b', $byte);
}
return implode('', $parts);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getAddressType()
*/
public function getAddressType()
{
return Type::T_IPv4;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
*/
public static function getDefaultReservedRangeType()
{
return RangeType::T_PUBLIC;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getReservedRanges()
*/
public static function getReservedRanges()
{
if (self::$reservedRanges === null) {
$reservedRanges = array();
foreach (array(
// RFC 5735
'0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)),
// RFC 5735
'10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK),
// RFC 6598
'100.64.0.0/10' => array(RangeType::T_CGNAT),
// RFC 5735
'127.0.0.0/8' => array(RangeType::T_LOOPBACK),
// RFC 5735
'169.254.0.0/16' => array(RangeType::T_LINKLOCAL),
// RFC 5735
'172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK),
// RFC 5735
'192.0.0.0/24' => array(RangeType::T_RESERVED),
// RFC 5735
'192.0.2.0/24' => array(RangeType::T_RESERVED),
// RFC 5735
'192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY),
// RFC 5735
'192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK),
// RFC 5735
'198.18.0.0/15' => array(RangeType::T_RESERVED),
// RFC 5735
'198.51.100.0/24' => array(RangeType::T_RESERVED),
// RFC 5735
'203.0.113.0/24' => array(RangeType::T_RESERVED),
// RFC 5735
'224.0.0.0/4' => array(RangeType::T_MULTICAST),
// RFC 5735
'240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)),
) as $range => $data) {
$exceptions = array();
if (isset($data[1])) {
foreach ($data[1] as $exceptionRange => $exceptionType) {
$subnet = Subnet::parseString($exceptionRange);
/** @var Subnet $subnet */
$exceptions[] = new AssignedRange($subnet, $exceptionType);
}
}
$subnet = Subnet::parseString($range);
/** @var Subnet $subnet */
$reservedRanges[] = new AssignedRange($subnet, $data[0], $exceptions);
}
self::$reservedRanges = $reservedRanges;
}
return self::$reservedRanges;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getRangeType()
*/
public function getRangeType()
{
if ($this->rangeType === null) {
$rangeType = null;
foreach (static::getReservedRanges() as $reservedRange) {
$rangeType = $reservedRange->getAddressType($this);
if ($rangeType !== null) {
break;
}
}
$this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
}
return $this->rangeType;
}
/**
* Create an IPv6 representation of this address (in 6to4 notation).
*
* @return \IPLib\Address\IPv6
*/
public function toIPv6()
{
$myBytes = $this->getBytes();
$ipv6 = IPv6::parseString('2002:' . sprintf('%02x', $myBytes[0]) . sprintf('%02x', $myBytes[1]) . ':' . sprintf('%02x', $myBytes[2]) . sprintf('%02x', $myBytes[3]) . '::');
/** @var IPv6 $ipv6 */
return $ipv6;
}
/**
* Create an IPv6 representation of this address (in IPv6 IPv4-mapped notation).
*
* @return \IPLib\Address\IPv6
*
* @since 1.11.0
*/
public function toIPv6IPv4Mapped()
{
$ipv6 = IPv6::fromBytes(array_merge(array(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff), $this->getBytes()));
/** @var IPv6 $ipv6 */
return $ipv6;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getComparableString()
*/
public function getComparableString()
{
if ($this->comparableString === null) {
$chunks = array();
foreach ($this->getBytes() as $byte) {
$chunks[] = sprintf('%03d', $byte);
}
$this->comparableString = implode('.', $chunks);
}
return $this->comparableString;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::matches()
*/
public function matches(RangeInterface $range)
{
return $range->contains($this);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getAddressAtOffset()
*/
public function getAddressAtOffset($n)
{
if (is_int($n)) {
$thatChunks = NumberInChunks::fromInteger($n, NumberInChunks::CHUNKSIZE_BYTES);
} elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
$thatChunks = NumberInChunks::fromNumericString($s, NumberInChunks::CHUNKSIZE_BYTES);
} else {
return null;
}
$myBytes = $this->getBytes();
while (isset($myBytes[1]) && $myBytes[0] === 0) {
array_shift($myBytes);
}
$myChunks = new NumberInChunks(false, $myBytes, NumberInChunks::CHUNKSIZE_BYTES);
$result = $myChunks->add($thatChunks);
if ($result->negative || count($result->chunks) > 4) {
return null;
}
return static::fromBytes(array_pad($result->chunks, -4, 0));
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getNextAddress()
*/
public function getNextAddress()
{
return $this->getAddressAtOffset(1);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getPreviousAddress()
*/
public function getPreviousAddress()
{
return $this->getAddressAtOffset(-1);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
*/
public function getReverseDNSLookupName()
{
return implode(
'.',
array_reverse($this->getBytes())
) . '.in-addr.arpa';
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::shift()
*/
public function shift($bits)
{
$bits = (int) $bits;
if ($bits === 0) {
return $this;
}
$absBits = abs($bits);
if ($absBits >= 32) {
return new self('0.0.0.0');
}
$pad = str_repeat('0', $absBits);
$paddedBits = $this->getBits();
if ($bits > 0) {
$paddedBits = $pad . substr($paddedBits, 0, -$bits);
} else {
$paddedBits = substr($paddedBits, $absBits) . $pad;
}
$bytes = array_map('bindec', str_split($paddedBits, 8));
return new static(implode('.', $bytes));
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::add()
*/
public function add(AddressInterface $other)
{
if (!$other instanceof self) {
return null;
}
$myBytes = $this->getBytes();
$otherBytes = $other->getBytes();
$sum = array_fill(0, 4, 0);
$carry = 0;
for ($index = 3; $index >= 0; $index--) {
$byte = $myBytes[$index] + $otherBytes[$index] + $carry;
if ($byte > 0xFF) {
$carry = $byte >> 8;
$byte &= 0xFF;
} else {
$carry = 0;
}
$sum[$index] = $byte;
}
if ($carry !== 0) {
return null;
}
return new static(implode('.', $sum));
}
}
@@ -0,0 +1,679 @@
<?php
namespace IPLib\Address;
use IPLib\ParseStringFlag;
use IPLib\Range\RangeInterface;
use IPLib\Range\Subnet;
use IPLib\Range\Type as RangeType;
use IPLib\Service\BinaryMath;
use IPLib\Service\NumberInChunks;
/**
* An IPv6 address.
*
* @phpstan-consistent-constructor
*/
class IPv6 implements AddressInterface
{
/**
* The long string representation of the address.
*
* @var string
*
* @example '0000:0000:0000:0000:0000:0000:0000:0001'
*/
protected $longAddress;
/**
* The long string representation of the address.
*
* @var string|null
*
* @example '::1'
*/
protected $shortAddress;
/**
* The byte list of the IP address.
*
* @var int[]|null
*/
protected $bytes;
/**
* The word list of the IP address.
*
* @var int[]|null
*/
protected $words;
/**
* The type of the range of this IP address.
*
* @var int|null
*/
protected $rangeType;
/**
* An array containing RFC designated address ranges.
*
* @var \IPLib\Address\AssignedRange[]|null
*/
private static $reservedRanges;
/**
* Initializes the instance.
*
* @param string $longAddress
*/
public function __construct($longAddress)
{
$this->longAddress = $longAddress;
$this->shortAddress = null;
$this->bytes = null;
$this->words = null;
$this->rangeType = null;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::__toString()
*/
public function __toString()
{
return $this->toString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getNumberOfBits()
*/
public static function getNumberOfBits()
{
return 128;
}
/**
* @deprecated since 1.17.0: use the parseString() method instead.
* For upgrading:
* - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
* - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag
*
* @param string|mixed $address
* @param bool $mayIncludePort
* @param bool $mayIncludeZoneID
*
* @return static|null
*
* @see \IPLib\Address\IPv6::parseString()
* @since 1.1.0 added the $mayIncludePort argument
* @since 1.3.0 added the $mayIncludeZoneID argument
*/
public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
{
return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0));
}
/**
* Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
*
* @param string|mixed $address the address to parse
* @param int $flags A combination or zero or more flags
*
* @return static|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseString($address, $flags = 0)
{
if (!is_string($address)) {
return null;
}
$matches = null;
$flags = (int) $flags;
if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) {
if (preg_match('/^([0-9a-f](?:\.[0-9a-f]){31})\.ip6\.arpa\.?/i', $address, $matches)) {
$nibbles = array_reverse(explode('.', $matches[1]));
$quibbles = array();
foreach (array_chunk($nibbles, 4) as $n) {
$quibbles[] = implode('', $n);
}
$address = implode(':', $quibbles);
}
}
$result = null;
if (strpos($address, ':') !== false && strpos($address, ':::') === false) {
if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) {
$address = $matches[1];
}
if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) {
$percentagePos = strpos($address, '%');
if ($percentagePos > 0) {
$address = substr($address, 0, $percentagePos);
}
}
if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
$address6 = static::parseString($matches[1] . '0:0');
if ($address6 !== null) {
$address4 = IPv4::parseString($matches[2]);
if ($address4 !== null) {
$bytes4 = $address4->getBytes();
$address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
$result = $address6;
}
}
} else {
if (strpos($address, '::') === false) {
$chunks = explode(':', $address);
} else {
$chunks = array();
$parts = explode('::', $address);
if (count($parts) === 2) {
$before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
$after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
$missing = 8 - count($before) - count($after);
if ($missing >= 0) {
$chunks = $before;
if ($missing !== 0) {
$chunks = array_merge($chunks, array_fill(0, $missing, '0'));
}
$chunks = array_merge($chunks, $after);
}
}
}
if (count($chunks) === 8) {
$nums = array_map(
function ($chunk) {
return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
},
$chunks
);
if (!in_array(false, $nums, true)) {
$longAddress = implode(
':',
array_map(
function ($num) {
return sprintf('%04x', $num);
},
$nums
)
);
$result = new static($longAddress);
}
}
}
}
return $result;
}
/**
* Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
*
* @param array<int|mixed> $bytes
*
* @return static|null
*/
public static function fromBytes(array $bytes)
{
$result = null;
if (count($bytes) === 16) {
$address = '';
for ($i = 0; $i < 16; $i++) {
if ($i !== 0 && $i % 2 === 0) {
$address .= ':';
}
$byte = $bytes[$i];
if (is_int($byte) && $byte >= 0 && $byte <= 255) {
$address .= sprintf('%02x', $byte);
} else {
$address = null;
break;
}
}
if ($address !== null) {
$result = new static($address);
}
}
return $result;
}
/**
* Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
*
* @param array<int|mixed> $words
*
* @return static|null
*/
public static function fromWords(array $words)
{
$result = null;
if (count($words) === 8) {
$chunks = array();
for ($i = 0; $i < 8; $i++) {
$word = $words[$i];
if (is_int($word) && $word >= 0 && $word <= 0xffff) {
$chunks[] = sprintf('%04x', $word);
} else {
$chunks = null;
break;
}
}
if ($chunks !== null) {
$result = new static(implode(':', $chunks));
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::toString()
*/
public function toString($long = false)
{
if ($long) {
$result = $this->longAddress;
} else {
if ($this->shortAddress === null) {
if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
$lastBytes = array_slice($this->getBytes(), -4);
$this->shortAddress = '::ffff:' . implode('.', $lastBytes);
} else {
$chunks = array_map(
function ($word) {
return dechex($word);
},
$this->getWords()
);
$shortAddress = implode(':', $chunks);
$matches = null;
for ($i = 8; $i > 1; $i--) {
$search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
$shortAddress = $matches[1] . '::' . $matches[2];
break;
}
}
$this->shortAddress = $shortAddress;
}
}
$result = $this->shortAddress;
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getBytes()
*/
public function getBytes()
{
if ($this->bytes === null) {
$bytes = array();
foreach ($this->getWords() as $word) {
$bytes[] = $word >> 8;
$bytes[] = $word & 0xff;
}
$this->bytes = $bytes;
}
return $this->bytes;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getBits()
*/
public function getBits()
{
$parts = array();
foreach ($this->getBytes() as $byte) {
$parts[] = sprintf('%08b', $byte);
}
return implode('', $parts);
}
/**
* Get the word list of the IP address.
*
* @return int[]
*/
public function getWords()
{
if ($this->words === null) {
$this->words = array_map(
function ($chunk) {
return (int) hexdec($chunk);
},
explode(':', $this->longAddress)
);
}
return $this->words;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getAddressType()
*/
public function getAddressType()
{
return Type::T_IPv6;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
*/
public static function getDefaultReservedRangeType()
{
return RangeType::T_RESERVED;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getReservedRanges()
*/
public static function getReservedRanges()
{
if (self::$reservedRanges === null) {
$reservedRanges = array();
foreach (array(
// RFC 4291
'::/128' => array(RangeType::T_UNSPECIFIED),
// RFC 4291
'::1/128' => array(RangeType::T_LOOPBACK),
// RFC 4291
'100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
//'2002::/16' => array(RangeType::),
// RFC 4291
'2000::/3' => array(RangeType::T_PUBLIC),
// RFC 4193
'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
// RFC 4291
'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
// RFC 4291
'ff00::/8' => array(RangeType::T_MULTICAST),
// RFC 4291
//'::/8' => array(RangeType::T_RESERVED),
// RFC 4048
//'200::/7' => array(RangeType::T_RESERVED),
// RFC 4291
//'400::/6' => array(RangeType::T_RESERVED),
// RFC 4291
//'800::/5' => array(RangeType::T_RESERVED),
// RFC 4291
//'1000::/4' => array(RangeType::T_RESERVED),
// RFC 4291
//'4000::/3' => array(RangeType::T_RESERVED),
// RFC 4291
//'6000::/3' => array(RangeType::T_RESERVED),
// RFC 4291
//'8000::/3' => array(RangeType::T_RESERVED),
// RFC 4291
//'a000::/3' => array(RangeType::T_RESERVED),
// RFC 4291
//'c000::/3' => array(RangeType::T_RESERVED),
// RFC 4291
//'e000::/4' => array(RangeType::T_RESERVED),
// RFC 4291
//'f000::/5' => array(RangeType::T_RESERVED),
// RFC 4291
//'f800::/6' => array(RangeType::T_RESERVED),
// RFC 4291
//'fe00::/9' => array(RangeType::T_RESERVED),
// RFC 3879
//'fec0::/10' => array(RangeType::T_RESERVED),
) as $range => $data) {
$exceptions = array();
if (isset($data[1])) {
foreach ($data[1] as $exceptionRange => $exceptionType) {
$subnet = Subnet::parseString($exceptionRange);
/** @var Subnet $subnet */
$exceptions[] = new AssignedRange($subnet, $exceptionType);
}
}
$subnet = Subnet::parseString($range);
/** @var Subnet $subnet */
$reservedRanges[] = new AssignedRange($subnet, $data[0], $exceptions);
}
self::$reservedRanges = $reservedRanges;
}
return self::$reservedRanges;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getRangeType()
*/
public function getRangeType()
{
if ($this->rangeType === null) {
$ipv4 = $this->toIPv4();
if ($ipv4 !== null) {
$this->rangeType = $ipv4->getRangeType();
} else {
$rangeType = null;
foreach (static::getReservedRanges() as $reservedRange) {
$rangeType = $reservedRange->getAddressType($this);
if ($rangeType !== null) {
break;
}
}
$this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
}
}
return $this->rangeType;
}
/**
* Create an IPv4 representation of this address (if possible, otherwise returns null).
*
* @return \IPLib\Address\IPv4|null
*/
public function toIPv4()
{
if (strpos($this->longAddress, '2002:') === 0) {
// 6to4
return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
}
if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
// IPv4-mapped IPv6 addresses
return IPv4::fromBytes(array_slice($this->getBytes(), -4));
}
return null;
}
/**
* Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
*
* @param bool $ipV6Long render the IPv6 part in "long" format?
* @param bool $ipV4Long render the IPv4 part in "long" format?
*
* @return string
*
* @example '::13.1.68.3'
* @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
* @example '::013.001.068.003' when $ipV4Long is true
* @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
*
* @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
* @since 1.9.0
*/
public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
{
$myBytes = $this->getBytes();
$ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
$ipv6 = static::fromBytes($ipv6Bytes);
/** @var IPv6 $ipv6 */
$ipv6String = $ipv6->toString($ipV6Long);
$ipv4Bytes = array_slice($myBytes, 12, 4);
$ipv4 = IPv4::fromBytes($ipv4Bytes);
/** @var IPv4 $ipv4 */
$ipv4String = $ipv4->toString($ipV4Long);
$result = preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
/** @var string $result */
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getComparableString()
*/
public function getComparableString()
{
return $this->longAddress;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::matches()
*/
public function matches(RangeInterface $range)
{
return $range->contains($this);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getAddressAtOffset()
*/
public function getAddressAtOffset($n)
{
if (is_int($n)) {
$thatChunks = NumberInChunks::fromInteger($n, NumberInChunks::CHUNKSIZE_WORDS);
} elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
$thatChunks = NumberInChunks::fromNumericString($s, NumberInChunks::CHUNKSIZE_WORDS);
} else {
return null;
}
$myWords = $this->getWords();
while (isset($myWords[1]) && $myWords[0] === 0) {
array_shift($myWords);
}
$myChunks = new NumberInChunks(false, $myWords, NumberInChunks::CHUNKSIZE_WORDS);
$result = $myChunks->add($thatChunks);
if ($result->negative || count($result->chunks) > 8) {
return null;
}
return static::fromWords(array_pad($result->chunks, -8, 0));
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getNextAddress()
*/
public function getNextAddress()
{
return $this->getAddressAtOffset(1);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getPreviousAddress()
*/
public function getPreviousAddress()
{
return $this->getAddressAtOffset(-1);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
*/
public function getReverseDNSLookupName()
{
return implode(
'.',
array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
) . '.ip6.arpa';
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::shift()
*/
public function shift($bits)
{
$bits = (int) $bits;
if ($bits === 0) {
return $this;
}
$absBits = abs($bits);
if ($absBits >= 128) {
return new self('0000:0000:0000:0000:0000:0000:0000:0000');
}
$pad = str_repeat('0', $absBits);
$paddedBits = $this->getBits();
if ($bits > 0) {
$paddedBits = $pad . substr($paddedBits, 0, -$bits);
} else {
$paddedBits = substr($paddedBits, $absBits) . $pad;
}
$bytes = array_map('bindec', str_split($paddedBits, 16));
/** @var int[] $bytes */
$result = static::fromWords($bytes);
/** @var IPv6 $result */
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::add()
*/
public function add(AddressInterface $other)
{
if (!$other instanceof self) {
return null;
}
$myWords = $this->getWords();
$otherWords = $other->getWords();
$sum = array_fill(0, 8, 0);
$carry = 0;
for ($index = 7; $index >= 0; $index--) {
$word = $myWords[$index] + $otherWords[$index] + $carry;
if ($word > 0xFFFF) {
$carry = $word >> 16;
$word &= 0xFFFF;
} else {
$carry = 0;
}
$sum[$index] = $word;
}
if ($carry !== 0) {
return null;
}
return static::fromWords($sum);
}
}
@@ -0,0 +1,44 @@
<?php
namespace IPLib\Address;
/**
* Types of IP addresses.
*/
class Type
{
/**
* IPv4 address.
*
* @var int
*/
const T_IPv4 = 4;
/**
* IPv6 address.
*
* @var int
*/
const T_IPv6 = 6;
/**
* Get the name of a type.
*
* @param int|mixed $type
*
* @return string
*
* @since 1.1.0
*/
public static function getName($type)
{
switch ($type) {
case static::T_IPv4:
return 'IP v4';
case static::T_IPv6:
return 'IP v6';
default:
return $type === null ? 'Unknown type' : sprintf('Unknown type (%s)', print_r($type, true));
}
}
}
@@ -0,0 +1,342 @@
<?php
namespace IPLib;
use IPLib\Address\AddressInterface;
use IPLib\Range\Subnet;
use IPLib\Service\RangesFromBoundaryCalculator;
/**
* Factory methods to build class instances.
*/
class Factory
{
/**
* @deprecated since 1.17.0: use the parseAddressString() method instead.
* For upgrading:
* - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
* - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $address
* @param bool $mayIncludePort
* @param bool $mayIncludeZoneID
* @param bool $supportNonDecimalIPv4
*
* @return \IPLib\Address\AddressInterface|null
*
* @see \IPLib\Factory::parseAddressString()
* @since 1.1.0 added the $mayIncludePort argument
* @since 1.3.0 added the $mayIncludeZoneID argument
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function addressFromString($address, $mayIncludePort = true, $mayIncludeZoneID = true, $supportNonDecimalIPv4 = false)
{
return static::parseAddressString($address, 0 + ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) + ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0) + ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Parse an IP address string.
*
* @param string|mixed $address the address to parse
* @param int $flags A combination or zero or more flags
*
* @return \IPLib\Address\AddressInterface|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseAddressString($address, $flags = 0)
{
if (($result = Address\IPv4::parseString($address, $flags)) !== null) {
return $result;
}
if (($result = Address\IPv6::parseString($address, $flags)) !== null) {
return $result;
}
return null;
}
/**
* Convert a byte array to an address instance.
*
* @param array<int|mixed> $bytes
*
* @return \IPLib\Address\AddressInterface|null
*/
public static function addressFromBytes(array $bytes)
{
if (($result = Address\IPv4::fromBytes($bytes)) !== null) {
return $result;
}
if (($result = Address\IPv6::fromBytes($bytes)) !== null) {
return $result;
}
return null;
}
/**
* @deprecated since 1.17.0: use the parseRangeString() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $range
* @param bool $supportNonDecimalIPv4
*
* @return \IPLib\Range\RangeInterface|null
*
* @see \IPLib\Factory::parseRangeString()
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function rangeFromString($range, $supportNonDecimalIPv4 = false)
{
return static::parseRangeString($range, $supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0);
}
/**
* Parse an IP range string.
*
* @param string|mixed $range
* @param int $flags A combination or zero or more flags
*
* @return \IPLib\Range\RangeInterface|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseRangeString($range, $flags = 0)
{
$result = Range\Subnet::parseString($range, $flags);
if ($result === null) {
$result = Range\Pattern::parseString($range, $flags);
}
if ($result === null) {
$result = Range\Single::parseString($range, $flags);
}
return $result;
}
/**
* @deprecated since 1.17.0: use the getRangeFromBoundaries() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|\IPLib\Address\AddressInterface|mixed $from
* @param string|\IPLib\Address\AddressInterface|mixed $to
* @param bool $supportNonDecimalIPv4
*
* @return \IPLib\Range\RangeInterface|null
*
* @see \IPLib\Factory::getRangeFromBoundaries()
* @since 1.2.0
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function rangeFromBoundaries($from, $to, $supportNonDecimalIPv4 = false)
{
return static::getRangeFromBoundaries($from, $to, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Create the smallest address range that comprises two addresses.
*
* @param string|\IPLib\Address\AddressInterface|mixed $from
* @param string|\IPLib\Address\AddressInterface|mixed $to
* @param int $flags A combination or zero or more flags
*
* @return \IPLib\Range\RangeInterface|null return NULL if $from and/or $to are invalid addresses, or if both are NULL or empty strings, or if they are addresses of different types
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function getRangeFromBoundaries($from, $to, $flags = 0)
{
list($from, $to) = self::parseBoundaries($from, $to, $flags);
return $from === false || $to === false ? null : static::rangeFromBoundaryAddresses($from, $to);
}
/**
* Calculate the minimal range that contains all the specified addresses.
*
* @param array<non-empty-string|\IPLib\Address\AddressInterface|mixed> $addresses
* @param int $flags
*
* @return \IPLib\Range\RangeInterface|null Returns NULL if $addresses is empty, if it contains invalid addresses, or if the addresses aren't compatible (for example, both IPv4 and IPv6 addresses)
*
* @since 1.22.0
*/
public static function getRangeFromAddresses(array $addresses, $flags = 0)
{
$min = null;
$max = null;
$numberOfBits = null;
foreach ($addresses as $address) {
if (!$address instanceof AddressInterface) {
$address = Factory::parseAddressString($address, $flags);
if ($address === null) {
return null;
}
}
if ($numberOfBits === null) {
$min = $address;
$max = $address;
$numberOfBits = $address->getNumberOfBits();
} elseif ($numberOfBits !== $address->getNumberOfBits()) {
return null;
} else {
/** @var AddressInterface $min */
/** @var AddressInterface $max */
$comparable = $address->getComparableString();
if ($min->getComparableString() > $comparable) {
$min = $address;
}
if ($max->getComparableString() < $comparable) {
$max = $address;
}
}
}
if ($numberOfBits === null) {
return null;
}
return static::rangeFromBoundaryAddresses($min, $max);
}
/**
* @deprecated since 1.17.0: use the getRangesFromBoundaries() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|\IPLib\Address\AddressInterface|mixed $from
* @param string|\IPLib\Address\AddressInterface|mixed $to
* @param bool $supportNonDecimalIPv4
*
* @return \IPLib\Range\Subnet[]|null
*
* @see \IPLib\Factory::getRangesFromBoundaries()
* @since 1.14.0
*/
public static function rangesFromBoundaries($from, $to, $supportNonDecimalIPv4 = false)
{
return static::getRangesFromBoundaries($from, $to, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Create a list of Range instances that exactly describes all the addresses between the two provided addresses.
*
* @param string|\IPLib\Address\AddressInterface|mixed $from
* @param string|\IPLib\Address\AddressInterface|mixed $to
* @param int $flags A combination or zero or more flags
*
* @return \IPLib\Range\Subnet[]|null return NULL if $from and/or $to are invalid addresses, or if both are NULL or empty strings, or if they are addresses of different types
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function getRangesFromBoundaries($from, $to, $flags = 0)
{
list($from, $to) = self::parseBoundaries($from, $to, $flags);
if ($from === false || $to === false || ($from === null && $to === null)) {
return null;
}
if ($from === null || $to === null) {
$address = $from ? $from : $to;
/** @var AddressInterface $address */
return array(new Subnet($address, $address, $address->getNumberOfBits()));
}
$numberOfBits = $from->getNumberOfBits();
if ($to->getNumberOfBits() !== $numberOfBits) {
return null;
}
$calculator = new RangesFromBoundaryCalculator($numberOfBits);
return $calculator->getRanges($from, $to);
}
/**
* @param \IPLib\Address\AddressInterface|null $from
* @param \IPLib\Address\AddressInterface|null $to
*
* @return \IPLib\Range\RangeInterface|null
*
* @since 1.2.0
*/
protected static function rangeFromBoundaryAddresses($from = null, $to = null)
{
if (!$from instanceof AddressInterface && !$to instanceof AddressInterface) {
$result = null;
} elseif (!$to instanceof AddressInterface) {
$result = Range\Single::fromAddress($from);
} elseif (!$from instanceof AddressInterface) {
$result = Range\Single::fromAddress($to);
} else {
$result = null;
$addressType = $from->getAddressType();
if ($addressType === $to->getAddressType()) {
$cmp = strcmp($from->getComparableString(), $to->getComparableString());
if ($cmp === 0) {
$result = Range\Single::fromAddress($from);
} else {
if ($cmp > 0) {
list($from, $to) = array($to, $from);
}
$fromBytes = $from->getBytes();
$toBytes = $to->getBytes();
$numBytes = count($fromBytes);
$sameBits = 0;
for ($byteIndex = 0; $byteIndex < $numBytes; $byteIndex++) {
$fromByte = $fromBytes[$byteIndex];
$toByte = $toBytes[$byteIndex];
if ($fromByte === $toByte) {
$sameBits += 8;
} else {
$differentBitsInByte = decbin($fromByte ^ $toByte);
$sameBits += 8 - strlen($differentBitsInByte);
break;
}
}
$result = static::parseRangeString($from->toString() . '/' . (string) $sameBits);
}
}
}
return $result;
}
/**
* @param string|\IPLib\Address\AddressInterface|mixed $from
* @param string|\IPLib\Address\AddressInterface|mixed $to
* @param int $flags
*
* @return array{\IPLib\Address\AddressInterface|false|null, \IPLib\Address\AddressInterface|false|null}
*/
private static function parseBoundaries($from, $to, $flags = 0)
{
$result = array();
foreach (array('from', 'to') as $param) {
$value = $$param;
if (!($value instanceof AddressInterface)) {
$value = is_object($value) && method_exists($value, '__toString') || is_scalar($value) ? (string) $value : '';
if ($value === '') {
$value = null;
} else {
$value = static::parseAddressString($value, $flags);
if ($value === null) {
$value = false;
}
}
}
$result[] = $value;
}
/** @var array{\IPLib\Address\AddressInterface|false|null, \IPLib\Address\AddressInterface|false|null} $result */
if ($result[0] && $result[1] && strcmp($result[0]->getComparableString(), $result[1]->getComparableString()) > 0) {
$result = array($result[1], $result[0]);
}
return $result;
}
}
@@ -0,0 +1,79 @@
<?php
namespace IPLib;
/**
* Flags for the parseString() methods.
*
* @since 1.17.0
*/
class ParseStringFlag
{
/**
* Use this flag if the input string may include the port.
*
* @var int
*/
const MAY_INCLUDE_PORT = 1;
/**
* Use this flag if the input string may include a zone ID.
*
* @var int
*/
const MAY_INCLUDE_ZONEID = 2;
/**
* Use this flag if IPv4 addresses may be in decimal/octal/hexadecimal format.
* This notation is accepted by the implementation of inet_aton and inet_addr of the libc implementation of GNU, Windows and Mac (but not Musl), but not by inet_pton and ip2long.
*
* @var int
*
* @example 1.08.0x10.0 => 5.0.0.1
* @example 5.256 => 5.0.1.0
* @example 5.0.256 => 5.0.1.0
* @example 123456789 => 7.91.205.21
*/
const IPV4_MAYBE_NON_DECIMAL = 4;
/**
* Use this flag if IPv4 subnet ranges may be in compact form.
*
* @example 127/24 => 127.0.0.0/24
* @example 10/8 => 10.0.0.0/8
* @example 10/24 => 10.0.0.0/24
* @example 10.10.10/24 => 10.10.10.0/24
*
* @var int
*/
const IPV4SUBNET_MAYBE_COMPACT = 8;
/**
* Use this flag if IPv4 addresses may be in non quad-dotted notation.
* This notation is accepted by the implementation of inet_aton and inet_addr of the libc implementation of GNU, Windows and Mac (but not Musl), but not by inet_pton and ip2long.
*
* @var int
*
* @example 5.1 => 5.0.0.1
* @example 5.256 => 5.0.1.0
* @example 5.0.256 => 5.0.1.0
* @example 123456789 => 7.91.205.21
*
* @see https://man7.org/linux/man-pages/man3/inet_addr.3.html#DESCRIPTION
* @see https://www.freebsd.org/cgi/man.cgi?query=inet_net&sektion=3&apropos=0&manpath=FreeBSD+12.2-RELEASE+and+Ports#end
* @see http://git.musl-libc.org/cgit/musl/tree/src/network/inet_aton.c?h=v1.2.2
*/
const IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED = 16;
/**
* Use this flag if you want to accept parsing IPv4/IPv6 addresses in Reverse DNS Lookup Address format.
*
* @var int
*
* @since 1.18.0
*
* @example 140.13.12.10.in-addr.arpa => 10.12.13.140
* @example b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa => 4321:0:1:2:3:4:567:89ab
*/
const ADDRESS_MAYBE_RDNS = 32;
}
@@ -0,0 +1,188 @@
<?php
namespace IPLib\Range;
use IPLib\Address\AddressInterface;
use IPLib\Address\IPv4;
use IPLib\Address\IPv6;
use IPLib\Address\Type as AddressType;
use IPLib\Factory;
use IPLib\Service\BinaryMath;
use OutOfBoundsException;
/**
* Base class for range classes.
*/
abstract class AbstractRange implements RangeInterface
{
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getRangeType()
*/
public function getRangeType()
{
/** @var \IPLib\Range\Pattern|\IPLib\Range\Subnet $this */
// @phpstan-ignore varTag.nativeType
if ($this->rangeType === null) {
$addressType = $this->getAddressType();
if ($addressType === AddressType::T_IPv6 && Subnet::get6to4()->containsRange($this)) {
$fromAddress = $this->fromAddress;
/** @var IPv6 $fromAddress */
$toAddress = $this->toAddress;
/** @var IPv6 $toAddress */
$range = Factory::getRangeFromBoundaries($fromAddress->toIPv4(), $toAddress->toIPv4());
/** @var RangeInterface $range */
$this->rangeType = $range->getRangeType();
} else {
switch ($addressType) {
case AddressType::T_IPv4:
$defaultType = IPv4::getDefaultReservedRangeType();
$reservedRanges = IPv4::getReservedRanges();
break;
case AddressType::T_IPv6:
$defaultType = IPv6::getDefaultReservedRangeType();
$reservedRanges = IPv6::getReservedRanges();
break;
}
$rangeType = null;
foreach ($reservedRanges as $reservedRange) {
$rangeType = $reservedRange->getRangeType($this);
if ($rangeType !== null) {
break;
}
}
$this->rangeType = $rangeType === null ? $defaultType : $rangeType;
}
}
return $this->rangeType === false ? null : $this->rangeType;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getAddressAtOffset()
*/
public function getAddressAtOffset($n)
{
if (is_int($n)) {
$positive = $n >= 0;
} elseif (($s = BinaryMath::getInstance()->normalizeIntegerString($n)) !== '') {
$n = $s;
$positive = $n[0] !== '-';
} else {
return null;
}
if ($positive) {
$start = Factory::parseAddressString($this->getComparableStartString());
/** @var \IPLib\Address\AddressInterface $start */
$address = $start->getAddressAtOffset($n);
} else {
$end = Factory::parseAddressString($this->getComparableEndString());
/** @var \IPLib\Address\AddressInterface $end */
$nPlus1 = is_int($n) ? $n + 1 : BinaryMath::getInstance()->add1ToIntegerString($n);
$address = $end->getAddressAtOffset($nPlus1);
}
if ($address === null) {
return null;
}
return $this->contains($address) ? $address : null;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::contains()
*/
public function contains(AddressInterface $address)
{
$result = false;
if ($address->getAddressType() === $this->getAddressType()) {
$cmp = $address->getComparableString();
$from = $this->getComparableStartString();
if ($cmp >= $from) {
$to = $this->getComparableEndString();
if ($cmp <= $to) {
$result = true;
}
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::containsRange()
*/
public function containsRange(RangeInterface $range)
{
$result = false;
if ($range->getAddressType() === $this->getAddressType()) {
$myStart = $this->getComparableStartString();
$itsStart = $range->getComparableStartString();
if ($itsStart >= $myStart) {
$myEnd = $this->getComparableEndString();
$itsEnd = $range->getComparableEndString();
if ($itsEnd <= $myEnd) {
$result = true;
}
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::split()
*/
public function split($networkPrefix, $forceSubnet = false)
{
$networkPrefix = (int) $networkPrefix;
$myNetworkPrefix = $this->getNetworkPrefix();
if ($networkPrefix === $myNetworkPrefix) {
return array(
$forceSubnet ? $this->asSubnet() : $this,
);
}
if ($networkPrefix < $myNetworkPrefix) {
throw new OutOfBoundsException("The value of the \$networkPrefix parameter can't be smaller than the network prefix of the range ({$myNetworkPrefix})");
}
$startIp = $this->getStartAddress();
$maxPrefix = $startIp::getNumberOfBits();
if ($networkPrefix > $maxPrefix) {
throw new OutOfBoundsException("The value of the \$networkPrefix parameter can't be larger than the maximum network prefix of the range ({$maxPrefix})");
}
switch ($startIp->getAddressType()) {
case AddressType::T_IPv4:
$one = IPv4::fromBytes(array(0, 0, 0, 1));
break;
case AddressType::T_IPv6:
$one = IPv6::fromBytes(array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1));
break;
}
/** @var \IPLib\Address\AddressInterface $one */
$delta = $one->shift($networkPrefix - $maxPrefix);
$result = array();
while (true) {
$range = Subnet::parseString("{$startIp}/{$networkPrefix}");
/** @var Subnet $range */
if (!$forceSubnet && $this instanceof Pattern) {
$range = $range->asPattern() ?: $range;
}
$result[] = $range;
$startIp = $startIp->add($delta);
if ($startIp === null || !$this->contains($startIp)) {
break;
}
}
return $result;
}
}
@@ -0,0 +1,354 @@
<?php
namespace IPLib\Range;
use IPLib\Address\AddressInterface;
use IPLib\Address\IPv4;
use IPLib\Address\IPv6;
use IPLib\Address\Type as AddressType;
use IPLib\ParseStringFlag;
use IPLib\Service\BinaryMath;
/**
* Represents an address range in pattern format (only ending asterisks are supported).
*
* @example 127.0.*.*
* @example ::/8
*
* @phpstan-consistent-constructor
*/
class Pattern extends AbstractRange
{
/**
* Starting address of the range.
*
* @var \IPLib\Address\AddressInterface
*/
protected $fromAddress;
/**
* Final address of the range.
*
* @var \IPLib\Address\AddressInterface
*/
protected $toAddress;
/**
* Number of ending asterisks.
*
* @var int
*/
protected $asterisksCount;
/**
* The type of the range of this IP range.
*
* @var int|false|null false if this range crosses multiple range types, null if yet to be determined
*
* @since 1.5.0
*/
protected $rangeType;
/**
* Initializes the instance.
*
* @param \IPLib\Address\AddressInterface $fromAddress
* @param \IPLib\Address\AddressInterface $toAddress
* @param int $asterisksCount
*/
public function __construct(AddressInterface $fromAddress, AddressInterface $toAddress, $asterisksCount)
{
$this->fromAddress = $fromAddress;
$this->toAddress = $toAddress;
$this->asterisksCount = $asterisksCount;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::__toString()
*/
public function __toString()
{
return $this->toString();
}
/**
* @deprecated since 1.17.0: use the parseString() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $range
* @param bool $supportNonDecimalIPv4
*
* @return static|null
*
* @see \IPLib\Range\Pattern::parseString()
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function fromString($range, $supportNonDecimalIPv4 = false)
{
return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Try get the range instance starting from its string representation.
*
* @param string|mixed $range
* @param int $flags A combination or zero or more flags
*
* @return static|null
*
* @since 1.17.0
* @see \IPLib\ParseStringFlag
*/
public static function parseString($range, $flags = 0)
{
if (!is_string($range) || strpos($range, '*') === false) {
return null;
}
if ($range === '*.*.*.*') {
$fromAddress = IPv4::parseString('0.0.0.0');
/** @var \IPLib\Address\IPv4 $fromAddress */
$toAddress = IPv4::parseString('255.255.255.255');
/** @var \IPLib\Address\IPv4 $toAddress */
return new static($fromAddress, $toAddress, 4);
}
if ($range === '*:*:*:*:*:*:*:*') {
$fromAddress = IPv6::parseString('::');
/** @var \IPLib\Address\IPv6 $fromAddress */
$toAddress = IPv6::parseString('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff');
/** @var \IPLib\Address\IPv6 $toAddress */
return new static($fromAddress, $toAddress, 8);
}
$matches = null;
if (strpos($range, '.') !== false && preg_match('/^[^*]+((?:\.\*)+)$/', $range, $matches)) {
$asterisksCount = strlen($matches[1]) >> 1;
if ($asterisksCount > 0) {
$missingDots = 3 - substr_count($range, '.');
if ($missingDots > 0) {
$range .= str_repeat('.*', $missingDots);
$asterisksCount += $missingDots;
}
}
$fromAddress = IPv4::parseString(str_replace('*', '0', $range), $flags);
if ($fromAddress === null) {
return null;
}
$fixedBytes = array_slice($fromAddress->getBytes(), 0, -$asterisksCount);
$otherBytes = array_fill(0, $asterisksCount, 255);
$toAddress = IPv4::fromBytes(array_merge($fixedBytes, $otherBytes));
/** @var \IPLib\Address\IPv4 $toAddress */
return new static($fromAddress, $toAddress, $asterisksCount);
}
if (strpos($range, ':') !== false && preg_match('/^[^*]+((?::\*)+)$/', $range, $matches)) {
$asterisksCount = strlen($matches[1]) >> 1;
$fromAddress = IPv6::parseString(str_replace('*', '0', $range));
if ($fromAddress === null) {
return null;
}
$fixedWords = array_slice($fromAddress->getWords(), 0, -$asterisksCount);
$otherWords = array_fill(0, $asterisksCount, 0xffff);
$toAddress = IPv6::fromWords(array_merge($fixedWords, $otherWords));
/** @var \IPLib\Address\IPv6 $toAddress */
return new static($fromAddress, $toAddress, $asterisksCount);
}
return null;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::toString()
*/
public function toString($long = false)
{
if ($this->asterisksCount === 0) {
return $this->fromAddress->toString($long);
}
switch (true) {
case $this->fromAddress instanceof \IPLib\Address\IPv4:
$chunks = explode('.', $this->fromAddress->toString());
$chunks = array_slice($chunks, 0, -$this->asterisksCount);
$chunks = array_pad($chunks, 4, '*');
$result = implode('.', $chunks);
break;
case $this->fromAddress instanceof \IPLib\Address\IPv6:
if ($long) {
$chunks = explode(':', $this->fromAddress->toString(true));
$chunks = array_slice($chunks, 0, -$this->asterisksCount);
$chunks = array_pad($chunks, 8, '*');
$result = implode(':', $chunks);
} elseif ($this->asterisksCount === 8) {
$result = '*:*:*:*:*:*:*:*';
} else {
$bytes = $this->toAddress->getBytes();
$bytes = array_slice($bytes, 0, -$this->asterisksCount * 2);
$bytes = array_pad($bytes, 16, 1);
$address = IPv6::fromBytes($bytes);
/** @var IPv6 $address */
$before = substr($address->toString(false), 0, -strlen(':101') * $this->asterisksCount);
$result = $before . str_repeat(':*', $this->asterisksCount);
}
break;
default:
throw new \Exception('@todo'); // @codeCoverageIgnore
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getAddressType()
*/
public function getAddressType()
{
return $this->fromAddress->getAddressType();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getStartAddress()
*/
public function getStartAddress()
{
return $this->fromAddress;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getEndAddress()
*/
public function getEndAddress()
{
return $this->toAddress;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableStartString()
*/
public function getComparableStartString()
{
return $this->fromAddress->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableEndString()
*/
public function getComparableEndString()
{
return $this->toAddress->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asSubnet()
* @since 1.8.0
*/
public function asSubnet()
{
return new Subnet($this->getStartAddress(), $this->getEndAddress(), $this->getNetworkPrefix());
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asPattern()
*/
public function asPattern()
{
return $this;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSubnetMask()
*/
public function getSubnetMask()
{
if ($this->getAddressType() !== AddressType::T_IPv4) {
return null;
}
switch ($this->asterisksCount) {
case 0:
$bytes = array(255, 255, 255, 255);
break;
case 4:
$bytes = array(0, 0, 0, 0);
break;
default:
$bytes = array_pad(array_fill(0, 4 - $this->asterisksCount, 255), 4, 0);
break;
}
return IPv4::fromBytes($bytes);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getReverseDNSLookupName()
*/
public function getReverseDNSLookupName()
{
return $this->asterisksCount === 0 ? array($this->getStartAddress()->getReverseDNSLookupName()) : $this->asSubnet()->getReverseDNSLookupName();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSize()
*/
public function getSize()
{
$fromAddress = $this->fromAddress;
$maxPrefix = $fromAddress::getNumberOfBits();
$prefix = $this->getNetworkPrefix();
return pow(2, $maxPrefix - $prefix);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getExactSize()
*/
public function getExactSize()
{
$fromAddress = $this->fromAddress;
$maxPrefix = $fromAddress::getNumberOfBits();
$prefix = $this->getNetworkPrefix();
return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getNetworkPrefix()
*/
public function getNetworkPrefix()
{
switch ($this->getAddressType()) {
case AddressType::T_IPv4:
return 8 * (4 - $this->asterisksCount);
case AddressType::T_IPv6:
return 16 * (8 - $this->asterisksCount);
}
}
}
@@ -0,0 +1,198 @@
<?php
namespace IPLib\Range;
use IPLib\Address\AddressInterface;
/**
* Interface of all the range types.
*/
interface RangeInterface
{
/**
* Get the short string representation of this address.
*
* @return string
*/
public function __toString();
/**
* Get the string representation of this address.
*
* @param bool $long set to true to have a long/full representation, false otherwise
*
* @return string
*
* @example If $long is true, you'll get '0000:0000:0000:0000:0000:0000:0000:0001/128', '::1/128' otherwise.
*/
public function toString($long = false);
/**
* Get the type of the IP addresses contained in this range.
*
* @return int One of the \IPLib\Address\Type::T_... constants
*
* @phpstan-return \IPLib\Address\Type::T_IPv4|\IPLib\Address\Type::T_IPv6
*/
public function getAddressType();
/**
* Get the type of range of the IP address.
*
* @return int|null One of the \IPLib\Range\Type::T_... constants, or null if this range crosses multiple range types
*
* @since 1.5.0
*/
public function getRangeType();
/**
* Get the address at a certain offset of this range.
*
* @param int|numeric-string|mixed $n the offset of the address (support negative offset)
*
* @return \IPLib\Address\AddressInterface|null return NULL if $n is neither an integer nor a string containing a valid integer, or if the offset out of range
*
* @since 1.15.0
* @since 1.21.0 $n can also be a numeric string
*
* @example passing 256 to the range 127.0.0.0/16 will result in 127.0.1.0
* @example passing -1 to the range 127.0.1.0/16 will result in 127.0.255.255
* @example passing 256 to the range 127.0.0.0/24 will result in NULL
*/
public function getAddressAtOffset($n);
/**
* Check if this range contains an IP address.
*
* @param \IPLib\Address\AddressInterface $address
*
* @return bool
*/
public function contains(AddressInterface $address);
/**
* Check if this range contains another range.
*
* @param \IPLib\Range\RangeInterface $range
*
* @return bool
*
* @since 1.5.0
*/
public function containsRange(RangeInterface $range);
/**
* Get the initial address contained in this range.
*
* @return \IPLib\Address\AddressInterface
*
* @since 1.4.0
*/
public function getStartAddress();
/**
* Get the final address contained in this range.
*
* @return \IPLib\Address\AddressInterface
*
* @since 1.4.0
*/
public function getEndAddress();
/**
* Get a string representation of the starting address of this range than can be used when comparing addresses and ranges.
*
* @return string
*/
public function getComparableStartString();
/**
* Get a string representation of the final address of this range than can be used when comparing addresses and ranges.
*
* @return string
*/
public function getComparableEndString();
/**
* Get the subnet mask representing this range (only for IPv4 ranges).
*
* @return \IPLib\Address\IPv4|null return NULL if the range is an IPv6 range, the subnet mask otherwise
*
* @since 1.8.0
*/
public function getSubnetMask();
/**
* Get the subnet/CIDR representation of this range.
*
* @return \IPLib\Range\Subnet
*
* @since 1.13.0
*/
public function asSubnet();
/**
* Get the pattern/asterisk representation (if applicable) of this range.
*
* @return \IPLib\Range\Pattern|null return NULL if this range can't be represented by a pattern notation
*
* @since 1.13.0
*/
public function asPattern();
/**
* Get the Reverse DNS Lookup Addresses of this IP range.
*
* @return string[]
*
* @since 1.13.0
*
* @example for IPv4 it returns something like array('x.x.x.x.in-addr.arpa', 'x.x.x.x.in-addr.arpa') (where the number of 'x.' ranges from 1 to 4)
* @example for IPv6 it returns something like array('x.x.x.x..x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa', 'x.x.x.x..x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa') (where the number of 'x.' ranges from 1 to 32)
*/
public function getReverseDNSLookupName();
/**
* Get the count of addresses contained in this IP range (possibly approximated).
*
* @return int|float If the number of addresses exceeds PHP_INT_MAX a float containing an approximation will be returned
*
* @since 1.16.0
*/
public function getSize();
/**
* Get the exact count of addresses contained in this IP range.
*
* @return int|numeric-string If the number of addresses exceeds PHP_INT_MAX a string containing the exact number of addresses will be returned
*
* @since 1.21.0
*/
public function getExactSize();
/**
* Get the "network prefix", that is how many bits of the address are dedicated to the network portion.
*
* @return int
*
* @since 1.19.0
*
* @example for 10.0.0.0/24 it's 24
* @example for 10.0.0.* it's 24
*/
public function getNetworkPrefix();
/**
* Split the range into smaller ranges.
*
* @param int $networkPrefix
* @param bool $forceSubnet set to true to always have ranges in "subnet format" (ie 1.2.3.4/5), to false to try to keep the original format if possible (that is, pattern to pattern, single to single)
*
* @throws \OutOfBoundsException if $networkPrefix is not valid
*
* @return \IPLib\Range\RangeInterface[]
*
* @since 1.19.0
*/
public function split($networkPrefix, $forceSubnet = false);
}
@@ -0,0 +1,266 @@
<?php
namespace IPLib\Range;
use IPLib\Address\AddressInterface;
use IPLib\Address\IPv4;
use IPLib\Address\Type as AddressType;
use IPLib\Factory;
use IPLib\ParseStringFlag;
/**
* Represents a single address (eg a range that contains just one address).
*
* @example 127.0.0.1
* @example ::1
*
* @phpstan-consistent-constructor
*/
class Single extends AbstractRange
{
/**
* @var \IPLib\Address\AddressInterface
*/
protected $address;
/**
* Initializes the instance.
*
* @param \IPLib\Address\AddressInterface $address
*/
protected function __construct(AddressInterface $address)
{
$this->address = $address;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::__toString()
*/
public function __toString()
{
return $this->address->__toString();
}
/**
* @deprecated since 1.17.0: use the parseString() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $range
* @param bool $supportNonDecimalIPv4
*
* @return static|null
*
* @see \IPLib\Range\Single::parseString()
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function fromString($range, $supportNonDecimalIPv4 = false)
{
return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Try get the range instance starting from its string representation.
*
* @param string|mixed $range
* @param int $flags A combination or zero or more flags
*
* @return static|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseString($range, $flags = 0)
{
$result = null;
$flags = (int) $flags;
$address = Factory::parseAddressString($range, $flags);
if ($address !== null) {
$result = new static($address);
}
return $result;
}
/**
* Create the range instance starting from an address instance.
*
* @param \IPLib\Address\AddressInterface $address
*
* @return static
*
* @since 1.2.0
*/
public static function fromAddress(AddressInterface $address)
{
return new static($address);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::toString()
*/
public function toString($long = false)
{
return $this->address->toString($long);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getAddressType()
*/
public function getAddressType()
{
return $this->address->getAddressType();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getRangeType()
*/
public function getRangeType()
{
return $this->address->getRangeType();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::contains()
*/
public function contains(AddressInterface $address)
{
$result = false;
if ($address->getAddressType() === $this->getAddressType()) {
if ($address->toString(false) === $this->address->toString(false)) {
$result = true;
}
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getStartAddress()
*/
public function getStartAddress()
{
return $this->address;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getEndAddress()
*/
public function getEndAddress()
{
return $this->address;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableStartString()
*/
public function getComparableStartString()
{
return $this->address->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableEndString()
*/
public function getComparableEndString()
{
return $this->address->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asSubnet()
*/
public function asSubnet()
{
return new Subnet($this->address, $this->address, $this->getNetworkPrefix());
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asPattern()
*/
public function asPattern()
{
return new Pattern($this->address, $this->address, 0);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSubnetMask()
*/
public function getSubnetMask()
{
if ($this->getAddressType() !== AddressType::T_IPv4) {
return null;
}
return IPv4::fromBytes(array(255, 255, 255, 255));
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getReverseDNSLookupName()
*/
public function getReverseDNSLookupName()
{
return array($this->getStartAddress()->getReverseDNSLookupName());
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSize()
*/
public function getSize()
{
return 1;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getExactSize()
*/
public function getExactSize()
{
return 1;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getNetworkPrefix()
*/
public function getNetworkPrefix()
{
switch ($this->getAddressType()) {
case AddressType::T_IPv4:
return 32;
case AddressType::T_IPv6:
return 128;
}
}
}
@@ -0,0 +1,373 @@
<?php
namespace IPLib\Range;
use IPLib\Address\AddressInterface;
use IPLib\Address\IPv4;
use IPLib\Address\Type as AddressType;
use IPLib\Factory;
use IPLib\ParseStringFlag;
use IPLib\Service\BinaryMath;
/**
* Represents an address range in subnet format (eg CIDR).
*
* @example 127.0.0.1/32
* @example ::/8
*
* @phpstan-consistent-constructor
*/
class Subnet extends AbstractRange
{
/**
* Starting address of the range.
*
* @var \IPLib\Address\AddressInterface
*/
protected $fromAddress;
/**
* Final address of the range.
*
* @var \IPLib\Address\AddressInterface
*/
protected $toAddress;
/**
* Number of the same bits of the range.
*
* @var int
*/
protected $networkPrefix;
/**
* The type of the range of this IP range.
*
* @var int|null
*
* @since 1.5.0
*/
protected $rangeType;
/**
* The 6to4 address IPv6 address range.
*
* @var self|null
*/
private static $sixToFour;
/**
* Initializes the instance.
*
* @param \IPLib\Address\AddressInterface $fromAddress
* @param \IPLib\Address\AddressInterface $toAddress
* @param int $networkPrefix
*
* @internal
*/
public function __construct(AddressInterface $fromAddress, AddressInterface $toAddress, $networkPrefix)
{
$this->fromAddress = $fromAddress;
$this->toAddress = $toAddress;
$this->networkPrefix = $networkPrefix;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::__toString()
*/
public function __toString()
{
return $this->toString();
}
/**
* @deprecated since 1.17.0: use the parseString() method instead.
* For upgrading:
* - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
*
* @param string|mixed $range
* @param bool $supportNonDecimalIPv4
*
* @return static|null
*
* @see \IPLib\Range\Subnet::parseString()
* @since 1.10.0 added the $supportNonDecimalIPv4 argument
*/
public static function fromString($range, $supportNonDecimalIPv4 = false)
{
return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
}
/**
* Try get the range instance starting from its string representation.
*
* @param string|mixed $range
* @param int $flags A combination or zero or more flags
*
* @return static|null
*
* @see \IPLib\ParseStringFlag
* @since 1.17.0
*/
public static function parseString($range, $flags = 0)
{
if (!is_string($range)) {
return null;
}
$parts = explode('/', $range);
if (count($parts) !== 2) {
return null;
}
$flags = (int) $flags;
if (strpos($parts[0], ':') === false && $flags & ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT) {
$missingDots = 3 - substr_count($parts[0], '.');
if ($missingDots > 0) {
$parts[0] .= str_repeat('.0', $missingDots);
}
}
$address = Factory::parseAddressString($parts[0], $flags);
if ($address === null) {
return null;
}
if (!preg_match('/^[0-9]{1,9}$/', $parts[1])) {
return null;
}
$networkPrefix = (int) $parts[1];
$addressBytes = $address->getBytes();
$totalBytes = count($addressBytes);
$numDifferentBits = $totalBytes * 8 - $networkPrefix;
if ($numDifferentBits < 0) {
return null;
}
$numSameBytes = $networkPrefix >> 3;
$sameBytes = array_slice($addressBytes, 0, $numSameBytes);
$differentBytesStart = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 0);
$differentBytesEnd = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 255);
$startSameBits = $networkPrefix % 8;
if ($startSameBits !== 0) {
$varyingByte = $addressBytes[$numSameBytes];
$differentBytesStart[0] = $varyingByte & (int) bindec(str_pad(str_repeat('1', $startSameBits), 8, '0', STR_PAD_RIGHT));
$differentBytesEnd[0] = $differentBytesStart[0] + (int) bindec(str_repeat('1', 8 - $startSameBits));
}
$fromAddress = Factory::addressFromBytes(array_merge($sameBytes, $differentBytesStart));
/** @var \IPLib\Address\AddressInterface $fromAddress */
$toAddress = Factory::addressFromBytes(array_merge($sameBytes, $differentBytesEnd));
/** @var \IPLib\Address\AddressInterface $toAddress */
return new static($fromAddress, $toAddress, $networkPrefix);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::toString()
*/
public function toString($long = false)
{
return $this->fromAddress->toString($long) . '/' . $this->networkPrefix;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getAddressType()
*/
public function getAddressType()
{
return $this->fromAddress->getAddressType();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getStartAddress()
*/
public function getStartAddress()
{
return $this->fromAddress;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getEndAddress()
*/
public function getEndAddress()
{
return $this->toAddress;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableStartString()
*/
public function getComparableStartString()
{
return $this->fromAddress->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getComparableEndString()
*/
public function getComparableEndString()
{
return $this->toAddress->getComparableString();
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asSubnet()
*/
public function asSubnet()
{
return $this;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::asPattern()
* @since 1.8.0
*/
public function asPattern()
{
$address = $this->getStartAddress();
$networkPrefix = $this->getNetworkPrefix();
switch ($address->getAddressType()) {
case AddressType::T_IPv4:
return $networkPrefix % 8 === 0 ? new Pattern($address, $address, 4 - $networkPrefix / 8) : null;
case AddressType::T_IPv6:
return $networkPrefix % 16 === 0 ? new Pattern($address, $address, 8 - $networkPrefix / 16) : null;
}
}
/**
* Get the 6to4 address IPv6 address range.
*
* @return self
*
* @since 1.5.0
*/
public static function get6to4()
{
if (self::$sixToFour === null) {
$subnet = self::parseString('2002::/16');
/** @var Subnet $subnet */
self::$sixToFour = $subnet;
}
return self::$sixToFour;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getNetworkPrefix()
* @since 1.7.0
*/
public function getNetworkPrefix()
{
return $this->networkPrefix;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSubnetMask()
*/
public function getSubnetMask()
{
if ($this->getAddressType() !== AddressType::T_IPv4) {
return null;
}
$bytes = array();
$prefix = $this->getNetworkPrefix();
while ($prefix >= 8) {
$bytes[] = 255;
$prefix -= 8;
}
if ($prefix !== 0) {
$bytes[] = bindec(str_pad(str_repeat('1', $prefix), 8, '0'));
}
$bytes = array_pad($bytes, 4, 0);
return IPv4::fromBytes($bytes);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getReverseDNSLookupName()
*/
public function getReverseDNSLookupName()
{
switch ($this->getAddressType()) {
case AddressType::T_IPv4:
$unitSize = 8; // bytes
$maxUnits = 4;
$isHex = false;
$rxUnit = '\d+';
break;
case AddressType::T_IPv6:
$unitSize = 4; // nibbles
$maxUnits = 32;
$isHex = true;
$rxUnit = '[0-9A-Fa-f]';
break;
}
$totBits = $unitSize * $maxUnits;
$prefixUnits = (int) ($this->networkPrefix / $unitSize);
$extraBits = ($totBits - $this->networkPrefix) % $unitSize;
if ($extraBits !== 0) {
$prefixUnits += 1;
}
$numVariants = 1 << $extraBits;
$result = array();
$unitsToRemove = $maxUnits - $prefixUnits;
$initialPointer = preg_replace("/^(({$rxUnit})\.){{$unitsToRemove}}/", '', $this->getStartAddress()->getReverseDNSLookupName());
/** @var string $initialPointer */
$chunks = explode('.', $initialPointer, 2);
for ($index = 0; $index < $numVariants; $index++) {
if ($index !== 0) {
$chunks[0] = $isHex ? dechex(1 + (int) hexdec($chunks[0])) : (string) (1 + (int) $chunks[0]);
}
$result[] = implode('.', $chunks);
}
return $result;
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getSize()
*/
public function getSize()
{
$fromAddress = $this->fromAddress;
$maxPrefix = $fromAddress::getNumberOfBits();
$prefix = $this->getNetworkPrefix();
return pow(2, $maxPrefix - $prefix);
}
/**
* {@inheritdoc}
*
* @see \IPLib\Range\RangeInterface::getExactSize()
*/
public function getExactSize()
{
$fromAddress = $this->fromAddress;
$maxPrefix = $fromAddress::getNumberOfBits();
$prefix = $this->getNetworkPrefix();
return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix);
}
}
@@ -0,0 +1,152 @@
<?php
namespace IPLib\Range;
/**
* Types of IP address classes.
*/
class Type
{
/**
* Unspecified/unknown address.
*
* @var int
*/
const T_UNSPECIFIED = 1;
/**
* Reserved/internal use only.
*
* @var int
*/
const T_RESERVED = 2;
/**
* Refer to source hosts on "this" network.
*
* @var int
*/
const T_THISNETWORK = 3;
/**
* Internet host loopback address.
*
* @var int
*/
const T_LOOPBACK = 4;
/**
* Relay anycast address.
*
* @var int
*/
const T_ANYCASTRELAY = 5;
/**
* "Limited broadcast" destination address.
*
* @var int
*/
const T_LIMITEDBROADCAST = 6;
/**
* Multicast address assignments - Indentify a group of interfaces.
*
* @var int
*/
const T_MULTICAST = 7;
/**
* "Link local" address, allocated for communication between hosts on a single link.
*
* @var int
*/
const T_LINKLOCAL = 8;
/**
* Link local unicast / Linked-scoped unicast.
*
* @var int
*/
const T_LINKLOCAL_UNICAST = 9;
/**
* Discard-Only address.
*
* @var int
*/
const T_DISCARDONLY = 10;
/**
* Discard address.
*
* @var int
*/
const T_DISCARD = 11;
/**
* For use in private networks.
*
* @var int
*/
const T_PRIVATENETWORK = 12;
/**
* Public address.
*
* @var int
*/
const T_PUBLIC = 13;
/**
* Carrier-grade NAT address.
*
* @var int
*
* @since 1.10.0
*/
const T_CGNAT = 14;
/**
* Get the name of a type.
*
* @param int|mixed $type
*
* @return string
*/
public static function getName($type)
{
switch ($type) {
case static::T_UNSPECIFIED:
return 'Unspecified/unknown address';
case static::T_RESERVED:
return 'Reserved/internal use only';
case static::T_THISNETWORK:
return 'Refer to source hosts on "this" network';
case static::T_LOOPBACK:
return 'Internet host loopback address';
case static::T_ANYCASTRELAY:
return 'Relay anycast address';
case static::T_LIMITEDBROADCAST:
return '"Limited broadcast" destination address';
case static::T_MULTICAST:
return 'Multicast address assignments - Indentify a group of interfaces';
case static::T_LINKLOCAL:
return '"Link local" address, allocated for communication between hosts on a single link';
case static::T_LINKLOCAL_UNICAST:
return 'Link local unicast / Linked-scoped unicast';
case static::T_DISCARDONLY:
return 'Discard only';
case static::T_DISCARD:
return 'Discard';
case static::T_PRIVATENETWORK:
return 'For use in private networks';
case static::T_PUBLIC:
return 'Public address';
case static::T_CGNAT:
return 'Carrier-grade NAT';
default:
return $type === null ? 'Unknown type' : sprintf('Unknown type (%s)', print_r($type, true));
}
}
}
@@ -0,0 +1,246 @@
<?php
namespace IPLib\Service;
/**
* Helper class to work with unsigned binary integers.
*
* @internal
*/
class BinaryMath
{
/**
* @var \IPLib\Service\BinaryMath|null
*/
private static $instance;
/**
* @return \IPLib\Service\BinaryMath
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Trim the leading zeroes from a non-negative integer represented in binary form.
*
* @param string $value
*
* @return string
*/
public function reduce($value)
{
$value = ltrim($value, '0');
return $value === '' ? '0' : $value;
}
/**
* Compare two non-negative integers represented in binary form.
*
* @param string $a
* @param string $b
*
* @return int 1 if $a is greater than $b, -1 if $b is greater than $b, 0 if they are the same
*/
public function compare($a, $b)
{
list($a, $b) = $this->toSameLength($a, $b);
return $a < $b ? -1 : ($a > $b ? 1 : 0);
}
/**
* Add 1 to a non-negative integer represented in binary form.
*
* @param string $value
*
* @return string
*/
public function increment($value)
{
$lastZeroIndex = strrpos($value, '0');
if ($lastZeroIndex === false) {
return '1' . str_repeat('0', strlen($value));
}
return ltrim(substr($value, 0, $lastZeroIndex), '0') . '1' . str_repeat('0', strlen($value) - $lastZeroIndex - 1);
}
/**
* Calculate the bitwise AND of two non-negative integers represented in binary form.
*
* @param string $operand1
* @param string $operand2
*
* @return string
*/
public function andX($operand1, $operand2)
{
$operand1 = $this->reduce($operand1);
$operand2 = $this->reduce($operand2);
$numBits = min(strlen($operand1), strlen($operand2));
$operand1 = substr(str_pad($operand1, $numBits, '0', STR_PAD_LEFT), -$numBits);
$operand2 = substr(str_pad($operand2, $numBits, '0', STR_PAD_LEFT), -$numBits);
$result = '';
for ($index = 0; $index < $numBits; $index++) {
$result .= $operand1[$index] === '1' && $operand2[$index] === '1' ? '1' : '0';
}
return $this->reduce($result);
}
/**
* Calculate the bitwise OR of two non-negative integers represented in binary form.
*
* @param string $operand1
* @param string $operand2
*
* @return string
*/
public function orX($operand1, $operand2)
{
list($operand1, $operand2, $numBits) = $this->toSameLength($operand1, $operand2);
$result = '';
for ($index = 0; $index < $numBits; $index++) {
$result .= $operand1[$index] === '1' || $operand2[$index] === '1' ? '1' : '0';
}
return $result;
}
/**
* Compute 2 raised to the given exponent.
*
* If the result fits into a native PHP integer, an int is returned.
* If the result exceeds PHP_INT_MAX, a string containing the exact decimal representation is returned.
*
* @param int $exponent The non-negative exponent
*
* @return int|numeric-string
*/
public function pow2string($exponent)
{
if ($exponent < PHP_INT_SIZE * 8 - 1) {
return 1 << $exponent;
}
$digits = array(1);
for ($i = 0; $i < $exponent; $i++) {
$carry = 0;
foreach ($digits as $index => $digit) {
$product = $digit * 2 + $carry;
$digits[$index] = $product % 10;
$carry = (int) ($product / 10);
}
if ($carry !== 0) {
$digits[] = $carry;
}
}
$result = implode('', array_reverse($digits));
/** @var numeric-string $result */
return $result;
}
/**
* @param numeric-string|mixed $value
*
* @return numeric-string|'' empty string if $value is not a valid numeric string
*/
public function normalizeIntegerString($value)
{
if (!is_string($value) || $value === '') {
return '';
}
$sign = $value[0];
if ($sign === '-' || $sign === '+') {
$value = substr($value, 1);
}
$matches = null;
if (!preg_match('/^0*([0-9]+)$/', $value, $matches)) {
return '';
}
$numericString = $matches[1];
if ($sign === '-' && $numericString !== '0') {
$numericString = '-' . $numericString;
}
/** @var numeric-string $numericString */
return $numericString;
}
/**
* @param numeric-string $value a string that has been normalized with normalizeIntegerString()
*
* @return numeric-string
*/
public function add1ToIntegerString($value)
{
if ($value[0] === '-') {
if ($value === '-1') {
return '0';
}
$digits = str_split(substr($value, 1));
$i = count($digits) - 1;
while ($i >= 0) {
if ($digits[$i] !== '0') {
$digits[$i] = (string) ((int) $digits[$i] - 1);
break;
}
$digits[$i] = '9';
$i--;
}
$imploded = implode('', $digits);
if ($imploded[0] === '0') {
$imploded = substr($imploded, 1);
}
$result = '-' . $imploded;
/** @var numeric-string $result */
return $result; // @phpstan-ignore varTag.nativeType
}
$digits = str_split($value);
$carry = 1;
for ($i = count($digits) - 1; $i >= 0; $i--) {
$sum = (int) $digits[$i] + $carry;
$digits[$i] = (string) ($sum % 10);
$carry = (int) ($sum / 10);
if ($carry === 0) {
break;
}
if ($i === 0) {
array_unshift($digits, (string) $carry);
}
}
$result = implode('', $digits);
/** @var numeric-string $result */
return $result;
}
/**
* Zero-padding of two non-negative integers represented in binary form, so that they have the same length.
*
* @param string $num1
* @param string $num2
*
* @return array{string, string, int} The first array element is $num1 (padded), the first array element is $num2 (padded), the third array element is the number of bits
*/
private function toSameLength($num1, $num2)
{
$num1 = $this->reduce($num1);
$num2 = $this->reduce($num2);
$numBits = max(strlen($num1), strlen($num2));
return array(
str_pad($num1, $numBits, '0', STR_PAD_LEFT),
str_pad($num2, $numBits, '0', STR_PAD_LEFT),
$numBits,
);
}
}
@@ -0,0 +1,254 @@
<?php
namespace IPLib\Service;
use InvalidArgumentException;
/**
* @internal
*
* @readonly
*/
class NumberInChunks
{
const CHUNKSIZE_BYTES = 8;
const CHUNKSIZE_WORDS = 16;
/**
* @var bool
*/
public $negative;
/**
* @var int[]
*/
public $chunks;
/**
* @var int
*/
public $chunkSize;
/**
* @param bool $negative
* @param int[] $chunks
* @param int $chunkSize
*/
public function __construct($negative, array $chunks, $chunkSize)
{
$this->negative = $negative;
$this->chunks = $chunks;
$this->chunkSize = $chunkSize;
}
/**
* @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
*
* @return \IPLib\Service\NumberInChunks
*/
public function negate()
{
return new self($this->chunks === array(0) ? false : !$this->negative, $this->chunks, $this->chunkSize);
}
/**
* @throws \InvalidArgumentException if $other has a $chunkSize that's not the same as the $chunkSize of this
*
* @return \IPLib\Service\NumberInChunks
*/
public function add(NumberInChunks $that)
{
if ($this->chunkSize !== $that->chunkSize) {
throw new InvalidArgumentException('Incompatible chunk size');
}
if ($this->negative === $that->negative) {
return new self($this->negative, self::addChunks($this->chunks, $that->chunks, $this->chunkSize), $this->chunkSize);
}
if ($that->negative) {
list($negative, $chunks) = self::substractChunks($this->chunks, $that->chunks, $this->chunkSize);
} else {
list($negative, $chunks) = self::substractChunks($that->chunks, $this->chunks, $this->chunkSize);
}
return new self($negative, $chunks, $this->chunkSize);
}
/**
* @param int $int
* @param int $chunkSize
*
* @return \IPLib\Service\NumberInChunks
*/
public static function fromInteger($int, $chunkSize)
{
if ($int === 0) {
return new self(false, array(0), $chunkSize);
}
$negative = $int < 0;
if ($negative) {
$positiveInt = -$int;
/** @var int|float $positiveInt may be float because -PHP_INT_MIN is bigger than PHP_INT_MAX */
if (is_float($positiveInt)) {
return self::fromNumericString((string) $int, $chunkSize);
}
$int = $positiveInt;
}
$bitMask = (1 << $chunkSize) - 1;
$chunks = array();
while ($int !== 0) {
$chunks[] = $int & $bitMask;
$int >>= $chunkSize;
}
return new self($negative, array_reverse($chunks), $chunkSize);
}
/**
* @param string $numericString a string normalized with BinaryMath::normalizeIntegerString()
* @param int $chunkSize
*
* @return \IPLib\Service\NumberInChunks
*/
public static function fromNumericString($numericString, $chunkSize)
{
if ($numericString === '0') {
return new self(false, array(0), $chunkSize);
}
$negative = $numericString[0] === '-';
if ($negative) {
$numericString = substr($numericString, 1);
}
$chunks = array();
while ($numericString !== '0') {
$chunks[] = self::modulo($numericString, $chunkSize);
$numericString = self::divide($numericString, $chunkSize);
}
return new self($negative, array_reverse($chunks), $chunkSize);
}
/**
* @param string $numericString
* @param int $chunkSize
*
* @return int
*/
private static function modulo($numericString, $chunkSize)
{
$divisor = 1 << $chunkSize;
$carry = 0;
$len = strlen($numericString);
for ($i = 0; $i < $len; $i++) {
$digit = (int) $numericString[$i];
$carry = ($carry * 10 + $digit) % $divisor;
}
return $carry;
}
/**
* @param string $numericString
* @param int $chunkSize
*
* @return string
*/
private static function divide($numericString, $chunkSize)
{
$divisor = 1 << $chunkSize;
$quotient = '';
$carry = 0;
$len = strlen($numericString);
for ($i = 0; $i < $len; $i++) {
$digit = (int) $numericString[$i];
$value = $carry * 10 + $digit;
$quotient .= (string) ($value >> $chunkSize);
$carry = $value % $divisor;
}
return ltrim($quotient, '0') ?: '0';
}
/**
* @param int[] $addend1
* @param int[] $addend2
* @param int $chunkSize
*
* @return int[]
*/
private static function addChunks(array $addend1, array $addend2, $chunkSize)
{
$divisor = 1 << $chunkSize;
$result = array();
$carry = 0;
while ($addend1 !== array() || $addend2 !== array()) {
$sum = $carry + (array_pop($addend1) ?: 0) + (array_pop($addend2) ?: 0);
$result[] = $sum % $divisor;
$carry = $sum >> $chunkSize;
}
if ($carry !== 0) {
$result[] = $carry;
}
return array_reverse($result);
}
/**
* @param int[] $minuend
* @param int[] $subtrahend
* @param int $chunkSize
*
* @return array{bool, int[]}
*/
private static function substractChunks(array $minuend, array $subtrahend, $chunkSize)
{
$minuendCount = count($minuend);
$subtrahendCount = count($subtrahend);
if ($minuendCount > $subtrahendCount) {
$count = $minuendCount;
$negative = false;
} elseif ($minuendCount < $subtrahendCount) {
$count = $subtrahendCount;
$negative = true;
} else {
$count = $minuendCount;
$negative = false;
for ($i = 0; $i < $count; $i++) {
$delta = $minuend[$i] - $subtrahend[$i];
if ($delta === 0) {
continue;
}
if ($delta < 0) {
$negative = true;
}
break;
}
}
if ($negative) {
list($minuend, $subtrahend) = array($subtrahend, $minuend);
}
$subtrahend = array_pad($subtrahend, -$count, 0);
$borrowValue = 1 << $chunkSize;
$result = array();
$borrow = 0;
for ($i = $count - 1; $i >= 0; $i--) {
$value = $minuend[$i] - $subtrahend[$i] - $borrow;
if ($value < 0) {
$value += $borrowValue;
$borrow = 1;
} else {
$borrow = 0;
}
$result[] = $value;
}
while (isset($result[1])) {
$value = array_pop($result);
if ($value !== 0) {
$result[] = $value;
break;
}
}
return array($negative, array_reverse($result));
}
}
@@ -0,0 +1,175 @@
<?php
namespace IPLib\Service;
use IPLib\Address\AddressInterface;
use IPLib\Factory;
use IPLib\Range\Subnet;
/**
* Helper class to calculate the subnets describing all (and only all) the addresses between two boundaries.
*
* @internal
*/
class RangesFromBoundaryCalculator
{
/**
* The BinaryMath instance to be used to perform bitwise operations.
*
* @var \IPLib\Service\BinaryMath
*/
private $math;
/**
* The number of bits used to represent addresses.
*
* @var int
*
* @example 32 for IPv4, 128 for IPv6
*/
private $numBits;
/**
* The bit masks for every bit index.
*
* @var string[]
*/
private $masks;
/**
* The bit unmasks for every bit index.
*
* @var string[]
*/
private $unmasks;
/**
* Initializes the instance.
*
* @param int $numBits the number of bits used to represent addresses (32 for IPv4, 128 for IPv6)
*/
public function __construct($numBits)
{
$this->math = BinaryMath::getInstance();
$this->setNumBits($numBits);
}
/**
* Calculate the subnets describing all (and only all) the addresses between two boundaries.
*
* @param \IPLib\Address\AddressInterface $from
* @param \IPLib\Address\AddressInterface $to
*
* @return \IPLib\Range\Subnet[]|null return NULL if the two addresses have an invalid number of bits (that is, different from the one passed to the constructor of this class)
*/
public function getRanges(AddressInterface $from, AddressInterface $to)
{
if ($from->getNumberOfBits() !== $this->numBits || $to->getNumberOfBits() !== $this->numBits) {
return null;
}
if ($from->getComparableString() > $to->getComparableString()) {
list($from, $to) = array($to, $from);
}
$result = array();
$this->calculate($this->math->reduce($from->getBits()), $this->math->reduce($to->getBits()), $this->numBits, $result);
return $result;
}
/**
* Set the number of bits used to represent addresses (32 for IPv4, 128 for IPv6).
*
* @param int $numBits
*
* @return void
*/
private function setNumBits($numBits)
{
$numBits = (int) $numBits;
$masks = array();
$unmasks = array();
for ($bit = 0; $bit < $numBits; $bit++) {
$masks[$bit] = str_repeat('1', $numBits - $bit) . str_repeat('0', $bit);
$unmasks[$bit] = $bit === 0 ? '0' : str_repeat('1', $bit);
}
$this->numBits = $numBits;
$this->masks = $masks;
$this->unmasks = $unmasks;
}
/**
* Calculate the subnets.
*
* @param string $start the start address (represented in reduced bit form)
* @param string $end the end address (represented in reduced bit form)
* @param int $position the number of bits in the mask we are comparing at this cycle
* @param \IPLib\Range\Subnet[] $result found ranges will be added to this variable
*
* @return void
*/
private function calculate($start, $end, $position, array &$result)
{
if ($start === $end) {
$result[] = $this->subnetFromBits($start, $this->numBits);
return;
}
$startMasked = '';
for ($index = $position - 1; $index >= 0; $index--) {
$startMasked = $this->math->andX($start, $this->masks[$index]);
$endMasked = $this->math->andX($end, $this->masks[$index]);
if ($startMasked !== $endMasked) {
$position = $index;
break;
}
}
if ($startMasked === $start && $this->math->andX($this->math->increment($end), $this->unmasks[$position]) === '0') {
$result[] = $this->subnetFromBits($start, $this->numBits - 1 - $position);
return;
}
$middleAddress = $this->math->orX($start, $this->unmasks[$position]);
$this->calculate($start, $middleAddress, $position, $result);
$this->calculate($this->math->increment($middleAddress), $end, $position, $result);
}
/**
* Create an address instance starting from its bits.
*
* @param string $bits the bits of the address (represented in reduced bit form)
*
* @return \IPLib\Address\AddressInterface
*/
private function addressFromBits($bits)
{
$bits = str_pad($bits, $this->numBits, '0', STR_PAD_LEFT);
$bytes = array();
foreach (explode("\n", trim(chunk_split($bits, 8, "\n"))) as $byteBits) {
$bytes[] = (int) bindec($byteBits);
}
$result = Factory::addressFromBytes($bytes);
/** @var AddressInterface $result */
return $result;
}
/**
* Create an range instance starting from the bits if the address and the length of the network prefix.
*
* @param string $bits the bits of the address (represented in reduced bit form)
* @param int $networkPrefix the length of the network prefix
*
* @return \IPLib\Range\Subnet
*/
private function subnetFromBits($bits, $networkPrefix)
{
$startAddress = $this->addressFromBits($bits);
$numOnes = $this->numBits - $networkPrefix;
if ($numOnes === 0) {
return new Subnet($startAddress, $startAddress, $networkPrefix);
}
$endAddress = $this->addressFromBits(substr($bits, 0, -$numOnes) . str_repeat('1', $numOnes));
return new Subnet($startAddress, $endAddress, $networkPrefix);
}
}
@@ -0,0 +1,173 @@
<?php
namespace IPLib\Service;
/**
* Helper class to work with unsigned integers.
*
* @internal
*/
class UnsignedIntegerMath
{
/**
* Convert a string containing a decimal, octal or hexadecimal number into its bytes.
*
* @param string $value
* @param int $numBytes the wanted number of bytes
* @param bool $onlyDecimal Only parse decimal numbers
*
* @return int[]|null
*/
public function getBytes($value, $numBytes, $onlyDecimal = false)
{
$m = null;
if ($onlyDecimal) {
if (preg_match('/^0*(\d+)$/', $value, $m)) {
return $this->getBytesFromDecimal($m[1], $numBytes);
}
} else {
if (preg_match('/^0[Xx]0*([0-9A-Fa-f]+)$/', $value, $m)) {
return $this->getBytesFromHexadecimal($m[1], $numBytes);
}
if (preg_match('/^0+([0-7]*)$/', $value, $m)) {
return $this->getBytesFromOctal($m[1], $numBytes);
}
if (preg_match('/^[1-9][0-9]*$/', $value)) {
return $this->getBytesFromDecimal($value, $numBytes);
}
}
// Not a valid number
return null;
}
/**
* @return int
*/
protected function getMaxSignedInt()
{
return PHP_INT_MAX;
}
/**
* @param string $value never zero-length, never extra leading zeroes
* @param int $numBytes
*
* @return int[]|null
*/
private function getBytesFromBits($value, $numBytes)
{
$valueLength = strlen($value);
if ($valueLength > $numBytes << 3) {
// overflow
return null;
}
$remainderBits = $valueLength % 8;
if ($remainderBits !== 0) {
$value = str_pad($value, $valueLength + 8 - $remainderBits, '0', STR_PAD_LEFT);
}
$bytes = array_map('bindec', str_split($value, 8));
/** @var int[] $bytes */
return array_pad($bytes, -$numBytes, 0);
}
/**
* @param string $value may be zero-length, never extra leading zeroes
* @param int $numBytes
*
* @return int[]|null
*/
private function getBytesFromOctal($value, $numBytes)
{
if ($value === '') {
return array_fill(0, $numBytes, 0);
}
$bits = implode(
'',
array_map(
function ($octalDigit) {
return str_pad(decbin((int) octdec($octalDigit)), 3, '0', STR_PAD_LEFT);
},
str_split($value, 1)
)
);
$bits = ltrim($bits, '0');
return $bits === '' ? array_fill(0, $numBytes, 0) : self::getBytesFromBits($bits, $numBytes);
}
/**
* @param string $value never zero-length, never extra leading zeroes
* @param int $numBytes
*
* @return int[]|null
*/
private function getBytesFromDecimal($value, $numBytes)
{
$valueLength = strlen($value);
$maxSignedIntLength = strlen((string) $this->getMaxSignedInt());
if ($valueLength < $maxSignedIntLength) {
return $this->getBytesFromBits(decbin((int) $value), $numBytes);
}
// Divide by two, so that we have 1 less bit
$carry = 0;
$halfValue = ltrim(
implode(
'',
array_map(
function ($digit) use (&$carry) {
$number = $carry + (int) $digit;
$carry = ($number % 2) * 10;
return (string) $number >> 1;
},
str_split($value, 1)
)
),
'0'
);
$halfValueBytes = $this->getBytesFromDecimal($halfValue, $numBytes);
if ($halfValueBytes === null) {
return null;
}
$carry = $carry === 0 ? 0 : 1;
$result = array_fill(0, $numBytes, 0);
for ($index = $numBytes - 1; $index >= 0; $index--) {
$byte = $carry + ($halfValueBytes[$index] << 1);
if ($byte <= 0xFF) {
$carry = 0;
} else {
$carry = ($byte & ~0xFF) >> 8;
$byte -= 0x100;
}
$result[$index] = $byte;
}
if ($carry !== 0) {
// Overflow
return null;
}
return $result;
}
/**
* @param string $value never zero-length, never extra leading zeroes
* @param int $numBytes
*
* @return int[]|null
*/
private function getBytesFromHexadecimal($value, $numBytes)
{
$valueLength = strlen($value);
if ($valueLength > $numBytes << 1) {
// overflow
return null;
}
$value = str_pad($value, $valueLength + $valueLength % 2, '0', STR_PAD_LEFT);
$bytes = array_map('hexdec', str_split($value, 2));
/** @var int[] $bytes */
return array_pad($bytes, -$numBytes, 0);
}
}