MVC: Add IPPortField type (#7134)

Useful for Netflow and plugins. Tests included.
This commit is contained in:
Stephan de Wit 2024-01-11 15:41:40 +01:00 committed by GitHub
parent 587375aaed
commit 8846037d93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 306 additions and 4 deletions

2
plist
View File

@ -531,6 +531,7 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/EmailField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/HostnameField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IntegerField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/InterfaceField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/JsonKeyValueStoreField.php
@ -845,6 +846,7 @@
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/CertificateFieldTest/config.xml
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/CountryFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/Field_Framework_TestCase.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IPPortFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IntegerFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/InterfaceFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/InterfaceFieldTest/config.xml

View File

@ -0,0 +1,149 @@
<?php
/*
* Copyright (C) 2024 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\FieldTypes;
use OPNsense\Base\Validators\CallbackValidator;
use OPNsense\Firewall\Util;
/**
* Class IPPortField
* @package OPNsense\Base\FieldTypes
*/
class IPPortField extends BaseField
{
/**
* @var bool marks if this is a data node or a container
*/
protected $internalIsContainer = false;
/**
* @var string when multiple values could be provided at once, specify the split character
*/
protected $internalFieldSeparator = ',';
/**
* @var bool when set, results are returned as list (with all options enabled)
*/
protected $internalAsList = false;
/**
* @var string Network family (ipv4, ipv6)
*/
protected $internalAddressFamily = null;
/**
* trim IP-Port combination
* @param string $value
*/
public function setValue($value)
{
parent::setValue(trim($value));
}
/**
* get valid options, descriptions and selected value
* @return array
*/
public function getNodeData()
{
if ($this->internalAsList) {
// return result as list
$result = array();
foreach (explode($this->internalFieldSeparator, $this->internalValue) as $address) {
$result[$address] = array("value" => $address, "selected" => 1);
}
return $result;
} else {
// normal, single field response
return $this->internalValue;
}
}
/**
* setter for address family
* @param $value address family [ipv4, ipv6, empty for all]
*/
public function setAddressFamily($value)
{
$this->internalAddressFamily = trim(strtolower($value));
}
/**
* select if multiple IP-Port combinations may be selected at once
* @param $value string value Y/N
*/
public function setAsList($value)
{
$this->internalAsList = trim(strtoupper($value)) == "Y";
}
/**
* {@inheritdoc}
*/
protected function defaultValidationMessage()
{
return gettext('Invalid IP-port combination.');
}
/**
* retrieve field validators for this field type
* @return array returns validators
*/
public function getValidators()
{
$validators = parent::getValidators();
if ($this->internalValue != null) {
$validators[] = new CallbackValidator(["callback" => function ($data) {
foreach ($this->internalAsList ? explode($this->internalFieldSeparator, $data) : [$data] as $value) {
if ($this->internalAddressFamily == 'ipv4' || $this->internalAddressFamily == null) {
$parts = explode(':', $value);
if (count($parts) == 2 && Util::isIpv4Address($parts[0]) && Util::isPort($parts[1])) {
continue;
}
}
if ($this->internalAddressFamily == 'ipv6' || $this->internalAddressFamily == null) {
$parts = preg_split('/\[([^\]]+)\]/', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (count($parts) == 2 &&
Util::isIpv6Address($parts[0]) &&
str_contains($parts[1], ':') &&
Util::isPort(trim($parts[1], ': '))) {
continue;
}
}
return ["\"" . $value . "\" is invalid. " . $this->getValidationMessage()];
}
}]);
}
return $validators;
}
}

View File

@ -24,10 +24,9 @@
<v9>v9</v9>
</OptionValues>
</version>
<targets type="CSVListField">
<Mask>/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(6553[0-5]|655[0-2][0-9]|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3})$/u</Mask>
<ValidationMessage>Please enter valid targets (e.g. 192.168.0.1:2055).</ValidationMessage>
<MaskPerItem>Y</MaskPerItem>
<targets type="IPPortField">
<AsList>Y</AsList>
<AddressFamily>ipv4</AddressFamily>
</targets>
</capture>
<collect>

View File

@ -0,0 +1,152 @@
<?php
/**
* Copyright (C) 2024 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\IPPortField;
class IPPortFieldTest extends Field_Framework_TestCase
{
public function testCanBeCreated()
{
$this->assertInstanceOf('\OPNsense\Base\FieldTypes\IPPortField', new IPPortField());
}
public function testRequiredEmpty()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$this->expectExceptionMessage("PresenceOf");
$field = new IPPortField();
$field->setRequired("Y");
$field->setValue("");
$this->validateThrow($field);
}
public function testNotRequiredEmpty()
{
$field = new IPPortField();
$field->setValue("");
$this->assertEmpty($this->validate($field));
}
public function testRequiredNotEmpty()
{
$field = new IPPortField();
$field->setRequired("Y");
$field->setValue("127.0.0.1:2056");
$this->assertEmpty($this->validate($field));
}
public function testValidValueIpv4()
{
$field = new IPPortField();
$field->setValue("127.0.0.1:2056");
$this->assertEmpty($this->validate($field));
}
public function testValidValueAsListIpv4()
{
$field = new IPPortField();
$field->setAsList("Y");
$field->setValue("127.0.0.1:2056,192.168.1.1:1111");
$this->assertEmpty($this->validate($field));
}
public function testValidValueIpv6()
{
$field = new IPPortField();
$field->setValue("[::1]:2056");
$this->assertEmpty($this->validate($field));
}
public function testValidValueAsListIpv6()
{
$field = new IPPortField();
$field->setAsList("Y");
$field->setValue("[::1]:2056,[fe80::]:1111");
$this->assertEmpty($this->validate($field));
}
public function testInvalidValueIpv4()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setValue("abcdefg");
$this->validateThrow($field);
}
public function testInvalidValueAsListIpv4()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setAsList("Y");
$field->setValue("127.0.0.1:2056,abcdefg");
$this->validateThrow($field);
}
public function testInvalidValueIpv6()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setValue("[::1]");
$this->validateThrow($field);
}
public function testInvalidValueAsListIpv6()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setAsList("Y");
$field->setValue("[::1]:2056,[fe80::]");
$this->validateThrow($field);
}
public function testAddressFamilyIpv4()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setAddressFamily("ipv4");
$field->setValue("[::1]:2056");
$this->validateThrow($field);
}
public function testAddressFamilyIpv6()
{
$this->expectException(\Phalcon\Filter\Validation\Exception::class);
$field = new IPPortField();
$field->setAddressFamily("ipv6");
$field->setValue("192.168.1.1:1111");
$this->validateThrow($field);
}
}