Ip.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. /*
  3. * Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
  4. * SPDX-License-Identifier: MIT
  5. */
  6. declare(strict_types=1);
  7. namespace Respect\Validation\Rules;
  8. use Respect\Validation\Exceptions\ComponentException;
  9. use function bccomp;
  10. use function explode;
  11. use function filter_var;
  12. use function ip2long;
  13. use function is_string;
  14. use function long2ip;
  15. use function mb_strpos;
  16. use function mb_substr_count;
  17. use function sprintf;
  18. use function str_repeat;
  19. use function str_replace;
  20. use function strtr;
  21. use const FILTER_VALIDATE_IP;
  22. /**
  23. * Validates whether the input is a valid IP address.
  24. *
  25. * This validator uses the native filter_var() PHP function.
  26. *
  27. * @author Alexandre Gomes Gaigalas <alganet@gmail.com>
  28. * @author Danilo Benevides <danilobenevides01@gmail.com>
  29. * @author Henrique Moody <henriquemoody@gmail.com>
  30. * @author Luís Otávio Cobucci Oblonczyk <lcobucci@gmail.com>
  31. */
  32. final class Ip extends AbstractRule
  33. {
  34. /**
  35. * @var string|null
  36. */
  37. private $range;
  38. /**
  39. * @var int|null
  40. */
  41. private $options;
  42. /**
  43. * @var string|null
  44. */
  45. private $startAddress;
  46. /**
  47. * @var string|null
  48. */
  49. private $endAddress;
  50. /**
  51. * @var string|null
  52. */
  53. private $mask;
  54. /**
  55. * Initializes the rule defining the range and some options for filter_var().
  56. *
  57. * @throws ComponentException In case the range is invalid
  58. */
  59. public function __construct(string $range = '*', ?int $options = null)
  60. {
  61. $this->parseRange($range);
  62. $this->range = $this->createRange();
  63. $this->options = $options;
  64. }
  65. /**
  66. * {@inheritDoc}
  67. */
  68. public function validate($input): bool
  69. {
  70. if (!is_string($input)) {
  71. return false;
  72. }
  73. if (!$this->verifyAddress($input)) {
  74. return false;
  75. }
  76. if ($this->mask) {
  77. return $this->belongsToSubnet($input);
  78. }
  79. if ($this->startAddress && $this->endAddress) {
  80. return $this->verifyNetwork($input);
  81. }
  82. return true;
  83. }
  84. private function createRange(): ?string
  85. {
  86. if ($this->startAddress && $this->endAddress) {
  87. return $this->startAddress . '-' . $this->endAddress;
  88. }
  89. if ($this->startAddress && $this->mask) {
  90. return $this->startAddress . '/' . long2ip((int) $this->mask);
  91. }
  92. return null;
  93. }
  94. private function parseRange(string $input): void
  95. {
  96. if ($input == '*' || $input == '*.*.*.*' || $input == '0.0.0.0-255.255.255.255') {
  97. return;
  98. }
  99. if (mb_strpos($input, '-') !== false) {
  100. [$this->startAddress, $this->endAddress] = explode('-', $input);
  101. if ($this->startAddress !== null && !$this->verifyAddress($this->startAddress)) {
  102. throw new ComponentException('Invalid network range');
  103. }
  104. if ($this->endAddress !== null && !$this->verifyAddress($this->endAddress)) {
  105. throw new ComponentException('Invalid network range');
  106. }
  107. return;
  108. }
  109. if (mb_strpos($input, '*') !== false) {
  110. $this->parseRangeUsingWildcards($input);
  111. return;
  112. }
  113. if (mb_strpos($input, '/') !== false) {
  114. $this->parseRangeUsingCidr($input);
  115. return;
  116. }
  117. throw new ComponentException('Invalid network range');
  118. }
  119. private function fillAddress(string $address, string $fill = '*'): string
  120. {
  121. return $address . str_repeat('.' . $fill, 3 - mb_substr_count($address, '.'));
  122. }
  123. private function parseRangeUsingWildcards(string $input): void
  124. {
  125. $address = $this->fillAddress($input);
  126. $this->startAddress = strtr($address, '*', '0');
  127. $this->endAddress = str_replace('*', '255', $address);
  128. }
  129. private function parseRangeUsingCidr(string $input): void
  130. {
  131. $parts = explode('/', $input);
  132. $this->startAddress = $this->fillAddress($parts[0], '0');
  133. $isAddressMask = mb_strpos($parts[1], '.') !== false;
  134. if ($isAddressMask && $this->verifyAddress($parts[1])) {
  135. $this->mask = sprintf('%032b', ip2long($parts[1]));
  136. return;
  137. }
  138. if ($isAddressMask || $parts[1] < 8 || $parts[1] > 30) {
  139. throw new ComponentException('Invalid network mask');
  140. }
  141. $this->mask = sprintf('%032b', ip2long((string) long2ip(~(2 ** (32 - (int) $parts[1]) - 1))));
  142. }
  143. private function verifyAddress(string $address): bool
  144. {
  145. return filter_var($address, FILTER_VALIDATE_IP, ['flags' => $this->options]) !== false;
  146. }
  147. private function verifyNetwork(string $input): bool
  148. {
  149. $input = sprintf('%u', ip2long($input));
  150. return $this->startAddress !== null
  151. && $this->endAddress !== null
  152. && bccomp($input, sprintf('%u', ip2long($this->startAddress))) >= 0
  153. && bccomp($input, sprintf('%u', ip2long($this->endAddress))) <= 0;
  154. }
  155. private function belongsToSubnet(string $input): bool
  156. {
  157. if ($this->mask === null || $this->startAddress === null) {
  158. return false;
  159. }
  160. $min = sprintf('%032b', ip2long($this->startAddress));
  161. $input = sprintf('%032b', ip2long($input));
  162. return ($input & $this->mask) === ($min & $this->mask);
  163. }
  164. }