From 10b08049f87aa6b30ac9953bb0752a557d91a70d Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Mon, 16 Jun 2025 12:38:35 -0400 Subject: [PATCH 1/4] chore: require connect plugin to enable flash backup --- .../etc/rc.d/rc.flash_backup | 6 +- .../dynamix.unraid.net/etc/rc.d/rc.unraid-api | 21 ++--- .../dynamix.my.servers/include/state.php | 30 ++----- .../dynamix.unraid.net/scripts/api_utils.sh | 80 ++++++++++++++++--- 4 files changed, 95 insertions(+), 42 deletions(-) diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup index 33ee3e4fc9..6f1b854ff4 100755 --- a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup +++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup @@ -1,6 +1,7 @@ #!/bin/bash # This file is /etc/rc.d/rc.flash_backup # use at queue "f" for flash backup +scripts_dir="/usr/local/share/dynamix.unraid.net/scripts" QUEUE=" -q f " TASKNAME="/etc/rc.d/rc.flash_backup watch" TASKACTION="/usr/local/emhttp/plugins/dynamix.my.servers/scripts/UpdateFlashBackup update" @@ -153,7 +154,10 @@ _enabled() { local output output=$(git -C /boot config --get remote.origin.url 2>&1) if [[ "${output}" == *"backup.unraid.net"* ]]; then - return 0 + # Also check if the connect API plugin is enabled + if "$scripts_dir/api_utils.sh" is_api_plugin_enabled unraid-api-plugin-connect; then + return 0 + fi fi return 1 } diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api index 52027c530b..885d5dcbc8 100755 --- a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api +++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api @@ -28,7 +28,7 @@ uninstall() { # Service control functions start() { echo "Starting Unraid API service..." - + # Restore vendored API plugins if they were installed if [ -x "$scripts_dir/dependencies.sh" ]; then "$scripts_dir/dependencies.sh" restore || { @@ -37,21 +37,24 @@ start() { else echo "Warning: dependencies.sh script not found or not executable" fi - + # Create log directory if it doesn't exist mkdir -p /var/log/unraid-api - + # Copy env file if needed if [ -f "${api_base_dir}/.env.production" ] && [ ! -f "${api_base_dir}/.env" ]; then cp "${api_base_dir}/.env.production" "${api_base_dir}/.env" fi - - # Start the flash backup service if available + + # Start the flash backup service if available and connect plugin is enabled if [ -x "/etc/rc.d/rc.flash_backup" ]; then - echo "Starting flash backup service..." - /etc/rc.d/rc.flash_backup start + # Check if connect plugin is enabled before starting flash backup + if [ -x "$scripts_dir/api_utils.sh" ] && "$scripts_dir/api_utils.sh" is_api_plugin_enabled "unraid-api-plugin-connect"; then + echo "Starting flash backup service..." + /etc/rc.d/rc.flash_backup start + fi fi - + # Start the API service if [ -x "${unraid_binary_path}" ]; then "${unraid_binary_path}" start @@ -96,7 +99,7 @@ case "$1" in 'stop') stop ;; -'restart'|'reload') +'restart' | 'reload') restart ;; 'status') diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php index 5182e912df..5b37063d52 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php @@ -146,31 +146,15 @@ public function getWebguiGlobal(string $key, ?string $subkey = null) private function setConnectValues() { - $apiConfigPath = '/boot/config/plugins/dynamix.my.servers/configs/api.json'; - if (!file_exists($apiConfigPath)) { + $connect_plugin_name="unraid-api-plugin-connect"; // name of connect plugin's npm package + $scripts_dir="/usr/local/share/dynamix.unraid.net/scripts"; + $api_utils_sh="$scripts_dir/api_utils.sh"; + $connectEnabled = @exec("$api_utils_sh is_api_plugin_enabled $connect_plugin_name 2>/dev/null; echo $?"); + if ($connectEnabled !== '0') { return; // plugin is not installed; exit early } - - $apiConfig = @json_decode(file_get_contents($apiConfigPath), true); - $pluginName = 'unraid-api-plugin-connect'; // name of connect plugin's npm package - if ($apiConfig && is_array($apiConfig['plugins'])) { - foreach ($apiConfig['plugins'] as $plugin) { - // recognize npm version identifiers inside $apiConfig['plugins'] - if ($plugin === $pluginName || strpos($plugin, $pluginName . '@') === 0) { - $this->connectPluginInstalled = 'dynamix.unraid.net.plg'; - break; - } - } - } - - // exit early if the plugin is not installed - if (!$this->connectPluginInstalled) { - return; - } - - // Get version directly using api_utils.sh get_api_version function - $this->connectPluginVersion = trim(@exec('/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh get_api_version 2>/dev/null')) ?: 'unknown'; - + $this->connectPluginInstalled = 'dynamix.unraid.net.plg'; + $this->connectPluginVersion = trim(@exec("$api_utils_sh get_api_version 2>/dev/null")) ?: 'unknown'; $this->getMyServersCfgValues(); $this->getConnectKnownOrigins(); $this->getFlashBackupStatus(); diff --git a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh index ae7a6a0802..17d05c95c6 100755 --- a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh +++ b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh @@ -9,7 +9,7 @@ CONFIG_FILE="/usr/local/share/dynamix.unraid.net/config/vendor_archive.json" # Returns the API version string or empty if not found get_api_version() { local api_version="" - + # Get version from config file if [ -f "$CONFIG_FILE" ] && command -v jq >/dev/null 2>&1; then api_version=$(jq -r '.api_version' "$CONFIG_FILE") @@ -18,23 +18,23 @@ get_api_version() { return 0 fi fi - + # No version found return 1 } -# Get vendor archive configuration information +# Get vendor archive configuration information # Returns an array of values: api_version, vendor_store_url, vendor_store_path get_archive_information() { # Define all local variables at the top local api_version="" local vendor_store_path="" - + if [ ! -f "$CONFIG_FILE" ]; then echo "Vendor archive config file not found at $CONFIG_FILE" >&2 return 1 fi - + # Read values from JSON config using jq if command -v jq >/dev/null 2>&1; then api_version=$(jq -r '.api_version' "$CONFIG_FILE") @@ -43,20 +43,82 @@ get_archive_information() { echo "jq not found, can't parse config file" >&2 return 1 fi - + # Validate that all required values exist and are not null if [ -z "$api_version" ] || [ "$api_version" = "null" ]; then echo "Invalid or missing api_version in config file" >&2 return 1 fi - + if [ -z "$vendor_store_path" ] || [ "$vendor_store_path" = "null" ]; then echo "Invalid or missing vendor_store_path in config file" >&2 return 1 fi - + # Return the values echo "$api_version" echo "$vendor_store_path" return 0 -} \ No newline at end of file +} + +# Check if an API plugin is enabled +# Usage: is_api_plugin_enabled +# Returns 0 if enabled, 1 if not enabled or error +is_api_plugin_enabled() { + local plugin_name="$1" + local api_config_path="/boot/config/plugins/dynamix.my.servers/configs/api.json" + + # Check if plugin name is provided + if [ -z "$plugin_name" ]; then + return 1 + fi + + # Check if API config file exists + if [ ! -f "$api_config_path" ]; then + return 1 + fi + + # Check if jq is available + if ! command -v jq >/dev/null 2>&1; then + return 1 + fi + + # Parse JSON and check for plugin + local plugins=$(jq -r '.plugins[]?' "$api_config_path" 2>/dev/null) + if [ $? -ne 0 ]; then + return 1 + fi + + # Check each plugin entry + while IFS= read -r plugin; do + if [ "$plugin" = "$plugin_name" ] || [[ "$plugin" == "$plugin_name@"* ]]; then + return 0 + fi + done <<<"$plugins" + + return 1 +} + +# Main execution when script is called directly +# Handle command line arguments +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "$1" in + "get_api_version") + get_api_version + ;; + "get_archive_information") + get_archive_information + ;; + "is_api_plugin_enabled") + if [ -z "$2" ]; then + echo "Error: Plugin name required" >&2 + exit 1 + fi + is_api_plugin_enabled "$2" + ;; + *) + echo "Usage: $0 {get_api_version|get_archive_information|is_api_plugin_enabled }" >&2 + exit 1 + ;; + esac +fi From 8e952a0184030b23f65dd7dfb37219bef71d7acf Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Mon, 16 Jun 2025 12:47:10 -0400 Subject: [PATCH 2/4] show falsh backup setting when connect is installed --- .../local/emhttp/plugins/dynamix.my.servers/Connect.page | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page index 802ea0fa80..a910525d7f 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page @@ -28,6 +28,11 @@ $hasMyUnraidNetCert = preg_match('/.*\.myunraid\.net$/', $serverState->nginxCfg[ $isRegistered = $serverState->registered; $isMiniGraphConnected = $serverState->myServersMiniGraphConnected; +// Check if connect plugin is installed using api_utils.sh +$scripts_dir = "/usr/local/share/dynamix.unraid.net/scripts"; +$api_utils_sh = "$scripts_dir/api_utils.sh"; +$isConnectPluginInstalled = @exec("$api_utils_sh is_api_plugin_enabled unraid-api-plugin-connect 2>/dev/null; echo $?") === '0'; + $flashbackup_status = $serverState->flashbackupStatus; $passwd_result = exec('/usr/bin/passwd --status root'); @@ -486,6 +491,7 @@ $('body').on('click', '.js-setCurrentHostExtraOrigins', function(e) { }); +
_(Flash backup)_: @@ -563,6 +569,7 @@ $(function() {
+
From 59aa86dbd36319998d510a90db5f0dd85fa12b73 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Mon, 16 Jun 2025 13:05:31 -0400 Subject: [PATCH 3/4] refactor api_utils.sh usage in php --- .../plugins/dynamix.my.servers/Connect.page | 6 +-- .../dynamix.my.servers/include/api-config.php | 50 +++++++++++++++++++ .../dynamix.my.servers/include/state.php | 10 ++-- .../dynamix.unraid.net/scripts/api_utils.sh | 3 +- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page index a910525d7f..a6656514fd 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page @@ -14,6 +14,7 @@ Tag="globe" * all copies or substantial portions of the Software. */ require_once "$docroot/plugins/dynamix.my.servers/include/state.php"; +require_once "$docroot/plugins/dynamix.my.servers/include/api-config.php"; require_once "$docroot/webGui/include/Wrappers.php"; $serverState = new ServerState(); @@ -28,10 +29,7 @@ $hasMyUnraidNetCert = preg_match('/.*\.myunraid\.net$/', $serverState->nginxCfg[ $isRegistered = $serverState->registered; $isMiniGraphConnected = $serverState->myServersMiniGraphConnected; -// Check if connect plugin is installed using api_utils.sh -$scripts_dir = "/usr/local/share/dynamix.unraid.net/scripts"; -$api_utils_sh = "$scripts_dir/api_utils.sh"; -$isConnectPluginInstalled = @exec("$api_utils_sh is_api_plugin_enabled unraid-api-plugin-connect 2>/dev/null; echo $?") === '0'; +$isConnectPluginInstalled = ApiConfig::isConnectPluginEnabled(); $flashbackup_status = $serverState->flashbackupStatus; diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php new file mode 100644 index 0000000000..e1b710bccc --- /dev/null +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php @@ -0,0 +1,50 @@ +/dev/null; echo $?"); + return $result === '0'; + } + + /** + * Check if the unraid-api-plugin-connect is enabled + * @return bool True if connect plugin is enabled, false otherwise + */ + public static function isConnectPluginEnabled() + { + return self::isApiPluginEnabled('unraid-api-plugin-connect'); + } + + /** + * Get API version from api_utils.sh + * @return string The API version or 'unknown' if not found + */ + public static function getApiVersion() + { + $apiUtilsScript = self::getApiUtilsScript(); + return trim(@exec("$apiUtilsScript get_api_version 2>/dev/null")) ?: 'unknown'; + } +} diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php index 5b37063d52..530240328c 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php @@ -17,6 +17,7 @@ require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php"; require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php"; +require_once "$docroot/plugins/dynamix.my.servers/include/api-config.php"; /** * ServerState class encapsulates server-related information and settings. * @@ -146,15 +147,12 @@ public function getWebguiGlobal(string $key, ?string $subkey = null) private function setConnectValues() { - $connect_plugin_name="unraid-api-plugin-connect"; // name of connect plugin's npm package - $scripts_dir="/usr/local/share/dynamix.unraid.net/scripts"; - $api_utils_sh="$scripts_dir/api_utils.sh"; - $connectEnabled = @exec("$api_utils_sh is_api_plugin_enabled $connect_plugin_name 2>/dev/null; echo $?"); - if ($connectEnabled !== '0') { + if (!ApiConfig::isConnectPluginEnabled()) { return; // plugin is not installed; exit early } + $this->connectPluginInstalled = 'dynamix.unraid.net.plg'; - $this->connectPluginVersion = trim(@exec("$api_utils_sh get_api_version 2>/dev/null")) ?: 'unknown'; + $this->connectPluginVersion = ApiConfig::getApiVersion(); $this->getMyServersCfgValues(); $this->getConnectKnownOrigins(); $this->getFlashBackupStatus(); diff --git a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh index 17d05c95c6..b4168542bd 100755 --- a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh +++ b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/scripts/api_utils.sh @@ -84,7 +84,8 @@ is_api_plugin_enabled() { fi # Parse JSON and check for plugin - local plugins=$(jq -r '.plugins[]?' "$api_config_path" 2>/dev/null) + local plugins + plugins=$(jq -r '.plugins[]?' "$api_config_path" 2>/dev/null) if [ $? -ne 0 ]; then return 1 fi From ad1d5a5de65c72f9ea7d25abd82f2480f8a50cfc Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Mon, 16 Jun 2025 13:12:25 -0400 Subject: [PATCH 4/4] refactor: escaping & error handling in shell invocation from php --- .../dynamix.my.servers/include/api-config.php | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php index e1b710bccc..b090d41aa5 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/api-config.php @@ -17,6 +17,22 @@ private static function getApiUtilsScript() return self::$scriptsDir . "/api_utils.sh"; } + /** + * Execute a command safely with proper error handling + * @param string $command The command to execute + * @param int &$exitCode Reference to store the exit code + * @return string The command output + */ + private static function executeCommand($command, &$exitCode = null) + { + $output = []; + $exitCode = 0; + + exec($command, $output, $exitCode); + + return implode("\n", $output); + } + /** * Check if a specific API plugin is enabled * @param string $pluginName The name of the plugin to check @@ -24,9 +40,24 @@ private static function getApiUtilsScript() */ public static function isApiPluginEnabled($pluginName) { + if (empty($pluginName) || !is_string($pluginName)) { + return false; + } + $apiUtilsScript = self::getApiUtilsScript(); - $result = @exec("$apiUtilsScript is_api_plugin_enabled $pluginName 2>/dev/null; echo $?"); - return $result === '0'; + + if (!is_executable($apiUtilsScript)) { + return false; + } + + $escapedScript = escapeshellarg($apiUtilsScript); + $escapedPlugin = escapeshellarg($pluginName); + $command = "$escapedScript is_api_plugin_enabled $escapedPlugin 2>/dev/null"; + + $exitCode = 0; + self::executeCommand($command, $exitCode); + + return $exitCode === 0; } /** @@ -45,6 +76,22 @@ public static function isConnectPluginEnabled() public static function getApiVersion() { $apiUtilsScript = self::getApiUtilsScript(); - return trim(@exec("$apiUtilsScript get_api_version 2>/dev/null")) ?: 'unknown'; + + if (!is_executable($apiUtilsScript)) { + return 'unknown'; + } + + $escapedScript = escapeshellarg($apiUtilsScript); + $command = "$escapedScript get_api_version 2>/dev/null"; + + $exitCode = 0; + $output = self::executeCommand($command, $exitCode); + + if ($exitCode !== 0) { + return 'unknown'; + } + + $version = trim($output); + return !empty($version) ? $version : 'unknown'; } }