index.js 2.92 KB
const request = require('request-promise-native');
const uuid = require('uuid');
const {getProcess, killProcess, setAuthtoken, getVersion} = require('./process');

let processUrl = null;
let internalApi = null;
let tunnels = {};

async function connect (opts) {
  opts = defaults(opts);
  validate(opts);
  if (opts.authtoken) {
    await setAuthtoken(opts);
  }

  processUrl = await getProcess(opts);
  internalApi = request.defaults({baseUrl: processUrl});
  return connectRetry(opts);
}

function defaults (opts) {
  opts = opts || {proto: 'http', addr: 80}
  if (typeof opts === 'function') opts = {proto: 'http', addr: 80};
  if (typeof opts !== 'object') opts = {proto: 'http', addr: opts};
  if (!opts.proto) opts.proto = 'http';
  if (!opts.addr) opts.addr = opts.port || opts.host || 80;
  if (opts.httpauth) opts.auth = opts.httpauth;
  return opts
}

function validate  (opts) {
  if (opts.web_addr === false || opts.web_addr === 'false') {
    throw new Error('web_addr:false is not supported, module depends on internal ngrok api')
  }
}

async function connectRetry (opts, retryCount = 0) {
  opts.name = String(opts.name || uuid.v4());
  try {
    const response = await internalApi.post({url: 'api/tunnels', json: opts});
    const publicUrl = response.public_url;
    if (!publicUrl) {
      throw new Error('failed to start tunnel');
    }
    tunnels[publicUrl] = response.uri;
    if (opts.proto === 'http' && opts.bind_tls !== false) {
      tunnels[publicUrl.replace('https', 'http')] = response.uri + ' (http)';
    }
    return publicUrl;
  } catch (err) {
    if (!isRetriable(err) || retryCount >= 100) {
      throw err.error || err.response;
    }
    await new Promise((resolve) => setTimeout(resolve, 200));
    return connectRetry(opts, ++retryCount);
  }
 }

function isRetriable (err) {
  if (!err.response) return false;
  const body = err.response.body;
  const notReady500 = err.statusCode === 500 && /panic/.test(body)
  const notReady502 = err.statusCode === 502 && body.details && body.details.err === 'tunnel session not ready yet';
  const notReady503 = err.statusCode === 503 && body.details && body.details.err === 'a successful ngrok tunnel session has not yet been established';
  return notReady500 || notReady502 || notReady503;
}

async function disconnect (publicUrl) {
  if (!internalApi) return;
  if (!publicUrl) {
  	const disconnectAll = Object.keys(tunnels).map(disconnect);
  	return Promise.all(disconnectAll);
  }
  const tunnelUrl = tunnels[publicUrl];
  if (!tunnelUrl) {
    throw new Error(`there is no tunnel with url: ${publicUrl}`)
  }
  await internalApi.del(tunnelUrl)
  delete tunnels[publicUrl];
}

async function kill ()  {
  if (!internalApi) return;
  await killProcess();
  internalApi = null;
  tunnels = {}
}

function getUrl() {
  return processUrl;
}

function getApi() {
  return internalApi;
}

module.exports = {
  connect,
  disconnect,
  authtoken: setAuthtoken,
  kill,
  getUrl,
  getApi,
  getVersion
};