forked from coolaj86/telebit.js
		
	
		
			
				
	
	
		
			306 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
var common = module.exports;
 | 
						|
 | 
						|
var path = require('path');
 | 
						|
var url = require('url');
 | 
						|
var fs = require('fs');
 | 
						|
var mkdirp = require('mkdirp');
 | 
						|
var os = require('os');
 | 
						|
var homedir = os.homedir();
 | 
						|
var urequest = require('@coolaj86/urequest');
 | 
						|
 | 
						|
common._NOTIFICATIONS = {
 | 
						|
  'newsletter': [ 'newsletter', 'communityMember' ]
 | 
						|
, 'important': [ 'communityMember' ]
 | 
						|
};
 | 
						|
common.CONFIG_KEYS = [
 | 
						|
  'newsletter'
 | 
						|
, 'communityMember'
 | 
						|
, 'telemetry'
 | 
						|
, 'sshAuto'
 | 
						|
, 'email'
 | 
						|
, 'agreeTos'
 | 
						|
, 'relay'
 | 
						|
, 'token'
 | 
						|
, 'pretoken'
 | 
						|
, 'secret'
 | 
						|
];
 | 
						|
//, '_servernames' // list instead of object
 | 
						|
//, '_ports'       // list instead of object
 | 
						|
//, '_otp'         // otp should not be saved
 | 
						|
//, '_token'       // temporary token
 | 
						|
 | 
						|
common.getPort = function (config, cb) {
 | 
						|
  var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
 | 
						|
  if (cb) {
 | 
						|
    return fs.readFile(portfile, 'utf8', function (err, text) {
 | 
						|
      cb(err, parseInt((text||'').trim(), 10) || null);
 | 
						|
    });
 | 
						|
  } else {
 | 
						|
    try {
 | 
						|
      return parseInt(fs.readFileSync(portfile, 'utf8').trim(), 10) || null;
 | 
						|
    } catch(e) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
common.setPort = function (config, num, cb) {
 | 
						|
  var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
 | 
						|
  var numstr = (num || '').toString();
 | 
						|
  if (cb) {
 | 
						|
    return fs.writeFile(portfile, numstr, 'utf8', function (err) {
 | 
						|
      cb(err);
 | 
						|
    });
 | 
						|
  } else {
 | 
						|
    try {
 | 
						|
      return fs.writeFileSync(portfile, numstr, 'utf8');
 | 
						|
    } catch(e) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
common.removePort = function (config, cb) {
 | 
						|
  var portfile = path.resolve(config.sock || common.DEFAULT_SOCK_PATH, '..', 'telebit.port');
 | 
						|
  if (cb) {
 | 
						|
    return fs.unlink(portfile, function (err, text) {
 | 
						|
      cb(err, (text||'').trim());
 | 
						|
    });
 | 
						|
  } else {
 | 
						|
    try {
 | 
						|
      return fs.unlinkSync(portfile);
 | 
						|
    } catch(e) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
common.pipename = function (config) {
 | 
						|
  var _ipc = {
 | 
						|
    path: (config.sock || common.DEFAULT_SOCK_PATH)
 | 
						|
  , comment: (/^win/i.test(os.platform()) ? 'windows pipe' : 'unix socket')
 | 
						|
  , type: (/^win/i.test(os.platform()) ? 'pipe' : 'socket')
 | 
						|
  };
 | 
						|
  if ('pipe' === _ipc.type) {
 | 
						|
    _ipc.path = '\\\\?\\pipe' + _ipc.path.replace(/\//, '\\');
 | 
						|
  }
 | 
						|
  return _ipc;
 | 
						|
};
 | 
						|
common.DEFAULT_SOCK_PATH = path.join(homedir, '.local/share/telebit/var/run', 'telebit.sock');
 | 
						|
common.DEFAULT_CONFIG_PATH = path.join(homedir, '.config/telebit', 'telebitd.yml');
 | 
						|
 | 
						|
common.parseUrl = function (hostname) {
 | 
						|
  var location = url.parse(hostname);
 | 
						|
  if (!location.protocol || /\./.test(location.protocol)) {
 | 
						|
    hostname = 'https://' + hostname;
 | 
						|
    location = url.parse(hostname);
 | 
						|
  }
 | 
						|
  hostname = location.hostname + (location.port ? ':' + location.port : '');
 | 
						|
  hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
 | 
						|
  return hostname;
 | 
						|
};
 | 
						|
common.parseHostname = function (hostname) {
 | 
						|
  var location = url.parse(hostname);
 | 
						|
  if (!location.protocol || /\./.test(location.protocol)) {
 | 
						|
    hostname = 'https://' + hostname;
 | 
						|
    location = url.parse(hostname);
 | 
						|
  }
 | 
						|
  //hostname = location.hostname + (location.port ? ':' + location.port : '');
 | 
						|
  //hostname = location.protocol.replace(/https?/, 'https') + '//' + hostname + location.pathname;
 | 
						|
  return location.hostname;
 | 
						|
};
 | 
						|
 | 
						|
common.apiDirectory = '_apis/telebit.cloud/index.json';
 | 
						|
 | 
						|
common.otp = function getOtp() {
 | 
						|
  return Math.round(Math.random() * 9999).toString().padStart(4, '0');
 | 
						|
};
 | 
						|
common.signToken = function (state) {
 | 
						|
  var jwt = require('jsonwebtoken');
 | 
						|
  var tokenData = {
 | 
						|
    domains: Object.keys(state.config.servernames || {}).filter(function (name) {
 | 
						|
      return /\./.test(name);
 | 
						|
    })
 | 
						|
  , ports: Object.keys(state.config.ports || {}).filter(function (port) {
 | 
						|
      port = parseInt(port, 10);
 | 
						|
      return port > 0 && port <= 65535;
 | 
						|
    })
 | 
						|
  , aud: state._relayUrl
 | 
						|
  , iss: Math.round(Date.now() / 1000)
 | 
						|
  };
 | 
						|
 | 
						|
  return jwt.sign(tokenData, state.config.secret);
 | 
						|
};
 | 
						|
common.api = {};
 | 
						|
common.api.directory = function (state, next) {
 | 
						|
  state._relayUrl = common.parseUrl(state.relay);
 | 
						|
  urequest({ url: state._relayUrl + common.apiDirectory, json: true }, function (err, resp, dir) {
 | 
						|
    if (!dir) { dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } }; }
 | 
						|
    state._apiDirectory = dir;
 | 
						|
    next(err, dir);
 | 
						|
  });
 | 
						|
};
 | 
						|
common.api._parseWss = function (state, dir) {
 | 
						|
  if (!dir || !dir.api_host) {
 | 
						|
    dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
 | 
						|
  }
 | 
						|
  state._relayHostname = common.parseHostname(state.relay);
 | 
						|
  return dir.tunnel.method + '://' + dir.api_host.replace(/:hostname/g, state._relayHostname) + dir.tunnel.pathname;
 | 
						|
};
 | 
						|
common.api.wss = function (state, cb) {
 | 
						|
  common.api.directory(state, function (err, dir) {
 | 
						|
    cb(err, common.api._parseWss(state, dir));
 | 
						|
  });
 | 
						|
};
 | 
						|
common.api.token = function (state, handlers) {
 | 
						|
  common.api.directory(state, function (err, dir) {
 | 
						|
    // directory, requested, connect, tunnelUrl, offer, granted, end
 | 
						|
    function afterDir() {
 | 
						|
      //console.log('[debug] after dir');
 | 
						|
      state.wss = common.api._parseWss(state, dir);
 | 
						|
 | 
						|
      handlers.tunnelUrl(state.wss, function () {
 | 
						|
        //console.log('[debug] after tunnelUrl');
 | 
						|
        if (state.config.secret /* && !state.config.token */) {
 | 
						|
          state.config._token = common.signToken(state);
 | 
						|
        }
 | 
						|
        state.token = state.token || state.config.token || state.config._token;
 | 
						|
        if (state.token) {
 | 
						|
          //console.log('[debug] token via token or secret');
 | 
						|
          // { token, pretoken }
 | 
						|
          handlers.connect(state.token, function () {
 | 
						|
            handlers.end(null, function () {});
 | 
						|
          });
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // backwards compat (TODO remove)
 | 
						|
        if (err || !dir || !dir.pair_request) {
 | 
						|
          //console.log('[debug] no dir, connect');
 | 
						|
          handlers.error(new Error("No token found or generated, and no pair_request api found."));
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // TODO sign token with own private key, including public key and thumbprint
 | 
						|
        //      (much like ACME JOSE account)
 | 
						|
        var otp = state.config._otp; // common.otp();
 | 
						|
        var authReq = {
 | 
						|
          subject: state.config.email
 | 
						|
        , subject_scheme: 'mailto'
 | 
						|
          // TODO create domains list earlier
 | 
						|
        , scope: (state.config._servernames || Object.keys(state.config.servernames || {}))
 | 
						|
            .concat(state.config._ports || Object.keys(state.config.ports || {})).join(',')
 | 
						|
        , otp: otp
 | 
						|
        , hostname: os.hostname()
 | 
						|
          // Used for User-Agent
 | 
						|
        , os_type: os.type()
 | 
						|
        , os_platform: os.platform()
 | 
						|
        , os_release: os.release()
 | 
						|
        , os_arch: os.arch()
 | 
						|
        };
 | 
						|
        var pairRequestUrl = url.resolve('https://' + dir.api_host.replace(/:hostname/g, state._relayHostname), dir.pair_request.pathname);
 | 
						|
        var req = {
 | 
						|
          url: pairRequestUrl
 | 
						|
        , method: dir.pair_request.method
 | 
						|
        , json: authReq
 | 
						|
        };
 | 
						|
        var firstReq = true;
 | 
						|
        var firstReady = true;
 | 
						|
 | 
						|
        function gotoNext(req) {
 | 
						|
          //console.log('[debug] gotoNext called');
 | 
						|
          urequest(req, function (err, resp, body) {
 | 
						|
            if (err) {
 | 
						|
              //console.log('[debug] gotoNext error');
 | 
						|
              err._request = req;
 | 
						|
              err._hint = '[telebitd.js] pair request';
 | 
						|
              handlers.error(err, function () {});
 | 
						|
              return;
 | 
						|
            }
 | 
						|
 | 
						|
            function checkLocation() {
 | 
						|
              //console.log('[debug] checkLocation');
 | 
						|
              // pending, try again
 | 
						|
              if ('pending' === body.status && resp.headers.location) {
 | 
						|
                //console.log('[debug] pending');
 | 
						|
                setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              if ('ready' === body.status) {
 | 
						|
                //console.log('[debug] ready');
 | 
						|
                if (firstReady) {
 | 
						|
                  //console.log('[debug] first ready');
 | 
						|
                  firstReady = false;
 | 
						|
                  state.token = body.access_token;
 | 
						|
                  state.config.token = state.token;
 | 
						|
                  handlers.offer(body.access_token, function () {
 | 
						|
                    /*ignore*/
 | 
						|
                  });
 | 
						|
                }
 | 
						|
                setTimeout(gotoNext, 2 * 1000, req);
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              if ('complete' === body.status) {
 | 
						|
                //console.log('[debug] complete');
 | 
						|
                handlers.granted(null, function () {
 | 
						|
                  handlers.end(null, function () {});
 | 
						|
                });
 | 
						|
                return;
 | 
						|
              }
 | 
						|
 | 
						|
              //console.log('[debug] bad status');
 | 
						|
              var err = new Error("Bad State:" + body.status);
 | 
						|
              err._request = req;
 | 
						|
              handlers.error(err, function () {});
 | 
						|
            }
 | 
						|
 | 
						|
            if (firstReq) {
 | 
						|
              //console.log('[debug] first req');
 | 
						|
              handlers.requested(authReq, function () {
 | 
						|
                handlers.connect(body.access_token || body.jwt, function () {
 | 
						|
                  var err;
 | 
						|
                  if (!resp.headers.location) {
 | 
						|
                    err = new Error("bad authentication request response");
 | 
						|
                    err._resp = resp.toJSON();
 | 
						|
                    handlers.error(err, function () {});
 | 
						|
                    return;
 | 
						|
                  }
 | 
						|
                  setTimeout(gotoNext, 2 * 1000, { url: resp.headers.location, json: true });
 | 
						|
                });
 | 
						|
              });
 | 
						|
              firstReq = false;
 | 
						|
              return;
 | 
						|
            } else {
 | 
						|
              //console.log('[debug] other req');
 | 
						|
              checkLocation();
 | 
						|
            }
 | 
						|
          });
 | 
						|
        }
 | 
						|
 | 
						|
        gotoNext(req);
 | 
						|
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    if (dir && dir.api_host) {
 | 
						|
      handlers.directory(dir, afterDir);
 | 
						|
    } else {
 | 
						|
      // backwards compat
 | 
						|
      dir = { api_host: ':hostname', tunnel: { method: "wss", pathname: "" } };
 | 
						|
      afterDir();
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
common._init = function (rootpath, confpath) {
 | 
						|
  try {
 | 
						|
    mkdirp.sync(path.join(rootpath, 'var', 'log'));
 | 
						|
    mkdirp.sync(path.join(rootpath, 'var', 'run'));
 | 
						|
    mkdirp.sync(path.join(confpath));
 | 
						|
  } catch(e) {
 | 
						|
    console.error(e);
 | 
						|
  }
 | 
						|
};
 |