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
|
|
|
|
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;
|
2020-02-10 20:13:06 +00:00
|
|
|
setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
|
|
|
|
removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
|
2019-01-13 20:40:40 +00:00
|
|
|
environment: 'production' | 'integration';
|
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;
|
2020-02-19 18:48:49 +00:00
|
|
|
private smartdns = new plugins.smartdns.Smartdns({});
|
2020-08-13 03:10:37 +00:00
|
|
|
public logger: plugins.smartlog.ConsoleLog;
|
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
|
2020-02-10 20:13:06 +00:00
|
|
|
private setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
|
|
|
|
private removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => 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
|
|
|
|
2019-01-08 19:45:35 +00:00
|
|
|
constructor(optionsArg: ISmartAcmeOptions) {
|
|
|
|
this.options = optionsArg;
|
2020-08-13 03:10:37 +00:00
|
|
|
this.logger = new plugins.smartlog.ConsoleLog();
|
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 =
|
2020-02-10 11:15:47 +00:00
|
|
|
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()).toString();
|
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, {
|
2020-08-12 16:36:06 +00:00
|
|
|
mongoDescriptor: this.options.mongoDescriptor,
|
2019-01-08 19:45:35 +00:00
|
|
|
});
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
})(),
|
2020-08-12 16:36:06 +00:00
|
|
|
accountKey: this.privateKey,
|
2019-01-06 19:41:21 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
/* Register account */
|
|
|
|
await this.client.createAccount({
|
|
|
|
termsOfServiceAgreed: true,
|
2020-08-12 16:36:06 +00:00
|
|
|
contact: [`mailto:${this.options.accountEmail}`],
|
2019-01-06 19:41:21 +00:00
|
|
|
});
|
2018-08-11 23:35:14 +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
|
2020-02-10 20:13:06 +00:00
|
|
|
* * if in the database and still valid return it
|
|
|
|
* * if not in the database announce it
|
2019-01-17 21:50:21 +00:00
|
|
|
* * 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> {
|
2020-02-10 20:13:06 +00:00
|
|
|
const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
|
|
|
|
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
|
|
|
|
2020-02-21 10:48:08 +00:00
|
|
|
if (
|
|
|
|
!retrievedCertificate &&
|
|
|
|
(await this.certmanager.interestMap.checkInterest(certDomainName))
|
|
|
|
) {
|
2020-02-10 20:13:06 +00:00
|
|
|
const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName);
|
|
|
|
const certificate = existingCertificateInterest.interestFullfilled;
|
|
|
|
return certificate;
|
|
|
|
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
|
2019-01-08 19:45:35 +00:00
|
|
|
return retrievedCertificate;
|
2020-02-10 20:36:01 +00:00
|
|
|
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed()) {
|
2020-05-17 16:21:25 +00:00
|
|
|
await retrievedCertificate.delete();
|
2019-01-08 19:45:35 +00:00
|
|
|
}
|
|
|
|
|
2020-02-10 20:13:06 +00:00
|
|
|
// lets make sure others get the same interest
|
2020-02-19 19:17:58 +00:00
|
|
|
const currentDomainInterst = await this.certmanager.interestMap.addInterest(certDomainName);
|
2020-02-10 20:13:06 +00:00
|
|
|
|
2019-01-06 19:41:21 +00:00
|
|
|
/* Place new order */
|
|
|
|
const order = await this.client.createOrder({
|
2020-02-10 11:15:47 +00:00
|
|
|
identifiers: [
|
2020-02-10 20:13:06 +00:00
|
|
|
{ type: 'dns', value: certDomainName },
|
2020-08-12 16:36:06 +00:00
|
|
|
{ type: 'dns', value: `*.${certDomainName}` },
|
|
|
|
],
|
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);
|
2020-02-10 20:13:06 +00:00
|
|
|
const fullHostName: string = `_acme-challenge.${authz.identifier.value}`;
|
2020-08-12 16:36:06 +00:00
|
|
|
const dnsChallenge: string = authz.challenges.find((challengeArg) => {
|
2019-01-06 22:30:38 +00:00
|
|
|
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 */
|
2020-02-10 20:13:06 +00:00
|
|
|
await this.setChallenge({
|
|
|
|
hostName: fullHostName,
|
2020-08-12 16:36:06 +00:00
|
|
|
challenge: keyAuthorization,
|
2020-02-10 20:13:06 +00:00
|
|
|
});
|
2020-08-13 03:10:37 +00:00
|
|
|
await plugins.smartdelay.delayFor(30000);
|
2020-02-10 20:13:06 +00:00
|
|
|
await this.smartdns.checkUntilAvailable(fullHostName, '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 {
|
2020-02-10 20:13:06 +00:00
|
|
|
await this.removeChallenge({
|
|
|
|
hostName: fullHostName,
|
2020-08-12 16:36:06 +00:00
|
|
|
challenge: keyAuthorization,
|
2020-02-10 20:13:06 +00:00
|
|
|
});
|
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({
|
2020-02-10 20:13:06 +00:00
|
|
|
commonName: `*.${certDomainName}`,
|
2020-08-12 16:36:06 +00:00
|
|
|
altNames: [certDomainName],
|
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(),
|
2020-02-10 20:13:06 +00:00
|
|
|
domainName: certDomainName,
|
2019-01-12 12:44:18 +00:00
|
|
|
privateKey: key.toString(),
|
|
|
|
publicKey: cert.toString(),
|
|
|
|
csr: csr.toString(),
|
2020-02-10 20:13:06 +00:00
|
|
|
created: Date.now(),
|
|
|
|
validUntil:
|
|
|
|
Date.now() +
|
|
|
|
plugins.smarttime.getMilliSecondsFromUnits({
|
2020-08-12 16:36:06 +00:00
|
|
|
days: 90,
|
|
|
|
}),
|
2019-01-12 12:44:18 +00:00
|
|
|
});
|
|
|
|
|
2020-02-10 20:13:06 +00:00
|
|
|
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
|
2020-02-19 19:17:58 +00:00
|
|
|
currentDomainInterst.fullfillInterest(newCertificate);
|
|
|
|
currentDomainInterst.destroy();
|
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
|
|
|
}
|