mirror of
https://github.com/OneUptime/oneuptime
synced 2024-11-21 14:49:07 +00:00
Remove unused Greenlock configuration files and import AcmeCertificate in Model and Service files
This commit is contained in:
parent
0f2a970ede
commit
9d180a2dcb
@ -1 +0,0 @@
|
||||
{}
|
11
CommonServer/Services/AcmeCertificateService.ts
Normal file
11
CommonServer/Services/AcmeCertificateService.ts
Normal 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();
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
@ -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: {},
|
||||
};
|
||||
},
|
||||
};
|
@ -1 +0,0 @@
|
||||
{"manager":"/usr/src/CommonServer/Utils/Greenlock/Manager.ts","configDir":"./greenlock.d"}
|
145
Model/Models/AcmeCertificate.ts
Normal file
145
Model/Models/AcmeCertificate.ts
Normal 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;
|
||||
}
|
@ -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
|
||||
];
|
||||
|
145
Nginx/Jobs/AcmeWriteCertificates.ts
Normal file
145
Nginx/Jobs/AcmeWriteCertificates.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user