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,20 @@
The MIT License (MIT)
Copyright (c) 2016 Michele Locati
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,823 @@
[![Tests](https://github.com/mlocati/ip-lib/actions/workflows/tests.yml/badge.svg)](https://github.com/mlocati/ip-lib/actions/workflows/tests.yml)
[![Code Coverage](https://img.shields.io/coverallsCoverage/github/mlocati/ip-lib?branch=main&label=Coverage)](https://coveralls.io/github/mlocati/ip-lib?branch=main)
[![Packagist Downloads](https://img.shields.io/packagist/dt/mlocati/ip-lib?label=Downloads)](https://packagist.org/packages/mlocati/ip-lib)
[![Open in Gitpod](https://img.shields.io/badge/Open%20in-Gitpod-%232cb64c?logo=gitpod)](https://gitpod.io/#https://github.com/mlocati/ip-lib)
# IPLib - Handle IPv4, IPv6 and IP ranges
## Introduction
IPLib is a modern, PSR-compliant, test-driven IP addresses and subnets manipulation library. It implements primitives to handle IPv4 and IPv6 addresses, as well as IP ranges (subnets), in CIDR format (like `::1/128` or `127.0.0.1/32`) and in pattern format (like `::*:*` or `127.0.*.*`).
## Requirements
IPLib has very basic requirements as:
- Works with any PHP version greater than 5.3.3 (PHP **5.3.x**, **5.4.x**, **5.5.x**, **5.6.x**, **7.x**, and **8.x** are fully supported).
- **No external dependencies**
- **No special PHP configuration needed** (yes, it will __always work__ even if PHP has not been built with IPv6 support!).
## Installation
### Manual installation
[Download](https://github.com/mlocati/ip-lib/releases) the latest version, unzip it and add these lines in our PHP files:
```php
require_once 'path/to/iplib/ip-lib.php';
```
### Installation with Composer
Simply run
```sh
composer require mlocati/ip-lib
```
or add these lines to your `composer.json` file:
```json
"require": {
"mlocati/ip-lib": "^1"
}
```
## Sample usage
### Parse an address
To parse an IPv4 address:
```php
$address = \IPLib\Address\IPv4::parseString('127.0.0.1');
```
To parse an IPv6 address:
```php
$address = \IPLib\Address\IPv6::parseString('::1');
```
To parse an address in any format (IPv4 or IPv6):
```php
$address = \IPLib\Factory::parseAddressString('::1');
$address = \IPLib\Factory::parseAddressString('127.0.0.1');
```
### Get the next/previous addresses
```php
$address = \IPLib\Factory::parseAddressString('::1');
// This will print ::
echo (string) $address->getPreviousAddress();
// This will print ::2
echo (string) $address->getNextAddress();
```
### Shifting the bits of an address
You can use the `shift` method to shift the address bits to the right (with positive values) or to the left (negative values):
```php
$address = \IPLib\Factory::parseAddressString('2.4.8.16');
// This will print 1.2.4.8
echo (string) $address->shift(1);
// This will print 4.8.16.32
echo (string) $address->shift(-1);
// This will print 4.8.16.0
echo (string) $address->shift(-8);
$address = \IPLib\Factory::parseAddressString('::10');
// This will print ::8
echo (string) $address->shift(1);
// This will print ::20
echo (string) $address->shift(-1);
// This will print ::10:0
echo (string) $address->shift(-16);
```
### Adding two IP addresses
You can calculate the sum of 2 IP addresses using the `add` method:
```php
$a = \IPLib\Factory::parseAddressString('1.2.3.4');
$b = \IPLib\Factory::parseAddressString('10.0.0.0');
// This will print 11.2.3.4
echo (string) $a->add($b);
```
### Get the addresses at a specified offset
For addresses:
```php
$address = \IPLib\Factory::parseAddressString('::1');
// This will print ::1
echo (string) $address->getAddressAtOffset(0);
// This will print ::2
echo (string) $address->getAddressAtOffset(1);
// This will print ::3
echo (string) $address->getAddressAtOffset(2);
// This will print ::3e9
echo (string) $address->getAddressAtOffset(1000);
// This will print ::
echo (string) $address->getAddressAtOffset(-1);
// This will print NULL
echo var_dump($address->getAddressAtOffset(-2));
// This will print ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
echo (string) $address->getAddressAtOffset('340282366920938463463374607431768211454');
```
For ranges:
```php
$range = \IPLib\Factory::parseRangeString('::ff00/120');
// This will print ::ff00
echo (string) $range->getAddressAtOffset(0);
// This will print ::ff10
echo (string) $range->getAddressAtOffset(16);
// This will print ::ff64
echo (string) $range->getAddressAtOffset(100);
// This will print NULL because the address ::1:0 is out of the range
var_dump($range->getAddressAtOffset(256));
// This will print ::ffff
echo (string) $range->getAddressAtOffset(-1);
// This will print ::fff0
echo (string) $range->getAddressAtOffset(-16);
// This will print ::ff00
echo (string) $range->getAddressAtOffset(-256);
// This will print NULL because the address ::feff is out of the range
var_dump($range->getAddressAtOffset(-257));
$range2 = \IPLib\Factory::parseRangeString('::/0');
// This will print ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
echo (string) $range2->getAddressAtOffset(-1);
// This will print ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
echo (string) $range2->getAddressAtOffset('340282366920938463463374607431768211455');
// This will print ::1
echo (string) $range2->getAddressAtOffset('-340282366920938463463374607431768211455');
```
### Parse an IP address range
To parse a subnet (CIDR) range:
```php
$range = \IPLib\Range\Subnet::parseString('127.0.0.1/24');
$range = \IPLib\Range\Subnet::parseString('::1/128');
```
To parse a pattern (asterisk notation) range:
```php
$range = \IPLib\Range\Pattern::parseString('127.0.0.*');
$range = \IPLib\Range\Pattern::parseString('::*');
```
To parse an address as a range:
```php
$range = \IPLib\Range\Single::parseString('127.0.0.1');
$range = \IPLib\Range\Single::parseString('::1');
```
To parse a range in any format:
```php
$range = \IPLib\Factory::parseRangeString('127.0.0.*');
$range = \IPLib\Factory::parseRangeString('::1/128');
$range = \IPLib\Factory::parseRangeString('::');
```
### Retrieve a range from its boundaries
You can calculate the smallest range that comprises two addresses:
```php
$range = \IPLib\Factory::getRangeFromBoundaries('192.168.0.1', '192.168.255.255');
// This will print 192.168.0.0/16
echo (string) $range;
```
You can also calculate a list of ranges that exactly describes all the addresses between two addresses:
```php
$ranges = \IPLib\Factory::getRangesFromBoundaries('192.168.0.0', '192.168.0.5');
// This will print 192.168.0.0/30 192.168.0.4/31
echo implode(' ', $ranges);
```
### Retrieve a range that contains a set of IP addresses
You can use `IPLib\Factory::getRangeFromAddresses()` to retrieve the minimal IP range that contains all the provided IP addresses:
```php
$range = \IPLib\Factory::getRangeFromAddresses(array(
'1.2.2.225',
'1.2.1.124',
'1.2.3.237',
));
// This will print 1.2.0.0/22
echo (string) $range;
```
### Retrieve the boundaries of a range
```php
$range = \IPLib\Factory::parseRangeString('127.0.0.*');
// This will print 127.0.0.0
echo (string) $range->getStartAddress();
// This will print 127.0.0.255
echo (string) $range->getEndAddress();
```
### Format addresses and ranges
Both IP addresses and ranges have a `toString` method that you can use to retrieve a textual representation:
```php
// This will print 127.0.0.1
echo \IPLib\Factory::parseAddressString('127.0.0.1')->toString();
// This will print 127.0.0.1
echo \IPLib\Factory::parseAddressString('127.000.000.001')->toString();
// This will print ::1
echo \IPLib\Factory::parseAddressString('::1')->toString();
// This will print ::1
echo \IPLib\Factory::parseAddressString('0:0::1')->toString();
// This will print ::1/64
echo \IPLib\Factory::parseRangeString('0:0::1/64')->toString();
```
When working with IPv6, you may want the full (expanded) representation of the addresses. In this case, simply use a `true` parameter for the `toString` method:
```php
// This will print 0000:0000:0000:0000:0000:0000:0000:0000
echo \IPLib\Factory::parseAddressString('::')->toString(true);
// This will print 0000:0000:0000:0000:0000:0000:0000:0001
echo \IPLib\Factory::parseAddressString('::1')->toString(true);
// This will print 0fff:0000:0000:0000:0000:0000:0000:0000
echo \IPLib\Factory::parseAddressString('fff::')->toString(true);
// This will print 0000:0000:0000:0000:0000:0000:0000:0000
echo \IPLib\Factory::parseAddressString('::0:0')->toString(true);
// This will print 0001:0002:0003:0004:0005:0006:0007:0008
echo \IPLib\Factory::parseAddressString('1:2:3:4:5:6:7:8')->toString(true);
// This will print 0000:0000:0000:0000:0000:0000:0000:0001/64
echo \IPLib\Factory::parseRangeString('0:0::1/64')->toString();
```
You may also want a *long* representation for IPv4 addresses: here again you can use `true`as the parameter for the `toString` method:
```php
// This will print 1.2.3.4
echo \IPLib\Factory::parseAddressString('1.2.3.4')->toString();
// This will print 001.002.003.004
echo \IPLib\Factory::parseAddressString('1.2.3.4')->toString(true);
```
The address and range objects implements the `__toString()` method, which call the `toString()` method.
So, if you want the string (short) representation of an object, you can do any of the following:
```php
$address = \IPLib\Address\IPv6::parseString('::1');
// All these will print ::1
echo $address->toString();
echo $address->toString(false);
echo (string) $address;
```
### Check if an address is contained in a range
All the range types offer a `contains` method, and all the IP address types offer a `matches` method: you can call them to check if an address is contained in a range:
```php
$address = \IPLib\Factory::parseAddressString('1:2:3:4:5:6:7:8');
$range = \IPLib\Factory::parseRangeString('0:0::1/64');
$contained = $address->matches($range);
// that's equivalent to
$contained = $range->contains($address);
```
Please remark that if the address is IPv4 and the range is IPv6 (or vice-versa), the result will always be `false`.
### Check if a range contains another range
All the range types offer a `containsRange` method: you can call them to check if an address range fully contains another range:
```php
$range1 = \IPLib\Factory::parseRangeString('0:0::1/64');
$range2 = \IPLib\Factory::parseRangeString('0:0::1/65');
$contained = $range1->containsRange($range2);
```
### Getting the type of an IP address
If you want to know if an address is within a private network, or if it's a public IP, or whatever you want, you can use the `getRangeType` method:
```php
$address = \IPLib\Factory::parseAddressString('::');
$type = $address->getRangeType();
$typeName = \IPLib\Range\Type::getName($type);
```
The most notable values of the range type are:
- `\IPLib\Range\Type::T_UNSPECIFIED` if the address is all zeros (`0.0.0.0` or `::`)
- `\IPLib\Range\Type::T_LOOPBACK` if the address is the localhost (usually `127.0.0.1` or `::1`)
- `\IPLib\Range\Type::T_PRIVATENETWORK` if the address is in the local network (for instance `192.168.0.1` or `fc00::1`)
- `\IPLib\Range\Type::T_PUBLIC` if the address is for public usage (for instance `104.25.25.33` or `2001:503:ba3e::2:30`)
### Getting the type of an IP address range
If you want to know the type of an address range, you can use the `getRangeType` method:
```php
$range = \IPLib\Factory::parseRangeString('2000:0::1/64');
// $type will contain the value of \IPLib\Range\Type::T_PUBLIC
$type = $range->getRangeType();
// This will print Public address
echo \IPLib\Range\Type::getName($type);
```
Please note that if a range spans across multiple range types, you'll get NULL as the range type:
```php
$range = \IPLib\Factory::parseRangeString('::/127');
// $type will contain null
$type = $range->getRangeType();
// This will print Unknown type
echo \IPLib\Range\Type::getName($type);
```
### Converting IP addresses
This library supports converting IPv4 to/from IPv6 addresses using the [6to4 notation](https://tools.ietf.org/html/rfc3056) or the [IPv4-mapped notation](https://tools.ietf.org/html/rfc4291#section-2.5.5.2):
```php
$ipv4 = \IPLib\Factory::parseAddressString('1.2.3.4');
// 6to4 notation
$ipv6 = $ipv4->toIPv6();
// This will print 2002:102:304::
echo (string) $ipv6;
// This will print 1.2.3.4
echo $ipv6->toIPv4();
// IPv4-mapped notation
$ipv6_6to4 = $ipv4->toIPv6IPv4Mapped();
// This will print ::ffff:1.2.3.4
echo (string) $ipv6_6to4;
// This will print 1.2.3.4
echo $ipv6_6to4->toIPv4();
```
### Converting IP ranges
This library supports IPv4/IPv6 ranges in pattern format (eg. `192.168.*.*`) and in CIDR/subnet format (eg. `192.168.0.0/16`), and it offers a way to convert between the two formats:
```php
// This will print ::*:*:*:*
echo \IPLib\Factory::parseRangeString('::/64')->asPattern()->toString();
// This will print 1:2::/96
echo \IPLib\Factory::parseRangeString('1:2::*:*')->asSubnet()->toString();
// This will print 192.168.0.0/24
echo \IPLib\Factory::parseRangeString('192.168.0.*')->asSubnet()->toString();
// This will print 10.*.*.*
echo \IPLib\Factory::parseRangeString('10.0.0.0/8')->asPattern()->toString();
```
Please remark that all the range types implement the `asPattern()` and `asSubnet()` methods.
### Splitting IP ranges
If you need to divide an IP address range into smaller ranges, you can use the `split` method.
You can specify the length of the network prefix, as well as indicate whether you want to force the Subnet notation (by default, it is not).
For example:
```php
$subnet = \IPLib\Factory::parseRangeString('192.168.112.203/24');
$smallerSubnets = $subnet->split(25);
print_r(array_map('strval', $smallerSubnets));
/*
* You'll have:
* Array
* (
* [0] => 192.168.112.0/25
* [1] => 192.168.112.128/25
* )
*/
$subnet = \IPLib\Factory::parseRangeString('192.168.*.*');
$smallerSubnets = $subnet->split(24);
print_r(array_map('strval', $smallerSubnets));
/*
* You'll have:
* Array
* (
* [0] => 192.168.0.*
* [1] => 192.168.1.*
* [...]
* [254] => 192.168.254.*
* [255] => 192.168.255.*
* )
*/
$subnet = \IPLib\Factory::parseRangeString('192.168.*.*');
$smallerSubnets = $subnet->split(24, true);
print_r(array_map('strval', $smallerSubnets));
/*
* You'll have:
* Array
* (
* [0] => 192.168.0.0/24
* [1] => 192.168.1.0/24
* [...]
* [254] => 192.168.254.0/24
* [255] => 192.168.255.0/24
* )
*/
```
### Getting the subnet mask for IPv4 ranges
You can use the `getSubnetMask()` to get the subnet mask for IPv4 ranges:
```php
// This will print 255.255.255.0
echo \IPLib\Factory::parseRangeString('192.168.0.*')->getSubnetMask()->toString();
// This will print 255.255.255.252
echo \IPLib\Factory::parseRangeString('192.168.0.12/30')->getSubnetMask()->toString();
```
### Getting the range size
You can use the `getSize()` to get the count of addresses this IP range contains:
```php
// This will print 256
echo \IPLib\Factory::parseRangeString('192.168.0.*')->getSize();
// This will print 4
echo \IPLib\Factory::parseRangeString('192.168.0.12/30')->getSize();
// This will print 1
echo \IPLib\Factory::parseRangeString('192.168.0.1')->getSize();
```
Please note that if the number of IP addresses contained in the range is greater than the maximum integer supported by the operating system (2,147,483,647 for 32-bit systems, 9,223,372,036,854,775,807 for 64-bit systems), the `getSize()` method will return a `float` (which may be not precise).
If instead you want the exact number of IP addresses, you can use the `getExactSize()` method, which will return a string containing the number of IP addresses in decimal format in case of such big numbers.
```php
// This will print:
// int(1)
var_dump(\IPLib\Factory::parseRangeString('0.0.0.0/32')->getExactSize());
// On 32-bit systems, this will print
// string(10) "2147483648"
// On 64-bit systems, this will print
// int(2147483648)
var_dump(\IPLib\Factory::parseRangeString('0.0.0.0/1')->getExactSize());
// This will print:
// int(1073741824)
var_dump(\IPLib\Factory::parseRangeString('::/98')->getExactSize());
// On 32-bit systems, this will print
// string(10) "2147483648"
// On 64-bit systems, this will print
// int(2147483648)
var_dump(\IPLib\Factory::parseRangeString('::/97')->getExactSize());
// On 32-bit and 64-bit systems, this will print
// string(39) "170141183460469231731687303715884105728"
var_dump(\IPLib\Factory::parseRangeString('::/1')->getExactSize());
```
### Getting the reverse DNS lookup address
To perform reverse DNS queries, you need to use a special format of the IP addresses.
You can use the `getReverseDNSLookupName()` method of the IP address instances to retrieve it easily:
```php
$ipv4 = \IPLib\Factory::parseAddressString('1.2.3.255');
$ipv6 = \IPLib\Factory::parseAddressString('1234:abcd::cafe:babe');
// This will print 255.3.2.1.in-addr.arpa
echo $ipv4->getReverseDNSLookupName();
// This will print e.b.a.b.e.f.a.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa
echo $ipv6->getReverseDNSLookupName();
```
To parse addresses in reverse DNS lookup format you can use the `IPLib\ParseStringFlag::ADDRESS_MAYBE_RDNS` flag when parsing a string:
```php
$ipv4 = \IPLib\Factory::parseAddressString('255.3.2.1.in-addr.arpa', \IPLib\ParseStringFlag::ADDRESS_MAYBE_RDNS);
$ipv6 = \IPLib\Factory::parseAddressString('e.b.a.b.e.f.a.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa', \IPLib\ParseStringFlag::ADDRESS_MAYBE_RDNS);
// This will print 1.2.3.255
echo $ipv4->toString();
// This will print 1234:abcd::cafe:babe
echo $ipv6->toString();
```
You can also use `getReverseDNSLookupName()` for IP ranges.
In this case, the result is an array of strings:
```php
$range = \IPLib\Factory::parseRangeString('10.155.16.0/22');
/*
* This will print:
* array (
* 0 => '16.155.10.in-addr.arpa',
* 1 => '17.155.10.in-addr.arpa',
* 2 => '18.155.10.in-addr.arpa',
* 3 => '19.155.10.in-addr.arpa',
* )
*/
var_export($range->getReverseDNSLookupName());
```
### Using a database
This package offers a great feature: you can store address ranges in a database table, and check if an address is contained in one of the saved ranges with a simple query.
To save a range, you need to store the address type (for IPv4 it's `4`, for IPv6 it's `6`), as well as two values representing the start and the end of the range.
These methods are:
```php
$range->getAddressType();
$range->getComparableStartString();
$range->getComparableEndString();
```
Let's assume that you saved the type in a field called `addressType`, and the range boundaries in two fields called `rangeFrom` and `rangeTo`.
When you want to check if an address is within a stored range, simply use the `getComparableString` method of the address and check if it's between the fields `rangeFrom` and `rangeTo`, and check if the stored `addressType` is the same as the one of the address instance you want to check.
Here's a sample code:
```php
/*
* Let's assume that:
* - $pdo is a PDO instance
* - $range is a range object
* - $address is an address object
*/
// Save the $range object
$insertQuery = $pdo->prepare('
insert into ranges (addressType, rangeFrom, rangeTo)
values (:addressType, :rangeFrom, :rangeTo)
');
$insertQuery->execute(array(
':addressType' => $range->getAddressType(),
':rangeFrom' => $range->getComparableStartString(),
':rangeTo' => $range->getComparableEndString(),
));
// Retrieve the saved ranges where an address $address falls:
$searchQuery = $pdo->prepare('
select * from ranges
where addressType = :addressType
and :address between rangeFrom and rangeTo
');
$searchQuery->execute(array(
':addressType' => $address->getAddressType(),
':address' => $address->getComparableString(),
));
$rows = $searchQuery->fetchAll();
$searchQuery->closeCursor();
```
## Handling non-standard address and range strings
### Accepting ports
If you want to accept addresses that may include ports, you can specify the `IPLib\ParseStringFlag::MAY_INCLUDE_PORT` flag:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
require_once __DIR__ . '/../ip-lib.php';
// These will print NULL
var_export(Factory::parseAddressString('127.0.0.1:80'));
var_export(Factory::parseAddressString('[::]:80'));
// This will print 127.0.0.1
echo (string) Factory::parseAddressString('127.0.0.1:80', ParseStringFlag::MAY_INCLUDE_PORT);
// This will print ::
echo (string) Factory::parseAddressString('[::]:80', ParseStringFlag::MAY_INCLUDE_PORT);
```
### Accepting IPv6 zone IDs
If you want to accept IPv6 addresses that may include a zone ID, you can specify the `IPLib\ParseStringFlag::MAY_INCLUDE_ZONEID` flag:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print NULL
var_export(Factory::parseAddressString('::%11'));
// This will print ::
echo (string) Factory::parseAddressString('::%11', ParseStringFlag::MAY_INCLUDE_ZONEID);
```
### Accepting non-decimal IPv4 addresses
IPv4 addresses are usually expressed in decimal notation, for example as `192.168.0.1`.
By the way, the GNU (used in many Linux distros), BSD (used in Mac) and Windows implementations of `inet_aton` and `inet_addr` accept IPv4 addresses with numbers in octal and/or hexadecimal format.
Please remark that this does not apply to the `inet_pton` and `ip2long` functions, as well as to the Musl implementation (used in Alpine Linux) of `inet_aton` and `inet_addr`.
So, for example, these addresses are all equivalent to `192.168.0.1`:
- `0xC0.0xA8.0x0.0x01` (only hexadecimal)
- `0300.0250.00.01` (only octal)
- `192.0250.0.0x01` (decimal, octal and hexadecimal numbers)
(try it: if you browse to [`http://0177.0.0.0x1`](http://0177.0.0.0x1), your browser will try to browse `http://127.0.0.1`).
If you want to accept this non-decimal syntax, you may use the `IPLib\ParseStringFlag::IPV4_MAYBE_NON_DECIMAL` flag:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print NULL
var_export(Factory::parseAddressString('0177.0.0.0x1'));
// This will print 127.0.0.1
var_export((string) Factory::parseAddressString('0177.0.0.0x1', ParseStringFlag::IPV4_MAYBE_NON_DECIMAL));
// This will print NULL
var_export(Factory::parseRangeString('0177.0.0.0x1/32'));
// This will print 127.0.0.1/32
var_export((string) Factory::parseRangeString('0177.0.0.0x1/32', ParseStringFlag::IPV4_MAYBE_NON_DECIMAL));
```
Please be aware that the `IPV4_MAYBE_NON_DECIMAL` flag may also affect parsing decimal numbers:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print 127.0.0.10 since the last digit is assumed to be decimal
var_export((string) Factory::parseAddressString('127.0.0.010'));
// This will print 127.0.0.8 since the last digit is assumed to be octal
var_export((string) Factory::parseAddressString('127.0.0.010', ParseStringFlag::IPV4_MAYBE_NON_DECIMAL));
```
### Accepting IPv4 addresses in not-quad-dotted notation
IPv4 addresses are usually expressed with 4 numbers, for example as `192.168.0.1`.
By the way, the GNU (used in many Linux distros), BSD (used in Mac) and Windows implementations of `inet_aton` and `inet_addr` [accept IPv4 addresses with 1 to 4 numbers](https://man7.org/linux/man-pages/man3/inet_addr.3.html#DESCRIPTION).
Please remark that this does not apply to the `inet_pton` and `ip2long` functions, as well as to the Musl implementation (used in Alpine Linux) of `inet_aton` and `inet_addr`.
If you want to accept this non-decimal syntax, you may use the `IPLib\ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED` flag:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print NULL
var_export(Factory::parseAddressString('1.2.500'));
// This will print 0.0.0.0
var_export((string) Factory::parseAddressString('0', ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED));
// This will print 0.0.0.1
var_export((string) Factory::parseAddressString('1', ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED));
// This will print 0.0.1.244
var_export((string) Factory::parseAddressString('0.0.500', ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED));
// This will print 255.255.255.255
var_export((string) Factory::parseAddressString('4294967295', ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED));
```
### Accepting compact IPv4 subnet notation
Even if there isn't an RFC that describe it, IPv4 subnet notation may also be written in a compact form, omitting extra digits (for example, `127.0.0.0/24` may be written as `127/24`).
If you want to accept such format, you can specify the `IPLib\ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT` flag:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print NULL
var_export(Factory::parseRangeString('127/24'));
// This will print 127.0.0.0/24
echo (string) Factory::parseRangeString('127/24', ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT);
```
### Combining multiple flags
Of course, you may use more than one `IPLib\ParseStringFlag` flag at once:
```php
use IPLib\Factory;
use IPLib\ParseStringFlag;
// This will print 127.0.0.255
var_export((string) Factory::parseAddressString('127.0.0.0xff:80', ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::IPV4_MAYBE_NON_DECIMAL));
// This will print ::
var_export((string) Factory::parseAddressString('[::%11]:80', ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID));
```
## Gitpod Environment Variables
The following features can be enabled through environment variables that have been set in your [Gitpod preferences](https://gitpod.io/variables).:
\* _Please note that storing sensitive data in environment variables is not ultimately secure but should be OK for most development situations._
- ### Sign Git commits with a GPG key
- `GPG_KEY_ID` (required)
- The ID of the GPG key you want to use to sign your git commits
- `GPG_KEY` (required)
- Base64 encoded private GPG key that corresponds to your `GPG_KEY_ID`
- `GPG_MATCH_GIT_TO_EMAIL` (optional)
- Sets your git user.email in `~/.gitconfig` to the value provided
- `GPG_AUTO_ULTIMATE_TRUST` (optional)
- If the value is set to `yes` or `YES` then your `GPG_KEY` will be automatically ultimately trusted
- ### Activate an Intelliphense License Key
- `INTELEPHENSE_LICENSEKEY`
- Creates `~/intelephense/licence.txt` and will contain the value provided
- This will activate [Intelliphense](https://intelephense.com/) for you each time the workspace is created or restarted
## Do you really want to say thank you?
You can offer me a [monthly coffee](https://github.com/sponsors/mlocati) or a [one-time coffee](https://paypal.me/mlocati) :wink:
@@ -0,0 +1,60 @@
{
"name": "mlocati/ip-lib",
"description": "Handle IPv4, IPv6 addresses and ranges",
"type": "library",
"license": "MIT",
"homepage": "https://github.com/mlocati/ip-lib",
"authors": [
{
"name": "Michele Locati",
"homepage": "https://github.com/mlocati",
"email": "mlocati@gmail.com",
"role": "Author"
}
],
"keywords": [
"ip",
"ipv4",
"ipv6",
"range",
"network",
"networking",
"address",
"addresses",
"subnet",
"matching",
"managing",
"manage"
],
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-4": {
"IPLib\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"IPLib\\Test\\": "test/tests/",
"IPLib\\Test\\Helpers\\": "test/helpers/"
}
},
"require-dev": {
"ext-pdo_sqlite": "*",
"phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.5"
},
"scripts": {
"test": "phpunit"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mlocati"
},
{
"type": "other",
"url": "https://paypal.me/mlocati"
}
]
}
@@ -0,0 +1,13 @@
<?php
spl_autoload_register(
function ($class) {
if (strpos($class, 'IPLib\\') !== 0) {
return;
}
$file = __DIR__ . DIRECTORY_SEPARATOR . 'src' . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen('IPLib'))) . '.php';
if (is_file($file)) {
require_once $file;
}
}
);
@@ -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);
}
}