From 39c0047fb617dfa5d78eb445b8f57f9fc37b0d14 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:37:39 -0800 Subject: [PATCH 01/25] begin ursa --- backends/ursa.js | 255 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 backends/ursa.js diff --git a/backends/ursa.js b/backends/ursa.js new file mode 100644 index 0000000..fdbeae1 --- /dev/null +++ b/backends/ursa.js @@ -0,0 +1,255 @@ +'use strict'; + +var PromiseA = require('bluebird'); +var path = require('path'); +var fs = PromiseA.promisifyAll(require('fs')); +var cutils = PromiseA.promisifyAll(require('crypto-utils-ursa')); +//var futils = require('letsencrypt-forge/lib/crypto-utils'); +var requestAsync = PromiseA.promisify(require('request')); +var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); +var knownUrls = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert']; + +var ipc = {}; // in-process cache + +//function noop() {} +function getAcmeUrls(args) { + var now = Date.now(); + + // TODO check response header on request for cache time + if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) { + return PromiseA.resolve(ipc.acmeUrls); + } + + return requestAsync({ + url: args.server + }).then(function (data) { + if (4 !== Object.keys(data).length) { + console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand"); + } + if (!knownUrls.every(function (url) { + return data[url]; + })) { + console.warn("This Let's Encrypt / ACME server is missing urls that this client may need."); + } + + ipc.acmeUrlsUpdatedAt = Date.now(); + ipc.acmeUrls = { + newAuthz: data['new-authz'] + , newCert: data['new-cert'] + , newReg: data['new-reg'] + , revokeCert: data['revoke-cert'] + }; + + return ipc.acmeUrls; + }); +} + +function createAccount(args, handlers) { + var mkdirpAsync = PromiseA.promisify(require('mkdirp')); + var os = require("os"); + var localname = os.hostname(); + + // TODO support ECDSA + // arg.rsaBitLength args.rsaExponent + return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (obj) { + /* obj = { privateKeyPem, publicKeyPem, publicKeyMd5 } */ + + var accountId = obj.publicKeyMd5; // I would have chosen sha1 or sha2... but whatever + var accountDir = path.join(args.accountsDir, accountId); + var isoDate = new Date().toISOString(); + + /* + files.accountId = accountId; // md5sum(publicKeyPem) + files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } + */ + + // TODO register + return lef.registerNewAccountAsync({ + email: args.email + , domains: Array.isArray(args.domains) || (args.domains||'').split(',') + , newReg: args.server + , debug: args.debug || handlers.debug + , webroot: args.webrootPath + , setChallenge: function (domain, key, value, done) { + args.domains = [domain]; + handlers.setChallenge(args, key, value, done); + } + , removeChallenge: function (domain, key, done) { + args.domains = [domain]; + handlers.removeChallenge(args, key, done); + } + , agreeToTerms: function (tosUrl, agree) { + // args.email = email; + args.tosUrl = tosUrl; + handlers.agreeToTerms(args, agree); + } + // TODO send either privateKeyPem or privateKeyJson or privateKeyJwk (?) + , privateKeyPem: obj.privateKeyPem + }).then(function (body) { + if ('string' === typeof body) { + try { + body = JSON.parse(body); + } catch(e) { + // ignore + } + } + return mkdirpAsync(args.accountDir, function () { + var jwk = cutils.toAcmePrivateKey(obj.privateKeyPem); + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} + // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } + // regr.json { "body" } + /* + { body: + { contact: [ 'mailto:coolaj86@gmail.com' ], + agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', + key: + { e: 'AQAB', + kty: 'RSA', + n: '...' } }, + uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', + new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', + terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } + */ + return PromiseA.all([ + fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify({ creation_host: localname, creation_dt: isoDate }), 'utf8') + , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(jwk), 'utf8') + , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') + ]); + }); + }); + }); +} + +function getAccount(args, accountId) { + var accountDir = path.join(args.accountsDir, accountId); + var files = {}; + var configs = ['meta.json', 'private_key.json', 'regr.json']; + + return PromiseA.all(configs.map(function (filename) { + var keyname = filename.slice(0, -5); + + return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { + var data; + + try { + data = JSON.parse(text); + } catch(e) { + files[keyname] = { error: e }; + return; + } + + files[keyname] = data; + }, function (err) { + files[keyname] = err; + }); + })).then(function () { + + if (!Object.keys(files).every(function (key) { + return !files[key].error; + })) { + console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); + return createAccount(args); + } + + return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { + files.accountId = accountId; // md5sum(publicKeyPem) + files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... + files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } + + return files; + }); + }); +} + +function getAccountByEmail(args) { + // If we read 10,000 account directories looking for + // just one email address, that could get crazy. + // We should have a folder per email and list + // each account as a file in the folder + // TODO + return PromiseA.resolve(null); +} + +module.exports.create = function (defaults, opts) { + var LE = require('./'); + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + if (!opts) { + opts = {}; + } + + /* + defaults.webroot = true; + defaults.renewByDefault = true; + defaults.text = true; + */ + defaults.server = defaults.server || LE.liveServer; + + var wrapped = { + registerAsync: function (args) { + args.server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory + var hostname = require('url').parse(args.server).hostname; + var configDir = args.configDir || defaults.configDir || LE.configDir; + args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', hostname + '.conf'); + args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', hostname, 'directory'); + + pyconf.readFileAsync(args.renewalDir).then(function (renewal) { + return renewal.account; + }, function (err) { + if ("EENOENT" === err.code) { + return getAccountByEmail(args); + } + + return err; + }).then(function (accountId) { + // Note: the ACME urls are always fetched fresh on purpose + return getAcmeUrls(args).then(function (urls) { + args._acmeUrls = urls; + + if (accountId) { + return getAccount(args, accountId); + } else { + return createAccount(args); + } + }); + }).then(function (account) { + throw new Error("IMPLEMENTATION NOT COMPLETE"); + }); +/* + return fs.readdirAsync(accountsDir, function (nodes) { + return PromiseA.all(nodes.map(function (node) { + var reMd5 = /[a-f0-9]{32}/i; + if (reMd5.test(node)) { + } + })); + }); +*/ + } + , fetchAsync: function (args) { + var hostname = args.domains[0]; + var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); + var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); + + return PromiseA.all([ + fs.readFileAsync(privpath, 'ascii') + , fs.readFileAsync(crtpath, 'ascii') + // stat the file, not the link + , fs.statAsync(crtpath) + ]).then(function (arr) { + return { + key: arr[0] // privkey.pem + , cert: arr[1] // fullchain.pem + // TODO parse centificate for lifetime / expiresAt + , issuedAt: arr[2].mtime.valueOf() + }; + }, function () { + return null; + }); + } + }; + + return wrapped; +}; From 4c027a38465dc566177f47dfc86fbe4c1674d802 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:37:56 -0800 Subject: [PATCH 02/25] backwards compat --- backends/python.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/backends/python.js b/backends/python.js index 4fc1bc9..5df646b 100644 --- a/backends/python.js +++ b/backends/python.js @@ -3,7 +3,17 @@ var PromiseA = require('bluebird'); var fs = PromiseA.promisifyAll(require('fs')); -module.exports.create = function (defaults, opts) { +module.exports.create = function (defaults, opts, extra) { + // v1.0.0 backwards compat + if (3 === arguments.length) { + opts.pythonClientPath = defaults; + defaults = opts; + opts = extra; + } + else if (2 !== arguments.length) { + throw new Error("Instead of creating the python backend yourself, just pass it to LE. See the README.md"); + } + defaults.webroot = true; defaults.renewByDefault = true; defaults.text = true; From d42c3482049300efc39e0d13811e9b2849af31bf Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:38:21 -0800 Subject: [PATCH 03/25] more default handlers --- index.js | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 5c6d3c1..2d6324e 100644 --- a/index.js +++ b/index.js @@ -29,18 +29,6 @@ LE.merge = function merge(defaults, args) { }; LE.create = function (backend, defaults, handlers) { - if ('function' === typeof backend.create) { - backend.create(defaults, handlers); - } - else if ('string' === typeof backend) { - // TODO I'll probably regret this - // I don't like dynamic requires because they cause build / minification issues. - backend = require(path.join('backends', backend)).create(defaults, handlers); - } - else { - // ignore - // this backend was created the v1.0.0 way - } if (!handlers) { handlers = {}; } if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } @@ -51,9 +39,36 @@ LE.create = function (backend, defaults, handlers) { cb(null, null); }; } + if (!handlers.setChallenge) { + if (!defaults.webrootPath) { + // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} + throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); + } + handlers.setChallenge = require('lib/default-set-challenge'); + } + if (!handlers.removeChallenge) { + if (!defaults.webrootPath) { + // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} + throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); + } + handlers.removeChallenge = require('lib/default-remove-challenge'); + } + if (!handlers.agreeToTerms) { + if (defaults.agreeTos) { + console.warn("[WARN] Agreeing to terms by default is risky business..."); + } + handlers.removeChallenge = require('lib/default-handlers').agreeToTerms; + } + if ('function' === typeof backend.create) { + backend = backend.create(defaults, handlers); + } + else { + // ignore + // this backend was created the v1.0.0 way + } backend = PromiseA.promisifyAll(backend); - var utils = require('./utils'); + var utils = require('./utils'); //var attempts = {}; // should exist in master process only var ipc = {}; // in-process cache var le; From 72b9cfbe3ad0fcf538f2038c786c8149a2b598d0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:38:40 -0800 Subject: [PATCH 04/25] moved pyconf to own module --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c1c7448..4255ed2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "bluebird": "^3.0.6", + "pyconf": "^1.0.0", "safe-replace": "^1.0.0", "serve-static": "^1.10.0" } From 84f17d95d10e749b1f776a0aafbf28b00db4063a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:39:01 -0800 Subject: [PATCH 05/25] ursa+forge hybrid utils --- crypto-utils-ursa.js | 73 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 crypto-utils-ursa.js diff --git a/crypto-utils-ursa.js b/crypto-utils-ursa.js new file mode 100644 index 0000000..55fd1ea --- /dev/null +++ b/crypto-utils-ursa.js @@ -0,0 +1,73 @@ +'use strict'; + +var crypto = require('crypto'); +var ursa = require('ursa'); +var forge = require('node-forge'); + +function binstr2b64(binstr) { + return new Buffer(binstr, 'binary').toString('base64'); +} + +function toAcmePrivateKey(privkeyPem) { + var forgePrivkey = forge.pki.privateKeyFromPem(privkeyPem); + + return { + kty: "RSA" + , n: binstr2b64(forgePrivkey.n) + , e: binstr2b64(forgePrivkey.e) + , d: binstr2b64(forgePrivkey.d) + , p: binstr2b64(forgePrivkey.p) + , q: binstr2b64(forgePrivkey.q) + , dp: binstr2b64(forgePrivkey.dP) + , dq: binstr2b64(forgePrivkey.dQ) + , qi: binstr2b64(forgePrivkey.qInv) + }; +} + +function generateRsaKeypair(bitlen, exp, cb) { + var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/); + var pems = { + publicKeyPem: keypair.toPublicPem() + , privateKeyPem: keypair.toPrivatePem() + }; + + pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex'); + // TODO thumbprint + // TODO jwk + // TODO json + + cb(null, pems); +} + +function parseAccountPrivateKey(pkj, cb) { + Object.keys(pkj).forEach(function (key) { + pkj[key] = new Buffer(pkj[key], 'base64'); + }); + + var priv; + + try { + priv = ursa.createPrivateKeyFromComponents( + pkj.n // modulus + , pkj.e // exponent + , pkj.p + , pkj.q + , pkj.dp + , pkj.dq + , pkj.qi + , pkj.d + ); + } catch(e) { + cb(e); + return; + } + + cb(null, { + privateKeyPem: priv.toPrivatePem.toString('ascii') + , publicKeyPem: priv.toPrivatePem.toString('ascii') + }); +} + +module.exports.parseAccountPrivateKey = parseAccountPrivateKey; +module.exports.generateRsaKeypair = generateRsaKeypair; +module.exports.toAcmePrivateKey = toAcmePrivateKey; From d03c6ac577d6cf38faba3df94fca6ad45a2116d7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:44:09 -0800 Subject: [PATCH 06/25] add ursa example --- examples/ursa.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 examples/ursa.js diff --git a/examples/ursa.js b/examples/ursa.js new file mode 100644 index 0000000..d86562c --- /dev/null +++ b/examples/ursa.js @@ -0,0 +1,52 @@ +'use strict'; + +var LE = require('../'); +var config = require('./config-minimal'); + +// Note: you should make this special dir in your product and leave it empty +config.le.webrootPath = __dirname + '/../tests/acme-challenge'; +config.le.server = LE.stagingServer; + + +// +// Manual Registration +// +var le = LE.create(require('../backends/ursa'), config.le); +le.register({ + agreeTos: true +, domains: ['example.com'] // CHANGE TO YOUR DOMAIN +, email: 'user@email.com' // CHANGE TO YOUR EMAIL +}, function (err) { + if (err) { + console.error('[Error]: node-letsencrypt/examples/ursa'); + console.error(err.stack); + } else { + console.log('success'); + } + + plainServer.close(); + tlsServer.close(); +}); + +// +// Express App +// +var app = require('express')(); +app.use('/', le.middleware()); + + +// +// HTTP & HTTPS servers +// (required for domain validation) +// +var plainServer = require('http').createServer(app).listen(config.plainPort, function () { + console.log('Listening http', this.address()); +}); + +var tlsServer = require('https').createServer({ + key: config.tlsKey +, cert: config.tlsCert +, SNICallback: le.sniCallback +}, app).listen(config.tlsPort, function () { + console.log('Listening http', this.address()); +}); From d4a44f893cc78f9f395e6d7856ad8ef2124b0b02 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 03:51:41 -0800 Subject: [PATCH 07/25] add default handlers --- lib/default-handlers.js | 5 +++++ lib/default-remove-challenge.js | 10 ++++++++++ lib/default-set-challenge.js | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 lib/default-handlers.js create mode 100644 lib/default-remove-challenge.js create mode 100644 lib/default-set-challenge.js diff --git a/lib/default-handlers.js b/lib/default-handlers.js new file mode 100644 index 0000000..88ed6b4 --- /dev/null +++ b/lib/default-handlers.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports.agreeToTerms = function (args, agree) { + agree(args.agreeTos || args.agree); +}; diff --git a/lib/default-remove-challenge.js b/lib/default-remove-challenge.js new file mode 100644 index 0000000..1aedfdd --- /dev/null +++ b/lib/default-remove-challenge.js @@ -0,0 +1,10 @@ +'use strict'; + +var path = require('path'); +var fs = require('fs'); + +module.exports = function (args, key, done) { + //var hostname = args.domains[0]; + + fs.unlinkSync(path.join(args.webroot, key), done); +}; diff --git a/lib/default-set-challenge.js b/lib/default-set-challenge.js new file mode 100644 index 0000000..4aec345 --- /dev/null +++ b/lib/default-set-challenge.js @@ -0,0 +1,21 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +module.exports = function (args, challengePath, keyAuthorization, done) { + //var hostname = args.domains[0]; + var mkdirp = require('mkdirp'); + + // TODO should be args.webrootPath + mkdirp(path.join(args.webrootPath, challengePath), function (err) { + if (err) { + done(err); + return; + } + + fs.writeFile(path.join(args.webrootPath, challengePath), keyAuthorization, 'utf8', function (err) { + done(err); + }); + }); +}; From 1ad3e6f2f27d1d41bd1ed820b67be10af86c9514 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 12:01:05 +0000 Subject: [PATCH 08/25] updates --- backends/ursa.js | 2 +- index.js | 4 +-- .../crypto-utils-ursa.js | 0 lib/default-handlers.js | 34 ++++++++++++++++++- lib/default-remove-challenge.js | 10 ------ lib/default-set-challenge.js | 21 ------------ package.json | 2 ++ 7 files changed, 38 insertions(+), 35 deletions(-) rename crypto-utils-ursa.js => lib/crypto-utils-ursa.js (100%) delete mode 100644 lib/default-remove-challenge.js delete mode 100644 lib/default-set-challenge.js diff --git a/backends/ursa.js b/backends/ursa.js index fdbeae1..e22989d 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -3,7 +3,7 @@ var PromiseA = require('bluebird'); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); -var cutils = PromiseA.promisifyAll(require('crypto-utils-ursa')); +var cutils = PromiseA.promisifyAll(require('../lib/crypto-utils-ursa')); //var futils = require('letsencrypt-forge/lib/crypto-utils'); var requestAsync = PromiseA.promisify(require('request')); var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); diff --git a/index.js b/index.js index 2d6324e..32fa3e0 100644 --- a/index.js +++ b/index.js @@ -44,14 +44,14 @@ LE.create = function (backend, defaults, handlers) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); } - handlers.setChallenge = require('lib/default-set-challenge'); + handlers.setChallenge = require('./lib/default-handlers').setChallenge; } if (!handlers.removeChallenge) { if (!defaults.webrootPath) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); } - handlers.removeChallenge = require('lib/default-remove-challenge'); + handlers.removeChallenge = require('./lib/default-handlers').remove-Challenge; } if (!handlers.agreeToTerms) { if (defaults.agreeTos) { diff --git a/crypto-utils-ursa.js b/lib/crypto-utils-ursa.js similarity index 100% rename from crypto-utils-ursa.js rename to lib/crypto-utils-ursa.js diff --git a/lib/default-handlers.js b/lib/default-handlers.js index 88ed6b4..4becc64 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -1,5 +1,37 @@ 'use strict'; +var fs = require('fs'); +var path = require('path'); + module.exports.agreeToTerms = function (args, agree) { - agree(args.agreeTos || args.agree); + agree(args.agreeTos); +}; + +module.exports.setChallenge = function (args, challengePath, keyAuthorization, done) { + //var hostname = args.domains[0]; + var mkdirp = require('mkdirp'); + + // TODO should be args.webrootPath + mkdirp(path.join(args.webrootPath, challengePath), function (err) { + if (err) { + done(err); + return; + } + + fs.writeFile(path.join(args.webrootPath, challengePath), keyAuthorization, 'utf8', function (err) { + done(err); + }); + }); +}; + +module.exports.getChallenge = function (args, key, done) { + //var hostname = args.domains[0]; + + fs.readFile(path.join(args.webroot, key), 'utf8', done); +}; + +module.exports.removeChallenge = function (args, key, done) { + //var hostname = args.domains[0]; + + fs.unlinkSync(path.join(args.webroot, key), done); }; diff --git a/lib/default-remove-challenge.js b/lib/default-remove-challenge.js deleted file mode 100644 index 1aedfdd..0000000 --- a/lib/default-remove-challenge.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -var path = require('path'); -var fs = require('fs'); - -module.exports = function (args, key, done) { - //var hostname = args.domains[0]; - - fs.unlinkSync(path.join(args.webroot, key), done); -}; diff --git a/lib/default-set-challenge.js b/lib/default-set-challenge.js deleted file mode 100644 index 4aec345..0000000 --- a/lib/default-set-challenge.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); - -module.exports = function (args, challengePath, keyAuthorization, done) { - //var hostname = args.domains[0]; - var mkdirp = require('mkdirp'); - - // TODO should be args.webrootPath - mkdirp(path.join(args.webrootPath, challengePath), function (err) { - if (err) { - done(err); - return; - } - - fs.writeFile(path.join(args.webrootPath, challengePath), keyAuthorization, 'utf8', function (err) { - done(err); - }); - }); -}; diff --git a/package.json b/package.json index 4255ed2..bc441d3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ }, "dependencies": { "bluebird": "^3.0.6", + "mkdirp": "^0.5.1", "pyconf": "^1.0.0", + "request": "^2.67.0", "safe-replace": "^1.0.0", "serve-static": "^1.10.0" } From c38d44a69f209a47b032b2d95fd3a131baa7e06c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 12:01:46 +0000 Subject: [PATCH 09/25] typo fix --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 32fa3e0..ac3085f 100644 --- a/index.js +++ b/index.js @@ -51,7 +51,7 @@ LE.create = function (backend, defaults, handlers) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); } - handlers.removeChallenge = require('./lib/default-handlers').remove-Challenge; + handlers.removeChallenge = require('./lib/default-handlers').removeChallenge; } if (!handlers.agreeToTerms) { if (defaults.agreeTos) { From 03826d845d9f1d043f17fa73caca38b68d55dd22 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 12:12:15 +0000 Subject: [PATCH 10/25] can register new accounts --- backends/ursa.js | 12 +++++++----- examples/config-minimal.js | 2 +- index.js | 5 +++-- package.json | 1 + 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backends/ursa.js b/backends/ursa.js index e22989d..ac4b251 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -123,6 +123,7 @@ function createAccount(args, handlers) { } function getAccount(args, accountId) { + console.log(args.accountsDir, accountId); var accountDir = path.join(args.accountsDir, accountId); var files = {}; var configs = ['meta.json', 'private_key.json', 'regr.json']; @@ -174,7 +175,7 @@ function getAccountByEmail(args) { } module.exports.create = function (defaults, opts) { - var LE = require('./'); + var LE = require('../'); var pyconf = PromiseA.promisifyAll(require('pyconf')); if (!opts) { @@ -191,10 +192,10 @@ module.exports.create = function (defaults, opts) { var wrapped = { registerAsync: function (args) { args.server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory - var hostname = require('url').parse(args.server).hostname; + var acmeHostname = require('url').parse(args.server).hostname; var configDir = args.configDir || defaults.configDir || LE.configDir; - args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', hostname + '.conf'); - args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', hostname, 'directory'); + args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); + args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); pyconf.readFileAsync(args.renewalDir).then(function (renewal) { return renewal.account; @@ -203,7 +204,7 @@ module.exports.create = function (defaults, opts) { return getAccountByEmail(args); } - return err; + return PromiseA.reject(err); }).then(function (accountId) { // Note: the ACME urls are always fetched fresh on purpose return getAcmeUrls(args).then(function (urls) { @@ -216,6 +217,7 @@ module.exports.create = function (defaults, opts) { } }); }).then(function (account) { + console.log(account); throw new Error("IMPLEMENTATION NOT COMPLETE"); }); /* diff --git a/examples/config-minimal.js b/examples/config-minimal.js index 2a88cb9..3708bcc 100644 --- a/examples/config-minimal.js +++ b/examples/config-minimal.js @@ -2,7 +2,7 @@ var path = require('path'); -var binpath = require('os').homedir() + '/.local/share/letsencrypt/bin/letsencrypt'; +var binpath = require('homedir') + '/.local/share/letsencrypt/bin/letsencrypt'; var config = { diff --git a/index.js b/index.js index ac3085f..8fee0af 100644 --- a/index.js +++ b/index.js @@ -57,7 +57,7 @@ LE.create = function (backend, defaults, handlers) { if (defaults.agreeTos) { console.warn("[WARN] Agreeing to terms by default is risky business..."); } - handlers.removeChallenge = require('lib/default-handlers').agreeToTerms; + handlers.removeChallenge = require('./lib/default-handlers').agreeToTerms; } if ('function' === typeof backend.create) { backend = backend.create(defaults, handlers); @@ -119,7 +119,8 @@ LE.create = function (backend, defaults, handlers) { } le = { - validate: function (hostnames, cb) { + backend: backend + , validate: function (hostnames, cb) { // TODO check dns, etc if ((!hostnames.length && hostnames.every(le.isValidDomain))) { cb(new Error("node-letsencrypt: invalid hostnames: " + hostnames.join(','))); diff --git a/package.json b/package.json index bc441d3..9ab1dcf 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "bluebird": "^3.0.6", + "homedir": "^0.6.0", "mkdirp": "^0.5.1", "pyconf": "^1.0.0", "request": "^2.67.0", From bdeb1357d4803abe971226c328c4a16e0401c4d0 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 12:45:36 +0000 Subject: [PATCH 11/25] updates --- backends/ursa.js | 91 ++++++++++++++++++---------------------- lib/crypto-utils-ursa.js | 11 +++-- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/backends/ursa.js b/backends/ursa.js index ac4b251..e4ef705 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -51,42 +51,19 @@ function createAccount(args, handlers) { // TODO support ECDSA // arg.rsaBitLength args.rsaExponent - return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (obj) { - /* obj = { privateKeyPem, publicKeyPem, publicKeyMd5 } */ + return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { + /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ - var accountId = obj.publicKeyMd5; // I would have chosen sha1 or sha2... but whatever - var accountDir = path.join(args.accountsDir, accountId); - var isoDate = new Date().toISOString(); - - /* - files.accountId = accountId; // md5sum(publicKeyPem) - files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... - files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... - files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } - */ - - // TODO register return lef.registerNewAccountAsync({ email: args.email - , domains: Array.isArray(args.domains) || (args.domains||'').split(',') , newReg: args.server , debug: args.debug || handlers.debug - , webroot: args.webrootPath - , setChallenge: function (domain, key, value, done) { - args.domains = [domain]; - handlers.setChallenge(args, key, value, done); - } - , removeChallenge: function (domain, key, done) { - args.domains = [domain]; - handlers.removeChallenge(args, key, done); - } , agreeToTerms: function (tosUrl, agree) { - // args.email = email; + // args.email = email; // already there args.tosUrl = tosUrl; handlers.agreeToTerms(args, agree); } - // TODO send either privateKeyPem or privateKeyJson or privateKeyJwk (?) - , privateKeyPem: obj.privateKeyPem + , accountPrivateKeyPem: pems.privateKeyPem }).then(function (body) { if ('string' === typeof body) { try { @@ -95,34 +72,41 @@ function createAccount(args, handlers) { // ignore } } + return mkdirpAsync(args.accountDir, function () { - var jwk = cutils.toAcmePrivateKey(obj.privateKeyPem); + + var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); + var isoDate = new Date().toISOString(); + var accountMeta = { + creation_host: localname + , creation_dt: isoDate + }; + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } - // regr.json { "body" } + // regr.json: /* { body: { contact: [ 'mailto:coolaj86@gmail.com' ], agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', - key: - { e: 'AQAB', - kty: 'RSA', - n: '...' } }, + key: { e: 'AQAB', kty: 'RSA', n: '...' } }, uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } */ return PromiseA.all([ - fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify({ creation_host: localname, creation_dt: isoDate }), 'utf8') - , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(jwk), 'utf8') + fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') + , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8') , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') - ]); + ]).then(function () { + return pems; + }); }); }); }); } -function getAccount(args, accountId) { +function getAccount(accountId, args, handlers) { console.log(args.accountsDir, accountId); var accountDir = path.join(args.accountsDir, accountId); var files = {}; @@ -143,7 +127,7 @@ function getAccount(args, accountId) { files[keyname] = data; }, function (err) { - files[keyname] = err; + files[keyname] = { error: err }; }); })).then(function () { @@ -151,11 +135,12 @@ function getAccount(args, accountId) { return !files[key].error; })) { console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); - return createAccount(args); + return createAccount(args, handlers); } return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { files.accountId = accountId; // md5sum(publicKeyPem) + files.publicKeyMd5 = accountId; // md5sum(publicKeyPem) files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN... files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc } @@ -174,19 +159,10 @@ function getAccountByEmail(args) { return PromiseA.resolve(null); } -module.exports.create = function (defaults, opts) { +module.exports.create = function (defaults, handlers) { var LE = require('../'); var pyconf = PromiseA.promisifyAll(require('pyconf')); - if (!opts) { - opts = {}; - } - - /* - defaults.webroot = true; - defaults.renewByDefault = true; - defaults.text = true; - */ defaults.server = defaults.server || LE.liveServer; var wrapped = { @@ -201,7 +177,7 @@ module.exports.create = function (defaults, opts) { return renewal.account; }, function (err) { if ("EENOENT" === err.code) { - return getAccountByEmail(args); + return getAccountByEmail(args, handlers); } return PromiseA.reject(err); @@ -211,12 +187,25 @@ module.exports.create = function (defaults, opts) { args._acmeUrls = urls; if (accountId) { - return getAccount(args, accountId); + return getAccount(accountId, args, handlers); } else { return createAccount(args); } }); }).then(function (account) { + /* + , domains: Array.isArray(args.domains) || (args.domains||'').split(',') + , webroot: args.webrootPath + , accountPrivateKeyPem: obj.privateKeyPem + , setChallenge: function (domain, key, value, done) { + args.domains = [domain]; + handlers.setChallenge(args, key, value, done); + } + , removeChallenge: function (domain, key, done) { + args.domains = [domain]; + handlers.removeChallenge(args, key, done); + } + */ console.log(account); throw new Error("IMPLEMENTATION NOT COMPLETE"); }); diff --git a/lib/crypto-utils-ursa.js b/lib/crypto-utils-ursa.js index 55fd1ea..c5ed3e2 100644 --- a/lib/crypto-utils-ursa.js +++ b/lib/crypto-utils-ursa.js @@ -27,14 +27,17 @@ function toAcmePrivateKey(privkeyPem) { function generateRsaKeypair(bitlen, exp, cb) { var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/); var pems = { - publicKeyPem: keypair.toPublicPem() - , privateKeyPem: keypair.toPrivatePem() + publicKeyPem: keypair.toPublicPem() // ascii PEM: ----BEGIN... + , privateKeyPem: keypair.toPrivatePem() // ascii PEM: ----BEGIN... }; + // I would have chosen sha1 or sha2... but whatever pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex'); + // json { n: ..., e: ..., iq: ..., etc } + pems.privateKeyJwk = toAcmePrivateKey(pems.privateKeyPem); + pems.privateKeyJson = pems.privateKeyJwk; + // TODO thumbprint - // TODO jwk - // TODO json cb(null, pems); } From c44935e7df14e286fd38778a7046a5a288509dbf Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 13:11:19 +0000 Subject: [PATCH 12/25] more working --- backends/ursa.js | 20 ++++++++++++++++---- lib/crypto-utils-ursa.js | 6 +++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backends/ursa.js b/backends/ursa.js index e4ef705..2798dbd 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -22,14 +22,26 @@ function getAcmeUrls(args) { return requestAsync({ url: args.server - }).then(function (data) { + }).then(function (resp) { + var data = resp.body; + + if ('string' === typeof data) { + try { + data = JSON.parse(data); + } catch(e) { + return PromiseA.reject(e); + } + } + if (4 !== Object.keys(data).length) { console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand"); + console.warn(data); } if (!knownUrls.every(function (url) { return data[url]; })) { console.warn("This Let's Encrypt / ACME server is missing urls that this client may need."); + console.warn(data); } ipc.acmeUrlsUpdatedAt = Date.now(); @@ -56,7 +68,7 @@ function createAccount(args, handlers) { return lef.registerNewAccountAsync({ email: args.email - , newReg: args.server + , newReg: args._acmeUrls.newReg , debug: args.debug || handlers.debug , agreeToTerms: function (tosUrl, agree) { // args.email = email; // already there @@ -173,7 +185,7 @@ module.exports.create = function (defaults, handlers) { args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); - pyconf.readFileAsync(args.renewalDir).then(function (renewal) { + return pyconf.readFileAsync(args.renewalDir).then(function (renewal) { return renewal.account; }, function (err) { if ("EENOENT" === err.code) { @@ -189,7 +201,7 @@ module.exports.create = function (defaults, handlers) { if (accountId) { return getAccount(accountId, args, handlers); } else { - return createAccount(args); + return createAccount(args, handlers); } }); }).then(function (account) { diff --git a/lib/crypto-utils-ursa.js b/lib/crypto-utils-ursa.js index c5ed3e2..f9e9de6 100644 --- a/lib/crypto-utils-ursa.js +++ b/lib/crypto-utils-ursa.js @@ -27,8 +27,8 @@ function toAcmePrivateKey(privkeyPem) { function generateRsaKeypair(bitlen, exp, cb) { var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/); var pems = { - publicKeyPem: keypair.toPublicPem() // ascii PEM: ----BEGIN... - , privateKeyPem: keypair.toPrivatePem() // ascii PEM: ----BEGIN... + publicKeyPem: keypair.toPublicPem().toString('ascii') // ascii PEM: ----BEGIN... + , privateKeyPem: keypair.toPrivatePem().toString('ascii') // ascii PEM: ----BEGIN... }; // I would have chosen sha1 or sha2... but whatever @@ -67,7 +67,7 @@ function parseAccountPrivateKey(pkj, cb) { cb(null, { privateKeyPem: priv.toPrivatePem.toString('ascii') - , publicKeyPem: priv.toPrivatePem.toString('ascii') + , publicKeyPem: priv.toPublicPem.toString('ascii') }); } From 278125bfd975c2a60c9ba2ee95a7c28d33bb4c72 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 13:12:16 +0000 Subject: [PATCH 13/25] typo fix --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 8fee0af..641ca1d 100644 --- a/index.js +++ b/index.js @@ -57,7 +57,7 @@ LE.create = function (backend, defaults, handlers) { if (defaults.agreeTos) { console.warn("[WARN] Agreeing to terms by default is risky business..."); } - handlers.removeChallenge = require('./lib/default-handlers').agreeToTerms; + handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms; } if ('function' === typeof backend.create) { backend = backend.create(defaults, handlers); From 3af1523a3543e10ab7767c44b6644340c2059a7d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 13:13:52 +0000 Subject: [PATCH 14/25] typo fix --- lib/default-handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/default-handlers.js b/lib/default-handlers.js index 4becc64..d7859cd 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -4,7 +4,7 @@ var fs = require('fs'); var path = require('path'); module.exports.agreeToTerms = function (args, agree) { - agree(args.agreeTos); + agree(null, args.agreeTos); }; module.exports.setChallenge = function (args, challengePath, keyAuthorization, done) { From 65ea5fbe2ab783f5102c972c9e157c362b2f3934 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 13:21:26 +0000 Subject: [PATCH 15/25] one step closer --- backends/ursa.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backends/ursa.js b/backends/ursa.js index 2798dbd..b9f7276 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -77,6 +77,9 @@ function createAccount(args, handlers) { } , accountPrivateKeyPem: pems.privateKeyPem }).then(function (body) { + if (body instanceof Buffer) { + body = body.toString('utf8'); + } if ('string' === typeof body) { try { body = JSON.parse(body); @@ -85,9 +88,10 @@ function createAccount(args, handlers) { } } - return mkdirpAsync(args.accountDir, function () { + var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); + + return mkdirpAsync(accountDir).then(function () { - var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); var isoDate = new Date().toISOString(); var accountMeta = { creation_host: localname @@ -119,7 +123,6 @@ function createAccount(args, handlers) { } function getAccount(accountId, args, handlers) { - console.log(args.accountsDir, accountId); var accountDir = path.join(args.accountsDir, accountId); var files = {}; var configs = ['meta.json', 'private_key.json', 'regr.json']; From 9d8da8331d0cffaaba2c73b75df2a9c79754ab92 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 13:28:52 +0000 Subject: [PATCH 16/25] use backend directly --- examples/ursa.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/ursa.js b/examples/ursa.js index d86562c..4a9feaf 100644 --- a/examples/ursa.js +++ b/examples/ursa.js @@ -12,20 +12,22 @@ config.le.server = LE.stagingServer; // Manual Registration // var le = LE.create(require('../backends/ursa'), config.le); -le.register({ +le.backend.registerAsync({ agreeTos: true -, domains: ['example.com'] // CHANGE TO YOUR DOMAIN -, email: 'user@email.com' // CHANGE TO YOUR EMAIL -}, function (err) { +, domains: ['example.com'] // CHANGE TO YOUR DOMAIN +, email: 'user@example.com' // CHANGE TO YOUR EMAIL +}, function (err, body) { if (err) { console.error('[Error]: node-letsencrypt/examples/ursa'); console.error(err.stack); } else { - console.log('success'); + console.log('success', body); } plainServer.close(); tlsServer.close(); +}).then(function () {}, function (err) { + console.error(err.stack); }); // From 8944048d8248150341310612e298c5e6a1b16d1e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 15:21:27 +0000 Subject: [PATCH 17/25] nearly all working --- backends/common.js | 26 ++++ backends/python.js | 20 +--- backends/ursa.js | 258 ++++++++++++++++++++++++++++------------ lib/default-handlers.js | 4 +- 4 files changed, 210 insertions(+), 98 deletions(-) create mode 100644 backends/common.js diff --git a/backends/common.js b/backends/common.js new file mode 100644 index 0000000..7be610b --- /dev/null +++ b/backends/common.js @@ -0,0 +1,26 @@ +'use strict'; + +var fs = require('fs'); +var PromiseA = require('bluebird'); + +module.exports.fetchFromDisk = function (args, defaults) { + var hostname = args.domains[0]; + var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); + var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); + + return PromiseA.all([ + fs.readFileAsync(privpath, 'ascii') + , fs.readFileAsync(crtpath, 'ascii') + // stat the file, not the link + , fs.statAsync(crtpath) + ]).then(function (arr) { + return { + key: arr[0] // privkey.pem + , cert: arr[1] // fullchain.pem + // TODO parse centificate for lifetime / expiresAt + , issuedAt: arr[2].mtime.valueOf() + }; + }, function () { + return null; + }); +}; diff --git a/backends/python.js b/backends/python.js index 5df646b..7f889b0 100644 --- a/backends/python.js +++ b/backends/python.js @@ -26,25 +26,7 @@ module.exports.create = function (defaults, opts, extra) { return lep.registerAsync('certonly', args); } , fetchAsync: function (args) { - var hostname = args.domains[0]; - var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); - var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); - - return PromiseA.all([ - fs.readFileAsync(privpath, 'ascii') - , fs.readFileAsync(crtpath, 'ascii') - // stat the file, not the link - , fs.statAsync(crtpath) - ]).then(function (arr) { - return { - key: arr[0] // privkey.pem - , cert: arr[1] // fullchain.pem - // TODO parse centificate for lifetime / expiresAt - , issuedAt: arr[2].mtime.valueOf() - }; - }, function () { - return null; - }); + require('./common').fetchFromDisk(args, defaults); } }; diff --git a/backends/ursa.js b/backends/ursa.js index b9f7276..1e692e3 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -3,15 +3,17 @@ var PromiseA = require('bluebird'); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); -var cutils = PromiseA.promisifyAll(require('../lib/crypto-utils-ursa')); -//var futils = require('letsencrypt-forge/lib/crypto-utils'); var requestAsync = PromiseA.promisify(require('request')); -var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); + +var LE = require('../'); var knownUrls = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert']; +var ucrypto = PromiseA.promisifyAll(require('../lib/crypto-utils-ursa')); +//var fcrypto = PromiseA.promisifyAll(require('../lib/crypto-utils-forge')); +var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); +var fetchFromConfigLiveDir = require('./common').fetchFromDisk; var ipc = {}; // in-process cache -//function noop() {} function getAcmeUrls(args) { var now = Date.now(); @@ -63,7 +65,7 @@ function createAccount(args, handlers) { // TODO support ECDSA // arg.rsaBitLength args.rsaExponent - return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { + return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ return lef.registerNewAccountAsync({ @@ -153,7 +155,7 @@ function getAccount(accountId, args, handlers) { return createAccount(args, handlers); } - return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { + return ucrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { files.accountId = accountId; // md5sum(publicKeyPem) files.publicKeyMd5 = accountId; // md5sum(publicKeyPem) files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... @@ -174,86 +176,186 @@ function getAccountByEmail(args) { return PromiseA.resolve(null); } -module.exports.create = function (defaults, handlers) { - var LE = require('../'); +function getCertificateAsync(account, args, defaults, handlers) { var pyconf = PromiseA.promisifyAll(require('pyconf')); + return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { + return lef.getCertificateAsync({ + domains: args.domains + , accountPrivateKeyPem: account.privateKeyPem + , domainPrivateKeyPem: domain.privateKeyPem + , setChallenge: function (domain, key, value, done) { + args.domains = [domain]; + args.webrootPath = args.webrootPath || defaults.webrootPath; + handlers.setChallenge(args, key, value, done); + } + , removeChallenge: function (domain, key, done) { + args.domains = [domain]; + args.webrootPath = args.webrootPath || defaults.webrootPath; + handlers.removeChallenge(args, key, done); + } + , newAuthorizationUrl: args._acmeUrls.newAuthz + , newCertificateUrl: args._acmeUrls.newCert + }).then(function (result) { + console.log(result); + throw new Error("IMPLEMENTATION NOT COMPLETE"); + }); + }); +} + +function registerWithAcme(args, defaults, handlers) { + var pyconf = PromiseA.promisifyAll(require('pyconf')); + var server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory + var acmeHostname = require('url').parse(server).hostname; + var configDir = args.configDir || defaults.configDir || LE.configDir; + + args.server = server; + args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); + args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); + + return pyconf.readFileAsync(args.renewalDir).then(function (renewal) { + var accountId = renewal.account; + renewal = renewal.account; + + return accountId; + }, function (err) { + if ("EENOENT" === err.code) { + return getAccountByEmail(args, handlers); + } + + return PromiseA.reject(err); + }).then(function (accountId) { + // Note: the ACME urls are always fetched fresh on purpose + return getAcmeUrls(args).then(function (urls) { + args._acmeUrls = urls; + + if (accountId) { + return getAccount(accountId, args, handlers); + } else { + return createAccount(args, handlers); + } + }); + }).then(function (account) { + /* + if (renewal.account !== account) { + // the account has become corrupt, re-register + return; + } + */ + + console.log(account); + return fetchFromConfigLiveDir(args, defaults).then(function (certs) { + // if nothing, register and save + // if something, check date (don't register unless 30+ days) + // if good, don't bother registering + // (but if we get to the point that we're actually calling + // this function, that shouldn't be the case, right?) + console.log(certs); + if (!certs) { + // no certs, seems like a good time to get some + return getCertificateAsync(account, args, defaults, handlers); + } + else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) { + // cert is at least 27 days old we can renew that + return getCertificateAsync(account, args, defaults, handlers); + } + else if (args.force) { + // YOLO! I be gettin' fresh certs 'erday! Yo! + return getCertificateAsync(account, args, defaults, handlers); + } + else { + console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.force to force.'); + // We're happy with what we have + return certs; + } + }); + + /* + cert = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/cert.pem + privkey = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/privkey.pem + chain = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/chain.pem + fullchain = /home/aj/node-letsencrypt/tests/letsencrypt.config/live/lds.io/fullchain.pem + + # Options and defaults used in the renewal process + [renewalparams] + apache_enmod = a2enmod + no_verify_ssl = False + ifaces = None + apache_dismod = a2dismod + register_unsafely_without_email = False + uir = None + installer = none + config_dir = /home/aj/node-letsencrypt/tests/letsencrypt.config + text_mode = True + func = + prepare = False + work_dir = /home/aj/node-letsencrypt/tests/letsencrypt.work + tos = True + init = False + http01_port = 80 + duplicate = False + key_path = None + nginx = False + fullchain_path = /home/aj/node-letsencrypt/chain.pem + email = coolaj86@gmail.com + csr = None + agree_dev_preview = None + redirect = None + verbose_count = -3 + config_file = None + renew_by_default = True + hsts = False + authenticator = webroot + domains = lds.io, + rsa_key_size = 2048 + checkpoints = 1 + manual_test_mode = False + apache = False + cert_path = /home/aj/node-letsencrypt/cert.pem + webroot_path = /home/aj/node-letsencrypt/examples/../tests/acme-challenge, + strict_permissions = False + apache_server_root = /etc/apache2 + account = 1c41c64dfaf10d511db8aef0cc33b27f + manual_public_ip_logging_ok = False + chain_path = /home/aj/node-letsencrypt/chain.pem + standalone = False + manual = False + server = https://acme-staging.api.letsencrypt.org/directory + standalone_supported_challenges = "http-01,tls-sni-01" + webroot = True + apache_init_script = None + user_agent = None + apache_ctl = apache2ctl + apache_le_vhost_ext = -le-ssl.conf + debug = False + tls_sni_01_port = 443 + logs_dir = /home/aj/node-letsencrypt/tests/letsencrypt.logs + configurator = None + [[webroot_map]] + lds.io = /home/aj/node-letsencrypt/examples/../tests/acme-challenge + */ + }); +/* + return fs.readdirAsync(accountsDir, function (nodes) { + return PromiseA.all(nodes.map(function (node) { + var reMd5 = /[a-f0-9]{32}/i; + if (reMd5.test(node)) { + } + })); + }); +*/ +} + +module.exports.create = function (defaults, handlers) { defaults.server = defaults.server || LE.liveServer; var wrapped = { registerAsync: function (args) { - args.server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory - var acmeHostname = require('url').parse(args.server).hostname; - var configDir = args.configDir || defaults.configDir || LE.configDir; - args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); - args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); - - return pyconf.readFileAsync(args.renewalDir).then(function (renewal) { - return renewal.account; - }, function (err) { - if ("EENOENT" === err.code) { - return getAccountByEmail(args, handlers); - } - - return PromiseA.reject(err); - }).then(function (accountId) { - // Note: the ACME urls are always fetched fresh on purpose - return getAcmeUrls(args).then(function (urls) { - args._acmeUrls = urls; - - if (accountId) { - return getAccount(accountId, args, handlers); - } else { - return createAccount(args, handlers); - } - }); - }).then(function (account) { - /* - , domains: Array.isArray(args.domains) || (args.domains||'').split(',') - , webroot: args.webrootPath - , accountPrivateKeyPem: obj.privateKeyPem - , setChallenge: function (domain, key, value, done) { - args.domains = [domain]; - handlers.setChallenge(args, key, value, done); - } - , removeChallenge: function (domain, key, done) { - args.domains = [domain]; - handlers.removeChallenge(args, key, done); - } - */ - console.log(account); - throw new Error("IMPLEMENTATION NOT COMPLETE"); - }); -/* - return fs.readdirAsync(accountsDir, function (nodes) { - return PromiseA.all(nodes.map(function (node) { - var reMd5 = /[a-f0-9]{32}/i; - if (reMd5.test(node)) { - } - })); - }); -*/ + //require('./common').registerWithAcme(args, defaults, handlers); + return registerWithAcme(args, defaults, handlers); } , fetchAsync: function (args) { - var hostname = args.domains[0]; - var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); - var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); - - return PromiseA.all([ - fs.readFileAsync(privpath, 'ascii') - , fs.readFileAsync(crtpath, 'ascii') - // stat the file, not the link - , fs.statAsync(crtpath) - ]).then(function (arr) { - return { - key: arr[0] // privkey.pem - , cert: arr[1] // fullchain.pem - // TODO parse centificate for lifetime / expiresAt - , issuedAt: arr[2].mtime.valueOf() - }; - }, function () { - return null; - }); + return fetchFromConfigLiveDir(args, defaults); } }; diff --git a/lib/default-handlers.js b/lib/default-handlers.js index d7859cd..a07782e 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -12,6 +12,8 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d var mkdirp = require('mkdirp'); // TODO should be args.webrootPath + console.log('args.webrootPath, challengePath'); + console.log(args.webrootPath, challengePath); mkdirp(path.join(args.webrootPath, challengePath), function (err) { if (err) { done(err); @@ -33,5 +35,5 @@ module.exports.getChallenge = function (args, key, done) { module.exports.removeChallenge = function (args, key, done) { //var hostname = args.domains[0]; - fs.unlinkSync(path.join(args.webroot, key), done); + fs.unlinkSync(path.join(args.webrootPath, key), done); }; From 3e6fecf008ec751e1d627119aed0b40ebea84e11 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 15:23:05 +0000 Subject: [PATCH 18/25] fix bad path.join --- lib/default-handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/default-handlers.js b/lib/default-handlers.js index a07782e..6c35e46 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -14,7 +14,7 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d // TODO should be args.webrootPath console.log('args.webrootPath, challengePath'); console.log(args.webrootPath, challengePath); - mkdirp(path.join(args.webrootPath, challengePath), function (err) { + mkdirp(args.webrootPath, function (err) { if (err) { done(err); return; From c48de554c2d83e23d648dce8da86a5e4a553f3de Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 15 Dec 2015 15:40:44 +0000 Subject: [PATCH 19/25] Certificate registration completes successfully! :-) --- backends/ursa.js | 5 +++++ index.js | 32 ++++++++++++++++++++++++++++---- lib/default-handlers.js | 3 ++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/backends/ursa.js b/backends/ursa.js index 1e692e3..53874d8 100644 --- a/backends/ursa.js +++ b/backends/ursa.js @@ -184,6 +184,11 @@ function getCertificateAsync(account, args, defaults, handlers) { domains: args.domains , accountPrivateKeyPem: account.privateKeyPem , domainPrivateKeyPem: domain.privateKeyPem + , getChallenge: function (domain, key, done) { + args.domains = [domain]; + args.webrootPath = args.webrootPath || defaults.webrootPath; + handlers.getChallenge(args, key, done); + } , setChallenge: function (domain, key, value, done) { args.domains = [domain]; args.webrootPath = args.webrootPath || defaults.webrootPath; diff --git a/index.js b/index.js index 641ca1d..dc0b80b 100644 --- a/index.js +++ b/index.js @@ -39,6 +39,22 @@ LE.create = function (backend, defaults, handlers) { cb(null, null); }; } + if (!handlers.getChallenge) { + if (!defaults.webrootPath) { + // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} + throw new Error("handlers.getChallenge or defaults.webrootPath must be set"); + } + handlers.getChallenge = function (hostname, key, done) { + // TODO associate by hostname? + // hmm... I don't think there's a direct way to associate this with + // the request it came from... it's kinda stateless in that way + // but realistically there only needs to be one handler and one + // "directory" for this. It's not that big of a deal. + var defaultos = LE.merge(defaults, {}); + defaultos.domains = [hostname]; + require('./lib/default-handlers').getChallenge(defaultos, key, done); + }; + } if (!handlers.setChallenge) { if (!defaults.webrootPath) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} @@ -49,7 +65,7 @@ LE.create = function (backend, defaults, handlers) { if (!handlers.removeChallenge) { if (!defaults.webrootPath) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} - throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); + throw new Error("handlers.removeChallenge or defaults.webrootPath must be set"); } handlers.removeChallenge = require('./lib/default-handlers').removeChallenge; } @@ -143,17 +159,25 @@ LE.create = function (backend, defaults, handlers) { cb(null, true); } , middleware: function () { - //console.log('[DEBUG] webrootPath', defaults.webrootPath); - var serveStatic = require('serve-static')(defaults.webrootPath, { dotfiles: 'allow' }); var prefix = '/.well-known/acme-challenge/'; return function (req, res, next) { if (0 !== req.url.indexOf(prefix)) { + console.log('[LE middleware]: pass'); next(); return; } - serveStatic(req, res, next); + //args.domains = [req.hostname]; + console.log('[LE middleware]:', req.hostname, req.url, req.url.slice(prefix.length)); + handlers.getChallenge(req.hostname, req.url.slice(prefix.length), function (err, token) { + if (err) { + res.send("Error: These aren't the tokens you're looking for. Move along."); + return; + } + + res.send(token); + }); }; } , SNICallback: sniCallback diff --git a/lib/default-handlers.js b/lib/default-handlers.js index 6c35e46..a4bcacc 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -29,7 +29,8 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d module.exports.getChallenge = function (args, key, done) { //var hostname = args.domains[0]; - fs.readFile(path.join(args.webroot, key), 'utf8', done); + console.log("getting the challenge", args, key); + fs.readFile(path.join(args.webrootPath, key), 'utf8', done); }; module.exports.removeChallenge = function (args, key, done) { From 3151ec392217a590048975f99283bf5891de5a22 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 01:11:31 -0800 Subject: [PATCH 20/25] gutting python --- README.md | 49 ++++--------- backends/python.js | 34 --------- examples/commandline-minimal.js | 2 +- examples/commandline.js | 4 +- examples/config-minimal.js | 3 - examples/express-minimal.js | 2 +- examples/express.js | 7 -- index.js | 90 +++++++++++++---------- {backends => lib}/common.js | 0 backends/ursa.js => lib/letiny-core.js | 99 ++++++++++++-------------- package.json | 7 +- 11 files changed, 112 insertions(+), 185 deletions(-) delete mode 100644 backends/python.js rename {backends => lib}/common.js (100%) rename backends/ursa.js => lib/letiny-core.js (84%) diff --git a/README.md b/README.md index 83e7afe..ff2f40f 100644 --- a/README.md +++ b/README.md @@ -263,52 +263,27 @@ and then make sure to set all of of the following to a directory that your user * `webrootPath` * `configDir` -* `workDir` (python backend only) -* `logsDir` (python backend only) API === ```javascript -LetsEncrypt.create(backend, bkDefaults, handlers) // wraps a given "backend" (the python client) -LetsEncrypt.stagingServer // string of staging server for testing +LetsEncrypt.init(leConfig, handlers) // wraps a given +LetsEncrypt.create(backend, leConfig, handlers) // wraps a given "backend" (the python or node client) +LetsEncrypt.stagingServer // string of staging server for testing -le.middleware() // middleware for serving webrootPath to /.well-known/acme-challenge -le.sniCallback(hostname, function (err, tlsContext) {}) // uses fetch (below) and formats for https.SNICallback -le.register({ domains, email, agreeTos, ... }, cb) // registers or renews certs for a domain -le.fetch({domains, email, agreeTos, ... }, cb) // fetches certs from in-memory cache, occasionally refreshes from disk -le.validate(domains, cb) // do some sanity checks before attempting to register -le.registrationFailureCallback(err, args, certInfo, cb) // called when registration fails (not implemented yet) +le.middleware() // middleware for serving webrootPath to /.well-known/acme-challenge +le.sniCallback(hostname, function (err, tlsContext) {}) // uses fetch (below) and formats for https.SNICallback +le.register({ domains, email, agreeTos, ... }, cb) // registers or renews certs for a domain +le.fetch({domains, email, agreeTos, ... }, cb) // fetches certs from in-memory cache, occasionally refreshes from disk +le.validate(domains, cb) // do some sanity checks before attempting to register +le.registrationFailureCallback(err, args, certInfo, cb) // called when registration fails (not implemented yet) ``` -### `LetsEncrypt.create(backend, bkDefaults, handlers)` +### `LetsEncrypt.create(backend, leConfig, handlers)` -#### backend - -Currently only `letsencrypt-python` is supported, but we plan to work on -native javascript support in February or so (when ECDSA keys are available). - -If you'd like to help with that, see **how to write a backend** below and also -look at the wrapper `backend-python.js`. - -**Example**: -```javascript -{ fetch: function (args, cb) { - // cb(err) when there is an actual error (db, fs, etc) - // cb(null, null) when the certificate was NOT available on disk - // cb(null, { cert: '', key: '', renewedAt: 0, duration: 0 }) cert + meta - } -, register: function (args, setChallenge, cb) { - // setChallenge(hostnames, key, value, cb) when a challenge needs to be set - // cb(err) when there is an error - // cb(null, null) when the registration is successful, but fetch still needs to be called - // cb(null, cert /*see above*/) if registration can easily return the same as fetch - } -} -``` - -#### bkDefaults +#### leConfig The arguments passed here (typically `webpathRoot`, `configDir`, etc) will be merged with any `args` (typically `domains`, `email`, and `agreeTos`) and passed to the backend whenever @@ -326,7 +301,7 @@ Typically the backend wrapper will already merge any necessary backend-specific ``` Note: `webrootPath` can be set as a default, semi-locally with `webrootPathTpl`, or per -registration as `webrootPath` (which overwrites `defaults.webrootPath`). +registration as `webrootPath` (which overwrites `leConfig.webrootPath`). #### handlers *optional* diff --git a/backends/python.js b/backends/python.js deleted file mode 100644 index 7f889b0..0000000 --- a/backends/python.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -var PromiseA = require('bluebird'); -var fs = PromiseA.promisifyAll(require('fs')); - -module.exports.create = function (defaults, opts, extra) { - // v1.0.0 backwards compat - if (3 === arguments.length) { - opts.pythonClientPath = defaults; - defaults = opts; - opts = extra; - } - else if (2 !== arguments.length) { - throw new Error("Instead of creating the python backend yourself, just pass it to LE. See the README.md"); - } - - defaults.webroot = true; - defaults.renewByDefault = true; - defaults.text = true; - - var leBinPath = defaults.pythonClientPath; - var LEP = require('letsencrypt-python'); - var lep = PromiseA.promisifyAll(LEP.create(leBinPath, opts)); - var wrapped = { - registerAsync: function (args) { - return lep.registerAsync('certonly', args); - } - , fetchAsync: function (args) { - require('./common').fetchFromDisk(args, defaults); - } - }; - - return wrapped; -}; diff --git a/examples/commandline-minimal.js b/examples/commandline-minimal.js index 48dd923..5b63799 100644 --- a/examples/commandline-minimal.js +++ b/examples/commandline-minimal.js @@ -11,7 +11,7 @@ config.le.server = LE.stagingServer; // // Manual Registration // -var le = LE.create(config.backend, config.le); +var le = LE.create(config.le); le.register({ agreeTos: true , domains: ['example.com'] // CHANGE TO YOUR DOMAIN diff --git a/examples/commandline.js b/examples/commandline.js index 3ee6925..fe4ce8c 100644 --- a/examples/commandline.js +++ b/examples/commandline.js @@ -32,9 +32,7 @@ var bkDefaults = { , pythonClientPath: require('os').homedir() + '/.local/share/letsencrypt/bin/letsencrypt' }; -var LEP = require('../backends/python'); - -var le = LE.create(LEP, bkDefaults, { +var le = LE.create(bkDefaults, { /* setChallenge: function (hostnames, key, value, cb) { // the python backend needs fs.watch implemented diff --git a/examples/config-minimal.js b/examples/config-minimal.js index 3708bcc..bc4b26b 100644 --- a/examples/config-minimal.js +++ b/examples/config-minimal.js @@ -26,7 +26,4 @@ var config = { }; -//config.backend = require('letsencrypt/backends/python').create(binpath, config.le); -config.backend = require('../backends/python'); - module.exports = config; diff --git a/examples/express-minimal.js b/examples/express-minimal.js index dde1717..4e3cdba 100644 --- a/examples/express-minimal.js +++ b/examples/express-minimal.js @@ -7,7 +7,7 @@ var config = require('./config-minimal'); config.le.webrootPath = __dirname + '/../tests/acme-challenge'; config.le.server = LE.stagingServer; -var le = LE.create(config.backend, config.le, { +var le = LE.create(config.le, { sniRegisterCallback: function (args, expiredCert, cb) { // In theory you should never get an expired certificate because // the certificates automatically renew in the background starting diff --git a/examples/express.js b/examples/express.js index 81ade0e..356d583 100644 --- a/examples/express.js +++ b/examples/express.js @@ -24,15 +24,8 @@ var bkDefaults = { , privkeyTpl: '/live/:hostname/privkey.pem' , configDir: path.join(__dirname, '..', 'tests', 'letsencrypt.config') , server: LE.stagingServer - -// python-specific -, logsDir: path.join(__dirname, '..', 'tests', 'letsencrypt.logs') -, workDir: path.join(__dirname, '..', 'tests', 'letsencrypt.work') -, pythonClientPath: require('os').homedir() + '/.local/share/letsencrypt/bin/letsencrypt' }; -var LEP = require('../backends/python'); - var le = LE.create(LEP, bkDefaults, { sniRegisterCallback: function (args, certInfo, cb) { var allowedDomains = conf.domains; // require('../tests/config').allowedDomains; diff --git a/index.js b/index.js index dc0b80b..15e04fa 100644 --- a/index.js +++ b/index.js @@ -6,14 +6,18 @@ var PromiseA = require('bluebird'); var crypto = require('crypto'); var tls = require('tls'); var path = require('path'); +var leCore = require('./backends/letiny-core'); var LE = module.exports; +LE.productionServerUrl = leCore.productionServerUrl; +LE.stagingServer = leCore.stagingServerUrl; +LE.configDir = leCore.configDir; +LE.acmeChallengPrefix = leCore.acmeChallengPrefix; +LE.knownEndpoints = leCore.knownEndpoints; -LE.liveServer = "https://acme-v01.api.letsencrypt.org/directory"; -LE.stagingServer = "https://acme-staging.api.letsencrypt.org/directory"; -LE.configDir = "/etc/letsencrypt/"; -LE.logsDir = "/var/log/letsencrypt/"; -LE.workDir = "/var/lib/letsencrypt/"; +// backwards compat +LE.liveServer = leCore.productionServerUrl; +LE.knownUrls = leCore.knownEndpoints; LE.merge = function merge(defaults, args) { var copy = {}; @@ -28,7 +32,48 @@ LE.merge = function merge(defaults, args) { return copy; }; -LE.create = function (backend, defaults, handlers) { +LE.cacheCertInfo = function (args, certInfo, ipc, handlers) { + // TODO IPC via process and worker to guarantee no races + // rather than just "really good odds" + + var hostname = args.domains[0]; + var now = Date.now(); + + // Stagger randomly by plus 0% to 25% to prevent all caches expiring at once + var rnd1 = (crypto.randomBytes(1)[0] / 255); + var memorizeFor = Math.floor(handlers.memorizeFor + ((handlers.memorizeFor / 4) * rnd1)); + // Stagger randomly to renew between n and 2n days before renewal is due + // this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once + var rnd2 = (crypto.randomBytes(1)[0] / 255); + var bestIfUsedBy = certInfo.expiresAt - (handlers.renewWithin + Math.floor(handlers.renewWithin * rnd2)); + // Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes + // renewing at once on boot when the certs have expired + var rnd3 = (crypto.randomBytes(1)[0] / 255); + var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3); + + certInfo.context = tls.createSecureContext({ + key: certInfo.key + , cert: certInfo.cert + //, ciphers // node's defaults are great + }); + certInfo.loadedAt = now; + certInfo.memorizeFor = memorizeFor; + certInfo.bestIfUsedBy = bestIfUsedBy; + certInfo.renewTimeout = renewTimeout; + + ipc[hostname] = certInfo; + return ipc[hostname]; +}; + + // backend, defaults, handlers +LE.create = function (defaults, handlers, backend) { + var d, b, h; + // backwards compat for <= v1.0.2 + if (defaults.registerAsync || defaults.create) { + b = defaults; d = handlers; h = backend; + defaults = d; handlers = h; backend = b; + } + if (!backend) { backend = require('./lib/letiny-core'); } if (!handlers) { handlers = {}; } if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } @@ -324,36 +369,3 @@ LE.create = function (backend, defaults, handlers) { return le; }; - -LE.cacheCertInfo = function (args, certInfo, ipc, handlers) { - // TODO IPC via process and worker to guarantee no races - // rather than just "really good odds" - - var hostname = args.domains[0]; - var now = Date.now(); - - // Stagger randomly by plus 0% to 25% to prevent all caches expiring at once - var rnd1 = (crypto.randomBytes(1)[0] / 255); - var memorizeFor = Math.floor(handlers.memorizeFor + ((handlers.memorizeFor / 4) * rnd1)); - // Stagger randomly to renew between n and 2n days before renewal is due - // this *greatly* reduces the risk of multiple cluster processes renewing the same domain at once - var rnd2 = (crypto.randomBytes(1)[0] / 255); - var bestIfUsedBy = certInfo.expiresAt - (handlers.renewWithin + Math.floor(handlers.renewWithin * rnd2)); - // Stagger randomly by plus 0 to 5 min to reduce risk of multiple cluster processes - // renewing at once on boot when the certs have expired - var rnd3 = (crypto.randomBytes(1)[0] / 255); - var renewTimeout = Math.floor((5 * 60 * 1000) * rnd3); - - certInfo.context = tls.createSecureContext({ - key: certInfo.key - , cert: certInfo.cert - //, ciphers // node's defaults are great - }); - certInfo.loadedAt = now; - certInfo.memorizeFor = memorizeFor; - certInfo.bestIfUsedBy = bestIfUsedBy; - certInfo.renewTimeout = renewTimeout; - - ipc[hostname] = certInfo; - return ipc[hostname]; -}; diff --git a/backends/common.js b/lib/common.js similarity index 100% rename from backends/common.js rename to lib/common.js diff --git a/backends/ursa.js b/lib/letiny-core.js similarity index 84% rename from backends/ursa.js rename to lib/letiny-core.js index 53874d8..d436330 100644 --- a/backends/ursa.js +++ b/lib/letiny-core.js @@ -6,10 +6,10 @@ var fs = PromiseA.promisifyAll(require('fs')); var requestAsync = PromiseA.promisify(require('request')); var LE = require('../'); -var knownUrls = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert']; -var ucrypto = PromiseA.promisifyAll(require('../lib/crypto-utils-ursa')); +var LeCore = PromiseA.promisifyAll(require('letiny-core')); +var ucrypto = PromiseA.promisifyAll(LeCore.leCrypto); + //var fcrypto = PromiseA.promisifyAll(require('../lib/crypto-utils-forge')); -var lef = PromiseA.promisifyAll(require('letsencrypt-forge')); var fetchFromConfigLiveDir = require('./common').fetchFromDisk; var ipc = {}; // in-process cache @@ -22,30 +22,7 @@ function getAcmeUrls(args) { return PromiseA.resolve(ipc.acmeUrls); } - return requestAsync({ - url: args.server - }).then(function (resp) { - var data = resp.body; - - if ('string' === typeof data) { - try { - data = JSON.parse(data); - } catch(e) { - return PromiseA.reject(e); - } - } - - if (4 !== Object.keys(data).length) { - console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand"); - console.warn(data); - } - if (!knownUrls.every(function (url) { - return data[url]; - })) { - console.warn("This Let's Encrypt / ACME server is missing urls that this client may need."); - console.warn(data); - } - + return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { ipc.acmeUrlsUpdatedAt = Date.now(); ipc.acmeUrls = { newAuthz: data['new-authz'] @@ -68,7 +45,7 @@ function createAccount(args, handlers) { return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ - return lef.registerNewAccountAsync({ + return LeCore.registerNewAccountAsync({ email: args.email , newReg: args._acmeUrls.newReg , debug: args.debug || handlers.debug @@ -79,17 +56,6 @@ function createAccount(args, handlers) { } , accountPrivateKeyPem: pems.privateKeyPem }).then(function (body) { - if (body instanceof Buffer) { - body = body.toString('utf8'); - } - if ('string' === typeof body) { - try { - body = JSON.parse(body); - } catch(e) { - // ignore - } - } - var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); return mkdirpAsync(accountDir).then(function () { @@ -100,21 +66,20 @@ function createAccount(args, handlers) { , creation_dt: isoDate }; - // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} - // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } - // regr.json: - /* - { body: - { contact: [ 'mailto:coolaj86@gmail.com' ], - agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', - key: { e: 'AQAB', kty: 'RSA', n: '...' } }, - uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', - new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', - terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } - */ return PromiseA.all([ + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') + // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8') + // regr.json: + /* + { body: { contact: [ 'mailto:coolaj86@gmail.com' ], + agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', + key: { e: 'AQAB', kty: 'RSA', n: '...' } }, + uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', + new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', + terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } + */ , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8') ]).then(function () { return pems; @@ -180,15 +145,21 @@ function getCertificateAsync(account, args, defaults, handlers) { var pyconf = PromiseA.promisifyAll(require('pyconf')); return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { - return lef.getCertificateAsync({ - domains: args.domains + return lef.getCertificateAsyncAsync({ + newAuthorizationUrl: args._acmeUrls.newAuthz + , newCertificateUrl: args._acmeUrls.newCert + , accountPrivateKeyPem: account.privateKeyPem , domainPrivateKeyPem: domain.privateKeyPem + , domains: args.domains + + /* , getChallenge: function (domain, key, done) { args.domains = [domain]; args.webrootPath = args.webrootPath || defaults.webrootPath; handlers.getChallenge(args, key, done); } + */ , setChallenge: function (domain, key, value, done) { args.domains = [domain]; args.webrootPath = args.webrootPath || defaults.webrootPath; @@ -199,9 +170,27 @@ function getCertificateAsync(account, args, defaults, handlers) { args.webrootPath = args.webrootPath || defaults.webrootPath; handlers.removeChallenge(args, key, done); } - , newAuthorizationUrl: args._acmeUrls.newAuthz - , newCertificateUrl: args._acmeUrls.newCert }).then(function (result) { + // TODO write pems={ca,cert,key} to disk + var liveDir = path.join(args.configDir, 'live', args.domains[0]); + var certPath = path.join(liveDir, 'cert.pem'); + var fullchainPath = path.join(liveDir, 'fullchain.pem'); + var chainPath = path.join(liveDir, 'chain.pem'); + var privkeyPath = path.join(liveDir, 'privkey.pem'); + + result.fullchain = result.cert + '\n' + result.ca; + + // TODO write to archive first, then write to live + return mkdirpAsync(liveDir).then(function () { + return PromisA.all([ + sfs.writeFileAsync(certPath, result.cert, 'ascii') + , sfs.writeFileAsync(chainPath, result.chain, 'ascii') + , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') + , sfs.writeFileAsync(privkeyPath, result.key, 'ascii') + ]); + }); + + console.log(liveDir); console.log(result); throw new Error("IMPLEMENTATION NOT COMPLETE"); }); diff --git a/package.json b/package.json index 9ab1dcf..2ef3f64 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,13 @@ "localhost.daplie.com-certificates": "^1.1.2" }, "optionalDependencies": { - "letsencrypt-python": "^1.0.3", - "letsencrypt-forge": "file:letsencrypt-forge", - "letsencrypt-ursa": "file:letsencrypt-ursa", - "node-forge": "^0.6.38", - "letiny": "0.0.4-beta", "ursa": "^0.9.1" }, "dependencies": { + "node-forge": "^0.6.38", "bluebird": "^3.0.6", "homedir": "^0.6.0", + "letiny-core": "^1.0.1", "mkdirp": "^0.5.1", "pyconf": "^1.0.0", "request": "^2.67.0", From 8cb372105a0af60f51add405e2876d25d675a317 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 01:19:08 -0800 Subject: [PATCH 21/25] gutting python --- README.md | 92 +++++-------------------------------------------------- 1 file changed, 7 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index ff2f40f..11e414e 100644 --- a/README.md +++ b/README.md @@ -24,38 +24,23 @@ Install npm install --save letsencrypt ``` -Right now this uses [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python), -but it's built to be able to use a node-only javascript version (in progress). - -```bash -# install the python client (takes 2 minutes normally, 20 on a raspberry pi) -git clone https://github.com/letsencrypt/letsencrypt -pushd letsencrypt - -./letsencrypt-auto -``` - -**moving towards a python-free version** - -There are a few partially written javascript implementation, but they use `forge` instead of using node's native `crypto` and `ursa` - so their performance is outright horrific (especially on Raspberry Pi et al). For the moment it's faster to use the wrapped python version. - -Once the `forge` crud is gutted away it should slide right in without a problem. Ping [@coolaj86](https://coolaj86.com) if you'd like to help. - Usage ===== -Here's a simple snippet: +See [letsencrypt-cli](https://github.com/Daplie/node-letsencrypt-cli) +and [letsencrypt-express](https://github.com/Daplie/letsencrypt-express) ```javascript var config = require('./examples/config-minimal'); config.le.webrootPath = __dirname + '/tests/acme-challenge'; -var le = require('letsencrypt').create(config.backend, config.le); +var le = require('letsencrypt').create(config.le); le.register({ agreeTos: true , domains: ['example.com'] // CHANGE TO YOUR DOMAIN , email: 'user@email.com' // CHANGE TO YOUR EMAIL +, standalone: true }, function (err) { if (err) { console.error('[Error]: node-letsencrypt/examples/standalone'); @@ -407,20 +392,6 @@ Checks in-memory cache of certificates for `args.domains` and calls then calls ` Not yet implemented -Backends --------- - -* [`letsencrypt-python`](https://github.com/Daplie/node-letsencrypt-python) (complete) -* [`letiny`](https://github.com/Daplie/node-letiny) (in progress) - -#### How to write a backend - -A backend must implement (or be wrapped to implement) this API: - -* `fetch(hostname, cb)` will cb(err, certs) with certs from disk (or null or error) -* `register(args, challengeCb, done)` will register and or renew a cert - * args = `{ domains, email, agreeTos }` MUST check that agreeTos === true - * challengeCb = `function (challenge, cb) { }` handle challenge as needed, call cb() This is what `args` looks like: @@ -441,61 +412,12 @@ This is what the implementation should look like: (it's expected that the client will follow the same conventions as the python client, but it's not necessary) -```javascript -return { - fetch: function (args, cb) { - // NOTE: should return an error if args.domains cannot be satisfied with a single cert - // (usually example.com and www.example.com will be handled on the same cert, for example) - if (errorHappens) { - // return an error if there is an actual error (db, etc) - cb(err); - return; - } - // return null if there is no error, nor a certificate - else if (!cert) { - cb(null, null); - return; - } - - // NOTE: if the certificate is available but expired it should be - // returned and the calling application will decide to renew when - // it is convenient - - // NOTE: the application should handle caching, not the library - - // return the cert with metadata - cb(null, { - cert: "/*contcatonated certs in pem format: cert + intermediate*/" - , key: "/*private keypair in pem format*/" - , renewedAt: new Date() // fs.stat cert.pem should also work - , duration: 90 * 24 * 60 * 60 * 1000 // assumes 90-days unless specified - }); - } -, register: function (args, challengeCallback, completeCallback) { - // **MUST** reject if args.agreeTos is not true - - // once you're ready for the caller to know the challenge - if (challengeCallback) { - challengeCallback(challenge, function () { - continueRegistration(); - }) - } else { - continueRegistration(); - } - - function continueRegistration() { - // it is not necessary to to return the certificates here - // the client will call fetch() when it needs them - completeCallback(err); - } - } -}; -``` - Change History ============== -v1.0.0 Thar be dragons +* v1.1.0 Added letiny-core, removed node-letsencrypt-python +* v1.0.2 Works with node-letsencrypt-python +* v1.0.0 Thar be dragons LICENSE ======= From eaa58694f074559b9ea5c8067ede7ed9a416a55a Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 09:25:39 +0000 Subject: [PATCH 22/25] remove junk crypto --- lib/letiny-core.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/letiny-core.js b/lib/letiny-core.js index d436330..05e13db 100644 --- a/lib/letiny-core.js +++ b/lib/letiny-core.js @@ -3,13 +3,11 @@ var PromiseA = require('bluebird'); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); -var requestAsync = PromiseA.promisify(require('request')); var LE = require('../'); var LeCore = PromiseA.promisifyAll(require('letiny-core')); -var ucrypto = PromiseA.promisifyAll(LeCore.leCrypto); +var leCrypto = PromiseA.promisifyAll(LeCore.leCrypto); -//var fcrypto = PromiseA.promisifyAll(require('../lib/crypto-utils-forge')); var fetchFromConfigLiveDir = require('./common').fetchFromDisk; var ipc = {}; // in-process cache @@ -42,7 +40,7 @@ function createAccount(args, handlers) { // TODO support ECDSA // arg.rsaBitLength args.rsaExponent - return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { + return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) { /* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */ return LeCore.registerNewAccountAsync({ @@ -120,7 +118,7 @@ function getAccount(accountId, args, handlers) { return createAccount(args, handlers); } - return ucrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { + return leCrypto.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) { files.accountId = accountId; // md5sum(publicKeyPem) files.publicKeyMd5 = accountId; // md5sum(publicKeyPem) files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN... @@ -144,7 +142,7 @@ function getAccountByEmail(args) { function getCertificateAsync(account, args, defaults, handlers) { var pyconf = PromiseA.promisifyAll(require('pyconf')); - return ucrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { + return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { return lef.getCertificateAsyncAsync({ newAuthorizationUrl: args._acmeUrls.newAuthz , newCertificateUrl: args._acmeUrls.newCert From 9cd4be8bf6e98303884bb2a54e3adc72de6f173f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 09:34:39 +0000 Subject: [PATCH 23/25] no backends --- examples/ursa.js | 2 +- index.js | 2 +- lib/letiny-core.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/ursa.js b/examples/ursa.js index 4a9feaf..519e8cd 100644 --- a/examples/ursa.js +++ b/examples/ursa.js @@ -11,7 +11,7 @@ config.le.server = LE.stagingServer; // // Manual Registration // -var le = LE.create(require('../backends/ursa'), config.le); +var le = LE.create(config.le); le.backend.registerAsync({ agreeTos: true , domains: ['example.com'] // CHANGE TO YOUR DOMAIN diff --git a/index.js b/index.js index 15e04fa..91e9284 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ var PromiseA = require('bluebird'); var crypto = require('crypto'); var tls = require('tls'); var path = require('path'); -var leCore = require('./backends/letiny-core'); +var leCore = require('./lib/letiny-core'); var LE = module.exports; LE.productionServerUrl = leCore.productionServerUrl; diff --git a/lib/letiny-core.js b/lib/letiny-core.js index 05e13db..7930a5c 100644 --- a/lib/letiny-core.js +++ b/lib/letiny-core.js @@ -114,6 +114,7 @@ function getAccount(accountId, args, handlers) { if (!Object.keys(files).every(function (key) { return !files[key].error; })) { + // TODO log renewal.conf console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one..."); return createAccount(args, handlers); } From b965141dd2c2fd56fc019bda0b45fb34b93d3c05 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 10:07:00 +0000 Subject: [PATCH 24/25] readying for launch --- examples/commandline-minimal.js | 4 ++-- examples/commandline.js | 2 -- examples/config-minimal.js | 3 --- index.js | 5 +++-- lib/default-handlers.js | 2 +- lib/letiny-core.js | 36 +++++++++++++-------------------- package.json | 4 ++-- 7 files changed, 22 insertions(+), 34 deletions(-) diff --git a/examples/commandline-minimal.js b/examples/commandline-minimal.js index 5b63799..61d3cb8 100644 --- a/examples/commandline-minimal.js +++ b/examples/commandline-minimal.js @@ -14,8 +14,8 @@ config.le.server = LE.stagingServer; var le = LE.create(config.le); le.register({ agreeTos: true -, domains: ['example.com'] // CHANGE TO YOUR DOMAIN -, email: 'user@example.com' // CHANGE TO YOUR EMAIL +, domains: [process.argv[3] || 'example.com'] // CHANGE TO YOUR DOMAIN +, email: process.argv[2] || 'user@example.com' // CHANGE TO YOUR EMAIL }, function (err) { if (err) { console.error('[Error]: node-letsencrypt/examples/standalone'); diff --git a/examples/commandline.js b/examples/commandline.js index fe4ce8c..47604d0 100644 --- a/examples/commandline.js +++ b/examples/commandline.js @@ -28,8 +28,6 @@ var bkDefaults = { // backend-specific , logsDir: path.join(__dirname, '..', 'tests', 'letsencrypt.logs') , workDir: path.join(__dirname, '..', 'tests', 'letsencrypt.work') -, text: true -, pythonClientPath: require('os').homedir() + '/.local/share/letsencrypt/bin/letsencrypt' }; var le = LE.create(bkDefaults, { diff --git a/examples/config-minimal.js b/examples/config-minimal.js index bc4b26b..454119f 100644 --- a/examples/config-minimal.js +++ b/examples/config-minimal.js @@ -2,8 +2,6 @@ var path = require('path'); -var binpath = require('homedir') + '/.local/share/letsencrypt/bin/letsencrypt'; - var config = { plainPort: 80 @@ -21,7 +19,6 @@ var config = { // these are specific to the python client and won't be needed with the purejs library , logsDir: path.join(__dirname, '..', 'tests', 'letsencrypt.logs') , workDir: path.join(__dirname, '..', 'tests', 'letsencrypt.work') - , pythonClientPath: binpath } }; diff --git a/index.js b/index.js index 91e9284..3d23ae8 100644 --- a/index.js +++ b/index.js @@ -5,13 +5,14 @@ var PromiseA = require('bluebird'); var crypto = require('crypto'); var tls = require('tls'); -var path = require('path'); var leCore = require('./lib/letiny-core'); var LE = module.exports; LE.productionServerUrl = leCore.productionServerUrl; LE.stagingServer = leCore.stagingServerUrl; LE.configDir = leCore.configDir; +LE.logsDir = leCore.logsDir; +LE.workDir = leCore.workDir; LE.acmeChallengPrefix = leCore.acmeChallengPrefix; LE.knownEndpoints = leCore.knownEndpoints; @@ -204,7 +205,7 @@ LE.create = function (defaults, handlers, backend) { cb(null, true); } , middleware: function () { - var prefix = '/.well-known/acme-challenge/'; + var prefix = leCore.acmeChallengePrefix; return function (req, res, next) { if (0 !== req.url.indexOf(prefix)) { diff --git a/lib/default-handlers.js b/lib/default-handlers.js index a4bcacc..dfcd225 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -36,5 +36,5 @@ module.exports.getChallenge = function (args, key, done) { module.exports.removeChallenge = function (args, key, done) { //var hostname = args.domains[0]; - fs.unlinkSync(path.join(args.webrootPath, key), done); + fs.unlink(path.join(args.webrootPath, key), done); }; diff --git a/lib/letiny-core.js b/lib/letiny-core.js index 7930a5c..bc1b790 100644 --- a/lib/letiny-core.js +++ b/lib/letiny-core.js @@ -1,8 +1,10 @@ 'use strict'; var PromiseA = require('bluebird'); +var mkdirpAsync = PromiseA.promisify(require('mkdirp')); var path = require('path'); var fs = PromiseA.promisifyAll(require('fs')); +var sfs = require('safe-replace'); var LE = require('../'); var LeCore = PromiseA.promisifyAll(require('letiny-core')); @@ -22,19 +24,13 @@ function getAcmeUrls(args) { return LeCore.getAcmeUrlsAsync(args.server).then(function (data) { ipc.acmeUrlsUpdatedAt = Date.now(); - ipc.acmeUrls = { - newAuthz: data['new-authz'] - , newCert: data['new-cert'] - , newReg: data['new-reg'] - , revokeCert: data['revoke-cert'] - }; + ipc.acmeUrls = data; return ipc.acmeUrls; }); } function createAccount(args, handlers) { - var mkdirpAsync = PromiseA.promisify(require('mkdirp')); var os = require("os"); var localname = os.hostname(); @@ -45,14 +41,15 @@ function createAccount(args, handlers) { return LeCore.registerNewAccountAsync({ email: args.email - , newReg: args._acmeUrls.newReg - , debug: args.debug || handlers.debug + , newRegUrl: args._acmeUrls.newReg , agreeToTerms: function (tosUrl, agree) { // args.email = email; // already there args.tosUrl = tosUrl; handlers.agreeToTerms(args, agree); } , accountPrivateKeyPem: pems.privateKeyPem + + , debug: args.debug || handlers.debug }).then(function (body) { var accountDir = path.join(args.accountsDir, pems.publicKeyMd5); @@ -144,21 +141,14 @@ function getCertificateAsync(account, args, defaults, handlers) { var pyconf = PromiseA.promisifyAll(require('pyconf')); return leCrypto.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (domain) { - return lef.getCertificateAsyncAsync({ - newAuthorizationUrl: args._acmeUrls.newAuthz - , newCertificateUrl: args._acmeUrls.newCert + return LeCore.getCertificateAsync({ + newAuthzUrl: args._acmeUrls.newAuthz + , newCertUrl: args._acmeUrls.newCert , accountPrivateKeyPem: account.privateKeyPem , domainPrivateKeyPem: domain.privateKeyPem , domains: args.domains - /* - , getChallenge: function (domain, key, done) { - args.domains = [domain]; - args.webrootPath = args.webrootPath || defaults.webrootPath; - handlers.getChallenge(args, key, done); - } - */ , setChallenge: function (domain, key, value, done) { args.domains = [domain]; args.webrootPath = args.webrootPath || defaults.webrootPath; @@ -181,7 +171,7 @@ function getCertificateAsync(account, args, defaults, handlers) { // TODO write to archive first, then write to live return mkdirpAsync(liveDir).then(function () { - return PromisA.all([ + return PromiseA.all([ sfs.writeFileAsync(certPath, result.cert, 'ascii') , sfs.writeFileAsync(chainPath, result.chain, 'ascii') , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') @@ -198,11 +188,13 @@ function getCertificateAsync(account, args, defaults, handlers) { function registerWithAcme(args, defaults, handlers) { var pyconf = PromiseA.promisifyAll(require('pyconf')); - var server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory + var server = args.server || defaults.server || LeCore.stagingServerUrl; // https://acme-v01.api.letsencrypt.org/directory var acmeHostname = require('url').parse(server).hostname; var configDir = args.configDir || defaults.configDir || LE.configDir; args.server = server; + console.log('args.server'); + console.log(server); args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); @@ -212,7 +204,7 @@ function registerWithAcme(args, defaults, handlers) { return accountId; }, function (err) { - if ("EENOENT" === err.code) { + if ("ENOENT" === err.code) { return getAccountByEmail(args, handlers); } diff --git a/package.json b/package.json index 2ef3f64..23dcce6 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,14 @@ "ursa": "^0.9.1" }, "dependencies": { - "node-forge": "^0.6.38", "bluebird": "^3.0.6", "homedir": "^0.6.0", "letiny-core": "^1.0.1", "mkdirp": "^0.5.1", + "node-forge": "^0.6.38", "pyconf": "^1.0.0", "request": "^2.67.0", - "safe-replace": "^1.0.0", + "safe-replace": "^1.0.2", "serve-static": "^1.10.0" } } From 2bc14032001c1e92ec89f6b45370993b9a0af35c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Dec 2015 12:57:53 +0000 Subject: [PATCH 25/25] pure node :-) --- index.js | 14 +++++++++----- lib/common.js | 8 ++++++-- lib/default-handlers.js | 6 +++--- lib/letiny-core.js | 26 +++++++++++++++----------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 3d23ae8..3ad21e8 100644 --- a/index.js +++ b/index.js @@ -86,7 +86,7 @@ LE.create = function (defaults, handlers, backend) { }; } if (!handlers.getChallenge) { - if (!defaults.webrootPath) { + if (!defaults.manual && !defaults.webrootPath) { // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} throw new Error("handlers.getChallenge or defaults.webrootPath must be set"); } @@ -209,13 +209,13 @@ LE.create = function (defaults, handlers, backend) { return function (req, res, next) { if (0 !== req.url.indexOf(prefix)) { - console.log('[LE middleware]: pass'); + //console.log('[LE middleware]: pass'); next(); return; } //args.domains = [req.hostname]; - console.log('[LE middleware]:', req.hostname, req.url, req.url.slice(prefix.length)); + //console.log('[LE middleware]:', req.hostname, req.url, req.url.slice(prefix.length)); handlers.getChallenge(req.hostname, req.url.slice(prefix.length), function (err, token) { if (err) { res.send("Error: These aren't the tokens you're looking for. Move along."); @@ -245,9 +245,9 @@ LE.create = function (defaults, handlers, backend) { return; } - console.log("[NLE]: begin registration"); + //console.log("[NLE]: begin registration"); return backend.registerAsync(copy).then(function () { - console.log("[NLE]: end registration"); + //console.log("[NLE]: end registration"); // calls fetch because fetch calls cacheCertInfo return le.fetch(args, cb); }, cb); @@ -317,6 +317,10 @@ LE.create = function (defaults, handlers, backend) { le._fetchHelper(args, cb); } , register: function (args, cb) { + if (!Array.isArray(args.domains)) { + cb(new Error('args.domains should be an array of domains')); + return; + } // this may be run in a cluster environment // in that case it should NOT check the cache // but ensure that it has the most fresh copy diff --git a/lib/common.js b/lib/common.js index 7be610b..f8ccde6 100644 --- a/lib/common.js +++ b/lib/common.js @@ -5,8 +5,12 @@ var PromiseA = require('bluebird'); module.exports.fetchFromDisk = function (args, defaults) { var hostname = args.domains[0]; - var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname); - var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname); + var crtpath = (args.fullchainPath || defaults.fullchainPath) + || (defaults.configDir + + (args.fullchainTpl || defaults.fullchainTpl || ':hostname/fullchain.pem').replace(/:hostname/, hostname)); + var privpath = (args.privkeyPath || defaults.privkeyPath) + || (defaults.configDir + + (args.privkeyTpl || defaults.privkeyTpl || ':hostname/privkey.pem').replace(/:hostname/, hostname)); return PromiseA.all([ fs.readFileAsync(privpath, 'ascii') diff --git a/lib/default-handlers.js b/lib/default-handlers.js index dfcd225..17852a2 100644 --- a/lib/default-handlers.js +++ b/lib/default-handlers.js @@ -12,8 +12,8 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d var mkdirp = require('mkdirp'); // TODO should be args.webrootPath - console.log('args.webrootPath, challengePath'); - console.log(args.webrootPath, challengePath); + //console.log('args.webrootPath, challengePath'); + //console.log(args.webrootPath, challengePath); mkdirp(args.webrootPath, function (err) { if (err) { done(err); @@ -29,7 +29,7 @@ module.exports.setChallenge = function (args, challengePath, keyAuthorization, d module.exports.getChallenge = function (args, key, done) { //var hostname = args.domains[0]; - console.log("getting the challenge", args, key); + //console.log("getting the challenge", args, key); fs.readFile(path.join(args.webrootPath, key), 'utf8', done); }; diff --git a/lib/letiny-core.js b/lib/letiny-core.js index bc1b790..6be57e0 100644 --- a/lib/letiny-core.js +++ b/lib/letiny-core.js @@ -176,12 +176,18 @@ function getCertificateAsync(account, args, defaults, handlers) { , sfs.writeFileAsync(chainPath, result.chain, 'ascii') , sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii') , sfs.writeFileAsync(privkeyPath, result.key, 'ascii') - ]); + ]).then(function () { + // TODO format result licesy + //console.log(liveDir); + //console.log(result); + return { + certPath: certPath + , chainPath: chainPath + , fullchainPath: fullchainPath + , privkeyPath: privkeyPath + }; + }); }); - - console.log(liveDir); - console.log(result); - throw new Error("IMPLEMENTATION NOT COMPLETE"); }); }); } @@ -193,8 +199,6 @@ function registerWithAcme(args, defaults, handlers) { var configDir = args.configDir || defaults.configDir || LE.configDir; args.server = server; - console.log('args.server'); - console.log(server); args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf'); args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory'); @@ -228,14 +232,14 @@ function registerWithAcme(args, defaults, handlers) { } */ - console.log(account); + //console.log(account); return fetchFromConfigLiveDir(args, defaults).then(function (certs) { // if nothing, register and save // if something, check date (don't register unless 30+ days) // if good, don't bother registering // (but if we get to the point that we're actually calling // this function, that shouldn't be the case, right?) - console.log(certs); + //console.log(certs); if (!certs) { // no certs, seems like a good time to get some return getCertificateAsync(account, args, defaults, handlers); @@ -244,12 +248,12 @@ function registerWithAcme(args, defaults, handlers) { // cert is at least 27 days old we can renew that return getCertificateAsync(account, args, defaults, handlers); } - else if (args.force) { + else if (args.duplicate) { // YOLO! I be gettin' fresh certs 'erday! Yo! return getCertificateAsync(account, args, defaults, handlers); } else { - console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.force to force.'); + console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.'); // We're happy with what we have return certs; }