1<?php
2
3namespace IPLib\Address;
4
5use IPLib\ParseStringFlag;
6use IPLib\Range\RangeInterface;
7use IPLib\Range\Subnet;
8use IPLib\Range\Type as RangeType;
9
10/**
11 * An IPv6 address.
12 */
13class IPv6 implements AddressInterface
14{
15    /**
16     * The long string representation of the address.
17     *
18     * @var string
19     *
20     * @example '0000:0000:0000:0000:0000:0000:0000:0001'
21     */
22    protected $longAddress;
23
24    /**
25     * The long string representation of the address.
26     *
27     * @var string|null
28     *
29     * @example '::1'
30     */
31    protected $shortAddress;
32
33    /**
34     * The byte list of the IP address.
35     *
36     * @var int[]|null
37     */
38    protected $bytes;
39
40    /**
41     * The word list of the IP address.
42     *
43     * @var int[]|null
44     */
45    protected $words;
46
47    /**
48     * The type of the range of this IP address.
49     *
50     * @var int|null
51     */
52    protected $rangeType;
53
54    /**
55     * An array containing RFC designated address ranges.
56     *
57     * @var array|null
58     */
59    private static $reservedRanges;
60
61    /**
62     * Initializes the instance.
63     *
64     * @param string $longAddress
65     */
66    public function __construct($longAddress)
67    {
68        $this->longAddress = $longAddress;
69        $this->shortAddress = null;
70        $this->bytes = null;
71        $this->words = null;
72        $this->rangeType = null;
73    }
74
75    /**
76     * {@inheritdoc}
77     *
78     * @see \IPLib\Address\AddressInterface::__toString()
79     */
80    public function __toString()
81    {
82        return $this->toString();
83    }
84
85    /**
86     * {@inheritdoc}
87     *
88     * @see \IPLib\Address\AddressInterface::getNumberOfBits()
89     */
90    public static function getNumberOfBits()
91    {
92        return 128;
93    }
94
95    /**
96     * @deprecated since 1.17.0: use the parseString() method instead.
97     * For upgrading:
98     * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
99     * - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag
100     *
101     * @param string|mixed $address
102     * @param bool $mayIncludePort
103     * @param bool $mayIncludeZoneID
104     *
105     * @return static|null
106     *
107     * @see \IPLib\Address\IPv6::parseString()
108     * @since 1.1.0 added the $mayIncludePort argument
109     * @since 1.3.0 added the $mayIncludeZoneID argument
110     */
111    public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
112    {
113        return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0));
114    }
115
116    /**
117     * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
118     *
119     * @param string|mixed $address the address to parse
120     * @param int $flags A combination or zero or more flags
121     *
122     * @return static|null
123     *
124     * @see \IPLib\ParseStringFlag
125     * @since 1.17.0
126     */
127    public static function parseString($address, $flags = 0)
128    {
129        $flags = (int) $flags;
130        $result = null;
131        if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
132            $matches = null;
133            if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) {
134                $address = $matches[1];
135            }
136            if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) {
137                $percentagePos = strpos($address, '%');
138                if ($percentagePos > 0) {
139                    $address = substr($address, 0, $percentagePos);
140                }
141            }
142            if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
143                $address6 = static::parseString($matches[1] . '0:0');
144                if ($address6 !== null) {
145                    $address4 = IPv4::parseString($matches[2]);
146                    if ($address4 !== null) {
147                        $bytes4 = $address4->getBytes();
148                        $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
149                        $result = $address6;
150                    }
151                }
152            } else {
153                if (strpos($address, '::') === false) {
154                    $chunks = explode(':', $address);
155                } else {
156                    $chunks = array();
157                    $parts = explode('::', $address);
158                    if (count($parts) === 2) {
159                        $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
160                        $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
161                        $missing = 8 - count($before) - count($after);
162                        if ($missing >= 0) {
163                            $chunks = $before;
164                            if ($missing !== 0) {
165                                $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
166                            }
167                            $chunks = array_merge($chunks, $after);
168                        }
169                    }
170                }
171                if (count($chunks) === 8) {
172                    $nums = array_map(
173                        function ($chunk) {
174                            return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
175                        },
176                        $chunks
177                    );
178                    if (!in_array(false, $nums, true)) {
179                        $longAddress = implode(
180                            ':',
181                            array_map(
182                                function ($num) {
183                                    return sprintf('%04x', $num);
184                                },
185                                $nums
186                            )
187                        );
188                        $result = new static($longAddress);
189                    }
190                }
191            }
192        }
193
194        return $result;
195    }
196
197    /**
198     * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
199     *
200     * @param int[]|array $bytes
201     *
202     * @return static|null
203     */
204    public static function fromBytes(array $bytes)
205    {
206        $result = null;
207        if (count($bytes) === 16) {
208            $address = '';
209            for ($i = 0; $i < 16; $i++) {
210                if ($i !== 0 && $i % 2 === 0) {
211                    $address .= ':';
212                }
213                $byte = $bytes[$i];
214                if (is_int($byte) && $byte >= 0 && $byte <= 255) {
215                    $address .= sprintf('%02x', $byte);
216                } else {
217                    $address = null;
218                    break;
219                }
220            }
221            if ($address !== null) {
222                $result = new static($address);
223            }
224        }
225
226        return $result;
227    }
228
229    /**
230     * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
231     *
232     * @param int[]|array $words
233     *
234     * @return static|null
235     */
236    public static function fromWords(array $words)
237    {
238        $result = null;
239        if (count($words) === 8) {
240            $chunks = array();
241            for ($i = 0; $i < 8; $i++) {
242                $word = $words[$i];
243                if (is_int($word) && $word >= 0 && $word <= 0xffff) {
244                    $chunks[] = sprintf('%04x', $word);
245                } else {
246                    $chunks = null;
247                    break;
248                }
249            }
250            if ($chunks !== null) {
251                $result = new static(implode(':', $chunks));
252            }
253        }
254
255        return $result;
256    }
257
258    /**
259     * {@inheritdoc}
260     *
261     * @see \IPLib\Address\AddressInterface::toString()
262     */
263    public function toString($long = false)
264    {
265        if ($long) {
266            $result = $this->longAddress;
267        } else {
268            if ($this->shortAddress === null) {
269                if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
270                    $lastBytes = array_slice($this->getBytes(), -4);
271                    $this->shortAddress = '::ffff:' . implode('.', $lastBytes);
272                } else {
273                    $chunks = array_map(
274                        function ($word) {
275                            return dechex($word);
276                        },
277                        $this->getWords()
278                    );
279                    $shortAddress = implode(':', $chunks);
280                    $matches = null;
281                    for ($i = 8; $i > 1; $i--) {
282                        $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
283                        if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
284                            $shortAddress = $matches[1] . '::' . $matches[2];
285                            break;
286                        }
287                    }
288                    $this->shortAddress = $shortAddress;
289                }
290            }
291            $result = $this->shortAddress;
292        }
293
294        return $result;
295    }
296
297    /**
298     * {@inheritdoc}
299     *
300     * @see \IPLib\Address\AddressInterface::getBytes()
301     */
302    public function getBytes()
303    {
304        if ($this->bytes === null) {
305            $bytes = array();
306            foreach ($this->getWords() as $word) {
307                $bytes[] = $word >> 8;
308                $bytes[] = $word & 0xff;
309            }
310            $this->bytes = $bytes;
311        }
312
313        return $this->bytes;
314    }
315
316    /**
317     * {@inheritdoc}
318     *
319     * @see \IPLib\Address\AddressInterface::getBits()
320     */
321    public function getBits()
322    {
323        $parts = array();
324        foreach ($this->getBytes() as $byte) {
325            $parts[] = sprintf('%08b', $byte);
326        }
327
328        return implode('', $parts);
329    }
330
331    /**
332     * Get the word list of the IP address.
333     *
334     * @return int[]
335     */
336    public function getWords()
337    {
338        if ($this->words === null) {
339            $this->words = array_map(
340                function ($chunk) {
341                    return hexdec($chunk);
342                },
343                explode(':', $this->longAddress)
344            );
345        }
346
347        return $this->words;
348    }
349
350    /**
351     * {@inheritdoc}
352     *
353     * @see \IPLib\Address\AddressInterface::getAddressType()
354     */
355    public function getAddressType()
356    {
357        return Type::T_IPv6;
358    }
359
360    /**
361     * {@inheritdoc}
362     *
363     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
364     */
365    public static function getDefaultReservedRangeType()
366    {
367        return RangeType::T_RESERVED;
368    }
369
370    /**
371     * {@inheritdoc}
372     *
373     * @see \IPLib\Address\AddressInterface::getReservedRanges()
374     */
375    public static function getReservedRanges()
376    {
377        if (self::$reservedRanges === null) {
378            $reservedRanges = array();
379            foreach (array(
380                // RFC 4291
381                '::/128' => array(RangeType::T_UNSPECIFIED),
382                // RFC 4291
383                '::1/128' => array(RangeType::T_LOOPBACK),
384                // RFC 4291
385                '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
386                //'2002::/16' => array(RangeType::),
387                // RFC 4291
388                '2000::/3' => array(RangeType::T_PUBLIC),
389                // RFC 4193
390                'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
391                // RFC 4291
392                'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
393                // RFC 4291
394                'ff00::/8' => array(RangeType::T_MULTICAST),
395                // RFC 4291
396                //'::/8' => array(RangeType::T_RESERVED),
397                // RFC 4048
398                //'200::/7' => array(RangeType::T_RESERVED),
399                // RFC 4291
400                //'400::/6' => array(RangeType::T_RESERVED),
401                // RFC 4291
402                //'800::/5' => array(RangeType::T_RESERVED),
403                // RFC 4291
404                //'1000::/4' => array(RangeType::T_RESERVED),
405                // RFC 4291
406                //'4000::/3' => array(RangeType::T_RESERVED),
407                // RFC 4291
408                //'6000::/3' => array(RangeType::T_RESERVED),
409                // RFC 4291
410                //'8000::/3' => array(RangeType::T_RESERVED),
411                // RFC 4291
412                //'a000::/3' => array(RangeType::T_RESERVED),
413                // RFC 4291
414                //'c000::/3' => array(RangeType::T_RESERVED),
415                // RFC 4291
416                //'e000::/4' => array(RangeType::T_RESERVED),
417                // RFC 4291
418                //'f000::/5' => array(RangeType::T_RESERVED),
419                // RFC 4291
420                //'f800::/6' => array(RangeType::T_RESERVED),
421                // RFC 4291
422                //'fe00::/9' => array(RangeType::T_RESERVED),
423                // RFC 3879
424                //'fec0::/10' => array(RangeType::T_RESERVED),
425            ) as $range => $data) {
426                $exceptions = array();
427                if (isset($data[1])) {
428                    foreach ($data[1] as $exceptionRange => $exceptionType) {
429                        $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType);
430                    }
431                }
432                $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions);
433            }
434            self::$reservedRanges = $reservedRanges;
435        }
436
437        return self::$reservedRanges;
438    }
439
440    /**
441     * {@inheritdoc}
442     *
443     * @see \IPLib\Address\AddressInterface::getRangeType()
444     */
445    public function getRangeType()
446    {
447        if ($this->rangeType === null) {
448            $ipv4 = $this->toIPv4();
449            if ($ipv4 !== null) {
450                $this->rangeType = $ipv4->getRangeType();
451            } else {
452                $rangeType = null;
453                foreach (static::getReservedRanges() as $reservedRange) {
454                    $rangeType = $reservedRange->getAddressType($this);
455                    if ($rangeType !== null) {
456                        break;
457                    }
458                }
459                $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
460            }
461        }
462
463        return $this->rangeType;
464    }
465
466    /**
467     * Create an IPv4 representation of this address (if possible, otherwise returns null).
468     *
469     * @return \IPLib\Address\IPv4|null
470     */
471    public function toIPv4()
472    {
473        if (strpos($this->longAddress, '2002:') === 0) {
474            // 6to4
475            return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
476        }
477        if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
478            // IPv4-mapped IPv6 addresses
479            return IPv4::fromBytes(array_slice($this->getBytes(), -4));
480        }
481
482        return null;
483    }
484
485    /**
486     * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
487     *
488     * @param bool $ipV6Long render the IPv6 part in "long" format?
489     * @param bool $ipV4Long render the IPv4 part in "long" format?
490     *
491     * @return string
492     *
493     * @example '::13.1.68.3'
494     * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
495     * @example '::013.001.068.003' when $ipV4Long is true
496     * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
497     *
498     * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
499     * @since 1.9.0
500     */
501    public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
502    {
503        $myBytes = $this->getBytes();
504        $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
505        $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long);
506        $ipv4Bytes = array_slice($myBytes, 12, 4);
507        $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long);
508
509        return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
510    }
511
512    /**
513     * {@inheritdoc}
514     *
515     * @see \IPLib\Address\AddressInterface::getComparableString()
516     */
517    public function getComparableString()
518    {
519        return $this->longAddress;
520    }
521
522    /**
523     * {@inheritdoc}
524     *
525     * @see \IPLib\Address\AddressInterface::matches()
526     */
527    public function matches(RangeInterface $range)
528    {
529        return $range->contains($this);
530    }
531
532    /**
533     * {@inheritdoc}
534     *
535     * @see \IPLib\Address\AddressInterface::getAddressAtOffset()
536     */
537    public function getAddressAtOffset($n)
538    {
539        if (!is_int($n)) {
540            return null;
541        }
542
543        $boundary = 0x10000;
544        $mod = $n;
545        $words = $this->getWords();
546        for ($i = count($words) - 1; $i >= 0; $i--) {
547            $tmp = ($words[$i] + $mod) % $boundary;
548            $mod = (int) floor(($words[$i] + $mod) / $boundary);
549            if ($tmp < 0) {
550                $tmp += $boundary;
551            }
552
553            $words[$i] = $tmp;
554        }
555
556        if ($mod !== 0) {
557            return null;
558        }
559
560        return static::fromWords($words);
561    }
562
563    /**
564     * {@inheritdoc}
565     *
566     * @see \IPLib\Address\AddressInterface::getNextAddress()
567     */
568    public function getNextAddress()
569    {
570        return $this->getAddressAtOffset(1);
571    }
572
573    /**
574     * {@inheritdoc}
575     *
576     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
577     */
578    public function getPreviousAddress()
579    {
580        return $this->getAddressAtOffset(-1);
581    }
582
583    /**
584     * {@inheritdoc}
585     *
586     * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
587     */
588    public function getReverseDNSLookupName()
589    {
590        return implode(
591            '.',
592            array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
593        ) . '.ip6.arpa';
594    }
595}
596