Skip to content

Commit

Permalink
Use IpRanges from yiisoft/network-utilities (#740)
Browse files Browse the repository at this point in the history
* Use `IpRanges`

* improve

* changelog
  • Loading branch information
vjik authored Aug 7, 2024
1 parent 7b91776 commit 502e098
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 102 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Yii Validator Change Log

## 2.0.1 under development
## 2.1.0 under development

- no changes in this release.
- Enh #740: Use `Yiisoft\NetworkUtilities\IpRanges` in `Ip` rule: add `getIpRanges()` method and deprecate
`getRanges()`, `getNetworks()`, `isAllowed()` methods (@vjik)

## 2.0.0 August 02, 2024

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"psr/http-message": "^1.0|^2.0",
"yiisoft/arrays": "^2.1|^3.0",
"yiisoft/friendly-exception": "^1.0",
"yiisoft/network-utilities": "^1.0",
"yiisoft/network-utilities": "^1.1",
"yiisoft/strings": "^2.1",
"yiisoft/translator": "^2.1|^3.0"
},
Expand Down
114 changes: 16 additions & 98 deletions src/Rule/Ip.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Attribute;
use Closure;
use InvalidArgumentException;
use Yiisoft\NetworkUtilities\IpHelper;
use Yiisoft\NetworkUtilities\IpRanges;
use Yiisoft\Validator\DumpedRuleInterface;
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
Expand All @@ -16,9 +16,6 @@
use Yiisoft\Validator\SkipOnErrorInterface;
use Yiisoft\Validator\WhenInterface;

use function array_key_exists;
use function strlen;

/**
* Checks if the value is a valid IPv4/IPv6 address or subnet.
*
Expand All @@ -34,30 +31,7 @@ final class Ip implements DumpedRuleInterface, SkipOnErrorInterface, WhenInterfa
use SkipOnErrorTrait;
use WhenTrait;

/**
* Negation character.
*
* Used to negate {@see $ranges} or {@see $network} or to negate value validated when {@see $allowNegation}
* is used.
*/
private const NEGATION_CHARACTER = '!';
/**
* @psalm-var array<string, list<string>>
*
* @var array Default network aliases that can be used in {@see $ranges}.
*
* @see $networks
*/
private array $defaultNetworks = [
'*' => ['any'],
'any' => ['0.0.0.0/0', '::/0'],
'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fd00::/8'],
'multicast' => ['224.0.0.0/4', 'ff00::/8'],
'linklocal' => ['169.254.0.0/16', 'fe80::/10'],
'localhost' => ['127.0.0.0/8', '::1'],
'documentation' => ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24', '2001:db8::/32'],
'system' => ['multicast', 'linklocal', 'localhost', 'documentation'],
];
private IpRanges $ranges;

/**
* @param array $networks Custom network aliases, that can be used in {@see $ranges}:
Expand Down Expand Up @@ -178,7 +152,7 @@ final class Ip implements DumpedRuleInterface, SkipOnErrorInterface, WhenInterfa
* @throws InvalidArgumentException If configuration is wrong.
*/
public function __construct(
private array $networks = [],
array $networks = [],
private bool $allowIpv4 = true,
private bool $allowIpv6 = true,
private bool $allowSubnet = false,
Expand All @@ -192,7 +166,7 @@ public function __construct(
private string $noSubnetMessage = '{Property} must be an IP address with specified subnet.',
private string $hasSubnetMessage = '{Property} must not be a subnet.',
private string $notInRangeMessage = '{Property} is not in the allowed range.',
private array $ranges = [],
array $ranges = [],
bool|callable|null $skipOnEmpty = null,
private bool $skipOnError = false,
private Closure|null $when = null,
Expand All @@ -201,22 +175,14 @@ public function __construct(
throw new InvalidArgumentException('Both IPv4 and IPv6 checks can not be disabled at the same time.');
}

foreach ($networks as $key => $_values) {
if (array_key_exists($key, $this->defaultNetworks)) {
throw new InvalidArgumentException("Network alias \"{$key}\" already set as default.");
}
}

$this->networks = array_merge($this->defaultNetworks, $this->networks);

if ($requireSubnet) {
// Might be a bug of XDebug, because this line is covered by tests (see "IpTest").
// @codeCoverageIgnoreStart
$this->allowSubnet = true;
// @codeCoverageIgnoreEnd
}

$this->ranges = $this->prepareRanges($ranges);
$this->ranges = new IpRanges($ranges, $networks);
$this->skipOnEmpty = $skipOnEmpty;
}

Expand All @@ -230,11 +196,11 @@ public function getName(): string
*
* @return array Network aliases.
*
* @see $networks
* @deprecated Use {@see IpRanges::getNetworks()} instead.
*/
public function getNetworks(): array
{
return $this->networks;
return $this->ranges->getNetworks();
}

/**
Expand Down Expand Up @@ -404,80 +370,32 @@ public function getNotInRangeMessage(): string
*
* @return string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
*
* @see $ranges
* @deprecated Use {@see IpRanges::getRanges()} instead.
*/
public function getRanges(): array
{
return $this->ranges;
}

/**
* Parses IP address/range for the negation with {@see NEGATION_CHARACTER}.
*
* @return array The result array consists of 2 elements:
* - `boolean`: whether the string is negated
* - `string`: the string without negation (when the negation were present)
*
* @psalm-return array{0: bool, 1: string}
*/
private function parseNegatedRange(string $string): array
{
$isNegated = str_starts_with($string, self::NEGATION_CHARACTER);
return [$isNegated, $isNegated ? substr($string, strlen(self::NEGATION_CHARACTER)) : $string];
return $this->ranges->getRanges();
}

/**
* Prepares array to fill in {@see $ranges}:
*
* - Recursively substitutes aliases, described in {@see $networks} with their values.
* - Removes duplicates.
*
* @param string[] $ranges
*
* @return string[]
*/
private function prepareRanges(array $ranges): array
public function getIpRanges(): IpRanges
{
$result = [];
foreach ($ranges as $string) {
[$isRangeNegated, $range] = $this->parseNegatedRange($string);
if (isset($this->networks[$range])) {
$replacements = $this->prepareRanges($this->networks[$range]);
foreach ($replacements as &$replacement) {
[$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement);
$result[] = ($isRangeNegated && !$isReplacementNegated ? self::NEGATION_CHARACTER : '') . $replacement;
}
} else {
$result[] = $string;
}
}

return array_unique($result);
return $this->ranges;
}

/**
* Whether the IP address with specified CIDR is allowed according to the {@see $ranges} list.
*
* @deprecated Use {@see IpRanges::isAllowed()} instead.
*/
public function isAllowed(string $ip): bool
{
if (empty($this->ranges)) {
return true;
}

foreach ($this->ranges as $string) {
[$isNegated, $range] = $this->parseNegatedRange($string);
if (IpHelper::inRange($ip, $range)) {
return !$isNegated;
}
}

return false;
return $this->ranges->isAllowed($ip);
}

public function getOptions(): array
{
return [
'networks' => $this->networks,
'networks' => $this->ranges->getNetworks(),
'allowIpv4' => $this->allowIpv4,
'allowIpv6' => $this->allowIpv6,
'allowSubnet' => $this->allowSubnet,
Expand Down Expand Up @@ -515,7 +433,7 @@ public function getOptions(): array
'template' => $this->notInRangeMessage,
'parameters' => [],
],
'ranges' => $this->ranges,
'ranges' => $this->ranges->getRanges(),
'skipOnEmpty' => $this->getSkipOnEmptyOption(),
'skipOnError' => $this->skipOnError,
];
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/IpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private static function validateCidr(
}
}

if (!$rule->isAllowed($ipCidr)) {
if (!$rule->getIpRanges()->isAllowed($ipCidr)) {
return self::getGenericErrorResult($rule->getNotInRangeMessage(), $context, $value);
}

Expand Down

0 comments on commit 502e098

Please sign in to comment.