1
0
Fork 0
mirror of synced 2024-06-02 10:54:44 +12:00

PR review changes

This commit is contained in:
Matej Bačo 2022-05-11 13:11:58 +00:00
parent e4379ec850
commit 6d94c1d6e8
6 changed files with 352 additions and 264 deletions

View file

@ -93,7 +93,7 @@ $logError = function(Throwable $error, string $action) use ($register) {
$server->error($logError);
function getConsoleDB(Registry &$register, string $namespace)
function getDatabase(Registry &$register, string $namespace)
{
$attempts = 0;
@ -141,7 +141,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
go(function () use ($register, $containerId, &$statsDocument, $logError) {
try {
[$database, $returnDatabase] = getConsoleDB($register, '_console');
[$database, $returnDatabase] = getDatabase($register, '_console');
$document = new Document([
'$id' => $database->getId(),
'$collection' => 'realtime',
@ -194,7 +194,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
}
try {
[$database, $returnDatabase] = getConsoleDB($register, '_console');
[$database, $returnDatabase] = getDatabase($register, '_console');
$statsDocument
->setAttribute('timestamp', time())
@ -221,7 +221,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
*/
if ($realtime->hasSubscriber('console', 'role:member', 'project')) {
[$database, $returnDatabase] = getConsoleDB($register, '_console');
[$database, $returnDatabase] = getDatabase($register, '_console');
$payload = [];
@ -325,7 +325,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
return;
}
[$database, $returnDatabase] = getConsoleDB($register, "_{$projectId}");
[$database, $returnDatabase] = getDatabase($register, "_{$projectId}");
$user = $database->getDocument('users', $userId);

View file

@ -9,26 +9,22 @@ use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Registry\Registry;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Database\Query;
function getConsoleDB(Registry &$register)
function getConsoleDB(): Database
{
global $register;
$attempts = 0;
do {
try {
$attempts++;
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$cache = new Cache(new RedisCache($register->get('cache')));
$database = new Database(new MariaDB($register->get('db')), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace('_console');
$database->setNamespace('_console'); // Main DB
break; // leave loop if successful
} catch(\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
@ -39,20 +35,13 @@ function getConsoleDB(Registry &$register)
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return [
$database,
function () use ($register, $db, $redis) {
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis);
}
];
};
return $database;
}
$cli
->task('maintenance')
->desc('Schedules maintenance tasks and publishes them to resque')
->action(function () use ($register) {
->action(function () {
Console::title('Maintenance V1');
Console::success(APP_NAME.' maintenance process v1 has started');
@ -128,25 +117,17 @@ $cli
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600');//36 hours
$usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days
Console::loop(function() use ($register, $interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
go(function () use ($register, $interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
try {
[$database, $returnDatabase] = getConsoleDB($register, '_console');
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
renewCertificates($database);
} catch (\Throwable $th) {
throw $th;
} finally {
call_user_func($returnDatabase);
}
});
Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
$database = getConsoleDB();
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
renewCertificates($database);
}, $interval);
});

View file

@ -17,5 +17,6 @@ $cli
// Scheduje a job
Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [
'domain' => $domain,
'skipRenewCheck' => true
]);
});

View file

@ -339,6 +339,7 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
@ -471,7 +472,6 @@ services:
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER

View file

@ -17,7 +17,13 @@ Console::success(APP_NAME . ' certificates worker v1 has started');
class CertificatesV1 extends Worker
{
private $certificate = null; // run function fills this. onError callback uses it
/**
* Database connection shared across all methods of this file
*
* @var Database
*/
private $dbForConsole;
public function getName(): string {
return "certificates";
@ -32,16 +38,14 @@ class CertificatesV1 extends Worker
{
Authorization::disable();
$dbForConsole = $this->getConsoleDB();
$this->certificate = new Document();
$this->dbForConsole = $this->getConsoleDB();
/**
* 1. Read arguments and validate domain
* 2. Get main domain
* 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain)
* 4. Validate renew date with certificate file, unless requested to skip by parameter
* 5. Validate security email. Cannot be empty, required by LetsEncrypt
* 4. Validate security email. Cannot be empty, required by LetsEncrypt
* 5. Validate renew date with certificate file, unless requested to skip by parameter
* 6. Issue a certificate using certbot CLI
* 7. Update 'log' attribute on certificate document with Certbot message
* 8. Create storage folder for certificate, if not ready already
@ -67,225 +71,71 @@ class CertificatesV1 extends Worker
*/
try {
// Get attributes
// Read arguments
$domain = $this->args['domain']; // String of domain (hostname)
$skipRenewCheck = $this->args['skipRenewCheck'] ?? false; // If true, we won't double-check expiry from cert file
$domain = new Domain((!empty($domain)) ? $domain : '');
$this->certificate->setAttribute('domain', $domain->get());
$skipRenewCheck = $this->args['skipRenewCheck'] ?? false; // If true, we won't double-check expiry from cert file
$mainDomain = null; // ENV or first ever visited domain
if (!empty(App::getEnv('_APP_DOMAIN', ''))) {
$mainDomain = App::getEnv('_APP_DOMAIN', '');
} else {
$domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
}
// If not main domain, we will check CNAME record
$validateCNAME = false;
if ($domain->get() !== $mainDomain) {
$validateCNAME = true;
}
if (empty($domain->get())) {
throw new Exception('Missing certificate domain.');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new Exception('Unknown public suffix for domain.');
}
if ($validateCNAME) {
// TODO: Would be awesome to also support A/AAAA records here. Maybe dry run?
// Validate if domain target is properly configured
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if (!$target->isKnown() || $target->isTest()) {
throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.');
}
// Verify domain with DNS records
$validator = new CNAME($target->get());
if (!$validator->isValid($domain->get())) {
throw new Exception('Failed to verify domain DNS records.');
}
} else {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
// Get current certificate
$certificate = $this->dbForConsole->findOne('certificates', [ new Query('domain', Query::TYPE_EQUAL, [$domain->get()]) ]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if(!$certificate) {
$certificate = new Document();
$certificate->setAttribute('domain', $domain->get());
}
// If certificate exists already, double-check expiry date
// If asked to skip, we won't
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/cert.pem';
if (!$skipRenewCheck && \file_exists($certPath)) {
$validTo = null;
try {
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
throw new Exception('Invalid expiry date.');
}
} catch(\Throwable $th) {
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60*60*24*30);
if ($validTo - $expiryInAdvance > \time()) {
$validToVerbose = date('d-m-Y H:i:s', $validTo);
throw new Exception('Renew isn\'t required. Next renew at ' . $validToVerbose);
}
}
// Email for alerts is required by LetsEncrypt
$email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.');
}
// LetsEncrypt communication to issue certificate (using certbot CLI)
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain->get()}", '', $stdout, $stderr);
// All exceptions from now on will be marked to increment attempts count. This allows us to only limit attempts for domains that failed on LectEncrypt side.
// Such attempts count allows us to prevent API limit abuse with always failing domains
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
$mainDomain = $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
$this->validateDomain($domain, $isMainDomain);
// If certificate exists already, double-check expiry date
// If asked to skip, we won't
if(!$skipRenewCheck && !$this->isRenewRequired($domain->get())) {
throw new Exception('Renew isn\'t required.');
}
// Generate certificate files using Let's Encrypt
$letsEncryptData = $this->letsEncryptAction($domain->get(), $email);
// Command succeeded, store all data into document
// We store stderr too, because it may include warnings
// This is only stored if everytng below passes too. Otherwise, it will be overwritten by error message
$this->certificate->setAttribute('log', \json_encode([
'stdout' => $stdout,
'stderr' => $stderr,
$certificate->setAttribute('log', \json_encode([
'stdout' => $letsEncryptData['stdout'],
'stderr' => $letsEncryptData['stderr'],
]));
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain->get();
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files from certbot into our storage
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem: ' . \json_encode($stdout));
}
// This multi-line syntax helps IDE
$config =
"tls:" .
" certificates:" .
" - certFile: /storage/certificates/{$domain->get()}/fullchain.pem" .
" keyFile: /storage/certificates/{$domain->get()}/privkey.pem";
// Give certificates to Traefik
$this->applyCertificateFiles($domain->get());
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
// Read new renew date from cert file
// TODO: This might not be required, we could calculate it. But this feels safer
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
$expiryInAdvance = (60*60*24*30);
$this->certificate->setAttribute('renewDate', $validTo - $expiryInAdvance);
// All went well at this point 🥳
// Reset attempts count for next renwal
$this->certificate->setAttribute('attempts', 0);
// Mark issue date
$this->certificate->setAttribute('issueDate', \time());
// Update certificate info stored in database
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
$certificate->setAttribute('attempts', 0);
$certificate->setAttribute('issueDate', \time());
} catch(Throwable $e) {
// These exceptions are expected if renew shouldn't or can't happen
// Set exception as log in certificate document
$certificate->setAttribute('log', $e->getMessage());
// Add exception as log into certificate
$this->certificate->setAttribute('log', $e->getMessage());
$attempt = $this->certificate->getAttribute('attempts', 0);
$attempt++;
// Save increased attempts count
$this->certificate->setAttribute('attempts', $attempt);
Console::warning('Cannot renew domain (' . $domain->get() . ') on attempt no. ' . $attempt . ' certificate: ' . $e->getMessage());
// Increase attempts count
$attempts = $certificate->getAttribute('attempts', 0) + 1;
$certificate->setAttribute('attempts', $attempts);
// Send email to security email
Resque::enqueue(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME, [
'from' => 'console',
'project' => 'console',
'name' => 'Appwrite Administrator',
'recipient' => App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'),
'url' => 'https://' . $domain->get(),
'locale' => App::getEnv('_APP_LOCALE', 'en'),
'type' => MAIL_TYPE_CERTIFICATE,
'domain' => $domain->get(),
'error' => $e->getMessage(),
'attempt' => $attempt
]);
$this->notifyError($domain->get(), $e->getMessage(), $attempts);
} finally {
// All actions result in new updatedAt date
$this->certificate->setAttribute('updated', \time());
// Save certificate data into database
// Check if update or insert required
$certificateDocument = $dbForConsole->findOne('certificates', [ new Query('domain', Query::TYPE_EQUAL, [$domain->get()]) ]);
if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) {
// Merge new data with current data
$this->certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $this->certificate->getArrayCopy()));
$this->certificate = $dbForConsole->updateDocument('certificates', $this->certificate->getId(), $this->certificate);
} else {
$this->certificate = $dbForConsole->createDocument('certificates', $this->certificate);
}
// Update domains with new certificate ID
$certificateId = $this->certificate->getId();
$domains = $dbForConsole->find('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain->get()])
], 1000);
foreach ($domains as $domainDocument) {
$domainDocument->setAttribute('updated', \time());
$domainDocument->setAttribute('certificateId', $certificateId);
$dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument);
$dbForConsole->deleteCachedDocument('projects', $domainDocument->getAttribute('projectId'));
}
$certificate->setAttribute('updated', \time());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate);
Authorization::reset();
}
}
@ -293,4 +143,260 @@ class CertificatesV1 extends Worker
public function shutdown(): void
{
}
/**
* Save certificate data into database.
*
* @param string $domain Domain name that certificate is for
* @param Document $certificate Certificate document that we need to save
*
* @return void
*/
private function saveCertificateDocument(string $domain, Document $certificate) {
// Check if update or insert required
$certificateDocument = $this->dbForConsole->findOne('certificates', [ new Query('domain', Query::TYPE_EQUAL, [$domain]) ]);
if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $this->dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
} else {
$certificate = $this->dbForConsole->createDocument('certificates', $certificate);
}
$certificateId = $certificate->getId();
$this->updateDomainDocuments($certificateId, $domain);
}
/**
* Get main domain. Needed as we do different checks for main and non-main domains.
*
* @return null|string Returns main domain. If null, there is no main domain yet.
*/
private function getMainDomain(): ?string {
if (!empty(App::getEnv('_APP_DOMAIN', ''))) {
$mainDomain = App::getEnv('_APP_DOMAIN', '');
} else {
$domainDocument = $this->dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
if($domainDocument) {
$mainDomain = $domainDocument->getAttribute('domain');
}
}
return $mainDomain;
}
/**
* Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt)
* - Domain must have proper DNS record
*
* @param Domain $domain Domain which we validate
* @param bool $isMainDomain In case of master domain, we look for different DNS configurations
*
* @return void
*/
private function validateDomain(Domain $domain, bool $isMainDomain): void {
if (empty($domain->get())) {
throw new Exception('Missing certificate domain.');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new Exception('Unknown public suffix for domain.');
}
if (!$isMainDomain) {
// TODO: Would be awesome to also support A/AAAA records here. Maybe dry run?
// Validate if domain target is properly configured
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if (!$target->isKnown() || $target->isTest()) {
throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.');
}
// Verify domain with DNS records
$validator = new CNAME($target->get());
if (!$validator->isValid($domain->get())) {
throw new Exception('Failed to verify domain DNS records.');
}
} else {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
}
}
/**
* Reads expiry date of certificate from file and decides if renewal is required or not.
*
* @param string $domain Domain for which we check certificate file
*
* @return bool True, if certificate needs to be renewed
*/
private function isRenewRequired(string $domain): bool {
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$validTo = null;
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60*60*24*30);
if ($validTo - $expiryInAdvance > \time()) {
return false;
}
}
return true;
}
/**
* LetsEncrypt communication to issue certificate (using certbot CLI)
*
* @param string $domain Domain to generate certificate for
*
* @return array Named array with keys 'stdout' and 'stderr', both string
*/
private function letsEncryptAction(string $domain, string $email): array {
$staging = (App::isProduction()) ? '' : ' --dry-run';
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}", '', $stdout, $stderr);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
return [
'stdout' => $stdout,
'stderr' => $stderr
];
}
/**
* Read new renew date from certificate file generated by Let's Encrypt
*
* @param string $domain Domain which certificate was generated for
*
* @return int
*/
private function getRenewDate(string $domain): int {
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
$expiryInAdvance = (60*60*24*30); // 30 days
return $validTo - $expiryInAdvance;
}
/**
* Method to take files from Let's Encrypt, and put it into Traefik.
*
* @param string $domain Domain which certificate was generated for
*
* @return void
*/
private function applyCertificateFiles(string $domain): void {
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files from certbot into our storage
if(!@\rename('/etc/letsencrypt/live/'.$domain.'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain.'/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem.');
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem.');
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem.');
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem.');
}
$config =
"tls:" .
" certificates:" .
" - certFile: /storage/certificates/{$domain}/fullchain.pem" .
" keyFile: /storage/certificates/{$domain}/privkey.pem";
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
}
/**
* Method to make sure information about error is delivered to admnistrator.
*
* @param string $domain Domain that caused the error
* @param string $errorMessage Verbose error message
* @param int $attempt How many times it failed already
*
* @return void
*/
private function notifyError(string $domain, string $errorMessage, int $attempt): void {
// Log error into console
Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage);
// Send mail to administratore mail
Resque::enqueue(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME, [
'from' => 'console',
'project' => 'console',
'name' => 'Appwrite Administrator',
'recipient' => App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'),
'url' => 'https://' . $domain,
'locale' => App::getEnv('_APP_LOCALE', 'en'),
'type' => MAIL_TYPE_CERTIFICATE,
'domain' => $domain,
'error' => $errorMessage,
'attempt' => $attempt
]);
}
/**
* Update all existing domain documents so they have relation to correct certificate document.
* This solved issues:
* - when adding a domain for which there is already a certificate
* - when renew creates new document? It might?
* - overall makes it more reliable
*
* @param string $certificateId ID of a new or updated certificate document
* @param string $domain Domain that is affected by new certificate
*
* @return void
*/
private function updateDomainDocuments(string $certificateId, string $domain): void {
$domains = $this->dbForConsole->find('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain])
], 1000);
foreach ($domains as $domainDocument) {
$domainDocument->setAttribute('updated', \time());
$domainDocument->setAttribute('certificateId', $certificateId);
$this->dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument);
$this->dbForConsole->deleteCachedDocument('projects', $domainDocument->getAttribute('projectId'));
}
}
}

36
composer.lock generated
View file

@ -3550,16 +3550,16 @@
},
{
"name": "matthiasmullie/minify",
"version": "1.3.67",
"version": "1.3.68",
"source": {
"type": "git",
"url": "https://github.com/matthiasmullie/minify.git",
"reference": "acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9"
"reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9",
"reference": "acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297",
"reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297",
"shasum": ""
},
"require": {
@ -3608,7 +3608,7 @@
],
"support": {
"issues": "https://github.com/matthiasmullie/minify/issues",
"source": "https://github.com/matthiasmullie/minify/tree/1.3.67"
"source": "https://github.com/matthiasmullie/minify/tree/1.3.68"
},
"funding": [
{
@ -3616,7 +3616,7 @@
"type": "github"
}
],
"time": "2022-03-24T08:54:59+00:00"
"time": "2022-04-19T08:28:56+00:00"
},
{
"name": "matthiasmullie/path-converter",
@ -5710,16 +5710,16 @@
},
{
"name": "symfony/console",
"version": "v6.0.7",
"version": "v6.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e"
"reference": "0d00aa289215353aa8746a31d101f8e60826285c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"url": "https://api.github.com/repos/symfony/console/zipball/0d00aa289215353aa8746a31d101f8e60826285c",
"reference": "0d00aa289215353aa8746a31d101f8e60826285c",
"shasum": ""
},
"require": {
@ -5785,7 +5785,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.0.7"
"source": "https://github.com/symfony/console/tree/v6.0.8"
},
"funding": [
{
@ -5801,7 +5801,7 @@
"type": "tidelift"
}
],
"time": "2022-03-31T17:18:25+00:00"
"time": "2022-04-20T15:01:42+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@ -6135,16 +6135,16 @@
},
{
"name": "symfony/string",
"version": "v6.0.3",
"version": "v6.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2"
"reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/522144f0c4c004c80d56fa47e40e17028e2eefc2",
"reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2",
"url": "https://api.github.com/repos/symfony/string/zipball/ac0aa5c2282e0de624c175b68d13f2c8f2e2649d",
"reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d",
"shasum": ""
},
"require": {
@ -6200,7 +6200,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.0.3"
"source": "https://github.com/symfony/string/tree/v6.0.8"
},
"funding": [
{
@ -6216,7 +6216,7 @@
"type": "tidelift"
}
],
"time": "2022-01-02T09:55:41+00:00"
"time": "2022-04-22T08:18:02+00:00"
},
{
"name": "textalk/websocket",