'HARICA', 'letsencrypt' => "Let's Encrypt", 'zerossl' => 'ZeroSSL.com', //'geant/sectigo' => 'GEANT via Sectigo', 'custom' => '...', ]; const PROVIDER_ALIASES = [ 'geant/sectigo' => 'https://acme.sectigo.com/v2/GEANTOV', 'harica' => 'https://acme-v02.harica.gr/acme/directory', ]; public static function getLastError(): ?string { return Property::get(self::PROP_ERROR, null); } public static function getProvider(): ?string { return Property::get(self::PROP_PROVIDER, null); } public static function getServerUrl(): ?string { return Property::get(self::PROP_CUSTOM_ACME_URL, null); } public static function getKeyId(): ?string { return Property::get(self::PROP_KEY_ID, null); } public static function getHmacKey(): ?string { return Property::get(self::PROP_HMAC_KEY, null); } /** * @return string[] list of [id] => friendly name */ public static function getProviders(): array { return self::VALID_PROVIDERS; } public static function getMail(): ?string { return Property::get(self::PROP_MAIL, null); } public static function getDomains(): array { return explode(' ', Property::get(self::PROP_DOMAINS)); } /** * Sets the configuration to the specified provider with optional server URL and authentication keys. * * @param string $provider The provider identifier, either 'custom' or a key in the valid providers list. * @param string $mail The email address associated with the provider. * @param string|null $serverUrl The custom server URL for the provider, required for the 'custom' provider and must use HTTPS. * @param string|null $keyId The optional key ID used for authentication. * @param string|null $hmacKey The optional HMAC key for authentication. * * @return bool Returns true if the configuration is successfully set, false otherwise. */ public static function setConfig(string $provider, string $mail, ?string $serverUrl = null, ?string $keyId = null, ?string $hmacKey = null): bool { if ($provider === 'custom') { if (substr($serverUrl, 0, 6) !== 'https:') { Message::addError('webinterface.acme-invalid-url', $serverUrl); return false; } Property::set(self::PROP_CUSTOM_ACME_URL, $serverUrl); // Only update if custom is selected } elseif (!isset(self::VALID_PROVIDERS[$provider])) { Message::addError('webinterface.acme-invalid-provider', $provider); return false; } Property::set(self::PROP_PROVIDER, $provider); Property::set(self::PROP_MAIL, $mail); Property::set(self::PROP_KEY_ID, $keyId); Property::set(self::PROP_HMAC_KEY, $hmacKey); return true; } public static function setDomains(array $list): void { Property::set(self::PROP_DOMAINS, implode(' ', $list)); } private static function handleAcmeResultAsync($task): void { if (!is_array($task) || !Taskmanager::isTask($task)) return; $task = Taskmanager::waitComplete($task, 250); $args = [ 'user' => User::getLogin(), 'previous' => Property::get(WebInterface::PROP_TYPE), // Remember in case of failure ]; if (Taskmanager::isFinished($task)) { self::callbackErrorCheck($task, $args); } else { Property::set(WebInterface::PROP_TYPE, 'acme'); Property::set(self::PROP_ERROR, false); TaskmanagerCallback::addCallback($task, 'acmeErrors', $args); } } public static function callbackErrorCheck(array $task, $args): void { if (!Taskmanager::isFinished($task)) return; $otherError = false; if (isset($task['data']['error'])) { // This should never happen, so make it an error if (strpos($task['data']['error'], " is not an issued domain, skipping.") !== false) { $otherError = true; } } if (Taskmanager::isFailed($task) || $otherError) { if (isset($args['previous']) && Property::get(WebInterface::PROP_TYPE) === 'acme') { Property::set(WebInterface::PROP_TYPE, $args['previous']); } if (($args['user'] ?? null) === null) { EventLog::warning('Automatic ACME renewal of HTTPS certificate failed', print_r($task, true)); } Property::set(self::PROP_ERROR, $task['data']['error'] ?? 'Unknown error'); } else { // If the cronjob called us and there is nothing to do, suppress the event log entry if (($args['user'] ?? null) !== null || strpos($task['data']['error'] ?? '', 'Skipping. Next renewal time is: ') === false) { EventLog::info('ACME issue/renewal of HTTPS certificate by ' . ($args['user'] ?? 'automatic cronjob'), print_r($task, true)); } Property::set(self::PROP_ERROR, false); } } /** * Issues a new certificate using the configured ACME provider and other relevant details. * * @param bool $wipeAll Indicates whether all existing certificates and accounts should be wiped before issuing a new one. * @return ?string The task ID of the certificate issuance process, or null if an error occurred. */ public static function issueNewCertificate(bool $wipeAll = false): ?string { $provider = self::getProvider(); if ($provider === 'custom') { $provider = Property::get(self::PROP_CUSTOM_ACME_URL, null); } if ($provider === null) { Message::addError('webinterface.acme-no-provider'); return null; } $mail = self::getMail(); if (empty($mail)) { Message::addError('webinterface.acme-no-mail'); return null; } $domains = self::getDomains(); if (empty($domains)) { Message::addError('webinterface.acme-no-domains'); return null; } $redirect = Property::get(WebInterface::PROP_REDIRECT); $task = Taskmanager::submit('LighttpdHttps', [ 'redirect' => $redirect, 'acmeMode' => 'issue', 'acmeMail' => $mail, 'acmeDomains' => $domains, 'acmeProvider' => self::PROVIDER_ALIASES[$provider] ?? $provider, 'acmeKeyId' => self::getKeyId(), 'acmeHmacKey' => self::getHmacKey(), 'acmeWipeAll' => $wipeAll, ]); self::handleAcmeResultAsync($task); return $task['id'] ?? null; } /** * Renews certificates based on available domains. * This expects a valid configuration and existing account. * * @return ?string ID of the submitted task for the renewal process or null if no domains are available */ public static function renew(): ?string { error_log("Renew called"); $domains = self::getDomains(); if (empty($domains)) { Message::addError('webinterface.acme-no-domains'); return null; } $redirect = Property::get(WebInterface::PROP_REDIRECT); $task = Taskmanager::submit('LighttpdHttps', [ 'redirect' => $redirect, 'acmeMode' => 'renew', 'acmeDomains' => $domains, ]); self::handleAcmeResultAsync($task); return $task['id'] ?? null; } public static function tryEnable(): bool { $redirect = Property::get(WebInterface::PROP_REDIRECT); $task = Taskmanager::submit('LighttpdHttps', [ 'redirect' => $redirect, 'acmeMode' => 'try-enable', ]); $task = Taskmanager::waitComplete($task, 10000); return Taskmanager::isFinished($task) && !Taskmanager::isFailed($task); } }