oneuptime/Workers/Jobs/StatusPageCerts/StausPageCerts.ts

565 lines
16 KiB
TypeScript
Raw Normal View History

2023-01-02 11:34:11 +00:00
import {
EVERY_FIVE_MINUTE,
EVERY_HOUR,
EVERY_MINUTE,
} from '../../Utils/CronTime';
2022-11-27 20:10:55 +00:00
import RunCron from '../../Utils/Cron';
import { IsDevelopment } from 'CommonServer/Config';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService';
2022-11-28 18:26:07 +00:00
// @ts-ignore
2022-11-27 20:10:55 +00:00
import Greenlock from 'greenlock';
2022-11-28 18:26:07 +00:00
import logger from 'CommonServer/Utils/Logger';
import BadDataException from 'Common/Types/Exception/BadDataException';
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from 'CommonServer/Utils/Express';
import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization';
import { JSONObject } from 'Common/Types/JSON';
import Response from 'CommonServer/Utils/Response';
2022-12-05 10:40:28 +00:00
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
2022-12-11 09:10:17 +00:00
import axios, { AxiosResponse } from 'axios';
2022-12-11 08:02:56 +00:00
import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService';
import fs from 'fs';
2023-01-02 11:31:50 +00:00
import SelfSignedSSL from '../../Utils/SelfSignedSSL';
2022-11-27 20:10:55 +00:00
2022-11-28 18:26:07 +00:00
const router: ExpressRouter = Express.getRouter();
2022-12-07 06:44:17 +00:00
const greenlock: any = Greenlock.create({
2022-12-05 10:40:28 +00:00
configFile: '/greenlockrc',
packageRoot: `/usr/src/app`,
2022-12-07 06:44:17 +00:00
manager: '/usr/src/app/Utils/Greenlock/Manager.ts',
2022-11-28 18:26:07 +00:00
approveDomains: async (opts: any) => {
2022-12-07 06:44:17 +00:00
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
query: {
fullDomain: opts.domain,
},
select: {
_id: true,
fullDomain: true,
},
props: {
isRoot: true,
},
});
2022-11-28 18:26:07 +00:00
if (!domain) {
2022-12-07 06:44:17 +00:00
throw new BadDataException(
`Domain ${opts.domain} does not exist in StatusPageDomain`
);
2022-11-28 18:26:07 +00:00
}
return opts; // or Promise.resolve(opts);
2022-11-27 20:10:55 +00:00
},
2022-12-05 18:05:47 +00:00
store: {
2022-12-07 06:44:17 +00:00
module: '/usr/src/app/Utils/Greenlock/Store.ts',
2022-12-05 18:05:47 +00:00
},
2022-11-27 20:10:55 +00:00
// Staging for testing environments
2022-12-08 16:23:21 +00:00
// staging: IsDevelopment,
2022-11-27 20:10:55 +00:00
// 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
2022-11-28 18:26:07 +00:00
maintainerEmail: 'lets-encrypt@oneuptime.com',
2022-11-27 20:10:55 +00:00
// for an RFC 8555 / RFC 7231 ACME client user agent
2022-12-07 06:44:17 +00:00
packageAgent: 'oneuptime/1.0.0',
2022-12-08 16:23:21 +00:00
2022-11-28 18:26:07 +00:00
notify: function (event: string, details: any) {
if ('error' === event) {
2022-12-09 08:19:51 +00:00
logger.error('Greenlock Notify: ' + event);
2022-11-28 18:26:07 +00:00
logger.error(details);
}
2022-12-09 08:19:51 +00:00
logger.info('Greenlock Notify: ' + event);
2022-12-08 16:23:21 +00:00
logger.info(details);
2022-11-28 18:26:07 +00:00
},
agreeToTerms: true,
challenges: {
2022-12-05 18:05:47 +00:00
'http-01': {
2022-12-07 06:44:17 +00:00
module: '/usr/src/app/Utils/Greenlock/HttpChallenge.ts',
},
2022-11-28 18:26:07 +00:00
},
2022-11-27 20:10:55 +00:00
});
2022-11-28 18:26:07 +00:00
// Delete
router.delete(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
2022-12-07 06:44:17 +00:00
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2022-11-28 18:26:07 +00:00
try {
const body: JSONObject = req.body;
2022-12-05 10:40:28 +00:00
2022-11-28 18:26:07 +00:00
if (!body['domain']) {
2022-12-07 06:44:17 +00:00
throw new BadDataException('Domain is required');
2022-11-28 18:26:07 +00:00
}
await greenlock.remove({
2022-12-09 08:19:51 +00:00
subject: body['domain'],
2022-11-28 18:26:07 +00:00
});
2022-11-27 20:10:55 +00:00
2022-11-28 18:26:07 +00:00
return Response.sendEmptyResponse(req, res);
} catch (err) {
next(err);
2022-11-27 20:10:55 +00:00
}
2022-11-28 18:26:07 +00:00
}
);
2022-11-27 20:10:55 +00:00
2022-11-28 18:26:07 +00:00
// Create
router.post(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
2022-12-07 06:44:17 +00:00
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2022-11-28 18:26:07 +00:00
try {
const body: JSONObject = req.body;
2022-12-05 10:40:28 +00:00
2022-11-28 18:26:07 +00:00
if (!body['domain']) {
2022-12-07 06:44:17 +00:00
throw new BadDataException('Domain is required');
2022-11-28 18:26:07 +00:00
}
await greenlock.add({
2022-12-07 06:44:17 +00:00
subject: body['domain'],
2022-12-09 08:19:51 +00:00
altnames: [body['domain']],
2022-11-28 18:26:07 +00:00
});
return Response.sendEmptyResponse(req, res);
} catch (err) {
next(err);
}
2022-11-27 20:10:55 +00:00
}
2022-11-28 18:26:07 +00:00
);
// Create
router.get(
`/certs`,
ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
2022-12-07 06:44:17 +00:00
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2022-11-28 18:26:07 +00:00
try {
const body: JSONObject = req.body;
2022-12-05 10:40:28 +00:00
2022-11-28 18:26:07 +00:00
if (!body['domain']) {
2022-12-07 06:44:17 +00:00
throw new BadDataException('Domain is required');
2022-11-28 18:26:07 +00:00
}
2022-12-07 06:44:17 +00:00
const site: JSONObject = await greenlock.get({
servername: body['domain'] as string,
});
2022-11-28 18:26:07 +00:00
return Response.sendJsonObjectResponse(req, res, site);
} catch (err) {
next(err);
}
}
);
2022-12-08 16:23:21 +00:00
RunCron(
'StatusPageCerts:OrderCerts',
2023-03-02 19:23:03 +00:00
{ schedule: IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, runOnStartup: true },
2022-12-08 16:23:21 +00:00
async () => {
// Fetch all domains where certs are added to greenlock.
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
2022-12-09 08:19:51 +00:00
isSslProvisioned: false,
2022-12-08 16:23:21 +00:00
},
select: {
_id: true,
greenlockConfig: true,
2022-12-09 08:19:51 +00:00
fullDomain: true,
2022-12-08 16:23:21 +00:00
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const domain of domains) {
logger.info(
`StatusPageCerts:OrderCerts - Checking CNAME ${domain.fullDomain}`
);
await greenlock.order(domain.greenlockConfig);
}
2022-12-07 06:44:17 +00:00
}
);
2022-11-28 18:26:07 +00:00
2022-12-07 06:44:17 +00:00
RunCron(
'StatusPageCerts:AddCerts',
2023-03-02 19:23:03 +00:00
{ schedule: IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, runOnStartup: true },
2022-12-07 06:44:17 +00:00
async () => {
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: false,
2022-12-05 10:40:28 +00:00
},
2022-12-07 06:44:17 +00:00
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true,
},
limit: LIMIT_MAX,
skip: 0,
2022-12-05 10:40:28 +00:00
props: {
2022-12-07 06:44:17 +00:00
isRoot: true,
},
});
2022-12-05 10:40:28 +00:00
2022-12-07 06:44:17 +00:00
for (const domain of domains) {
logger.info(
`StatusPageCerts:AddCerts - Checking CNAME ${domain.fullDomain}`
);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid: boolean = await checkCnameValidation(
domain.fullDomain!,
domain.cnameVerificationToken!
);
if (isValid) {
logger.info(
`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is valid. Adding domain to greenlock.`
);
2022-12-08 16:23:21 +00:00
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isCnameVerified: true,
},
props: {
isRoot: true,
},
});
2022-12-07 06:44:17 +00:00
await greenlock.add({
subject: domain.fullDomain,
2022-12-09 08:19:51 +00:00
altnames: [domain.fullDomain],
2022-12-07 06:44:17 +00:00
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: true,
},
props: {
isRoot: true,
},
});
logger.info(
`StatusPageCerts:AddCerts - ${domain.fullDomain} added to greenlock.`
);
} else {
logger.info(
`StatusPageCerts:AddCerts - CNAME for ${domain.fullDomain} is invalid. Removing cert`
);
}
2022-12-05 10:40:28 +00:00
}
}
2022-12-07 06:44:17 +00:00
);
2022-12-05 10:40:28 +00:00
2022-12-07 06:44:17 +00:00
RunCron(
'StatusPageCerts:RemoveCerts',
2023-03-02 19:23:03 +00:00
{ schedule: IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, runOnStartup: true },
2022-12-07 06:44:17 +00:00
async () => {
// Fetch all domains where certs are added to greenlock.
2022-12-05 10:40:28 +00:00
2022-12-07 06:44:17 +00:00
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
2022-12-05 10:40:28 +00:00
},
2022-12-07 06:44:17 +00:00
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true,
},
limit: LIMIT_MAX,
skip: 0,
2022-12-05 10:40:28 +00:00
props: {
2022-12-07 06:44:17 +00:00
isRoot: true,
},
});
2022-12-05 10:40:28 +00:00
2022-12-07 06:44:17 +00:00
for (const domain of domains) {
logger.info(
`StatusPageCerts:RemoveCerts - Checking CNAME ${domain.fullDomain}`
);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid: boolean = await checkCnameValidation(
domain.fullDomain!,
domain.cnameVerificationToken!
);
if (!isValid) {
logger.info(
`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is invalid. Removing domain from greenlock.`
);
await greenlock.remove({
subject: domain.fullDomain,
});
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isAddedtoGreenlock: false,
isCnameVerified: false,
},
props: {
isRoot: true,
},
});
logger.info(
`StatusPageCerts:RemoveCerts - ${domain.fullDomain} removed from greenlock.`
);
} else {
logger.info(
`StatusPageCerts:RemoveCerts - CNAME for ${domain.fullDomain} is valid`
);
}
2022-12-05 10:40:28 +00:00
}
}
2022-12-07 06:44:17 +00:00
);
2022-12-05 10:40:28 +00:00
2023-01-02 11:31:50 +00:00
RunCron(
'StatusPageCerts:WriteSelfSignedCertsToDisk',
EVERY_FIVE_MINUTE,
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 stausPageDomains: 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(
2023-01-02 11:34:11 +00:00
(cert: GreenlockCertificate) => {
2023-01-02 11:31:50 +00:00
return cert.key;
}
);
// Generate self signed certs
for (const domain of stausPageDomains) {
if (greenlockCertDomains.includes(domain.fullDomain)) {
continue;
}
if (!domain.fullDomain) {
continue;
}
await SelfSignedSSL.generate(
'/usr/src/Certs/StatusPageCerts',
domain.fullDomain
);
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isSelfSignedSslGenerated: true,
},
props: {
ignoreHooks: true,
isRoot: true,
2023-01-02 11:34:11 +00:00
},
2023-01-02 11:31:50 +00:00
});
}
}
);
2022-12-11 08:02:56 +00:00
RunCron(
2023-01-02 11:31:50 +00:00
'StatusPageCerts:WriteGreelockCertsToDisk',
2023-03-02 19:23:03 +00:00
{ schedule: IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, runOnStartup: true },
2022-12-11 08:02:56 +00:00
async () => {
// Fetch all domains where certs are added to greenlock.
const certs: Array<GreenlockCertificate> =
await GreenlockCertificateService.findBy({
2022-12-11 09:10:17 +00:00
query: {},
2022-12-11 08:02:56 +00:00
select: {
isKeyPair: true,
2022-12-11 09:10:17 +00:00
key: true,
2022-12-11 08:02:56 +00:00
blob: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const cert of certs) {
if (!cert.isKeyPair) {
continue;
}
2022-12-11 09:10:17 +00:00
const certBlob: GreenlockCertificate | undefined = certs.find(
(i: GreenlockCertificate) => {
return i.key === cert.key && !i.isKeyPair;
}
);
2022-12-11 08:02:56 +00:00
if (!certBlob) {
2022-12-11 09:10:17 +00:00
continue;
2022-12-11 08:02:56 +00:00
}
2022-12-11 09:10:17 +00:00
const key: string = JSON.parse(cert.blob || '{}').privateKeyPem;
2022-12-14 08:27:34 +00:00
let crt: string = JSON.parse(certBlob.blob || '{}').cert;
if (JSON.parse(certBlob.blob || '{}').chain) {
2022-12-15 06:37:15 +00:00
crt += '\n' + '\n' + JSON.parse(certBlob.blob || '{}').chain;
2022-12-14 08:27:34 +00:00
}
2022-12-11 08:02:56 +00:00
2022-12-11 09:10:17 +00:00
// Write to disk.
fs.writeFileSync(
`/usr/src/Certs/StatusPageCerts/${cert.key}.crt`,
2022-12-15 06:37:15 +00:00
crt
2022-12-11 09:10:17 +00:00
);
fs.writeFileSync(
`/usr/src/Certs/StatusPageCerts/${cert.key}.key`,
2022-12-15 06:37:15 +00:00
key
2022-12-11 09:10:17 +00:00
);
2022-12-11 08:02:56 +00:00
}
}
);
2022-12-08 16:23:21 +00:00
RunCron(
'StatusPageCerts:CheckSslProvisioningStatus',
2023-03-02 19:23:03 +00:00
{ schedule: IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, runOnStartup: true },
2022-12-08 16:23:21 +00:00
async () => {
// Fetch all domains where certs are added to greenlock.
const domains: Array<StatusPageDomain> =
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
},
select: {
_id: true,
fullDomain: true,
cnameVerificationToken: true,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
for (const domain of domains) {
logger.info(
`StatusPageCerts:RemoveCerts - Checking CNAME ${domain.fullDomain}`
);
// Check CNAME validation and if that fails. Remove certs from Greenlock.
const isValid: boolean = await isSslProvisioned(
domain.fullDomain!,
domain.cnameVerificationToken!
);
if (!isValid) {
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isSslProvisioned: false,
},
props: {
isRoot: true,
},
});
} else {
await StatusPageDomainService.updateOneById({
id: domain.id!,
data: {
isSslProvisioned: true,
},
props: {
isRoot: true,
},
});
}
}
}
);
2022-12-08 14:52:17 +00:00
const checkCnameValidation: Function = async (
2022-12-07 06:44:17 +00:00
fulldomain: string,
token: string
): Promise<boolean> => {
2022-12-08 14:52:17 +00:00
try {
2022-12-11 09:10:17 +00:00
const result: AxiosResponse = await axios.get(
2023-01-02 08:26:51 +00:00
'http://' +
2023-03-02 19:36:06 +00:00
fulldomain +
'/status-page-api/cname-verification/' +
token
2022-12-09 08:19:51 +00:00
);
2022-12-08 14:52:17 +00:00
if (result.status === 200) {
return true;
2022-12-05 10:40:28 +00:00
}
2022-12-09 08:19:51 +00:00
return false;
2022-12-08 14:52:17 +00:00
} catch (err) {
2023-01-03 09:49:41 +00:00
logger.info('Failed checking for CNAME ' + fulldomain);
logger.info('Token: ' + token);
logger.info(err);
2022-12-09 08:19:51 +00:00
return false;
2022-12-08 14:52:17 +00:00
}
2022-12-07 06:44:17 +00:00
};
2022-12-05 10:40:28 +00:00
2022-12-08 16:23:21 +00:00
const isSslProvisioned: Function = async (
fulldomain: string,
token: string
): Promise<boolean> => {
try {
2022-12-11 09:10:17 +00:00
const result: AxiosResponse = await axios.get(
2022-12-09 08:19:51 +00:00
'https://' +
2023-03-02 19:36:06 +00:00
fulldomain +
'/status-page-api/cname-verification/' +
token
2022-12-09 08:19:51 +00:00
);
2022-12-08 16:23:21 +00:00
if (result.status === 200) {
return true;
}
2022-12-09 08:19:51 +00:00
return false;
2022-12-08 16:23:21 +00:00
} catch (err) {
2022-12-09 08:19:51 +00:00
return false;
2022-12-08 16:23:21 +00:00
}
};
2022-12-07 06:44:17 +00:00
export default router;