diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index f20269ece..1de535b21 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -384,6 +384,7 @@ if [ -f /tmp/restore-files-dynamix-unraid-net ]; then "/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php" "/usr/local/emhttp/plugins/dynamix/include/ProvisionCert.php" "/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php" + "/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php" "/usr/local/emhttp/plugins/dynamix/include/Wrappers.php" "/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page" "/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page" @@ -505,6 +506,7 @@ echo preserveFilesDirs=( "move:/usr/local/emhttp/plugins/dynamix/Registration.page:preventDowngrade" "move:/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php:preventDowngrade" + "move:/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php:preventDowngrade" "move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page:preventDowngrade" "move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page:preventDowngrade" "move:/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck:preventDowngrade" diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php new file mode 100644 index 000000000..640867b02 --- /dev/null +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix/include/ReplaceKey.php @@ -0,0 +1,239 @@ +docroot = $GLOBALS['docroot'] ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'; + + $this->var = (array)@parse_ini_file('/var/local/emhttp/var.ini'); + $this->guid = @$this->var['regGUID'] ?? null; + + $keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']); + if ($keyfileBase64 !== false) { + $keyfileBase64 = @base64_encode($keyfileBase64); + $this->keyfile = str_replace(['+', '/', '='], ['-', '_', ''], trim($keyfileBase64)); + } + + $this->regExp = @$this->var['regExp'] ?? null; + } + + private function request($url, $method, $payload = null, $headers = null) + { + $ch = curl_init($url); + + // Set the request method + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + // store the response in a variable instead of printing it + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Set the payload if present + if ($payload !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + } + + if ($headers !== null) { + // Set the headers + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + // Set additional options as needed + + // Execute the request + $response = curl_exec($ch); + + // Check for errors + if (curl_errno($ch)) { + $error = [ + 'heading' => 'CurlError', + 'message' => curl_error($ch), + 'level' => 'error', + 'ref' => 'curlError', + 'type' => 'request', + ]; + // @todo store error + } + + // Close the cURL session + curl_close($ch); + + return $response; + } + + private function validateGuid() + { + $headers = [ + 'Content-Type: application/x-www-form-urlencoded', + ]; + + $params = [ + 'guid' => $this->guid, + 'keyfile' => $this->keyfile, + ]; + + /** + * returns {JSON} + * hasNewerKeyfile : boolean; + * purchaseable: true; + * registered: false; + * replaceable: false; + * upgradeable: false; + * upgradeAllowed: string[]; + * updatesRenewable: false; + */ + $response = $this->request( + self::KEY_SERVER_URL . '/validate/guid', + 'POST', + http_build_query($params), + $headers, + ); + + // Handle the response as needed (parsing JSON, etc.) + $decodedResponse = json_decode($response, true); + + if (!empty($decodedResponse)) { + return $decodedResponse; + } + + // @todo save error response somewhere + return []; + } + + private function getLatestKey() + { + $headers = [ + 'Content-Type: application/x-www-form-urlencoded', + ]; + + $params = [ + 'keyfile' => $this->keyfile, + ]; + + /** + * returns {JSON} + * license: string; + */ + $response = $this->request( + self::KEY_SERVER_URL . '/key/latest', + 'POST', + http_build_query($params), + $headers, + ); + + // Handle the response as needed (parsing JSON, etc.) + $decodedResponse = json_decode($response, true); + + if (!empty($decodedResponse) && !empty($decodedResponse['license'])) { + return $decodedResponse['license']; + } + return null; + } + + private function installNewKey($key): bool + { + require_once "$this->docroot/webGui/include/InstallKey.php"; + + $KeyInstaller = new KeyInstaller(); + $installResponse = $KeyInstaller->installKey($key); + + $installSuccess = false; + + if (!empty($installResponse)) { + $decodedResponse = json_decode($installResponse, true); + if (isset($decodedResponse['error'])) { + $this->writeJsonFile( + '/tmp/ReplaceKey/error.json', + [ + 'error' => $decodedResponse['error'], + 'ts' => time(), + ] + ); + $installSuccess = false; + } else { + $installSuccess = true; + } + } + + // Set up notification for key installation result + $keyType = basename($key, '.key'); + $output = _var($notify,'plugin'); + $script = '/usr/local/emhttp/webGui/scripts/notify'; + + if ($installSuccess) { + $event = "Installed New $keyType License"; + $subject = "Your new $keyType license key has been automatically installed"; + $description = ""; + $importance = "normal $output"; + } else { + $event = "Failed to Install New $keyType License"; + $subject = "Failed to automatically install your new $keyType license key"; + $description = isset($decodedResponse['error']) ? $decodedResponse['error'] : "Unknown error occurred"; + $importance = "alert $output"; + } + + exec("$script -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i ".escapeshellarg($importance)." -l '/Tools/Registration' -x"); + + return $installSuccess; + } + + private function writeJsonFile($file, $data) + { + if (!is_dir(dirname($file))) { + mkdir(dirname($file)); + } + + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + public function check(bool $forceCheck = false): ?bool + { + // we don't need to check + if (empty($this->guid) || empty($this->keyfile) || empty($this->regExp)) { + return null; + } + + // Check if we're within the 7-day window before and after regExp + $now = time(); + $sevenDaysBefore = strtotime('-7 days', $this->regExp); + $sevenDaysAfter = strtotime('+7 days', $this->regExp); + + $isWithinWindow = ($now >= $sevenDaysBefore && $now <= $sevenDaysAfter); + + if (!$forceCheck && !$isWithinWindow) { + return null; + } + + // see if we have a new key + $validateGuidResponse = $this->validateGuid(); + + $hasNewerKeyfile = @$validateGuidResponse['hasNewerKeyfile'] ?? false; + if (!$hasNewerKeyfile) { + return null; // if there is no newer keyfile, we don't need to do anything + } + + $latestKey = $this->getLatestKey(); + if (!$latestKey) { + // we supposedly have a new key, but didn't get it back… + $this->writeJsonFile( + '/tmp/ReplaceKey/error.json', + [ + 'error' => 'Failed to retrieve latest key after getting a `hasNewerKeyfile` in the validation response.', + 'ts' => time(), + ] + ); + return null; + }; + + return $this->installNewKey($latestKey); + } +}