1 | const { EventEmitter } = require('events');
|
2 | const debug = require('debug')('localtunnel:client');
|
3 | const fs = require('fs');
|
4 | const net = require('net');
|
5 | const tls = require('tls');
|
6 |
|
7 | const HeaderHostTransformer = require('./HeaderHostTransformer');
|
8 |
|
9 |
|
10 | module.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 |
|
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 |
|
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 |
|
48 |
|
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 |
|
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 |
|
98 |
|
99 |
|
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 |
|
111 | setTimeout(connLocal, 1000);
|
112 | });
|
113 |
|
114 | local.once('connect', () => {
|
115 | debug('connected locally');
|
116 | remote.resume();
|
117 |
|
118 | let stream = remote;
|
119 |
|
120 |
|
121 |
|
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 |
|
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 |
|
147 | remote.once('connect', () => {
|
148 | this.emit('open', remote);
|
149 | connLocal();
|
150 | });
|
151 | }
|
152 | };
|