'use strict'; module.exports.create = function (deps, conf, greenlockMiddleware) { var PromiseA = require('bluebird'); var statAsync = PromiseA.promisify(require('fs').stat); 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); }); } // 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 = {}; var ipv4Re = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; var ipv6Re = /^\[[0-9a-fA-F:]+\]$/; function redirectHttps(req, res) { var host = separatePort(req.headers.host); if (!redirecters[host.port]) { redirecters[host.port] = require('redirect-https')({ port: host.port }); } // 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. if (host.host === 'localhost') { req.headers.host = 'localhost.daplie.me' + (host.port ? ':'+host.port : ''); } // 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 (ipv4Re.test(host.host) || ipv6Re.test(host.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; } }); }); } if (conf.http.primaryDomain) { req.headers.host = conf.http.primaryDomain + (host.port ? ':'+host.port : ''); } } redirecters[host.port](req, res); } function emitConnection(server, 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; } var acmeServer; function checkACME(conn, opts, headers) { if (headers.url.indexOf('/.well-known/acme-challenge/') !== 0) { return false; } if (!acmeServer) { acmeServer = require('http').createServer(greenlockMiddleware); } return emitConnection(acmeServer, conn, opts); } var httpsRedirectServer; function checkHttps(conn, opts, headers) { if (conf.http.allowInsecure || conn.encrypted) { return false; } if (conf.http.trustProxy && 'https' === headers['x-forwarded-proto']) { return false; } if (!httpsRedirectServer) { httpsRedirectServer = require('http').createServer(redirectHttps); } return emitConnection(httpsRedirectServer, conn, opts); } var adminServer; function checkAdmin(conn, opts, headers) { var admin = adminDomains.some(function (re) { return re.test(headers.host); }); if (admin) { if (!adminServer) { adminServer = require('./admin').create(deps, conf); } return emitConnection(adminServer, conn, opts); } return false; } function checkProxy(mod, conn, opts, headers) { if (!moduleMatchesHost(headers, mod)) { return false; } var index = opts.firstChunk.indexOf('\r\n\r\n'); var body = opts.firstChunk.slice(index); var head = opts.firstChunk.slice(0, index).toString(); var headLines = head.split('\r\n'); // First strip any existing `X-Forwarded-*` headers (for security purposes?) headLines = headLines.filter(function (line) { return !/^x-forwarded/i.test(line); }); // Then add our own `X-Forwarded` headers at the end. if (conf.http.trustProxy && headers['x-forwarded-proto']) { headLines.push('X-Forwarded-Proto: ' + headers['x-forwarded-proto']); } else { headLines.push('X-Forwarded-Proto: ' + conn.encrypted ? 'https' : 'http'); } var proxyChain = (headers['x-forwarded-for'] || '').split(/ *, */).filter(Boolean); proxyChain.push(opts.remoteAddress || opts.address || conn.remoteAddress); headLines.push('X-Forwarded-For: ' + proxyChain.join(', ')); headLines.push('X-Forwarded-Host: ' + headers.host); // Then convert all of the head lines back into a header buffer. head = Buffer.from(headLines.join('\r\n')); opts.firstChunk = Buffer.concat([head, body]); 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; deps.proxy(conn, newConnOpts, opts.firstChunk); return true; } function checkRedirect(mod, conn, opts, headers) { if (!moduleMatchesHost(headers, mod)) { return false; } if (!mod.fromRe || mod.fromRe.origSrc !== mod.from) { // 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 + '/?$'); fromRe.origSrc = mod.from; // We don't want this property showing up in the actual config file or the API, // so we define it this way so it's not enumberable. Object.defineProperty(mod, 'fromRe', {value: fromRe, configurable: true}); } var match = mod.fromRe.exec(headers.url); if (!match) { return false; } var to = mod.to; match.slice(1).forEach(function (globMatch, index) { to = to.replace(':'+(index+1), globMatch); }); var status = mod.status || 301; var code = require('http').STATUS_CODES[status] || 'Unknown'; conn.end([ 'HTTP/1.1 ' + status + ' ' + code , 'Date: ' + (new Date()).toUTCString() , 'Location: ' + to , 'Connection: close' , 'Content-Length: 0' , '' , '' ].join('\r\n')); return true; } var staticServer; var staticHandlers = {}; function serveStatic(req, res) { var rootDir = req.connection.rootDir; if (!staticHandlers[rootDir]) { staticHandlers[rootDir] = require('express').static(rootDir, { fallthrough: false }); } staticHandlers[rootDir](req, res, function (err) { if (err) { res.statusCode = err.statusCode; } else { res.statusCode = 404; } res.setHeader('Content-Type', 'text/html'); if (res.statusCode === 404) { res.end('File Not Found'); } else { res.end(require('http').STATUS_CODES[res.statusCode]); } }); } function checkStatic(mod, conn, opts, headers) { if (!moduleMatchesHost(headers, mod)) { return false; } var rootDir = mod.root.replace(':hostname', separatePort(headers.host).host); return statAsync(rootDir) .then(function (stats) { if (!stats || !stats.isDirectory()) { return false; } if (!staticServer) { staticServer = require('http').createServer(serveStatic); } conn.rootDir = rootDir; return emitConnection(staticServer, conn, opts); }) .catch(function (err) { if (err.code !== 'ENOENT') { console.warn('errored stating', rootDir, 'for serving static files', err); } return false; }) ; } function handleConnection(conn) { var opts = conn.__opts; parseHeaders(conn, opts) .then(function (headers) { if (checkACME(conn, opts, headers)) { return; } if (checkHttps(conn, opts, headers)) { return; } if (checkAdmin(conn, opts, headers)) { return; } var prom = PromiseA.resolve(false); (conf.http.modules || []).forEach(function (mod) { prom = prom.then(function (handled) { if (handled) { return handled; } if (mod.name === 'proxy') { return checkProxy(mod, conn, opts, headers); } if (mod.name === 'redirect') { return checkRedirect(mod, conn, opts, headers); } if (mod.name === 'static') { return checkStatic(mod, conn, opts, headers); } console.warn('unknown HTTP module found', mod); }); }); prom.then(function (handled) { if (!handled) { conn.end([ 'HTTP/1.1 404 Not Found' , 'Date: ' + (new Date()).toUTCString() , 'Content-Type: text/html' , 'Content-Length: 9' , 'Connection: close' , '' , 'Not Found' ].join('\r\n')); } }); }) ; } return { emit: function (type, value) { if (type === 'connection') { handleConnection(value); } } }; };