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",