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