diff --git a/config.js b/config.js index 478f8cd..005abd0 100644 --- a/config.js +++ b/config.js @@ -5,5 +5,16 @@ module.exports = { email: "jon.doe@example.com", configDir: path.join(__dirname, "acme"), srv: "/srv/www/", - api: "/srv/api/" + api: "/srv/api/", + proxy: { + "example.com": "http://localhost:4080", + "*.example.com": "http://localhost:4080" + }, + + // DNS-01 challenges only + challenges: { + "*.example.com": require("acme-dns-01-YOUR_DNS_HOST").create({ + token: "xxxx" + }) + } }; diff --git a/package-lock.json b/package-lock.json index 3d138ea..4eb104c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,16 +4,16 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@coolaj86/urequest": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", - "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" - }, "@root/mkdirp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" }, + "@root/request": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", + "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -38,12 +38,12 @@ "integrity": "sha512-Aa4bUpq6ftX1VODiShOetOY5U0tsXY5EV7+fQwme3Q8Y9rjYBArBXHgFCAVKtK1AF+Ev8pIuF6Z42hzMFa73/w==" }, "acme-v2": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/acme-v2/-/acme-v2-1.7.7.tgz", - "integrity": "sha512-Pg0EQ45h8N2e4K2goYedutCgWxAmtcruwDHr6hgPBgAWEORVb5SQEdXjtEhCrn+APtr7MyFPryyzXpYpDD5ecA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acme-v2/-/acme-v2-1.8.2.tgz", + "integrity": "sha512-uYGA+DuTnA44EsGXE413XnbTotGHCzkucXjMk23QRwGnaGlnr0lNBoYjByyeIVLSzj0W6Y9FqA9h+15+H+ltMw==", "requires": { - "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^2.0.6" + "@root/request": "^1.3.11", + "rsa-compat": "^2.0.8" } }, "array-flatten": { @@ -175,6 +175,12 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "dev": true + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -237,6 +243,32 @@ "unpipe": "~1.0.0" } }, + "follow-redirects": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", + "dev": true, + "requires": { + "debug": "^3.2.6" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -250,13 +282,13 @@ "dev": true }, "greenlock": { - "version": "2.7.24", - "resolved": "https://registry.npmjs.org/greenlock/-/greenlock-2.7.24.tgz", - "integrity": "sha512-GQb2LMF6IiEzhp01F6eIN7HlPVlUWpWsBZZn7DOIo9upFAWhFpn2w1PStjGb17VmTkg+lgxzcajqcy6AJhCHUQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/greenlock/-/greenlock-2.8.2.tgz", + "integrity": "sha512-pCAYjgVova1ZoUHhuCfIw/3Rs5tE6DK1YF2LI7Cyh15QFBZJNU7pngMvDfeFft3It4WqnHezNgyDWAeV2pWFaw==", "requires": { "acme": "^1.3.0", "acme-dns-01-cli": "^3.0.0", - "acme-v2": "^1.7.7", + "acme-v2": "^1.8.1", "cert-info": "^1.5.1", "greenlock-store-fs": "^3.0.2", "keypairs": "^1.2.14", @@ -287,6 +319,17 @@ "statuses": ">= 1.4.0 < 2" } }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "dev": true, + "requires": { + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -467,6 +510,12 @@ "escape-html": "^1.0.3" } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, "rsa-compat": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz", diff --git a/package.json b/package.json index 4409e7a..2091993 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "example": "examples" }, "dependencies": { - "greenlock": "^2.7.24", + "greenlock": "^2.8.2", "redirect-https": "^1.1.5" }, "files": [ @@ -18,6 +18,7 @@ "spdy": "^3.4.7" }, "devDependencies": { + "http-proxy": "^1.17.0", "express": "^4.16.3", "express-basic-auth": "^1.2.0", "finalhandler": "^1.1.1", diff --git a/server.js b/server.js index c586801..c2323c2 100644 --- a/server.js +++ b/server.js @@ -58,6 +58,31 @@ if (require.main === module) { }); } +function matchConfig(thing, domain) { + if (!thing) { + return false; + } + if (thing[domain]) { + return domain; + } + + var keys = Object.keys(thing); + var result = null; + keys.some(function(k) { + if ("*" !== k[0]) { + return; + } + + // "foo.whatever.com".endsWith("*.whatever.com".slice(1)) + if (domain.endsWith(k.slice(1).toLowerCase())) { + result = k; + return true; + } + }); + + return result; +} + function myApproveDomains(opts) { console.info("SNI:", opts.domain); // In this example the filesystem is our "database". @@ -67,6 +92,52 @@ function myApproveDomains(opts) { var domains = []; var original = opts.domain; var bare = original.replace(/^(www|api)\./, ""); + var challenger = matchConfig(config.challenges, original); + if (challenger) { + opts.challenges = { + "dns-01": config.challenges[challenger] + }; + domains.push(challenger); + return approveThem(); + } + + if (matchConfig(config.proxy, original)) { + console.log("debug: found proxy for", original); + domains.push(original); + return approveThem(); + } + + function approveThem() { + console.info("Approved domains:", domains); + opts.domains = domains; + //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); + } + // The goal here is to support both bare and www domains // // dns:example.com + fs:www.example.com => both @@ -77,7 +148,6 @@ function myApproveDomains(opts) { // // dns:example.com + fs:example.com => example.com // dns:www.example.com + fs:www.example.com => www.example.com - // return checkWwws(bare) .then(function(hostname) { // hostname is either example.com or www.example.com @@ -116,34 +186,7 @@ function myApproveDomains(opts) { return Promise.reject(new Error("no bare, www., or api. domain matching '" + opts.domain + "'")); } - console.info("Approved domains:", domains); - opts.domains = domains; - //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); + return approveThem(); }); } exports.myApproveDomains = myApproveDomains; @@ -213,12 +256,35 @@ function checkWwws(_hostname) { } exports.checkWwws = checkWwws; +var httpProxy = require("http-proxy"); + +var proxy = httpProxy.createProxyServer({ + xfwd: true +}); + +proxy.on("error", function(req, res) { + res.statusCode = 500; + res.end("500: Server Error"); +}); + function myVhostApp(req, res) { req.on("error", function(err) { console.error("HTTPS Request Network Connection Error:"); console.error(err); }); + // this is protected by greenlock-express from domain fronting attacks + var host = req.headers.host; + // ex: example.com + // ex: example.com:4080 + console.log("debug: host is", host); + var domain = matchConfig(config.proxy, host); + if (domain) { + console.log("debug: forwarding to", config.proxy[domain]); + proxy.web(req, res, { target: config.proxy[domain] }); + return; + } + // 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.info("");