diff --git a/index.js b/index.js index f63cd7a..077a42c 100644 --- a/index.js +++ b/index.js @@ -4,14 +4,9 @@ var PromiseA = require('bluebird'); var leCore = require('letiny-core'); -var utils = require('./lib/common'); -var merge = require('./lib/common').merge; -var tplCopy = require('./lib/common').tplCopy; var LE = module.exports; -LE.merge = require('./lib/common').merge; - LE.defaults = { server: leCore.productionServerUrl , stagingServer: leCore.stagingServerUrl @@ -28,12 +23,11 @@ Object.keys(LE.defaults).forEach(function (key) { LE[key] = LE.defaults[key]; }); - // backend, defaults, handlers LE.create = function (defaults, handlers, backend) { - var Backend = require('./lib/core'); - if (!backend) { backend = require('./lib/pycompat').create(defaults); } + var Core = require('./lib/core'); + var core; + if (!backend) { backend = require('./lib/pycompat'); } if (!handlers) { handlers = {}; } - if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; } if (!handlers.sniRegisterCallback) { @@ -42,188 +36,45 @@ LE.create = function (defaults, handlers, backend) { cb(null, null); }; } - if (!handlers.getChallenge) { - 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"); - } - 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); - var getChallenge = require('./lib/default-handlers').getChallenge; - var copy = merge({ domains: [hostname] }, defaults); - tplCopy(copy); - defaultos.domains = [hostname]; - - if (3 === getChallenge.length) { - console.warn('[WARNING] Deprecated use. Define getChallenge as function (opts, domain, key, cb) { }'); - getChallenge(defaultos, key, done); - } - else if (4 === getChallenge.length) { - getChallenge(defaultos, hostname, key, done); - } - else { - done(new Error("handlers.getChallenge [1] receives the wrong number of arguments")); - } - }; + if (backend.create) { + backend = backend.create(defaults); } - 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-handlers').setChallenge; - } - if (!handlers.removeChallenge) { - if (!defaults.webrootPath) { - // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}} - throw new Error("handlers.removeChallenge or defaults.webrootPath must be set"); - } - handlers.removeChallenge = require('./lib/default-handlers').removeChallenge; - } - if (!handlers.agreeToTerms) { - if (defaults.agreeTos) { - console.warn("[WARN] Agreeing to terms by default is risky business..."); - } - handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms; - } - - backend = Backend.create(defaults, handlers); backend = PromiseA.promisifyAll(backend); + core = Core.create(defaults, handlers, backend); - //var attempts = {}; // should exist in master process only - var le; - - // TODO check certs on initial load - // TODO expect that certs expire every 90 days - // TODO check certs with setInterval? - //options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000); - - le = { + var le = { backend: backend - , pyToJson: function (pyobj) { - if (!pyobj) { - return null; - } - - var jsobj = {}; - Object.keys(pyobj).forEach(function (key) { - jsobj[key] = pyobj[key]; - }); - jsobj.__lines = undefined; - jsobj.__keys = undefined; - - return jsobj; - } - , register: function (args, cb) { - if (defaults.debug || args.debug) { - console.log('[LE] register'); - } - if (!Array.isArray(args.domains)) { - cb(new Error('args.domains should be an array of domains')); - return; - } - - var copy = LE.merge(args, defaults); - var err; - - if (!utils.isValidDomain(args.domains[0])) { - err = new Error("invalid domain name: '" + args.domains + "'"); - err.code = "INVALID_DOMAIN"; - cb(err); - return; - } - - if ((!args.domains.length && args.domains.every(le.isValidDomain))) { - // NOTE: this library can't assume to handle the http loopback - // (or dns-01 validation may be used) - // so we do not check dns records or attempt a loopback here - cb(new Error("node-letsencrypt: invalid hostnames: " + args.domains.join(','))); - return; - } - - if (defaults.debug || args.debug) { - console.log("[NLE]: begin registration"); - } - - return backend.registerAsync(copy).then(function (pems) { - if (defaults.debug || args.debug) { - console.log("[NLE]: end registration"); - } + , core: core + // register + , create: function (args, cb) { + return core.registerAsync(args).then(function (pems) { cb(null, pems); - //return le.fetch(args, cb); }, cb); } - , fetch: function (args, cb) { - if (defaults.debug || args.debug) { - console.log('[LE] fetch'); - } - - // TODO figure out what TPLs are needed - var copy = merge(args, defaults); - tplCopy(copy); - - return backend.fetchAsync(args).then(function (certInfo) { - if (args.debug) { - console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo)); - } - if (!certInfo) { cb(null, null); return; } - - // key, cert, issuedAt, lifetime, expiresAt - if (!certInfo.expiresAt) { - certInfo.expiresAt = certInfo.issuedAt + (certInfo.lifetime || handlers.lifetime); - } - if (!certInfo.lifetime) { - certInfo.lifetime = (certInfo.lifetime || handlers.lifetime); - } - // a pretty good hard buffer - certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100); - + // fetch + , domain: function (args, cb) { + // TODO must return email, domains, tos, pems + return core.fetchAsync(args).then(function (certInfo) { cb(null, certInfo); }, cb); } - , getConfig: function (args, cb) { - if (defaults.debug || args.debug) { - console.log('[LE] getConfig'); - } - backend.getConfigAsync(args).then(function (pyobj) { - cb(null, le.pyToJson(pyobj)); - }, function (err) { - console.error("[letsencrypt/index.js] getConfig"); - console.error(err.stack); - return cb(null, []); - }); + , domains: function (args, cb) { + // TODO show all domains or limit by account + throw new Error('not implemented'); } - , getConfigs: function (args, cb) { - if (defaults.debug || args.debug) { - console.log('[LE] getConfigs'); - } - backend.getConfigsAsync(args).then(function (configs) { - cb(null, configs.map(le.pyToJson)); - }, function (err) { - if ('ENOENT' === err.code) { - cb(null, []); - } else { - console.error("[letsencrypt/index.js] getConfigs"); - console.error(err.stack); - cb(err); - } - }); + , accounts: function (args, cb) { + // TODO show all accounts or limit by domain + throw new Error('not implemented'); } - , setConfig: function (args, cb) { - if (defaults.debug || args.debug) { - console.log('[LE] setConfig'); - } - backend.configureAsync(args).then(function (pyobj) { - cb(null, le.pyToJson(pyobj)); - }); + , account: function (args, cb) { + // TODO return one account + throw new Error('not implemented'); } }; + // exists + // get + return le; }; diff --git a/lib/common.js b/lib/common.js index 9af37e3..e00919f 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,9 +1,6 @@ 'use strict'; -var fs = require('fs'); var path = require('path'); -var PromiseA = require('bluebird'); - var homeRe = new RegExp("^~(\\/|\\\|\\" + path.sep + ")"); var re = /^[a-zA-Z0-9\.\-]+$/; var punycode = require('punycode'); @@ -22,16 +19,6 @@ module.exports.isValidDomain = function (domain) { return ''; }; -module.exports.tplConfigDir = function (configDir, defaults) { - var homedir = require('homedir')(); - Object.keys(defaults).forEach(function (key) { - if ('string' === typeof defaults[key]) { - defaults[key] = defaults[key].replace(':config', configDir).replace(':conf', configDir); - defaults[key] = defaults[key].replace(homeRe, homedir + path.sep); - } - }); -}; - module.exports.merge = function (/*defaults, args*/) { var allDefaults = Array.prototype.slice.apply(arguments); var args = args.shift(); @@ -51,18 +38,6 @@ module.exports.merge = function (/*defaults, args*/) { }; module.exports.tplCopy = function (copy) { - var url = require('url'); - var acmeLocation = url.parse(copy.server); - var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname); - copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath); - // TODO move these defaults elsewhere? - //args.renewalDir = args.renewalDir || ':config/renewal/'; - args.renewalPath = args.renewalPath || ':config/renewal/:hostname.conf'; - // Note: the /directory is part of the server url and, as such, bleeds into the pathname - // So :config/accounts/:server/directory is *incorrect*, but the following *is* correct: - args.accountsDir = args.accountsDir || ':config/accounts/:server'; - hargs.renewalDir = hargs.renewalDir || ':config/renewal/'; - copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf'); var homedir = require('homedir')(); var tpls = { hostname: (copy.domains || [])[0] @@ -72,19 +47,20 @@ module.exports.tplCopy = function (copy) { }; Object.keys(copy).forEach(function (key) { - if ('string' === typeof copy[key]) { - Object.keys(tpls).sort(function (a, b) { - return b.length - a.length; - }).forEach(function (tplname) { - if (!tpls[tplname]) { - // what can't be templated now may be templatable later - return; - } - copy[key] = copy[key].replace(':' + tplname, tpls[tplname]); - copy[key] = copy[key].replace(homeRe, homedir + path.sep); - }); + if ('string' !== typeof copy[key]) { + return; } - }); - //return copy; + copy[key] = copy[key].replace(homeRe, homedir + path.sep); + + Object.keys(tpls).sort(function (a, b) { + return b.length - a.length; + }).forEach(function (tplname) { + if (!tpls[tplname]) { + // what can't be templated now may be templatable later + return; + } + copy[key] = copy[key].replace(':' + tplname, tpls[tplname]); + }); + }); }; diff --git a/lib/core.js b/lib/core.js index 85f5001..93d73b6 100644 --- a/lib/core.js +++ b/lib/core.js @@ -4,6 +4,8 @@ var LE = require('../'); var ipc = {}; // in-process cache module.exports.create = function (defaults, handlers, backend) { + var backendDefaults = backend.getDefaults && backend.getDefaults || backend.defaults || {}; + defaults.server = defaults.server || LE.liveServer; handlers.merge = require('./common').merge; handlers.tplCopy = require('./common').tplCopy; @@ -13,9 +15,22 @@ module.exports.create = function (defaults, handlers, backend) { var LeCore = PromiseA.promisifyAll(require('letiny-core')); var crypto = require('crypto'); + function attachCertInfo(results) { + var getCertInfo = require('./cert-info').getCertInfo; + // XXX Note: Parsing the certificate info comes at a great cost (~500kb) + var certInfo = getCertInfo(results.cert); + + //results.issuedAt = arr[3].mtime.valueOf() + results.issuedAt = Date(certInfo.notBefore.value).valueOf(); // Date.now() + results.expiresAt = Date(certInfo.notAfter.value).valueOf(); + + return results; + } + function createAccount(args, handlers) { - // arg.rsaBitLength args.rsaExponent - return RSA.generateKeypairAsync(args.rsaKeySize || 2048, 65537, { public: true, pem: true }).then(function (keypair) { + args.rsaKeySize = args.rsaKeySize || 2048; + + return RSA.generateKeypairAsync(args.rsaKeySize, 65537, { public: true, pem: true }).then(function (keypair) { return LeCore.registerNewAccountAsync({ email: args.email @@ -71,6 +86,9 @@ module.exports.create = function (defaults, handlers, backend) { } function getCertificateAsync(args, defaults, handlers) { + args.rsaKeySize = args.rsaKeySize || 2048; + args.challengeType = args.challengeType || 'http-01'; + function log() { if (args.debug || defaults.debug) { console.log.apply(console, arguments); @@ -106,6 +124,7 @@ module.exports.create = function (defaults, handlers, backend) { , accountKeypair: RSA.import(account.keypair) , domainKeypair: domainKeypair , domains: args.domains + , challengeType: args.challengeType // // IMPORTANT @@ -116,39 +135,36 @@ module.exports.create = function (defaults, handlers, backend) { // (args is per-request, defaults is per instance) // , setChallenge: function (domain, key, value, done) { - var copy = handlers.merge({ domains: [domain] }, defaults); + var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); handlers.tplCopy(copy); - args.domains = [domain]; - //args.domains = args.domains || [domain]; - if (4 === handlers.setChallenge.length) { - console.warn('[WARNING] deprecated use. Define setChallenge as function (opts, domain, key, val, cb) { }'); - handlers.setChallenge(copy, key, value, done); - } - else if (5 === handlers.setChallenge.length) { - handlers.setChallenge(copy, domain, key, value, done); - } - else { - done(new Error("handlers.setChallenge receives the wrong number of arguments")); + //args.domains = [domain]; + args.domains = args.domains || [domain]; + + if (5 !== handlers.setChallenge.length) { + done(new Error("handlers.setChallenge receives the wrong number of arguments." + + " You must define setChallenge as function (opts, domain, key, val, cb) { }")); + return; } + + handlers.setChallenge(copy, domain, key, value, done); } , removeChallenge: function (domain, key, done) { - var copy = handlers.merge({ domains: [domain] }, defaults); + var copy = handlers.merge({ domains: [domain] }, defaults, backendDefaults); handlers.tplCopy(copy); - if (3 === handlers.removeChallenge.length) { - handlers.removeChallenge(copy, key, done); - } - else if (4 === handlers.removeChallenge.length) { - handlers.removeChallenge(copy, domain, key, done); - } - else { - done(new Error("handlers.removeChallenge receives the wrong number of arguments")); + if (4 !== handlers.removeChallenge.length) { + done(new Error("handlers.removeChallenge receives the wrong number of arguments." + + " You must define removeChallenge as function (opts, domain, key, cb) { }")); + return; } + + handlers.removeChallenge(copy, domain, key, done); } - }); + }).then(attachCertInfo); }).then(function (results) { // { cert, chain, fullchain, privkey } + args.pems = results; return backend.setRegistration(args, defaults, handlers); }); @@ -160,7 +176,7 @@ module.exports.create = function (defaults, handlers, backend) { return getCertificateAsync(args, defaults, handlers); } - return backend.getRegistration(args).then(function (certs) { + return wrapped.fetchAsync(args).then(function (certs) { var halfLife = (certs.expiresAt - certs.issuedAt) / 2; if (!certs || (Date.now() - certs.issuedAt) > halfLife) { @@ -196,27 +212,34 @@ module.exports.create = function (defaults, handlers, backend) { log('[le/core.js] use account'); args.accountId = accountId; - return Accounts.getAccount(args, handlers); + return backend.getAccount(args, handlers); } else { log('[le/core.js] create account'); - return Accounts.createAccount(args, handlers); + return createAccount(args, handlers); } }); - }).then(function (account) { - /* - if (renewal.account !== account) { - // the account has become corrupt, re-register - return; - } - */ - log('[le/core.js] created account'); - return account; }); } var wrapped = { - registerAsync: function (args) { - var copy = handlers.merge(args, defaults); + registerAsync: function (args) { + var utils = require('./lib/common'); + var err; + + if (!Array.isArray(args.domains)) { + return PromiseA.reject(new Error('args.domains should be an array of domains')); + } + + if (!(args.domains.length && args.domains.every(utils.isValidDomain))) { + // NOTE: this library can't assume to handle the http loopback + // (or dns-01 validation may be used) + // so we do not check dns records or attempt a loopback here + err = new Error("invalid domain name(s): '" + args.domains + "'"); + err.code = "INVALID_DOMAIN"; + return PromiseA.reject(err); + } + + var copy = handlers.merge(args, defaults, backendDefaults); handlers.tplCopy(copy); return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { @@ -234,20 +257,23 @@ module.exports.create = function (defaults, handlers, backend) { }); } , getOrCreateAccount: function (args) { - // TODO - keypair.privateKeyPem = RSA.exportPrivatePem(keypair); - keypair.publicKeyPem = RSA.exportPublicPem(keypair); return createAccount(args, handlers); } , configureAsync: function (hargs) { - var copy = merge(hargs, defaults); - tplCopy(copy); + var copy = handlers.merge(hargs, defaults, backendDefaults); + handlers.tplCopy(copy); return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) { copy.account = account; return backend.getOrCreateRenewal(copy); }); } + , fetchAsync: function (args) { + var copy = handlers.merge(args, defaults); + handlers.tplCopy(copy); + + return backend.fetchAsync(copy).then(attachCertInfo); + } }; return wrapped;