separating concerns
This commit is contained in:
parent
31b25af1cb
commit
89ef517338
|
@ -0,0 +1,77 @@
|
|||
options.newReg=options.newReg || 'https://acme-v01.api.letsencrypt.org/acme/new-reg';
|
||||
|
||||
if (!options.email) {
|
||||
return cb(new Error('No "email" option specified!'));
|
||||
}
|
||||
if (typeof options.domains==='string') {
|
||||
state.domains=options.domains.split(/[, ]+/);
|
||||
} else if (options.domains && options.domains instanceof Array) {
|
||||
state.domains=options.domains.slice();
|
||||
} else {
|
||||
return cb(new Error('No valid "domains" option specified!'));
|
||||
}
|
||||
|
||||
if ((_DEBUG=options.debug)) {
|
||||
if (!''.green) {
|
||||
require('colors');
|
||||
}
|
||||
log=console.log.bind(console);
|
||||
} else {
|
||||
log=NOOP;
|
||||
}
|
||||
|
||||
if (options.fork && !~process.argv.indexOf('--letiny-fork')) {
|
||||
state.child=child.fork(__filename, ['--letiny-fork']);
|
||||
if (options.challenge) {
|
||||
return cb(new Error('fork+challenge not supported yet'));
|
||||
}
|
||||
state.child.send({request:options});
|
||||
state.child.on('message', function(msg) {
|
||||
var res;
|
||||
if (msg.result) {
|
||||
res=msg.result;
|
||||
cb(res.err ? new Error(res.err) : null, res.cert, res.key, res.ca);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.accountKey) {
|
||||
if (options.accountKey.length>255) {
|
||||
state.accountKeyPEM=options.accountKey;
|
||||
} else {
|
||||
try {
|
||||
state.accountKeyPEM=fs.readFileSync(options.accountKey);
|
||||
} catch(err) {
|
||||
if (err.code==='ENOENT') {
|
||||
makeAccountKeyPair(true);
|
||||
} else {
|
||||
return handleErr(err, 'Failed to load accountKey');
|
||||
}
|
||||
}
|
||||
try {
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to parse accountKey');
|
||||
}
|
||||
initAcme();
|
||||
}
|
||||
} else {
|
||||
makeAccountKeyPair();
|
||||
}
|
||||
|
||||
function makeAccountKeyPair(save) {
|
||||
var keypair;
|
||||
log('Generating account keypair...');
|
||||
keypair=pki.rsa.generateKeyPair(2048);
|
||||
state.accountKeyPEM=pki.privateKeyToPem(keypair.privateKey);
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPEM);
|
||||
if (save) {
|
||||
try {
|
||||
fs.writeFileSync(options.accountKey, state.accountKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to save accountKey');
|
||||
}
|
||||
}
|
||||
initAcme();
|
||||
}
|
243
lib/client.js
243
lib/client.js
|
@ -103,114 +103,24 @@ Acme.prototype.post=function(url, body, cb) {
|
|||
};
|
||||
|
||||
|
||||
function getCert(options, cb) {
|
||||
var state={
|
||||
validatedDomains:[],
|
||||
validAuthorizationURLs:[]
|
||||
};
|
||||
|
||||
options.newReg=options.newReg || 'https://acme-v01.api.letsencrypt.org/acme/new-reg';
|
||||
|
||||
function registerNewAccount(state, options, cb) {
|
||||
if (!options.agreeToTerms) {
|
||||
cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))"));
|
||||
return;
|
||||
}
|
||||
if (!options.newReg) {
|
||||
cb(new Error("options.newReg must be the a new registration url"));
|
||||
return;
|
||||
}
|
||||
if (!options.email) {
|
||||
return cb(new Error('No "email" option specified!'));
|
||||
}
|
||||
if (typeof options.domains==='string') {
|
||||
state.domains=options.domains.split(/[, ]+/);
|
||||
} else if (options.domains && options.domains instanceof Array) {
|
||||
state.domains=options.domains.slice();
|
||||
} else {
|
||||
return cb(new Error('No valid "domains" option specified!'));
|
||||
}
|
||||
|
||||
if ((_DEBUG=options.debug)) {
|
||||
if (!''.green) {
|
||||
require('colors');
|
||||
}
|
||||
log=console.log.bind(console);
|
||||
} else {
|
||||
log=NOOP;
|
||||
}
|
||||
|
||||
if (options.fork && !~process.argv.indexOf('--letiny-fork')) {
|
||||
state.child=child.fork(__filename, ['--letiny-fork']);
|
||||
if (options.challenge) {
|
||||
return cb(new Error('fork+challenge not supported yet'));
|
||||
}
|
||||
state.child.send({request:options});
|
||||
state.child.on('message', function(msg) {
|
||||
var res;
|
||||
if (msg.result) {
|
||||
res=msg.result;
|
||||
cb(res.err ? new Error(res.err) : null, res.cert, res.key, res.ca);
|
||||
}
|
||||
});
|
||||
cb(new Error("options.email must be an email"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.accountKey) {
|
||||
if (options.accountKey.length>255) {
|
||||
state.accountKeyPEM=options.accountKey;
|
||||
} else {
|
||||
try {
|
||||
state.accountKeyPEM=fs.readFileSync(options.accountKey);
|
||||
} catch(err) {
|
||||
if (err.code==='ENOENT') {
|
||||
makeAccountKeyPair(true);
|
||||
} else {
|
||||
return handleErr(err, 'Failed to load accountKey');
|
||||
}
|
||||
}
|
||||
try {
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to parse accountKey');
|
||||
}
|
||||
initAcme();
|
||||
}
|
||||
} else {
|
||||
makeAccountKeyPair();
|
||||
}
|
||||
|
||||
function makeAccountKeyPair(save) {
|
||||
var keypair;
|
||||
log('Generating account keypair...');
|
||||
keypair=pki.rsa.generateKeyPair(2048);
|
||||
state.accountKeyPEM=pki.privateKeyToPem(keypair.privateKey);
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPEM);
|
||||
if (save) {
|
||||
try {
|
||||
fs.writeFileSync(options.accountKey, state.accountKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to save accountKey');
|
||||
}
|
||||
}
|
||||
initAcme();
|
||||
}
|
||||
|
||||
function initAcme() {
|
||||
state.acme=new Acme(state.accountKeyPair);
|
||||
makeKeyPair();
|
||||
}
|
||||
|
||||
function makeKeyPair() {
|
||||
var keypair;
|
||||
if (options.privateKey) {
|
||||
state.certPrivateKeyPEM=options.privateKey;
|
||||
} else {
|
||||
log('Generating cert keypair...');
|
||||
keypair=pki.rsa.generateKeyPair(2048);
|
||||
state.certPrivateKeyPEM=pki.privateKeyToPem(keypair.privateKey);
|
||||
}
|
||||
try {
|
||||
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to parse privateKey');
|
||||
}
|
||||
register();
|
||||
}
|
||||
|
||||
function register() {
|
||||
post(options.newReg, {
|
||||
state.acme.post(options.newReg, {
|
||||
resource:'new-reg',
|
||||
contact:['mailto:'+options.email]
|
||||
}, getTerms);
|
||||
|
@ -233,15 +143,29 @@ function getCert(options, cb) {
|
|||
state.termsRequired=('terms-of-service' in links);
|
||||
|
||||
if (state.termsRequired) {
|
||||
state.termsURL=links['terms-of-service'];
|
||||
options.agreeToTerms({
|
||||
tosUrl: state.termsURL
|
||||
, email: options.email
|
||||
}, function (err, agree) {
|
||||
if (err) {
|
||||
return handleErr(err);
|
||||
}
|
||||
if (!agree) {
|
||||
return handleErr(new Error("You must agree to the terms of use at '" + state.termsURL + "'"));
|
||||
}
|
||||
|
||||
state.agreeTerms = agree;
|
||||
state.termsURL=links['terms-of-service'];
|
||||
log(state.termsURL);
|
||||
request.get(state.termsURL, getAgreement);
|
||||
});
|
||||
} else {
|
||||
getChallenges();
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
function getAgreement(err, res, body) {
|
||||
function getAgreement(err/*, res, body*/) {
|
||||
if (err) {
|
||||
return handleErr(err, 'Couldn\'t get agreement');
|
||||
}
|
||||
|
@ -250,24 +174,61 @@ function getCert(options, cb) {
|
|||
}
|
||||
|
||||
function sendAgreement() {
|
||||
if (state.termsRequired && !options.agreeTerms) {
|
||||
if (state.termsRequired && !state.agreeTerms) {
|
||||
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL);
|
||||
}
|
||||
|
||||
log('Posting agreement to: '+state.registrationURL);
|
||||
|
||||
post(state.registrationURL, {
|
||||
state.acme.post(state.registrationURL, {
|
||||
resource:'reg',
|
||||
agreement:state.termsURL
|
||||
}, function(err, res, body) {
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
||||
} else {
|
||||
nextDomain();
|
||||
cb(null, body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleErr(err, text, info) {
|
||||
log(text, err, info);
|
||||
cb(err || new Error(text));
|
||||
}
|
||||
}
|
||||
|
||||
function getCert(options, cb) {
|
||||
var state={
|
||||
validatedDomains:[],
|
||||
validAuthorizationURLs:[]
|
||||
};
|
||||
|
||||
if (!options.accountPrivateKeyPem) {
|
||||
return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem"));
|
||||
}
|
||||
if (!options.domainPrivateKeyPem) {
|
||||
return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem"));
|
||||
}
|
||||
if (!options.setChallenge) {
|
||||
return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}"));
|
||||
}
|
||||
if (!options.removeChallenge) {
|
||||
return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}"));
|
||||
}
|
||||
|
||||
try {
|
||||
state.accountKeyPem=options.accountPrivateKeyPem;
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPEM);
|
||||
state.acme=new Acme(state.accountKeyPair);
|
||||
state.certPrivateKeyPEM=options.domainPrivateKeyPem;
|
||||
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPEM);
|
||||
} catch(err) {
|
||||
return handleErr(err, 'Failed to parse privateKey');
|
||||
}
|
||||
|
||||
nextDomain();
|
||||
|
||||
function nextDomain() {
|
||||
if (state.domains.length > 0) {
|
||||
getChallenges(state.domains.shift());
|
||||
|
@ -280,7 +241,7 @@ function getCert(options, cb) {
|
|||
function getChallenges(domain) {
|
||||
state.domain=domain;
|
||||
|
||||
post(state.newAuthorizationURL, {
|
||||
state.acme.post(state.newAuthorizationURL, {
|
||||
resource:'new-authz',
|
||||
identifier:{
|
||||
type:'dns',
|
||||
|
@ -320,29 +281,17 @@ function getCert(options, cb) {
|
|||
state.responseURL=challenge['uri'];
|
||||
state.path=challengePath;
|
||||
|
||||
if (options.webroot) {
|
||||
try {
|
||||
mkdirp(path.dirname(options.webroot+'/'+challengePath));
|
||||
fs.writeFileSync(path.normalize(options.webroot+'/'+challengePath), keyAuthorization);
|
||||
challengeDone();
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not write challange file to disk');
|
||||
}
|
||||
} else if (typeof options.challenge==='function') {
|
||||
options.challenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone);
|
||||
} else {
|
||||
return handleErr(null, 'No "challenge" function or "webroot" option given.');
|
||||
}
|
||||
options.setChallenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone);
|
||||
|
||||
function challengeDone() {
|
||||
post(state.responseURL, {
|
||||
state.acme.post(state.responseURL, {
|
||||
resource:'challenge',
|
||||
keyAuthorization:keyAuthorization
|
||||
}, function(err, res, body) {
|
||||
ensureValidation(err, res, body, function unlink() {
|
||||
if (options.webroot) {
|
||||
fs.unlinkSync(path.normalize(options.webroot+'/'+challengePath));
|
||||
}
|
||||
options.removeChallenge(state.domain, '/'+challengePath, function () {
|
||||
// ignore
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -382,7 +331,7 @@ function getCert(options, cb) {
|
|||
function getCertificate() {
|
||||
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
||||
log('Requesting certificate...');
|
||||
post(state.newCertificateURL, {
|
||||
state.acme.post(state.newCertificateURL, {
|
||||
resource:'new-cert',
|
||||
csr:csr,
|
||||
authorizations:state.validAuthorizationURLs
|
||||
|
@ -440,40 +389,22 @@ function getCert(options, cb) {
|
|||
}
|
||||
|
||||
function done() {
|
||||
var cert, pfx;
|
||||
var cert;
|
||||
|
||||
try {
|
||||
cert=certBufferToPEM(state.certificate);
|
||||
if (options.certFile) {
|
||||
fs.writeFileSync(options.certFile, cert);
|
||||
}
|
||||
if (options.keyFile) {
|
||||
fs.writeFileSync(options.keyFile, state.certPrivateKeyPEM);
|
||||
}
|
||||
if (options.caFile) {
|
||||
fs.writeFileSync(options.caFile, state.caCert);
|
||||
}
|
||||
if (options.pfxFile) {
|
||||
try {
|
||||
pfx=forge.pkcs12.toPkcs12Asn1(
|
||||
pki.privateKeyFromPem(state.certPrivateKeyPEM),
|
||||
[pki.certificateFromPem(cert), pki.certificateFromPem(state.caCert)],
|
||||
options.pfxPassword || '',
|
||||
options.aes ? {} : {algorithm:'3des'}
|
||||
);
|
||||
pfx=new Buffer(forge.asn1.toDer(pfx).toHex(), 'hex');
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not convert to PKCS#12');
|
||||
}
|
||||
fs.writeFileSync(options.pfxFile, pfx);
|
||||
}
|
||||
cb(null, cert, state.certPrivateKeyPEM, state.caCert);
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not write output files. Please check permissions!');
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e.stack);
|
||||
//cb(new Error("Could not write output files. Please check permissions!"));
|
||||
handleErr(e, 'Could not write output files. Please check permissions!');
|
||||
return;
|
||||
}
|
||||
|
||||
function post(url, body, cb) {
|
||||
return state.acme.post(url, body, cb);
|
||||
cb(null, {
|
||||
cert: cert
|
||||
, key: state.certPrivateKeyPEM
|
||||
, ca: state.caCert
|
||||
});
|
||||
}
|
||||
|
||||
function handleErr(err, text, info) {
|
||||
|
@ -534,5 +465,5 @@ if (~process.argv.indexOf('--letiny-fork')) {
|
|||
});
|
||||
}
|
||||
|
||||
exports.registerNewAccount=registerNewAccount;
|
||||
exports.getCert=getCert;
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
if (options.certFile) {
|
||||
fs.writeFileSync(options.certFile, cert);
|
||||
}
|
||||
if (options.keyFile) {
|
||||
fs.writeFileSync(options.keyFile, state.certPrivateKeyPEM);
|
||||
}
|
||||
if (options.caFile) {
|
||||
fs.writeFileSync(options.caFile, state.caCert);
|
||||
}
|
||||
if (options.pfxFile) {
|
||||
try {
|
||||
pfx=forge.pkcs12.toPkcs12Asn1(
|
||||
pki.privateKeyFromPem(state.certPrivateKeyPEM),
|
||||
[pki.certificateFromPem(cert), pki.certificateFromPem(state.caCert)],
|
||||
options.pfxPassword || '',
|
||||
options.aes ? {} : {algorithm:'3des'}
|
||||
);
|
||||
pfx=new Buffer(forge.asn1.toDer(pfx).toHex(), 'hex');
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not convert to PKCS#12');
|
||||
}
|
||||
fs.writeFileSync(options.pfxFile, pfx);
|
||||
}
|
Loading…
Reference in New Issue