From 191b3210331117a64ec6f3d82cbedf13adbdea79 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 5 Dec 2024 21:44:24 -0700 Subject: [PATCH 01/36] fix: remove all direct config accessors This addresses an issue in pfSense 24.11 where the global variable has been deprecated --- .../files/usr/local/pkg/RESTAPI/Core/Form.inc | 2 +- .../files/usr/local/pkg/RESTAPI/Core/Model.inc | 18 ++++++------------ .../usr/local/pkg/RESTAPI/Core/TestCase.inc | 9 +++------ .../pkg/RESTAPI/Models/RoutingGateway.inc | 4 ++-- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc index fe691e89c..5ece7ee03 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc @@ -319,7 +319,7 @@ class Form { */ final public function print_form(): void { # Print the static pfSense UI and include any custom CSS or JS files - global $config, $user_settings; + global $user_settings; $pgtitle = $this->title_path; include 'head.inc'; echo ""; diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index f36c6c635..2bb43abc0 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -550,8 +550,7 @@ class Model { */ protected static function init_config(string $path) { # Initialize the configuration array of a specified path. - global $config; - array_init_path($config, $path); + config_init_path($path); } /** @@ -562,8 +561,7 @@ class Model { * path keys an empty string and $default is non-null */ public static function get_config(string $path, mixed $default = null) { - global $config; - return array_get_path($config, $path, $default); + return config_get_path($path, $default); } /** @@ -574,8 +572,7 @@ class Model { * @returns mixed $val or $default if the path prefix does not exist */ public static function set_config(string $path, mixed $value, mixed $default = null) { - global $config; - return array_set_path($config, $path, $value, $default); + return config_set_path($path, $value, $default); } /** @@ -616,8 +613,7 @@ class Model { * @returns array copy of the removed value or null */ public static function del_config(string $path): mixed { - global $config; - return array_del_path($config, $path); + return config_del_path($path); } /** @@ -630,8 +626,7 @@ class Model { * non-null value, otherwise false. */ public static function is_config_enabled(string $path, string $enable_key = 'enable'): bool { - global $config; - return array_path_enabled($config, $path, $enable_key); + return config_path_enabled($path, $enable_key); } /** @@ -680,8 +675,7 @@ class Model { * @param bool $force_parse Force an entire reparse of the config.xml file instead of the cached config. */ public static function reload_config(bool $force_parse = false): void { - global $config; - $config = parse_config(parse: $force_parse); + config_set_path("", parse_config(parse: $force_parse)); } /** diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc index b9a2c97e0..505ca285a 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc @@ -44,9 +44,6 @@ class TestCase { * @throws Error|Exception The exception thrown by failed Tests, typically an AssertionError. */ public function run(): void { - # Need direct config access to revert any changes made by Tests - global $config; - # Collect available environment variables $this->get_envs(); @@ -64,7 +61,7 @@ class TestCase { # If this method starts with `test`, run the function. if (str_starts_with($method, 'test')) { # Backup the current config so we can undo any changes made during the test - $original_config = unserialize(serialize($config)); + $original_config = config_get_path(""); # Set the current method undergoing testing $this->method = $method; @@ -78,14 +75,14 @@ class TestCase { $this->$method(); } catch (Error | Exception $e) { # Restore the original configuration, teardown the TestCase and throw the encountered error - $config = $original_config; + config_set_path("", $original_config); write_config("Restored config after API test '$method'"); $this->teardown(); throw $e; } # Restore the config as it was when the test began. - $config = $original_config; + config_set_path("", $original_config); write_config("Restored config after API test '$method'"); } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGateway.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGateway.inc index aec1ef5b2..e65976c9c 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGateway.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGateway.inc @@ -207,8 +207,8 @@ class RoutingGateway extends Model { # Ensure the gateway cannot be disabled while it is in use by a system DNS server # TODO: replace with a Model query when the SystemDNSServer Model is developed $dnsgw_counter = 1; - while (isset($config['system']["dns{$dnsgw_counter}gw"])) { - if ($this->name->value == $config['system']["dns{$dnsgw_counter}gw"]) { + while ($this->get_config("system/dns{$dnsgw_counter}gw")) { + if ($this->name->value == $this->get_config("system/dns{$dnsgw_counter}gw")) { throw new ValidationError( message: 'Gateway cannot be disabled because it is in use by a system DNS ' . "server with ID `$dnsgw_counter`", From 80b14c9d5d86848b1de81bfde38992425b440dc2 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 16:42:25 -0700 Subject: [PATCH 02/36] chore: undo config changes in tests --- .../files/usr/local/pkg/RESTAPI/Core/TestCase.inc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc index 505ca285a..b9a2c97e0 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/TestCase.inc @@ -44,6 +44,9 @@ class TestCase { * @throws Error|Exception The exception thrown by failed Tests, typically an AssertionError. */ public function run(): void { + # Need direct config access to revert any changes made by Tests + global $config; + # Collect available environment variables $this->get_envs(); @@ -61,7 +64,7 @@ class TestCase { # If this method starts with `test`, run the function. if (str_starts_with($method, 'test')) { # Backup the current config so we can undo any changes made during the test - $original_config = config_get_path(""); + $original_config = unserialize(serialize($config)); # Set the current method undergoing testing $this->method = $method; @@ -75,14 +78,14 @@ class TestCase { $this->$method(); } catch (Error | Exception $e) { # Restore the original configuration, teardown the TestCase and throw the encountered error - config_set_path("", $original_config); + $config = $original_config; write_config("Restored config after API test '$method'"); $this->teardown(); throw $e; } # Restore the config as it was when the test began. - config_set_path("", $original_config); + $config = $original_config; write_config("Restored config after API test '$method'"); } } From e2ae2e91e942f22bb515ab94f236796b3c99f476 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 16:54:05 -0700 Subject: [PATCH 03/36] fix: fixed broken priv handling in Plus 24.11 This corrects the handling of pfSense's getUserEntry return value. Starting in 24.11, this returns the user entry nested under an 'item' key. --- .../files/usr/local/pkg/RESTAPI/Core/Auth.inc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc index 900e356c4..9bbae41c6 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc @@ -162,7 +162,14 @@ class Auth { public function authorize(): bool { # Variables $is_not_authorized = false; - $this->client_privileges = get_user_privileges(getUserEntry($this->username)); + + # Start with pfSense 24.11, getUserEntry returns an array with the key 'item' containing the user data. + # We need to handle both cases to support both. + $user_ent = getUserEntry($this->username); + $user_ent = (array_key_exists('item', $user_ent)) ? $user_ent['item'] : $user_ent; + + # Obtain the client's privileges and check if they have the required privileges + $this->client_privileges = get_user_privileges($user_ent); # This client is not authorized if the client does not have at least one of the required privileges if ($this->required_privileges and !array_intersect($this->required_privileges, $this->client_privileges)) { From d6063aab255854763fdfab2bfbebdb9b01e7c96e Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 16:55:01 -0700 Subject: [PATCH 04/36] ci: add pfSense Plus 24.11 to build matrix --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3db366a79..ea95f957f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,8 @@ jobs: PFSENSE_VERSION: "2.7.2" - FREEBSD_VERSION: FreeBSD-15.0-CURRENT PFSENSE_VERSION: "24.03" + - FREEBSD_VERSION: FreeBSD-15.0-CURRENT + PFSENSE_VERSION: "24.11" steps: - uses: actions/checkout@v4 From 8253481d48b7f4c1e713edd046a3f2845740d276 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 16:57:17 -0700 Subject: [PATCH 05/36] fix: escape branch in make_package.py --- tools/make_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/make_package.py b/tools/make_package.py index 42d52959c..ec58dba39 100644 --- a/tools/make_package.py +++ b/tools/make_package.py @@ -140,7 +140,7 @@ def build_on_remote_host(self): "mkdir -p ~/build/", f"rm -rf ~/build/{REPO_NAME}", f"git clone https://github.com/{REPO_OWNER}/{REPO_NAME}.git ~/build/{REPO_NAME}/", - f"git -C ~/build/{REPO_NAME} checkout " + self.args.branch, + f"git -C ~/build/{REPO_NAME} checkout '{self.args.branch}'", f"composer install --working-dir ~/build/{REPO_NAME}", f"cp -r ~/build/{REPO_NAME}/vendor/* {includes_dir}", f"python3 ~/build/{REPO_NAME}/tools/make_package.py --tag {self.args.tag} {notests}", From de2d2ca7cba1b53d3b233abf7c5cf0dc487b71f1 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 17:00:31 -0700 Subject: [PATCH 06/36] style: run prettier on changed files --- composer.lock | 290 +++++++++--------- .../files/usr/local/pkg/RESTAPI/Core/Auth.inc | 2 +- .../usr/local/pkg/RESTAPI/Core/Model.inc | 2 +- 3 files changed, 142 insertions(+), 152 deletions(-) diff --git a/composer.lock b/composer.lock index 3125900bd..87ac4f886 100644 --- a/composer.lock +++ b/composer.lock @@ -1,156 +1,146 @@ { - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "df390555a5bc256768abe12103f30b54", - "packages": [ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "df390555a5bc256768abe12103f30b54", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["BSD-3-Clause"], + "authors": [ { - "name": "firebase/php-jwt", - "version": "v6.10.0", - "source": { - "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", - "shasum": "" - }, - "require": { - "php": "^7.4||^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" - }, - "suggest": { - "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" - }, - "type": "library", - "autoload": { - "psr-4": { - "Firebase\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Neuman Vong", - "email": "neuman+pear@twilio.com", - "role": "Developer" - }, - { - "name": "Anant Narayanan", - "email": "anant@php.net", - "role": "Developer" - } - ], - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "jwt", - "php" - ], - "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" - }, - "time": "2023-12-01T16:26:39+00:00" + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" }, { - "name": "webonyx/graphql-php", - "version": "v15.18.1", - "source": { - "type": "git", - "url": "https://github.com/webonyx/graphql-php.git", - "reference": "a167afab66d8aa74b7f552759c0bbd906afb4134" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/a167afab66d8aa74b7f552759c0bbd906afb4134", - "reference": "a167afab66d8aa74b7f552759c0bbd906afb4134", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.4 || ^8" - }, - "require-dev": { - "amphp/amp": "^2.6", - "amphp/http-server": "^2.1", - "dms/phpunit-arraysubset-asserts": "dev-master", - "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.64.0", - "mll-lab/php-cs-fixer-config": "^5.9.2", - "nyholm/psr7": "^1.5", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.12.10", - "phpstan/phpstan-phpunit": "1.4.1", - "phpstan/phpstan-strict-rules": "1.6.1", - "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", - "psr/http-message": "^1 || ^2", - "react/http": "^1.6", - "react/promise": "^2.0 || ^3.0", - "rector/rector": "^1.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", - "thecodingmachine/safe": "^1.3 || ^2" - }, - "suggest": { - "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" - }, - "type": "library", - "autoload": { - "psr-4": { - "GraphQL\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", - "keywords": [ - "api", - "graphql" - ], - "support": { - "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.18.1" - }, - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], - "time": "2024-11-13T16:21:54+00:00" + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": ["jwt", "php"], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + }, + "time": "2023-12-01T16:26:39+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.18.1", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "a167afab66d8aa74b7f552759c0bbd906afb4134" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/a167afab66d8aa74b7f552759c0bbd906afb4134", + "reference": "a167afab66d8aa74b7f552759c0bbd906afb4134", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.4 || ^8" + }, + "require-dev": { + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.64.0", + "mll-lab/php-cs-fixer-config": "^5.9.2", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.12.10", + "phpstan/phpstan-phpunit": "1.4.1", + "phpstan/phpstan-strict-rules": "1.6.1", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^1.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7", + "thecodingmachine/safe": "^1.3 || ^2" + }, + "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", + "psr/http-message": "To use standard GraphQL server", + "react/promise": "To leverage async resolving on React PHP platform" + }, + "type": "library", + "autoload": { + "psr-4": { + "GraphQL\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": ["MIT"], + "description": "A PHP port of GraphQL reference implementation", + "homepage": "https://github.com/webonyx/graphql-php", + "keywords": ["api", "graphql"], + "support": { + "issues": "https://github.com/webonyx/graphql-php/issues", + "source": "https://github.com/webonyx/graphql-php/tree/v15.18.1" + }, + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.6.0" + ], + "time": "2024-11-13T16:21:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc index 9bbae41c6..94f99bbf3 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc @@ -166,7 +166,7 @@ class Auth { # Start with pfSense 24.11, getUserEntry returns an array with the key 'item' containing the user data. # We need to handle both cases to support both. $user_ent = getUserEntry($this->username); - $user_ent = (array_key_exists('item', $user_ent)) ? $user_ent['item'] : $user_ent; + $user_ent = array_key_exists('item', $user_ent) ? $user_ent['item'] : $user_ent; # Obtain the client's privileges and check if they have the required privileges $this->client_privileges = get_user_privileges($user_ent); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index 2bb43abc0..36424af7b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -675,7 +675,7 @@ class Model { * @param bool $force_parse Force an entire reparse of the config.xml file instead of the cached config. */ public static function reload_config(bool $force_parse = false): void { - config_set_path("", parse_config(parse: $force_parse)); + config_set_path('', parse_config(parse: $force_parse)); } /** From 96ab4c07cff87e51491daf8efa84272b7c5441ae Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 6 Dec 2024 17:03:28 -0700 Subject: [PATCH 07/36] chore: remove boto3 from requriements.txt This was initially used to run the testing environment in AWS but it proved to be too slow and costly to sustain. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eed3ea924..cc5659e41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ jinja2~=3.1.4 pylint~=3.3.1 black~=24.10.0 mkdocs~=1.6.1 -boto3~=1.35.54 \ No newline at end of file From ef2075f22ca2beb80d87eb581d930c950ac412a7 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 7 Dec 2024 20:26:26 -0700 Subject: [PATCH 08/36] fix: don't use / as root path in CARP model --- pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CARP.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CARP.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CARP.inc index 9292a7346..2b5c20f24 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CARP.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CARP.inc @@ -69,7 +69,7 @@ class CARP extends Model { public function get_carp_internal(): array { return [ 'enable' => $this->is_carp_enabled(), - 'maintenance_mode' => $this->is_config_enabled('/', 'virtualip_carp_maintenancemode'), + 'maintenance_mode' => $this->is_config_enabled('', 'virtualip_carp_maintenancemode'), ]; } From 13b81c254742d9e0f7092c6552e7ef212fdc27ac Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 9 Dec 2024 21:58:42 -0700 Subject: [PATCH 09/36] feat: add Models and Endpoints for GRE interfaces #156 --- .../Endpoints/InterfaceGREEndpoint.inc | 23 +++ .../Endpoints/InterfaceGREsEndpoint.inc | 24 +++ .../local/pkg/RESTAPI/Models/InterfaceGRE.inc | 167 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREEndpoint.inc create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREsEndpoint.inc create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREEndpoint.inc new file mode 100644 index 000000000..51760e2ce --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREEndpoint.inc @@ -0,0 +1,23 @@ +url = '/api/v2/interface/gre'; + $this->model_name = 'InterfaceGRE'; + $this->request_method_options = ['GET', 'POST', 'PATCH', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREsEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREsEndpoint.inc new file mode 100644 index 000000000..a9c3a3b37 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceGREsEndpoint.inc @@ -0,0 +1,24 @@ +url = '/api/v2/interface/gres'; + $this->model_name = 'InterfaceGRE'; + $this->many = true; + $this->request_method_options = ['GET', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc new file mode 100644 index 000000000..61c008f9e --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc @@ -0,0 +1,167 @@ +config_path = 'gres/gre'; + $this->many = true; + $this->always_apply = true; + + # Set model fields + $this->if = new InterfaceField( + required: true, + help_text: 'The pfSense interface interface serving as the local address to be used for the GRE tunnel.', + ); + $this->greif = new StringField( + default: null, + allow_null: true, + read_only: true, + help_text: 'The real interface name for this GRE interface.', + ); + $this->descr = new StringField( + default: '', + allow_empty: true, + help_text: 'A description for this GRE interface.', + ); + $this->add_static_route = new BooleanField( + default: false, + internal_name: 'link1', + help_text: 'Whether to add an explicit static route for the remote inner tunnel address/subnet via the ' . + 'local tunnel address.', + ); + $this->remote_addr = new StringField( + required: true, + internal_name: 'remote-addr', + validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: true)], + help_text: 'The remote address to use for the GRE tunnel.', + ); + $this->tunnel_local_addr = new StringField( + default: null, + allow_null: true, + internal_name: 'tunnel-local-addr', + validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)], + help_text: 'The local IPv4 address to use for the GRE tunnel.', + ); + $this->tunnel_remote_addr = new StringField( + required: true, + unique: true, + internal_name: 'tunnel-remote-addr', + conditions: ['!tunnel_local_addr' => null], + validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: false)], + help_text: 'The remote IPv4 address to use for the GRE tunnel.', + ); + $this->tunnel_remote_net = new IntegerField( + default: 32, + minimum: 1, + maximum: 32, + internal_name: 'tunnel-remote-net', + conditions: ['!tunnel_local_addr' => null], + help_text: 'The remote IPv4 subnet bitmask to use for the GRE tunnel.', + ); + $this->tunnel_local_addr6 = new StringField( + default: null, + allow_null: true, + internal_name: 'tunnel-local-addr6', + validators: [new IPAddressValidator(allow_ipv4: false, allow_ipv6: true)], + help_text: 'The local IPv6 address to use for the GRE tunnel.', + ); + $this->tunnel_remote_addr6 = new StringField( + required: true, + unique: true, + internal_name: 'tunnel-remote-addr6', + conditions: ['!tunnel_local_addr6' => null], + validators: [new IPAddressValidator(allow_ipv4: false, allow_ipv6: true)], + help_text: 'The remote IPv6 address to use for the GRE tunnel.', + ); + $this->tunnel_remote_net6 = new IntegerField( + default: 128, + minimum: 1, + maximum: 128, + internal_name: 'tunnel-remote-net6', + conditions: ['!tunnel_local_addr6' => null], + help_text: 'The remote IPv6 subnet bitmask to use for the GRE tunnel.', + ); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Adds extra validation to this entire Model. + * @throws ValidationError If neither a local IPv4 nor IPv6 tunnel address is present. + */ + public function validate_extra(): void { + # Require either a local IPv4 and/or IPv6 address to present + if (!$this->tunnel_local_addr->value and !$this->tunnel_local_addr6->value) { + throw new ValidationError( + message: 'GRE tunnel must have a `tunnel_local_addr` and/or `tunnel_local_addr6`.', + response_id: 'INTERFACE_GRE_NO_LOCAL_ADDRESS', + ); + } + } + + /** + * Applies changes to this Interface GRE Tunnel. + */ + public function apply(): void { + # Create the GRE interface if no greif exists + if (!$this->greif->value) { + interface_gre_configure($this->to_internal()); + } + + # If the greif is already assigned to an interface, reconfigure the interface + $if_q = NetworkInterface::query(if: $this->greif->value); + if ($if_q->exists()) { + interface_configure($if_q->first()->id); + } + } + + /** + * Applies the deletion of this Interface GRE Tunnel. + */ + public function apply_delete(): void { + pfSense_interface_destroy($this->greif->value); + } + + /** + * Extend the default _delete method to prevent deletion of the GRE interface while in use. + * @throws ConflictError If the GRE interface is in use. + */ + public function _delete(): void { + # If the greif is in use, don't allow deletion + $if_q = NetworkInterface::query(if: $this->greif->value); + if ($if_q->exists()) { + throw new ConflictError( + message: 'Cannot delete GRE interface while it is in use.', + response_id: 'INTERFACE_GRE_CANNOT_DELETE_WHILE_IN_USE', + ); + } + + parent::_delete(); + } +} From f5e08a9ba4f65781ba51e99e3b232bf85eb98dbe Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 11 Dec 2024 21:57:20 -0700 Subject: [PATCH 10/36] fix: ensure config is loaded in forms --- pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc index 5ece7ee03..fe691e89c 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Form.inc @@ -319,7 +319,7 @@ class Form { */ final public function print_form(): void { # Print the static pfSense UI and include any custom CSS or JS files - global $user_settings; + global $config, $user_settings; $pgtitle = $this->title_path; include 'head.inc'; echo ""; From ec3d22b7e35dd1696f181227ffe9bc3a15783254 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 11 Dec 2024 22:26:59 -0700 Subject: [PATCH 11/36] fix: handle gre tunnel apply correctly --- .../local/pkg/RESTAPI/Models/InterfaceGRE.inc | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc index 61c008f9e..c88980caf 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc @@ -129,11 +129,6 @@ class InterfaceGRE extends Model { * Applies changes to this Interface GRE Tunnel. */ public function apply(): void { - # Create the GRE interface if no greif exists - if (!$this->greif->value) { - interface_gre_configure($this->to_internal()); - } - # If the greif is already assigned to an interface, reconfigure the interface $if_q = NetworkInterface::query(if: $this->greif->value); if ($if_q->exists()) { @@ -148,6 +143,23 @@ class InterfaceGRE extends Model { pfSense_interface_destroy($this->greif->value); } + /** + * Extend the default _create method to create the GRE interface and obtain the real interface name. + */ + public function _create(): void + { + $this->greif->value = interface_gre_configure($this->to_internal()); + parent::_create(); + } + + /** + * Extend the default _update method to reconfigure the GRE interface. + */ + public function _update(): void { + interface_gre_configure($this->to_internal()); + parent::_update(); + } + /** * Extend the default _delete method to prevent deletion of the GRE interface while in use. * @throws ConflictError If the GRE interface is in use. From c00e1a9f0e089e85b8d4e4af4af4205f081b492a Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 11 Dec 2024 22:53:48 -0700 Subject: [PATCH 12/36] fix: adjust gre in use error id --- .../files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc index c88980caf..62dc1d747 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc @@ -120,7 +120,7 @@ class InterfaceGRE extends Model { if (!$this->tunnel_local_addr->value and !$this->tunnel_local_addr6->value) { throw new ValidationError( message: 'GRE tunnel must have a `tunnel_local_addr` and/or `tunnel_local_addr6`.', - response_id: 'INTERFACE_GRE_NO_LOCAL_ADDRESS', + response_id: 'INTERFACE_GRE_HAS_NO_LOCAL_ADDRESS', ); } } From aad2d6607bb7b566c67a6c499e3e8cd56d8a222c Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 11 Dec 2024 22:54:10 -0700 Subject: [PATCH 13/36] tests: add tests for InterfaceGRE --- .../Tests/APIModelsInterfaceGRETestCase.inc | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc new file mode 100644 index 000000000..986e4cd08 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc @@ -0,0 +1,104 @@ +assert_throws_response( + response_id: "INTERFACE_GRE_HAS_NO_LOCAL_ADDRESS", + code: 400, + callable: function () { + $gre = new InterfaceGRE(); + $gre->validate_extra(); + } + ); + + # Ensure no error is thrown in a tunnel_local_addr is assigned + $this->assert_does_not_throw( + callable: function () { + $gre = new InterfaceGRE(tunnel_local_addr: "1.2.3.4"); + $gre->validate_extra(); + } + ); + + # Ensure no error is thrown in a tunnel_local_addr6 is assigned + $this->assert_does_not_throw( + callable: function () { + $gre = new InterfaceGRE(tunnel_local_addr6: "1234::1"); + $gre->validate_extra(); + } + ); + } + + /** + * Ensure an existing GRE cannot be deleted if it is in use. + */ + public function test_delete_in_use(): void + { + # Ensure an error is thrown if the GRE is in use + $this->assert_throws_response( + response_id: "INTERFACE_GRE_CANNOT_DELETE_WHILE_IN_USE", + code: 409, + callable: function () { + $gre = new InterfaceGRE( + if: "lan", + remote_addr: "1.2.3.4", + tunnel_local_addr: "4.3.2.1", + tunnel_remote_addr: "1.1.2.2", + tunnel_remote_net: 32 + ); + $gre->create(); + + # Mock a GRE interface in use + InterfaceGRE::set_config("interfaces/opt99", ["if" => $gre->greif->value]); + $gre->delete(); + } + ); + + # Remove all GRE interfaces used in the test + InterfaceGRE::del_config("interfaces/opt99"); + InterfaceGRE::delete_all(); + } + + /** + * Ensure we can create, read, update and delete a GRE interface. + */ + public function test_crud(): void { + # Create a new GRE tunnel and ensure it's interface is seen in ifconfig + $gre = new InterfaceGRE( + if: "lan", + remote_addr: "1.2.3.4", + tunnel_local_addr: "4.3.2.1", + tunnel_remote_addr: "1.1.2.2", + tunnel_remote_net: 32 + ); + $gre->create(); + $ifconfig_output = new Command("ifconfig"); + $this->assert_str_contains($ifconfig_output->output, $gre->greif->value); + $this->assert_str_contains($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 1.2.3.4"); + $this->assert_str_contains($ifconfig_output->output, "inet 4.3.2.1 --> 1.1.2.2 netmask 0xffffffff"); + + # Update the GRE tunnel and ensure the changes are reflected in ifconfig + $gre->remote_addr->value = "2.3.4.5"; + $gre->update(); + $ifconfig_output = new Command("ifconfig"); + $this->assert_str_does_not_contain($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 1.2.3.4"); + $this->assert_str_contains($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 2.3.4.5"); + + # Delete the GRE tunnel and ensure it's interface is no longer seen in ifconfig + $gre->delete(); + $ifconfig_output = new Command("ifconfig"); + $this->assert_str_does_not_contain($ifconfig_output->output, $gre->greif->value); + + + } +} \ No newline at end of file From 8d468256d560cde4ba8e0f4fe4634384a86a84c9 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Wed, 11 Dec 2024 22:54:39 -0700 Subject: [PATCH 14/36] style: ran prettier on changed files --- .../local/pkg/RESTAPI/Models/InterfaceGRE.inc | 3 +- .../Tests/APIModelsInterfaceGRETestCase.inc | 66 +++++++++---------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc index 62dc1d747..2ec2f8cb3 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceGRE.inc @@ -146,8 +146,7 @@ class InterfaceGRE extends Model { /** * Extend the default _create method to create the GRE interface and obtain the real interface name. */ - public function _create(): void - { + public function _create(): void { $this->greif->value = interface_gre_configure($this->to_internal()); parent::_create(); } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc index 986e4cd08..08925400b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc @@ -6,66 +6,64 @@ use RESTAPI\Core\Command; use RESTAPI\Core\TestCase; use RESTAPI\Models\InterfaceGRE; -class APIModelsInterfaceGRETestCase extends TestCase -{ +class APIModelsInterfaceGRETestCase extends TestCase { /** * Ensure that either a `tunnel_local_addr` or `tunnel_local_addr6` is required. */ public function test_tunnel_local_addr_or_tunnel_local_addr6_required(): void { # Ensure an error is thrown if no local address is provided $this->assert_throws_response( - response_id: "INTERFACE_GRE_HAS_NO_LOCAL_ADDRESS", + response_id: 'INTERFACE_GRE_HAS_NO_LOCAL_ADDRESS', code: 400, callable: function () { $gre = new InterfaceGRE(); $gre->validate_extra(); - } + }, ); # Ensure no error is thrown in a tunnel_local_addr is assigned $this->assert_does_not_throw( callable: function () { - $gre = new InterfaceGRE(tunnel_local_addr: "1.2.3.4"); + $gre = new InterfaceGRE(tunnel_local_addr: '1.2.3.4'); $gre->validate_extra(); - } + }, ); # Ensure no error is thrown in a tunnel_local_addr6 is assigned $this->assert_does_not_throw( callable: function () { - $gre = new InterfaceGRE(tunnel_local_addr6: "1234::1"); + $gre = new InterfaceGRE(tunnel_local_addr6: '1234::1'); $gre->validate_extra(); - } + }, ); } /** * Ensure an existing GRE cannot be deleted if it is in use. */ - public function test_delete_in_use(): void - { + public function test_delete_in_use(): void { # Ensure an error is thrown if the GRE is in use $this->assert_throws_response( - response_id: "INTERFACE_GRE_CANNOT_DELETE_WHILE_IN_USE", + response_id: 'INTERFACE_GRE_CANNOT_DELETE_WHILE_IN_USE', code: 409, callable: function () { $gre = new InterfaceGRE( - if: "lan", - remote_addr: "1.2.3.4", - tunnel_local_addr: "4.3.2.1", - tunnel_remote_addr: "1.1.2.2", - tunnel_remote_net: 32 + if: 'lan', + remote_addr: '1.2.3.4', + tunnel_local_addr: '4.3.2.1', + tunnel_remote_addr: '1.1.2.2', + tunnel_remote_net: 32, ); $gre->create(); # Mock a GRE interface in use - InterfaceGRE::set_config("interfaces/opt99", ["if" => $gre->greif->value]); + InterfaceGRE::set_config('interfaces/opt99', ['if' => $gre->greif->value]); $gre->delete(); - } + }, ); # Remove all GRE interfaces used in the test - InterfaceGRE::del_config("interfaces/opt99"); + InterfaceGRE::del_config('interfaces/opt99'); InterfaceGRE::delete_all(); } @@ -75,30 +73,28 @@ class APIModelsInterfaceGRETestCase extends TestCase public function test_crud(): void { # Create a new GRE tunnel and ensure it's interface is seen in ifconfig $gre = new InterfaceGRE( - if: "lan", - remote_addr: "1.2.3.4", - tunnel_local_addr: "4.3.2.1", - tunnel_remote_addr: "1.1.2.2", - tunnel_remote_net: 32 + if: 'lan', + remote_addr: '1.2.3.4', + tunnel_local_addr: '4.3.2.1', + tunnel_remote_addr: '1.1.2.2', + tunnel_remote_net: 32, ); $gre->create(); - $ifconfig_output = new Command("ifconfig"); + $ifconfig_output = new Command('ifconfig'); $this->assert_str_contains($ifconfig_output->output, $gre->greif->value); - $this->assert_str_contains($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 1.2.3.4"); - $this->assert_str_contains($ifconfig_output->output, "inet 4.3.2.1 --> 1.1.2.2 netmask 0xffffffff"); + $this->assert_str_contains($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 1.2.3.4'); + $this->assert_str_contains($ifconfig_output->output, 'inet 4.3.2.1 --> 1.1.2.2 netmask 0xffffffff'); # Update the GRE tunnel and ensure the changes are reflected in ifconfig - $gre->remote_addr->value = "2.3.4.5"; + $gre->remote_addr->value = '2.3.4.5'; $gre->update(); - $ifconfig_output = new Command("ifconfig"); - $this->assert_str_does_not_contain($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 1.2.3.4"); - $this->assert_str_contains($ifconfig_output->output, "tunnel inet 192.168.1.1 --> 2.3.4.5"); + $ifconfig_output = new Command('ifconfig'); + $this->assert_str_does_not_contain($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 1.2.3.4'); + $this->assert_str_contains($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 2.3.4.5'); # Delete the GRE tunnel and ensure it's interface is no longer seen in ifconfig $gre->delete(); - $ifconfig_output = new Command("ifconfig"); + $ifconfig_output = new Command('ifconfig'); $this->assert_str_does_not_contain($ifconfig_output->output, $gre->greif->value); - - } -} \ No newline at end of file +} From c5d8ac1d1a9fe23a82027cd075bad5ab7a5a5461 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 18:46:50 -0700 Subject: [PATCH 15/36] feat: add model and endpoints for LAGG interfaces --- .../Endpoints/InterfaceLAGGEndpoint.inc | 23 +++ .../Endpoints/InterfaceLAGGsEndpoint.inc | 24 +++ .../pkg/RESTAPI/Models/InterfaceLAGG.inc | 147 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGEndpoint.inc create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGsEndpoint.inc create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGEndpoint.inc new file mode 100644 index 000000000..4e2b84421 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGEndpoint.inc @@ -0,0 +1,23 @@ +url = '/api/v2/interface/lagg'; + $this->model_name = 'InterfaceLAGG'; + $this->request_method_options = ['GET', 'POST', 'PATCH', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGsEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGsEndpoint.inc new file mode 100644 index 000000000..4fe78175d --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/InterfaceLAGGsEndpoint.inc @@ -0,0 +1,24 @@ +url = '/api/v2/interface/laggs'; + $this->model_name = 'InterfaceLAGG'; + $this->many = true; + $this->request_method_options = ['GET', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc new file mode 100644 index 000000000..b7161382f --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc @@ -0,0 +1,147 @@ +config_path = 'laggs/lagg'; + $this->many = true; + $this->always_apply = true; + + # Set model fields + $this->laggif = new StringField( + required: true, + read_only: true, + maximum_length: 32, + help_text: 'The real name of the LAGG interface.', + ); + $this->descr = new StringField( + default: '', + allow_empty: true, + help_text: 'A description to help document the purpose of this LAGG interface.', + ); + $this->members = new StringField( + required: true, + many: true, + many_minimum: 1, + help_text: 'A list of member interfaces to include in the LAGG.', + ); + $this->proto = new StringField( + required: true, + choices: ['lacp', 'failover', 'loadbalance', 'roundrobin', 'none'], + help_text: 'The LAGG protocol to use.', + ); + $this->lacptimeout = new StringField( + default: 'slow', + choices: ['slow', 'fast'], + conditions: ['proto' => 'lacp'], + help_text: 'The LACP timeout mode to use.', + ); + $this->lagghash = new StringField( + default: 'l2,l3,l4', + choices: ['l2', 'l3', 'l4', 'l2,l3', 'l2,l4', 'l3,l4', 'l2,l3,l4'], + conditions: ['proto' => ['lacp', 'loadbalance']], + help_text: 'The LAGG hash algorithm to use.', + ); + $this->failovermaster = new StringField( + default: 'auto', + conditions: ['proto' => 'failover'], + help_text: 'The failover master interface to use.', + ); + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Add extra validation to the `members` field. + * @param string $value The value to validate. + * @return string The validated value to assign. + * @throws ValidationError When the requested member interface does not exist. + */ + public function validate_members(string $value): string { + # Throw a validation error if the request member does not exist + if (!does_interface_exist($value)) { + throw new ValidationError( + message: "Member interface `$value` does not exist or is invalid.", + response_id: 'INTERFACE_LAGG_MEMBER_DOES_NOT_EXIST', + ); + } + + return $value; + } + + /** + * Adds extra validation to the `failovermaster` field. + * @param string $value The value to validate. + * @return string The validated value to assign. + * @throws ValidationError When the requested failover master interface is not assigned as a member. + */ + public function validate_failovermaster(string $value): string { + # Always allow the `auto` value + if ($value === 'auto') { + return $value; + } + + # Throw a validation error if the requested failover master is not a member + if (!in_array($value, $this->members->value)) { + throw new ValidationError( + message: "Failover master interface `$value` is not a member of the LAGG.", + response_id: 'INTERFACE_LAGG_MASTER_NOT_MEMBER', + ); + } + + return $value; + } + + /** + * Extends the default _create method to capture the LAGG interface name of the newly created LAGG. + */ + public function _create(): void { + $this->laggif->value = interface_lagg_configure($this->to_internal()); + parent::_create(); + } + + /** + * Extends the default _update method to reconfigure the updated LAGG. + */ + public function _update(): void { + interface_lagg_configure($this->to_internal()); + parent::_update(); + } + + /** + * Extends the default _delete method to prevent deletion of the LAGG interface if it is in use, and remove + * the LAGG interface from the system if it is not in use. + */ + public function _delete(): void { + # Query for an interface using this lagg + $if_q = NetworkInterface::query(if: $this->laggif->value); + if ($if_q->exists()) { + throw new ConflictError( + message: "Cannot delete LAGG interface `{$this->laggif->value}` because it is in use by interface " . + "with ID `{$if_q->first()->id}`.", + response_id: 'INTERFACE_LAGG_CANNOT_BE_DELETED_WHILE_IN_USE', + ); + } + + # Remove the LAGG interface from the system + pfSense_interface_destroy($this->laggif->value); + parent::_delete(); + } +} From aed2e191389bb9f53ddda320053ca998a1934f57 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 19:15:18 -0700 Subject: [PATCH 16/36] fix: correct the handling of applying laggs --- .../pkg/RESTAPI/Models/InterfaceLAGG.inc | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc index b7161382f..efa0a7d9b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/InterfaceLAGG.inc @@ -27,7 +27,6 @@ class InterfaceLAGG extends Model { # Set model fields $this->laggif = new StringField( - required: true, read_only: true, maximum_length: 32, help_text: 'The real name of the LAGG interface.', @@ -126,8 +125,7 @@ class InterfaceLAGG extends Model { } /** - * Extends the default _delete method to prevent deletion of the LAGG interface if it is in use, and remove - * the LAGG interface from the system if it is not in use. + * Extends the default _delete method to prevent deletion of the LAGG interface if it is in use. */ public function _delete(): void { # Query for an interface using this lagg @@ -140,8 +138,30 @@ class InterfaceLAGG extends Model { ); } - # Remove the LAGG interface from the system - pfSense_interface_destroy($this->laggif->value); parent::_delete(); } + + /** + * Reconfigures VLANs and interfaces using this LAGG after successful creation or update. + */ + public function apply(): void { + # Check for interfaces using this LAGG and reconfigure them + $if_q = NetworkInterface::query(if: $this->laggif->value); + foreach ($if_q->model_objects as $if) { + interface_configure($if->id); + } + + # Check for VLANs using this LAGG and reconfigure them + $vlan_q = InterfaceVLAN::query(if: $this->laggif->value); + foreach ($vlan_q->model_objects as $vlan) { + interface_vlan_configure($vlan->to_internal()); + } + } + + /** + * Applies the deletion of this LAGG interface. + */ + public function apply_delete(): void { + pfSense_interface_destroy($this->laggif->value); + } } From cdadd067a00334ab637343e80e61b14c7098ced4 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 21:18:37 -0700 Subject: [PATCH 17/36] tests: add tests for InterfaceLAGG model --- .../Tests/APIModelsInterfaceLAGGTestCase.inc | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceLAGGTestCase.inc diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceLAGGTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceLAGGTestCase.inc new file mode 100644 index 000000000..84ee7ea79 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceLAGGTestCase.inc @@ -0,0 +1,108 @@ +assert_throws_response( + response_id: 'INTERFACE_LAGG_MEMBER_DOES_NOT_EXIST', + code: 400, + callable: function () { + $lagg = new InterfaceLAGG(); + $lagg->validate_members('nonexistent'); + }, + ); + + # Ensure no error is thrown if the member interface exists + $this->assert_does_not_throw( + callable: function () { + $lagg = new InterfaceLAGG(); + $lagg->validate_members($this->env['PFREST_OPT1_IF']); + }, + ); + } + + /** + * Checks that the `failovermaster` field must be a member of the LAGG. + */ + public function test_failovermaster_field_must_be_member_of_lagg(): void { + # Ensure an error is thrown if the failover master is not a member + $this->assert_throws_response( + response_id: 'INTERFACE_LAGG_MASTER_NOT_MEMBER', + code: 400, + callable: function () { + $lagg = new InterfaceLAGG(); + $lagg->members->value = [$this->env['PFREST_OPT1_IF']]; + $lagg->validate_failovermaster('nonexistent'); + }, + ); + + # Ensure no error is thrown if the failover master is a member + $this->assert_does_not_throw( + callable: function () { + $lagg = new InterfaceLAGG(); + $lagg->members->value = [$this->env['PFREST_OPT1_IF']]; + $lagg->validate_failovermaster($this->env['PFREST_OPT1_IF']); + }, + ); + } + + /** + * Ensure that we can create, read, update and delete a LAGG interface. + */ + public function test_crud(): void { + # Create a new LAGG interface + $lagg = new InterfaceLAGG(members: [$this->env['PFREST_OPT1_IF']], proto: 'lacp'); + $lagg->create(); + $this->assert_is_not_empty($lagg->laggif->value); + $ifconfig_output = new Command("ifconfig {$lagg->laggif->value}"); + $this->assert_str_contains($ifconfig_output->output, 'laggproto lacp'); + $this->assert_str_contains($ifconfig_output->output, "laggport: {$this->env['PFREST_OPT1_IF']}"); + + # Update the LAGG interface + $lagg->proto->value = 'failover'; + $lagg->update(); + $ifconfig_output = new Command("ifconfig {$lagg->laggif->value}"); + $this->assert_str_contains($ifconfig_output->output, 'laggproto failover'); + $this->assert_str_does_not_contain($ifconfig_output->output, 'laggproto lacp'); + + # Delete the LAGG interface + $lagg->delete(); + $ifconfig_output = new Command("ifconfig {$lagg->laggif->value}"); + $this->assert_str_contains($ifconfig_output->output, 'does not exist'); + } + + /** + * Ensure that we cannot delete a LAGG that is in use. + */ + public function test_cannot_delete_lagg_in_use(): void { + # Create a new LAGG + $lagg = new InterfaceLAGG(members: [$this->env['PFREST_OPT1_IF']], proto: 'lacp'); + $lagg->create(); + + # Mock an interface that is using the LAGG + InterfaceLAGG::set_config('interfaces/opt99', ['if' => $lagg->laggif->value]); + + # Ensure we cannot delete the LAGG + $this->assert_throws_response( + response_id: 'INTERFACE_LAGG_CANNOT_BE_DELETED_WHILE_IN_USE', + code: 409, + callable: function () use ($lagg) { + # Try to delete the LAGG + $lagg->delete(); + }, + ); + + # Remove the mock interface and actually delete the LAGG + InterfaceLAGG::del_config('interfaces/opt99'); + $lagg->delete(); + } +} From 5d72c1fddde5e05033f72f9ca638c47d8c934651 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 21:20:39 -0700 Subject: [PATCH 18/36] docs(oas): clarify oas query param documentation --- .../files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc index bff7c7cf5..d8ad888fe 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc @@ -350,8 +350,8 @@ class OpenAPISchema extends Schema { 'explode' => true, 'description' => 'The arbitrary query parameters to include in the request.

' . - 'Note: This does not define an real parameter, rather it allows for any arbitrary query ' . - 'parameters to be included in the request.', + 'Note: This does not define a real parameter (e.g. there is no `query` parameter), '. + 'rather it allows for any arbitrary query parameters to be included in the request.', 'schema' => [ 'type' => 'object', 'default' => new stdClass(), From 040d285cfa771a9cfe97758afc86fbde4cb4b4ce Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 21:22:00 -0700 Subject: [PATCH 19/36] fix: restore old reload_config behavior --- pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index 36424af7b..ce04d08b2 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -675,7 +675,8 @@ class Model { * @param bool $force_parse Force an entire reparse of the config.xml file instead of the cached config. */ public static function reload_config(bool $force_parse = false): void { - config_set_path('', parse_config(parse: $force_parse)); + global $config; + $config = parse_config(parse: $force_parse); } /** From aa6cd7446b1e48e3c7649c1586cb12b56ab64bec Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 21:25:59 -0700 Subject: [PATCH 20/36] style: run prettier on changed files --- .../files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc index d8ad888fe..4a67f54b5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Schemas/OpenAPISchema.inc @@ -350,7 +350,7 @@ class OpenAPISchema extends Schema { 'explode' => true, 'description' => 'The arbitrary query parameters to include in the request.

' . - 'Note: This does not define a real parameter (e.g. there is no `query` parameter), '. + 'Note: This does not define a real parameter (e.g. there is no `query` parameter), ' . 'rather it allows for any arbitrary query parameters to be included in the request.', 'schema' => [ 'type' => 'object', From 293cbcaa058854110a9e1317a226b45091e2ab30 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 21:39:29 -0700 Subject: [PATCH 21/36] fix: handle missing gateways in RoutingGatewayGroup::from_internal_ipprotocol() #603 --- .../local/pkg/RESTAPI/Models/RoutingGatewayGroup.inc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGatewayGroup.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGatewayGroup.inc index 022140a65..25edefe66 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGatewayGroup.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/RoutingGatewayGroup.inc @@ -68,7 +68,16 @@ class RoutingGatewayGroup extends Model { protected function from_internal_ipprotocol(): string { # Check the IP protocol of the gateways in this group foreach ($this->priorities->value as $priority) { - $gw_obj = RoutingGateway::query(name: $priority['gateway'])->first(); + # Query for the gateway related to this priority + $gw_q = RoutingGateway::query(name: $priority['gateway']); + + # Skip this priority if the gateway does not exist. This can happen if the gateway is not in config. + if (!$gw_q->exists()) { + continue; + } + + # Check the IP protocol of the gateway + $gw_obj = $gw_q->first(); if ($gw_obj->ipprotocol->value === 'inet') { return 'inet'; } elseif ($gw_obj->ipprotocol->value === 'inet6') { From 95cbc35d3c0f8b3b68a6d7d8b1a8aa0075993dc7 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 22:41:42 -0700 Subject: [PATCH 22/36] tests: adjust expected string in ACMECertificateRenew test Seems an update to the acme package changed the text we are expected. This change updates the test to expect the new variation. --- .../pkg/RESTAPI/Tests/APIModelsACMECertificateTestCase.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsACMECertificateTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsACMECertificateTestCase.inc index 56f09d2ea..b724b5c7b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsACMECertificateTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsACMECertificateTestCase.inc @@ -86,7 +86,7 @@ class APIModelsACMECertificateTestCase extends TestCase { $renew = new ACMECertificateRenew(certificate: $cert->name->value, async: false); $renew->create(); $this->assert_equals($renew->status->value, 'completed'); - $this->assert_str_contains($renew->result_log->value, "Renew: '{$cert->a_domainlist->value[0]['name']}'"); + $this->assert_str_contains($renew->result_log->value, "Renewing: '{$cert->a_domainlist->value[0]['name']}'"); # Delete the ACME certificate $cert->delete(); From 48f90a7cb1e2745631e6890a8a473220ac1a3557 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 22:54:08 -0700 Subject: [PATCH 23/36] docs: add pfSense Plus 24.11 to supported versions --- docs/INSTALL_AND_CONFIG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 6e426f1fc..815b4f13f 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -16,6 +16,7 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements - pfSense CE 2.7.2 - pfSense Plus 24.03 +- pfSense Plus 24.11 !!! Tip Don't see your version of pfSense? Older versions of pfSense may be supported by older versions of this package. From 0daec1ca6abb74e1a0838072042c7c97983e1677 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 23:01:37 -0700 Subject: [PATCH 24/36] docs: add notes about dangers of installing on unsupported pfsense versions --- README.md | 5 +++++ docs/INSTALL_AND_CONFIG.md | 3 +++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 493d6d9d3..a15b8df37 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ API package and provide you with the information you need to configure and use t For new users, it is recommended to refer to the links in the [Getting Started section](#getting-started) to begin. Otherwise, the installation commands are included below for quick reference. +> [!WARNING] +> Before installing the package, always ensure your pfSense version is supported! Support version are listed [here](https://pfrest.org/INSTALL_AND_CONFIG/#supported-pfsense-versions). +> Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. + Install on pfSense CE: ```bash @@ -50,6 +54,7 @@ pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/re > the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases) to find the package built for > your version of pfSense. + ## Disclaimers > [!CAUTION] diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 815b4f13f..699518b68 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -18,6 +18,9 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements - pfSense Plus 24.03 - pfSense Plus 24.11 +!!! Warning + Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. + !!! Tip Don't see your version of pfSense? Older versions of pfSense may be supported by older versions of this package. Check the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases). From 2b6dd5e2d3c048d76c1071b25daf84e5ff56a56b Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Thu, 12 Dec 2024 23:04:24 -0700 Subject: [PATCH 25/36] docs: adjust readme structure --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a15b8df37..fb0d8d1d5 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,6 @@ API package and provide you with the information you need to configure and use t For new users, it is recommended to refer to the links in the [Getting Started section](#getting-started) to begin. Otherwise, the installation commands are included below for quick reference. -> [!WARNING] -> Before installing the package, always ensure your pfSense version is supported! Support version are listed [here](https://pfrest.org/INSTALL_AND_CONFIG/#supported-pfsense-versions). -> Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. - Install on pfSense CE: ```bash @@ -49,7 +45,11 @@ Install on pfSense Plus: pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-24.03-pkg-RESTAPI.pkg ``` -> [!IMPORTANT] +> [!WARNING] +> Before installing the package, always ensure your pfSense version is supported! Supported version are listed [here](https://pfrest.org/INSTALL_AND_CONFIG/#supported-pfsense-versions). +> Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. + +> [!TIP] > You may need to customize the installation command to reference the package built for your pfSense version. Check > the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases) to find the package built for > your version of pfSense. From bbb4ccbaea6d97842ea78a2f8cf4a15128dbd918 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 13 Dec 2024 19:07:01 -0700 Subject: [PATCH 26/36] fix: do not load all NetworkInterface objects in DHCPServer::init_interfaces This fixes an issue that caused extra processing and memory usage when interacting with the DHCPServer Model class. --- .../usr/local/pkg/RESTAPI/Models/DHCPServer.inc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc index 39fc3a761..44f779770 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc @@ -252,22 +252,22 @@ class DHCPServer extends Model { } /** - * Initializes configurations objects for defined interface that have not yet configured the DHCP server + * Initializes configuration objects for defined interface that have not yet configured the DHCP server */ private function init_interfaces(): void { # Variables - $ifs_using_dhcpd = array_keys($this->get_config(path: $this->config_path, default: [])); + $ifs_using_dhcp_server = array_keys($this->get_config(path: $this->config_path, default: [])); # Loop through each defined interface - foreach (NetworkInterface::read_all()->model_objects as $if) { + foreach ($this->get_config("interfaces", []) as $if_id => $if) { # Skip this interface if it is not a static interface or the subnet value is greater than or equal to 31 - if ($if->typev4->value !== 'static' or $if->subnet->value >= 31) { + if (empty($if["ipaddr"]) or $if["ipaddr"] === "dhcp" or $if->subnet->value >= 31) { continue; } # Otherwise, make this interface eligible for a DHCP server - if (!in_array($if->id, $ifs_using_dhcpd)) { - $this->set_config(path: "$this->config_path/$if->id", value: []); + if (!in_array($if_id, $ifs_using_dhcp_server)) { + $this->set_config(path: "$this->config_path/$if_id", value: []); } } } From f44f06569c688ea95181b73992505fa52891050c Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 13 Dec 2024 22:08:52 -0700 Subject: [PATCH 27/36] tests: prevent InterfaceGRE routes from conflicting with other tests --- .../RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc index 08925400b..f7e431427 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsInterfaceGRETestCase.inc @@ -74,22 +74,22 @@ class APIModelsInterfaceGRETestCase extends TestCase { # Create a new GRE tunnel and ensure it's interface is seen in ifconfig $gre = new InterfaceGRE( if: 'lan', - remote_addr: '1.2.3.4', - tunnel_local_addr: '4.3.2.1', - tunnel_remote_addr: '1.1.2.2', + remote_addr: '127.1.2.3', + tunnel_local_addr: '127.3.2.1', + tunnel_remote_addr: '127.5.5.5', tunnel_remote_net: 32, ); $gre->create(); $ifconfig_output = new Command('ifconfig'); $this->assert_str_contains($ifconfig_output->output, $gre->greif->value); - $this->assert_str_contains($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 1.2.3.4'); - $this->assert_str_contains($ifconfig_output->output, 'inet 4.3.2.1 --> 1.1.2.2 netmask 0xffffffff'); + $this->assert_str_contains($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 127.1.2.3'); + $this->assert_str_contains($ifconfig_output->output, 'inet 127.3.2.1 --> 127.5.5.5 netmask 0xffffffff'); # Update the GRE tunnel and ensure the changes are reflected in ifconfig $gre->remote_addr->value = '2.3.4.5'; $gre->update(); $ifconfig_output = new Command('ifconfig'); - $this->assert_str_does_not_contain($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 1.2.3.4'); + $this->assert_str_does_not_contain($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 127.1.2.3'); $this->assert_str_contains($ifconfig_output->output, 'tunnel inet 192.168.1.1 --> 2.3.4.5'); # Delete the GRE tunnel and ensure it's interface is no longer seen in ifconfig From d79fb368c1f96389417d2e345e02d3cb37616845 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 13 Dec 2024 22:09:29 -0700 Subject: [PATCH 28/36] docs: fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb0d8d1d5..651a48d37 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/re ``` > [!WARNING] -> Before installing the package, always ensure your pfSense version is supported! Supported version are listed [here](https://pfrest.org/INSTALL_AND_CONFIG/#supported-pfsense-versions). +> Before installing the package, always ensure your pfSense version is supported! Supported versions are listed [here](https://pfrest.org/INSTALL_AND_CONFIG/#supported-pfsense-versions). > Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. > [!TIP] From 7b5a9f79fe87eca36a8393f6bb55723f39d32316 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 13 Dec 2024 22:11:03 -0700 Subject: [PATCH 29/36] style: run prettier on changed files --- README.md | 1 - .../files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 651a48d37..bb759714c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/re > the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases) to find the package built for > your version of pfSense. - ## Disclaimers > [!CAUTION] diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc index 44f779770..636d418fc 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/DHCPServer.inc @@ -259,9 +259,9 @@ class DHCPServer extends Model { $ifs_using_dhcp_server = array_keys($this->get_config(path: $this->config_path, default: [])); # Loop through each defined interface - foreach ($this->get_config("interfaces", []) as $if_id => $if) { + foreach ($this->get_config('interfaces', []) as $if_id => $if) { # Skip this interface if it is not a static interface or the subnet value is greater than or equal to 31 - if (empty($if["ipaddr"]) or $if["ipaddr"] === "dhcp" or $if->subnet->value >= 31) { + if (empty($if['ipaddr']) or $if['ipaddr'] === 'dhcp' or $if->subnet->value >= 31) { continue; } From eba6105af3b41d4c2df63377248b454c4c4eb9a7 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 13 Dec 2024 22:40:59 -0700 Subject: [PATCH 30/36] feat: implement support carp unicast mode (plus only) #424 --- .../usr/local/pkg/RESTAPI/Models/VirtualIP.inc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc index a4e402de9..020342fb5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/VirtualIP.inc @@ -31,6 +31,8 @@ class VirtualIP extends Model { public IntegerField $advskew; public StringField $password; public StringField $carp_status; + public StringField $carp_mode; + public StringField $carp_peer; public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], mixed ...$options) { # Define model attributes @@ -122,6 +124,20 @@ class VirtualIP extends Model { help_text: 'The current CARP status of this virtual IP. This will display show whether this CARP node ' . 'is the primary or backup peer.', ); + $this->carp_mode = new StringField( + default: 'mcast', + choices: ['mcast', 'ucast'], + conditions: ['mode' => 'carp'], + help_text: 'The CARP mode to use for this virtual IP. Please note this field is exclusive to ' . + 'pfSense Plus and has no effect on CE.', + ); + $this->carp_peer = new StringField( + required: true, + conditions: ['carp_mode' => 'ucast'], + validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: true)], + help_text: 'The IP address of the CARP peer. Please note this field is exclusive to pfSense Plus and ' . + 'has no effect on CE.', + ); parent::__construct($id, $parent_id, $data, ...$options); } From d940dcd6b66cf98ea158973ce4f5978389327da5 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 14 Dec 2024 13:28:46 -0700 Subject: [PATCH 31/36] fix: do not require CertificateAuthority's prv field #605 --- .../usr/local/pkg/RESTAPI/Models/CertificateAuthority.inc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthority.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthority.inc index 1ebe10cbc..bfe4645ac 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthority.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthority.inc @@ -63,7 +63,8 @@ class CertificateAuthority extends Model { help_text: 'The X509 certificate string.', ); $this->prv = new Base64Field( - required: true, + default: null, + allow_null: true, sensitive: true, validators: [new X509Validator(allow_prv: true, allow_ecprv: true)], help_text: 'The X509 private key string.', From f2c529f0ad8a05ae3aa76dd48569265726599fa2 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sun, 15 Dec 2024 12:33:36 -0700 Subject: [PATCH 32/36] fix: ensure carefs are relinked after cert imports #605 --- .../local/pkg/RESTAPI/Models/Certificate.inc | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Certificate.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Certificate.inc index 4cbdac411..be3715e83 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Certificate.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/Certificate.inc @@ -17,6 +17,7 @@ use RESTAPI\Validators\X509Validator; class Certificate extends Model { public StringField $descr; public UIDField $refid; + public StringField $caref; public StringField $type; public Base64Field $csr; public Base64Field $crt; @@ -37,6 +38,13 @@ class Certificate extends Model { help_text: 'The unique ID assigned to this certificate for internal system use. This value is generated ' . 'by this system and cannot be changed.', ); + $this->caref = new StringField( + default: null, + allow_null: true, + read_only: true, + help_text: 'The unique ID of the existing pfSense Certificate Authority that signed this certificate.' . + 'This value is assigned by this system and cannot be changed.', + ); $this->type = new StringField( default: 'server', choices: ['server', 'user'], @@ -83,6 +91,19 @@ class Certificate extends Model { return $prv; } + /** + * Extends the default _create() method to ensure the certificate is fully imported before creating it. + */ + public function _create(): void { + # Import the cert first using pfSense's cert_import function and relink CAs (if necessary) + $config_data = $this->to_internal(); + cert_import($config_data, $this->crt->value, $this->prv->value); + $this->caref->value = $config_data['caref'] ?? null; + + # Create the Certificate object + parent::_create(); + } + /** * Extends the default _update() method to ensure any `csr` value is removed before updating a Certificate. */ From 7a55f2f9abe022d7032121bf0185c89a73079224 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 16 Dec 2024 22:50:31 -0700 Subject: [PATCH 33/36] fix: exclude unnecessary properties for serialization for the Model class #617 --- .../files/usr/local/pkg/RESTAPI/Core/Model.inc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index ce04d08b2..f4b5af153 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -342,6 +342,21 @@ class Model { $this->related_objects = new ModelSet(); } + /** + * Magic method to serialize only the necessary properties of this Model object. + * @return int[]|string[] + */ + public function __sleep() { + # Variables + $excluded_properties = ['initial_object', 'client', 'parent_model', 'related_objects']; + $properties = array_keys(get_object_vars($this)); + + # Filter out excluded properties from the list of properties to serialize + return array_filter($properties, function ($prop) use ($excluded_properties) { + return !in_array($prop, $excluded_properties, true); + }); + } + /** * Checks for options passed in during object construction and maps known options to Model properties. Any * options that are not known options to the core Model will be returned so they can be merged into From 95cadfa6a7ef525e5ed9061c70cb337f6b0b226b Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Mon, 16 Dec 2024 23:23:16 -0700 Subject: [PATCH 34/36] tests: add test for relinking certs to their ca upon import #605 --- .../Tests/APIModelsCertificateTestCase.inc | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateTestCase.inc index 71f6ba483..a5e5245d9 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateTestCase.inc @@ -4,6 +4,8 @@ namespace RESTAPI\Tests; use RESTAPI\Core\TestCase; use RESTAPI\Models\Certificate; +use RESTAPI\Models\CertificateAuthorityGenerate; +use RESTAPI\Models\CertificateGenerate; class APIModelsCertificateTestCase extends TestCase { const EXAMPLE_CRT = "-----BEGIN CERTIFICATE----- @@ -110,4 +112,56 @@ R02Pul8ulWQ8Kl3Q3pou8As7W1mMzA2DxQ== }, ); } + + /** + * Checks that certificates are relinked to their CAs (if found) when they are created/imported. + */ + public function test_certificate_is_relinked_to_ca_on_create(): void { + # Create a CA we can use to test the relinking + $ca = new CertificateAuthorityGenerate( + descr: 'test_ca', + trust: true, + randomserial: true, + is_intermediate: false, + keytype: 'RSA', + keylen: 2048, + digest_alg: 'sha256', + lifetime: 3650, + dn_country: 'US', + dn_state: 'UT', + dn_city: 'Salt Lake City', + dn_organization: 'ACME Org', + dn_organizationalunit: 'IT', + ); + $ca->always_apply = false; # Disable always_apply so we can test the create method without overloading cpu + $ca->create(); + + # Generate a new certificates using the CA + $cert = new CertificateGenerate( + descr: 'testcert', + caref: $ca->refid->value, + keytype: 'RSA', + keylen: 2048, + digest_alg: 'sha256', + lifetime: 3650, + type: 'user', + dn_country: 'US', + dn_state: 'UT', + dn_city: 'Salt Lake City', + dn_organization: 'ACME Org', + dn_organizationalunit: 'IT', + dn_commonname: 'testcert.example.com', + ); + $cert->create(); + + # Capture the crt and prv values of the certificate and delete it + $crt = $cert->crt->value; + $prv = $cert->prv->value; + $cert->delete(); + + # Import the certificate and ensure it is automatically relinked to the CA + $cert = new Certificate(descr: 'testcert', type: 'user', crt: $crt, prv: $prv); + $cert->create(); + $this->assert_equals($ca->refid->value, $cert->caref->value); + } } From 192546cefb43c1489f9fdc651120504cff79747b Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Tue, 17 Dec 2024 00:20:01 -0700 Subject: [PATCH 35/36] tests: add test for excessive memory usage when copying model #617 objects --- .../usr/local/pkg/RESTAPI/Core/Model.inc | 26 ++++++++-- .../usr/local/pkg/RESTAPI/Core/Tools.inc | 25 +++++++++ .../RESTAPI/Tests/APICoreModelTestCase.inc | 51 +++++++++++++++++++ .../RESTAPI/Tests/APICoreToolsTestCase.inc | 10 ++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc index f4b5af153..1e0604487 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc @@ -357,6 +357,22 @@ class Model { }); } + /** + * Creates an unlinked copy of this Model object. + * @return Model The cloned Model object. + */ + public function copy(): Model { + # Create a copy of this object by serializing and unserializing it + $copy = unserialize(serialize($this)); + + # Reassign the parent Model object if it exists + if ($this->parent_model) { + $copy->parent_model = $this->parent_model; + } + + return $copy; + } + /** * Checks for options passed in during object construction and maps known options to Model properties. Any * options that are not known options to the core Model will be returned so they can be merged into @@ -409,7 +425,7 @@ class Model { if ($this->config_path or $this->internal_callable) { $this->id = $id; $this->from_internal(); - $this->initial_object = unserialize(serialize($this)); + $this->initial_object = $this->copy(); } } } @@ -989,7 +1005,7 @@ class Model { # Obtain the object from its internal form $this->from_internal(); - $this->initial_object = unserialize(serialize($this)); + $this->initial_object = $this->copy(); } # Loop through each field in this Model and assign their values using the `representation_data`. @@ -2028,7 +2044,7 @@ class Model { } # Refresh the initial object - $this->initial_object = unserialize(serialize($this)); + $this->initial_object = $this->copy(); } # Return the current representation of this object @@ -2118,7 +2134,7 @@ class Model { } # Refresh the initial object - $this->initial_object = unserialize(serialize($this)); + $this->initial_object = $this->copy(); } # Return the current representation of this object @@ -2296,7 +2312,7 @@ class Model { } # Refresh the initial object - $this->initial_object = unserialize(serialize($this)); + $this->initial_object = $this->copy(); } # Return the current representation of this object diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Tools.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Tools.inc index dfede7047..ee03055b2 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Tools.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Tools.inc @@ -168,3 +168,28 @@ function get_classes_from_namespace(string $namespace, bool $shortnames = false) return $classes; } + +/** + * Generates a random MAC address string. + * @returns string A random MAC address string. + */ +function generate_mac_address(): string { + # Generate the first three octets (OUI) with fixed bits for unicast and locally administered addresses + $first_octet = dechex((mt_rand(0x00, 0xff) & 0xfe) | 0x02); // Ensure it is locally administered + $mac = [ + $first_octet, + dechex(mt_rand(0x00, 0xff)), + dechex(mt_rand(0x00, 0xff)), + dechex(mt_rand(0x00, 0xff)), + dechex(mt_rand(0x00, 0xff)), + dechex(mt_rand(0x00, 0xff)), + ]; + + # Zero-pad single-character hex values and return the MAC address + return implode( + ':', + array_map(function ($part) { + return str_pad($part, 2, '0', STR_PAD_LEFT); + }, $mac), + ); +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelTestCase.inc index e2d0d8411..306928de6 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelTestCase.inc @@ -7,10 +7,13 @@ use RESTAPI; use RESTAPI\Caches\RESTAPIVersionReleasesCache; use RESTAPI\Core\Auth; use RESTAPI\Core\Model; +use RESTAPI\Models\DHCPServer; +use RESTAPI\Models\DHCPServerStaticMapping; use RESTAPI\Models\FirewallAlias; use RESTAPI\Models\InterfaceVLAN; use RESTAPI\Models\SystemStatus; use RESTAPI\Models\Test; +use function RESTAPI\Models\DHCPServerStaticMapping; class APICoreModelTestCase extends RESTAPI\Core\TestCase { /** @@ -1197,4 +1200,52 @@ class APICoreModelTestCase extends RESTAPI\Core\TestCase { # Remove the write lock unlink(Model::WRITE_LOCK_FILE); } + + /** + * Ensure the copy() method creates an unlinked copy of the Model object. + */ + public function test_copy(): void { + # Create a new FirewallAlias model to test with + $alias = new FirewallAlias(name: 'test_alias', type: 'host', address: ['1.2.3.4']); + $alias->id = 5; + $alias_copy = $alias->copy(); + + # Ensure the values are equal + $this->assert_equals($alias->to_internal(), $alias_copy->to_internal()); + $this->assert_equals($alias_copy->id, 5); + $this->assert_equals($alias->to_representation(), $alias_copy->to_representation()); + + # Change the copy's values and ensure the original values are not changed + $alias_copy->name->value = 'new_name'; + $this->assert_not_equals($alias->name->value, $alias_copy->name->value); + } + + /** + * Ensure the copy() method does not use excessive memory in larger datasets. This is a regression test for #617. + */ + public function test_copy_memory_usage(): void { + # Generate lots of DHCP Server Static Mappings + $static_mappings = []; + $used_macs = []; + while (count($static_mappings) < 450) { + # Generate a unique mac address for each static mapping + $mac = RESTAPI\Core\Tools\generate_mac_address(); + if (in_array($mac, $used_macs)) { + continue; + } + $static_mappings[] = ['mac' => $mac]; + } + + # Update the DHCP server with our static mappings + $dhcp_server = new DHCPServer(id: 'lan', staticmap: $static_mappings); + $dhcp_server->update(); + + # Read a static mapping and copy it + $static_mapping = new DHCPServerStaticMapping(id: 50, parent_id: 'lan'); + $static_mapping->copy(); + + # Remove the static mappings + $dhcp_server->staticmap->value = []; + $dhcp_server->update(apply: true); + } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreToolsTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreToolsTestCase.inc index 00779f693..f62b220ac 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreToolsTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreToolsTestCase.inc @@ -6,6 +6,7 @@ require_once 'RESTAPI/autoloader.inc'; use RESTAPI\Core\TestCase; use function RESTAPI\Core\Tools\bandwidth_to_bits; +use function RESTAPI\Core\Tools\generate_mac_address; use function RESTAPI\Core\Tools\is_assoc_array; use function RESTAPI\Core\Tools\to_upper_camel_case; @@ -52,4 +53,13 @@ class APICoreToolsTestCase extends TestCase { $this->assert_equals(to_upper_camel_case('this is a test'), 'ThisIsATest'); $this->assert_equals(to_upper_camel_case('_this is-a test'), 'ThisIsATest'); } + + /** + * Checks that the generate_mac_address() function correctly generates a random MAC address. + */ + public function test_generate_mac_address(): void { + $mac_address = generate_mac_address(); + $this->assert_equals(strlen($mac_address), 17); + $this->assert_is_true(preg_match('/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/', $mac_address) === 1); + } } From d333b43d8af6f46caddf76f5c95e9d4b00e555d2 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Tue, 17 Dec 2024 21:50:13 -0700 Subject: [PATCH 36/36] fix: use queries to check for nested aliases #619 --- .../pkg/RESTAPI/Models/FirewallAlias.inc | 31 +++++++++++-------- .../Tests/APIModelsFirewallAliasTestCase.inc | 18 +++++++++++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc index dd417aa18..b626884b5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FirewallAlias.inc @@ -93,34 +93,39 @@ class FirewallAlias extends Model { * @returns string The validated value to set. * @throws ValidationError When the `address` value is invalid. */ - public function validate_address(string $addresses): string { + public function validate_address(string $address): string { + # Variables + $aliases = $this->read_all(); + $type = $this->type->value; + # Ensure value is a port, port range or port alias when `type` is `port` - if ($this->type->value === 'port' and !is_port_or_range_or_alias($addresses)) { + $port_alias_q = $aliases->query(name: $address, type: 'port'); + if ($type === 'port' and !is_port_or_range($address) and !$port_alias_q->exists()) { throw new ValidationError( - message: "Port alias 'address' value '$addresses' is not a valid port, range, or alias.", + message: "Port alias 'address' value '$address' is not a valid port, range, or alias.", response_id: 'INVALID_PORT_ALIAS_ADDRESS', ); } # Ensure value is an IP, FQDN or alias when `type` is `host` - if ($this->type->value === 'host' and !is_ipaddroralias($addresses) and !is_fqdn($addresses)) { + $host_alias_q = $aliases->query(name: $address, type: 'host'); + if ($type === 'host' and !is_ipaddr($address) and !is_fqdn($address) and !$host_alias_q->exists()) { throw new ValidationError( - message: "Host alias 'address' value '$addresses' is not a valid IP, FQDN, or alias.", + message: "Host alias 'address' value '$address' is not a valid IP, FQDN, or alias.", response_id: 'INVALID_HOST_ALIAS_ADDRESS', ); } # Ensure value is a CIDR, FQDN or alias when `type` is `network` - if ($this->type->value === 'network') { - if (!is_subnet($addresses) and alias_get_type($addresses) != 'network' and !is_fqdn($addresses)) { - throw new ValidationError( - message: "Host alias 'address' value '$addresses' is not a valid CIDR, FQDN, or alias.", - response_id: 'INVALID_NETWORK_ALIAS_ADDRESS', - ); - } + $network_alias_q = $aliases->query(name: $address, type: 'network'); + if ($type === 'network' and !is_subnet($address) and !is_fqdn($address) and !$network_alias_q->exists()) { + throw new ValidationError( + message: "Host alias 'address' value '$address' is not a valid CIDR, FQDN, or alias.", + response_id: 'INVALID_NETWORK_ALIAS_ADDRESS', + ); } - return $addresses; + return $address; } /** diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc index 21c5591c4..c98d8e425 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallAliasTestCase.inc @@ -144,4 +144,22 @@ class APIModelsFirewallAliasTestCase extends TestCase { }, ); } + + /** + * Checks that we can reference a nested alias during replace_all() calls. This is regression test for #619. + */ + public function test_nested_alias_reference_in_replace_all(): void { + # Ensure we can reference a nested alias during replace_all() calls without an error being thrown + $this->assert_does_not_throw( + callable: function () { + $alias = new FirewallAlias(); + $alias->replace_all( + data: [ + ['name' => 'test_alias1', 'type' => 'host', 'address' => []], + ['name' => 'test_alias2', 'type' => 'host', 'address' => ['test_alias1']], + ], + ); + }, + ); + } }