Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions docs/INSTALL_AND_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions docs/QUERIES_FILTERS_AND_SORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = null;
}

parent::_update();
}

/**
* Obtains the next available WireGuard tunnel interface name.
* @return string The next available WireGuard tunnel interface name (i.e. tun_wg0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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 {}
}