diff --git a/lib/account-old.js b/lib/account-old.js new file mode 100644 index 0000000..03dc33a --- /dev/null +++ b/lib/account-old.js @@ -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(); + } diff --git a/lib/client.js b/lib/client.js index d01a680..f86bfff 100644 --- a/lib/client.js +++ b/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(); - } + register(); function register() { - post(options.newReg, { + state.acme.post(options.newReg, { resource:'new-reg', contact:['mailto:'+options.email] }, getTerms); @@ -234,14 +144,28 @@ function getCert(options, cb) { if (state.termsRequired) { state.termsURL=links['terms-of-service']; - log(state.termsURL); - request.get(state.termsURL, getAgreement); + 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; - diff --git a/lib/write-old.js b/lib/write-old.js new file mode 100644 index 0000000..20332de --- /dev/null +++ b/lib/write-old.js @@ -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); + }