Last active
January 4, 2018 20:28
-
-
Save jwickens/0a6dc0655f756d01d01f7057537b057b to your computer and use it in GitHub Desktop.
ngrok wrapper es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const request = require('request-promise-native') | |
const { spawn } = require('child_process') | |
const { EventEmitter } = require('events') | |
const platform = require('os').platform() | |
const uuid = require('uuid') | |
// const url = require('url') | |
const path = require('path') | |
const bin = './ngrok' + (platform === 'win32' ? '.exe' : '') | |
const ready = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/ | |
const inUse = /address already in use/ | |
// note that this file "index.js" was moved into a src dir.. | |
const binDir = path.join(__dirname, '..', '/bin') | |
const MAX_RETRY = 100 | |
class NGrok extends EventEmitter { | |
constructor () { | |
super() | |
this.tunnels = {} | |
} | |
async connect (opts) { | |
opts = this.defaults(opts) | |
this.validate(opts) | |
if (!this.api) { | |
await this.runNgrok(opts) | |
} | |
return this.runTunnel(opts) | |
} | |
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 | |
} | |
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 runNgrok (opts) { | |
const start = ['start', '--none', '--log=stdout'] | |
if (opts.region) { | |
start.push('--region=' + opts.region) | |
} | |
if (opts.configPath) { | |
start.push('--config=' + opts.configPath) | |
} | |
this.ngrok = spawn(bin, start, {cwd: binDir}) | |
let resolvePromise, rejectPromise | |
const promise = new Promise((resolve, reject) => { | |
resolvePromise = resolve | |
rejectPromise = reject | |
}) | |
this.ngrok.stdout.on('data', (data) => { | |
const msg = data.toString() | |
const addr = msg.match(ready) | |
if (addr) { | |
this.api = request.defaults({baseUrl: 'http://' + addr[1]}) | |
resolvePromise() | |
} else if (msg.match(inUse)) { | |
rejectPromise(new Error(msg.substring(0, 10000))) | |
} else { | |
console.warn('not sure when this happens in ngrok') | |
} | |
}) | |
this.ngrok.stderr.on('data', (data) => { | |
const info = data.toString().substring(0, 10000) | |
rejectPromise(new Error(info)) | |
}) | |
this.ngrok.on('close', () => { | |
this.emit('close') | |
}) | |
process.on('exit', async () => { | |
await this.kill() | |
}) | |
try { | |
const response = await promise | |
return response | |
} finally { | |
this.ngrok.stdout.removeAllListeners('data') | |
this.ngrok.stderr.removeAllListeners('data') | |
} | |
} | |
async runTunnel (opts, retryCount = 0) { | |
opts.name = String(opts.name || uuid.v4()) | |
try { | |
const response = await this.api.post({url: 'api/tunnels', json: opts}) | |
const publicUrl = response.public_url | |
if (!publicUrl) { | |
throw new Error(response.msg || 'failed to start tunnel') | |
} | |
this.tunnels[publicUrl] = response.uri | |
if (opts.proto === 'http' && opts.bind_tls !== false) { | |
this.tunnels[publicUrl.replace('https', 'http')] = response.uri + ' (http)' | |
} | |
return publicUrl | |
} catch (err) { | |
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' | |
if ((notReady500 || notReady502) && retryCount < MAX_RETRY) { | |
await new Promise((resolve) => setTimeout(resolve, 200)) | |
retryCount++ | |
return this.runTunnel(opts, retryCount) | |
} | |
throw err | |
} | |
} | |
async authtoken (token, configPath) { | |
const authtoken = ['authtoken', token] | |
if (configPath) { | |
authtoken.push('--config=' + configPath) | |
} | |
const a = spawn( | |
bin, | |
authtoken, | |
{cwd: binDir}) | |
const promise = new Promise((resolve, reject) => { | |
a.stdout.once('data', () => { | |
a.kill() | |
resolve(token) | |
}) | |
a.stderr.once('data', () => { | |
a.kill() | |
reject(new Error('cant set authtoken')) | |
}) | |
}) | |
return promise | |
} | |
async disconnect (publicUrl) { | |
if (!this.api) { | |
return | |
} | |
if (publicUrl) { | |
await this.api.del(this.tunnels[publicUrl]) | |
delete this.tunnels[publicUrl] | |
this.emit('disconnect', publicUrl) | |
return | |
} | |
for (const url in Object.keys(this.tunnels)) { | |
await this.disconnect(url) | |
} | |
this.emit('disconnect') | |
} | |
async kill () { | |
if (!this.ngrok) { | |
return | |
} | |
let resolvePromise | |
const promise = new Promise((resolve) => { | |
resolvePromise = resolve | |
}) | |
this.ngrok.on('exit', () => { | |
this.api = null | |
this.tunnels = {} | |
this.emit('disconnect') | |
resolvePromise() | |
}) | |
this.ngrok.kill() | |
return promise | |
} | |
} | |
module.exports = NGrok |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @bubenshchykov before i submit this as a PR just had a few questions about how to proceed:
A) "ui url"
The uiUrl parsed here - https://github.com/bubenshchykov/ngrok/blob/4a6d1c656ee1bd825a17e6801b3398ebdaaf2136/index.js#L188
What is that? How should we return it as on here in this gist i return just the publicUrl when you
await ngrok.connect
the alternative i thought of was to return an object instead but that doesnt seem really neat.B) auth token
The authToken method here https://gist.github.com/jwickens/0a6dc0655f756d01d01f7057537b057b#file-index-js-L146
I couldnt figure out in the original code how this works, how the auth token makes it back in. In fact this method is never called by the connect method here in the gist.
(C) node.js version supported
Please note that request-promise-native requires node greater than 0.12 (https://github.com/request/request-promise-native/blob/1874877850a59152915c9e9cbacbdc577486cca5/package.json#L33)
The async/await, classes and destructuring runs fine natively on node.js >= 8.3
For versions below that babel can be used, however that comes at the cost of making the code less easily inspectable from node_modules depending on how low you want to go.
So what do you want to target?