NetAddress::fromInt()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
use Ivory\Exception\NotImplementedException;
6
use Ivory\Value\Alg\IEqualable;
7
8
/**
9
 * Representation of an IPv4 or IPv6 host or network address.
10
 *
11
 * Both a host address and, optionally, its subnet may be represented in a single `NetAddress` object. Besides from
12
 * representing host addresses, `NetAddress` may also represent a network address.
13
 *
14
 * The objects are immutable.
15
 */
16
class NetAddress implements IEqualable
17
{
18
    private $addrStr;
19
    private $ipVersion;
20
    private $netmaskLength;
21
22
    /**
23
     * Creates a host or network address from its string representation, optionally accompanied by the netmask length.
24
     *
25
     * The CIDR (`address/y`) notation is automatically recognized in `$addr` and, if detected, the
26
     * `$netmaskLengthOrNetmask` argument gets ignored (a user warning is emitted if explicitly given anyway).
27
     *
28
     * @param string $addr the address, e.g., <tt>192.168.0.1</tt> or <tt>2001:4f8:3:ba::</tt>
29
     * @param int|string $netmaskLengthOrNetmask number of bits to take from <tt>$addr</tt> as the network prefix, or an
30
     *                                             IPv4 netmask, e.g., <tt>255.255.255.240</tt> being equivalent to 28
31
     * @return NetAddress
32
     */
33
    public static function fromString(string $addr, $netmaskLengthOrNetmask = null): NetAddress
34
    {
35
        if (strpos($addr, '/') !== false) {
36
            if ($netmaskLengthOrNetmask !== null) {
37
                trigger_error('Ignoring the netmask-related argument - a CIDR notation detected', E_USER_WARNING);
38
            }
39
            return self::fromCidrString($addr);
40
        }
41
42
        if (filter_var($addr, FILTER_VALIDATE_IP) === false) {
43
            throw new \InvalidArgumentException('Invalid IP address');
44
        }
45
46
        return new NetAddress($addr, $netmaskLengthOrNetmask);
47
    }
48
49
    /**
50
     * Creates a host or network address from its CIDR notation.
51
     *
52
     * @param string $cidrAddr the address followed by a slash and the netmask length, e.g., <tt>192.168.0.3/24</tt>
53
     * @return NetAddress
54
     */
55
    public static function fromCidrString(string $cidrAddr): NetAddress
56
    {
57
        $sp = strrpos($cidrAddr, '/');
58
        if ($sp === false) {
59
            throw new \InvalidArgumentException('$cidrAddr');
60
        }
61
62
        $addr = substr($cidrAddr, 0, $sp);
63
        $netmaskLen = substr($cidrAddr, $sp + 1);
64
        if (strpos($addr, '/') !== false) {
65
            throw new \InvalidArgumentException('$cidrAddr');
66
        }
67
68
        return self::fromString($addr, $netmaskLen);
69
    }
70
71
    /**
72
     * Creates a host or network address from a binary string, also referred to as the "packed in_addr representation".
73
     *
74
     * E.g., the four-byte `"\x7f\x00\x00\x01"` binary string results in the address `127.0.0.1`.
75
     *
76
     * @param string $bytes binary string
77
     * @param int|string $netmaskLengthOrNetmask number of bits to take from <tt>$addr</tt> as the network prefix, or an
78
     *                                             IPv4 netmask, e.g., <tt>255.255.255.240</tt> being equivalent to 28
79
     * @return NetAddress
80
     */
81
    public static function fromByteString(string $bytes, $netmaskLengthOrNetmask = null): NetAddress
82
    {
83
        if (strlen($bytes) > 4 && !self::ipv6Support()) {
84
            throw new NotImplementedException('PHP must be compiled with IPv6 support');
85
        }
86
87
        $addrStr = @inet_ntop($bytes);
88
        if ($addrStr === false) {
89
            throw new \InvalidArgumentException('$bytes');
90
        }
91
92
        return new NetAddress($addrStr, $netmaskLengthOrNetmask);
93
    }
94
95
    /**
96
     * Creates an IPv4 address from its representation in an integer.
97
     *
98
     * @param int $ipv4Addr a proper IPv4 address representation; the same as what {@link long2ip()} accepts
99
     * @param int|string $netmaskLengthOrNetmask number of bits to take from <tt>$addr</tt> as the network prefix, or an
100
     *                                             IPv4 netmask, e.g., <tt>255.255.255.240</tt> being equivalent to 28
101
     * @return NetAddress
102
     */
103
    public static function fromInt(int $ipv4Addr, $netmaskLengthOrNetmask = null): NetAddress
104
    {
105
        $addrStr = @long2ip($ipv4Addr); // @: a warning would be issued when passed, e.g., an array
106
        if ($addrStr === null) {
107
            throw new \InvalidArgumentException('$ipv4Addr');
108
        }
109
110
        return new NetAddress($addrStr, $netmaskLengthOrNetmask);
0 ignored issues
show
Bug introduced by
It seems like $addrStr can also be of type false; however, parameter $addrStr of Ivory\Value\NetAddress::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        return new NetAddress(/** @scrutinizer ignore-type */ $addrStr, $netmaskLengthOrNetmask);
Loading history...
111
    }
112
113
    /**
114
     * @return bool whether the PHP was built with IPv6 support enabled
115
     */
116
    private static function ipv6Support(): bool
117
    {
118
        static $cached = null;
119
        if ($cached === null) {
120
            ob_start();
121
            phpinfo(INFO_GENERAL);
122
            $info = ob_get_clean();
123
            $cached = (strpos($info, 'IPv6 Support => enabled') !== false);
124
        }
125
        return $cached;
126
    }
127
128
129
    private function __construct(string $addrStr, $netmaskLengthOrNetmask = null)
130
    {
131
        $this->addrStr = $addrStr;
132
        $this->ipVersion = (strpos($addrStr, ':') !== false ? 6 : 4);
133
134
        if ($netmaskLengthOrNetmask === null) {
135
            $this->netmaskLength = ($this->ipVersion == 6 ? 128 : 32);
136
        } else {
137
            if (filter_var($netmaskLengthOrNetmask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
138
                if ($this->ipVersion == 4) {
139
                    // taken from http://php.net/manual/en/function.ip2long.php#94787
140
                    $netmask = ip2long($netmaskLengthOrNetmask);
141
                    $base = ip2long('255.255.255.255');
142
                    $bits = log(($netmask ^ $base) + 1, 2);
143
                    if (abs($bits - (int)$bits) > 1e-9) {
144
                        throw new \InvalidArgumentException('Invalid netmask');
145
                    }
146
                    $this->netmaskLength = 32 - (int)$bits;
147
                } else {
148
                    throw new \InvalidArgumentException('Netmask may only be used for an IPv4 address.');
149
                }
150
            } else {
151
                $this->netmaskLength = filter_var($netmaskLengthOrNetmask, FILTER_VALIDATE_INT);
152
                if ($this->netmaskLength === false) {
153
                    throw new \InvalidArgumentException('$netmaskLengthOrNetmask');
154
                }
155
                $maxLen = ($this->ipVersion == 6 ? 128 : 32);
156
                if ($this->netmaskLength < 0 || $this->netmaskLength > $maxLen) {
157
                    throw new \OutOfBoundsException(
158
                        "Netmask length must be in range <0,$maxLen> for IPv{$this->ipVersion} addresses"
159
                    );
160
                }
161
            }
162
        }
163
    }
164
165
166
    /**
167
     * @return string the represented address
168
     */
169
    public function getAddressString(): string
170
    {
171
        return $this->addrStr;
172
    }
173
174
    /**
175
     * @return int number of bits used from the address as the network prefix
176
     */
177
    public function getNetmaskLength(): int
178
    {
179
        return $this->netmaskLength;
180
    }
181
182
    /**
183
     * @return int the IP version of this address
184
     */
185
    public function getIpVersion(): int
186
    {
187
        return $this->ipVersion;
188
    }
189
190
    /**
191
     * Expands the (IPv6) address to its full explicit form.
192
     *
193
     * There are several rules for abbreviating IPv6 addresses:
194
     * * leading zeros may be omitted in each group of four hexadecimal digits;
195
     * * one or more consecutive groups of 16 bits of zero may be left out;
196
     * * an IPv4-shaped address (dot-separated) may be used instead of the last two groups of hexadecimal digits.
197
     *
198
     * An address abbreviated using these rules gets expanded to the equivalent, fully explicit form, so the resulting
199
     * string is in the "x:x:x:x:x:x:x:x" format, where each "x" stands for four hexadecimal digits.
200
     *
201
     * Moreover, letters get converted to lowercase.
202
     *
203
     * @return string the IPv6 address expanded to its full explicit form; or the IPv4 address, as is
204
     */
205
    public function getExpandedAddress(): string
206
    {
207
        if ($this->ipVersion == 4) {
208
            return $this->addrStr;
209
        } else {
210
            if (!self::ipv6Support()) {
211
                throw new NotImplementedException('PHP must be compiled with IPv6 support');
212
            }
213
            $hexdigits = unpack('H*', inet_pton($this->addrStr))[1];
214
            $result = substr($hexdigits, 0, 4);
215
            for ($i = 1; $i < 8; $i++) {
216
                $result .= ':' . substr($hexdigits, $i * 4, 4);
217
            }
218
            return $result;
219
        }
220
    }
221
222
    /**
223
     * Returns the (IPv6) address in the canonical form, as described in RFC 5952.
224
     *
225
     * The rules for canonization are as follows:
226
     * * leading zeros must be suppressed (a single 16-bit zero field must be represented as 0, though);
227
     * * the "::" symbol must be used to shorten the address as much as possible, it must not be used to shorten just
228
     *   one 16-bit 0 field, and in case of a tie, it must shorten the first sequence of zero bits;
229
     * * the letters A-F must be represented in lowercase.
230
     *
231
     * There are some optional rules for embedded IPv4 addresses, using the dot decimal notation. These are not
232
     * implemented by this method, though. That is, the dot decimal notation is never used in the output of this method.
233
     *
234
     * @return string the IPv6 address canonized according to RFC 5952; or the IPv4 address, as is
235
     */
236
    public function getCanonicalAddress(): string
237
    {
238
        if ($this->ipVersion == 4) {
239
            return $this->addrStr;
240
        }
241
242
        $fields = explode(':', $this->getExpandedAddress());
243
        $fields[] = ''; // auxiliary sentinel
244
245
        // compute the longest zero sequence
246
        $lzsStart = -1;
247
        $lzsLen = 1; // so that only sequences of length at least 2 are considered
248
        $czsStart = null;
249
        $czsLen = 0;
250
        foreach ($fields as $i => $field) {
251
            if ($field === '0000') {
252
                if ($czsStart === null) {
253
                    $czsStart = $i;
254
                    $czsLen = 1;
255
                } else {
256
                    $czsLen++;
257
                }
258
            } else {
259
                if ($czsStart !== null) {
260
                    if ($czsLen > $lzsLen) {
261
                        $lzsStart = $czsStart;
262
                        $lzsLen = $czsLen;
263
                    }
264
                    $czsStart = null;
265
                }
266
            }
267
        }
268
269
        $result = '';
270
        if ($lzsStart == 0) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lzsStart of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
271
            $result .= ':';
272
        }
273
        for ($i = 0; $i < 8; $i++) {
274
            if ($i > 0) {
275
                $result .= ':';
276
            }
277
            if ($i == $lzsStart) {
278
                $i += $lzsLen - 1;
279
                continue;
280
            }
281
            $result .= (ltrim($fields[$i], '0') ? : '0');
282
        }
283
        if ($lzsStart + $lzsLen == 8) {
284
            $result .= ':';
285
        }
286
        return $result;
287
    }
288
289
    /**
290
     * @return bool whether this address represents just a single host address, without any subnet specification
291
     */
292
    public function isSingleHost(): bool
293
    {
294
        if ($this->ipVersion == 6) {
295
            return ($this->netmaskLength == 128);
296
        } else {
297
            return ($this->netmaskLength == 32);
298
        }
299
    }
300
301
    /**
302
     * @return bool whether this address represents a network, which is iff all the host number bits are zero;
303
     *              e.g., <tt>127.0.49.0/24</tt> represents a network, while <tt>127.0.49.0/23</tt> does not
304
     */
305
    public function isNetwork(): bool
306
    {
307
        if ($this->ipVersion == 4) {
308
            $hostPartLen = 32 - $this->netmaskLength;
309
            $mask = (1 << $hostPartLen) - 1;
310
            return ((ip2long($this->addrStr) & $mask) == 0);
311
        } else {
312
            $hostPartLen = 128 - $this->netmaskLength;
313
            $hostPartOctets = (int)floor($hostPartLen / 8);
314
            $hostPartLeadBits = $hostPartLen % 8;
315
            $bs = $this->toByteString();
316
            for ($i = 15; $i >= 16 - $hostPartOctets; $i--) {
317
                if (ord($bs[$i]) != 0) {
318
                    return false;
319
                }
320
            }
321
            if ($hostPartLeadBits > 0) {
322
                $leadByte = ord($bs[$i]);
323
                $mask = (1 << $hostPartLeadBits) - 1;
324
                if (($leadByte & $mask) != 0) {
325
                    return false;
326
                }
327
            }
328
            return true;
329
        }
330
    }
331
332
    /**
333
     * Finds out whether this address is equal to the given one.
334
     *
335
     * Two addresses are considered as equal iff they are of the same IP version, the same netmask, and the same
336
     * expanded address.
337
     *
338
     * @param NetAddress|string $other a {@link NetAddress} or anything {@link NetAddress::fromString()} accepts as
339
     *                                     its first argument
340
     * @return bool
341
     */
342
    public function equals($other): bool
343
    {
344
        if ($other === null) {
345
            return false;
346
        }
347
        if (!$other instanceof NetAddress) {
348
            $other = NetAddress::fromString($other);
349
        }
350
351
        return (
352
            $this->ipVersion == $other->ipVersion &&
353
            $this->netmaskLength == $other->netmaskLength &&
354
            $this->getExpandedAddress() == $other->getExpandedAddress()
355
        );
356
    }
357
358
    /**
359
     * Finds out whether this network contains a host address or a whole given network as a subnetwork.
360
     *
361
     * Note that, in conformance with the standard PostgreSQL `>>` operator, equal networks are not considered as
362
     * containing each other, i.e., this method is a test for strict containment. To also permit equality, use
363
     * {@link NetAddress::containsOrEquals()}.
364
     *
365
     * For addresses of different IP version, `false` is always returned.
366
     *
367
     * @param NetAddress|string $address a {@link NetAddress} or anything {@link NetAddress::fromString()} accepts as
368
     *                                     its first argument
369
     * @return bool <tt>true</tt> if <tt>$address</tt> is strictly contained in this address, <tt>false</tt> otherwise
370
     *              (especially in case both networks are the same, or if this is actually a single host, not a network)
371
     */
372
    public function contains($address): bool
373
    {
374
        if (!$address instanceof NetAddress) {
375
            $address = NetAddress::fromString($address);
376
        }
377
378
        if ($this->ipVersion != $address->ipVersion) {
379
            return false;
380
        }
381
        if ($this->netmaskLength >= $address->netmaskLength) {
382
            return false;
383
        }
384
385
        // now, the first $this->netmaskLength bits must match, and that's it
386
        $octets = (int)floor($this->netmaskLength / 8);
387
        $remBits = $this->netmaskLength % 8;
388
389
        $thisBS = $this->toByteString();
390
        $addrBS = $address->toByteString();
391
        if (strncmp($thisBS, $addrBS, $octets) != 0) {
392
            return false;
393
        }
394
395
        if ($remBits > 0) {
396
            $thisByte = ord($thisBS[$octets]);
397
            $addrByte = ord($addrBS[$octets]);
398
            $mask = ord("\xff") << (8 - $remBits);
399
            if ((($thisByte ^ $addrByte) & $mask) != 0) {
400
                return false;
401
            }
402
        }
403
404
        return true;
405
    }
406
407
    /**
408
     * @param NetAddress|string $address
409
     * @return bool
410
     */
411
    public function containsOrEquals($address): bool
412
    {
413
        return ($this->equals($address) || $this->contains($address));
414
    }
415
416
    /**
417
     * Returns the (human-readable) string representation of the address.
418
     *
419
     * If only a single host is represented by this address, just the host address. On the contrary, if a network is
420
     * represented by this address, the CIDR representation is used.
421
     *
422
     * @return string the (human-readable) string representation of the address
423
     */
424
    public function toString(): string
425
    {
426
        if ($this->isSingleHost()) {
427
            return $this->addrStr;
428
        } else {
429
            return $this->toCidrString();
430
        }
431
    }
432
433
    /**
434
     * @return string the CIDR representation of this address, as defined in RFC 4632, e.g., <tt>"123.8.9.1/26"</tt>
435
     */
436
    public function toCidrString(): string
437
    {
438
        return $this->addrStr . '/' . $this->netmaskLength;
439
    }
440
441
    /**
442
     * Packs the address to a binary string, also referred to as the "packed in_addr representation".
443
     *
444
     * E.g., the address `127.0.0.1` results in the four-byte `"\x7f\x00\x00\x01"` binary string.
445
     *
446
     * @return string the address packed to a binary string
447
     */
448
    public function toByteString(): string
449
    {
450
        if ($this->ipVersion == 4 || self::ipv6Support()) {
451
            return inet_pton($this->addrStr);
452
        } else {
453
            throw new NotImplementedException('PHP must be compiled with IPv6 support');
454
        }
455
    }
456
457
    /**
458
     * Packs the IPv4 address to an integer.
459
     *
460
     * Note that results may vary depending on the platform: on a 32-bit PHP, the highest bit set yields a negative
461
     * integer, whereas on a 64-bit PHP, any value returned by this method is a non-negative integer.
462
     *
463
     * @return int the address represented as an integer
464
     * @throws \LogicException when called on an IPv6 address, as such an address does not fit into the integer type
465
     */
466
    public function toInt(): int
467
    {
468
        if ($this->ipVersion == 4) {
469
            return ip2long($this->addrStr);
470
        } else {
471
            throw new \LogicException('IPv6 address cannot be converted to long');
472
        }
473
    }
474
475
    public function __toString()
476
    {
477
        return $this->toString();
478
    }
479
}
480