Refactored SSL genration

This commit is contained in:
Matej Bačo
2022-04-10 09:38:22 +00:00
parent 3c98e1b3b0
commit 752079c874
10 changed files with 412 additions and 224 deletions
+3
View File
@@ -5,6 +5,9 @@ require_once __DIR__.'/controllers/general.php';
use Utopia\App;
use Utopia\CLI\CLI;
use Utopia\CLI\Console;
use Utopia\Database\Validator\Authorization;
Authorization::disable();
$cli = new CLI();
+2 -2
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
@@ -1389,8 +1390,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
$dbForConsole->deleteCachedDocument('projects', $project->getId());
// Issue a TLS certificate when domain is verified
Resque::enqueue('v1-certificates', 'CertificatesV1', [
'document' => $domain->getArrayCopy(),
Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [
'domain' => $domain->getAttribute('domain'),
]);
+3 -5
View File
@@ -12,6 +12,7 @@ use Appwrite\Extend\Exception;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
@@ -90,11 +91,8 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in a few seconds...');
Resque::enqueue('v1-certificates', 'CertificatesV1', [
'document' => $domainDocument,
'domain' => $domain->get(),
'validateTarget' => false,
'validateCNAME' => false,
Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [
'domain' => $domain->get()
]);
}
+87 -9
View File
@@ -1,15 +1,58 @@
<?php
global $cli;
global $register;
use Appwrite\Event\Event;
use Utopia\App;
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 getDatabase(Registry &$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);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace('_console');
break; // leave loop if successful
} catch(\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: '. $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return [
$database,
function () use ($register, $db, $redis) {
$register->get('dbPool')->put($db);
$register->get('redisPool')->put($redis);
}
];
};
$cli
->task('maintenance')
->desc('Schedules maintenance tasks and publishes them to resque')
->action(function () {
->action(function () use ($register) {
Console::title('Maintenance V1');
Console::success(APP_NAME.' maintenance process v1 has started');
@@ -54,6 +97,29 @@ $cli
]);
}
function renewCertificates($dbForConsole)
{
$time = date('d-m-Y H:i:s', time());
/** @var Utopia\Database\Database $dbForConsole */
$certificates = $dbForConsole->find('certificates', [
new Query('attempts', Query::TYPE_LESSER, [5]), // Maximum 5 attempts
new Query('renewDate', Query::TYPE_LESSEREQUAL, [\time()]) // includes 60 days cooldown (we have 30 days to renew)
], 300); // Limit 300 comes from LetsEncrypt (orders per 3 hours)
if(\count($certificates) > 0) {
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
foreach ($certificates as $certificate) {
Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [
'domain' => $certificate->getAttribute('domain'),
]);
}
} else {
Console::info("[{$time}] No certificates for renewal.");
}
}
// # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
@@ -62,13 +128,25 @@ $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 ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying deletes workers every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
Console::loop(function() use ($register, $interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
go(function () use ($register, $interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
try {
[$database, $returnDatabase] = getDatabase($register, '_console');
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying deletes workers 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);
}
});
}, $interval);
});
+7 -5
View File
@@ -11,13 +11,15 @@ $cli
->action(function () {
$domain = App::getEnv('_APP_DOMAIN', '');
Console::log('Issue a TLS certificate for master domain ('.$domain.') in 30 seconds.
// TODO: Instead of waiting, let's ping Traefik. If responds, we can schedule instantly
// TODO: Add support for argument (domain)
Console::log('Issue a TLS certificate for master domain ('.$domain.') in 2 seconds.
Make sure your domain points to your server or restart to try again.');
ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [
'document' => [],
// Const for types not available here
ResqueScheduler::enqueueAt(\time() + 2, 'v1-certificates', 'CertificatesV1', [
'domain' => $domain,
'validateTarget' => false,
'validateCNAME' => false,
'skipRenewCheck' => true // TODO: Discuss this behabiour. true? false? parameter? How do we document it?
]);
});
+3
View File
@@ -449,6 +449,9 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
+204 -147
View File
@@ -1,7 +1,9 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Resque\Worker;
use Appwrite\Exception\Certificate as ExceptionCertificate;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
@@ -26,169 +28,224 @@ class CertificatesV1 extends Worker
public function run(): void
{
$dbForConsole = $this->getConsoleDB();
/**
* 1. Get new domain document - DONE
* 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE
* 2. Check if a certificate already exists - DONE
* 3. Check if certificate is about to expire, if not - skip it
* 3.1. Create / renew certificate
* 3.2. Update loadblancer
* 3.3. Update database (domains, change date, expiry)
* 3.4. Set retry on failure
* 3.5. Schedule to renew certificate in 60 days
*/
Authorization::disable();
// Args
$document = $this->args['document'];
$domain = $this->args['domain'];
$dbForConsole = $this->getConsoleDB();
// Validation Args
$validateTarget = $this->args['validateTarget'] ?? true;
$validateCNAME = $this->args['validateCNAME'] ?? true;
$certificate = new Document();
// Options
$domain = new Domain((!empty($domain)) ? $domain : '');
$expiry = 60 * 60 * 24 * 30 * 2; // 60 days
$safety = 60 * 60; // 1 hour
$renew = (\time() + $expiry);
try {
/**
* TODO: Update
* 1. Get new domain document - DONE
* 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE
* 2. Check if a certificate already exists - DONE
* 3. Check if certificate is about to expire, if not - skip it
* 3.1. Create / renew certificate
* 3.2. Update loadblancer
* 3.3. Update database (domains, change date, expiry)
* 3.4. Set retry on failure
* 3.5. Schedule to renew certificate in 60 days
*/
// Get attributes
$domain = $this->args['domain']; // String of domain (hostname)
$domain = new Domain((!empty($domain)) ? $domain : '');
if (empty($domain->get())) {
throw new Exception('Missing domain');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new Exception('Unknown public suffix for domain');
}
if ($validateTarget) {
$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.');
$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 ($validateCNAME) {
$validator = new CNAME($target->get()); // Verify Domain with DNS records
if(!$validator->isValid($domain->get())) {
throw new Exception('Failed to verify domain DNS records');
// If not main domain, we will check CNAME record
$validateCNAME = false;
if ($domain->get() !== $mainDomain) {
$validateCNAME = true;
}
}
$certificate = $dbForConsole->findOne('certificates', [
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
]);
// $condition = ($certificate
// && $certificate instanceof Document
// && isset($certificate['issueDate'])
// && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false';
// throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition);
$certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : [];
if (
!empty($certificate)
&& isset($certificate['issueDate'])
&& (($certificate['issueDate'] + ($expiry)) > \time())
) { // Check last issue time
throw new Exception('Renew isn\'t required');
}
$staging = (App::isProduction()) ? '' : ' --dry-run';
$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');
}
$stdout = '';
$stderr = '';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain->get()}", '', $stdout, $stderr);
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
$path = APP_STORAGE_CERTIFICATES . '/' . $domain->get();
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path...');
if (empty($domain->get())) {
throw new ExceptionCertificate('Missing certificate domain.');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new ExceptionCertificate('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 ExceptionCertificate('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 ExceptionCertificate('Failed to verify domain DNS records.');
}
} else {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
}
}
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 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;
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));
}
try {
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'];
if (empty($validTo)) {
throw new Exception('Invalid expiry date.');
}
} catch(\Throwable $th) {
throw new ExceptionCertificate('Unable to read certificate file (cert.pem).');
}
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));
}
// 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 ExceptionCertificate('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 ExceptionCertificate('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.');
}
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));
}
// 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 ExceptionCertificate('Failed to issue a certificate with message: ' . $stderr, true);
}
$certificate = new Document(\array_merge($certificate, [
'domain' => $domain->get(),
'issueDate' => \time(),
'renewDate' => $renew,
'attempts' => 0,
'log' => \json_encode($stdout),
]));
$certificate = $dbForConsole->createDocument('certificates', $certificate);
if (!$certificate) {
throw new Exception('Failed saving certificate to DB');
}
if(!empty($document)) {
$certificate = new Document(\array_merge($document, [
'updated' => \time(),
'certificateId' => $certificate->getId(),
// 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
$certificate->setAttribute('log', \json_encode([
'stdout' => $stdout,
'stderr' => $stderr,
]));
$certificate = $dbForConsole->updateDocument('domains', $certificate->getId(), $certificate);
if(!$certificate) {
throw new Exception('Failed saving domain to DB');
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain->get();
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new ExceptionCertificate('Failed to create path for certificate.', true);
}
}
// 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 ExceptionCertificate('Failed to rename certificate cert.pem: '.\json_encode($stdout), true);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/chain.pem')) {
throw new ExceptionCertificate('Failed to rename certificate chain.pem: ' . \json_encode($stdout), true);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/fullchain.pem')) {
throw new ExceptionCertificate('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout), true);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/privkey.pem')) {
throw new ExceptionCertificate('Failed to rename certificate privkey.pem: ' . \json_encode($stdout), true);
}
// This multi-line syntax helps IDE
$config =
"tls:" .
" certificates:" .
" - certFile: /storage/certificates/{$domain->get()}/fullchain.pem" .
" keyFile: /storage/certificates/{$domain->get()}/privkey.pem";
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) {
throw new ExceptionCertificate('Failed to save Traefik configuration.', true);
}
// 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'];
$expiryInAdvance = (60*60*24*30);
$certificate->setAttribute('renewDate', $validTo - $expiryInAdvance);
// All went well at this point 🥳
// Reset attempts count for next renwal
$certificate->setAttribute('attempts', 0);
// Mark issue date
$certificate->setAttribute('issueDate', \time());
} catch(ExceptionCertificate $e) {
// These exceptions are expected if renew shouldn't or can't happen
// Add exception as log into certificate
$certificate->setAttribute('log', $e->getMessage());
$attempt = $certificate->getAttribute('attempts', 0);
$attempt++;
// Save increased attempts count if requested by exception
if($e->getIncrementAttempts()) {
$certificate->setAttribute('attempts', $attempt);
}
Console::warning('Cannot renew domain (' . $domain->get() . ') on attempt no. ' . $attempt . ' certificate: ' . $e->getMessage());
} finally {
// All actions result in new updatedAt date
$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
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
} else {
$certificate = $dbForConsole->createDocument('certificates', $certificate);
}
// Update domains with new certificate ID
$certificateId = $certificate->getId();
// TODO: Add logic for updating domains
Authorization::reset();
}
$config =
"tls:
certificates:
- certFile: /storage/certificates/{$domain->get()}/fullchain.pem
keyFile: /storage/certificates/{$domain->get()}/privkey.pem";
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) {
throw new Exception('Failed to save SSL configuration');
}
ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [
'document' => [],
'domain' => $domain->get(),
'validateTarget' => $validateTarget,
'validateCNAME' => $validateCNAME,
]); // Async task rescheduale
Authorization::reset();
}
public function shutdown(): void
Generated
+59 -56
View File
@@ -300,20 +300,23 @@
},
{
"name": "colinmollenhour/credis",
"version": "v1.12.1",
"version": "v1.13.0",
"source": {
"type": "git",
"url": "https://github.com/colinmollenhour/credis.git",
"reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb"
"reference": "afec8e58ec93d2291c127fa19709a048f28641e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/colinmollenhour/credis/zipball/c27faa11724229986335c23f4b6d0f1d8d6547fb",
"reference": "c27faa11724229986335c23f4b6d0f1d8d6547fb",
"url": "https://api.github.com/repos/colinmollenhour/credis/zipball/afec8e58ec93d2291c127fa19709a048f28641e5",
"reference": "afec8e58ec93d2291c127fa19709a048f28641e5",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
"php": ">=5.6.0"
},
"suggest": {
"ext-redis": "Improved performance for communicating with redis"
},
"type": "library",
"autoload": {
@@ -338,9 +341,9 @@
"homepage": "https://github.com/colinmollenhour/credis",
"support": {
"issues": "https://github.com/colinmollenhour/credis/issues",
"source": "https://github.com/colinmollenhour/credis/tree/v1.12.1"
"source": "https://github.com/colinmollenhour/credis/tree/v1.13.0"
},
"time": "2020-11-06T16:09:14+00:00"
"time": "2022-04-07T14:57:22+00:00"
},
{
"name": "composer/package-versions-deprecated",
@@ -1580,16 +1583,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.0.0",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced"
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
"reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c",
"shasum": ""
},
"require": {
@@ -1627,7 +1630,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1"
},
"funding": [
{
@@ -1643,7 +1646,7 @@
"type": "tidelift"
}
],
"time": "2021-11-01T23:48:49+00:00"
"time": "2022-01-02T09:55:41+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -3192,16 +3195,16 @@
},
{
"name": "composer/semver",
"version": "3.3.1",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "5d8e574bb0e69188786b8ef77d43341222a41a71"
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71",
"reference": "5d8e574bb0e69188786b8ef77d43341222a41a71",
"url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9",
"reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9",
"shasum": ""
},
"require": {
@@ -3253,7 +3256,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.3.1"
"source": "https://github.com/composer/semver/tree/3.3.2"
},
"funding": [
{
@@ -3269,7 +3272,7 @@
"type": "tidelift"
}
],
"time": "2022-03-16T11:22:07+00:00"
"time": "2022-04-01T19:23:25+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -3491,16 +3494,16 @@
},
{
"name": "felixfbecker/language-server-protocol",
"version": "1.5.1",
"version": "v1.5.2",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server-protocol.git",
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
"reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842",
"reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842",
"shasum": ""
},
"require": {
@@ -3541,9 +3544,9 @@
],
"support": {
"issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2"
},
"time": "2021-02-22T14:02:09+00:00"
"time": "2022-03-02T22:36:06+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -4118,16 +4121,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.6.0",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706"
"reference": "77a32518733312af16a44300404e945338981de3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706",
"reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
"reference": "77a32518733312af16a44300404e945338981de3",
"shasum": ""
},
"require": {
@@ -4162,9 +4165,9 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
},
"time": "2022-01-04T19:58:01+00:00"
"time": "2022-03-15T21:29:03+00:00"
},
{
"name": "phpspec/prophecy",
@@ -5073,16 +5076,16 @@
},
{
"name": "sebastian/environment",
"version": "5.1.3",
"version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "388b6ced16caa751030f6a69e588299fa09200ac"
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
"reference": "388b6ced16caa751030f6a69e588299fa09200ac",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"shasum": ""
},
"require": {
@@ -5124,7 +5127,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
},
"funding": [
{
@@ -5132,7 +5135,7 @@
"type": "github"
}
],
"time": "2020-09-28T05:52:38+00:00"
"time": "2022-04-03T09:37:03+00:00"
},
{
"name": "sebastian/exporter",
@@ -5715,16 +5718,16 @@
},
{
"name": "symfony/console",
"version": "v6.0.5",
"version": "v6.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1"
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/3bebf4108b9e07492a2a4057d207aa5a77d146b1",
"reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1",
"url": "https://api.github.com/repos/symfony/console/zipball/70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e",
"shasum": ""
},
"require": {
@@ -5790,7 +5793,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.0.5"
"source": "https://github.com/symfony/console/tree/v6.0.7"
},
"funding": [
{
@@ -5806,7 +5809,7 @@
"type": "tidelift"
}
],
"time": "2022-02-25T10:48:52+00:00"
"time": "2022-03-31T17:18:25+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -6058,16 +6061,16 @@
},
{
"name": "symfony/service-contracts",
"version": "v3.0.0",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603"
"reference": "e517458f278c2131ca9f262f8fbaf01410f2c65c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/36715ebf9fb9db73db0cb24263c79077c6fe8603",
"reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/e517458f278c2131ca9f262f8fbaf01410f2c65c",
"reference": "e517458f278c2131ca9f262f8fbaf01410f2c65c",
"shasum": ""
},
"require": {
@@ -6120,7 +6123,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.0.0"
"source": "https://github.com/symfony/service-contracts/tree/v3.0.1"
},
"funding": [
{
@@ -6136,7 +6139,7 @@
"type": "tidelift"
}
],
"time": "2021-11-04T17:53:12+00:00"
"time": "2022-03-13T20:10:05+00:00"
},
{
"name": "symfony/string",
@@ -6324,16 +6327,16 @@
},
{
"name": "twig/twig",
"version": "v3.3.8",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "972d8604a92b7054828b539f2febb0211dd5945c"
"reference": "8442df056c51b706793adf80a9fd363406dd3674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/972d8604a92b7054828b539f2febb0211dd5945c",
"reference": "972d8604a92b7054828b539f2febb0211dd5945c",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/8442df056c51b706793adf80a9fd363406dd3674",
"reference": "8442df056c51b706793adf80a9fd363406dd3674",
"shasum": ""
},
"require": {
@@ -6384,7 +6387,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.3.8"
"source": "https://github.com/twigphp/Twig/tree/v3.3.10"
},
"funding": [
{
@@ -6396,7 +6399,7 @@
"type": "tidelift"
}
],
"time": "2022-02-04T06:59:48+00:00"
"time": "2022-04-06T06:47:41+00:00"
},
{
"name": "vimeo/psalm",
@@ -6580,5 +6583,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.3.0"
}
+6
View File
@@ -370,6 +370,7 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
@@ -519,6 +520,11 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Appwrite\Exception;
class Certificate extends \Exception
{
private $incrementAttempts = false;
// Only set incrementAttempts to TRUE if exception occured AFTER certbot command execution
public function __construct(string $message, bool $incrementAttempts = false)
{
$this->incrementAttempts = $incrementAttempts;
parent::__construct($message);
}
/**
* Get value if attempts should be incremented.
*
* @return boolean
*/
public function getIncrementAttempts(): bool
{
return $this->incrementAttempts;
}
/**
* Set if attempts should be incremented
*
* @param boolean $value
*
* @return void
*/
public function setIncrementAttempts(bool $value): void
{
$this->incrementAttempts = $value;
}
}