1<?php 2 3namespace IPLib\Range; 4 5use IPLib\Address\AddressInterface; 6use IPLib\Address\IPv4; 7use IPLib\Address\IPv6; 8use IPLib\Address\Type as AddressType; 9use IPLib\Factory; 10 11/** 12 * Represents an address range in subnet format (eg CIDR). 13 * 14 * @example 127.0.0.1/32 15 * @example ::/8 16 */ 17class Subnet implements RangeInterface 18{ 19 /** 20 * Starting address of the range. 21 * 22 * @var \IPLib\Address\AddressInterface 23 */ 24 protected $fromAddress; 25 26 /** 27 * Final address of the range. 28 * 29 * @var \IPLib\Address\AddressInterface 30 */ 31 protected $toAddress; 32 33 /** 34 * Number of the same bits of the range. 35 * 36 * @var int 37 */ 38 protected $networkPrefix; 39 40 /** 41 * The type of the range of this IP range. 42 * 43 * @var int|null 44 */ 45 protected $rangeType; 46 47 /** 48 * The 6to4 address IPv6 address range. 49 * 50 * @var self|null 51 */ 52 private static $sixToFour; 53 54 /** 55 * Initializes the instance. 56 * 57 * @param \IPLib\Address\AddressInterface $fromAddress 58 * @param \IPLib\Address\AddressInterface $toAddress 59 * @param int $networkPrefix 60 * 61 * @internal 62 */ 63 public function __construct(AddressInterface $fromAddress, AddressInterface $toAddress, $networkPrefix) 64 { 65 $this->fromAddress = $fromAddress; 66 $this->toAddress = $toAddress; 67 $this->networkPrefix = $networkPrefix; 68 } 69 70 /** 71 * Try get the range instance starting from its string representation. 72 * 73 * @param string|mixed $range 74 * 75 * @return static|null 76 */ 77 public static function fromString($range) 78 { 79 $result = null; 80 if (is_string($range)) { 81 $parts = explode('/', $range); 82 if (count($parts) === 2) { 83 $address = Factory::addressFromString($parts[0]); 84 if ($address !== null) { 85 if (preg_match('/^[0-9]{1,9}$/', $parts[1])) { 86 $networkPrefix = (int) $parts[1]; 87 if ($networkPrefix >= 0) { 88 $addressBytes = $address->getBytes(); 89 $totalBytes = count($addressBytes); 90 $numDifferentBits = $totalBytes * 8 - $networkPrefix; 91 if ($numDifferentBits >= 0) { 92 $numSameBytes = $networkPrefix >> 3; 93 $sameBytes = array_slice($addressBytes, 0, $numSameBytes); 94 $differentBytesStart = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 0); 95 $differentBytesEnd = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 255); 96 $startSameBits = $networkPrefix % 8; 97 if ($startSameBits !== 0) { 98 $varyingByte = $addressBytes[$numSameBytes]; 99 $differentBytesStart[0] = $varyingByte & bindec(str_pad(str_repeat('1', $startSameBits), 8, '0', STR_PAD_RIGHT)); 100 $differentBytesEnd[0] = $differentBytesStart[0] + bindec(str_repeat('1', 8 - $startSameBits)); 101 } 102 $result = new static( 103 Factory::addressFromBytes(array_merge($sameBytes, $differentBytesStart)), 104 Factory::addressFromBytes(array_merge($sameBytes, $differentBytesEnd)), 105 $networkPrefix 106 ); 107 } 108 } 109 } 110 } 111 } 112 } 113 114 return $result; 115 } 116 117 /** 118 * {@inheritdoc} 119 * 120 * @see \IPLib\Range\RangeInterface::toString() 121 */ 122 public function toString($long = false) 123 { 124 return $this->fromAddress->toString($long).'/'.$this->networkPrefix; 125 } 126 127 /** 128 * {@inheritdoc} 129 * 130 * @see \IPLib\Range\RangeInterface::__toString() 131 */ 132 public function __toString() 133 { 134 return $this->toString(); 135 } 136 137 /** 138 * {@inheritdoc} 139 * 140 * @see \IPLib\Range\RangeInterface::getAddressType() 141 */ 142 public function getAddressType() 143 { 144 return $this->fromAddress->getAddressType(); 145 } 146 147 /** 148 * {@inheritdoc} 149 * 150 * @see \IPLib\Range\RangeInterface::getRangeType() 151 */ 152 public function getRangeType() 153 { 154 if ($this->rangeType === null) { 155 $addressType = $this->getAddressType(); 156 if ($addressType === AddressType::T_IPv6 && static::get6to4()->containsRange($this)) { 157 $this->rangeType = Factory::rangeFromBoundaries($this->fromAddress->toIPv4(), $this->toAddress->toIPv4())->getRangeType(); 158 } else { 159 switch ($addressType) { 160 case AddressType::T_IPv4: 161 $defaultType = IPv4::getDefaultReservedRangeType(); 162 $reservedRanges = IPv4::getReservedRanges(); 163 break; 164 case AddressType::T_IPv6: 165 $defaultType = IPv6::getDefaultReservedRangeType(); 166 $reservedRanges = IPv6::getReservedRanges(); 167 break; 168 default: 169 throw new \Exception('@todo'); // @codeCoverageIgnore 170 } 171 $rangeType = null; 172 foreach ($reservedRanges as $reservedRange) { 173 $rangeType = $reservedRange->getRangeType($this); 174 if ($rangeType !== null) { 175 break; 176 } 177 } 178 $this->rangeType = $rangeType === null ? $defaultType : $rangeType; 179 } 180 } 181 182 return $this->rangeType === false ? null : $this->rangeType; 183 } 184 185 /** 186 * {@inheritdoc} 187 * 188 * @see \IPLib\Range\RangeInterface::contains() 189 */ 190 public function contains(AddressInterface $address) 191 { 192 $result = false; 193 if ($address->getAddressType() === $this->getAddressType()) { 194 $cmp = $address->getComparableString(); 195 $from = $this->getComparableStartString(); 196 if ($cmp >= $from) { 197 $to = $this->getComparableEndString(); 198 if ($cmp <= $to) { 199 $result = true; 200 } 201 } 202 } 203 204 return $result; 205 } 206 207 /** 208 * {@inheritdoc} 209 * 210 * @see \IPLib\Range\RangeInterface::containsRange() 211 */ 212 public function containsRange(RangeInterface $range) 213 { 214 $result = false; 215 if ($range->getAddressType() === $this->getAddressType()) { 216 $myStart = $this->getComparableStartString(); 217 $itsStart = $range->getComparableStartString(); 218 if ($itsStart >= $myStart) { 219 $myEnd = $this->getComparableEndString(); 220 $itsEnd = $range->getComparableEndString(); 221 if ($itsEnd <= $myEnd) { 222 $result = true; 223 } 224 } 225 } 226 227 return $result; 228 } 229 230 /** 231 * {@inheritdoc} 232 * 233 * @see \IPLib\Range\RangeInterface::getStartAddress() 234 */ 235 public function getStartAddress() 236 { 237 return $this->fromAddress; 238 } 239 240 /** 241 * {@inheritdoc} 242 * 243 * @see \IPLib\Range\RangeInterface::getEndAddress() 244 */ 245 public function getEndAddress() 246 { 247 return $this->toAddress; 248 } 249 250 /** 251 * {@inheritdoc} 252 * 253 * @see \IPLib\Range\RangeInterface::getComparableStartString() 254 */ 255 public function getComparableStartString() 256 { 257 return $this->fromAddress->getComparableString(); 258 } 259 260 /** 261 * {@inheritdoc} 262 * 263 * @see \IPLib\Range\RangeInterface::getComparableEndString() 264 */ 265 public function getComparableEndString() 266 { 267 return $this->toAddress->getComparableString(); 268 } 269 270 /** 271 * Get the 6to4 address IPv6 address range. 272 * 273 * @return self 274 */ 275 public static function get6to4() 276 { 277 if (self::$sixToFour === null) { 278 self::$sixToFour = self::fromString('2002::/16'); 279 } 280 281 return self::$sixToFour; 282 } 283 284 /** 285 * Get subnet prefix. 286 * 287 * @return int 288 */ 289 public function getNetworkPrefix() 290 { 291 return $this->networkPrefix; 292 } 293 294 /** 295 * Get the pattern representation (if applicable) of this range. 296 * 297 * @return \IPLib\Range\Pattern|null return NULL if this range can't be represented by a pattern notation 298 */ 299 public function asPattern() 300 { 301 $address = $this->getStartAddress(); 302 $networkPrefix = $this->getNetworkPrefix(); 303 switch ($address->getAddressType()) { 304 case AddressType::T_IPv4: 305 return $networkPrefix % 8 === 0 ? new Pattern($address, $address, 4 - $networkPrefix / 8) : null; 306 case AddressType::T_IPv6: 307 return $networkPrefix % 16 === 0 ? new Pattern($address, $address, 8 - $networkPrefix / 16) : null; 308 } 309 } 310 311 /** 312 * {@inheritdoc} 313 * 314 * @see \IPLib\Range\RangeInterface::getSubnetMask() 315 */ 316 public function getSubnetMask() 317 { 318 if ($this->getAddressType() !== AddressType::T_IPv4) { 319 return null; 320 } 321 $bytes = array(); 322 $prefix = $this->getNetworkPrefix(); 323 while ($prefix >= 8) { 324 $bytes[] = 255; 325 $prefix -= 8; 326 } 327 if ($prefix !== 0) { 328 $bytes[] = bindec(str_pad(str_repeat('1', $prefix), 8, '0')); 329 } 330 $bytes = array_pad($bytes, 4, 0); 331 332 return IPv4::fromBytes($bytes); 333 } 334} 335