progress
This commit is contained in:
parent
620a4e1b58
commit
e7702c636e
|
@ -0,0 +1,120 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.create = function (lebinpath, defaults, options) {
|
||||
var PromiseA = require('bluebird');
|
||||
var tls = require('tls');
|
||||
var fs = PromiseA.promisifyAll(require('fs'));
|
||||
var letsencrypt = PromiseA.promisifyAll(require('./le-exec-wrapper'));
|
||||
|
||||
//var attempts = {}; // should exist in master process only
|
||||
var ipc = {}; // in-process cache
|
||||
var count = 0;
|
||||
|
||||
//var certTpl = "/live/:hostname/cert.pem";
|
||||
var certTpl = "/live/:hostname/fullchain.pem";
|
||||
var privTpl = "/live/:hostname/privkey.pem";
|
||||
|
||||
options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
|
||||
|
||||
defaults.webroot = true;
|
||||
defaults.webrootPath = '/srv/www/acme-challenge';
|
||||
|
||||
return letsencrypt.optsAsync(lebinpath).then(function (keys) {
|
||||
var now;
|
||||
var le;
|
||||
|
||||
le = {
|
||||
validate: function () {
|
||||
}
|
||||
, argnames: keys
|
||||
, readCerts: function (hostname) {
|
||||
var crtpath = defaults.configDir + certTpl.replace(/:hostname/, hostname);
|
||||
var privpath = defaults.configDir + privTpl.replace(/:hostname/, hostname);
|
||||
|
||||
return PromiseA.all([
|
||||
fs.readFileAsync(privpath, 'ascii')
|
||||
, fs.readFileAsync(crtpath, 'ascii')
|
||||
// stat the file, not the link
|
||||
, fs.statAsync(crtpath, 'ascii')
|
||||
]).then(function (arr) {
|
||||
|
||||
|
||||
return arr;
|
||||
});
|
||||
}
|
||||
, cacheCerts: function (hostname, certs) {
|
||||
// assume 90 day renewals based on stat time, for now
|
||||
ipc[hostname] = {
|
||||
context: tls.createSecureContext({
|
||||
key: certs[0] // privkey.pem
|
||||
, cert: certs[1] // fullchain.pem
|
||||
//, ciphers // node's defaults are great
|
||||
})
|
||||
, updated: Date.now()
|
||||
};
|
||||
|
||||
return ipc[hostname];
|
||||
}
|
||||
, readAndCacheCerts: function (hostname) {
|
||||
return le.readCerts(hostname).then(function (certs) {
|
||||
return le.cacheCerts(hostname, certs);
|
||||
});
|
||||
}
|
||||
, get: function (hostname, args, opts, cb) {
|
||||
count += 1;
|
||||
|
||||
if (count >= 1000) {
|
||||
now = Date.now();
|
||||
count = 0;
|
||||
}
|
||||
|
||||
var cached = ipc[hostname];
|
||||
// TODO handle www and no-www together
|
||||
if (cached && ((now - cached.updated) < options.cacheContextsFor)) {
|
||||
cb(null, cached.context);
|
||||
return;
|
||||
}
|
||||
|
||||
return le.readCerts(hostname).then(function (cached) {
|
||||
cb(null, cached.context);
|
||||
}, function (/*err*/) {
|
||||
var copy = {};
|
||||
var arr;
|
||||
|
||||
// TODO validate domains and such
|
||||
Object.keys(defaults).forEach(function (key) {
|
||||
copy[key] = defaults[key];
|
||||
});
|
||||
Object.keys(args).forEach(function (key) {
|
||||
copy[key] = args[key];
|
||||
});
|
||||
|
||||
arr = letsencrypt.objToArr(keys, copy);
|
||||
// TODO validate domains empirically before trying le
|
||||
return letsencrypt.execAsync(lebinpath, arr, opts).then(function () {
|
||||
// wait at least n minutes
|
||||
return le.readCerts(hostname).then(function (cached) {
|
||||
// success
|
||||
cb(null, cached.context);
|
||||
}, function (err) {
|
||||
// still couldn't read the certs after success... that's weird
|
||||
cb(err);
|
||||
});
|
||||
}, function (err) {
|
||||
console.error("[Error] Let's Encrypt failed:");
|
||||
console.error(err.stack || new Error(err.message || err.toString()));
|
||||
|
||||
// wasn't successful with lets encrypt, don't try again for n minutes
|
||||
ipc[hostname] = {
|
||||
context: null
|
||||
, updated: Date.now()
|
||||
};
|
||||
cb(null, ipc[hostname]);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return le;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
var letsencrypt = module.exports;
|
||||
|
||||
letsencrypt.parseOptions = function (text) {
|
||||
var options = {};
|
||||
var re = /--([a-z0-9\-]+)/g;
|
||||
var m;
|
||||
|
||||
function uc(match, c) {
|
||||
return c.toUpperCase();
|
||||
}
|
||||
|
||||
while ((m = re.exec(text))) {
|
||||
var key = m[1].replace(/-([a-z0-9])/g, uc);
|
||||
|
||||
options[key] = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
letsencrypt.opts = function (lebinpath, cb) {
|
||||
letsencrypt.exec(lebinpath, ['--help', 'all'], function (err, text) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, Object.keys(letsencrypt.parseOptions(text)));
|
||||
});
|
||||
};
|
||||
|
||||
letsencrypt.exec = function (lebinpath, args, opts, cb) {
|
||||
// TODO create and watch the directory for challenge callback
|
||||
if (opts.challengeCallback) {
|
||||
return PromiseA.reject({
|
||||
message: "challengeCallback not yet supported"
|
||||
});
|
||||
}
|
||||
|
||||
var le = spawn(lebinpath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
var text = '';
|
||||
var errtext = '';
|
||||
var err;
|
||||
|
||||
le.on('error', function (error) {
|
||||
err = error;
|
||||
});
|
||||
|
||||
le.stdout.on('data', function (chunk) {
|
||||
text += chunk.toString('ascii');
|
||||
});
|
||||
|
||||
le.stderr.on('data', function (chunk) {
|
||||
errtext += chunk.toString('ascii');
|
||||
});
|
||||
|
||||
le.on('close', function (code, signal) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errtext) {
|
||||
err = new Error(errtext);
|
||||
err.code = code;
|
||||
err.signal = signal;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (0 !== code) {
|
||||
err = new Error("exited with code '" + code + "'");
|
||||
err.code = code;
|
||||
err.signal = signal;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null, text);
|
||||
});
|
||||
};
|
||||
|
||||
letsencrypt.objToArr = function (params, opts) {
|
||||
var args = {};
|
||||
var arr = [];
|
||||
|
||||
Object.keys(opts).forEach(function (key) {
|
||||
var val = opts[key];
|
||||
|
||||
if (!val && 0 !== val) {
|
||||
// non-zero value which is false, null, or otherwise falsey
|
||||
// falsey values should not be passed
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.indexOf(key)) {
|
||||
// key is not recognized by the python client
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
args[key] = opts[key].join(',');
|
||||
} else {
|
||||
args[key] = opts[key];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(args).forEach(function (key) {
|
||||
if ('tlsSni01Port' === key) {
|
||||
arr.push('--tls-sni-01-port');
|
||||
}
|
||||
else if ('http01Port' === key) {
|
||||
arr.push('--http-01-port');
|
||||
}
|
||||
else {
|
||||
arr.push('--' + key.replace(/([A-Z])/g, '-$1').toLowerCase());
|
||||
}
|
||||
|
||||
if (true !== opts[key]) {
|
||||
// value is truthy, but not true (and falsies were weeded out above)
|
||||
arr.push(opts[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return arr;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
var re = /^[a-zA-Z0-9\.\-]+$/;
|
||||
var punycode = require('punycode');
|
||||
|
||||
var utils = module.exports;
|
||||
|
||||
utils.isValidDomain = function (domain) {
|
||||
if (re.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
domain = punycode.toASCII(domain);
|
||||
|
||||
if (re.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
Loading…
Reference in New Issue