2017-04-27 22:05:34 +00:00
|
|
|
'use strict';
|
|
|
|
|
2017-05-10 22:05:54 +00:00
|
|
|
module.exports.create = function (deps, conf, greenlockMiddleware) {
|
2017-05-16 23:19:26 +00:00
|
|
|
var PromiseA = require('bluebird');
|
2017-05-09 22:50:07 +00:00
|
|
|
var express = require('express');
|
|
|
|
var app = express();
|
2017-05-09 21:46:49 +00:00
|
|
|
var adminApp = require('./admin').create(deps, conf);
|
2017-05-16 19:04:08 +00:00
|
|
|
var domainMatches = require('../domain-utils').match;
|
|
|
|
var separatePort = require('../domain-utils').separatePort;
|
2017-05-09 20:16:21 +00:00
|
|
|
|
2017-05-09 21:46:49 +00:00
|
|
|
var adminDomains = [
|
|
|
|
/\blocalhost\.admin\./
|
|
|
|
, /\blocalhost\.alpha\./
|
|
|
|
, /\badmin\.localhost\./
|
|
|
|
, /\balpha\.localhost\./
|
|
|
|
];
|
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
function parseHeaders(conn, opts) {
|
|
|
|
// There should already be a `firstChunk` on the opts, but because we might sometimes
|
|
|
|
// need more than that to get all the headers it's easier to always read the data off
|
|
|
|
// the connection and put it back later if we need to.
|
|
|
|
opts.firstChunk = Buffer.alloc(0);
|
|
|
|
|
|
|
|
// First we make sure we have all of the headers.
|
|
|
|
return new PromiseA(function (resolve, reject) {
|
|
|
|
if (opts.firstChunk.includes('\r\n\r\n')) {
|
|
|
|
resolve(opts.firstChunk.toString());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var errored = false;
|
|
|
|
function handleErr(err) {
|
|
|
|
errored = true;
|
|
|
|
reject(err);
|
|
|
|
}
|
|
|
|
conn.once('error', handleErr);
|
|
|
|
|
|
|
|
function handleChunk(chunk) {
|
|
|
|
if (!errored) {
|
|
|
|
opts.firstChunk = Buffer.concat([opts.firstChunk, chunk]);
|
|
|
|
if (opts.firstChunk.includes('\r\n\r\n')) {
|
|
|
|
resolve(opts.firstChunk.toString());
|
|
|
|
conn.removeListener('error', handleErr);
|
|
|
|
} else {
|
|
|
|
conn.once('data', handleChunk);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
conn.once('data', handleChunk);
|
|
|
|
}).then(function (firstStr) {
|
|
|
|
var headerSection = firstStr.split('\r\n\r\n')[0];
|
|
|
|
var lines = headerSection.split('\r\n');
|
|
|
|
var result = {};
|
|
|
|
|
|
|
|
lines.slice(1).forEach(function (line) {
|
|
|
|
var match = /(.*)\s*:\s*(.*)/.exec(line);
|
|
|
|
if (match) {
|
|
|
|
result[match[1].toLowerCase()] = match[2];
|
|
|
|
} else {
|
|
|
|
console.error('HTTP header line does not match pattern', line);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var match = /^([a-zA-Z]+)\s+(\S+)\s+HTTP/.exec(lines[0]);
|
|
|
|
if (!match) {
|
|
|
|
throw new Error('first line of "HTTP" does not match pattern: '+lines[0]);
|
|
|
|
}
|
|
|
|
result.method = match[1].toUpperCase();
|
|
|
|
result.url = match[2];
|
|
|
|
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-05-16 19:04:08 +00:00
|
|
|
function moduleMatchesHost(req, mod) {
|
2017-05-16 23:19:26 +00:00
|
|
|
var host = separatePort((req.headers || req).host).host;
|
2017-05-16 19:04:08 +00:00
|
|
|
|
|
|
|
return mod.domains.some(function (pattern) {
|
|
|
|
return domainMatches(pattern, host);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-05-12 01:16:23 +00:00
|
|
|
function verifyHost(fullHost) {
|
2017-05-16 19:04:08 +00:00
|
|
|
var host = separatePort(fullHost).host;
|
2017-05-12 01:16:23 +00:00
|
|
|
|
|
|
|
if (host === 'localhost') {
|
|
|
|
return fullHost.replace(host, 'localhost.daplie.me');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test for IPv4 and IPv6 addresses. These patterns will match some invalid addresses,
|
|
|
|
// but since those still won't be valid domains that won't really be a problem.
|
|
|
|
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) {
|
|
|
|
if (!conf.http.primaryDomain) {
|
|
|
|
(conf.http.modules || []).some(function (mod) {
|
|
|
|
return mod.domains.some(function (domain) {
|
|
|
|
if (domain[0] !== '*') {
|
|
|
|
conf.http.primaryDomain = domain;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return fullHost.replace(host, conf.http.primaryDomain || host);
|
|
|
|
}
|
|
|
|
|
|
|
|
return fullHost;
|
|
|
|
}
|
|
|
|
|
2017-05-09 20:16:21 +00:00
|
|
|
// We handle both HTTPS and HTTP traffic on the same ports, and we want to redirect
|
|
|
|
// any unencrypted requests to the same port they came from unless it came in on
|
|
|
|
// the default HTTP port, in which case there wont be a port specified in the host.
|
|
|
|
var redirecters = {};
|
|
|
|
function redirectHttps(req, res, next) {
|
2017-05-12 01:16:23 +00:00
|
|
|
if (conf.http.allowInsecure) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-05-16 19:04:08 +00:00
|
|
|
var port = separatePort(req.headers.host).port;
|
2017-05-10 22:05:54 +00:00
|
|
|
if (!redirecters[port]) {
|
|
|
|
redirecters[port] = require('redirect-https')({
|
|
|
|
port: port
|
|
|
|
, trustProxy: conf.http.trustProxy
|
|
|
|
});
|
2017-05-09 20:16:21 +00:00
|
|
|
}
|
2017-05-12 01:16:23 +00:00
|
|
|
|
|
|
|
// localhost and IP addresses cannot have real SSL certs (and don't contain any useful
|
|
|
|
// info for redirection either), so we direct some hosts to either localhost.daplie.me
|
|
|
|
// or the "primary domain" ie the first manually specified domain.
|
|
|
|
req.headers.host = verifyHost(req.headers.host);
|
|
|
|
|
2017-05-10 22:05:54 +00:00
|
|
|
redirecters[port](req, res, next);
|
2017-05-09 20:16:21 +00:00
|
|
|
}
|
|
|
|
|
2017-05-09 21:46:49 +00:00
|
|
|
function handleAdmin(req, res, next) {
|
|
|
|
var admin = adminDomains.some(function (re) {
|
|
|
|
return re.test(req.headers.host);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (admin) {
|
|
|
|
adminApp(req, res);
|
|
|
|
} else {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-09 20:16:21 +00:00
|
|
|
function respond404(req, res) {
|
|
|
|
res.writeHead(404);
|
|
|
|
res.end('Not Found');
|
|
|
|
}
|
|
|
|
|
2017-05-16 19:04:08 +00:00
|
|
|
function createRedirectRoute(mod) {
|
|
|
|
// Escape any characters that (can) have special meaning in regular expression
|
|
|
|
// but that aren't the special characters we have interest in.
|
|
|
|
var from = mod.from.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&');
|
|
|
|
// Then modify the characters we are interested in so they do what we want in
|
|
|
|
// the regular expression after being compiled.
|
|
|
|
from = from.replace(/\*/g, '(.*)');
|
|
|
|
var fromRe = new RegExp('^' + from + '/?$');
|
|
|
|
|
|
|
|
return function (req, res, next) {
|
|
|
|
if (!moduleMatchesHost(req, mod)) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var match = fromRe.exec(req.url);
|
|
|
|
if (!match) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var to = mod.to;
|
|
|
|
match.slice(1).forEach(function (globMatch, index) {
|
|
|
|
to = to.replace(':'+(index+1), globMatch);
|
|
|
|
});
|
2017-05-16 19:11:27 +00:00
|
|
|
res.writeHead(mod.status || 301, { 'Location': to });
|
2017-05-16 19:04:08 +00:00
|
|
|
res.end();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-05-09 22:50:07 +00:00
|
|
|
function createStaticRoute(mod) {
|
|
|
|
var getStaticApp, staticApp;
|
|
|
|
if (/:hostname/.test(mod.root)) {
|
|
|
|
staticApp = {};
|
|
|
|
getStaticApp = function (hostname) {
|
|
|
|
if (!staticApp[hostname]) {
|
|
|
|
staticApp[hostname] = express.static(mod.root.replace(':hostname', hostname));
|
|
|
|
}
|
|
|
|
return staticApp[hostname];
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
staticApp = express.static(mod.root);
|
|
|
|
getStaticApp = function () {
|
|
|
|
return staticApp;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return function (req, res, next) {
|
2017-05-16 19:04:08 +00:00
|
|
|
if (moduleMatchesHost(req, mod)) {
|
|
|
|
getStaticApp(separatePort(req.headers.host).host)(req, res, next);
|
2017-05-09 22:50:07 +00:00
|
|
|
} else {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-05-10 22:05:54 +00:00
|
|
|
app.use(greenlockMiddleware);
|
2017-05-09 20:16:21 +00:00
|
|
|
app.use(redirectHttps);
|
2017-05-09 21:46:49 +00:00
|
|
|
app.use(handleAdmin);
|
2017-05-09 20:16:21 +00:00
|
|
|
|
|
|
|
(conf.http.modules || []).forEach(function (mod) {
|
2017-05-16 23:19:26 +00:00
|
|
|
if (mod.name === 'redirect') {
|
2017-05-16 19:04:08 +00:00
|
|
|
app.use(createRedirectRoute(mod));
|
|
|
|
}
|
2017-05-09 22:50:07 +00:00
|
|
|
else if (mod.name === 'static') {
|
|
|
|
app.use(createStaticRoute(mod));
|
|
|
|
}
|
2017-05-09 20:16:21 +00:00
|
|
|
else {
|
|
|
|
console.warn('unknown HTTP module', mod);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
app.use(respond404);
|
2017-05-16 07:20:02 +00:00
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
var server = require('http').createServer(app);
|
|
|
|
|
|
|
|
function handleHttp(conn, opts) {
|
|
|
|
server.emit('connection', conn);
|
|
|
|
|
|
|
|
// We need to put back whatever data we read off to determine the connection was HTTP
|
|
|
|
// and to parse the headers. Must be done after data handlers added but before any new
|
|
|
|
// data comes in.
|
|
|
|
process.nextTick(function () {
|
|
|
|
conn.unshift(opts.firstChunk);
|
|
|
|
});
|
2017-05-16 07:20:02 +00:00
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
// Convenience return for all the check* functions.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkACME(conn, opts, headers) {
|
|
|
|
if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) {
|
|
|
|
return false;
|
2017-05-16 07:20:02 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
return handleHttp(conn, opts);
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkRedirect(conn, opts, headers) {
|
|
|
|
if (conf.http.allowInsecure || conn.encrypted) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) {
|
|
|
|
return false;
|
2017-05-16 07:20:02 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
return handleHttp(conn, opts);
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkAdmin(conn, opts, headers) {
|
|
|
|
var admin = adminDomains.some(function (re) {
|
|
|
|
return re.test(headers.host);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (admin) {
|
|
|
|
return handleHttp(conn, opts);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkProxy(mod, conn, opts, headers) {
|
|
|
|
if (!moduleMatchesHost(headers, mod)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var connected = false;
|
|
|
|
var newConnOpts = separatePort(mod.address);
|
|
|
|
newConnOpts.servername = separatePort(headers.host).host;
|
|
|
|
newConnOpts.data = opts.firstChunk;
|
2017-05-16 07:20:02 +00:00
|
|
|
|
2017-05-16 23:19:26 +00:00
|
|
|
newConnOpts.remoteFamily = opts.family || conn.remoteFamily;
|
|
|
|
newConnOpts.remoteAddress = opts.address || conn.remoteAddress;
|
|
|
|
newConnOpts.remotePort = opts.port || conn.remotePort;
|
|
|
|
|
|
|
|
var newConn = deps.net.createConnection(newConnOpts, function () {
|
|
|
|
connected = true;
|
|
|
|
newConn.write(opts.firstChunk);
|
|
|
|
newConn.pipe(conn);
|
|
|
|
conn.pipe(newConn);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Not sure how to effectively report this to the user or client, but we need to listen
|
|
|
|
// for the event to prevent it from crashing us.
|
|
|
|
newConn.on('error', function (err) {
|
|
|
|
if (connected) {
|
|
|
|
console.error('HTTP proxy remote error', err);
|
|
|
|
conn.end();
|
|
|
|
} else {
|
|
|
|
require('../proxy-err-resp').sendBadGateway(conn, err, conf.debug);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
conn.on('error', function (err) {
|
|
|
|
console.error('HTTP proxy client error', err);
|
|
|
|
newConn.end();
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleConnection(conn) {
|
|
|
|
var opts = conn.__opts;
|
|
|
|
parseHeaders(conn, opts)
|
|
|
|
.then(function (headers) {
|
|
|
|
if (checkACME(conn, opts, headers)) { return; }
|
|
|
|
if (checkRedirect(conn, opts, headers)) { return; }
|
|
|
|
if (checkAdmin(conn, opts, headers)) { return; }
|
|
|
|
|
|
|
|
var handled = (conf.http.modules || []).some(function (mod) {
|
|
|
|
if (mod.name === 'proxy') {
|
|
|
|
return checkProxy(mod, conn, opts, headers);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (handled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
server.emit('connection', conn);
|
|
|
|
process.nextTick(function () {
|
|
|
|
conn.unshift(opts.firstChunk);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
emit: function (type, value) {
|
|
|
|
if (type === 'connection') {
|
|
|
|
handleConnection(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2017-04-27 22:05:34 +00:00
|
|
|
};
|