Remove unused Greenlock configuration files and import AcmeCertificate in Model and Service files

This commit is contained in:
Simon Larsen 2024-04-30 13:22:06 +01:00
parent 0f2a970ede
commit 9d180a2dcb
No known key found for this signature in database
GPG Key ID: AB45983AA9C81CDE
12 changed files with 438 additions and 628 deletions

View File

@ -0,0 +1,11 @@
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
import Model from 'Model/Models/AcmeCertificate';
import DatabaseService from './DatabaseService';
export class Service extends DatabaseService<Model> {
public constructor(postgresDatabase?: PostgresDatabase) {
super(Model, postgresDatabase);
}
}
export default new Service();

View File

@ -139,7 +139,10 @@ import UsageBillingService from './TelemetryUsageBillingService';
import ProjectCallSMSConfigService from './ProjectCallSMSConfigService';
import MonitorMetricsByMinuteService from './MonitorMetricsByMinuteService';
import AcmeCertificateService from './AcmeCertificateService';
const services: Array<BaseService> = [
AcmeCertificateService,
PromoCodeService,
ResellerService,

View File

@ -90,14 +90,14 @@ export class Service extends DatabaseService<StatusPageDomain> {
}
public async orderCert(statusPageDomain: StatusPageDomain): Promise<void> {
if (!statusPageDomain.greenlockConfig) {
if (!statusPageDomain.fullDomain) {
const fetchedStatusPageDomain = await this.findOneBy({
query: {
_id: statusPageDomain.id!.toString(),
},
select: {
_id: true,
greenlockConfig: true,
fullDomain: true,
},
props: {
isRoot: true,
@ -108,16 +108,11 @@ export class Service extends DatabaseService<StatusPageDomain> {
throw new BadDataException('Domain not found');
}
if (!fetchedStatusPageDomain.greenlockConfig) {
throw new BadDataException('Greenlock config not found');
}
statusPageDomain.greenlockConfig =
fetchedStatusPageDomain.greenlockConfig;
}
await GreenlockUtil.orderCert(
statusPageDomain.greenlockConfig as JSONObject
statusPageDomain.fullDomain as string
);
// update the order.
@ -320,7 +315,7 @@ export class Service extends DatabaseService<StatusPageDomain> {
},
});
await GreenlockUtil.addDomain(statusPageDomain.fullDomain!);
await GreenlockUtil.orderCert(statusPageDomain.fullDomain!);
await this.updateOneById({
id: statusPageDomain.id!,
@ -476,7 +471,7 @@ export class Service extends DatabaseService<StatusPageDomain> {
}
public async renewCertsWhichAreExpiringSoon(): Promise<void> {
await GreenlockUtil.renewAllCerts();
await GreenlockUtil.renewAllCertsWhichAreExpiringSoon();
}
}
export default new Service();

View File

@ -1,109 +1,144 @@
import BadDataException from 'Common/Types/Exception/BadDataException';
import { JSONObject } from 'Common/Types/JSON';
import acme from 'acme-client';
import { LetsEncryptNotificationEmail } from '../../EnvironmentConfig';
import StatusPageDomainService from '../../Services/StatusPageDomainService';
import GreenlockChallenge from 'Model/Models/GreenlockChallenge';
import GreenlockChallengeService from '../../Services/GreenlockChallengeService';
import AcmeCertificate from 'Model/Models/AcmeCertificate';
import AcmeCertificateService from '../../Services/AcmeCertificateService';
import logger from '../Logger';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
// @ts-ignore
import Greenlock from 'greenlock';
export default class GreenlockUtil {
private static greenLockInstance: any = null;
private static getGreenlockInstance(): any {
if (this.greenLockInstance) {
return this.greenLockInstance;
}
public static async renewAllCertsWhichAreExpiringSoon(): Promise<void> {
this.greenLockInstance = Greenlock.create({
configFile: '//usr/src/CommonServer/Utils/Greenlock/greenlockrc',
packageRoot: `/usr/src/CommonServer/greenlock`,
manager: '/usr/src/CommonServer/Utils/Greenlock/Manager.ts',
directoryUrl: 'https://acme-v02.api.letsencrypt.org/directory',
renewOffset: '45d',
renewStagger: '3d',
approveDomains: async (opts: any) => {
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts.domain,
},
select: {
_id: true,
fullDomain: true,
},
props: {
isRoot: true,
},
});
if (!domain) {
throw new BadDataException(
`Domain ${opts.domain} does not exist in StatusPageDomain`
);
}
return opts; // or Promise.resolve(opts);
},
store: {
module: '/usr/src/CommonServer/Utils/Greenlock/Store.ts',
},
// Staging for testing environments
// staging: IsDevelopment,
// This should be the contact who receives critical bug and security notifications
// Optionally, you may receive other (very few) updates, such as important new features
maintainerEmail: LetsEncryptNotificationEmail.toString(),
// for an RFC 8555 / RFC 7231 ACME client user agent
packageAgent: 'oneuptime/1.0.0',
notify: function (event: string, details: any) {
if ('error' === event) {
logger.error('Greenlock Notify: ' + event);
logger.error(details);
}
logger.info('Greenlock Notify: ' + event);
logger.info(details);
},
agreeToTerms: true,
challenges: {
'http-01': {
module: '/usr/src/CommonServer/Utils/Greenlock/HttpChallenge.ts',
},
},
});
return this.greenLockInstance;
logger.info('Renewing all certificates');
// TODO: Implement renewAllCerts
}
public static async removeDomain(domain: string): Promise<void> {
await this.getGreenlockInstance().remove({
subject: domain,
// remove certificate for this domain.
await AcmeCertificateService.deleteBy({
query: {
key: domain
},
limit: 1,
skip: 0,
props: {
isRoot: true
}
});
}
public static async addDomain(domain: string): Promise<void> {
await this.getGreenlockInstance().add({
subject: domain,
altnames: [domain],
});
}
public static async orderCert(domain: string): Promise<void> {
public static async getCert(domain: string): Promise<JSONObject> {
const site: JSONObject = await this.getGreenlockInstance().get({
servername: domain,
domain = domain.trim().toLowerCase();
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey: await acme.crypto.createPrivateKey()
});
return site;
}
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: domain
});
public static async renewAllCerts(): Promise<void> {
await this.getGreenlockInstance().renew();
}
const certificate: string = await client.auto({
csr: certificateRequest,
email: LetsEncryptNotificationEmail.toString(),
termsOfServiceAgreed: true,
challengePriority: ['http-01'], // only http-01 challenge is supported by oneuptime
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
// Satisfy challenge here
/* http-01 */
if (challenge.type === 'http-01') {
public static async orderCert(greenlockConfig: JSONObject): Promise<void> {
await this.getGreenlockInstance().order(greenlockConfig);
const greenlockChallenge = new GreenlockChallenge();
greenlockChallenge.challenge = keyAuthorization;
greenlockChallenge.token = challenge.token;
greenlockChallenge.key = authz.identifier.value;
await GreenlockChallengeService.create({
data: greenlockChallenge,
props: {
isRoot: true
}
});
}
},
challengeRemoveFn: async (authz, challenge) => {
// Clean up challenge here
if (challenge.type === 'http-01') {
await GreenlockChallengeService.deleteBy({
query: {
key: authz.identifier.value
},
limit: 1,
skip: 0,
props: {
isRoot: true
}
});
}
}
});
// get expires at date from certificate
const cert = await acme.forge.readCertificateInfo(certificate);
const issuedAt = cert.notBefore;
const expiresAt = cert.notAfter;
// check if the certificate is already in the database.
const existingCertificate: AcmeCertificate | null = await AcmeCertificateService.findOneBy({
query: {
key: domain
},
select:{
_id: true
},
props: {
isRoot: true
}
});
const blob: string = JSON.stringify({
certificate: certificate.toString(),
certificateKey: certificateKey.toString()
});
if(existingCertificate){
// update the certificate
await AcmeCertificateService.updateBy({
query: {
key: domain
},
limit: 1,
skip: 0,
data: {
blob: blob
},
props: {
isRoot: true
}
});
} else {
// create the certificate
const AcmeCertificate = new AcmeCertificate();
AcmeCertificate.key = domain;
AcmeCertificate.blob = blob;
await AcmeCertificateService.create({
data: AcmeCertificate,
props: {
isRoot: true
}
});
}
}
}

View File

@ -1,112 +0,0 @@
import GreenlockChallenge from 'Model/Models/GreenlockChallenge';
import GreenlockChallengeService from '../../Services/GreenlockChallengeService';
import logger from '../../Utils/Logger';
// because greenlock package expects module.exports.
module.exports = {
create: (_opts: any) => {
return {
init: async (): Promise<null> => {
logger.info('Greenlock HTTP Challenge Init');
return Promise.resolve(null);
},
set: async (data: any): Promise<null> => {
logger.info('Greenlock HTTP Challenge Set');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
const token: string = ch.token;
let challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
challenge = new GreenlockChallenge();
challenge.key = key;
challenge.token = token;
challenge.challenge = ch.keyAuthorization;
await GreenlockChallengeService.create({
data: challenge,
props: {
isRoot: true,
},
});
} else {
challenge.challenge = ch.keyAuthorization;
challenge.token = token;
await GreenlockChallengeService.updateOneById({
id: challenge.id!,
data: challenge,
props: {
isRoot: true,
},
});
}
//
return null;
},
get: async (data: any): Promise<null | any> => {
logger.info('Greenlock HTTP Challenge Get');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
return null;
}
return { keyAuthorization: challenge.challenge };
},
remove: async (data: any): Promise<null> => {
logger.info('Greenlock HTTP Challenge Remove');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
await GreenlockChallengeService.deleteOneBy({
query: {
key: key,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
return null;
},
};
},
};

View File

@ -1,190 +0,0 @@
// Docs: https://git.rootprojects.org/root/greenlock-manager.js
import StatusPageDomainService from '../../Services/StatusPageDomainService';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
import logger from '../Logger';
import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from '../../Services/GreenlockCertificateService';
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
import { JSONObject } from 'Common/Types/JSON';
import JSONFunctions from 'Common/Types/JSONFunctions';
// because greenlock package expects module.exports.
module.exports = {
create: () => {
return {
// Get
get: async ({
servername,
}: {
servername: string;
}): Promise<JSONObject | undefined> => {
// Required: find the certificate with the subject of `servername`
// Optional (multi-domain certs support): find a certificate with `servername` as an altname
// Optional (wildcard support): find a certificate with `wildname` as an altname
// { subject, altnames, renewAt, deletedAt, challenges, ... }
logger.info('Greenlock Manager Get');
logger.info(servername);
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: servername,
},
props: {
isRoot: true,
ignoreHooks: true,
},
select: {
_id: true,
greenlockConfig: true,
},
});
if (!domain || !domain.greenlockConfig) {
logger.info(
'Greenlock Manager GET ' +
servername +
' - No domain found.'
);
return undefined;
}
logger.info('Greenlock Manager GET ' + servername + ' RESULT');
logger.info(domain.greenlockConfig);
return domain.greenlockConfig;
},
find: async function (args: any) {
logger.info('Manager Find: ');
logger.info(JSON.stringify(args, null, 2));
// { subject, servernames, altnames, renewBefore }
// i.e. find certs more than 30 days old
args.issuedBefore = Date.now() - 30 * 24 * 60 * 60 * 1000;
// i.e. find certs more that will expire in less than 45 days
args.expiresBefore = Date.now() + 45 * 24 * 60 * 60 * 1000;
const issuedBefore: any = args.issuedBefore || Infinity;
const expiresBefore: any = args.expiresBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
const renewBefore: any = args.renewBefore || Infinity; //Date.now() + 21 * 24 * 60 * 60 * 1000;
// if there's anything to match, only return matches
// if there's nothing to match, return everything
const nameKeys: Array<string> = ['subject', 'altnames'];
const matchAll: boolean = !nameKeys.some((k: string) => {
return k in args;
});
const querynames: string = (args.altnames || []).slice(0);
const greenlockCertificates: Array<GreenlockCertificate> =
await GreenlockCertificateService.findBy({
query: {
isKeyPair: false,
},
limit: LIMIT_MAX,
skip: 0,
select: {
blob: true,
key: true,
},
props: {
isRoot: true,
},
});
const sites: Array<JSONObject> = greenlockCertificates
.filter((i: GreenlockCertificate) => {
return i.blob;
})
.map((i: GreenlockCertificate) => {
return JSONFunctions.parseJSONObject(i.blob!);
})
.filter((site: any) => {
logger.info('Filter Site: ');
logger.info(site);
if (site.deletedAt) {
logger.info('Filter Site: DeletedAt');
return false;
}
if (site.expiresAt >= expiresBefore) {
logger.info('Filter Site: expiresAt');
return false;
}
if (site.issuedAt >= issuedBefore) {
logger.info('Filter Site: issuedAt');
return false;
}
if (site.renewAt >= renewBefore) {
logger.info('Filter Site: renewAt');
return false;
}
// after attribute filtering, before cert filtering
if (matchAll) {
logger.info('Filter Site: MatchAll');
return true;
}
// if subject is specified, don't return anything else
if (site.subject === args.subject) {
logger.info('Filter Site: Subject');
return true;
}
// altnames, servername, and wildname all get rolled into one
return site.altnames.some((altname: any) => {
return querynames.includes(altname);
});
});
logger.info('Sites: ');
logger.info(sites);
return sites;
},
// Set
set: async (opts: any) => {
logger.info('Greenlock Manager Set');
logger.info(opts);
// { subject, altnames, renewAt, deletedAt }
// Required: updated `renewAt` and `deletedAt` for certificate matching `subject`
if (!opts.subject) {
return;
}
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts['subject'] as string,
},
props: {
isRoot: true,
},
select: {
_id: true,
greenlockConfig: true,
},
});
if (!domain) {
return;
}
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
greenlockConfig: opts,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
},
};
},
};

View File

@ -1,223 +0,0 @@
/// https://git.rootprojects.org/root/greenlock-store-memory.js/src/branch/master/index.js
import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from '../../Services/GreenlockCertificateService';
import logger from '../../Utils/Logger';
import JSONFunctions from 'Common/Types/JSONFunctions';
type SaveCertificateFunction = (
id: string,
blob: string,
isKeyPair: boolean
) => Promise<null>;
type GetCertificateFunction = (
id: string,
isKeyPair: boolean
) => Promise<null | string>;
module.exports = {
create: (_opts: any) => {
const saveCertificate: SaveCertificateFunction = async (
id: string,
blob: string,
isKeyPair: boolean
): Promise<null> => {
logger.info('Save Certificates: ' + id);
let cert: GreenlockCertificate | null =
await GreenlockCertificateService.findOneBy({
query: {
key: id,
isKeyPair: isKeyPair,
},
select: {
_id: true,
isKeyPair: isKeyPair,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!cert) {
cert = new GreenlockCertificate();
cert.key = id;
cert.blob = blob;
cert.isKeyPair = isKeyPair;
await GreenlockCertificateService.create({
data: cert,
props: {
isRoot: true,
},
});
} else {
cert.blob = blob;
cert.isKeyPair = isKeyPair;
await GreenlockCertificateService.updateOneById({
id: cert.id!,
data: {
blob: blob,
isKeyPair: isKeyPair,
},
props: {
isRoot: true,
},
});
}
//
return null;
};
const getCertificate: GetCertificateFunction = async (
id: string,
isKeyPair: boolean
): Promise<null | string> => {
logger.info('Get Certificate - ' + id);
const cert: GreenlockCertificate | null =
await GreenlockCertificateService.findOneBy({
query: {
key: id,
isKeyPair: isKeyPair,
},
select: {
_id: true,
blob: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!cert || !cert.blob) {
logger.info('Certificate not found');
return null;
}
logger.info('Certificate found');
return cert.blob;
};
type SaveKeyPairFunction = (id: string, blob: string) => Promise<null>;
const saveKeypair: SaveKeyPairFunction = async (
id: string,
blob: string
): Promise<null> => {
logger.info('Save Keypair: ' + id);
return await saveCertificate(id, blob, true);
};
type GetKeypairFunction = (id: string) => Promise<null | string>;
const getKeypair: GetKeypairFunction = async (
id: string
): Promise<null | string> => {
logger.info('Get Keypair: ' + id);
return await getCertificate(id, true);
};
return {
accounts: {
// Whenever a new keypair is used to successfully create an account, we need to save its keypair
setKeypair: async (opts: any): Promise<null> => {
logger.info('Accounts Set Keypair: ');
logger.info(JSON.stringify(opts, null, 2));
const id: string =
opts.account.id || opts.email || 'default';
const keypair: any = opts.keypair;
return await saveKeypair(id, JSON.stringify(keypair)); // Must return or Promise `null` instead of `undefined`
},
// We need a way to retrieve a prior account's keypair for renewals and additional ACME certificate "orders"
checkKeypair: async (opts: any): Promise<any | null> => {
logger.info('Accounts Check Keypair: ');
logger.info(JSON.stringify(opts, null, 2));
const id: string =
opts.account.id || opts.email || 'default';
const keyblob: any = await getKeypair(id);
if (!keyblob) {
return null;
}
return JSONFunctions.parse(keyblob);
},
},
certificates: {
setKeypair: async (opts: any): Promise<null> => {
logger.info('Certificates Set Keypair: ');
logger.info(JSON.stringify(opts, null, 2));
// The ID is a string that doesn't clash between accounts and certificates.
// That's all you need to know... unless you're doing something special (in which case you're on your own).
const id: string =
opts.certificate.kid ||
opts.certificate.id ||
opts.subject;
const keypair: any = opts.keypair;
return await saveKeypair(id, JSON.stringify(keypair)); // Must return or Promise `null` instead of `undefined`
// Side Note: you can use the "keypairs" package to convert between
// public and private for jwk and pem, as well as convert JWK <-> PEM
},
// You won't be able to use a certificate without it's private key, gotta save it
checkKeypair: async (opts: any): Promise<any | null> => {
logger.info('Certificates Check Keypair: ');
logger.info(JSON.stringify(opts, null, 2));
const id: string =
opts.certificate.kid ||
opts.certificate.id ||
opts.subject;
const keyblob: any = await getKeypair(id);
if (!keyblob) {
return null;
}
return JSONFunctions.parse(keyblob);
},
// And you'll also need to save certificates. You may find the metadata useful to save
// (perhaps to delete expired keys), but the same information can also be redireved from
// the key using the "cert-info" package.
set: async (opts: any): Promise<null> => {
logger.info('Certificates Set: ');
logger.info(JSON.stringify(opts, null, 2));
const id: string = opts.certificate.id || opts.subject;
const pems: any = opts.pems;
return await saveCertificate(
id,
JSON.stringify(pems),
false
); // Must return or Promise `null` instead of `undefined`
},
// This is actually the first thing to be called after approveDomins(),
// but it's easiest to implement last since it's not useful until there
// are certs that can actually be loaded from storage.
check: async (opts: any): Promise<null | any> => {
logger.info('Certificates Check: ');
logger.info(JSON.stringify(opts, null, 2));
const id: string = opts.certificate?.id || opts.subject;
const certblob: any = await getCertificate(id, false);
if (!certblob) {
return null;
}
return JSONFunctions.parse(certblob);
},
},
options: {},
};
},
};

View File

@ -1 +0,0 @@
{"manager":"/usr/src/CommonServer/Utils/Greenlock/Manager.ts","configDir":"./greenlock.d"}

View File

@ -0,0 +1,145 @@
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import BaseModel from 'Common/Models/BaseModel';
import TableColumnType from 'Common/Types/Database/TableColumnType';
import TableColumn from 'Common/Types/Database/TableColumn';
import ColumnType from 'Common/Types/Database/ColumnType';
import ColumnLength from 'Common/Types/Database/ColumnLength';
import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import TableMetadata from 'Common/Types/Database/TableMetadata';
import IconProp from 'Common/Types/Icon/IconProp';
import User from './User';
import ObjectID from 'Common/Types/ObjectID';
@TableAccessControl({
create: [],
read: [],
delete: [],
update: [],
})
@TableMetadata({
tableName: 'AcmeCertificate',
singularName: 'Acme Certificate',
pluralName: 'Acme Certificate',
icon: IconProp.Lock,
tableDescription: 'Lets Encrypt Certificates',
})
@Entity({
name: 'AcmeCertificate',
})
export default class AcmeCertificate extends BaseModel {
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.LongText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public domain?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.VeryLongText })
@Column({
type: ColumnType.VeryLongText,
nullable: false,
unique: false,
})
public certificate?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.VeryLongText })
@Column({
type: ColumnType.VeryLongText,
nullable: false,
unique: false,
})
public certificateKey?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.Date })
@Column({
type: ColumnType.Date,
nullable: false,
default: false,
unique: false,
})
public issuedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.Date })
@Column({
type: ColumnType.Date,
nullable: false,
default: false,
unique: false,
})
public expiresAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: 'deletedByUserId',
type: TableColumnType.Entity,
title: 'Deleted by User',
description:
'Relation to User who deleted this object (if this object was deleted by a User)',
})
@ManyToOne(
(_type: string) => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: 'CASCADE',
orphanedRowAction: 'nullify',
}
)
@JoinColumn({ name: 'deletedByUserId' })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: 'Deleted by User ID',
description:
'User ID who deleted this object (if this object was deleted by a User)',
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@ -142,6 +142,7 @@ import OnCallDutyPolicyEscalationRuleSchedule from './OnCallDutyPolicyEscalation
import UsageBilling from './TelemetryUsageBilling';
import ProjectCallSMSConfig from './ProjectCallSMSConfig';
import AcmeCertificate from './AcmeCertificate';
export default [
User,
@ -272,4 +273,6 @@ export default [
UsageBilling,
ProjectCallSMSConfig,
AcmeCertificate
];

View File

@ -0,0 +1,145 @@
import {
EVERY_FIVE_MINUTE,
EVERY_FIFTEEN_MINUTE,
EVERY_MINUTE,
} from 'Common/Utils/CronTime';
import BasicCron from 'CommonServer/Utils/BasicCron';
import { IsDevelopment } from 'CommonServer/EnvironmentConfig';
// @ts-ignore
import logger from 'CommonServer/Utils/Logger';
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService';
import LocalFile from 'CommonServer/Utils/LocalFile';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService';
import SelfSignedSSL from '../Utils/SelfSignedSSL';
export default class Jobs {
public static init(): void {
BasicCron({
jobName: 'StatusPageCerts:WriteAcmeCertsToDisk',
options: {
schedule: IsDevelopment ? EVERY_MINUTE : EVERY_FIFTEEN_MINUTE,
runOnStartup: true,
},
runFunction: async () => {
// Fetch all domains where certs are added to greenlock.
const certs: Array<GreenlockCertificate> =
await GreenlockCertificateService.findBy({
query: {},
select: {
key: true,
blob: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const cert of certs) {
try {
await LocalFile.makeDirectory(
'/etc/nginx/certs/StatusPageCerts'
);
} catch (err) {
// directory already exists, ignore.
logger.error('Create directory err');
logger.error(err);
}
const certBlob = JSON.parse(cert.blob!);
// Write to disk.
await LocalFile.write(
`/etc/nginx/certs/StatusPageCerts/${cert.key}.crt`,
certBlob['certificate']
);
await LocalFile.write(
`/etc/nginx/certs/StatusPageCerts/${cert.key}.key`,
certBlob['certificateKey']
);
}
},
});
BasicCron({
jobName: 'StatusPageCerts:WriteSelfSignedCertsToDisk',
options: {
schedule: IsDevelopment ? EVERY_MINUTE : EVERY_FIVE_MINUTE,
runOnStartup: true,
},
runFunction: async () => {
// Fetch all domains where certs are added to greenlock.
const certs: Array<GreenlockCertificate> =
await GreenlockCertificateService.findBy({
query: {},
select: {
key: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
const statusPageDomains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isSelfSignedSslGenerated: false,
},
select: {
fullDomain: true,
_id: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
ignoreHooks: true,
},
});
const greenlockCertDomains: Array<string | undefined> =
certs.map((cert: GreenlockCertificate) => {
return cert.key;
});
// Generate self signed certs
for (const domain of statusPageDomains) {
if (greenlockCertDomains.includes(domain.fullDomain)) {
continue;
}
if (!domain.fullDomain) {
continue;
}
await SelfSignedSSL.generate(
'/etc/nginx/certs/StatusPageCerts',
domain.fullDomain
);
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isSelfSignedSslGenerated: true,
},
props: {
ignoreHooks: true,
isRoot: true,
},
});
}
},
});
}
}