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) {
|
function registerNewAccount(state, options, cb) {
|
||||||
var state={
|
if (!options.agreeToTerms) {
|
||||||
validatedDomains:[],
|
cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))"));
|
||||||
validAuthorizationURLs:[]
|
return;
|
||||||
};
|
}
|
||||||
|
if (!options.newReg) {
|
||||||
options.newReg=options.newReg || 'https://acme-v01.api.letsencrypt.org/acme/new-reg';
|
cb(new Error("options.newReg must be the a new registration url"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!options.email) {
|
if (!options.email) {
|
||||||
return cb(new Error('No "email" option specified!'));
|
cb(new Error("options.email must be an email"));
|
||||||
}
|
|
||||||
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;
|
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();
|
register();
|
||||||
}
|
|
||||||
|
|
||||||
function register() {
|
function register() {
|
||||||
post(options.newReg, {
|
state.acme.post(options.newReg, {
|
||||||
resource:'new-reg',
|
resource:'new-reg',
|
||||||
contact:['mailto:'+options.email]
|
contact:['mailto:'+options.email]
|
||||||
}, getTerms);
|
}, getTerms);
|
||||||
|
@ -233,15 +143,29 @@ function getCert(options, cb) {
|
||||||
state.termsRequired=('terms-of-service' in links);
|
state.termsRequired=('terms-of-service' in links);
|
||||||
|
|
||||||
if (state.termsRequired) {
|
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'];
|
state.termsURL=links['terms-of-service'];
|
||||||
log(state.termsURL);
|
log(state.termsURL);
|
||||||
request.get(state.termsURL, getAgreement);
|
request.get(state.termsURL, getAgreement);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
getChallenges();
|
cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAgreement(err, res, body) {
|
function getAgreement(err/*, res, body*/) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return handleErr(err, 'Couldn\'t get agreement');
|
return handleErr(err, 'Couldn\'t get agreement');
|
||||||
}
|
}
|
||||||
|
@ -250,24 +174,61 @@ function getCert(options, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAgreement() {
|
function sendAgreement() {
|
||||||
if (state.termsRequired && !options.agreeTerms) {
|
if (state.termsRequired && !state.agreeTerms) {
|
||||||
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL);
|
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Posting agreement to: '+state.registrationURL);
|
log('Posting agreement to: '+state.registrationURL);
|
||||||
|
|
||||||
post(state.registrationURL, {
|
state.acme.post(state.registrationURL, {
|
||||||
resource:'reg',
|
resource:'reg',
|
||||||
agreement:state.termsURL
|
agreement:state.termsURL
|
||||||
}, function(err, res, body) {
|
}, function(err, res, body) {
|
||||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||||
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
||||||
} else {
|
} 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() {
|
function nextDomain() {
|
||||||
if (state.domains.length > 0) {
|
if (state.domains.length > 0) {
|
||||||
getChallenges(state.domains.shift());
|
getChallenges(state.domains.shift());
|
||||||
|
@ -280,7 +241,7 @@ function getCert(options, cb) {
|
||||||
function getChallenges(domain) {
|
function getChallenges(domain) {
|
||||||
state.domain=domain;
|
state.domain=domain;
|
||||||
|
|
||||||
post(state.newAuthorizationURL, {
|
state.acme.post(state.newAuthorizationURL, {
|
||||||
resource:'new-authz',
|
resource:'new-authz',
|
||||||
identifier:{
|
identifier:{
|
||||||
type:'dns',
|
type:'dns',
|
||||||
|
@ -320,29 +281,17 @@ function getCert(options, cb) {
|
||||||
state.responseURL=challenge['uri'];
|
state.responseURL=challenge['uri'];
|
||||||
state.path=challengePath;
|
state.path=challengePath;
|
||||||
|
|
||||||
if (options.webroot) {
|
options.setChallenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone);
|
||||||
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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function challengeDone() {
|
function challengeDone() {
|
||||||
post(state.responseURL, {
|
state.acme.post(state.responseURL, {
|
||||||
resource:'challenge',
|
resource:'challenge',
|
||||||
keyAuthorization:keyAuthorization
|
keyAuthorization:keyAuthorization
|
||||||
}, function(err, res, body) {
|
}, function(err, res, body) {
|
||||||
ensureValidation(err, res, body, function unlink() {
|
ensureValidation(err, res, body, function unlink() {
|
||||||
if (options.webroot) {
|
options.removeChallenge(state.domain, '/'+challengePath, function () {
|
||||||
fs.unlinkSync(path.normalize(options.webroot+'/'+challengePath));
|
// ignore
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -382,7 +331,7 @@ function getCert(options, cb) {
|
||||||
function getCertificate() {
|
function getCertificate() {
|
||||||
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
||||||
log('Requesting certificate...');
|
log('Requesting certificate...');
|
||||||
post(state.newCertificateURL, {
|
state.acme.post(state.newCertificateURL, {
|
||||||
resource:'new-cert',
|
resource:'new-cert',
|
||||||
csr:csr,
|
csr:csr,
|
||||||
authorizations:state.validAuthorizationURLs
|
authorizations:state.validAuthorizationURLs
|
||||||
|
@ -440,40 +389,22 @@ function getCert(options, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function done() {
|
function done() {
|
||||||
var cert, pfx;
|
var cert;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cert=certBufferToPEM(state.certificate);
|
cert=certBufferToPEM(state.certificate);
|
||||||
if (options.certFile) {
|
} catch(e) {
|
||||||
fs.writeFileSync(options.certFile, cert);
|
console.error(e.stack);
|
||||||
}
|
//cb(new Error("Could not write output files. Please check permissions!"));
|
||||||
if (options.keyFile) {
|
handleErr(e, 'Could not write output files. Please check permissions!');
|
||||||
fs.writeFileSync(options.keyFile, state.certPrivateKeyPEM);
|
return;
|
||||||
}
|
|
||||||
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!');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function post(url, body, cb) {
|
cb(null, {
|
||||||
return state.acme.post(url, body, cb);
|
cert: cert
|
||||||
|
, key: state.certPrivateKeyPEM
|
||||||
|
, ca: state.caCert
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleErr(err, text, info) {
|
function handleErr(err, text, info) {
|
||||||
|
@ -534,5 +465,5 @@ if (~process.argv.indexOf('--letiny-fork')) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.registerNewAccount=registerNewAccount;
|
||||||
exports.getCert=getCert;
|
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