2020-02-23 21:58:11 +13:00
|
|
|
<?php
|
|
|
|
|
2020-02-24 06:45:51 +13:00
|
|
|
use Utopia\App;
|
2020-02-23 21:58:11 +13:00
|
|
|
use Utopia\Domains\Domain;
|
2020-03-25 06:56:32 +13:00
|
|
|
use Appwrite\Database\Database;
|
|
|
|
use Appwrite\Database\Validator\Authorization;
|
2020-06-12 07:36:10 +12:00
|
|
|
use Appwrite\Network\Validator\CNAME;
|
2020-02-23 21:58:11 +13:00
|
|
|
|
|
|
|
require_once __DIR__.'/../init.php';
|
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
\cli_set_process_title('Certificates V1 Worker');
|
2020-02-23 21:58:11 +13:00
|
|
|
|
|
|
|
echo APP_NAME.' certificates worker v1 has started';
|
|
|
|
|
|
|
|
class CertificatesV1
|
|
|
|
{
|
|
|
|
public $args = [];
|
|
|
|
|
|
|
|
public function setUp()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
public function perform()
|
|
|
|
{
|
2020-06-29 08:45:36 +12:00
|
|
|
global $consoleDB;
|
2020-02-23 21:58:11 +13:00
|
|
|
|
|
|
|
/**
|
2020-02-24 06:05:42 +13:00
|
|
|
* 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
|
2020-05-28 19:00:19 +12:00
|
|
|
* 3. Check if certificate is about to expire, if not - skip it
|
2020-02-23 21:58:11 +13:00
|
|
|
* 3.1. Create / renew certificate
|
|
|
|
* 3.2. Update loadblancer
|
|
|
|
* 3.3. Update database (domains, change date, expiry)
|
|
|
|
* 3.4. Set retry on failure
|
2020-05-28 19:00:19 +12:00
|
|
|
* 3.5. Schedule to renew certificate in 60 days
|
2020-02-23 21:58:11 +13:00
|
|
|
*/
|
|
|
|
|
|
|
|
Authorization::disable();
|
|
|
|
|
2020-03-01 19:33:19 +13:00
|
|
|
// Args
|
|
|
|
$document = $this->args['document'];
|
|
|
|
$domain = $this->args['domain'];
|
|
|
|
|
|
|
|
// Validation Args
|
|
|
|
$validateTarget = (isset($this->args['validateTarget'])) ? $this->args['validateTarget'] : true;
|
|
|
|
$validateCNAME = (isset($this->args['validateCNAME'])) ? $this->args['validateCNAME'] : true;
|
|
|
|
|
|
|
|
// Options
|
|
|
|
$domain = new Domain((!empty($domain)) ? $domain : '');
|
|
|
|
$expiry = 60 * 60 * 24 * 30 * 2; // 60 days
|
|
|
|
$safety = 60 * 60; // 1 hour
|
2020-06-20 23:20:49 +12:00
|
|
|
$renew = (\time() + $expiry);
|
2020-02-23 21:58:11 +13:00
|
|
|
|
|
|
|
if(empty($domain->get())) {
|
|
|
|
throw new Exception('Missing domain');
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!$domain->isKnown() || $domain->isTest()) {
|
|
|
|
throw new Exception('Unkown public suffix for domain');
|
|
|
|
}
|
|
|
|
|
2020-03-01 19:33:19 +13:00
|
|
|
if($validateTarget) {
|
2020-06-29 08:45:36 +12:00
|
|
|
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
|
2020-03-01 19:33:19 +13:00
|
|
|
|
|
|
|
if(!$target->isKnown() || $target->isTest()) {
|
|
|
|
throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.');
|
|
|
|
}
|
2020-02-23 21:58:11 +13:00
|
|
|
}
|
|
|
|
|
2020-03-01 19:33:19 +13:00
|
|
|
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');
|
|
|
|
}
|
2020-02-23 21:58:11 +13:00
|
|
|
}
|
|
|
|
|
2020-06-27 02:41:13 +12:00
|
|
|
$certificate = $consoleDB->getCollectionFirst([
|
2020-02-23 21:58:11 +13:00
|
|
|
'limit' => 1,
|
|
|
|
'offset' => 0,
|
|
|
|
'orderField' => 'id',
|
|
|
|
'orderType' => 'ASC',
|
|
|
|
'orderCast' => 'string',
|
|
|
|
'filters' => [
|
|
|
|
'$collection='.Database::SYSTEM_COLLECTION_CERTIFICATES,
|
|
|
|
'domain='.$domain->get(),
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
|
2020-03-02 01:04:29 +13:00
|
|
|
// $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);
|
2020-03-02 00:20:39 +13:00
|
|
|
|
2020-02-23 21:58:11 +13:00
|
|
|
$certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : [];
|
|
|
|
|
2020-03-02 01:10:38 +13:00
|
|
|
if(!empty($certificate)
|
2020-02-23 21:58:11 +13:00
|
|
|
&& isset($certificate['issueDate'])
|
2020-06-20 23:20:49 +12:00
|
|
|
&& (($certificate['issueDate'] + ($expiry)) > \time())) { // Check last issue time
|
2020-03-02 01:10:38 +13:00
|
|
|
throw new Exception('Renew isn\'t required');
|
2020-02-23 21:58:11 +13:00
|
|
|
}
|
|
|
|
|
2020-06-30 09:42:21 +12:00
|
|
|
$staging = (App::isProduction()) ? '' : ' --dry-run';
|
2020-02-24 06:45:51 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
$response = \shell_exec("certbot certonly --webroot --noninteractive --agree-tos{$staging} \
|
2020-06-29 08:45:36 +12:00
|
|
|
--email ".App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', 'security@localhost.test')." \
|
2020-02-23 21:58:11 +13:00
|
|
|
-w ".APP_STORAGE_CERTIFICATES." \
|
2020-03-02 01:10:38 +13:00
|
|
|
-d {$domain->get()}");
|
2020-02-23 21:58:11 +13:00
|
|
|
|
|
|
|
if(!$response) {
|
|
|
|
throw new Exception('Failed to issue a certificate');
|
|
|
|
}
|
2020-02-25 22:21:56 +13:00
|
|
|
|
|
|
|
$path = APP_STORAGE_CERTIFICATES.'/'.$domain->get();
|
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
if(!\is_readable($path)) {
|
|
|
|
if (!\mkdir($path, 0755, true)) {
|
2020-02-25 22:21:56 +13:00
|
|
|
throw new Exception('Failed to create path...');
|
|
|
|
}
|
|
|
|
}
|
2020-02-24 06:05:42 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
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($response));
|
2020-02-24 06:05:42 +13:00
|
|
|
}
|
2020-02-23 21:58:11 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
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($response));
|
2020-02-25 06:02:23 +13:00
|
|
|
}
|
2020-02-25 05:47:45 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
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($response));
|
2020-02-25 06:02:23 +13:00
|
|
|
}
|
2020-02-25 05:47:45 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
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($response));
|
2020-02-25 06:02:23 +13:00
|
|
|
}
|
2020-02-25 05:47:45 +13:00
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
$certificate = \array_merge($certificate, [
|
2020-02-23 21:58:11 +13:00
|
|
|
'$collection' => Database::SYSTEM_COLLECTION_CERTIFICATES,
|
|
|
|
'$permissions' => [
|
|
|
|
'read' => [],
|
|
|
|
'write' => [],
|
|
|
|
],
|
|
|
|
'domain' => $domain->get(),
|
2020-06-20 23:20:49 +12:00
|
|
|
'issueDate' => \time(),
|
2020-02-29 19:24:46 +13:00
|
|
|
'renewDate' => $renew,
|
2020-02-23 21:58:11 +13:00
|
|
|
'attempts' => 0,
|
2020-06-20 23:20:49 +12:00
|
|
|
'log' => \json_encode($response),
|
2020-02-23 21:58:11 +13:00
|
|
|
]);
|
|
|
|
|
|
|
|
$certificate = $consoleDB->createDocument($certificate);
|
|
|
|
|
|
|
|
if(!$certificate) {
|
|
|
|
throw new Exception('Failed saving certificate to DB');
|
|
|
|
}
|
|
|
|
|
2020-02-29 19:24:46 +13:00
|
|
|
if(!empty($document)) {
|
2020-06-20 23:20:49 +12:00
|
|
|
$document = \array_merge($document, [
|
|
|
|
'updated' => \time(),
|
2020-02-29 19:24:46 +13:00
|
|
|
'certificateId' => $certificate->getId(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$document = $consoleDB->updateDocument($document);
|
|
|
|
|
|
|
|
if(!$document) {
|
|
|
|
throw new Exception('Failed saving domain to DB');
|
|
|
|
}
|
2020-02-23 21:58:11 +13:00
|
|
|
}
|
2020-02-25 23:04:12 +13:00
|
|
|
|
|
|
|
$config =
|
|
|
|
"tls:
|
|
|
|
certificates:
|
|
|
|
- certFile: /storage/certificates/{$domain->get()}/fullchain.pem
|
|
|
|
keyFile: /storage/certificates/{$domain->get()}/privkey.pem";
|
|
|
|
|
2020-06-20 23:20:49 +12:00
|
|
|
if(!\file_put_contents(APP_STORAGE_CONFIG.'/'.$domain->get().'.yml', $config)) {
|
2020-02-25 23:04:12 +13:00
|
|
|
throw new Exception('Failed to save SSL configuration');
|
|
|
|
}
|
2020-02-23 21:58:11 +13:00
|
|
|
|
2020-02-29 19:24:46 +13:00
|
|
|
ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [
|
|
|
|
'document' => [],
|
2020-03-02 00:14:55 +13:00
|
|
|
'domain' => $domain->get(),
|
|
|
|
'validateTarget' => $validateTarget,
|
|
|
|
'validateCNAME' => $validateCNAME,
|
2020-02-29 19:24:46 +13:00
|
|
|
]); // Async task rescheduale
|
|
|
|
|
2020-02-23 21:58:11 +13:00
|
|
|
Authorization::reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function tearDown()
|
|
|
|
{
|
|
|
|
// ... Remove environment for this job
|
|
|
|
}
|
|
|
|
}
|