Merge branch 'forwarding'
This commit is contained in:
		
						commit
						5719a8a434
					
				@ -19,7 +19,7 @@ function run(config) {
 | 
			
		||||
      worker.send(config);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  console.log('config.tcp.ports', config.tcp.ports);
 | 
			
		||||
  console.log('config.tcp.bind', config.tcp.bind);
 | 
			
		||||
  work();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -127,13 +127,13 @@ function readConfigAndRun(args) {
 | 
			
		||||
  var PromiseA = require('bluebird');
 | 
			
		||||
  var tcpProm, dnsProm;
 | 
			
		||||
 | 
			
		||||
  if (config.tcp.ports) {
 | 
			
		||||
  if (config.tcp.bind) {
 | 
			
		||||
    tcpProm = PromiseA.resolve();
 | 
			
		||||
  } else {
 | 
			
		||||
    tcpProm = new PromiseA(function (resolve, reject) {
 | 
			
		||||
      require('../lib/check-ports').checkTcpPorts(function (failed, bound) {
 | 
			
		||||
        config.tcp.ports = Object.keys(bound);
 | 
			
		||||
        if (config.tcp.ports.length) {
 | 
			
		||||
        config.tcp.bind = Object.keys(bound);
 | 
			
		||||
        if (config.tcp.bind.length) {
 | 
			
		||||
          resolve();
 | 
			
		||||
        } else {
 | 
			
		||||
          reject(failed);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								goldilocks.example.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								goldilocks.example.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
tcp:
 | 
			
		||||
  bind:
 | 
			
		||||
    - 22
 | 
			
		||||
    - 80
 | 
			
		||||
    - 443
 | 
			
		||||
  modules:
 | 
			
		||||
    - name: forward
 | 
			
		||||
      ports:
 | 
			
		||||
        - 22
 | 
			
		||||
      address: '127.0.0.1:8022'
 | 
			
		||||
 | 
			
		||||
tls:
 | 
			
		||||
  modules:
 | 
			
		||||
    - name: proxy
 | 
			
		||||
      domains:
 | 
			
		||||
        - localhost.bar.daplie.me
 | 
			
		||||
        - localhost.foo.daplie.me
 | 
			
		||||
      address: '127.0.0.1:5443'
 | 
			
		||||
 | 
			
		||||
http:
 | 
			
		||||
  trustProxy: true
 | 
			
		||||
  allowInsecure: false
 | 
			
		||||
  primaryDomain: localhost.foo.daplie.me
 | 
			
		||||
  modules:
 | 
			
		||||
    - name: proxy
 | 
			
		||||
      domains:
 | 
			
		||||
        - localhost.daplie.me
 | 
			
		||||
      address: '127.0.0.1:4000'
 | 
			
		||||
    - name: static
 | 
			
		||||
      domains:
 | 
			
		||||
        - '*.localhost.daplie.me'
 | 
			
		||||
      root: '/srv/www/:hostname'
 | 
			
		||||
@ -5,219 +5,56 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
 | 
			
		||||
  //var PromiseA = global.Promise;
 | 
			
		||||
  var PromiseA = require('bluebird');
 | 
			
		||||
  var greenlock = require('greenlock');
 | 
			
		||||
  var listeners = require('./servers').listeners;
 | 
			
		||||
  var parseSni = require('sni');
 | 
			
		||||
  var modules = { };
 | 
			
		||||
  var program = {
 | 
			
		||||
    tlsOptions: require('localhost.daplie.me-certificates').merge({})
 | 
			
		||||
//  , acmeDirectoryUrl: 'https://acme-v01.api.letsencrypt.org/directory'
 | 
			
		||||
  , acmeDirectoryUrl: 'https://acme-staging.api.letsencrypt.org/directory'
 | 
			
		||||
//  , challengeType: 'tls-sni-01' // won't work with a tunnel
 | 
			
		||||
  , challengeType: 'http-01'
 | 
			
		||||
  };
 | 
			
		||||
  var secureContexts = {};
 | 
			
		||||
  var tunnelAdminTlsOpts = {};
 | 
			
		||||
  var tls = require('tls');
 | 
			
		||||
  var modules;
 | 
			
		||||
 | 
			
		||||
  var tcpRouter = {
 | 
			
		||||
    _map: { }
 | 
			
		||||
  , _create: function (address, port) {
 | 
			
		||||
      // port provides hinting for http, smtp, etc
 | 
			
		||||
      return function (conn, firstChunk, opts) {
 | 
			
		||||
        console.log('[tcpRouter] ' + address + ':' + port + ' ' + (opts.servername || ''));
 | 
			
		||||
 | 
			
		||||
        var m;
 | 
			
		||||
        var str;
 | 
			
		||||
        var hostname;
 | 
			
		||||
        var newHeads;
 | 
			
		||||
 | 
			
		||||
        // TODO test per-module
 | 
			
		||||
        // Maybe HTTP
 | 
			
		||||
        if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | 
			
		||||
          str = firstChunk.toString();
 | 
			
		||||
          m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im);
 | 
			
		||||
          hostname = (m && m[1].toLowerCase() || '').split(':')[0];
 | 
			
		||||
          console.log('[tcpRouter] hostname', hostname);
 | 
			
		||||
          if (/HTTP\//i.test(str)) {
 | 
			
		||||
            //conn.__service = 'http';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!hostname) {
 | 
			
		||||
          // TODO allow tcp tunneling
 | 
			
		||||
          // TODO we need some way of tagging tcp as either terminated tls or insecure
 | 
			
		||||
          conn.write(
 | 
			
		||||
            "HTTP/1.1 404 Not Found\r\n"
 | 
			
		||||
          + "Date: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
 | 
			
		||||
          + "Content-Type: text/html\r\n"
 | 
			
		||||
          + "Content-Length: " + 9 + "\r\n"
 | 
			
		||||
          + "\r\n"
 | 
			
		||||
          + "Not Found"
 | 
			
		||||
          );
 | 
			
		||||
          conn.end();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // Poor-man's http proxy
 | 
			
		||||
        // XXX SECURITY XXX: should strip existing X-Forwarded headers
 | 
			
		||||
        newHeads =
 | 
			
		||||
          [ "X-Forwarded-Proto: " + (opts.encrypted ? 'https' : 'http')
 | 
			
		||||
          , "X-Forwarded-For: " + (opts.remoteAddress || conn.remoteAddress)
 | 
			
		||||
          , "X-Forwarded-Host: " + hostname
 | 
			
		||||
          ];
 | 
			
		||||
 | 
			
		||||
        if (!opts.encrypted) {
 | 
			
		||||
          // a exists-only header that a bad client could not remove
 | 
			
		||||
          newHeads.push("X-Not-Encrypted: yes");
 | 
			
		||||
        }
 | 
			
		||||
        if (opts.servername) {
 | 
			
		||||
          newHeads.push("X-Forwarded-Sni: " + opts.servername);
 | 
			
		||||
          if (opts.servername !== hostname) {
 | 
			
		||||
            // an exists-only header that a bad client could not remove
 | 
			
		||||
            newHeads.push("X-Two-Servernames: yes");
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        firstChunk = firstChunk.toString('utf8');
 | 
			
		||||
        // JSON.stringify("Host: example.com\r\nNext: Header".replace(/(Host: [^\r\n]*)/i, "$1" + "\r\n" + "X: XYZ"))
 | 
			
		||||
        firstChunk = firstChunk.replace(/(Host: [^\r\n]*)/i, "$1" + "\r\n" + newHeads.join("\r\n"));
 | 
			
		||||
 | 
			
		||||
        process.nextTick(function () {
 | 
			
		||||
          conn.unshift(Buffer.from(firstChunk, 'utf8'));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //
 | 
			
		||||
        // hard-coded routes for the admin interface
 | 
			
		||||
        if (
 | 
			
		||||
          /\blocalhost\.admin\./.test(hostname) || /\badmin\.localhost\./.test(hostname)
 | 
			
		||||
          || /\blocalhost\.alpha\./.test(hostname) || /\balpha\.localhost\./.test(hostname)
 | 
			
		||||
        ) {
 | 
			
		||||
          if (!modules.admin) {
 | 
			
		||||
            modules.admin = require('./modules/admin.js').create(deps, config);
 | 
			
		||||
          }
 | 
			
		||||
          modules.admin.emit('connection', conn);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // TODO static file handiling and such or whatever
 | 
			
		||||
        if (!modules.http) {
 | 
			
		||||
          modules.http = require('./modules/http.js').create(deps, config);
 | 
			
		||||
        }
 | 
			
		||||
        opts.hostname = hostname;
 | 
			
		||||
        conn.__opts = opts;
 | 
			
		||||
 | 
			
		||||
        modules.http.emit('connection', conn);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  , get: function getTcpRouter(address, port) {
 | 
			
		||||
      address = address || '0.0.0.0';
 | 
			
		||||
 | 
			
		||||
      var id = address + ':' + port;
 | 
			
		||||
      if (!tcpRouter._map[id]) {
 | 
			
		||||
        tcpRouter._map[id] = tcpRouter._create(address, port);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return tcpRouter._map[id];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  var tlsRouter = {
 | 
			
		||||
    _map: { }
 | 
			
		||||
  , _create: function (address, port/*, nextServer*/) {
 | 
			
		||||
      // port provides hinting for https, smtps, etc
 | 
			
		||||
      return function (socket, firstChunk, opts) {
 | 
			
		||||
        if (opts.hyperPeek) {
 | 
			
		||||
          // See "PEEK COMMENT" for more info
 | 
			
		||||
          // This was peeked at properly, so we don't have to re-wrap it
 | 
			
		||||
          // in order to get the connection to not hang.
 | 
			
		||||
          // The real first 'data' and 'readable' events will occur as they should
 | 
			
		||||
          program.tlsTunnelServer.emit('connection', socket);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var servername = opts.servername;
 | 
			
		||||
        var packerStream = require('tunnel-packer').Stream;
 | 
			
		||||
        var myDuplex = packerStream.create(socket);
 | 
			
		||||
 | 
			
		||||
        myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress;
 | 
			
		||||
        myDuplex.remotePort = opts.remotePort || myDuplex.remotePort;
 | 
			
		||||
        console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress);
 | 
			
		||||
 | 
			
		||||
        // needs to wind up in one of 3 states:
 | 
			
		||||
        // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
 | 
			
		||||
        // 2. Admin Interface (skips the proxying)
 | 
			
		||||
        // 3. Terminated (goes on to a particular module or route)
 | 
			
		||||
        //myDuplex.__tlsTerminated = true;
 | 
			
		||||
 | 
			
		||||
        process.nextTick(function () {
 | 
			
		||||
          // this must happen after the socket is emitted to the next in the chain,
 | 
			
		||||
          // but before any more data comes in via the network
 | 
			
		||||
          socket.unshift(firstChunk);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // nextServer.emit could be used here
 | 
			
		||||
        program.tlsTunnelServer.emit('connection', myDuplex);
 | 
			
		||||
 | 
			
		||||
        // Why all this wacky-do with the myDuplex?
 | 
			
		||||
        // because https://github.com/nodejs/node/issues/8854, that's why
 | 
			
		||||
        // (because node's internal networking layer == 💩  sometimes)
 | 
			
		||||
        socket.on('data', function (chunk) {
 | 
			
		||||
          console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
 | 
			
		||||
          myDuplex.push(chunk);
 | 
			
		||||
        });
 | 
			
		||||
        socket.on('error', function (err) {
 | 
			
		||||
          console.error('[error] httpsTunnel (Admin) TODO close');
 | 
			
		||||
          console.error(err);
 | 
			
		||||
          myDuplex.emit('error', err);
 | 
			
		||||
        });
 | 
			
		||||
        socket.on('close', function () {
 | 
			
		||||
          myDuplex.end();
 | 
			
		||||
        });
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  , get: function getTcpRouter(address, port) {
 | 
			
		||||
      address = address || '0.0.0.0';
 | 
			
		||||
 | 
			
		||||
      var id = address + ':' + port;
 | 
			
		||||
      if (!tlsRouter._map[id]) {
 | 
			
		||||
        tlsRouter._map[id] = tlsRouter._create(address, port);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return tlsRouter._map[id];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  function loadModules() {
 | 
			
		||||
    modules = {};
 | 
			
		||||
 | 
			
		||||
    modules.tls = require('./modules/tls').create(deps, config, netHandler);
 | 
			
		||||
    modules.http = require('./modules/http.js').create(deps, config, modules.tls.middleware);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
 | 
			
		||||
  function peek(conn, firstChunk, opts) {
 | 
			
		||||
    if (!modules) {
 | 
			
		||||
      loadModules();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    opts.firstChunk = firstChunk;
 | 
			
		||||
    conn.__opts = opts;
 | 
			
		||||
    // TODO port/service-based routing can do here
 | 
			
		||||
 | 
			
		||||
    // TLS byte 1 is handshake and byte 6 is client hello
 | 
			
		||||
    if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) {
 | 
			
		||||
      console.log('tryTls');
 | 
			
		||||
      opts.servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid';
 | 
			
		||||
      tlsRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
      modules.tls.emit('connection', conn);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log('tryTcp');
 | 
			
		||||
 | 
			
		||||
    if (opts.hyperPeek) {
 | 
			
		||||
      // even though we've already peeked, this logic is just as well to let be
 | 
			
		||||
      // since it works properly either way, unlike the tls socket
 | 
			
		||||
      conn.once('data', function (chunk) {
 | 
			
		||||
        console.log('hyperPeek re-peek data', chunk.toString('utf8'));
 | 
			
		||||
        tcpRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, chunk, opts);
 | 
			
		||||
    // This doesn't work with TLS, but now that we know this isn't a TLS connection we can
 | 
			
		||||
    // unshift the first chunk back onto the connection for future use. The unshift should
 | 
			
		||||
    // happen after any listeners are attached to it but before any new data comes in.
 | 
			
		||||
    if (!opts.hyperPeek) {
 | 
			
		||||
      process.nextTick(function () {
 | 
			
		||||
        conn.unshift(firstChunk);
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tcpRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, firstChunk, opts);
 | 
			
		||||
    // Connection is not TLS, check for HTTP next.
 | 
			
		||||
    if (firstChunk[0] > 32 && firstChunk[0] < 127) {
 | 
			
		||||
      var firstStr = firstChunk.toString();
 | 
			
		||||
      if (/HTTP\//i.test(firstStr)) {
 | 
			
		||||
        modules.http.emit('connection', conn);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.warn('failed to identify protocol from first chunk', firstChunk);
 | 
			
		||||
    conn.close();
 | 
			
		||||
  }
 | 
			
		||||
  function netHandler(conn, opts) {
 | 
			
		||||
    opts = opts || {};
 | 
			
		||||
    console.log('[netHandler]', conn.localAddres, conn.localPort, opts.encrypted);
 | 
			
		||||
    console.log('[netHandler]', conn.localAddress, conn.localPort, opts.encrypted);
 | 
			
		||||
 | 
			
		||||
    // XXX PEEK COMMENT XXX
 | 
			
		||||
    // TODO we can have our cake and eat it too
 | 
			
		||||
@ -242,90 +79,42 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
    socket.send(msg, config.dns.proxy.port, config.dns.proxy.address || '127.0.0.1');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function approveDomains(opts, certs, cb) {
 | 
			
		||||
    // This is where you check your database and associated
 | 
			
		||||
    // email addresses with domains and agreements and such
 | 
			
		||||
  function createTcpForwarder(mod) {
 | 
			
		||||
    var destination = mod.address.split(':');
 | 
			
		||||
    var connected = false;
 | 
			
		||||
 | 
			
		||||
    // The domains being approved for the first time are listed in opts.domains
 | 
			
		||||
    // Certs being renewed are listed in certs.altnames
 | 
			
		||||
    return function (conn) {
 | 
			
		||||
      var newConn = deps.net.createConnection({
 | 
			
		||||
        port: destination[1]
 | 
			
		||||
      , host: destination[0] || '127.0.0.1'
 | 
			
		||||
 | 
			
		||||
    function complete(err, stuff) {
 | 
			
		||||
      opts.email = stuff.email;
 | 
			
		||||
      opts.agreeTos = stuff.agreeTos;
 | 
			
		||||
      opts.server = stuff.server;
 | 
			
		||||
      opts.challengeType = stuff.challengeType;
 | 
			
		||||
      , remoteFamily:  conn.remoteFamily
 | 
			
		||||
      , remoteAddress: conn.remoteAddress
 | 
			
		||||
      , remotePort:    conn.remotePort
 | 
			
		||||
      }, function () {
 | 
			
		||||
        connected = true;
 | 
			
		||||
 | 
			
		||||
      cb(null, { options: opts, certs: certs });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (certs) {
 | 
			
		||||
      // TODO make sure the same options are used for renewal as for registration?
 | 
			
		||||
      opts.domains = certs.altnames;
 | 
			
		||||
 | 
			
		||||
      cb(null, { options: opts, certs: certs });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // check config for domain name
 | 
			
		||||
    if (-1 !== config.tls.servernames.indexOf(opts.domain)) {
 | 
			
		||||
      // TODO how to handle SANs?
 | 
			
		||||
      // TODO fetch domain-specific email
 | 
			
		||||
      // TODO fetch domain-specific acmeDirectory
 | 
			
		||||
      // NOTE: you can also change other options such as `challengeType` and `challenge`
 | 
			
		||||
      // opts.challengeType = 'http-01';
 | 
			
		||||
      // opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
 | 
			
		||||
      complete(null, {
 | 
			
		||||
        email: config.tls.email, agreeTos: true, server: program.acmeDirectoryUrl, challengeType: program.challengeType });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO ask http module about the default path (/srv/www/:hostname)
 | 
			
		||||
    // (if it exists, we can allow and add to config)
 | 
			
		||||
    if (!modules.http) {
 | 
			
		||||
      modules.http = require('./modules/http.js').create(deps, config);
 | 
			
		||||
    }
 | 
			
		||||
    modules.http.checkServername(opts.domain).then(function (stuff) {
 | 
			
		||||
      if (!stuff || !stuff.domains) {
 | 
			
		||||
        // TODO once precheck is implemented we can just let it pass if it passes, yknow?
 | 
			
		||||
        cb(new Error('domain is not allowed'));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      complete(null, {
 | 
			
		||||
        domain: stuff.domain || stuff.domains[0]
 | 
			
		||||
      , domains: stuff.domains
 | 
			
		||||
      , email: stuff.email || program.email
 | 
			
		||||
      , server: stuff.acmeDirectoryUrl || program.acmeDirectoryUrl
 | 
			
		||||
      , challengeType: stuff.challengeType || program.challengeType
 | 
			
		||||
      , challenge: stuff.challenge
 | 
			
		||||
        newConn.pipe(conn);
 | 
			
		||||
        conn.pipe(newConn);
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }, cb);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getAcme() {
 | 
			
		||||
    return greenlock.create({
 | 
			
		||||
      // 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('TCP forward remote error', err);
 | 
			
		||||
          conn.end();
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log('TCP forward connection error', err);
 | 
			
		||||
          require('./proxy-err-resp').sendBadGateway(conn, err, config.debug);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      conn.on('error', function (err) {
 | 
			
		||||
        console.error('TCP forward client error', err);
 | 
			
		||||
        newConn.end();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      //server: 'staging'
 | 
			
		||||
      server: 'https://acme-v01.api.letsencrypt.org/directory'
 | 
			
		||||
 | 
			
		||||
    , challenges: {
 | 
			
		||||
        // TODO dns-01
 | 
			
		||||
        'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug })
 | 
			
		||||
      , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
 | 
			
		||||
      //, 'dns-01': require('le-challenge-ddns').create()
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
 | 
			
		||||
 | 
			
		||||
    //, email: program.email
 | 
			
		||||
 | 
			
		||||
    //, agreeTos: program.agreeTos
 | 
			
		||||
 | 
			
		||||
    , approveDomains: approveDomains
 | 
			
		||||
 | 
			
		||||
    //, approvedDomains: program.servernames
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deps.tunnel = deps.tunnel || {};
 | 
			
		||||
@ -406,63 +195,55 @@ module.exports.create = function (deps, config) {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Object.keys(program.tlsOptions).forEach(function (key) {
 | 
			
		||||
    tunnelAdminTlsOpts[key] = program.tlsOptions[key];
 | 
			
		||||
  });
 | 
			
		||||
  tunnelAdminTlsOpts.SNICallback = function (sni, cb) {
 | 
			
		||||
    console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'");
 | 
			
		||||
 | 
			
		||||
    var tlsOptions;
 | 
			
		||||
 | 
			
		||||
    // Static Certs
 | 
			
		||||
    if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
 | 
			
		||||
      // TODO implement
 | 
			
		||||
      if (!secureContexts[sni]) {
 | 
			
		||||
        tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {});
 | 
			
		||||
      }
 | 
			
		||||
      if (tlsOptions) {
 | 
			
		||||
        secureContexts[sni] = tls.createSecureContext(tlsOptions);
 | 
			
		||||
      }
 | 
			
		||||
      if (secureContexts[sni]) {
 | 
			
		||||
        console.log('Got static secure context:', sni, secureContexts[sni]);
 | 
			
		||||
        cb(null, secureContexts[sni]);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
  var listenPromises = [];
 | 
			
		||||
  var tcpPortMap = {};
 | 
			
		||||
  function addPorts(bindList) {
 | 
			
		||||
    if (!bindList) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!program.greenlock) {
 | 
			
		||||
      program.greenlock = getAcme();
 | 
			
		||||
    if (Array.isArray(bindList)) {
 | 
			
		||||
      bindList.forEach(function (port) {
 | 
			
		||||
        tcpPortMap[port] = true;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    (program.greenlock.tlsOptions||program.greenlock.httpsOptions).SNICallback(sni, cb);
 | 
			
		||||
  };
 | 
			
		||||
    else {
 | 
			
		||||
      tcpPortMap[bindList] = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) {
 | 
			
		||||
    console.log('(pre-terminated) tls connection, addr:', tlsSocket.remoteAddress);
 | 
			
		||||
    // things get a little messed up here
 | 
			
		||||
    //tlsSocket.on('data', function (chunk) {
 | 
			
		||||
    //  console.log('terminated data:', chunk.toString());
 | 
			
		||||
    //});
 | 
			
		||||
    //(program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket);
 | 
			
		||||
    //tcpRouter.get(conn.localAddress, conn.localPort)(conn, firstChunk, { encrypted: false });
 | 
			
		||||
    netHandler(tlsSocket, {
 | 
			
		||||
      servername: tlsSocket.servername
 | 
			
		||||
    , encrypted: true
 | 
			
		||||
      // remoteAddress... ugh... https://github.com/nodejs/node/issues/8854
 | 
			
		||||
    , remoteAddress: tlsSocket.remoteAddress || tlsSocket._remoteAddress || tlsSocket._handle._parent.owner.stream.remoteAddress
 | 
			
		||||
    , remotePort: tlsSocket.remotePort || tlsSocket._remotePort || tlsSocket._handle._parent.owner.stream.remotePort
 | 
			
		||||
    , remoteFamily: tlsSocket.remoteFamily || tlsSocket._remoteFamily || tlsSocket._handle._parent.owner.stream.remoteFamily
 | 
			
		||||
    });
 | 
			
		||||
  addPorts(config.tcp.bind);
 | 
			
		||||
  (config.tcp.modules || []).forEach(function (mod) {
 | 
			
		||||
    if (mod.name === 'forward') {
 | 
			
		||||
      var forwarder = createTcpForwarder(mod);
 | 
			
		||||
      mod.ports.forEach(function (port) {
 | 
			
		||||
        if (!tcpPortMap[port]) {
 | 
			
		||||
          console.log("forwarding port", port, "that wasn't specified in bind");
 | 
			
		||||
        } else {
 | 
			
		||||
          delete tcpPortMap[port];
 | 
			
		||||
        }
 | 
			
		||||
        listenPromises.push(listeners.tcp.add(port, forwarder));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      console.warn('unknown TCP module specified', mod);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  var listenPromises = config.tcp.ports.map(function (port) {
 | 
			
		||||
    return listeners.tcp.add(port, netHandler);
 | 
			
		||||
  // Even though these ports were specified in different places we treat any TCP
 | 
			
		||||
  // connections we haven't been told to just forward exactly as is equal so that
 | 
			
		||||
  // we can potentially use the same ports for different protocols.
 | 
			
		||||
  addPorts(config.tls.bind);
 | 
			
		||||
  addPorts(config.http.bind);
 | 
			
		||||
 | 
			
		||||
  Object.keys(tcpPortMap).forEach(function (port) {
 | 
			
		||||
    listenPromises.push(listeners.tcp.add(port, netHandler));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (config.dns.bind) {
 | 
			
		||||
    if (Array.isArray(config.dns.bind)) {
 | 
			
		||||
      listenPromises = listenPromises.concat(config.dns.bind.map(function (port) {
 | 
			
		||||
        return listeners.udp.add(port, dnsListener);
 | 
			
		||||
      }));
 | 
			
		||||
      config.dns.bind.map(function (port) {
 | 
			
		||||
        listenPromises.push(listeners.udp.add(port, dnsListener));
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      listenPromises.push(listeners.udp.add(config.dns.bind, dnsListener));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								lib/match-domain.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/match-domain.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports.match = function (pattern, servername) {
 | 
			
		||||
  // Everything matches '*'
 | 
			
		||||
  if (pattern === '*') {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (/^\*./.test(pattern)) {
 | 
			
		||||
    // get rid of the leading "*." to more easily check the servername against it
 | 
			
		||||
    pattern = pattern.slice(2);
 | 
			
		||||
    return pattern === servername.slice(-pattern.length);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // pattern doesn't contains any wildcards, so exact match is required
 | 
			
		||||
  return pattern === servername;
 | 
			
		||||
};
 | 
			
		||||
@ -60,7 +60,5 @@ module.exports.create = function (deps, conf) {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /* device, addresses, cwd, http */
 | 
			
		||||
  var app = require('../app.js')(deps, conf, opts);
 | 
			
		||||
  var http = require('http');
 | 
			
		||||
  return http.createServer(app);
 | 
			
		||||
  return require('../app.js')(deps, conf, opts);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,179 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports.create = function (deps, conf) {
 | 
			
		||||
  // This should be able to handle things like default web path (i.e. /srv/www/hostname),
 | 
			
		||||
  // no-www redirect, and transpilation of static assets (i.e. cached versions of raw html)
 | 
			
		||||
  // but right now it's a very dumb proxy
 | 
			
		||||
module.exports.create = function (deps, conf, greenlockMiddleware) {
 | 
			
		||||
  var express = require('express');
 | 
			
		||||
  var app = express();
 | 
			
		||||
  var adminApp = require('./admin').create(deps, conf);
 | 
			
		||||
  var domainMatches = require('../match-domain').match;
 | 
			
		||||
 | 
			
		||||
  function createConnection(conn) {
 | 
			
		||||
    var opts = conn.__opts;
 | 
			
		||||
    var newConn = deps.net.createConnection({
 | 
			
		||||
      port: conf.http.proxy.port
 | 
			
		||||
    , host: '127.0.0.1'
 | 
			
		||||
  var adminDomains = [
 | 
			
		||||
    /\blocalhost\.admin\./
 | 
			
		||||
  , /\blocalhost\.alpha\./
 | 
			
		||||
  , /\badmin\.localhost\./
 | 
			
		||||
  , /\balpha\.localhost\./
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
    , servername: opts.servername
 | 
			
		||||
    , data: opts.data
 | 
			
		||||
    , remoteFamily: opts.family || conn.remoteFamily
 | 
			
		||||
    , remoteAddress: opts.address || conn.remoteAddress
 | 
			
		||||
    , remotePort: opts.port || conn.remotePort
 | 
			
		||||
    }, function () {
 | 
			
		||||
      //console.log("[=>] first packet from tunneler to '" + cid + "' as '" + opts.service + "'", opts.data.byteLength);
 | 
			
		||||
      // this will happen before 'data' is triggered
 | 
			
		||||
      //newConn.write(opts.data);
 | 
			
		||||
    });
 | 
			
		||||
  function verifyHost(fullHost) {
 | 
			
		||||
    var host = /^(.*?)(:\d+)?$/.exec(fullHost)[1];
 | 
			
		||||
 | 
			
		||||
    newConn.pipe(conn);
 | 
			
		||||
    conn.pipe(newConn);
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    emit: function (type, conn) {
 | 
			
		||||
      createConnection(conn);
 | 
			
		||||
  // 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 = (/:(\d+)$/.exec(req.headers.host) || [])[1];
 | 
			
		||||
    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 createProxyRoute(mod) {
 | 
			
		||||
    // This is the easiest way to override the createConnections function the proxy
 | 
			
		||||
    // module uses, but take note the since we don't have control over where this is
 | 
			
		||||
    // called the extra options availabled will be different.
 | 
			
		||||
    var agent = new require('http').Agent({});
 | 
			
		||||
    agent.createConnection = deps.net.createConnection;
 | 
			
		||||
 | 
			
		||||
    var proxy = require('http-proxy').createProxyServer({
 | 
			
		||||
      agent: agent
 | 
			
		||||
    , target: 'http://' + mod.address
 | 
			
		||||
    , xfwd: true
 | 
			
		||||
    , toProxy: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // We want to override the default value for some headers with the extra information we
 | 
			
		||||
    // have available to us in the opts object attached to the connection.
 | 
			
		||||
    proxy.on('proxyReq', function (proxyReq, req) {
 | 
			
		||||
      var conn = req.connection;
 | 
			
		||||
      var opts = conn.__opts;
 | 
			
		||||
      proxyReq.setHeader('X-Forwarded-For', opts.remoteAddress || conn.remoteAddress);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    proxy.on('error', function (err, req, res) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
      res.statusCode = 502;
 | 
			
		||||
      res.setHeader('Content-Type', 'text/html');
 | 
			
		||||
      res.setHeader('Connection', 'close');
 | 
			
		||||
      res.end(require('../proxy-err-resp').getRespBody(err, conf.debug));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return function (req, res, next) {
 | 
			
		||||
      var hostname = req.headers.host.split(':')[0];
 | 
			
		||||
      var relevant = mod.domains.some(function (pattern) {
 | 
			
		||||
        return domainMatches(pattern, hostname);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (relevant) {
 | 
			
		||||
        proxy.web(req, res);
 | 
			
		||||
      } else {
 | 
			
		||||
        next();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
      var hostname = req.headers.host.split(':')[0];
 | 
			
		||||
      var relevant = mod.domains.some(function (pattern) {
 | 
			
		||||
        return domainMatches(pattern, hostname);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (relevant) {
 | 
			
		||||
        getStaticApp(hostname)(req, res, next);
 | 
			
		||||
      } else {
 | 
			
		||||
        next();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  app.use(greenlockMiddleware);
 | 
			
		||||
  app.use(redirectHttps);
 | 
			
		||||
  app.use(handleAdmin);
 | 
			
		||||
 | 
			
		||||
  (conf.http.modules || []).forEach(function (mod) {
 | 
			
		||||
    if (mod.name === 'proxy') {
 | 
			
		||||
      app.use(createProxyRoute(mod));
 | 
			
		||||
    }
 | 
			
		||||
    else if (mod.name === 'static') {
 | 
			
		||||
      app.use(createStaticRoute(mod));
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      console.warn('unknown HTTP module', mod);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  app.use(respond404);
 | 
			
		||||
  return require('http').createServer(app);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										283
									
								
								lib/modules/tls.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								lib/modules/tls.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,283 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports.create = function (deps, config, netHandler) {
 | 
			
		||||
  var tls = require('tls');
 | 
			
		||||
  var parseSni = require('sni');
 | 
			
		||||
  var greenlock = require('greenlock');
 | 
			
		||||
  var localhostCerts = require('localhost.daplie.me-certificates');
 | 
			
		||||
  var domainMatches = require('../match-domain').match;
 | 
			
		||||
 | 
			
		||||
  function extractSocketProp(socket, propName) {
 | 
			
		||||
    // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854
 | 
			
		||||
    return socket[propName]
 | 
			
		||||
      || socket['_' + propName]
 | 
			
		||||
      || socket._handle._parent.owner.stream[propName]
 | 
			
		||||
      ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function wrapSocket(socket, opts) {
 | 
			
		||||
    var myDuplex = require('tunnel-packer').Stream.create(socket);
 | 
			
		||||
    myDuplex.remoteFamily = opts.remoteFamily || myDuplex.remoteFamily;
 | 
			
		||||
    myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress;
 | 
			
		||||
    myDuplex.remotePort = opts.remotePort || myDuplex.remotePort;
 | 
			
		||||
 | 
			
		||||
    socket.on('data', function (chunk) {
 | 
			
		||||
      console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
 | 
			
		||||
      myDuplex.push(chunk);
 | 
			
		||||
    });
 | 
			
		||||
    socket.on('error', function (err) {
 | 
			
		||||
      console.error('[error] httpsTunnel (Admin) TODO close');
 | 
			
		||||
      console.error(err);
 | 
			
		||||
      myDuplex.emit('error', err);
 | 
			
		||||
    });
 | 
			
		||||
    socket.on('close', function () {
 | 
			
		||||
      myDuplex.end();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    process.nextTick(function () {
 | 
			
		||||
      // this must happen after the socket is emitted to the next in the chain,
 | 
			
		||||
      // but before any more data comes in via the network
 | 
			
		||||
      socket.unshift(opts.firstChunk);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return myDuplex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var le = greenlock.create({
 | 
			
		||||
    // server: 'staging'
 | 
			
		||||
    server: 'https://acme-v01.api.letsencrypt.org/directory'
 | 
			
		||||
 | 
			
		||||
  , challenges: {
 | 
			
		||||
      'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges', debug: config.debug })
 | 
			
		||||
    , 'tls-sni-01': require('le-challenge-sni').create({ debug: config.debug })
 | 
			
		||||
      // TODO dns-01
 | 
			
		||||
      //, 'dns-01': require('le-challenge-ddns').create()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' })
 | 
			
		||||
 | 
			
		||||
  , approveDomains: function (opts, certs, cb) {
 | 
			
		||||
      // This is where you check your database and associated
 | 
			
		||||
      // email addresses with domains and agreements and such
 | 
			
		||||
 | 
			
		||||
      // The domains being approved for the first time are listed in opts.domains
 | 
			
		||||
      // Certs being renewed are listed in certs.altnames
 | 
			
		||||
      if (certs) {
 | 
			
		||||
        // TODO make sure the same options are used for renewal as for registration?
 | 
			
		||||
        opts.domains = certs.altnames;
 | 
			
		||||
 | 
			
		||||
        cb(null, { options: opts, certs: certs });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function complete(optsOverride) {
 | 
			
		||||
        Object.keys(optsOverride).forEach(function (key) {
 | 
			
		||||
          opts[key] = optsOverride[key];
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        cb(null, { options: opts, certs: certs });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      // check config for domain name
 | 
			
		||||
      if (-1 !== (config.tls.servernames || []).indexOf(opts.domain)) {
 | 
			
		||||
        // TODO how to handle SANs?
 | 
			
		||||
        // TODO fetch domain-specific email
 | 
			
		||||
        // TODO fetch domain-specific acmeDirectory
 | 
			
		||||
        // NOTE: you can also change other options such as `challengeType` and `challenge`
 | 
			
		||||
        // opts.challengeType = 'http-01';
 | 
			
		||||
        // opts.challenge = require('le-challenge-fs').create({}); // TODO this doesn't actually work yet
 | 
			
		||||
        complete({
 | 
			
		||||
          email: config.tls.email
 | 
			
		||||
        , agreeTos: true
 | 
			
		||||
        , server: config.tls.acmeDirectoryUrl || le.server
 | 
			
		||||
        , challengeType: config.tls.challengeType || 'http-01'
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // TODO ask http module (and potentially all other modules) about what domains it can
 | 
			
		||||
      // handle. We can allow any domains that other modules will handle after we terminate TLS.
 | 
			
		||||
      cb(new Error('domain is not allowed'));
 | 
			
		||||
      // if (!modules.http) {
 | 
			
		||||
      //   modules.http = require('./modules/http.js').create(deps, config);
 | 
			
		||||
      // }
 | 
			
		||||
      // modules.http.checkServername(opts.domain).then(function (stuff) {
 | 
			
		||||
      //   if (!stuff || !stuff.domains) {
 | 
			
		||||
      //     // TODO once precheck is implemented we can just let it pass if it passes, yknow?
 | 
			
		||||
      //     cb(new Error('domain is not allowed'));
 | 
			
		||||
      //     return;
 | 
			
		||||
      //   }
 | 
			
		||||
 | 
			
		||||
      //   complete({
 | 
			
		||||
      //     domain: stuff.domain || stuff.domains[0]
 | 
			
		||||
      //   , domains: stuff.domains
 | 
			
		||||
      //   , email: stuff.email || program.email
 | 
			
		||||
      //   , server: stuff.acmeDirectoryUrl || program.acmeDirectoryUrl
 | 
			
		||||
      //   , challengeType: stuff.challengeType || program.challengeType
 | 
			
		||||
      //   , challenge: stuff.challenge
 | 
			
		||||
      //   });
 | 
			
		||||
      //   return;
 | 
			
		||||
      // }, cb);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  le.tlsOptions = le.tlsOptions || le.httpsOptions;
 | 
			
		||||
 | 
			
		||||
  var secureContexts = {};
 | 
			
		||||
  var terminatorOpts = require('localhost.daplie.me-certificates').merge({});
 | 
			
		||||
  terminatorOpts.SNICallback = function (sni, cb) {
 | 
			
		||||
    console.log("[tlsOptions.SNICallback] SNI: '" + sni + "'");
 | 
			
		||||
 | 
			
		||||
    var tlsOptions;
 | 
			
		||||
 | 
			
		||||
    // Static Certs
 | 
			
		||||
    if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
 | 
			
		||||
      // TODO implement
 | 
			
		||||
      if (!secureContexts[sni]) {
 | 
			
		||||
        tlsOptions = localhostCerts.mergeTlsOptions(sni, {});
 | 
			
		||||
      }
 | 
			
		||||
      if (tlsOptions) {
 | 
			
		||||
        secureContexts[sni] = tls.createSecureContext(tlsOptions);
 | 
			
		||||
      }
 | 
			
		||||
      if (secureContexts[sni]) {
 | 
			
		||||
        console.log('Got static secure context:', sni, secureContexts[sni]);
 | 
			
		||||
        cb(null, secureContexts[sni]);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    le.tlsOptions.SNICallback(sni, cb);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  var terminateServer = tls.createServer(terminatorOpts, function (socket) {
 | 
			
		||||
    console.log('(pre-terminated) tls connection, addr:', socket.remoteAddress);
 | 
			
		||||
 | 
			
		||||
    netHandler(socket, {
 | 
			
		||||
      servername: socket.servername
 | 
			
		||||
    , encrypted: true
 | 
			
		||||
      // remoteAddress... ugh... https://github.com/nodejs/node/issues/8854
 | 
			
		||||
    , remoteAddress: extractSocketProp(socket, 'remoteAddress')
 | 
			
		||||
    , remotePort:    extractSocketProp(socket, 'remotePort')
 | 
			
		||||
    , remoteFamily:  extractSocketProp(socket, 'remoteFamily')
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function proxy(socket, opts, mod) {
 | 
			
		||||
    var destination = mod.address.split(':');
 | 
			
		||||
    var connected = false;
 | 
			
		||||
 | 
			
		||||
    var newConn = deps.net.createConnection({
 | 
			
		||||
        port: destination[1]
 | 
			
		||||
      , host: destination[0] || '127.0.0.1'
 | 
			
		||||
 | 
			
		||||
      , servername: opts.servername
 | 
			
		||||
      , data: opts.firstChunk
 | 
			
		||||
      , remoteFamily:  opts.family  || extractSocketProp(socket, 'remoteFamily')
 | 
			
		||||
      , remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress')
 | 
			
		||||
      , remotePort:    opts.port    || extractSocketProp(socket, 'remotePort')
 | 
			
		||||
    }, function () {
 | 
			
		||||
      connected = true;
 | 
			
		||||
      if (!opts.hyperPeek) {
 | 
			
		||||
        newConn.write(opts.firstChunk);
 | 
			
		||||
      }
 | 
			
		||||
      newConn.pipe(socket);
 | 
			
		||||
      socket.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('TLS proxy remote error', err);
 | 
			
		||||
        socket.end();
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log('TLS proxy connection error', err);
 | 
			
		||||
        var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true});
 | 
			
		||||
        var decrypted;
 | 
			
		||||
        if (opts.hyperPeek) {
 | 
			
		||||
          decrypted = new tls.TLSSocket(socket, tlsOpts);
 | 
			
		||||
        } else {
 | 
			
		||||
          decrypted = new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts);
 | 
			
		||||
        }
 | 
			
		||||
        require('../proxy-err-resp').sendBadGateway(decrypted, err, config.debug);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    socket.on('error', function (err) {
 | 
			
		||||
      console.error('TLS proxy client error', err);
 | 
			
		||||
      newConn.end();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function terminate(socket, opts) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      '[tls-terminate]'
 | 
			
		||||
    , opts.localAddress || socket.localAddress +':'+ opts.localPort || socket.localPort
 | 
			
		||||
    , 'servername=' + opts.servername
 | 
			
		||||
    , opts.remoteAddress || socket.remoteAddress
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (opts.hyperPeek) {
 | 
			
		||||
      // This connection was peeked at using a method that doesn't interferre with the TLS
 | 
			
		||||
      // server's ability to handle it properly. Currently the only way this happens is
 | 
			
		||||
      // with tunnel connections where we have the first chunk of data before creating the
 | 
			
		||||
      // new connection (thus removing need to get data off the new connection).
 | 
			
		||||
      terminateServer.emit('connection', socket);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // The hyperPeek flag wasn't set, so we had to read data off of this connection, which
 | 
			
		||||
      // means we can no longer use it directly in the TLS server.
 | 
			
		||||
      // See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes)
 | 
			
		||||
      terminateServer.emit('connection', wrapSocket(socket, opts));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleConn(socket, opts) {
 | 
			
		||||
    opts.servername = (parseSni(opts.firstChunk)||'').toLowerCase() || 'localhost.invalid';
 | 
			
		||||
    // needs to wind up in one of 2 states:
 | 
			
		||||
    // 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
 | 
			
		||||
    // 2. Terminated (goes on to a particular module or route, including the admin interface)
 | 
			
		||||
    // 3. Closed (we don't recognize the SNI servername as something we actually want to handle)
 | 
			
		||||
 | 
			
		||||
    // We always want to terminate is the SNI matches the challenge pattern, unless a client
 | 
			
		||||
    // on the south side has temporarily claimed a particular challenge. For the time being
 | 
			
		||||
    // we don't have a way for the south-side to communicate with us, so that part isn't done.
 | 
			
		||||
    if (domainMatches('*.acme-challenge.invalid', opts.servername)) {
 | 
			
		||||
      terminate(socket, opts);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var handled = (config.tls.modules || []).some(function (mod) {
 | 
			
		||||
      var relevant = mod.domains.some(function (pattern) {
 | 
			
		||||
        return domainMatches(pattern, opts.servername);
 | 
			
		||||
      });
 | 
			
		||||
      if (!relevant) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (mod.name === 'proxy') {
 | 
			
		||||
        proxy(socket, opts, mod);
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        console.error('saw unknown TLS module', mod);
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // TODO: figure out all of the domains that the other modules intend to handle, and only
 | 
			
		||||
    // terminate those ones, closing connections for all others.
 | 
			
		||||
    if (!handled) {
 | 
			
		||||
      terminate(socket, opts);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    emit: function (type, socket) {
 | 
			
		||||
      if (type === 'connection') {
 | 
			
		||||
        handleConn(socket, socket.__opts);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  , middleware: le.middleware()
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										32
									
								
								lib/proxy-err-resp.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/proxy-err-resp.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
function getRespBody(err, debug) {
 | 
			
		||||
  if (debug) {
 | 
			
		||||
    return err.toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (err.code === 'ECONNREFUSED') {
 | 
			
		||||
    return 'The connection was refused. Most likely the service being connected to '
 | 
			
		||||
      + 'has stopped running or the configuration is wrong.';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'Bad Gateway: ' + err.code;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendBadGateway(conn, err, debug) {
 | 
			
		||||
  var body = getRespBody(err, debug);
 | 
			
		||||
 | 
			
		||||
  conn.write([
 | 
			
		||||
    'HTTP/1.1 502 Bad Gateway'
 | 
			
		||||
  , 'Date: ' + (new Date()).toUTCString()
 | 
			
		||||
  , 'Connection: close'
 | 
			
		||||
  , 'Content-Type: text/html'
 | 
			
		||||
  , 'Content-Length: ' + body.length
 | 
			
		||||
  , ''
 | 
			
		||||
  , body
 | 
			
		||||
  ].join('\r\n'));
 | 
			
		||||
  conn.end();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports.getRespBody = getRespBody;
 | 
			
		||||
module.exports.sendBadGateway = sendBadGateway;
 | 
			
		||||
@ -4,6 +4,9 @@
 | 
			
		||||
process.on('message', function (conf) {
 | 
			
		||||
  var deps = {
 | 
			
		||||
    messenger: process
 | 
			
		||||
    // Note that if a custom createConnections is used it will be called with different
 | 
			
		||||
    // sets of custom options based on what is actually being proxied. Most notably the
 | 
			
		||||
    // HTTP proxying connection creation is not something we currently control.
 | 
			
		||||
  , net: require('net')
 | 
			
		||||
  };
 | 
			
		||||
  require('./goldilocks.js').create(deps, conf);
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@
 | 
			
		||||
    "finalhandler": "^0.4.0",
 | 
			
		||||
    "greenlock": "git+https://git.daplie.com/Daplie/node-greenlock.git#master",
 | 
			
		||||
    "greenlock-express": "git+https://git.daplie.com/Daplie/greenlock-express.git#master",
 | 
			
		||||
    "http-proxy": "^1.16.2",
 | 
			
		||||
    "httpolyglot": "^0.1.1",
 | 
			
		||||
    "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
 | 
			
		||||
    "ipify": "^1.1.0",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user