/* eslint-disable consistent-return, no-underscore-dangle */ const { parse } = require('url'); const { EventEmitter } = require('events'); const axios = require('axios'); const debug = require('debug')('localtunnel:client'); const TunnelCluster = require('./TunnelCluster'); module.exports = class Tunnel extends EventEmitter { constructor(opts = {}) { super(opts); this.opts = opts; this.closed = false; if (!this.opts.host) { this.opts.host = 'https://localtunnel.me'; } } _getInfo(body) { /* eslint-disable camelcase */ const { id, ip, port, url, cached_url, max_conn_count } = body; const { host, port: local_port, local_host } = this.opts; const { local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts; return { name: id, url, cached_url, max_conn: max_conn_count || 1, remote_host: parse(host).hostname, remote_ip: ip, remote_port: port, local_port, local_host, local_https, local_cert, local_key, local_ca, allow_invalid_cert, }; /* eslint-enable camelcase */ } // initialize connection // callback with connection info _init(cb) { const opt = this.opts; const getInfo = this._getInfo.bind(this); const params = { responseType: 'json', }; const baseUri = `${opt.host}/`; // no subdomain at first, maybe use requested domain const assignedDomain = opt.subdomain; // where to quest const uri = baseUri + (assignedDomain || '?new'); (function getUrl() { axios .get(uri, params) .then(res => { const body = res.data; debug('got tunnel information', res.data); if (res.status !== 200) { const err = new Error( (body && body.message) || 'localtunnel server returned an error, please try again' ); return cb(err); } cb(null, getInfo(body)); }) .catch(err => { debug(`tunnel server offline: ${err.message}, retry 1s`); return setTimeout(getUrl, 1000); }); })(); } _establish(info) { // increase max event listeners so that localtunnel consumers don't get // warning messages as soon as they setup even one listener. See #71 this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10)); this.tunnelCluster = new TunnelCluster(info); // only emit the url the first time this.tunnelCluster.once('open', () => { this.emit('url', info.url); }); // re-emit socket error this.tunnelCluster.on('error', err => { debug('got socket error', err.message); this.emit('error', err); }); let tunnelCount = 0; // track open count this.tunnelCluster.on('open', tunnel => { tunnelCount++; debug('tunnel open [total: %d]', tunnelCount); const closeHandler = () => { tunnel.destroy(); }; if (this.closed) { return closeHandler(); } this.once('close', closeHandler); tunnel.once('close', () => { this.removeListener('close', closeHandler); }); }); // when a tunnel dies, open a new one this.tunnelCluster.on('dead', () => { tunnelCount--; debug('tunnel dead [total: %d]', tunnelCount); if (this.closed) { return; } this.tunnelCluster.open(); }); this.tunnelCluster.on('request', req => { this.emit('request', req); }); // establish as many tunnels as allowed for (let count = 0; count < info.max_conn; ++count) { this.tunnelCluster.open(); } } open(cb) { this._init((err, info) => { if (err) { return cb(err); } this.clientId = info.name; this.url = info.url; // `cached_url` is only returned by proxy servers that support resource caching. if (info.cached_url) { this.cachedUrl = info.cached_url; } this._establish(info); cb(); }); } close() { this.closed = true; this.emit('close'); } };