264 lines
5.5 KiB
JavaScript
264 lines
5.5 KiB
JavaScript
|
var request = require('request');
|
||
|
var spawn = require('child_process').spawn;
|
||
|
var Emitter = require('events').EventEmitter;
|
||
|
var platform = require('os').platform();
|
||
|
var lock = require('lock')();
|
||
|
var async = require('async');
|
||
|
var uuid = require('uuid');
|
||
|
var url = require('url');
|
||
|
var logfmt = require('logfmt');
|
||
|
|
||
|
var bin = './ngrok' + (platform === 'win32' ? '.exe' : '');
|
||
|
|
||
|
var noop = function() {};
|
||
|
var emitter = new Emitter().on('error', noop);
|
||
|
var ngrok, api, tunnels = {};
|
||
|
|
||
|
function connect(opts, cb) {
|
||
|
|
||
|
if (typeof opts === 'function') {
|
||
|
cb = opts;
|
||
|
}
|
||
|
|
||
|
cb = cb || noop;
|
||
|
opts = defaults(opts);
|
||
|
|
||
|
if (api) {
|
||
|
return runTunnel(opts, cb);
|
||
|
}
|
||
|
|
||
|
lock('ngrok', function(release) {
|
||
|
function run(err) {
|
||
|
if (err) {
|
||
|
emitter.emit('error', err);
|
||
|
return cb(err);
|
||
|
}
|
||
|
runNgrok(opts, release(function(err) {
|
||
|
if (err) {
|
||
|
emitter.emit('error', err);
|
||
|
return cb(err);
|
||
|
}
|
||
|
runTunnel(opts, cb)
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
opts.authtoken ?
|
||
|
authtoken(opts.authtoken, opts.configPath, run) :
|
||
|
run(null);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
if (['us', 'eu', 'au', 'ap'].indexOf(opts.region) === -1) {
|
||
|
opts.region = 'us';
|
||
|
}
|
||
|
|
||
|
if (!opts.configPath) {
|
||
|
opts.configPath = '~/.ngrok2/ngrok.yml';
|
||
|
}
|
||
|
|
||
|
return opts;
|
||
|
}
|
||
|
|
||
|
function runNgrok(opts, cb) {
|
||
|
if (api) {
|
||
|
return cb();
|
||
|
}
|
||
|
|
||
|
emitter.emit('statuschange', 'starting');
|
||
|
|
||
|
ngrok = spawn(
|
||
|
bin,
|
||
|
['start', '--none', '--log=stdout', '--region=' + opts.region, '-config=' + opts.configPath],
|
||
|
{cwd: __dirname + '/bin'});
|
||
|
|
||
|
ngrok.stdout.on('data', function (chunk) {
|
||
|
var lines = chunk.toString().split(/\r?\n/);
|
||
|
lines.forEach(function (line) {
|
||
|
if (!line) return;
|
||
|
var data = logfmt.parse(line);
|
||
|
if (data.obj === 'web' && data.msg === 'starting web service' && data.addr) {
|
||
|
api = request.defaults({
|
||
|
baseUrl: 'http://' + data.addr,
|
||
|
json: true
|
||
|
});
|
||
|
cb();
|
||
|
} else if (data.obj === 'csess' && data.msg === 'session closed, starting reconnect loop') {
|
||
|
emitter.emit('statuschange', 'reconnecting');
|
||
|
} else if (data.obj === 'csess' && data.msg === 'client session established') {
|
||
|
emitter.emit('statuschange', 'online');
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
ngrok.stderr.on('data', function (data) {
|
||
|
var info = data.toString().substring(0, 10000);
|
||
|
return cb(new Error(info));
|
||
|
});
|
||
|
|
||
|
ngrok.on('exit', function () {
|
||
|
ngrok = null;
|
||
|
api = null;
|
||
|
tunnels = {};
|
||
|
});
|
||
|
|
||
|
ngrok.on('close', function () {
|
||
|
return emitter.emit('close');
|
||
|
});
|
||
|
|
||
|
process.on('exit', function() {
|
||
|
kill();
|
||
|
});
|
||
|
|
||
|
ngrok.on('error', function(err) {
|
||
|
cb(err);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function runTunnel(opts, cb) {
|
||
|
_runTunnel(opts, function(err, publicUrl, uiUrl) {
|
||
|
if (err) {
|
||
|
emitter.emit('error', err);
|
||
|
return cb(err);
|
||
|
}
|
||
|
emitter.emit('connect', publicUrl, uiUrl);
|
||
|
return cb(null, publicUrl, uiUrl);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function _runTunnel(opts, cb) {
|
||
|
var retries = 100;
|
||
|
opts.name = String(opts.name || uuid.v4());
|
||
|
var retry = function() {
|
||
|
api.post(
|
||
|
{url: 'api/tunnels', json: opts},
|
||
|
function(err, resp, body) {
|
||
|
if (err) {
|
||
|
return cb(err);
|
||
|
}
|
||
|
var notReady = resp.statusCode === 500 && /panic/.test(body) ||
|
||
|
resp.statusCode === 502 && body.details &&
|
||
|
body.details.err === 'tunnel session not ready yet';
|
||
|
|
||
|
if (notReady) {
|
||
|
return retries-- ?
|
||
|
setTimeout(retry, 200) :
|
||
|
cb(new Error(body));
|
||
|
}
|
||
|
var publicUrl = body && body.public_url;
|
||
|
if (!publicUrl) {
|
||
|
var err = Object.assign(new Error(body.msg || 'failed to start tunnel'), body);
|
||
|
return cb(err);
|
||
|
}
|
||
|
tunnels[publicUrl] = body.uri;
|
||
|
if (opts.proto === 'http' && opts.bind_tls !== false) {
|
||
|
tunnels[publicUrl.replace('https', 'http')] = body.uri + ' (http)';
|
||
|
}
|
||
|
var uiUrl = url.parse(resp.request.uri);
|
||
|
uiUrl = uiUrl.resolve('/').slice(0, -1);
|
||
|
return cb(null, publicUrl, uiUrl);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
retry();
|
||
|
}
|
||
|
|
||
|
function authtoken(token, configPath, cb) {
|
||
|
cb = cb || noop;
|
||
|
var a = spawn(
|
||
|
bin,
|
||
|
['authtoken', token, '-config=' + configPath],
|
||
|
{cwd: __dirname + '/bin'});
|
||
|
a.stdout.once('data', done.bind(null, null, token));
|
||
|
a.stderr.once('data', done.bind(null, new Error('cant set authtoken')));
|
||
|
a.on('error', function(err) {
|
||
|
cb(err);
|
||
|
});
|
||
|
|
||
|
function done(err, token) {
|
||
|
cb(err, token);
|
||
|
a.kill();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function disconnect(publicUrl, cb) {
|
||
|
cb = cb || noop;
|
||
|
if (typeof publicUrl === 'function') {
|
||
|
cb = publicUrl;
|
||
|
publicUrl = null;
|
||
|
}
|
||
|
if (!api) {
|
||
|
return cb();
|
||
|
}
|
||
|
if (publicUrl) {
|
||
|
return api.del(
|
||
|
tunnels[publicUrl],
|
||
|
function(err, resp, body) {
|
||
|
if (err || resp.statusCode !== 204) {
|
||
|
return cb(err || new Error(body));
|
||
|
}
|
||
|
delete tunnels[publicUrl];
|
||
|
emitter.emit('disconnect', publicUrl);
|
||
|
return cb();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return async.each(
|
||
|
Object.keys(tunnels),
|
||
|
disconnect,
|
||
|
function(err) {
|
||
|
if (err) {
|
||
|
emitter.emit('error', err);
|
||
|
return cb(err);
|
||
|
}
|
||
|
emitter.emit('disconnect');
|
||
|
return cb();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function kill(cb) {
|
||
|
cb = cb || noop;
|
||
|
if (!ngrok) {
|
||
|
return cb();
|
||
|
}
|
||
|
ngrok.on('exit', function() {
|
||
|
emitter.emit('disconnect');
|
||
|
return cb();
|
||
|
});
|
||
|
return ngrok.kill();
|
||
|
}
|
||
|
|
||
|
function ngrokProcess() {
|
||
|
return ngrok;
|
||
|
}
|
||
|
|
||
|
emitter.connect = connect;
|
||
|
emitter.disconnect = disconnect;
|
||
|
emitter.authtoken = authtoken;
|
||
|
emitter.kill = kill;
|
||
|
emitter.process = ngrokProcess;
|
||
|
|
||
|
module.exports = emitter;
|