Tunnel.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /* eslint-disable consistent-return, no-underscore-dangle */
  2. const { parse } = require('url');
  3. const { EventEmitter } = require('events');
  4. const axios = require('axios');
  5. const debug = require('debug')('localtunnel:client');
  6. const TunnelCluster = require('./TunnelCluster');
  7. module.exports = class Tunnel extends EventEmitter {
  8. constructor(opts = {}) {
  9. super(opts);
  10. this.opts = opts;
  11. this.closed = false;
  12. if (!this.opts.host) {
  13. this.opts.host = 'https://localtunnel.me';
  14. }
  15. }
  16. _getInfo(body) {
  17. /* eslint-disable camelcase */
  18. const { id, ip, port, url, cached_url, max_conn_count } = body;
  19. const { host, port: local_port, local_host } = this.opts;
  20. const { local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts;
  21. return {
  22. name: id,
  23. url,
  24. cached_url,
  25. max_conn: max_conn_count || 1,
  26. remote_host: parse(host).hostname,
  27. remote_ip: ip,
  28. remote_port: port,
  29. local_port,
  30. local_host,
  31. local_https,
  32. local_cert,
  33. local_key,
  34. local_ca,
  35. allow_invalid_cert,
  36. };
  37. /* eslint-enable camelcase */
  38. }
  39. // initialize connection
  40. // callback with connection info
  41. _init(cb) {
  42. const opt = this.opts;
  43. const getInfo = this._getInfo.bind(this);
  44. const params = {
  45. responseType: 'json',
  46. };
  47. const baseUri = `${opt.host}/`;
  48. // no subdomain at first, maybe use requested domain
  49. const assignedDomain = opt.subdomain;
  50. // where to quest
  51. const uri = baseUri + (assignedDomain || '?new');
  52. (function getUrl() {
  53. axios
  54. .get(uri, params)
  55. .then(res => {
  56. const body = res.data;
  57. debug('got tunnel information', res.data);
  58. if (res.status !== 200) {
  59. const err = new Error(
  60. (body && body.message) || 'localtunnel server returned an error, please try again'
  61. );
  62. return cb(err);
  63. }
  64. cb(null, getInfo(body));
  65. })
  66. .catch(err => {
  67. debug(`tunnel server offline: ${err.message}, retry 1s`);
  68. return setTimeout(getUrl, 1000);
  69. });
  70. })();
  71. }
  72. _establish(info) {
  73. // increase max event listeners so that localtunnel consumers don't get
  74. // warning messages as soon as they setup even one listener. See #71
  75. this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
  76. this.tunnelCluster = new TunnelCluster(info);
  77. // only emit the url the first time
  78. this.tunnelCluster.once('open', () => {
  79. this.emit('url', info.url);
  80. });
  81. // re-emit socket error
  82. this.tunnelCluster.on('error', err => {
  83. debug('got socket error', err.message);
  84. this.emit('error', err);
  85. });
  86. let tunnelCount = 0;
  87. // track open count
  88. this.tunnelCluster.on('open', tunnel => {
  89. tunnelCount++;
  90. debug('tunnel open [total: %d]', tunnelCount);
  91. const closeHandler = () => {
  92. tunnel.destroy();
  93. };
  94. if (this.closed) {
  95. return closeHandler();
  96. }
  97. this.once('close', closeHandler);
  98. tunnel.once('close', () => {
  99. this.removeListener('close', closeHandler);
  100. });
  101. });
  102. // when a tunnel dies, open a new one
  103. this.tunnelCluster.on('dead', () => {
  104. tunnelCount--;
  105. debug('tunnel dead [total: %d]', tunnelCount);
  106. if (this.closed) {
  107. return;
  108. }
  109. this.tunnelCluster.open();
  110. });
  111. this.tunnelCluster.on('request', req => {
  112. this.emit('request', req);
  113. });
  114. // establish as many tunnels as allowed
  115. for (let count = 0; count < info.max_conn; ++count) {
  116. this.tunnelCluster.open();
  117. }
  118. }
  119. open(cb) {
  120. this._init((err, info) => {
  121. if (err) {
  122. return cb(err);
  123. }
  124. this.clientId = info.name;
  125. this.url = info.url;
  126. // `cached_url` is only returned by proxy servers that support resource caching.
  127. if (info.cached_url) {
  128. this.cachedUrl = info.cached_url;
  129. }
  130. this._establish(info);
  131. cb();
  132. });
  133. }
  134. close() {
  135. this.closed = true;
  136. this.emit('close');
  137. }
  138. };