UNPKG

4.26 kBJavaScriptView Raw
1const { EventEmitter } = require('events');
2const debug = require('debug')('localtunnel:client');
3const fs = require('fs');
4const net = require('net');
5const tls = require('tls');
6
7const HeaderHostTransformer = require('./HeaderHostTransformer');
8
9// manages groups of tunnels
10module.exports = class TunnelCluster extends EventEmitter {
11 constructor(opts = {}) {
12 super(opts);
13 this.opts = opts;
14 }
15
16 open() {
17 const opt = this.opts;
18
19 // Prefer IP if returned by the server
20 const remoteHostOrIp = opt.remote_ip || opt.remote_host;
21 const remotePort = opt.remote_port;
22 const localHost = opt.local_host || 'localhost';
23 const localPort = opt.local_port;
24 const localProtocol = opt.local_https ? 'https' : 'http';
25 const allowInvalidCert = opt.allow_invalid_cert;
26
27 debug(
28 'establishing tunnel %s://%s:%s <> %s:%s',
29 localProtocol,
30 localHost,
31 localPort,
32 remoteHostOrIp,
33 remotePort
34 );
35
36 // connection to localtunnel server
37 const remote = net.connect({
38 host: remoteHostOrIp,
39 port: remotePort,
40 });
41
42 remote.setKeepAlive(true);
43
44 remote.on('error', err => {
45 debug('got remote connection error', err.message);
46
47 // emit connection refused errors immediately, because they
48 // indicate that the tunnel can't be established.
49 if (err.code === 'ECONNREFUSED') {
50 this.emit(
51 'error',
52 new Error(
53 `connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)`
54 )
55 );
56 }
57
58 remote.end();
59 });
60
61 const connLocal = () => {
62 if (remote.destroyed) {
63 debug('remote destroyed');
64 this.emit('dead');
65 return;
66 }
67
68 debug('connecting locally to %s://%s:%d', localProtocol, localHost, localPort);
69 remote.pause();
70
71 if (allowInvalidCert) {
72 debug('allowing invalid certificates');
73 }
74
75 const getLocalCertOpts = () =>
76 allowInvalidCert
77 ? { rejectUnauthorized: false }
78 : {
79 cert: fs.readFileSync(opt.local_cert),
80 key: fs.readFileSync(opt.local_key),
81 ca: opt.local_ca ? [fs.readFileSync(opt.local_ca)] : undefined,
82 };
83
84 // connection to local http server
85 const local = opt.local_https
86 ? tls.connect({ host: localHost, port: localPort, ...getLocalCertOpts() })
87 : net.connect({ host: localHost, port: localPort });
88
89 const remoteClose = () => {
90 debug('remote close');
91 this.emit('dead');
92 local.end();
93 };
94
95 remote.once('close', remoteClose);
96
97 // TODO some languages have single threaded servers which makes opening up
98 // multiple local connections impossible. We need a smarter way to scale
99 // and adjust for such instances to avoid beating on the door of the server
100 local.once('error', err => {
101 debug('local error %s', err.message);
102 local.end();
103
104 remote.removeListener('close', remoteClose);
105
106 if (err.code !== 'ECONNREFUSED') {
107 return remote.end();
108 }
109
110 // retrying connection to local server
111 setTimeout(connLocal, 1000);
112 });
113
114 local.once('connect', () => {
115 debug('connected locally');
116 remote.resume();
117
118 let stream = remote;
119
120 // if user requested specific local host
121 // then we use host header transform to replace the host header
122 if (opt.local_host) {
123 debug('transform Host header to %s', opt.local_host);
124 stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host }));
125 }
126
127 stream.pipe(local).pipe(remote);
128
129 // when local closes, also get a new remote
130 local.once('close', hadError => {
131 debug('local connection closed [%s]', hadError);
132 });
133 });
134 };
135
136 remote.on('data', data => {
137 const match = data.toString().match(/^(\w+) (\S+)/);
138 if (match) {
139 this.emit('request', {
140 method: match[1],
141 path: match[2],
142 });
143 }
144 });
145
146 // tunnel is considered open when remote connects
147 remote.once('connect', () => {
148 this.emit('open', remote);
149 connLocal();
150 });
151 }
152};