summaryrefslogblamecommitdiffstats
path: root/modules-available/webinterface/inc/acme.inc.php
blob: 3f5e76a09bd608975e1509a40a464a9ec0533a65 (plain) (tree)


















































































                                                                                                                              
                                                     
                                                     
                                                               

                                                               
                                                                                     


                 
                                                                           


                                                    







                                                                                                              
                                                               
                                                                                                                              
                         

                                                                                                   




                                                                                                                                                             

































































                                                                                         
<?php

class Acme
{

	const PROP_ERROR = 'acme.error-string';
	const PROP_PROVIDER = 'acme.provider';
	const PROP_KEY_ID = 'acme.key-id';
	const PROP_HMAC_KEY = 'acme.hmac-key';
	const PROP_DOMAINS = 'acme.domains';
	const PROP_MAIL = 'acme.mail';
	const VALID_PROVIDERS = [
		'letsencrypt' => "Let's Encrypt",
		'zerossl' => 'ZeroSSL.com',
		'buypass' => 'BuyPass.com',
		'geant/sectigo' => 'GEANT via Sectigo',
	];

	const PROVIDER_ALIASES = [
		'geant/sectigo' => 'https://acme.sectigo.com/v2/GEANTOV',
	];

	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 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));
	}

	public static function setConfig(string $provider, string $mail, ?string $keyId = null, ?string $hmacKey = null): bool
	{
		if (!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 handleErrorAsync($task): void
	{
		if (!is_array($task) || !Taskmanager::isTask($task))
			return;
		$task = Taskmanager::waitComplete($task, 250);
		$args = ['user' => User::getLogin()];
		if (Taskmanager::isFinished($task)) {
			self::callbackErrorCheck($task, $args);
		} else {
			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 (($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);
		}
	}

	public static function issueNewCertificate(bool $wipeAll = false): ?string
	{
		$provider = self::getProvider();
		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::handleErrorAsync($task);
		return $task['id'] ?? null;
	}

	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::handleErrorAsync($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);
	}

}