mvc: merge NetworkValidator into NetworkField to ease extensibility and add unit test

also needed for https://github.com/opnsense/core/issues/8329
This commit is contained in:
Ad Schellevis 2025-03-04 16:14:24 +01:00
parent bb37fa89e7
commit c93e0ac745
4 changed files with 214 additions and 145 deletions

2
plist
View File

@ -673,7 +673,6 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/Email.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/IntegerValidator.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/MinMaxValidator.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/NetworkValidator.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/Numericality.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/PresenceOf.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/Validators/Regex.php
@ -1042,6 +1041,7 @@
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/ModelRelationFieldTest/config.xml
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/NetworkAliasFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/NetworkAliasFieldTest/config.xml
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/NetworkFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/OptionFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/PortFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/ProtocolFieldTest.php

View File

@ -28,7 +28,8 @@
namespace OPNsense\Base\FieldTypes;
use OPNsense\Base\Validators\NetworkValidator;
use OPNsense\Firewall\Util;
use OPNsense\Base\Validators\CallbackValidator;
/**
* @package OPNsense\Base\FieldTypes
@ -190,24 +191,90 @@ class NetworkField extends BaseField
return gettext('Please specify a valid network segment or IP address.');
}
/**
* @param string $input data to test
* @return bool if valid network address or segment (using this objects settings)
*/
protected function isValidInput($input)
{
$result = true;
if ($this->internalFieldSeparator == null) {
$values = [$input];
} else {
$values = explode($this->internalFieldSeparator, $input);
}
foreach ($values as $value) {
// parse filter options
$filterOpt = 0;
switch (strtolower($this->internalAddressFamily ?? '')) {
case "ipv4":
$filterOpt |= FILTER_FLAG_IPV4;
break;
case "ipv6":
$filterOpt |= FILTER_FLAG_IPV6;
break;
default:
$filterOpt |= FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
}
// split network
if (strpos($value, "/") !== false) {
if ($this->internalNetMaskAllowed === false) {
return false;
} else {
$cidr = $value;
$parts = explode("/", $value);
if (count($parts) > 2 || !ctype_digit($parts[1])) {
// more parts then expected or second part is not numeric
return false;
} else {
$mask = $parts[1];
$value = $parts[0];
if (strpos($parts[0], ":") !== false) {
// probably ipv6, mask must be between 0..128
if ($mask < 0 || $mask > 128) {
return false;
}
} else {
// most likely ipv4 address, mask must be between 0..32
if ($mask < 0 || $mask > 32) {
return false;
}
}
}
if ($this->internalStrict === true && !Util::isSubnetStrict($cidr)) {
return false;
}
}
} elseif ($this->internalNetMaskRequired === true) {
return false;
}
if (filter_var($value, FILTER_VALIDATE_IP, $filterOpt) === false) {
return false;
}
}
return true;;
}
/**
* retrieve field validators for this field type
* @return array returns Text/regex validator
* @return array returns validators
*/
public function getValidators()
{
$validators = parent::getValidators();
if ($this->internalValue != null) {
if ($this->internalValue != "any" || $this->internalWildcardEnabled == false) {
// accept any as target
$validators[] = new NetworkValidator([
'message' => $this->getValidationMessage(),
'split' => $this->internalFieldSeparator,
'netMaskRequired' => $this->internalNetMaskRequired,
'netMaskAllowed' => $this->internalNetMaskAllowed,
'version' => $this->internalAddressFamily,
'strict' => $this->internalStrict
]);
$that = $this;
$validators[] = new CallbackValidator(["callback" => function ($data) use ($that) {
$messages = [];
if (!$that->isValidInput($data)) {
$messages[] = $this->getValidationMessage();
}
return $messages;
}]);
}
}
return $validators;

View File

@ -1,133 +0,0 @@
<?php
/*
* Copyright (C) 2015-2017 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Base\Validators;
use OPNsense\Base\BaseValidator;
use OPNsense\Firewall\Util;
use OPNsense\Base\Messages\Message;
/**
* Class NetworkValidator validate networks and ip addresses
* @package OPNsense\Base\Validators
*/
class NetworkValidator extends BaseValidator
{
/**
* Executes network / ip validation, accepts the following parameters as attributes:
* version : ipv4, ipv6, all (default)
* noReserved : true, false (default)
* noPrivate : true, false (default)
* noSubnet : true, false (default)
* netMaskRequired : true, false (default)
* strict: : true, false (default)
*
* @param $validator
* @param string $attribute
* @return boolean
*/
public function validate($validator, $attribute): bool
{
$result = true;
$msg = $this->getOption('message');
$fieldSplit = $this->getOption('split', null);
if ($fieldSplit == null) {
$values = array($validator->getValue($attribute));
} else {
$values = explode($fieldSplit, $validator->getValue($attribute));
}
foreach ($values as $value) {
// parse filter options
$filterOpt = 0;
switch (strtolower($this->getOption('version') ?? '')) {
case "ipv4":
$filterOpt |= FILTER_FLAG_IPV4;
break;
case "ipv6":
$filterOpt |= FILTER_FLAG_IPV6;
break;
default:
$filterOpt |= FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
}
if ($this->getOption('noReserved') === true) {
$filterOpt |= FILTER_FLAG_NO_RES_RANGE;
}
if ($this->getOption('noPrivate') === true) {
$filterOpt |= FILTER_FLAG_NO_PRIV_RANGE;
}
// split network
if (strpos($value, "/") !== false) {
if ($this->getOption('netMaskAllowed') === false) {
$result = false;
} else {
$cidr = $value;
$parts = explode("/", $value);
if (count($parts) > 2 || !ctype_digit($parts[1])) {
// more parts then expected or second part is not numeric
$result = false;
} else {
$mask = $parts[1];
$value = $parts[0];
if (strpos($parts[0], ":") !== false) {
// probably ipv6, mask must be between 0..128
if ($mask < 0 || $mask > 128) {
$result = false;
}
} else {
// most likely ipv4 address, mask must be between 0..32
if ($mask < 0 || $mask > 32) {
$result = false;
}
}
}
if ($this->getOption('strict') === true && !Util::isSubnetStrict($cidr)) {
$result = false;
}
}
} elseif ($this->getOption('netMaskRequired') === true) {
$result = false;
}
if (filter_var($value, FILTER_VALIDATE_IP, $filterOpt) === false) {
$result = false;
}
if (!$result) {
// append validation message
$validator->appendMessage(new Message($msg, $attribute, 'NetworkValidator'));
}
}
return $result;
}
}

View File

@ -0,0 +1,135 @@
<?php
/**
* Copyright (C) 2019 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace tests\OPNsense\Base\FieldTypes;
// @CodingStandardsIgnoreStart
require_once 'Field_Framework_TestCase.php';
// @CodingStandardsIgnoreEnd
use OPNsense\Base\FieldTypes\NetworkField;
class NetworkFieldTest extends Field_Framework_TestCase
{
public function testCanBeCreated()
{
$this->assertInstanceOf('\OPNsense\Base\FieldTypes\NetworkField', new NetworkField());
}
public function testRequiredEmpty()
{
$this->expectException(\OPNsense\Base\ValidationException::class);
$this->expectExceptionMessage("PresenceOf");
$field = new NetworkField();
$field->setRequired("Y");
$field->setValue("");
$this->validateThrow($field);
}
public function testRequiredNotEmpty()
{
$field = new NetworkField();
$field->setRequired("Y");
$field->setValue("192.168.1.1");
$this->assertEmpty($this->validate($field));
}
public function testValidValuesV4()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setAddressFamily("ipv4");
foreach (["192.168.1.1/24", "10.0.0.1/24"] as $value) {
$field->setValue($value);
$this->assertEmpty($this->validate($field));
}
}
public function testValidValuesV6()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setAddressFamily("ipv6");
foreach (["2000::1/128", "fe80::5a8c:fcff:0001:ffe2/64"] as $value) {
$field->setValue($value);
$this->assertEmpty($this->validate($field));
}
}
public function testInValidValuesV4()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setAddressFamily("ipv4");
foreach (["192.168.1.1", "2000::1", "A"] as $value) {
$field->setValue($value);
$this->assertNotEmpty($this->validate($field));
}
}
public function testInValidValuesV6()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setAddressFamily("ipv6");
foreach (["192.168.1.1", "2000::1", "A"] as $value) {
$field->setValue($value);
$this->assertNotEmpty($this->validate($field));
}
}
public function testValidValuesStrict()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setStrict("Y");
foreach (["192.168.1.0/24", "2000::0/64"] as $value) {
$field->setValue($value);
$this->assertEmpty($this->validate($field));
}
}
public function testInValidValuesStrict()
{
$field = new NetworkField();
$field->setNetMaskRequired("Y");
$field->setStrict("Y");
foreach (["192.168.1.1/24", "2000::1:1/64"] as $value) {
$field->setValue($value);
$this->assertNotEmpty($this->validate($field));
}
}
public function testIsContainer()
{
$field = new NetworkField();
$this->assertFalse($field->isContainer());
}
}