diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa49a528c..c58de7b4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,6 @@ jobs: strategy: matrix: include: - - FREEBSD_VERSION: FreeBSD-14.0-CURRENT - FREEBSD_ID: freebsd14 - FREEBSD_VERSION: FreeBSD-15.0-CURRENT FREEBSD_ID: freebsd15 @@ -57,8 +55,8 @@ jobs: strategy: matrix: include: - - PFSENSE_VERSION: pfSense-2.7.2-RELEASE - FREEBSD_ID: freebsd14 + - PFSENSE_VERSION: pfSense-2.8.0-RELEASE + FREEBSD_ID: freebsd15 steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -105,8 +103,8 @@ jobs: strategy: matrix: include: - - PFSENSE_VERSION: pfSense-2.7.2-RELEASE - FREEBSD_ID: freebsd14 + - PFSENSE_VERSION: pfSense-2.8.0-RELEASE + FREEBSD_ID: freebsd15 steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 @@ -132,8 +130,8 @@ jobs: strategy: matrix: include: - - PFSENSE_VERSION: pfSense-2.7.2-RELEASE - FREEBSD_ID: freebsd14 + - PFSENSE_VERSION: pfSense-2.8.0-RELEASE + FREEBSD_ID: freebsd15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea95f957f..71b6ac905 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ concurrency: build env: SWAGGER_UI_VERSION: "5.17.10" PYTHON_VERSION: "3.10" - DEFAULT_PFSENSE_VERSION: "2.7.2" + DEFAULT_PFSENSE_VERSION: "2.8.0" # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -26,10 +26,8 @@ jobs: matrix: include: # Note: The first item in this matrix must use env.DEFAULT_PFSENSE_VERSION as the PFSENSE_VERSION! - - FREEBSD_VERSION: FreeBSD-14.0-CURRENT - PFSENSE_VERSION: "2.7.2" - FREEBSD_VERSION: FreeBSD-15.0-CURRENT - PFSENSE_VERSION: "24.03" + PFSENSE_VERSION: "2.8.0" - FREEBSD_VERSION: FreeBSD-15.0-CURRENT PFSENSE_VERSION: "24.11" diff --git a/README.md b/README.md index bcc675b9c..7490d35a0 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ commands are included below for quick reference. Install on pfSense CE: ```bash -pkg-static add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.7.2-pkg-RESTAPI.pkg +pkg-static add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.8.0-pkg-RESTAPI.pkg ``` Install on pfSense Plus: ```bash -pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-24.03-pkg-RESTAPI.pkg +pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-24.11-pkg-RESTAPI.pkg ``` > [!WARNING] diff --git a/Vagrantfile b/Vagrantfile index 2f82c76e9..12b938bff 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,6 +1,6 @@ Vagrant.configure("2") do |config| config.vm.guest = :freebsd - config.vm.box = ENV['FREEBSD_VERSION'] || "freebsd/FreeBSD-14.0-CURRENT" + config.vm.box = ENV['FREEBSD_VERSION'] || "freebsd/FreeBSD-15.0-CURRENT" config.vm.synced_folder ".", "/vagrant", id: "vagrant-root", disabled: true config.ssh.shell = "sh" config.vm.base_mac = "080027D14C66" @@ -8,12 +8,12 @@ Vagrant.configure("2") do |config| config.vm.provision "shell", inline: <<-SHELL pkg update pkg upgrade - pkg install -y python38 + pkg install -y python311 pkg install -y php82-composer pkg install -y gitup gitup ports - su vagrant -c "python3.8 -m ensurepip" - su vagrant -c "python3.8 -m pip install jinja2" + su vagrant -c "python3.11 -m ensurepip" + su vagrant -c "python3.11 -m pip install jinja2" SHELL config.vm.provider "virtualbox" do |vb| vb.customize ["modifyvm", :id, "--memory", "1024"] diff --git a/composer.lock b/composer.lock index 47cf8b02d..472816ee7 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": "a32ab4a8fc071e68a251a9446caf15b9", - "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": "a32ab4a8fc071e68a251a9446caf15b9", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.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.11.1", - "source": { - "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^7.4", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psr/cache": "^2.0||^3.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.11.1" - }, - "time": "2025-04-09T20:32:01+00:00" + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" }, { - "name": "webonyx/graphql-php", - "version": "v15.20.0", - "source": { - "type": "git", - "url": "https://github.com/webonyx/graphql-php.git", - "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/60feb7ad5023c0ef411efbdf9792d3df5812e28f", - "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f", - "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.73.1", - "mll-lab/php-cs-fixer-config": "5.11.0", - "nyholm/psr7": "^1.5", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "2.1.8", - "phpstan/phpstan-phpunit": "2.0.4", - "phpstan/phpstan-strict-rules": "2.0.4", - "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": "^2.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", - "thecodingmachine/safe": "^1.3 || ^2 || ^3" - }, - "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.20.0" - }, - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], - "time": "2025-03-21T08:45:04+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.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "webonyx/graphql-php", + "version": "v15.20.0", + "source": { + "type": "git", + "url": "https://github.com/webonyx/graphql-php.git", + "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/60feb7ad5023c0ef411efbdf9792d3df5812e28f", + "reference": "60feb7ad5023c0ef411efbdf9792d3df5812e28f", + "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.73.1", + "mll-lab/php-cs-fixer-config": "5.11.0", + "nyholm/psr7": "^1.5", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.1.8", + "phpstan/phpstan-phpunit": "2.0.4", + "phpstan/phpstan-strict-rules": "2.0.4", + "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": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7", + "thecodingmachine/safe": "^1.3 || ^2 || ^3" + }, + "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.20.0" + }, + "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.3.0" + ], + "time": "2025-03-21T08:45:04+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 0cd0a40ae..c730e1d14 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -10,19 +10,18 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements !!! Warning - The package is currently not compatible with 32-bit builds of pfSense. It is recommended to use the [legacy v1 package](https://github.com/jaredhendrickson13/pfsense-api/tree/legacy) for 32-bit systems. - While the package should behave identically on 64-bit architectures other than amd64, automated testing only covers amd64 - builds of pfSense. Support on other architectures is not guaranteed. + builds of pfSense CE. Support on other architectures is not guaranteed. ### Supported pfSense versions -- pfSense CE 2.7.2 -- pfSense Plus 24.03 +- pfSense CE 2.8.0 - 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. + Don't see your version of pfSense listed? Older versions of pfSense may be supported by older versions of this package. Check the [releases page](https://github.com/jaredhendrickson13/pfsense-api/releases). ## Installing the package @@ -33,13 +32,13 @@ The pfSense REST API package is built just like any other pfSense package and ca **Install on pfSense CE** ```bash -pkg-static add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.7.2-pkg-RESTAPI.pkg +pkg-static add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.8.0-pkg-RESTAPI.pkg ``` **Install on pfSense Plus** ```bash -pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-24.03-pkg-RESTAPI.pkg +pkg-static -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-24.11-pkg-RESTAPI.pkg ``` !!! Important diff --git a/pfSense-pkg-RESTAPI/files/pkg-deinstall.in b/pfSense-pkg-RESTAPI/files/pkg-deinstall.in index b7866c4c9..b8823e77f 100644 --- a/pfSense-pkg-RESTAPI/files/pkg-deinstall.in +++ b/pfSense-pkg-RESTAPI/files/pkg-deinstall.in @@ -1,14 +1,23 @@ #!/bin/sh -if [ "${2}" != "DEINSTALL" ]; then - exit 0 -fi +if [ "${2}" == "DEINSTALL" ]; then + # Remove forms and dispatcher schedules + /usr/local/bin/php -f /usr/local/pkg/RESTAPI/.resources/scripts/manage.php removeforms + /usr/local/bin/php -f /usr/local/pkg/RESTAPI/.resources/scripts/manage.php unscheduledispatchers -# Unlink this package from pfSense -/usr/local/bin/php -f /etc/rc.packages %%PORTNAME%% POST-DEINSTALL + # Unlink this package from pfSense + /usr/local/bin/php -f /etc/rc.packages %%PORTNAME%% POST-DEINSTALL -# Remove the pfsense-RESTAPI command line tool -/bin/rm /usr/local/bin/pfsense-restapi + # Remove the pfsense-RESTAPI command line tool + /bin/rm /usr/local/bin/pfsense-restapi +fi -# Remove Endpoints -rm -rf /usr/local/www/api/v2 2>/dev/null +if [ "${2}" == "POST-DEINSTALL" ]; then + # Remove other files and directories generated by the package + printf "Removing endpoints, schemas, and caches..." + find /usr/local/www/api/v2/ -depth 1 -not -name documentation -not -name schema -exec rm -rf {} + + rm -rf /usr/local/www/api/v2/schema/* + rm /usr/local/pkg/RESTAPI/.resources/cache/* + rm /usr/local/pkg/RESTAPI/.resources/schemas/* + printf " done.\n" +fi diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/scripts/manage.php b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/scripts/manage.php index eda18d294..33524acd9 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/scripts/manage.php +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/scripts/manage.php @@ -82,6 +82,31 @@ function build_forms(): void { build_privs(); } +/** + * Removes Forms (UI pages) built by the REST API package. + */ +function remove_forms(): void { + # Print that we are starting to remove forms + print 'Removing forms... '; + + # Import each form class + foreach (glob('/usr/local/pkg/RESTAPI/Forms/*.inc') as $file) { + # Import classes files and create object + require_once $file; + $form_class = '\\RESTAPI\\Forms\\' . str_replace('.inc', '', basename($file)); + $form_obj = new $form_class(); + + # Remove the form URL + if (!$form_obj->delete_form_url()) { + print "failed! ($form_obj->url)"; + exit(1); + } + } + + # Print that the removal is done if we made it through the loop + print 'done.' . PHP_EOL; +} + /** * Automatically creates pfSense privileges for each Endpoint class defined in \RESTAPI\Endpoints and each Form class * defined in \RESTAPI\Forms. @@ -181,6 +206,30 @@ function schedule_dispatchers(): void { } } +/** + * Removes the cron jobs for all Dispatcher classes in \RESTAPI\Dispatchers and all Cache classes in \RESTAPI\Caches + * with configured schedules. + */ +function unschedule_dispatchers(): void { + # Variables + $dispatchers = get_classes_from_namespace('\\RESTAPI\\Dispatchers\\'); + $caches = get_classes_from_namespace('\\RESTAPI\\Caches\\'); + + # Include both Dispatcher classes and Cache classes. Cache classes inherit from the Dispatcher class. + $classes = array_merge($dispatchers, $caches); + + # Loop through each defined Dispatcher class and remove the cron jobs for dispatchers with schedules + foreach ($classes as $class) { + $dispatcher = new $class(); + if ($dispatcher->schedule) { + # Start removing the schedules + echo "Removing schedule for $class... "; + $dispatcher->remove_schedule(); + echo 'done.' . PHP_EOL; + } + } +} + /** * Refreshes the cache file by obtaining new day for a given Cache object. * @param string|null $cache_name The shortname of the Cache class that should have its cache file refreshed. @@ -367,7 +416,7 @@ function revert(string $tag): void { /** * Delete the REST API package and restart the webConfigurator to remove nginx changes. */ -function delete() { +function delete(): void { echo shell_exec('/usr/local/sbin/pkg-static delete -y pfSense-pkg-RESTAPI'); echo shell_exec('/etc/rc.restart_webgui'); } @@ -400,24 +449,26 @@ function help(): void { echo 'SYNTAX:' . PHP_EOL; echo ' pfsense-restapi ' . PHP_EOL; echo 'COMMANDS:' . PHP_EOL; - echo ' version : Display the current package version and build information' . PHP_EOL; - echo ' help : Display the help page (this page)' . PHP_EOL; - echo ' buildendpoints : Build all REST API Endpoints included in this package' . PHP_EOL; - echo ' buildforms : Build all REST API Forms included in this package' . PHP_EOL; - echo ' buildprivs : Build all REST API privileges included in this package' . PHP_EOL; - echo ' buildschemas : Build all Schema/documentation files' . PHP_EOL; - echo ' notifydispatcher : Start a dispatcher process' . PHP_EOL; - echo ' scheduledispatchers : Sets up cron jobs for dispatchers and caches on a schedule.' . PHP_EOL; - echo ' refreshcache : Refresh the cache file for a given cache class.' . PHP_EOL; - echo ' runtests : Run all REST API unit Tests. Warning: this may be disruptive!' . PHP_EOL; - echo ' restartwebgui : Restart the webConfigurator in the background' . PHP_EOL; - echo ' update : Update package to the latest stable version available' . PHP_EOL; - echo ' revert : Revert package to a specified version' . PHP_EOL; - echo ' delete : Delete package from this system' . PHP_EOL; - echo ' rotateserverkey : Rotate the REST API server key and remove all existing tokens' . PHP_EOL; - echo ' backup : Create a backup of the REST API configuration' . PHP_EOL; - echo ' restore : Restore the REST API configuration from the latest backup' . PHP_EOL; - echo " sync : Sync this system's REST API configuration to configured HA nodes" . PHP_EOL; + echo ' version : Display the current package version and build information' . PHP_EOL; + echo ' help : Display the help page (this page)' . PHP_EOL; + echo ' buildendpoints : Build all REST API Endpoints included in this package' . PHP_EOL; + echo ' buildforms : Build all REST API Forms included in this package' . PHP_EOL; + echo ' removeforms : Remove all REST API Forms included in this package' . PHP_EOL; + echo ' buildprivs : Build all REST API privileges included in this package' . PHP_EOL; + echo ' buildschemas : Build all Schema/documentation files' . PHP_EOL; + echo ' notifydispatcher : Start a dispatcher process' . PHP_EOL; + echo ' scheduledispatchers : Sets up cron jobs for dispatchers and caches on a schedule.' . PHP_EOL; + echo ' unscheduledispatchers : Removes cron jobs for dispatchers and caches on a schedule.' . PHP_EOL; + echo ' refreshcache : Refresh the cache file for a given cache class.' . PHP_EOL; + echo ' runtests : Run all REST API unit Tests. Warning: this may be disruptive!' . PHP_EOL; + echo ' restartwebgui : Restart the webConfigurator in the background' . PHP_EOL; + echo ' update : Update package to the latest stable version available' . PHP_EOL; + echo ' revert : Revert package to a specified version' . PHP_EOL; + echo ' delete : Delete package from this system' . PHP_EOL; + echo ' rotateserverkey : Rotate the REST API server key and remove all existing tokens' . PHP_EOL; + echo ' backup : Create a backup of the REST API configuration' . PHP_EOL; + echo ' restore : Restore the REST API configuration from the latest backup' . PHP_EOL; + echo " sync : Sync this system's REST API configuration to configured HA nodes" . PHP_EOL; echo PHP_EOL; } @@ -425,6 +476,10 @@ function help(): void { if ($argv[1] == 'buildforms') { build_forms(); } +# REMOVE_FORMS COMMAND +elseif ($argv[1] == 'removeforms') { + remove_forms(); +} # BUILDENDPOINTS COMMAND elseif ($argv[1] == 'buildendpoints') { build_endpoints(); @@ -447,6 +502,10 @@ function help(): void { elseif ($argv[1] == 'scheduledispatchers') { schedule_dispatchers(); } +# UNSCHEDULE_DISPATCHER COMMAND +elseif ($argv[1] == 'unscheduledispatchers') { + unschedule_dispatchers(); +} # REFRESH_CACHE COMMAND elseif ($argv[1] == 'refreshcache') { refresh_cache(cache_name: $argv[2]); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/AvailablePackageCache.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/AvailablePackageCache.inc index 37e2bc0eb..eb9a78013 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/AvailablePackageCache.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/AvailablePackageCache.inc @@ -12,8 +12,7 @@ use function RESTAPI\Dispatchers\get_pkg_info; */ class AvailablePackageCache extends Cache { public int $timeout = 120; - public string $schedule = 'hourly'; - + public string $schedule = '12 * * * *'; # Run at irregular interval to avoid conflicts with repo jobs /** * Retrieves the available package information to cache from external repos */ 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 6db84fb2e..74f00be6b 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 @@ -15,7 +15,7 @@ class RESTAPIVersionReleasesCache extends Cache { const RELEASES_URL = 'https://api.github.com/repos/jaredhendrickson13/pfsense-api/releases'; public int $timeout = 30; - public string $schedule = 'hourly'; + public string $schedule = '0 * * * *'; /** * Retrieves available release information from external repos and updates the releases cache files. 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 78e708d24..e3afd0730 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 @@ -19,10 +19,6 @@ class Dispatcher { * @const DISPATCH_SCRIPT The absolute file path to the dispatch.sh helper script. */ const DISPATCH_SCRIPT = '/usr/local/pkg/RESTAPI/.resources/scripts/dispatch.sh'; - /** - * @const SCHEDULE_OPTIONS The cron event schedules supported by Dispatchers. - */ - const SCHEDULE_OPTIONS = ['hourly', 'daily', 'weekly']; /** * @var string $full_name @@ -297,27 +293,27 @@ class Dispatcher { # Only proceed if a schedule was requested if ($this->schedule) { # Ensure the requested schedule is supported - if (!in_array($this->schedule, self::SCHEDULE_OPTIONS)) { + if (count(explode(' ', $this->schedule)) !== 5) { throw new ServerError( message: "Dispatcher schedule `$this->schedule` is not a supported schedule frequency.", response_id: 'DISPATCHER_SCHEDULE_UNSUPPORTED', ); } - # Check if a cron job already exists for this dispatcher - $dispatcher_cron_job_q = CronJob::query(command: $this->schedule_command); - - # Delete the cron job for this dispatcher if it exists, so we can recreate it with current values - if ($dispatcher_cron_job_q->exists()) { - $existing_cron_job = $dispatcher_cron_job_q->first(); - $existing_cron_job->packages = []; // Don't require the pfSense-pkg-Cron package to delete - $existing_cron_job->delete(); - } + # Remove any existing scheduled CronJob for this Dispatcher + $this->remove_schedule(); # Create the cron job for this dispatcher + $cron_expr = explode(' ', $this->schedule); $cron_job = new CronJob( - data: ['minute' => "@$this->schedule", 'who' => 'root', 'command' => $this->schedule_command], require_pkg: false, + minute: $cron_expr[0], + hour: $cron_expr[1], + mday: $cron_expr[2], + month: $cron_expr[3], + wday: $cron_expr[4], + who: 'root', + command: $this->schedule_command, ); $cron_job->create(); @@ -330,4 +326,19 @@ class Dispatcher { return null; } + + /** + * Removes the scheduled CronJob for this Dispatcher if it exists. + */ + public function remove_schedule(): void { + # Check if a cron job already exists for this dispatcher + $dispatcher_cron_job_q = CronJob::query(command: $this->schedule_command); + + # Delete the cron job for this dispatcher if it exists, so we can recreate it with current values + if ($dispatcher_cron_job_q->exists()) { + $existing_cron_job = $dispatcher_cron_job_q->first(); + $existing_cron_job->packages = []; // Don't require the pfSense-pkg-Cron package to delete + $existing_cron_job->delete(); + } + } } 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 769d1d195..f2bb0b1c0 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 @@ -285,7 +285,7 @@ class Form { $tb .= ''; if ($this->editable) { $tb .= - "" . + "' class='btn btn-success'>" . gettext('Add') . ''; $tb .= ''; @@ -562,4 +562,21 @@ class Form { } return false; } + + /** + * Deletes the Form's endpoint file from the pfSense webroot. + * @returns bool Returns true if the file was successfully deleted, otherwise false. + */ + public function delete_form_url(): bool { + # Assign the absolute path to the file. Assume index.php filename if not specified. + $filename = "/usr/local/www/$this->url"; + $filename = str_ends_with($filename, '.php') ? $filename : "$filename/index.php"; + $content = file_get_contents($filename); + + # Delete the file and return true if it was successfully deleted + if (is_file($filename) and str_contains($content, 'RESTAPI/Forms') and unlink($filename)) { + return true; + } + return false; + } } 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 0bec530c1..4c42b30c7 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 @@ -593,13 +593,29 @@ class Model { return $this->config_path; } + /** + * Normalizes a config path by removing extra slashes and ensuring it is not empty. + * @param string $path The config path to normalize. This is usually a string with '/' as separators. + * @return string The normalized config path + */ + public static function normalize_config_path(string $path): string { + # Remove leading and trailing slashes, and duplicate slashes, if slashes are present + if (str_contains($path, '/')) { + $path = trim($path, '/'); + $path = preg_replace('/\/+/', '/', $path); + } + + # Return the normalized config path + return $path; + } + /** * Initialize the configuration array of a specific config path * @param $path string config path with '/' as separators */ - protected static function init_config(string $path) { - # Initialize the configuration array of a specified path. - config_init_path($path); + protected static function init_config(string $path): void { + # Initialize the configuration array of a specified path + config_init_path(self::normalize_config_path($path)); } /** @@ -609,8 +625,8 @@ class Model { * @returns mixed value at path or $default if the path does not exist or if the * path keys an empty string and $default is non-null */ - public static function get_config(string $path, mixed $default = null) { - return config_get_path($path, $default); + public static function get_config(string $path, mixed $default = null): mixed { + return config_get_path(self::normalize_config_path($path), $default); } /** @@ -620,8 +636,8 @@ class Model { * @param $default mixed value to return if the path is not found * @returns mixed $val or $default if the path prefix does not exist */ - public static function set_config(string $path, mixed $value, mixed $default = null) { - return config_set_path($path, $value, $default); + public static function set_config(string $path, mixed $value, mixed $default = null): mixed { + return config_set_path(self::normalize_config_path($path), $value, $default); } /** @@ -630,7 +646,10 @@ class Model { * object that are not defined in a Field assigned to this Model will be left unchanged. * @param $path string The Model config path including any Model ID */ - public function merge_config(string $path) { + public function merge_config(string $path): void { + # Remove leading slashes and extra slashes + $path = self::normalize_config_path($path); + # Loop through each field known to this Model foreach ($this->get_fields() as $field) { # Determine the field path @@ -662,7 +681,7 @@ class Model { * @returns array copy of the removed value or null */ public static function del_config(string $path): mixed { - return config_del_path($path); + return config_del_path(self::normalize_config_path($path)); } /** @@ -675,7 +694,7 @@ class Model { * non-null value, otherwise false. */ public static function is_config_enabled(string $path, string $enable_key = 'enable'): bool { - return config_path_enabled($path, $enable_key); + return config_path_enabled(self::normalize_config_path($path), $enable_key); } /** diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/DHCPServerApplyDispatcher.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/DHCPServerApplyDispatcher.inc index 807b70237..32d70052b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/DHCPServerApplyDispatcher.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/DHCPServerApplyDispatcher.inc @@ -13,57 +13,7 @@ class DHCPServerApplyDispatcher extends Dispatcher { * Reloads the DHCP server and associated services. */ protected function _process(mixed ...$arguments): void { - $retvaldhcp = 0; - $retvaldns = 0; - - if (Model::is_config_enabled('dnsmasq') && Model::is_config_enabled('dnsmasq', 'regdhcpstatic')) { - $retvaldns |= services_dnsmasq_configure(); - if ($retvaldns == 0) { - clear_subsystem_dirty('hosts'); - clear_subsystem_dirty('dhcpd'); - } - if (Model::is_config_enabled('unbound') && Model::is_config_enabled('unbound', 'regdhcpstatic')) { - $retvaldns |= services_unbound_configure(); - if ($retvaldns == 0) { - clear_subsystem_dirty('unbound'); - clear_subsystem_dirty('hosts'); - clear_subsystem_dirty('dhcpd'); - } - } - } else { - $retvaldhcp |= services_dhcpd_configure(); - if ($retvaldhcp == 0) { - clear_subsystem_dirty('dhcpd'); - } - } - /* BIND package - Bug #3710 */ - if (!function_exists('is_package_installed')) { - require_once 'pkg-utils.inc'; - } - if ( - is_package_installed('pfSense-pkg-bind') && - Model::is_config_enabled('installedpackages/bind/config/0', 'enable_bind') - ) { - $reloadbind = false; - if (is_array(Model::get_config('installedpackages/bindzone'))) { - $bindzone = Model::get_config('installedpackages/bindzone/config'); - } else { - $bindzone = []; - } - for ($x = 0; $x < sizeof($bindzone); $x++) { - $zone = $bindzone[$x]; - if ($zone['regdhcpstatic'] == 'on') { - $reloadbind = true; - break; - } - } - if ($reloadbind === true) { - if (file_exists('/usr/local/pkg/bind.inc')) { - require_once '/usr/local/pkg/bind.inc'; - bind_sync(); - } - } - } - filter_configure(); + services_dhcpd_configure(); + clear_subsystem_dirty('dhcpd'); } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/RoutingApplyDispatcher.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/RoutingApplyDispatcher.inc index 7672a7cb7..eb3c62b5f 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/RoutingApplyDispatcher.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Dispatchers/RoutingApplyDispatcher.inc @@ -15,10 +15,12 @@ class RoutingApplyDispatcher extends Dispatcher { public function process(...$arguments): void { global $g; + # Clear the gateway cache to ensure the latest status is fetched during routing changes + unset($GLOBALS['GatewaysCache']); + # Check for the pending changes file and unserialize it if (file_exists("{$g['tmp_path']}/.system_routes.apply")) { $to_apply_list = unserialize(file_get_contents("{$g['tmp_path']}/.system_routes.apply")); - # Run commands to apply these changes foreach ($to_apply_list as $to_apply) { mwexec("{$to_apply}"); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesFreeRADIUSUserEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesFreeRADIUSUserEndpoint.inc new file mode 100644 index 000000000..c7d9edfcf --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesFreeRADIUSUserEndpoint.inc @@ -0,0 +1,26 @@ +url = '/api/v2/services/freeradius/user'; + $this->model_name = 'FreeRADIUSUser'; + $this->many = false; + $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/ServicesFreeRADIUSUsersEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesFreeRADIUSUsersEndpoint.inc new file mode 100644 index 000000000..cf3fcc81c --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/ServicesFreeRADIUSUsersEndpoint.inc @@ -0,0 +1,26 @@ +url = '/api/v2/services/freeradius/users'; + $this->model_name = 'FreeRADIUSUser'; + $this->many = true; + $this->request_method_options = ['GET', 'PUT', 'DELETE']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsAuthEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsAuthEndpoint.inc new file mode 100644 index 000000000..de3d2560f --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsAuthEndpoint.inc @@ -0,0 +1,23 @@ +url = '/api/v2/status/logs/auth'; + $this->model_name = 'AuthLog'; + $this->many = true; + $this->request_method_options = ['GET']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsOpenVPNEndpoint.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsOpenVPNEndpoint.inc new file mode 100644 index 000000000..038fb7c3b --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Endpoints/StatusLogsOpenVPNEndpoint.inc @@ -0,0 +1,23 @@ +url = '/api/v2/status/logs/openvpn'; + $this->model_name = 'OpenVPNLog'; + $this->many = true; + $this->request_method_options = ['GET']; + + # Construct the parent Endpoint object + parent::__construct(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPISettingsForm.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPISettingsForm.inc index 65239235c..418d1b721 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPISettingsForm.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Forms/SystemRESTAPISettingsForm.inc @@ -52,14 +52,14 @@ class SystemRESTAPISettingsForm extends Form { public array $buttons = [ 'rotate_server_key' => [ 'title' => 'Rotate Server Key', - 'icon' => 'fa-repeat', - 'classes' => ['btn-success', 'btn-sm'], + 'icon' => 'fa-solid fa-turn-up', + 'classes' => ['btn-success'], 'on_click' => "return confirm(\"Rotating the server key will void any existing JWTs. Proceed?\");", 'callable' => 'rotate_server_key', ], 'report_an_issue' => [ 'title' => 'Report an Issue', - 'icon' => 'fa-question-circle', + 'icon' => 'fa-solid fa-question-circle', 'link' => 'https://github.com/jaredhendrickson13/pfsense-api/issues/new/choose', 'classes' => ['btn-info'], ], diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/AuthLog.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/AuthLog.inc new file mode 100644 index 000000000..b178baa4a --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/AuthLog.inc @@ -0,0 +1,39 @@ +internal_callable = 'get_auth_log'; + $this->many = true; + + $this->text = new StringField(default: '', help_text: 'The raw text of the auth log entry.'); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Obtains the auth log as an array. This method is the internal callable for this Model. + * @return array The auth log as an array of objects. + */ + protected function get_auth_log(): array { + return $this->read_log($this->log_file); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthorityRenew.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthorityRenew.inc index 98f0d72ce..bbd6bc995 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthorityRenew.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateAuthorityRenew.inc @@ -95,7 +95,11 @@ class CertificateAuthorityRenew extends Model { public function _create(): void { # Extract details from the Certificate Authority to renew $ca_config = &lookup_ca($this->caref->value); - $this->oldserial->value = cert_get_serial($ca_config['crt']); + $this->oldserial->value = cert_get_serial($ca_config['item']['crt']); + $this->id = $ca_config['idx']; + + # The pfSense cert_renew() function expects a 'path' key with the config path + $ca_config['path'] = "ca/{$ca_config['idx']}"; # Renew the cert using pfSense's built in cert_renew function $renewed = cert_renew( @@ -114,8 +118,8 @@ class CertificateAuthorityRenew extends Model { } # Otherwise, continue with the renewal - $this->newserial->value = cert_get_serial($ca_config['crt']); - $msg = "Renewed CA {$ca_config['descr']} ({$ca_config['refid']}) - Serial {$this->oldserial->value} -> {$this->newserial->value}"; + $this->newserial->value = cert_get_serial($ca_config['item']['crt']); + $msg = "Renewed CA {$ca_config['item']['descr']} ({$ca_config['item']['refid']}) - Serial {$this->oldserial->value} -> {$this->newserial->value}"; $this->log_error($msg); $this->write_config($msg); } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRenew.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRenew.inc index dcfa69fb1..d2d3b673e 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRenew.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRenew.inc @@ -79,7 +79,8 @@ class CertificateRenew extends Model { */ public function validate_certref(string $certref): string { # Ensure the Certificate is capable of being renewed. - if (!is_cert_locally_renewable(lookup_cert($certref))) { + $cert = lookup_cert($certref); + if (!is_cert_locally_renewable($cert['item'])) { throw new NotAcceptableError( message: "Certificate with refid `$certref` is not capable of being renewed.", response_id: 'CERTIFICATE_RENEW_UNAVAILABLE', @@ -95,7 +96,10 @@ class CertificateRenew extends Model { public function _create(): void { # Extract details from the Certificate to renew $cert_config = &lookup_cert($this->certref->value); - $this->oldserial->value = cert_get_serial($cert_config['crt']); + $this->oldserial->value = cert_get_serial($cert_config['item']['crt']); + + # The pfSense cert_renew() function expects a 'path' key with the config path + $cert_config['path'] = "cert/{$cert_config['idx']}"; # Renew the cert using pfSense's built in cert_renew function $renewed = cert_renew( @@ -114,8 +118,8 @@ class CertificateRenew extends Model { } # Otherwise, continue with the renewal - $this->newserial->value = cert_get_serial($cert_config['crt']); - $msg = "Renewed certificate {$cert_config['descr']} ({$cert_config['refid']}) - Serial {$this->oldserial->value} -> {$this->newserial->value}"; + $this->newserial->value = cert_get_serial($cert_config['item']['crt']); + $msg = "Renewed certificate {$cert_config['item']['descr']} ({$cert_config['item']['refid']}) - Serial {$this->oldserial->value} -> {$this->newserial->value}"; $this->log_error($msg); $this->write_config($msg); } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRevocationList.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRevocationList.inc index 449f36528..031bf96a8 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRevocationList.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/CertificateRevocationList.inc @@ -134,7 +134,8 @@ class CertificateRevocationList extends Model { */ public function to_x509_crl(): string { # Prep the CRL config for generation - $crl_config = $this->to_internal(); + $crl_config = []; + $crl_config['item'] = $this->to_internal(); $crl_config['idx'] = $this->id; # Attempt to update/generate the CRL diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/ConfigHistoryRevision.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/ConfigHistoryRevision.inc index 6cea03f38..f0a85d85b 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/ConfigHistoryRevision.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/ConfigHistoryRevision.inc @@ -42,8 +42,18 @@ class ConfigHistoryRevision extends Model { * @return array The configuration history as an array of objects. */ protected function get_config_history(): array { + # Get our current configuration history, but remove the versions key. These are not needed for the Model. $config_history = get_backups(); unset($config_history['versions']); + + # Loop through each entry in the configuration history and normalize the data + foreach ($config_history as &$entry) { + # If filesize is not a numeric, set it to the default + if (!is_numeric($entry['filesize'])) { + $entry['filesize'] = $this->filesize->default; + } + } + return $config_history; } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FreeRADIUSUser.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FreeRADIUSUser.inc new file mode 100644 index 000000000..d5c2355e3 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/FreeRADIUSUser.inc @@ -0,0 +1,143 @@ +packages = ['pfSense-pkg-freeradius3']; + $this->package_includes = ['freeradius.inc']; + $this->config_path = 'installedpackages/freeradius/config'; + $this->many = true; + $this->always_apply = true; + + # Set model fields + $this->username = new StringField( + required: true, + unique: true, + internal_name: 'varusersusername', + help_text: 'The username for this user.', + ); + $this->password = new StringField( + required: true, + allow_empty: false, + allow_null: false, + sensitive: true, + internal_name: 'varuserspassword', + conditions: ['motp_enable' => false], + help_text: 'The password for this username.', + ); + $this->password_encryption = new StringField( + required: false, + default: 'Cleartext-Password', + choices: ['Cleartext-Password', 'MD5-Password', 'MD5-Password-hashed', 'NT-Password-hashed'], + internal_name: 'varuserspasswordencryption', + conditions: ['motp_enable' => false], + help_text: 'The encryption method for the password.', + ); + $this->motp_enable = new BooleanField( + required: true, + indicates_true: 'on', + indicates_false: '', + internal_name: 'varusersmotpenable', + help_text: 'Enable or disable the use of Mobile One-Time Password (MOTP) for this user.', + ); + $this->motp_authmethod = new StringField( + required: false, + default: 'googleauth', + choices: ['motp', 'googleauth'], + internal_name: 'varusersauthmethod', + conditions: ['motp_enable' => true], + help_text: 'The authentication method for the Mobile One-Time Password (MOTP).', + ); + $this->motp_secret = new StringField( + required: true, + allow_null: false, + sensitive: true, + internal_name: 'varusersmotpinitsecret', + conditions: ['motp_enable' => true], + help_text: 'The secret for the Mobile One-Time Password (MOTP).', + ); + $this->motp_pin = new StringField( + required: true, + allow_null: false, + sensitive: true, + minimum_length: 4, + maximum_length: 4, + internal_name: 'varusersmotppin', + conditions: ['motp_enable' => true], + help_text: 'The PIN for the Mobile One-Time Password (MOTP). It must be exactly 4 digits.', + ); + $this->motp_offset = new IntegerField( + required: false, + default: 0, + allow_null: false, + internal_name: 'varusersmotpoffset', + conditions: ['motp_enable' => true], + help_text: 'The timezone offset for this user.', + ); + $this->description = new StringField( + required: false, + default: '', + allow_empty: true, + validators: [ + new RegexValidator( + pattern: "/^[a-zA-Z0-9 _,.;:+=()-]*$/", + error_msg: 'Value contains invalid characters.', + ), + ], + help_text: 'A description for this user.', + ); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Perform additional validation on the Model's fields and data. + */ + public function validate_extra(): void { + # Run service level validations + $input_errors = []; + $user = $this->to_internal(); + freeradius_validate_users($user, $input_errors); + + # If there were validation errors that were not caught by the model fields, throw a ValidationError. + # Ideally the Model should catch all validation errors itself so prompt the user to report this error + if (!empty($input_errors)) { + throw new ValidationError( + message: "An unexpected validation error has occurred: $input_errors[0]. Please report this issue at " . + 'https://github.com/jaredhendrickson13/pfsense-api/issues/new', + response_id: 'FREERADIUS_USER_UNEXPECTED_VALIDATION_ERROR', + ); + } + } + + /** + * Apply the changes made to this Model to the FreeRADIUS configuration. + */ + public function apply(): void { + freeradius_users_resync(); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackendServer.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackendServer.inc index 52d616e40..3e70884a0 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackendServer.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/HAProxyBackendServer.inc @@ -23,6 +23,7 @@ class HAProxyBackendServer extends Model { public BooleanField $ssl; public BooleanField $sslserververify; public IntegerField $serverid; + public StringField $advanced; public function __construct(mixed $id = null, mixed $parent_id = null, mixed $data = [], ...$options) { # Set model attributes @@ -80,6 +81,12 @@ class HAProxyBackendServer extends Model { help_text: 'The unique ID for this backend server. This value is set by the system for internal use and ' . 'cannot be changed.', ); + $this->advanced = new StringField( + default: '', + allow_empty: true, + help_text: 'Allows adding custom HAProxy server settings to the server.', + ); + parent::__construct($id, $parent_id, $data, ...$options); } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNClientSpecificOverride.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNClientSpecificOverride.inc index 53d30c271..aeb0817bf 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNClientSpecificOverride.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNClientSpecificOverride.inc @@ -30,7 +30,7 @@ class OpenVPNClientSpecificOverride extends Model { public StringField $remote_networkv6; public BooleanField $gwredir; public BooleanField $push_reset; - public BooleanField $remove_route; + public StringField $remove_options; public StringField $dns_domain; public StringField $dns_server1; public StringField $dns_server2; @@ -130,16 +130,28 @@ class OpenVPNClientSpecificOverride extends Model { ); $this->push_reset = new BooleanField( default: false, - indicates_true: 'yes', - indicates_false: '', help_text: 'Enables or disables preventing this client from receiving any server-defined client settings.', ); - $this->remove_route = new BooleanField( - default: false, - indicates_true: 'yes', - indicates_false: '', - help_text: 'Enables or disables preventing this client from receiving any server-defined routes ' . - 'without removing any other options.', + $this->remove_options = new StringField( + default: [], + choices: [ + 'remove_route', + 'remove_iroute', + 'remove_redirect_gateway', + 'remove_inactive', + 'remove_ping', + 'remove_ping_action', + 'remove_dnsdomain', + 'remove_dnsservers', + 'remove_blockoutsidedns', + 'remove_ntpservers', + 'remove_netbios_ntype', + 'remove_netbios_scope', + 'remove_wins', + ], + many: true, + conditions: ['push_reset' => false], + help_text: 'Specifies the push-remove options to apply to the client', ); $this->dns_domain = new StringField( default: '', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNLog.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNLog.inc new file mode 100644 index 000000000..e98d3599b --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/OpenVPNLog.inc @@ -0,0 +1,39 @@ +internal_callable = 'get_openvpn_log'; + $this->many = true; + + $this->text = new StringField(default: '', help_text: 'The raw text of the openvpn log entry.'); + + parent::__construct($id, $parent_id, $data, ...$options); + } + + /** + * Obtains the openvpn log as an array. This method is the internal callable for this Model. + * @return array The openvpn log as an array of objects. + */ + protected function get_openvpn_log(): array { + return $this->read_log($this->log_file); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/User.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/User.inc index 8bca09199..afbe1bcd2 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/User.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/User.inc @@ -137,7 +137,7 @@ class User extends Model { if ($this->initial_object->password->value !== $password) { $hash = []; local_user_set_password($hash, $password); - return $hash[$this->password->internal_name]; + return $hash['item'][$this->password->internal_name]; } return $password; 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 721d6c95e..11a213ae0 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 @@ -51,6 +51,12 @@ class WireGuardTunnel extends Model { indicates_false: 'no', help_text: 'Enables or disables this tunnels and any associated peers.', ); + $this->descr = new StringField( + required: false, + default: '', + allow_empty: true, + help_text: 'A description for this WireGuard tunnel.', + ); $this->listenport = new PortField( unique: true, default: '51820', 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 148067f58..ceddaf442 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 @@ -134,10 +134,14 @@ class APICoreDispatcherTestCase extends TestCase { $this->assert_equals($dispatcher->setup_schedule(), null); # Assign the dispatcher a schedule and ensure setup_schedule() returns a CronJob object with the correct schedule - $dispatcher->schedule = 'daily'; + $dispatcher->schedule = '1 2 3 4 5'; $dispatcher_cron_job = $dispatcher->setup_schedule(); $cron_job_cmd = '/usr/local/pkg/RESTAPI/.resources/scripts/manage.php notifydispatcher Dispatcher'; - $this->assert_equals($dispatcher_cron_job->minute->value, "@$dispatcher->schedule"); + $this->assert_equals($dispatcher_cron_job->minute->value, '1'); + $this->assert_equals($dispatcher_cron_job->hour->value, '2'); + $this->assert_equals($dispatcher_cron_job->mday->value, '3'); + $this->assert_equals($dispatcher_cron_job->month->value, '4'); + $this->assert_equals($dispatcher_cron_job->wday->value, '5'); $this->assert_equals($dispatcher_cron_job->command->value, $cron_job_cmd); # Delete the CronJob 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 5820c475b..f05ceeb92 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 @@ -1289,4 +1289,15 @@ class APICoreModelTestCase extends RESTAPI\Core\TestCase { } } } + + /** + * Checks that the normalize_config_path() method correctly removes extra slashes in the config path + */ + public function test_normalize_config_path(): void { + $this->assert_equals(Model::normalize_config_path('/test/path'), 'test/path'); + $this->assert_equals(Model::normalize_config_path('/test//path'), 'test/path'); + $this->assert_equals(Model::normalize_config_path('test/path/'), 'test/path'); + $this->assert_equals(Model::normalize_config_path('/test//path/'), 'test/path'); + $this->assert_equals(Model::normalize_config_path('test'), 'test'); + } } diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsAuthLogTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsAuthLogTestCase.inc new file mode 100644 index 000000000..241933263 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsAuthLogTestCase.inc @@ -0,0 +1,18 @@ +model_objects as $auth_log) { + $this->assert_is_not_empty($auth_log->text->value); + } + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateAuthorityTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateAuthorityTestCase.inc index f5f4434db..d07f8a770 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateAuthorityTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsCertificateAuthorityTestCase.inc @@ -111,23 +111,17 @@ R02Pul8ulWQ8Kl3Q3pou8As7W1mMzA2DxQ== ); $ca->create(); - # Wait a few seconds for the trust store to be rebuilt - sleep(3); - - # Ensure the certificate is found in the trust store - $certctl_list = new Command('certctl list'); - $this->assert_str_contains($certctl_list->output, 'example.com'); + # Ensure the cert is in the trust store directory + $truststore_dir_ls = glob('/usr/local/etc/ssl/certs/*.crt'); + $this->assert_is_not_empty($truststore_dir_ls, message: 'Trust store directory should have one trusted CA!'); # Disable `trust` $ca->trust->value = false; $ca->update(); - # Wait a few seconds for the trust store to be rebuilt - sleep(3); - # Ensure the certificate is not found in the trust store - $certctl_list = new Command('certctl list'); - $this->assert_str_does_not_contain($certctl_list->output, 'example.com'); + $truststore_dir_ls = glob('/usr/local/etc/ssl/certs/*.crt'); + $this->assert_is_empty($truststore_dir_ls, message: 'Trust store directory should have no trusted CAs!'); # Delete the CA $ca->delete(); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsConfigHistoryRevisionTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsConfigHistoryRevisionTestCase.inc index b79e036c8..a487d59c6 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsConfigHistoryRevisionTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsConfigHistoryRevisionTestCase.inc @@ -15,7 +15,7 @@ class APIModelsConfigHistoryRevisionTestCase extends TestCase { $this->assert_is_not_empty($config_history->time->value); $this->assert_is_not_empty($config_history->description->value); $this->assert_is_not_empty($config_history->version->value); - $this->assert_is_not_empty($config_history->filesize->value); + $this->assert_is_true(is_integer($config_history->filesize->value)); } /** diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerAddressPoolTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerAddressPoolTestCase.inc index a66e1a619..a239cd1c2 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerAddressPoolTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsDHCPServerAddressPoolTestCase.inc @@ -360,8 +360,6 @@ class APIModelsDHCPServerAddressPoolTestCase extends TestCase { mac_allow: ['00:11:22:33:44:55'], mac_deny: ['55:44:33:22:11:00'], domainsearchlist: ['example.com'], - defaultleasetime: 7201, - maxleasetime: 86401, gateway: '192.168.1.2', dnsserver: ['127.0.0.53'], ntpserver: ['127.0.0.123'], @@ -374,8 +372,6 @@ class APIModelsDHCPServerAddressPoolTestCase extends TestCase { $kea_json = json_decode($kea_conf, associative: true); $kea_pool = $kea_json['Dhcp4']['subnet4'][0]['pools'][1]; $this->assert_equals($kea_pool['pool'], $pool->range_from->value . ' - ' . $pool->range_to->value); - $this->assert_equals($kea_pool['valid-lifetime'], (string) $pool->defaultleasetime->value); - $this->assert_equals($kea_pool['max-valid-lifetime'], (string) $pool->maxleasetime->value); $this->assert_equals( $this->get_kea_option_by_name('domain-name-servers', $kea_pool['option-data']), $pool->dnsserver->value[0], @@ -403,8 +399,6 @@ class APIModelsDHCPServerAddressPoolTestCase extends TestCase { mac_allow: ['55:44:33:22:11:00'], mac_deny: ['00:11:22:33:44:55'], domainsearchlist: ['new.example.com'], - defaultleasetime: 7205, - maxleasetime: 86405, gateway: '192.168.1.5', dnsserver: ['127.0.1.53'], ntpserver: ['127.0.1.123'], @@ -416,8 +410,6 @@ class APIModelsDHCPServerAddressPoolTestCase extends TestCase { $kea_json = json_decode($kea_conf, associative: true); $kea_pool = $kea_json['Dhcp4']['subnet4'][0]['pools'][1]; $this->assert_equals($kea_pool['pool'], $pool->range_from->value . ' - ' . $pool->range_to->value); - $this->assert_equals($kea_pool['valid-lifetime'], (string) $pool->defaultleasetime->value); - $this->assert_equals($kea_pool['max-valid-lifetime'], (string) $pool->maxleasetime->value); $this->assert_equals( $this->get_kea_option_by_name('domain-name-servers', $kea_pool['option-data']), $pool->dnsserver->value[0], diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallRuleTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallRuleTestCase.inc index d7eef9619..47ed22120 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallRuleTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFirewallRuleTestCase.inc @@ -280,7 +280,7 @@ class APIModelsFirewallRuleTestCase extends TestCase { # Ensure the pfctl rule with this rule object's tracker is set to sloppy $pfctl_rules = file_get_contents('/tmp/rules.debug'); - $pfctl_rule = "ridentifier {$rule->tracker->value} keep state ( sloppy )"; + $pfctl_rule = "ridentifier {$rule->tracker->value} keep state (sloppy)"; $this->assert_str_contains($pfctl_rules, $pfctl_rule); # Delete the firewall rule diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFreeRADIUSUserTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFreeRADIUSUserTestCase.inc new file mode 100644 index 000000000..cd09c610c --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsFreeRADIUSUserTestCase.inc @@ -0,0 +1,57 @@ +create(); + $raddb = file_get_contents('/usr/local/etc/raddb/users'); + $this->assert_str_contains($raddb, 'testuser" Cleartext-Password := "testpassword"'); + + # Ensure we can read the created user from the config + $read_user = new FreeRADIUSUser(id: $user->id); + $this->assert_equals($read_user->username->value, 'testuser'); + $this->assert_equals($read_user->password_encryption->value, 'Cleartext-Password'); + $this->assert_equals($read_user->motp_enable->value, false); + $this->assert_equals($read_user->description->value, 'Test User'); + + # Ensure we can update the user + $user = new FreeRADIUSUser( + id: $read_user->id, + username: 'motptestuser', + motp_enable: true, + motp_authmethod: 'motp', + motp_secret: 'abcdef0123456789', + motp_pin: '1234', + motp_offset: 30, + ); + $user->update(); + $raddb = file_get_contents('/usr/local/etc/raddb/users'); + $this->assert_str_does_not_contain($raddb, 'testuser" Cleartext-Password := "testpassword"'); + $this->assert_str_contains($raddb, '"motptestuser" Auth-Type = motp'); + $this->assert_str_contains($raddb, 'MOTP-Init-Secret = abcdef0123456789'); + $this->assert_str_contains($raddb, 'MOTP-PIN = 1234'); + $this->assert_str_contains($raddb, 'MOTP-Offset = 30'); + + # Delete the user and ensure it is removed from the database + $user->delete(); + $raddb = file_get_contents('/usr/local/etc/raddb/users'); + $this->assert_str_does_not_contain($raddb, '"motptestuser" Auth-Type = motp'); + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNClientSpecificOverrideTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNClientSpecificOverrideTestCase.inc index 68d5127c4..9ec8d0dc8 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNClientSpecificOverrideTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNClientSpecificOverrideTestCase.inc @@ -84,8 +84,8 @@ class APIModelsOpenVPNClientSpecificOverrideTestCase extends TestCase { remote_network: ['10.1.2.0/24'], remote_networkv6: ['1234::/64'], gwredir: true, - push_reset: true, - remove_route: true, + push_reset: false, + remove_options: ['remove_route'], dns_domain: 'example.com', dns_server1: '127.0.0.1', dns_server2: '127.0.0.2', @@ -105,7 +105,7 @@ class APIModelsOpenVPNClientSpecificOverrideTestCase extends TestCase { "/var/etc/openvpn/server{$this->ovpns->vpnid->value}/csc/{$cso->common_name->value}", ); $this->assert_str_contains($cso_conf, 'disable'); // For `block` field - $this->assert_str_contains($cso_conf, 'push-reset'); + $this->assert_str_does_not_contain($cso_conf, 'push-reset'); $this->assert_str_contains($cso_conf, 'push-remove route'); $this->assert_str_contains($cso_conf, 'push "route 10.1.2.0 255.255.255.0"'); $this->assert_str_contains($cso_conf, 'push "route-ipv6 1234::/64"'); @@ -137,8 +137,8 @@ class APIModelsOpenVPNClientSpecificOverrideTestCase extends TestCase { remote_network: ['10.2.3.0/24'], remote_networkv6: ['4321::/64'], gwredir: false, - push_reset: false, - remove_route: false, + push_reset: true, + remove_options: [], dns_domain: 'updated.example.com', dns_server1: '127.0.1.1', dns_server2: '127.0.1.2', @@ -158,7 +158,7 @@ class APIModelsOpenVPNClientSpecificOverrideTestCase extends TestCase { "/var/etc/openvpn/server{$this->ovpns->vpnid->value}/csc/{$cso->common_name->value}", ); $this->assert_str_does_not_contain($cso_conf, 'disable'); // For `block` field - $this->assert_str_does_not_contain($cso_conf, 'push-reset'); + $this->assert_str_contains($cso_conf, 'push-reset'); $this->assert_str_does_not_contain($cso_conf, 'push-remove route'); $this->assert_str_contains($cso_conf, 'push "route 10.2.3.0 255.255.255.0"'); $this->assert_str_contains($cso_conf, 'push "route-ipv6 4321::/64"'); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNLogTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNLogTestCase.inc new file mode 100644 index 000000000..3e713ee30 --- /dev/null +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsOpenVPNLogTestCase.inc @@ -0,0 +1,20 @@ +> /var/log/openvpn.log'); + $openvpn_logs = OpenVPNLog::read_all(limit: 5); + foreach ($openvpn_logs->model_objects as $openvpn_log) { + $this->assert_is_not_empty($openvpn_log->text->value); + } + } +} diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc index 6dcc29b20..6e9862a09 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc @@ -95,11 +95,8 @@ class APIModelsPortForwardTestCase extends TestCase { # Ensure the pure NAT reflection rule is present $rules_debug = file_get_contents('/tmp/rules.debug'); - $purenat_rule = "rdr on {$this->env['PFREST_LAN_IF']} inet proto tcp from any to 127.3.2.1 port 8443 -> 127.1.2.3"; - $purenat_rule_ovpn = "rdr on { {$this->env['PFREST_LAN_IF']} openvpn } inet proto tcp from any to 127.3.2.1 port 8443 -> 127.1.2.3"; - $this->assert_is_true( - str_contains($rules_debug, $purenat_rule) or str_contains($rules_debug, $purenat_rule_ovpn), - ); + $purenat_rule = 'inet proto tcp from any to 127.3.2.1 port 8443 -> 127.1.2.3'; + $this->assert_str_contains($rules_debug, $purenat_rule); # Update the NAT reflection mode NAT + proxy (enable) $port_forward->natreflection->value = 'enable'; @@ -107,14 +104,8 @@ class APIModelsPortForwardTestCase extends TestCase { # Ensure the pure NAT reflection rule is no longer present and the NAT proxy rule is present $rules_debug = file_get_contents('/tmp/rules.debug'); - $natproxy_rule = "rdr on {$this->env['PFREST_LAN_IF']} proto tcp from any to 127.3.2.1 port 8443 tag PFREFLECT -> 127.0.0.1 port 19000"; - $natproxy_rule_ovpn = "rdr on { {$this->env['PFREST_LAN_IF']} openvpn } proto tcp from any to 127.3.2.1 port 8443 tag PFREFLECT -> 127.0.0.1 port 19000"; - $this->assert_is_false( - str_contains($rules_debug, $purenat_rule) or str_contains($rules_debug, $purenat_rule_ovpn), - ); - $this->assert_is_true( - str_contains($rules_debug, $natproxy_rule) or str_contains($rules_debug, $natproxy_rule_ovpn), - ); + $natproxy_rule = 'proto tcp from any to 127.3.2.1 port 8443 tag PFREFLECT -> 127.0.0.1 port 19000'; + $this->assert_str_contains($rules_debug, $natproxy_rule); # Delete the port forward and ensure the previous reflection rule is no longer present $port_forward->delete(apply: true); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardTunnelTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardTunnelTestCase.inc index c7a391852..3a2f5cb4d 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardTunnelTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsWireGuardTunnelTestCase.inc @@ -124,6 +124,7 @@ class APIModelsWireGuardTunnelTestCase extends TestCase { $tunnel = new WireGuardTunnel( privatekey: 'KG0BA4UyPilHH5qnXCfr6Lw8ynecOPor88tljLy3AHk=', listenport: '55000', + descr: 'test', async: false, ); $tunnel->create(apply: true); @@ -134,6 +135,7 @@ class APIModelsWireGuardTunnelTestCase extends TestCase { $this->assert_str_contains($wg_showconf->output, 'ListenPort = ' . $tunnel->listenport->value); $this->assert_str_contains($wg_showconf->output, 'PrivateKey = ' . $tunnel->privatekey->value); $this->assert_str_contains($wg_show->output, 'public key: ' . $tunnel->publickey->value); + $this->assert_equals($tunnel->descr->value, 'test'); # Update the tunnel with new values $tunnel->from_representation(privatekey: 'GNdQw+ujEIVgys4B2dDCXcBpiiQsNd2bAq5hnTp+smg=', listenport: '51820'); diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Validators/FilterNameValidator.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Validators/FilterNameValidator.inc index 203b7c005..24d55a173 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Validators/FilterNameValidator.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Validators/FilterNameValidator.inc @@ -22,10 +22,10 @@ class FilterNameValidator extends Validator { * @throws ValidationError When the value is not a valid firewall filter name. */ public function validate(mixed $value, string $field_name = ''): void { - global $pf_reserved_keywords, $reserved_table_names; + $reserved_names = get_pf_reserved(); # Throw an exception if this name is reserved - if (in_array($value, array_merge($pf_reserved_keywords, $reserved_table_names))) { + if (in_array($value, $reserved_names)) { throw new ValidationError( message: "Field '$field_name' cannot be '$value' because it is a name reserved by the system.", response_id: 'FILTER_NAME_VALIDATOR_RESERVED_NAME_USED', diff --git a/vagrant-build.sh b/vagrant-build.sh index 6165f9ce3..d92abd17c 100755 --- a/vagrant-build.sh +++ b/vagrant-build.sh @@ -1,7 +1,7 @@ #!/bin/sh # Set build variables -FREEBSD_VERSION=${FREEBSD_VERSION:-"freebsd/FreeBSD-14.0-CURRENT"} +FREEBSD_VERSION=${FREEBSD_VERSION:-"freebsd/FreeBSD-15.0-CURRENT"} BUILD_VERSION=${BUILD_VERSION:-"0.0_0-dev"} # Start the vagrant box @@ -19,7 +19,7 @@ rsync -avz --progress -e "ssh -F $SSH_CONFIG_FILE" ../pfsense-api vagrant@defaul cat << END | vagrant ssh composer install --working-dir /home/vagrant/build/pfsense-api cp -r /home/vagrant/build/pfsense-api/vendor/* /home/vagrant/build/pfsense-api/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/vendor/ -python3.8 /home/vagrant/build/pfsense-api/tools/make_package.py -t $BUILD_VERSION +python3.11 /home/vagrant/build/pfsense-api/tools/make_package.py -t $BUILD_VERSION END # Copy the built package back to the host using SCP