Compare commits

...

18 Commits

Author SHA1 Message Date
ad54bf41ea 2.0.13 2019-01-09 00:01:02 +01:00
060ebf1b29 fix(core): update 2019-01-09 00:01:01 +01:00
a87c6acb8a 2.0.12 2019-01-08 20:45:36 +01:00
62d27619f4 fix(core): update 2019-01-08 20:45:35 +01:00
0faebf2a79 2.0.11 2019-01-07 12:29:11 +01:00
29ea50796c fix(core): update 2019-01-07 12:29:10 +01:00
26d1b7cbf0 2.0.10 2019-01-07 01:08:50 +01:00
c0c97835ea fix(core): update 2019-01-07 01:08:50 +01:00
d4d50b7dcf 2.0.9 2019-01-07 01:00:58 +01:00
2492fd4de2 fix(core): update 2019-01-07 01:00:58 +01:00
bef54799b6 2.0.8 2019-01-07 00:36:51 +01:00
dbe09f320a fix(core): update 2019-01-07 00:36:51 +01:00
18045dadaf 2.0.7 2019-01-06 23:54:47 +01:00
ee300c3e12 fix(core): update 2019-01-06 23:54:46 +01:00
ed4ba0cb61 2.0.6 2019-01-06 23:30:39 +01:00
a8ab27045d fix(core): update 2019-01-06 23:30:38 +01:00
975c3ed190 2.0.5 2019-01-06 20:41:43 +01:00
a99dea549b fix(core): update 2019-01-06 20:41:42 +01:00
14 changed files with 1232 additions and 158 deletions

1014
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@pushrocks/smartacme", "name": "@pushrocks/smartacme",
"version": "2.0.4", "version": "2.0.13",
"private": false, "private": false,
"description": "acme implementation in TypeScript", "description": "acme implementation in TypeScript",
"main": "dist/index.js", "main": "dist/index.js",
@ -25,9 +25,16 @@
}, },
"homepage": "https://gitlab.com/umbrellazone/smartacme#README", "homepage": "https://gitlab.com/umbrellazone/smartacme#README",
"dependencies": { "dependencies": {
"@pushrocks/lik": "^3.0.4",
"@pushrocks/smartdata": "^3.1.13",
"@pushrocks/smartdelay": "^2.0.2", "@pushrocks/smartdelay": "^2.0.2",
"@pushrocks/smartdns": "^3.0.8",
"@pushrocks/smartexpress": "^3.0.0",
"@pushrocks/smartpromise": "^2.0.5", "@pushrocks/smartpromise": "^2.0.5",
"acme-client": "^2.2.1" "@pushrocks/smartrequest": "^1.1.14",
"@pushrocks/smarttime": "^3.0.5",
"@pushrocks/smartunique": "^3.0.1",
"acme-client": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
"@gitzone/tsbuild": "^2.1.4", "@gitzone/tsbuild": "^2.1.4",

View File

@ -1,3 +1,6 @@
required: required:
- CF_EMAIL - CF_EMAIL
- CF_KEY - CF_KEY
- MONGODB_URL
- MONGODB_PASSWORD
- MONGODB_DATABASE

View File

@ -1,22 +1,33 @@
import { tap, expect } from '@pushrocks/tapbundle'; import { tap, expect } from '@pushrocks/tapbundle';
import { Qenv } from '@pushrocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
import * as smartacme from '../ts/index'; import * as smartacme from '../ts/index';
let smartAcmeInstance: smartacme.SmartAcme; let smartAcmeInstance: smartacme.SmartAcme;
tap.test('should create a valid instance of SmartAcme', async () => { tap.test('should create a valid instance of SmartAcme', async () => {
smartAcmeInstance = new smartacme.SmartAcme(); smartAcmeInstance = new smartacme.SmartAcme({
await smartAcmeInstance.init({
accountEmail: 'domains@lossless.org', accountEmail: 'domains@lossless.org',
accountPrivateKey: null, accountPrivateKey: null,
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL')
},
removeChallenge: async (...args) => { removeChallenge: async (...args) => {
console.log(args); console.log(args);
}, },
setChallenge: async (...args) => { setChallenge: async (...args) => {
console.log(args); console.log(args);
} },
validateRemoteRequest: async () => {
return true;
}
}); });
await smartAcmeInstance.getCertificateForDomain('bleu.de'); await smartAcmeInstance.init();
// await smartAcmeInstance.getCertificateForDomain('bleu.de');
}); });
tap.start(); tap.start();

View File

@ -1 +1,3 @@
export * from './smartacme.classes.smartacme'; export * from './smartacme.classes.smartacme';
export * from './smartacme.classes.certremoteclient';

9
ts/interfaces/cert.ts Normal file
View File

@ -0,0 +1,9 @@
export type TCertStatus = 'existing' | 'nonexisting' | 'pending' | 'failed';
export interface ICert {
domainName: string;
created: number;
privateKey: string;
publicKey: string;
csr: string;
}

View File

@ -0,0 +1,11 @@
import { ICert, TCertStatus } from './cert';
export interface ICertRemoteRequest {
secret: string;
domainName: string;
}
export interface ICertRemoteResponse {
status: TCertStatus;
certificate?: ICert;
}

View File

@ -1 +1,3 @@
export * from './accountdata'; export * from './accountdata';
export * from './cert';
export * from './certremote';

View File

@ -0,0 +1,37 @@
import * as plugins from './smartacme.plugins';
import * as interfaces from './interfaces';
import { CertManager } from './smartacme.classes.certmanager';
import { Collection, svDb, unI } from '@pushrocks/smartdata';
@plugins.smartdata.Collection(() => {
return CertManager.activeDB;
})
export class Cert extends plugins.smartdata.SmartDataDbDoc<Cert> implements interfaces.ICert {
@unI()
public index: string;
@svDb()
public domainName: string;
@svDb()
public created: number;
@svDb()
public privateKey: string;
@svDb()
public publicKey: string;
@svDb()
public csr: string;
constructor(privateKeyArg: string, publicKeyArg: string, csrArg: string) {
super();
this.privateKey = privateKeyArg;
this.publicKey = publicKeyArg;
this.csr = csrArg;
}
}

View File

@ -0,0 +1,92 @@
import * as plugins from './smartacme.plugins';
import { Cert } from './smartacme.classes.cert';
import { SmartAcme } from './smartacme.classes.smartacme';
import * as interfaces from './interfaces';
export class CertManager {
// =========
// STATIC
// =========
public static activeDB: plugins.smartdata.SmartdataDb;
// =========
// INSTANCE
// =========
private mongoDescriptor: plugins.smartdata.IMongoDescriptor;
public smartdataDb: plugins.smartdata.SmartdataDb;
public pendingMap: plugins.lik.Stringmap;
constructor(smartAcmeArg: SmartAcme,optionsArg: {
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
}) {
this.mongoDescriptor = optionsArg.mongoDescriptor;
}
public async init () {
// Smartdata DB
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.mongoDescriptor);
await this.smartdataDb.init();
CertManager.activeDB = this.smartdataDb;
// Pending Map
this.pendingMap = new plugins.lik.Stringmap();
};
/**
* retrieves a certificate
* @returns the Cert class or null
* @param domainName the domain Name to retrieve the vcertificate for
*/
public async retrieveCertificate(domainName: string): Promise<Cert> {
await this.checkCerts();
const existingCertificate: Cert = await Cert.getInstance({
name: domainName
});
if(existingCertificate) {
return existingCertificate;
} else {
return null;
}
}
/**
* stores the certificate with the
* @param publicKeyArg
* @param privateKeyArg
* @param csrArg
*/
public async storeCertificate(privateKeyArg: string, publicKeyArg: string, csrArg: string) {
const cert = new Cert(privateKeyArg, publicKeyArg, csrArg);
cert.save();
};
public async deleteCertificate(domainNameArg: string) {
}
public async getCertificateStatus(domainNameArg: string): Promise<interfaces.TCertStatus> {
const isPending = this.pendingMap.checkString('domainNameArg');
if (isPending) {
return 'pending';
}
// otherwise lets continue
const existingCertificate = this.retrieveCertificate(domainNameArg);
if (existingCertificate) {
return 'existing';
}
return 'nonexisting';
}
/**
* checks all certs for expiration
*/
private async checkCerts() {};
}

View File

@ -0,0 +1,47 @@
import * as plugins from './smartacme.plugins';
import * as interfaces from './interfaces';
import { ICertRemoteResponse } from './interfaces';
// tslint:disable-next-line: max-classes-per-file
export class CertRemoteClient {
private remoteUrl: string;
private secret: string;
constructor(optionsArg: {
remoteUrl: string;
secret: string;
}) {
this.remoteUrl = optionsArg.remoteUrl;
this.secret = optionsArg.secret;
}
/**
*
* @param domainNameArg
*/
async getCertificateForDomain(domainNameArg: string): Promise<interfaces.ICert> {
let certificate: interfaces.ICert;
const doRequestCycle = async (): Promise<interfaces.ICert> => {
const response: ICertRemoteResponse = (await plugins.smartrequest.postJson(this.remoteUrl, {
requestBody: <interfaces.ICertRemoteRequest>{
domainName: domainNameArg,
secret: this.secret
}
})).body;
switch(response.status) {
case 'pending':
await plugins.smartdelay.delayFor(5000);
const finalResponse = await doRequestCycle();
return finalResponse;
case 'existing':
return response.certificate;
case 'failed':
default:
console.log(`could not retrieve certificate for ${domainNameArg}`);
return null;
}
};
certificate = await doRequestCycle();
return certificate;
}
}

View File

@ -1,30 +1,92 @@
import * as plugins from './smartacme.plugins'; import * as plugins from './smartacme.plugins';
import { CertManager } from './smartacme.classes.certmanager';
import * as interfaces from './interfaces';
import { request } from 'http';
/** /**
* *
*/ */
export interface ISmartAcmeStorage {} export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
removeChallenge: (domainName: string) => Promise<any>;
validateRemoteRequest: () => Promise<boolean>;
}
export class SmartAcme { export class SmartAcme {
private options: ISmartAcmeOptions;
// the acme client // the acme client
private client: any; private client: any;
private smartdns = new plugins.smartdns.Smartdns();
// the account private key // the account private key
private privateKey: string; private privateKey: string;
// challenge fullfillment // challenge fullfillment
private setChallenge: (authz, challenge, keyAuthorization) => Promise<any>; private setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
private removeChallenge: (authz, challenge, keyAuthorization) => Promise<any>; private removeChallenge: (domainName: string) => Promise<any>;
private validateRemoteRequest: () => Promise<boolean>;
public async init(optionsArg: { // certmanager
accountPrivateKey?: string; private certmanager: CertManager;
accountEmail: string; private certremoteHandler: plugins.smartexpress.Handler;
setChallenge: (authz, challenge, keyAuthorization) => Promise<any>;
removeChallenge: (authz, challenge, keyAuthorization) => Promise<any>; constructor(optionsArg: ISmartAcmeOptions) {
}) { this.options = optionsArg;
this.privateKey = optionsArg.accountPrivateKey || (await plugins.acme.forge.createPrivateKey()); }
this.setChallenge = optionsArg.setChallenge;
this.removeChallenge = optionsArg.removeChallenge; /**
* inits the instance
*/
public async init() {
this.privateKey =
this.options.accountPrivateKey || (await plugins.acme.forge.createPrivateKey());
this.setChallenge = this.options.setChallenge;
this.removeChallenge = this.options.removeChallenge;
// CertMangaer
this.certmanager = new CertManager(this, {
mongoDescriptor: this.options.mongoDescriptor
});
await this.certmanager.init();
// CertRemoteHandler
this.certremoteHandler = new plugins.smartexpress.Handler('POST', async (req, res) => {
const requestBody: interfaces.ICertRemoteRequest = req.body;
const status: interfaces.TCertStatus = await this.certmanager.getCertificateStatus(requestBody.domainName);
const existingCertificate = await this.certmanager.retrieveCertificate(
requestBody.domainName
);
let response: interfaces.ICertRemoteResponse;
switch (status) {
case 'existing':
response = {
status,
certificate: {
created: existingCertificate.created,
csr: existingCertificate.csr,
domainName: existingCertificate.domainName,
privateKey: existingCertificate.privateKey,
publicKey: existingCertificate.publicKey
}
};
break;
default:
response = {
status
};
break;
}
res.status(200);
res.send(response);
res.end();
});
// ACME Client
this.client = new plugins.acme.Client({ this.client = new plugins.acme.Client({
directoryUrl: plugins.acme.directory.letsencrypt.staging, directoryUrl: plugins.acme.directory.letsencrypt.staging,
accountKey: this.privateKey accountKey: this.privateKey
@ -33,13 +95,19 @@ export class SmartAcme {
/* Register account */ /* Register account */
await this.client.createAccount({ await this.client.createAccount({
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
contact: [`mailto:${optionsArg.accountEmail}`] contact: [`mailto:${this.options.accountEmail}`]
}); });
} }
public async getCertificateForDomain(domainArg: string) { public async getCertificateForDomain(domainArg: string) {
const domain = domainArg; const domain = domainArg;
const retrievedCertificate = await this.certmanager.retrieveCertificate(domain);
if (retrievedCertificate) {
return retrievedCertificate;
}
/* Place new order */ /* Place new order */
const order = await this.client.createOrder({ const order = await this.client.createOrder({
identifiers: [{ type: 'dns', value: domain }, { type: 'dns', value: `*.${domain}` }] identifiers: [{ type: 'dns', value: domain }, { type: 'dns', value: `*.${domain}` }]
@ -48,34 +116,37 @@ export class SmartAcme {
/* Get authorizations and select challenges */ /* Get authorizations and select challenges */
const authorizations = await this.client.getAuthorizations(order); const authorizations = await this.client.getAuthorizations(order);
const promises = authorizations.map(async authz => { for (const authz of authorizations) {
const challenge = authz.challenges.pop(); console.log(authz);
const keyAuthorization = await this.client.getChallengeKeyAuthorization(challenge); 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);
try { try {
/* Satisfy challenge */ /* Satisfy challenge */
await this.setChallenge(authz, challenge, keyAuthorization); await this.setChallenge(domainDnsName, keyAuthorization);
await this.smartdns.checkUntilAvailable(domainDnsName, 'TXT', keyAuthorization, 100, 5000);
/* Verify that challenge is satisfied */ /* Verify that challenge is satisfied */
await this.client.verifyChallenge(authz, challenge); await this.client.verifyChallenge(authz, dnsChallenge);
/* Notify ACME provider that challenge is satisfied */ /* Notify ACME provider that challenge is satisfied */
await this.client.completeChallenge(challenge); await this.client.completeChallenge(dnsChallenge);
/* Wait for ACME provider to respond with valid status */ /* Wait for ACME provider to respond with valid status */
await this.client.waitForValidStatus(challenge); await this.client.waitForValidStatus(dnsChallenge);
} finally { } finally {
/* Clean up challenge response */ /* Clean up challenge response */
try { try {
await this.removeChallenge(authz, challenge, keyAuthorization); await this.removeChallenge(domainDnsName);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
} }
}); }
/* Wait for challenges to complete */
await Promise.all(promises);
/* Finalize order */ /* Finalize order */
const [key, csr] = await plugins.acme.forge.createCsr({ const [key, csr] = await plugins.acme.forge.createCsr({
@ -90,7 +161,7 @@ export class SmartAcme {
console.log(`CSR:\n${csr.toString()}`); console.log(`CSR:\n${csr.toString()}`);
console.log(`Private key:\n${key.toString()}`); console.log(`Private key:\n${key.toString()}`);
console.log(`Certificate:\n${cert.toString()}`); console.log(`Certificate:\n${cert.toString()}`);
}
toStorageObject () {}; this.certmanager.storeCertificate(key.toString(), cert.toString(), csr.toString());
}
} }

View File

@ -1,8 +1,15 @@
// @pushrocks scope // @pushrocks scope
import * as smartpromise from '@pushrocks/smartpromise'; import * as lik from '@pushrocks/lik';
import * as smartdata from '@pushrocks/smartdata';
import * as smartdelay from '@pushrocks/smartdelay'; import * as smartdelay from '@pushrocks/smartdelay';
import * as smartdns from '@pushrocks/smartdns';
import * as smartexpress from '@pushrocks/smartexpress';
import * as smartpromise from '@pushrocks/smartpromise';
import * as smartrequest from '@pushrocks/smartrequest';
import * as smartunique from '@pushrocks/smartunique';
import * as smarttime from '@pushrocks/smarttime';
export { smartpromise, smartdelay }; export { lik, smartdata, smartdelay, smartdns, smartexpress, smartpromise, smartrequest, smartunique, smarttime };
// thirs party scope // thirs party scope
import * as acme from 'acme-client'; import * as acme from 'acme-client';

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es2017",
"module": "commonjs"
}
}