'use strict'; module.exports.create = function (deps, conf, greenlockMiddleware) { var PromiseA = require('bluebird'); var express = require('express'); var app = express(); var adminApp = require('./admin').create(deps, conf); var domainMatches = require('../domain-utils').match; var separatePort = require('../domain-utils').separatePort; var adminDomains = [ /\blocalhost\.admin\./ , /\blocalhost\.alpha\./ , /\badmin\.localhost\./ , /\balpha\.localhost\./ ]; 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; }); } function moduleMatchesHost(req, mod) { var host = separatePort((req.headers || req).host).host; return mod.domains.some(function (pattern) { return domainMatches(pattern, host); }); } function verifyHost(fullHost) { var host = separatePort(fullHost).host; 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; } // 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) { if (conf.http.allowInsecure) { next(); return; } var port = separatePort(req.headers.host).port; if (!redirecters[port]) { redirecters[port] = require('redirect-https')({ port: port , trustProxy: conf.http.trustProxy }); } // 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); redirecters[port](req, res, next); } function handleAdmin(req, res, next) { var admin = adminDomains.some(function (re) { return re.test(req.headers.host); }); if (admin) { adminApp(req, res); } else { next(); } } function respond404(req, res) { res.writeHead(404); res.end('Not Found'); } 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); }); res.writeHead(mod.status || 301, { 'Location': to }); res.end(); }; } 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) { if (moduleMatchesHost(req, mod)) { getStaticApp(separatePort(req.headers.host).host)(req, res, next); } else { next(); } }; } app.use(greenlockMiddleware); app.use(redirectHttps); app.use(handleAdmin); (conf.http.modules || []).forEach(function (mod) { if (mod.name === 'redirect') { app.use(createRedirectRoute(mod)); } else if (mod.name === 'static') { app.use(createStaticRoute(mod)); } else { console.warn('unknown HTTP module', mod); } }); app.use(respond404); 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); }); // 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; } 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; } 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; 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); } } }; };