From f6cc67ff53203f3910c0c4e36cb4db3980954d2b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 4 Apr 2019 00:35:15 -0600 Subject: [PATCH] v2.7.4: add a server I use --- config.js | 9 +++ install.sh | 14 ++++ package-lock.json | 8 +- package.json | 5 +- server.js | 196 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 config.js create mode 100644 install.sh create mode 100644 server.js diff --git a/config.js b/config.js new file mode 100644 index 0000000..7017760 --- /dev/null +++ b/config.js @@ -0,0 +1,9 @@ +'use strict'; + +var path = require('path'); +module.exports = { + email: 'jon.doe@example.com' +, configDir: path.join(__dirname, 'acme') +, srv: '/srv/www/' +, api: '/srv/api/' +}; diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e6a50c3 --- /dev/null +++ b/install.sh @@ -0,0 +1,14 @@ +# This is just an example (but it works) +export NODE_PATH=$NPM_CONFIG_PREFIX/lib/node_modules +export NPM_CONFIG_PREFIX=/opt/node +curl -fsSL https://bit.ly/node-installer | bash + +/opt/node/bin/node /opt/node/bin/npm config set scripts-prepend-node-path true +/opt/node/bin/node /opt/node/bin/npm ci +sudo setcap 'cap_net_bind_service=+ep' /opt/node/bin/node +/opt/node/bin/node /opt/node/bin/npm start + +sudo rsync -av dist/etc/systemd/system/greenlock-express.service /etc/systemd/system/ +sudo systemctl daemon-reload + +sudo systemctl restart greenlock-express diff --git a/package-lock.json b/package-lock.json index 0b08357..ed76b3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "greenlock-express", - "version": "2.7.1", + "version": "2.7.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -334,9 +334,9 @@ } }, "le-store-fs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/le-store-fs/-/le-store-fs-1.0.0.tgz", - "integrity": "sha512-UVGFYwZO/kzkeoIbnbuPyUCB2HMWHAoKJQhsIeunyFakIa4J1ozqy136h3uV3GulSN+99ZJfQBT5aoqVZsmfzw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/le-store-fs/-/le-store-fs-1.0.1.tgz", + "integrity": "sha512-5/Q4mfPGXlmki3ccPar796oNkZ21bJCY/AAHmqjKlHBUF2Ck1hlnaADP/2nQFK4UNzPACVAgvZSv/f1rZYvwdA==", "requires": { "mkdirp": "^0.5.1", "safe-replace": "^1.1.0" diff --git a/package.json b/package.json index 29b342f..dc2dd12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "greenlock-express", - "version": "2.7.3", + "version": "2.7.4", "description": "Free SSL and managed or automatic HTTPS for node.js with Express, Koa, Connect, Hapi, and all other middleware systems.", "main": "index.js", "homepage": "https://git.coolaj86.com/coolaj86/greenlock-express.js", @@ -13,7 +13,7 @@ "le-challenge-fs": "^2.0.8", "le-sni-auto": "^2.1.8", "le-store-certbot": "^2.1.0", - "le-store-fs": "^1.0.0", + "le-store-fs": "^1.0.1", "redirect-https": "^1.1.5" }, "files": [ @@ -31,6 +31,7 @@ "ws": "^5.2.1" }, "scripts": { + "start": "node server.js ./config.js", "test": "node test/greenlock.js" }, "repository": { diff --git a/server.js b/server.js new file mode 100644 index 0000000..f2c825d --- /dev/null +++ b/server.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node +'use strict'; +/*global Promise*/ + +///////////////////////////////// +// an okay vhost + api example // +///////////////////////////////// + +// +// I run this on a few servers. It demonstrates dynamic virtual hosting + apis +// /srv/www -> static sites in plain folders +// ex: /srv/www/example.com +// +// /srv/api -> express apps +// ex: /srv/api/api.example.com +// + +var configpath = process.argv[2] || './config.js'; +var config = require(configpath); +// The prefix where sites go by name. +// For example: whatever.com may live in /srv/www/whatever.com, thus /srv/www is our path + +var path = require('path'); +var fs = require('fs').promises; +var finalhandler = require('finalhandler'); +var serveStatic = require('serve-static'); + +//var glx = require('greenlock-express') +var glx = require('./').create({ + + version: 'draft-11' // Let's Encrypt v2 is ACME draft 11 + +//, server: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, server: 'https://acme-v02.api.letsencrypt.org/directory' // If at first you don't succeed, stop and switch to staging + // https://acme-staging-v02.api.letsencrypt.org/directory + +, configDir: config.configDir // You MUST have access to write to directory where certs + // are saved. ex: /home/foouser/.config/acme + +, approveDomains: myApproveDomains // Greenlock's wraps around tls.SNICallback. Check the + // domain name here and reject invalid ones + +, app: myVhostApp // Any node-style http app (i.e. express, koa, hapi, rill) + + /* CHANGE TO A VALID EMAIL */ +, email: config.email // Email for Let's Encrypt account and Greenlock Security +, agreeTos: true // Accept Let's Encrypt ToS +//, communityMember: true // Join Greenlock to get important updates, no spam + +//, debug: true +, store: require('le-store-fs') + +}); + +var server = glx.listen(80, 443); +server.on('listening', function () { + console.info(server.type + " listening on", server.address()); +}); + +function myApproveDomains(opts) { + console.log(opts.domain); + // In this example the filesystem is our "database". + // We check in /srv/www for whatever.com and if it exists, it's allowed + // SECURITY Greenlock validates opts.domains ahead-of-time so you don't have to + + var domains = []; + var domain = opts.domain.replace(/^(www|api)\./, ''); + return checkWwws(domain).then(function (hostname) { + // tried both permutations already (failed first, succeeded second) + if (hostname !== domain) { + domains.push(hostname); + return; + } + + // only tried the bare domain, let's try www too + domains.push(domain); + return checkWwws('www.' + domain).then(function (hostname) { + if (hostname === domain) { + domains.push(domain); + } + }); + }).catch(function () { + // ignore error + return null; + }).then(function () { + // check for api prefix + return checkApi('api.' + domain).then(function () { + domains.push(opts.domain); + }).catch(function () { + return null; + }); + }).then(function () { + if (0 === domains.length) { + return Promise.reject(new Error("no bare, www., or api. domain matching '" + opts.domain + "'")); + } + + //opts.email = email; + opts.agreeTos = true; + // pick the shortest (bare) or latest (www. instead of api.) to be the subject + opts.subject = opts.domains.sort(function (a, b) { + var len = a.length - b.length; + if (0 !== len) { return len; } + if (a < b) { return 1; } else { return -1; } + })[0]; + + if (!opts.challenges) { opts.challenges = {}; } + opts.challenges['http-01'] = require('le-challenge-fs'); + //opts.challenges['dns-01'] = require('le-challenge-dns'); + + // explicitly set account id and certificate.id + opts.account = { id: opts.email }; + opts.certificate = { id: opts.subject }; + + return Promise.resolve(opts); + }); +} + +function checkApi(hostname) { + var apipath = path.join(config.api, hostname); + var link = ''; + return fs.stat(apipath).then(function (stats) { + if (stats.isDirectory()) { + return require(apipath); + } + return fs.readFile(apipath, 'utf8').then(function (txt) { + var linkpath = txt.split('\n')[0]; + link = (' => ' + linkpath + ' '); + return require(linkpath); + }); + }).catch(function (e) { + if ('ENOENT' === e.code) { return null; } + throw new Error("rejecting '" + hostname + "' because '" + apipath + link + "' failed at require()"); + }); +} + +function checkWwws(_hostname) { + var hostname = _hostname; + var hostdir = path.join(config.srv, hostname); + // TODO could test for www/no-www both in directory + return fs.readdir(hostdir).then(function () { + // TODO check for some sort of htaccess.json and use email in that + // NOTE: you can also change other options such as `challengeType` and `challenge` + // opts.challengeType = 'http-01'; + // opts.challenge = require('le-challenge-fs').create({}); + return hostname; + }).catch(function () { + if ('www.' === hostname.slice(0, 4)) { + // Assume we'll redirect to non-www if it's available. + hostname = hostname.slice(4); + hostdir = path.join(config.srv, hostname); + return fs.readdir(hostdir).then(function () { + return hostname; + }); + } else { + // Or check and see if perhaps we should redirect non-www to www + hostname = 'www.' + hostname; + hostdir = path.join(config.srv, hostname); + return fs.readdir(hostdir).then(function () { + return hostname; + }); + } + }).catch(function () { + throw new Error("rejecting '" + _hostname + "' because '" + hostdir + "' could not be read"); + }); +} + +function myVhostApp(req, res) { + // SECURITY greenlock pre-sanitizes hostnames to prevent unauthorized fs access so you don't have to + // (also: only domains approved above will get here) + console.log(req.method); + console.log(req.url); + console.log(req.headers); + + // We could cache wether or not a host exists for some amount of time + var fin = finalhandler(req, res); + return checkWwws(req.headers.host).then(function (hostname) { + if (hostname !== req.headers.host) { + res.statusCode = 302; + res.setHeader('Location', 'https://' + hostname); + // SECURITY this is safe only because greenlock disallows invalid hostnames + res.end(""); + return; + } + var serve = serveStatic(path.join(config.srv, hostname), { redirect: true }); + serve(req, res, fin); + }).catch(function (err) { + return checkApi(req.headers.host).then(function (app) { + if (app) { app(req, res); return; } + console.log("www error", err); + fin(); + }).catch(function (err) { + console.log("api error", err); + fin(err); + }); + }); +}