* * For the full copyright and license information, please view the "LICENSE.md" * file that was distributed with this source code. */ declare(strict_types=1); namespace Respect\Validation\Rules; use Respect\Validation\Exceptions\ComponentException; use function bccomp; use function explode; use function filter_var; use function ip2long; use function is_string; use function long2ip; use function mb_strpos; use function mb_substr_count; use function sprintf; use function str_repeat; use function str_replace; use function strtr; /** * Validates whether the input is a valid IP address. * * This validator uses the native filter_var() PHP function. * * @author Alexandre Gomes Gaigalas * @author Danilo Benevides * @author Henrique Moody * @author Luís Otávio Cobucci Oblonczyk */ final class Ip extends AbstractRule { /** * @var string|null */ private $range; /** * @var int|null */ private $options; /** * @var string|null */ private $startAddress; /** * @var string|null */ private $endAddress; /** * @var string|null */ private $mask; /** * Initializes the rule defining the range and some options for filter_var(). * * @throws ComponentException In case the range is invalid */ public function __construct(string $range = '*', int $options = null) { $this->parseRange($range); $this->range = $this->createRange(); $this->options = $options; } /** * {@inheritdoc} */ public function validate($input): bool { if (!is_string($input)) { return false; } if (!$this->verifyAddress($input)) { return false; } if ($this->mask) { return $this->belongsToSubnet($input); } if ($this->startAddress && $this->endAddress) { return $this->verifyNetwork($input); } return true; } private function createRange(): ?string { if ($this->endAddress && $this->endAddress) { return $this->startAddress.'-'.$this->endAddress; } if ($this->startAddress && $this->mask) { return $this->startAddress.'/'.long2ip((int) $this->mask); } return null; } private function parseRange(string $input): void { if ('*' == $input || '*.*.*.*' == $input || '0.0.0.0-255.255.255.255' == $input) { return; } if (false !== mb_strpos($input, '-')) { list($this->startAddress, $this->endAddress) = explode('-', $input); if (!$this->verifyAddress($this->startAddress)) { throw new ComponentException('Invalid network range'); } if (!$this->verifyAddress($this->endAddress)) { throw new ComponentException('Invalid network range'); } return; } if (false !== mb_strpos($input, '*')) { $this->parseRangeUsingWildcards($input); return; } if (false !== mb_strpos($input, '/')) { $this->parseRangeUsingCidr($input); return; } throw new ComponentException('Invalid network range'); } private function fillAddress(string $address, string $fill = '*'): string { return $address.str_repeat('.'.$fill, 3 - mb_substr_count($address, '.')); } private function parseRangeUsingWildcards(string $input): void { $address = $this->fillAddress($input); $this->startAddress = strtr($address, '*', '0'); $this->endAddress = str_replace('*', '255', $address); } private function parseRangeUsingCidr(string $input): void { $parts = explode('/', $input); $this->startAddress = $this->fillAddress($parts[0], '0'); $isAddressMask = false !== mb_strpos($parts[1], '.'); if ($isAddressMask && $this->verifyAddress($parts[1])) { $this->mask = sprintf('%032b', ip2long($parts[1])); return; } if ($isAddressMask || $parts[1] < 8 || $parts[1] > 30) { throw new ComponentException('Invalid network mask'); } $this->mask = sprintf('%032b', ip2long(long2ip(~(2 ** (32 - (int) $parts[1]) - 1)))); } private function verifyAddress(string $address): bool { return false !== filter_var($address, FILTER_VALIDATE_IP, ['flags' => $this->options]); } private function verifyNetwork(string $input): bool { $input = sprintf('%u', ip2long($input)); return bccomp($input, sprintf('%u', ip2long($this->startAddress))) >= 0 && bccomp($input, sprintf('%u', ip2long($this->endAddress))) <= 0; } private function belongsToSubnet(string $input): bool { if (null === $this->mask) { return false; } $min = sprintf('%032b', ip2long($this->startAddress)); $input = sprintf('%032b', ip2long($input)); return ($input & $this->mask) === ($min & $this->mask); } }