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 IPv4 address. 11 */ 12class IPv4 implements AddressInterface 13{ 14 /** 15 * The string representation of the address. 16 * 17 * @var string 18 * 19 * @example '127.0.0.1' 20 */ 21 protected $address; 22 23 /** 24 * The byte list of the IP address. 25 * 26 * @var int[]|null 27 */ 28 protected $bytes; 29 30 /** 31 * The type of the range of this IP address. 32 * 33 * @var int|null 34 */ 35 protected $rangeType; 36 37 /** 38 * A string representation of this address than can be used when comparing addresses and ranges. 39 * 40 * @var string 41 */ 42 protected $comparableString; 43 44 /** 45 * An array containing RFC designated address ranges. 46 * 47 * @var array|null 48 */ 49 private static $reservedRanges = null; 50 51 /** 52 * Initializes the instance. 53 * 54 * @param string $address 55 */ 56 protected function __construct($address) 57 { 58 $this->address = $address; 59 $this->bytes = null; 60 $this->rangeType = null; 61 $this->comparableString = null; 62 } 63 64 /** 65 * Parse a string and returns an IPv4 instance if the string is valid, or null otherwise. 66 * 67 * @param string|mixed $address the address to parse 68 * @param bool $mayIncludePort set to false to avoid parsing addresses with ports 69 * 70 * @return static|null 71 */ 72 public static function fromString($address, $mayIncludePort = true) 73 { 74 $result = null; 75 if (is_string($address) && strpos($address, '.')) { 76 $rx = '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})'; 77 if ($mayIncludePort) { 78 $rx .= '(?::\d+)?'; 79 } 80 $matches = null; 81 if (preg_match('/^'.$rx.'$/', $address, $matches)) { 82 $ok = true; 83 $nums = array(); 84 for ($i = 1; $ok && $i <= 4; ++$i) { 85 $ok = false; 86 $n = (int) $matches[$i]; 87 if ($n >= 0 && $n <= 255) { 88 $ok = true; 89 $nums[] = (string) $n; 90 } 91 } 92 if ($ok) { 93 $result = new static(implode('.', $nums)); 94 } 95 } 96 } 97 98 return $result; 99 } 100 101 /** 102 * Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise. 103 * 104 * @param int[]|array $bytes 105 * 106 * @return static|null 107 */ 108 public static function fromBytes(array $bytes) 109 { 110 $result = null; 111 if (count($bytes) === 4) { 112 $chunks = array_map( 113 function ($byte) { 114 return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false; 115 }, 116 $bytes 117 ); 118 if (in_array(false, $chunks, true) === false) { 119 $result = new static(implode('.', $chunks)); 120 } 121 } 122 123 return $result; 124 } 125 126 /** 127 * {@inheritdoc} 128 * 129 * @see \IPLib\Address\AddressInterface::toString() 130 */ 131 public function toString($long = false) 132 { 133 if ($long) { 134 return $this->getComparableString(); 135 } 136 137 return $this->address; 138 } 139 140 /** 141 * {@inheritdoc} 142 * 143 * @see \IPLib\Address\AddressInterface::__toString() 144 */ 145 public function __toString() 146 { 147 return $this->address; 148 } 149 150 /** 151 * {@inheritdoc} 152 * 153 * @see \IPLib\Address\AddressInterface::getBytes() 154 */ 155 public function getBytes() 156 { 157 if ($this->bytes === null) { 158 $this->bytes = array_map( 159 function ($chunk) { 160 return (int) $chunk; 161 }, 162 explode('.', $this->address) 163 ); 164 } 165 166 return $this->bytes; 167 } 168 169 /** 170 * {@inheritdoc} 171 * 172 * @see \IPLib\Address\AddressInterface::getAddressType() 173 */ 174 public function getAddressType() 175 { 176 return Type::T_IPv4; 177 } 178 179 /** 180 * {@inheritdoc} 181 * 182 * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType() 183 */ 184 public static function getDefaultReservedRangeType() 185 { 186 return RangeType::T_PUBLIC; 187 } 188 189 /** 190 * {@inheritdoc} 191 * 192 * @see \IPLib\Address\AddressInterface::getReservedRanges() 193 */ 194 public static function getReservedRanges() 195 { 196 if (self::$reservedRanges === null) { 197 $reservedRanges = array(); 198 foreach (array( 199 // RFC 5735 200 '0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)), 201 // RFC 5735 202 '10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK), 203 // RFC 5735 204 '127.0.0.0/8' => array(RangeType::T_LOOPBACK), 205 // RFC 5735 206 '169.254.0.0/16' => array(RangeType::T_LINKLOCAL), 207 // RFC 5735 208 '172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK), 209 // RFC 5735 210 '192.0.0.0/24' => array(RangeType::T_RESERVED), 211 // RFC 5735 212 '192.0.2.0/24' => array(RangeType::T_RESERVED), 213 // RFC 5735 214 '192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY), 215 // RFC 5735 216 '192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK), 217 // RFC 5735 218 '198.18.0.0/15' => array(RangeType::T_RESERVED), 219 // RFC 5735 220 '198.51.100.0/24' => array(RangeType::T_RESERVED), 221 // RFC 5735 222 '203.0.113.0/24' => array(RangeType::T_RESERVED), 223 // RFC 5735 224 '224.0.0.0/4' => array(RangeType::T_MULTICAST), 225 // RFC 5735 226 '240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)), 227 ) as $range => $data) { 228 $exceptions = array(); 229 if (isset($data[1])) { 230 foreach ($data[1] as $exceptionRange => $exceptionType) { 231 $exceptions[] = new AssignedRange(Subnet::fromString($exceptionRange), $exceptionType); 232 } 233 } 234 $reservedRanges[] = new AssignedRange(Subnet::fromString($range), $data[0], $exceptions); 235 } 236 self::$reservedRanges = $reservedRanges; 237 } 238 239 return self::$reservedRanges; 240 } 241 242 /** 243 * {@inheritdoc} 244 * 245 * @see \IPLib\Address\AddressInterface::getRangeType() 246 */ 247 public function getRangeType() 248 { 249 if ($this->rangeType === null) { 250 $rangeType = null; 251 foreach (static::getReservedRanges() as $reservedRange) { 252 $rangeType = $reservedRange->getAddressType($this); 253 if ($rangeType !== null) { 254 break; 255 } 256 } 257 $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType; 258 } 259 260 return $this->rangeType; 261 } 262 263 /** 264 * Create an IPv6 representation of this address. 265 * 266 * @return \IPLib\Address\IPv6 267 */ 268 public function toIPv6() 269 { 270 $myBytes = $this->getBytes(); 271 272 return IPv6::fromString('2002:'.sprintf('%02x', $myBytes[0]).sprintf('%02x', $myBytes[1]).':'.sprintf('%02x', $myBytes[2]).sprintf('%02x', $myBytes[3]).'::'); 273 } 274 275 /** 276 * {@inheritdoc} 277 * 278 * @see \IPLib\Address\AddressInterface::getComparableString() 279 */ 280 public function getComparableString() 281 { 282 if ($this->comparableString === null) { 283 $chunks = array(); 284 foreach ($this->getBytes() as $byte) { 285 $chunks[] = sprintf('%03d', $byte); 286 } 287 $this->comparableString = implode('.', $chunks); 288 } 289 290 return $this->comparableString; 291 } 292 293 /** 294 * {@inheritdoc} 295 * 296 * @see \IPLib\Address\AddressInterface::matches() 297 */ 298 public function matches(RangeInterface $range) 299 { 300 return $range->contains($this); 301 } 302 303 /** 304 * {@inheritdoc} 305 * 306 * @see \IPLib\Address\AddressInterface::getNextAddress() 307 */ 308 public function getNextAddress() 309 { 310 $overflow = false; 311 $bytes = $this->getBytes(); 312 for ($i = count($bytes) - 1; $i >= 0; --$i) { 313 if ($bytes[$i] === 255) { 314 if ($i === 0) { 315 $overflow = true; 316 break; 317 } 318 $bytes[$i] = 0; 319 } else { 320 ++$bytes[$i]; 321 break; 322 } 323 } 324 325 return $overflow ? null : static::fromBytes($bytes); 326 } 327 328 /** 329 * {@inheritdoc} 330 * 331 * @see \IPLib\Address\AddressInterface::getPreviousAddress() 332 */ 333 public function getPreviousAddress() 334 { 335 $overflow = false; 336 $bytes = $this->getBytes(); 337 for ($i = count($bytes) - 1; $i >= 0; --$i) { 338 if ($bytes[$i] === 0) { 339 if ($i === 0) { 340 $overflow = true; 341 break; 342 } 343 $bytes[$i] = 255; 344 } else { 345 --$bytes[$i]; 346 break; 347 } 348 } 349 350 return $overflow ? null : static::fromBytes($bytes); 351 } 352} 353