Skip to content
This repository was archived by the owner on Mar 18, 2024. It is now read-only.

Commit 390440e

Browse files
Merge pull request #7 from SamuelMwangiW/ip-utils
Add IP Address checks
2 parents 56d4157 + 619d1e4 commit 390440e

File tree

4 files changed

+278
-1
lines changed

4 files changed

+278
-1
lines changed

src/DTO/IPAddress.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,58 @@
44

55
namespace SamuelMwangiW\Linode\DTO;
66

7+
use SamuelMwangiW\Linode\Support\IpUtils;
8+
79
class IPAddress
810
{
11+
public string $ip;
12+
13+
protected array $v4 = [
14+
'0.0.0.0/8',
15+
'10.0.0.0/8',
16+
'127.0.0.0/8',
17+
'172.16.0.0/12',
18+
'192.168.0.0/16',
19+
'169.254.0.0/16',
20+
];
21+
22+
protected array $v6 = [
23+
'::1/128',
24+
'fc00::/7',
25+
'fd00::/8',
26+
'fe80::/10',
27+
];
28+
929
public function __construct(
10-
public string $ip
30+
string $ip
1131
) {
32+
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
33+
throw new \InvalidArgumentException('Invalid IP received');
34+
}
35+
36+
$this->ip = $ip;
37+
}
38+
39+
public function isV4(): bool
40+
{
41+
return !$this->isV6();
42+
}
43+
44+
public function isV6(): bool
45+
{
46+
return substr_count($this->ip, ':') > 1;
47+
}
48+
49+
public function isPrivate(): bool
50+
{
51+
$ips = $this->isV4() ? $this->v4 : $this->v6;
52+
53+
return IpUtils::checkIp($this->ip, $ips);
54+
}
55+
56+
public function isPublic(): bool
57+
{
58+
return !$this->isPrivate();
1259
}
1360

1461
public function __toString(): string

src/Support/IpUtils.php

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace SamuelMwangiW\Linode\Support;
13+
14+
class IpUtils
15+
{
16+
private static array $checkedIps = [];
17+
18+
/**
19+
* This class should not be instantiated.
20+
*/
21+
private function __construct()
22+
{
23+
}
24+
25+
/**
26+
* Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets.
27+
*
28+
* @param string|array $ips List of IPs or subnets (can be a string if only a single one)
29+
*/
30+
public static function checkIp(string $requestIp, string|array $ips): bool
31+
{
32+
if (!\is_array($ips)) {
33+
$ips = [$ips];
34+
}
35+
36+
$method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4';
37+
38+
foreach ($ips as $ip) {
39+
if (self::$method($requestIp, $ip)) {
40+
return true;
41+
}
42+
}
43+
44+
return false;
45+
}
46+
47+
/**
48+
* Compares two IPv4 addresses.
49+
* In case a subnet is given, it checks if it contains the request IP.
50+
*
51+
* @param string $ip IPv4 address or subnet in CIDR notation
52+
*
53+
* @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet
54+
*/
55+
public static function checkIp4(string $requestIp, string $ip): bool
56+
{
57+
$cacheKey = $requestIp.'-'.$ip;
58+
if (isset(self::$checkedIps[$cacheKey])) {
59+
return self::$checkedIps[$cacheKey];
60+
}
61+
62+
if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
63+
return self::$checkedIps[$cacheKey] = false;
64+
}
65+
66+
if (str_contains($ip, '/')) {
67+
[$address, $netmask] = explode('/', $ip, 2);
68+
69+
if ('0' === $netmask) {
70+
return self::$checkedIps[$cacheKey] = filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4);
71+
}
72+
73+
if ($netmask < 0 || $netmask > 32) {
74+
return self::$checkedIps[$cacheKey] = false;
75+
}
76+
} else {
77+
$address = $ip;
78+
$netmask = 32;
79+
}
80+
81+
if (false === ip2long($address)) {
82+
return self::$checkedIps[$cacheKey] = false;
83+
}
84+
85+
return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
86+
}
87+
88+
/**
89+
* Compares two IPv6 addresses.
90+
* In case a subnet is given, it checks if it contains the request IP.
91+
*
92+
* @author David Soria Parra <dsp at php dot net>
93+
*
94+
* @see https://github.com/dsp/v6tools
95+
*
96+
* @param string $ip IPv6 address or subnet in CIDR notation
97+
*
98+
* @throws \RuntimeException When IPV6 support is not enabled
99+
*/
100+
public static function checkIp6(string $requestIp, string $ip): bool
101+
{
102+
$cacheKey = $requestIp.'-'.$ip;
103+
if (isset(self::$checkedIps[$cacheKey])) {
104+
return self::$checkedIps[$cacheKey];
105+
}
106+
107+
if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) {
108+
throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".');
109+
}
110+
111+
if (str_contains($ip, '/')) {
112+
[$address, $netmask] = explode('/', $ip, 2);
113+
114+
if ('0' === $netmask) {
115+
return (bool) unpack('n*', @inet_pton($address));
116+
}
117+
118+
if ($netmask < 1 || $netmask > 128) {
119+
return self::$checkedIps[$cacheKey] = false;
120+
}
121+
} else {
122+
$address = $ip;
123+
$netmask = 128;
124+
}
125+
126+
$bytesAddr = unpack('n*', @inet_pton($address));
127+
$bytesTest = unpack('n*', @inet_pton($requestIp));
128+
129+
if (!$bytesAddr || !$bytesTest) {
130+
return self::$checkedIps[$cacheKey] = false;
131+
}
132+
133+
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
134+
$left = $netmask - 16 * ($i - 1);
135+
$left = ($left <= 16) ? $left : 16;
136+
$mask = ~(0xFFFF >> $left) & 0xFFFF;
137+
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
138+
return self::$checkedIps[$cacheKey] = false;
139+
}
140+
}
141+
142+
return self::$checkedIps[$cacheKey] = true;
143+
}
144+
145+
/**
146+
* Anonymizes an IP/IPv6.
147+
*
148+
* Removes the last byte for v4 and the last 8 bytes for v6 IPs
149+
*/
150+
public static function anonymize(string $ip): string
151+
{
152+
$wrappedIPv6 = false;
153+
if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) {
154+
$wrappedIPv6 = true;
155+
$ip = substr($ip, 1, -1);
156+
}
157+
158+
$packedAddress = inet_pton($ip);
159+
if (4 === \strlen($packedAddress)) {
160+
$mask = '255.255.255.0';
161+
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) {
162+
$mask = '::ffff:ffff:ff00';
163+
} elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) {
164+
$mask = '::ffff:ff00';
165+
} else {
166+
$mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000';
167+
}
168+
$ip = inet_ntop($packedAddress & inet_pton($mask));
169+
170+
if ($wrappedIPv6) {
171+
$ip = '['.$ip.']';
172+
}
173+
174+
return $ip;
175+
}
176+
}

tests/Datasets/Ip.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
dataset('valid-ipv4', [
4+
'8.8.8.8',
5+
'10.10.10.10',
6+
'192.168.20.13',
7+
'192.168.1.1',
8+
'1.2.3.4',
9+
]);
10+
11+
dataset('private-ipv4',[
12+
'10.10.10.10',
13+
'192.168.20.13',
14+
]);
15+
16+
dataset('public-ipv4',[
17+
'1.2.3.4',
18+
'8.8.8.8',
19+
'5.11.11.5',
20+
'9.9.9.9',
21+
]);
22+

tests/IPAddressTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use SamuelMwangiW\Linode\DTO\IPAddress;
4+
5+
it('returns true for ipv4 addresses', function (string $ip) {
6+
$IpAddressObj = new IPAddress($ip);
7+
8+
expect($IpAddressObj)
9+
->ip->toBe($ip)
10+
->isV4()->toBeTrue()
11+
->isV6()->toBeFalse();
12+
})->with('valid-ipv4');
13+
14+
it('returns true for private ipv4 addresses', function (string $ip) {
15+
$IpAddressObj = new IPAddress($ip);
16+
17+
expect($IpAddressObj)
18+
->ip->toBe($ip)
19+
->isV4()->toBeTrue()
20+
->isPrivate()->toBeTrue()
21+
->isPublic()->toBeFalse();
22+
})->with('private-ipv4');
23+
24+
it('returns true for public ipv4 addresses', function (string $ip) {
25+
$IpAddressObj = new IPAddress($ip);
26+
27+
expect($IpAddressObj)
28+
->ip->toBe($ip)
29+
->isV4()->toBeTrue()
30+
->isPublic()->toBeTrue()
31+
->isPrivate()->toBeFalse();
32+
})->with('public-ipv4');

0 commit comments

Comments
 (0)