smartacme/ts/smartacme.classes.smartacme.ts

232 lines
7.1 KiB
TypeScript
Raw Normal View History

2019-01-06 19:41:21 +00:00
import * as plugins from './smartacme.plugins';
2019-01-12 20:06:29 +00:00
import { Cert } from './smartacme.classes.cert';
2019-01-08 19:45:35 +00:00
import { CertManager } from './smartacme.classes.certmanager';
2019-01-13 01:10:00 +00:00
import { CertMatcher } from './smartacme.classes.certmatcher';
2019-01-08 23:01:01 +00:00
import * as interfaces from './interfaces';
import { request } from 'http';
2017-01-14 13:14:50 +00:00
2019-01-06 19:41:21 +00:00
/**
2019-01-12 18:12:52 +00:00
* the options for the class @see SmartAcme
2019-01-06 19:41:21 +00:00
*/
2019-01-08 19:45:35 +00:00
export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
2019-01-08 23:01:01 +00:00
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
2019-01-08 19:45:35 +00:00
setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
removeChallenge: (domainName: string) => Promise<any>;
2019-01-13 20:40:40 +00:00
environment: 'production' | 'integration';
2019-01-17 00:15:22 +00:00
logger?: plugins.smartlog.Smartlog;
2019-01-08 19:45:35 +00:00
}
2017-01-01 23:18:51 +00:00
2019-01-12 12:52:21 +00:00
/**
2019-01-12 18:11:39 +00:00
* class SmartAcme
* can be used for setting up communication with an ACME authority
2019-01-15 22:39:31 +00:00
*
2019-01-12 12:52:21 +00:00
* ```ts
* const mySmartAcmeInstance = new SmartAcme({
* // see ISmartAcmeOptions for options
* })
* ```
*/
2019-01-06 19:41:21 +00:00
export class SmartAcme {
2019-01-08 19:45:35 +00:00
private options: ISmartAcmeOptions;
2019-01-06 19:41:21 +00:00
// the acme client
private client: any;
2019-01-06 22:54:46 +00:00
private smartdns = new plugins.smartdns.Smartdns();
2019-01-17 00:15:22 +00:00
public logger: plugins.smartlog.Smartlog;
2016-11-01 19:16:43 +00:00
2019-01-06 19:41:21 +00:00
// the account private key
private privateKey: string;
2016-11-07 17:41:52 +00:00
2019-01-06 19:41:21 +00:00
// challenge fullfillment
2019-01-06 22:30:38 +00:00
private setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
private removeChallenge: (domainName: string) => Promise<any>;
2016-11-01 19:16:43 +00:00
2019-01-08 19:45:35 +00:00
// certmanager
private certmanager: CertManager;
2019-01-13 01:10:00 +00:00
private certmatcher: CertMatcher;
2019-01-14 01:46:36 +00:00
/**
* the remote handler to hand the request and response to.
*/
2019-01-17 00:15:22 +00:00
public certremoteHandler = async (
req: plugins.smartexpress.Request,
res: plugins.smartexpress.Response
) => {
2019-01-15 22:39:31 +00:00
const requestBody: interfaces.ICertRemoteRequest = req.body;
2019-01-17 21:13:10 +00:00
this.logger.log('ok', `got certificate request for ${requestBody.domainName}`);
2019-01-17 00:15:22 +00:00
const certDomain = this.certmatcher.getCertificateDomainNameByDomainName(
requestBody.domainName
2019-01-15 22:39:31 +00:00
);
2019-01-17 21:13:10 +00:00
this.logger.log('ok', `mapping ${requestBody.domainName} to ${certDomain}`);
2019-01-17 00:15:22 +00:00
let status: interfaces.TCertStatus = await this.certmanager.getCertificateStatus(certDomain);
2019-01-15 22:39:31 +00:00
let response: interfaces.ICertRemoteResponse;
switch (status) {
case 'existing':
2019-01-17 00:15:22 +00:00
this.logger.log('ok', `certificate exists for ${certDomain}. Sending certificate!`);
2019-01-15 22:39:31 +00:00
response = {
status,
certificate: await (await this.certmanager.retrieveCertificate(
certDomain
)).createSavableObject()
};
break;
default:
2019-01-17 00:15:22 +00:00
if (status === 'nonexisting') {
2019-01-15 22:39:31 +00:00
this.getCertificateForDomain(certDomain);
status = 'pending';
}
response = {
status
};
break;
}
res.status(200);
res.send(response);
res.end();
2019-02-06 08:47:33 +00:00
};
2019-01-08 19:45:35 +00:00
constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg;
2019-01-17 00:15:22 +00:00
this.options.logger
? (this.logger = optionsArg.logger)
: (this.logger = plugins.smartlog.defaultLogger);
2019-01-08 19:45:35 +00:00
}
2019-01-08 23:01:01 +00:00
/**
* inits the instance
2019-01-12 12:44:18 +00:00
* ```ts
* await myCloudlyInstance.init() // does not support options
* ```
2019-01-08 23:01:01 +00:00
*/
2019-01-08 19:45:35 +00:00
public async init() {
2019-01-08 23:01:01 +00:00
this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey());
2019-01-08 19:45:35 +00:00
this.setChallenge = this.options.setChallenge;
this.removeChallenge = this.options.removeChallenge;
2019-01-08 23:01:01 +00:00
// CertMangaer
this.certmanager = new CertManager(this, {
2019-01-08 19:45:35 +00:00
mongoDescriptor: this.options.mongoDescriptor
});
await this.certmanager.init();
2019-01-13 01:10:00 +00:00
// CertMatcher
this.certmatcher = new CertMatcher();
2019-01-08 23:01:01 +00:00
// ACME Client
2019-01-06 19:41:21 +00:00
this.client = new plugins.acme.Client({
2019-01-13 20:40:40 +00:00
directoryUrl: (() => {
2019-01-15 22:39:31 +00:00
if (this.options.environment === 'production') {
2019-01-13 20:40:40 +00:00
return plugins.acme.directory.letsencrypt.production;
} else {
return plugins.acme.directory.letsencrypt.staging;
}
})(),
2019-01-06 19:41:21 +00:00
accountKey: this.privateKey
});
/* Register account */
await this.client.createAccount({
termsOfServiceAgreed: true,
2019-01-08 19:45:35 +00:00
contact: [`mailto:${this.options.accountEmail}`]
2019-01-06 19:41:21 +00:00
});
}
2019-01-12 20:06:29 +00:00
public async stop() {
await this.certmanager.smartdataDb.close();
}
2019-01-12 12:44:18 +00:00
2019-01-17 21:50:21 +00:00
/**
* gets a certificate
* it runs through the following steps
2019-02-06 08:47:33 +00:00
*
2019-01-17 21:50:21 +00:00
* * look in the database
* * if in the database return it
* * of not in the database announce it
* * then get it from letsencrypt
* * store it
* * remove it from the pending map (which it go onto by announcing it)
* * retrieve it from the databse and return it
2019-02-06 08:47:33 +00:00
*
2019-01-17 21:50:21 +00:00
* @param domainArg
*/
2019-01-12 20:06:29 +00:00
public async getCertificateForDomain(domainArg: string): Promise<Cert> {
2019-01-13 01:11:56 +00:00
const certDomain = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomain);
2019-01-08 19:45:35 +00:00
2019-01-08 23:01:01 +00:00
if (retrievedCertificate) {
2019-01-08 19:45:35 +00:00
return retrievedCertificate;
2019-01-17 21:47:58 +00:00
} else {
await this.certmanager.announceCertificate(certDomain);
2019-01-08 19:45:35 +00:00
}
2019-01-06 19:41:21 +00:00
/* Place new order */
const order = await this.client.createOrder({
2019-01-13 01:11:56 +00:00
identifiers: [{ type: 'dns', value: certDomain }, { type: 'dns', value: `*.${certDomain}` }]
2019-01-06 19:41:21 +00:00
});
/* Get authorizations and select challenges */
const authorizations = await this.client.getAuthorizations(order);
2019-01-06 22:54:46 +00:00
for (const authz of authorizations) {
2019-01-06 22:30:38 +00:00
console.log(authz);
const domainDnsName: string = `_acme-challenge.${authz.identifier.value}`;
const dnsChallenge: string = authz.challenges.find(challengeArg => {
return challengeArg.type === 'dns-01';
});
// process.exit(1);
const keyAuthorization: string = await this.client.getChallengeKeyAuthorization(dnsChallenge);
2019-01-06 19:41:21 +00:00
try {
/* Satisfy challenge */
2019-01-06 22:30:38 +00:00
await this.setChallenge(domainDnsName, keyAuthorization);
2019-01-07 00:08:50 +00:00
await this.smartdns.checkUntilAvailable(domainDnsName, 'TXT', keyAuthorization, 100, 5000);
2019-01-13 18:15:03 +00:00
console.log('Cool down an extra 60 second for region availability');
await plugins.smartdelay.delayFor(60000);
2019-01-06 22:54:46 +00:00
2019-01-06 19:41:21 +00:00
/* Verify that challenge is satisfied */
2019-01-06 22:30:38 +00:00
await this.client.verifyChallenge(authz, dnsChallenge);
2019-01-06 19:41:21 +00:00
/* Notify ACME provider that challenge is satisfied */
2019-01-06 22:30:38 +00:00
await this.client.completeChallenge(dnsChallenge);
2019-01-06 19:41:21 +00:00
/* Wait for ACME provider to respond with valid status */
2019-01-06 22:30:38 +00:00
await this.client.waitForValidStatus(dnsChallenge);
2019-01-06 19:41:21 +00:00
} finally {
/* Clean up challenge response */
try {
2019-01-06 22:30:38 +00:00
await this.removeChallenge(domainDnsName);
2019-01-06 19:41:21 +00:00
} catch (e) {
console.log(e);
}
}
2019-01-06 22:54:46 +00:00
}
2019-01-06 19:41:21 +00:00
/* Finalize order */
const [key, csr] = await plugins.acme.forge.createCsr({
2019-01-13 01:11:56 +00:00
commonName: `*.${certDomain}`,
altNames: [certDomain]
2019-01-06 19:41:21 +00:00
});
await this.client.finalizeOrder(order, csr);
const cert = await this.client.getCertificate(order);
/* Done */
2019-01-08 19:45:35 +00:00
2019-01-12 12:44:18 +00:00
await this.certmanager.storeCertificate({
2019-01-16 21:34:38 +00:00
id: plugins.smartunique.shortId(),
2019-01-13 01:11:56 +00:00
domainName: certDomain,
2019-01-12 12:44:18 +00:00
privateKey: key.toString(),
publicKey: cert.toString(),
csr: csr.toString(),
created: Date.now()
});
2019-01-13 01:11:56 +00:00
const newCertificate = await this.certmanager.retrieveCertificate(certDomain);
2019-01-12 12:44:18 +00:00
return newCertificate;
2017-04-28 16:56:55 +00:00
}
2016-11-01 17:27:57 +00:00
}