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