456 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			456 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
var os = require('os');
 | 
						|
var path = require('path');
 | 
						|
 | 
						|
module.exports.print = function (config) {
 | 
						|
  var services = { https: {}, http: {}, tcp: {} };
 | 
						|
  // Note: the remote needs to know:
 | 
						|
  //   what servernames to forward
 | 
						|
  //   what ports to forward
 | 
						|
  //   what udp ports to forward
 | 
						|
  //   redirect http to https automatically
 | 
						|
  //   redirect www to nowww automatically
 | 
						|
  if (config.http) {
 | 
						|
    Object.keys(config.http).forEach(function (hostname) {
 | 
						|
      if ('*' === hostname) {
 | 
						|
        config.servernames.forEach(function (servername) {
 | 
						|
          services.https[servername] = config.http[hostname];
 | 
						|
          services.http[servername] = 'redirect-https';
 | 
						|
        });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      services.https[hostname] = config.http[hostname];
 | 
						|
      services.http[hostname] = 'redirect-https';
 | 
						|
    });
 | 
						|
  }
 | 
						|
  /*
 | 
						|
  Object.keys(config.localPorts).forEach(function (port) {
 | 
						|
    var proto = config.localPorts[port];
 | 
						|
    if (!proto) { return; }
 | 
						|
    if ('http' === proto) {
 | 
						|
      config.servernames.forEach(function (servername) {
 | 
						|
        services.http[servername] = port;
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if ('https' === proto) {
 | 
						|
      config.servernames.forEach(function (servername) {
 | 
						|
        services.https[servername] = port;
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (true === proto) { proto = 'tcp'; }
 | 
						|
    if ('tcp' !== proto) { throw new Error("unsupported protocol '" + proto + "'"); }
 | 
						|
  //services[proxy.protocol]['*'] = proxy.port;
 | 
						|
  //services[proxy.protocol][proxy.hostname] = proxy.port;
 | 
						|
    services[proto]['*'] = port;
 | 
						|
  });
 | 
						|
  */
 | 
						|
 | 
						|
  Object.keys(services).forEach(function (protocol) {
 | 
						|
    var subServices = services[protocol];
 | 
						|
    Object.keys(subServices).forEach(function (hostname) {
 | 
						|
      console.info('[local proxy]', protocol + '://' + hostname + ' => ' + subServices[hostname]);
 | 
						|
    });
 | 
						|
  });
 | 
						|
  console.info('');
 | 
						|
};
 | 
						|
 | 
						|
module.exports.assign = function (state, tun, cb) {
 | 
						|
  console.log('first message from', tun);
 | 
						|
  var net = state.net || require('net');
 | 
						|
 | 
						|
  function trySsh(tun, cb) {
 | 
						|
    // https://security.stackexchange.com/questions/43231/plausibly-deniable-ssh-does-it-make-sense?rq=1
 | 
						|
    // https://tools.ietf.org/html/rfc4253#section-4.2
 | 
						|
    var sshPort;
 | 
						|
    if (-1 !== ['true', 'enable', 'auto', 'on'].indexOf(state.config.sshAuto)) {
 | 
						|
      sshPort = 22;
 | 
						|
    } else {
 | 
						|
      sshPort = parseInt(state.config.sshAuto, 10);
 | 
						|
    }
 | 
						|
    if (!sshPort || 'SSH-2.0-' !== tun.data.slice(0, 8).toString()) {
 | 
						|
      cb(null, false);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    cb(null, getNetConn(sshPort));
 | 
						|
  }
 | 
						|
 | 
						|
  var handlers = {};
 | 
						|
  handlers.http = function (socket) {
 | 
						|
    if (!state.greenlock) {
 | 
						|
      state.greenlock = require('greenlock').create(state.greenlockConfig);
 | 
						|
    }
 | 
						|
    if (!state.httpRedirectServer) {
 | 
						|
      state.redirectHttps = require('redirect-https')();
 | 
						|
      state.httpRedirectServer = require('http').createServer(state.greenlock.middleware(state.redirectHttps));
 | 
						|
    }
 | 
						|
    state.httpRedirectServer.emit('connection', socket);
 | 
						|
  };
 | 
						|
  handlers.https = function (tlsSocket) {
 | 
						|
    console.log('Encrypted', tlsSocket.encrypted, tlsSocket.remoteAddress, tlsSocket.remotePort);
 | 
						|
    if (!state.defaultHttpServer) {
 | 
						|
      state._finalHandler = require('finalhandler');
 | 
						|
      state._serveStatic = require('serve-static');
 | 
						|
      state._defaultServe = state._serveStatic(path.join(__dirname, 'html'));
 | 
						|
      state.defaultHttpServer = require('http').createServer(function (req, res) {
 | 
						|
        state._defaultServe(req, res, state._finalHandler(req, res));
 | 
						|
      });
 | 
						|
    }
 | 
						|
    state.defaultHttpServer.emit('connection', tlsSocket);
 | 
						|
  };
 | 
						|
 | 
						|
  function getNetConn(port) {
 | 
						|
    var netOpts = {
 | 
						|
      port: port
 | 
						|
    , host: '127.0.0.1'
 | 
						|
 | 
						|
    , servername: tun.name
 | 
						|
    , name: tun.name
 | 
						|
    , serviceport: tun.serviceport
 | 
						|
    , data: tun.data
 | 
						|
    , remoteFamily: tun.family
 | 
						|
    , remoteAddress: tun.address
 | 
						|
    , remotePort: tun.port
 | 
						|
    };
 | 
						|
    var conn = net.createConnection(netOpts, function () {
 | 
						|
      // this will happen before 'data' or 'readable' is triggered
 | 
						|
      // We use the data from the netOpts object so that the createConnection function has
 | 
						|
      // the oppurtunity of removing/changing it if it wants/needs to handle it differently.
 | 
						|
    });
 | 
						|
    return conn;
 | 
						|
  }
 | 
						|
 | 
						|
  function redirectHttp(cb) {
 | 
						|
    var socketPair = require('socket-pair');
 | 
						|
    var conn = socketPair.create(function (err, other) {
 | 
						|
      if (err) { cb(err); return; }
 | 
						|
      handlers.http(other);
 | 
						|
      cb(null, conn);
 | 
						|
    });
 | 
						|
    //if (tun.data) { conn.write(tun.data); }
 | 
						|
    return conn;
 | 
						|
  }
 | 
						|
 | 
						|
  function echoTcp(cb) {
 | 
						|
    var socketPair = require('socket-pair');
 | 
						|
    var conn = socketPair.create(function (err, other) {
 | 
						|
      if (err) { cb(err); return; }
 | 
						|
 | 
						|
      other.on('data', function (chunk) {
 | 
						|
        other.write(chunk);
 | 
						|
      });
 | 
						|
 | 
						|
      other.on('end', function () {
 | 
						|
        other.end();
 | 
						|
      });
 | 
						|
 | 
						|
      cb(null, conn);
 | 
						|
 | 
						|
      other.write("\n" +
 | 
						|
      [ "[Telebit Echo Server] v1.0"
 | 
						|
      , "To configure tcp run the following:"
 | 
						|
      , "\ttelebit tcp <port number or module name>"
 | 
						|
      , "\tex: telebit tcp 5050"
 | 
						|
      , "\tex: telebit tcp /path/to/module"
 | 
						|
      , "\tex: telebit tcp none"
 | 
						|
      ].join("\n") + "\n\n");
 | 
						|
    });
 | 
						|
    //if (tun.data) { conn.write(tun.data); }
 | 
						|
    return conn;
 | 
						|
  }
 | 
						|
 | 
						|
  function defineProps(other, tun) {
 | 
						|
    Object.defineProperty(other, 'remoteFamily', {
 | 
						|
      enumerable: false,
 | 
						|
      configurable: true,
 | 
						|
      get: function() {
 | 
						|
        return tun.family;
 | 
						|
      }
 | 
						|
    });
 | 
						|
    Object.defineProperty(other, 'remoteAddress', {
 | 
						|
      enumerable: false,
 | 
						|
      configurable: true,
 | 
						|
      get: function() {
 | 
						|
        return tun.address;
 | 
						|
      }
 | 
						|
    });
 | 
						|
    Object.defineProperty(other, 'remotePort', {
 | 
						|
      enumerable: false,
 | 
						|
      configurable: true,
 | 
						|
      get: function() {
 | 
						|
        return parseInt(tun.port);
 | 
						|
      }
 | 
						|
    });
 | 
						|
    Object.defineProperty(other, 'localPort', {
 | 
						|
      enumerable: false,
 | 
						|
      configurable: true,
 | 
						|
      get: function() {
 | 
						|
        return parseInt(tun.serviceport);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function invokeTcpHandler(conf, socket, tun, id, cb) {
 | 
						|
    var conn;
 | 
						|
    if (parseInt(conf.handler, 10)) {
 | 
						|
      conn = getNetConn(conf.handler);
 | 
						|
      cb(null, conn);
 | 
						|
      return conn;
 | 
						|
    }
 | 
						|
 | 
						|
    var handle = tun.port;
 | 
						|
    var handler;
 | 
						|
    var handlerpath = conf.handler;
 | 
						|
    var homedir = os.homedir();
 | 
						|
    var localshare = path.join(homedir, '.local/share/telebit/apps');
 | 
						|
 | 
						|
    if (/^~/.test(handlerpath)) {
 | 
						|
      handlerpath = path.join(homedir, handlerpath.replace(/^~(\/?)/, ''));
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      handler = require(handlerpath);
 | 
						|
      console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
 | 
						|
    } catch(e1) {
 | 
						|
      try {
 | 
						|
        handler = require(path.join(localshare, handlerpath));
 | 
						|
        console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
 | 
						|
      } catch(e2) {
 | 
						|
        console.error("Failed to require('" + handlerpath + "'):", e1.message);
 | 
						|
        console.error("Failed to require('" + path.join(localshare, handlerpath) + "'):", e2.message);
 | 
						|
        console.warn("Using default handler for '" + handle + ":" + id + "'");
 | 
						|
        echoTcp(cb);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    var socketPair = require('socket-pair');
 | 
						|
    conn = socketPair.create(function (err, other) {
 | 
						|
      handler(other, tun, id);
 | 
						|
      cb(null, conn);
 | 
						|
    });
 | 
						|
    return conn;
 | 
						|
  }
 | 
						|
  var handlerservers = {};
 | 
						|
  function invokeHandler(conf, tlsSocket, tun, id) {
 | 
						|
    var conn;
 | 
						|
    if (parseInt(conf.handler, 10)) {
 | 
						|
      // TODO http-proxy with proper headers and ws support
 | 
						|
      conn = getNetConn(conf.handler);
 | 
						|
      console.info("Port-Forwarding '" + (tun.name || tun.serviceport) + "' to '" + conf.handler + "'");
 | 
						|
      conn.pipe(tlsSocket);
 | 
						|
      tlsSocket.pipe(conn);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    var handle = tun.name || tun.port;
 | 
						|
    var handler;
 | 
						|
    var handlerpath = conf.handler;
 | 
						|
    var homedir = os.homedir();
 | 
						|
    var localshare = path.join(homedir, '.local/share/telebit/apps');
 | 
						|
    var http = require('http');
 | 
						|
 | 
						|
    // 1. No modification handlerpath may be an aboslute path
 | 
						|
    // 2. it may be relative to a user home directory
 | 
						|
    // 3. it may be relative to a user local/share
 | 
						|
 | 
						|
    tlsSocket._tun = tun;
 | 
						|
    tlsSocket._id = id;
 | 
						|
    if (handlerservers[conf.handler]) {
 | 
						|
      handlerservers[conf.handler].emit('connection', tlsSocket);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (/^~/.test(handlerpath)) {
 | 
						|
      handlerpath = path.join(homedir, handlerpath.replace(/^~(\/?)/, ''));
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      handler = require(handlerpath);
 | 
						|
      console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
 | 
						|
    } catch(e1) {
 | 
						|
      try {
 | 
						|
        handler = require(path.join(localshare, handlerpath));
 | 
						|
        console.info("Handling '" + handle + ":" + id + "' with '" + handlerpath + "'");
 | 
						|
      } catch(e2) {
 | 
						|
        console.error("Failed to require('" + handlerpath + "'):", e1.message);
 | 
						|
        console.error("Failed to require('" + path.join(localshare, handlerpath) + "'):", e2.message);
 | 
						|
        console.warn("Using default handler for '" + handle + ":" + id + "'");
 | 
						|
        handler = null;
 | 
						|
        // fallthru
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (handler) {
 | 
						|
      handlerservers[conf.handler] = http.createServer(handler);
 | 
						|
      handlerservers[conf.handler].emit('connection', tlsSocket);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    handlers.https(tlsSocket, tun, id);
 | 
						|
  }
 | 
						|
 | 
						|
  function terminateTls(tun, cb) {
 | 
						|
    var socketPair = require('socket-pair');
 | 
						|
    var conn = socketPair.create(function (err, other) {
 | 
						|
      if (err) { cb(err); return; }
 | 
						|
 | 
						|
      //console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
 | 
						|
      defineProps(other, tun);
 | 
						|
      //console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
 | 
						|
 | 
						|
      if (!state.greenlock) {
 | 
						|
        state.greenlock = require('greenlock').create(state.greenlockConfig);
 | 
						|
      }
 | 
						|
 | 
						|
      if (!state.terminatorServer) {
 | 
						|
        state.terminatorServer = require('tls').createServer(state.greenlock.tlsOptions, function (tlsSocket) {
 | 
						|
          var Packer = require('proxy-packer');
 | 
						|
          var addr = Packer.socketToAddr(tlsSocket);
 | 
						|
          var id = Packer.addrToId(addr);
 | 
						|
 | 
						|
          defineProps(tlsSocket, addr);
 | 
						|
          //console.log('[hit tls server]', tlsSocket.remoteFamily, tlsSocket.remoteAddress, tlsSocket.remotePort, tlsSocket.localPort);
 | 
						|
          //console.log(addr);
 | 
						|
          var conf = state.servernames[tlsSocket.servername];
 | 
						|
          tlsSocket.once('data', function (firstChunk) {
 | 
						|
            tlsSocket.pause();
 | 
						|
            //tlsSocket.unshift(firstChunk);
 | 
						|
            tlsSocket._handle.onread(firstChunk.length, firstChunk);
 | 
						|
 | 
						|
            trySsh({ data: firstChunk }, function (err, conn) {
 | 
						|
              process.nextTick(function () { tlsSocket.resume(); });
 | 
						|
 | 
						|
              if (conn) {
 | 
						|
                conn.pipe(tlsSocket);
 | 
						|
                tlsSocket.pipe(conn);
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              if (!conf || !conf.handler || 'none' === conf.handler) {
 | 
						|
                console.log('https default handler');
 | 
						|
                handlers.https(tlsSocket);
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              console.log('https invokeHandler');
 | 
						|
              invokeHandler(conf, tlsSocket, tun, id);
 | 
						|
            });
 | 
						|
          });
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      //console.log('[hit tcp connection]', other.remoteFamily, other.remoteAddress, other.remotePort, other.localPort);
 | 
						|
      state.terminatorServer.emit('connection', other);
 | 
						|
      cb(null, conn);
 | 
						|
    });
 | 
						|
    //if (tun.data) { conn.write(tun.data); }
 | 
						|
    return conn;
 | 
						|
  }
 | 
						|
 | 
						|
  var handled;
 | 
						|
 | 
						|
  if (!tun.name && !tun.serviceport) {
 | 
						|
    console.log('tun:\n',tun);
 | 
						|
    //console.warn(tun.data.toString());
 | 
						|
    cb(new Error("No routing information for ':tun_id'. Missing both 'name' and 'serviceport'."));
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if ('http' === tun.service || 'https' === tun.service) {
 | 
						|
    if (!tun.name) {
 | 
						|
      cb(new Error("No routing information for ':tun_id'. Service '" + tun.service + "' is missing 'name'."));
 | 
						|
      return;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if ('http' === tun.service) {
 | 
						|
    // TODO match *.example.com
 | 
						|
    handled = Object.keys(state.servernames).some(function (sn) {
 | 
						|
      if (sn !== tun.name) { return; }
 | 
						|
 | 
						|
      console.log('Found config match for PLAIN', tun.name);
 | 
						|
      if (!state.servernames[sn]) { return; }
 | 
						|
 | 
						|
      if (false === state.servernames[sn].terminate) {
 | 
						|
        cb(new Error("insecure http not supported yet"));
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      console.log('Redirecting HTPTP for', tun.name);
 | 
						|
      redirectHttp(cb);
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
    if (!handled) {
 | 
						|
      redirectHttp(cb);
 | 
						|
    }
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if ('https' === tun.service) {
 | 
						|
    // TODO match *.example.com
 | 
						|
    handled = Object.keys(state.servernames).some(function (sn) {
 | 
						|
      if (sn !== tun.name) { return; }
 | 
						|
 | 
						|
      console.log('Found config match for TLS', tun.name);
 | 
						|
      if (!state.servernames[sn]) { return; }
 | 
						|
 | 
						|
      if (false === state.servernames[sn].terminate) {
 | 
						|
        cb(new Error("insecure http not supported yet"));
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      console.log('Terminating TLS for', tun.name);
 | 
						|
      terminateTls(tun, cb);
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
    if (!handled) {
 | 
						|
      terminateTls(tun, cb);
 | 
						|
    }
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if ('tcp' === tun.service) {
 | 
						|
    trySsh(tun, function (err, conn) {
 | 
						|
      if (conn) { cb(null, conn); return; }
 | 
						|
      // TODO add TCP handlers
 | 
						|
      var conf = state.ports[tun.serviceport];
 | 
						|
      if (!conf || !conf.handler || 'none' === conf.handler) {
 | 
						|
        console.log('Using echo server for tcp');
 | 
						|
        echoTcp(cb);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      var Packer = require('proxy-packer');
 | 
						|
      //var addr = Packer.socketToAddr(conn);
 | 
						|
      var id = Packer.addrToId(tun);
 | 
						|
      invokeTcpHandler(conf, conn, tun, id, cb);
 | 
						|
    });
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  console.warn("Unknown service '" + tun.service + "'");
 | 
						|
 | 
						|
  /*
 | 
						|
  var portList = state.services[service];
 | 
						|
  var port;
 | 
						|
  port = portList[tun.name];
 | 
						|
  if (!port) {
 | 
						|
    // Check for any wildcard domains, sorted longest to shortest so the one with the
 | 
						|
    // biggest natural match will be found first.
 | 
						|
    Object.keys(portList).filter(function (pattern) {
 | 
						|
      return pattern[0] === '*' && pattern.length > 1;
 | 
						|
    }).sort(function (a, b) {
 | 
						|
      return b.length - a.length;
 | 
						|
    }).some(function (pattern) {
 | 
						|
      var subPiece = pattern.slice(1);
 | 
						|
      if (subPiece === tun.name.slice(-subPiece.length)) {
 | 
						|
        port = portList[pattern];
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
  if (!port) {
 | 
						|
    port = portList['*'];
 | 
						|
  }
 | 
						|
  */
 | 
						|
};
 |