diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5faef96..0ba335f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "20" + node-version: "26" - name: Install npm dependencies run: npm i diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ec6e7cd7..f0a19bab 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 20 + node-version: 26 - name: Install npm packages run: npm install - name: Check Prettier diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d09b5a9c..19098db9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,8 @@ jobs: PFSENSE_VERSION: "25.11.1" - FREEBSD_VERSION: FreeBSD-16.0-CURRENT PFSENSE_VERSION: "26.03" + - FREEBSD_VERSION: FreeBSD-16.0-CURRENT + PFSENSE_VERSION: "26.03.1" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/README.md b/README.md index 8b902d1e..3687aa56 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ pkg-static add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/dow Install on pfSense Plus: ```bash -pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03-pkg-RESTAPI.pkg +pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03.1-pkg-RESTAPI.pkg ``` > [!WARNING] diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 63fadf30..0c21b0f2 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -17,6 +17,8 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements - pfSense CE 2.8.1 - pfSense Plus 25.11.1 - pfSense Plus 26.03 +- pfSense Plus 26.03.1 + !!! Warning Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. diff --git a/docs/QUERIES_FILTERS_AND_SORTING.md b/docs/QUERIES_FILTERS_AND_SORTING.md index 5e85533d..3d7773bf 100644 --- a/docs/QUERIES_FILTERS_AND_SORTING.md +++ b/docs/QUERIES_FILTERS_AND_SORTING.md @@ -56,10 +56,11 @@ value array contains a given value for array fields. Search for objects whose field value is within a given array of values (when the filter value is an array), or search for objects whose field value is a substring of a given string (when the filter value is a string). + - Name: `in` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` - - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` + - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` + - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` ### Less Than (lt) @@ -95,8 +96,8 @@ Search for objects whose field value matches a given format. - Name: `format` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` - - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` #### Supported Formats diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc index 7eddd76a..ca9b9277 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc @@ -32,7 +32,16 @@ class RESTAPIVersionReleasesCache extends Cache { ); $releases = json_decode($releases_json, true); - # Return the fetched releases if in a valid format, otherwise retain the existing cache - return is_array($releases) ? $releases : $this->read(); + # If we don't have an 'assets' key, the response is invalid. Log the response, keep the existing cache + if (!is_array($releases) or !array_key_exists(0, $releases) or !array_key_exists('assets', $releases[0])) { + Cache::log( + level: LOG_ERR, + message: "Releases data is malformed, received $releases_json. Keeping existing cache.", + ); + return $this->read(); + } + + # Return the fetched releases + return $releases; } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc index 5411d4a1..021e7591 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc @@ -3,6 +3,7 @@ namespace RESTAPI\Core; use RESTAPI\Models\CronJob; +use RESTAPI\Models\RESTAPISettings; use RESTAPI\Responses\FailedDependencyError; use RESTAPI\Responses\ServerError; use RESTAPI\Responses\ServiceUnavailableError; @@ -119,10 +120,18 @@ class Dispatcher { * @throws FailedDependencyError When a package requires a PHP include file that could not be found. */ private function check_required_packages(): void { + # Check if the user has opted in to using development (-devel) package variants + $pkg_config = RESTAPISettings::get_pkg_config(); + $allow_development_packages = ($pkg_config['allow_development_packages'] ?? '') === 'enabled'; + # Loop through each required package and ensure it is present on the system. foreach ($this->required_packages as $pkg) { - # Return an error if the package is not installed - if (!is_pkg_installed($pkg)) { + # When the development packages setting is enabled, also accept the -devel variant of the package + $devel_pkg = $pkg . '-devel'; + $pkg_installed = is_pkg_installed($pkg) || ($allow_development_packages && is_pkg_installed($devel_pkg)); + + # Return an error if neither the package nor its -devel variant (when allowed) is installed + if (!$pkg_installed) { throw new FailedDependencyError( message: "The requested action requires the '$pkg' package but it is not installed.", response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc index fcf38ce6..63a3b53b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc @@ -145,6 +145,18 @@ class WireGuardTunnel extends Model { return wg_is_key_clamped($privatekey) ? $privatekey : wg_clamp_key($privatekey); } + /** + * Extends the default _update method to ensure addresses are removed if the tunnel has an interface assignment + */ + public function _update(): void { + # Remove any existing addresses if this tunnel has an existing interface assignment + if (NetworkInterface::query(if: $this->name->value)->exists()) { + $this->addresses->value = []; + } + + parent::_update(); + } + /** * Obtains the next available WireGuard tunnel interface name. * @return string The next available WireGuard tunnel interface name (i.e. tun_wg0) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc index ceddaf44..52ea30d5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc @@ -6,6 +6,7 @@ use RESTAPI\Core\Dispatcher; use RESTAPI\Core\TestCase; use RESTAPI\Dispatchers\WireGuardApplyDispatcher; use RESTAPI\Models\Package; +use RESTAPI\Models\RESTAPISettings; class APICoreDispatcherTestCase extends TestCase { /** @@ -176,4 +177,100 @@ class APICoreDispatcherTestCase extends TestCase { # Remove the installed package $package->delete(); } + + /** + * Checks that a Dispatcher requiring a package that is not installed throws DISPATCHER_MISSING_REQUIRED_PACKAGE + * when the `allow_development_packages` setting is disabled (default). + */ + public function test_required_package_missing_throws_by_default(): void { + # Ensure the allow_development_packages setting is disabled + $settings = new RESTAPISettings(); + $settings->from_internal(); + $original = $settings->allow_development_packages->value; + + if ($original) { + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + # A Dispatcher requiring a package that is not installed should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required package is not installed + * and whose -devel counterpart is also not installed still throws DISPATCHER_MISSING_REQUIRED_PACKAGE. Since we + * cannot install arbitrary packages in tests, we verify the inverse: a package whose -devel name also doesn't + * exist still fails even with the setting enabled. + */ + public function test_devel_variant_still_fails_when_neither_installed(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # Even with the setting on, if neither the base package nor the -devel variant is installed, it should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required base package is installed + * (using pfSense-pkg-RESTAPI as a proxy for the base package) still passes the required package check. + */ + public function test_base_package_accepted_when_devel_enabled(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # The base package (pfSense-pkg-RESTAPI) is installed; the package check should still pass + $this->assert_does_not_throw( + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-RESTAPI']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } +} + +/** + * Defines a test-only Dispatcher used to exercise the $required_packages check without spawning a real background + * process. The $required_packages property is exposed publicly so individual tests can assign the package(s) to + * check, and _process() is intentionally a no-op so calling process() only runs the required package validation. + */ +class RequiredPackagesTestDispatcher extends Dispatcher { + public array $required_packages = []; + + /** + * Overrides the default _process() with a no-op so tests can validate the package check in isolation. + * @param mixed ...$arguments Unused arguments accepted to match the parent signature. + */ + protected function _process(mixed ...$arguments): void {} }