'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; 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')) { conn.once('data', handleChunk); return; } conn.removeListener('error', handleErr); conn.pause(); resolve(opts.firstChunk.toString()); } } 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 hostMatchesDomains(req, domains) { var host = separatePort((req.headers || req).host).host.toLowerCase(); return domains.some(function (pattern) { return domainMatches(pattern, host); }); } function determinePrimaryHost() { var result; if (Array.isArray(conf.http.domains)) { conf.http.domains.some(function (dom) { return dom.names.some(function (domain) { if (domain[0] !== '*') { result = domain; return true; } }); }); } if (result) { return result; } if (Array.isArray(conf.http.modules)) { conf.http.modules.some(function (mod) { return mod.domains.some(function (domain) { if (domain[0] !== '*') { result = domain; return true; } }); }); } return result; } // 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)) { var dest; if (conf.http.primaryDomain) { dest = conf.http.primaryDomain; } else { dest = determinePrimaryHost(); } if (dest) { req.headers.host = dest + (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); conn.resume(); }); // 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 (deps.tunnelServer.isClientDomain(separatePort(headers.host).host)) { deps.tunnelServer.handleClientConn(conn); process.nextTick(function () { conn.unshift(opts.firstChunk); conn.resume(); }); return true; } if (!acmeServer) { acmeServer = require('http').createServer(greenlockMiddleware); } return emitConnection(acmeServer, conn, opts); } function checkLoopback(conn, opts, headers) { if (headers.url.indexOf('/.well-known/cloud-challenge/') !== 0) { return false; } return emitConnection(deps.ddns.loopbackServer, 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 adminDomains; var adminServer; function checkAdmin(conn, opts, headers) { var host = separatePort(headers.host).host; if (!adminDomains) { adminDomains = require('./admin').adminDomains; } if (adminDomains.indexOf(host) !== -1) { if (!adminServer) { adminServer = require('./admin').create(deps, conf); } return emitConnection(adminServer, conn, opts); } if (deps.tunnelServer.isAdminDomain(host)) { deps.tunnelServer.handleAdminConn(conn); process.nextTick(function () { conn.unshift(opts.firstChunk); conn.resume(); }); return true; } return false; } var proxyServer; function createProxyServer() { var http = require('http'); var agent = new http.Agent(); agent.createConnection = deps.net.createConnection; var proxy = require('http-proxy').createProxyServer({ agent: agent , toProxy: true }); proxy.on('error', function (err, req, res) { res.statusCode = 502; res.setHeader('Connection', 'close'); res.setHeader('Content-Type', 'text/html'); res.end(require('../proxy-conn').getRespBody(err, conf.debug)); }); proxyServer = http.createServer(function (req, res) { proxy.web(req, res, req.connection.proxyOpts); }); proxyServer.on('upgrade', function (req, socket, head) { proxy.ws(req, socket, head, socket.proxyOpts); }); } function proxyRequest(mod, conn, opts, xHeaders) { if (!proxyServer) { createProxyServer(); } conn.proxyOpts = { target: 'http://'+(mod.address || (mod.host || 'localhost')+':'+mod.port) , headers: xHeaders }; return emitConnection(proxyServer, conn, opts); } function proxyWebsocket(mod, conn, opts, headers, xHeaders) { 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. Object.keys(xHeaders).forEach(function (key) { headLines.push(key + ': ' +xHeaders[key]); }); // 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.port = newConnOpts.port || mod.port; newConnOpts.host = newConnOpts.host || mod.host || 'localhost'; 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); } function checkProxy(mod, conn, opts, headers) { var xHeaders = {}; // Then add our own `X-Forwarded` headers at the end. if (conf.http.trustProxy && headers['x-forwarded-proto']) { xHeaders['X-Forwarded-Proto'] = headers['x-forwarded-proto']; } else { xHeaders['X-Forwarded-Proto'] = conn.encrypted ? 'https' : 'http'; } var proxyChain = (headers['x-forwarded-for'] || '').split(/ *, */).filter(Boolean); proxyChain.push(opts.remoteAddress || opts.address || conn.remoteAddress); xHeaders['X-Forwarded-For'] = proxyChain.join(', '); xHeaders['X-Forwarded-Host'] = headers.host; if ((headers.connection || '').toLowerCase() === 'upgrade') { proxyWebsocket(mod, conn, opts, headers, xHeaders); } else { proxyRequest(mod, conn, opts, xHeaders); } return true; } function checkRedirect(mod, conn, opts, headers) { 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) { 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; }) ; } var moduleChecks = { proxy: checkProxy , redirect: checkRedirect , static: checkStatic }; function handleConnection(conn) { var opts = conn.__opts; parseHeaders(conn, opts) .then(function (headers) { if (checkAcme(conn, opts, headers)) { return; } if (checkLoopback(conn, opts, headers)) { return; } if (checkHttps(conn, opts, headers)) { return; } if (checkAdmin(conn, opts, headers)) { return; } var prom = PromiseA.resolve(false); (conf.http.domains || []).forEach(function (dom) { prom = prom.then(function (handled) { if (handled) { return handled; } if (!hostMatchesDomains(headers, dom.names)) { return false; } var subProm = PromiseA.resolve(false); dom.modules.forEach(function (mod) { if (moduleChecks[mod.name]) { subProm = subProm.then(function (handled) { if (handled) { return handled; } return moduleChecks[mod.name](mod, conn, opts, headers); }); } else { console.warn('unknown HTTP module under domains', dom.names.join(','), mod); } }); return subProm; }); }); (conf.http.modules || []).forEach(function (mod) { prom = prom.then(function (handled) { if (handled) { return handled; } if (!hostMatchesDomains(headers, mod.domains)) { return false; } if (moduleChecks[mod.name]) { return moduleChecks[mod.name](mod, conn, opts, headers); } console.warn('unknown HTTP module found', mod); }); }); prom.then(function (handled) { // XXX TODO SECURITY html escape var host = (headers.host || '[no host header]').replace(/