Skip to content

Node 14: Upgrade greenlock and other dependencies #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
19 changes: 19 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"cSpell.words": [
"Letsecript",
"Optimalbits",
"Proxying",
"Unbundles",
"altnames",
"challege",
"fullchain",
"greenlock",
"letscrypt",
"letsencrypt",
"pems",
"pino",
"privkey",
"unbundle",
"webroot"
]
}
2 changes: 1 addition & 1 deletion hl-tests/letsencrypt/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ proxy.register('localhost', '127.0.0.1:3000', {
// proxy.register("localhost", "127.0.0.1:3000");

var http = require('http');
var keepAliveAgent = new http.Agent({ keepAlive: true, maxSockets: 1000 });
// var keepAliveAgent = new http.Agent({ keepAlive: true, maxSockets: 1000 });
// http.globalAgent = keepAliveAgent;

/*
Expand Down
8 changes: 8 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6"
},
"include": ["lib/**/*", "samples/**/*"],
"exclude": ["node_modules", "**/node_modules/*"]
}
8 changes: 8 additions & 0 deletions lib/helper/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { URL } = require('url');

/** @type {(url: string) => URL} */
const parseUrl = (url) => new URL(url, 'http://xxx');

module.exports = {
parseUrl,
};
315 changes: 186 additions & 129 deletions lib/letsencrypt.js
Original file line number Diff line number Diff line change
@@ -1,139 +1,196 @@
// @ts-check

/**
* Letsecript module for Redbird (c) Optimalbits 2016
*
*
*
*/

/**
* LetsEncrypt certificates are stored like the following:
*
* /example.com
* /
*
*
*
*/
var leStoreConfig = {};
var webrootPath = ':configDir/:hostname/.well-known/acme-challenge';

function init(certPath, port, logger) {
var http = require('http');
var path = require('path');
var url = require('url');
var fs = require('fs');

logger && logger.info('Initializing letsencrypt, path %s, port: %s', certPath, port);

leStoreConfig = {
configDir: certPath,
privkeyPath: ':configDir/:hostname/privkey.pem',
fullchainPath: ':configDir/:hostname/fullchain.pem',
certPath: ':configDir/:hostname/cert.pem',
chainPath: ':configDir/:hostname/chain.pem',

workDir: ':configDir/letsencrypt/var/lib',
logsDir: ':configDir/letsencrypt/var/log',

webrootPath: webrootPath,
debug: false,
};

// we need to proxy for example: 'example.com/.well-known/acme-challenge' -> 'localhost:port/example.com/'
http
.createServer(function (req, res) {
var uri = url.parse(req.url).pathname;
var filename = path.join(certPath, uri);
var isForbiddenPath = uri.length < 3 || filename.indexOf(certPath) !== 0;

if (isForbiddenPath) {
logger && logger.info('Forbidden request on LetsEncrypt port %s: %s', port, filename);
res.writeHead(403);
res.end();
return;
}

logger && logger.info('LetsEncrypt CA trying to validate challenge %s', filename);

fs.stat(filename, function (err, stats) {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('404 Not Found\n');
res.end();
return;
}

res.writeHead(200);
fs.createReadStream(filename, 'binary').pipe(res);
});
})
.listen(port);
}

/**
* Gets the certificates for the given domain.
* Handles all the LetsEncrypt protocol. Uses
* existing certificates if any, or negotiates a new one.
* Returns a promise that resolves to an object with the certificates.
* TODO: We should use something like https://github.com/PaquitoSoft/memored/blob/master/index.js
* to avoid
*/
function getCertificates(domain, email, production, renew, logger) {
var LE = require('greenlock');
var le;

// Storage Backend
var leStore = require('le-store-certbot').create(leStoreConfig);

// ACME Challenge Handlers
var leChallenge = require('le-challenge-fs').create({
webrootPath: webrootPath,
debug: false,
});

le = LE.create({
server: production
? 'https://acme-v02.api.letsencrypt.org/directory'
: 'https://acme-staging-v02.api.letsencrypt.org/directory',
store: leStore, // handles saving of config, accounts, and certificates
challenges: { 'http-01': leChallenge }, // handles /.well-known/acme-challege keys and tokens
challengeType: 'http-01', // default to this challenge type
debug: false,
log: function (debug) {
logger && logger.info(arguments, 'Lets encrypt debugger');
},
});

// Check in-memory cache of certificates for the named domain
return le.check({ domains: [domain] }).then(function (cert) {
var opts = {
domains: [domain],
email: email,
agreeTos: true,
rsaKeySize: 2048, // 2048 or higher
challengeType: 'http-01',
};

if (cert) {
if (renew) {
logger && logger.info('renewing cert for ' + domain);
opts.duplicate = true;
return le.renew(opts, cert).catch(function (err) {
logger && logger.error(err, 'Error renewing certificates for ', domain);
});
} else {
logger && logger.info('Using cached cert for ' + domain);
return cert;
}
} else {
// Register Certificate manually
logger && logger.info('Manually registering certificate for %s', domain);
return le.register(opts).catch(function (err) {
logger && logger.error(err, 'Error registering LetsEncrypt certificates');
});
}
});
}

module.exports.init = init;
module.exports.getCertificates = getCertificates;
let leStoreConfig = {};
let webrootPath = '';
let certPath = '';

/**
*
* @param {string} _certPath
* @param {number} port
* @param {*} logger
*/
function init(_certPath, port, logger) {
const http = require('http');
const path = require('path');
const fs = require('fs');
const { parseUrl } = require('./helper/url');
certPath = _certPath;
webrootPath = `${certPath}/{domain}/.well-known/acme-challenge`;

logger && logger.info('Initializing letsencrypt, path %s, port: %s', certPath, port);

// Storage Backend
leStoreConfig = {
basePath: certPath,
module: require.resolve('greenlock-store-fs'),
privkeyPath: ':basePath/:subject/privkey.pem',
fullchainPath: ':basePath/:subject/fullchain.pem',
certPath: ':basePath/:subject/cert.pem',
chainPath: ':basePath/:subject/chain.pem',

webrootPath,
};

// we need to proxy for example: 'example.com/.well-known/acme-challenge' -> 'localhost:port/example.com/'
http
.createServer((req, res) => {
const uri = parseUrl(req.url).pathname;
const filename = path.join(certPath, uri);
const isForbiddenPath = uri.length < 3 || filename.indexOf(certPath) !== 0;

if (isForbiddenPath) {
logger && logger.info('Forbidden request on LetsEncrypt port %s: %s', port, filename);
res.writeHead(403);
res.end();
return;
}

logger && logger.info('LetsEncrypt CA trying to validate challenge %s', filename);

fs.stat(filename, function (err, stats) {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('404 Not Found\n');
res.end();
return;
}

res.writeHead(200);
fs.createReadStream(filename, 'binary').pipe(res);
});
})
.listen(port);
}

/**
* Gets the certificates for the given domain.
* Handles all the LetsEncrypt protocol. Uses
* existing certificates if any, or negotiates a new one.
* Returns a promise that resolves to an object with the certificates.
* TODO: We should use something like https://github.com/PaquitoSoft/memored/blob/master/index.js
* to avoid
* @param {string} domain
* @param {string} email
* @param {boolean} [production]
* @param {*} [logger]
* @param {(err: Error, site: any) => void} [siteCallback]
* @param {{packageRoot?: string; configDir?: string; packageAgent?: string;}} [greenlockOpts]
*/
async function getCertificates(
domain,
email,
production = false,
logger,
siteCallback,
greenlockOpts = {}
) {
const findRoot = require('find-root');
const LE = require('@root/greenlock');
greenlockOpts.packageRoot = greenlockOpts.packageRoot || findRoot(certPath);

if (!greenlockOpts.packageAgent) {
const pkg = require(greenlockOpts.packageRoot + '/package.json');
greenlockOpts.packageAgent = greenlockOpts.packageAgent || pkg.name + '/' + pkg.version;
}

if (!siteCallback) {
siteCallback = () => {};
}

// ACME Challenge Handlers
const leChallenge = {
module: require.resolve('acme-http-01-webroot'),
webroot: webrootPath,
debug: !production,
};

/**
* @see https://git.rootprojects.org/root/acme.js#events
* @param {string} eventName
* @param {*} details
*/
const notifyLogger = (eventName, details) => {
switch (eventName) {
case 'error':
logger.error(details, eventName);
break;
case 'warning':
logger.warn(details, eventName);
break;
default:
logger.debug(details, eventName);
break;
}
};

const le = LE.create({
...greenlockOpts,
staging: !production,
maintainerEmail: email,
manager: '@greenlock/manager',
notify: notifyLogger,
});

le.manager.defaults({
agreeToTerms: true,
subscriberEmail: email,
challenges: {
// handles /.well-known/acme-challege keys and tokens
'http-01': leChallenge,
},
store: leStoreConfig, // handles saving of config, accounts, and certificates
});

const onSite = (site) => {
site.pems.fullchain = site.pems.cert + '\n' + site.pems.chain + '\n';
logger.info('Get LetsEncrypt certificates for ' + domain);
siteCallback(null, site);
};

// TODO test me
const onAdd = async (err, event) => {
if (err) {
logger.error(err, 'Error registering LetsEncrypt certificates for ' + domain);
return siteCallback(err);
}
logger.debug(event, 'After add');
const site = await le.get({ servername: domain });
if (site) {
return onSite(site);
}

return siteCallback(new Error(domain + ' was not found in any site config'));
};

const site = await le
.get({
servername: domain,
});

if (site) {
return onSite(site)
}

return le
.add({
subject: domain,
altnames: [domain],
})
.then(onAdd.bind(this, null))
.catch(onAdd);
}

module.exports.init = init;
module.exports.getCertificates = getCertificates;

Loading