forked from localtunnel/server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathClient.js
132 lines (109 loc) · 4.27 KB
/
Client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import http from 'http';
import Debug from 'debug';
import pump from 'pump';
import EventEmitter from 'events';
// A client encapsulates req/res handling using an agent
//
// If an agent is destroyed, the request handling will error
// The caller is responsible for handling a failed request
class Client extends EventEmitter {
constructor(options) {
super();
const agent = this.agent = options.agent;
const id = this.id = options.id;
this.debug = Debug(`lt:Client[${this.id}]`);
// client is given a grace period in which they can connect before they are _removed_
this.graceTimeout = setTimeout(() => {
this.close();
}, 1000).unref();
agent.on('online', () => {
this.debug('client online %s', id);
clearTimeout(this.graceTimeout);
});
agent.on('offline', () => {
this.debug('client offline %s', id);
// if there was a previous timeout set, we don't want to double trigger
clearTimeout(this.graceTimeout);
// client is given a grace period in which they can re-connect before they are _removed_
this.graceTimeout = setTimeout(() => {
this.close();
}, 1000).unref();
});
// TODO(roman): an agent error removes the client, the user needs to re-connect?
// how does a user realize they need to re-connect vs some random client being assigned same port?
agent.once('error', (err) => {
this.close();
});
}
stats() {
return this.agent.stats();
}
close() {
clearTimeout(this.graceTimeout);
this.agent.destroy();
this.emit('close');
}
handleRequest(req, res) {
this.debug('> %s', req.url);
const opt = {
path: req.url,
agent: this.agent,
method: req.method,
headers: req.headers
};
const clientReq = http.request(opt, (clientRes) => {
this.debug('< %s', req.url);
// write response code and headers
res.writeHead(clientRes.statusCode, clientRes.headers);
// using pump is deliberate - see the pump docs for why
pump(clientRes, res);
});
// this can happen when underlying agent produces an error
// in our case we 504 gateway error this?
// if we have already sent headers?
clientReq.once('error', (err) => {
// TODO(roman): if headers not sent - respond with gateway unavailable
});
// using pump is deliberate - see the pump docs for why
pump(req, clientReq);
}
handleUpgrade(req, socket) {
this.debug('> [up] %s', req.url);
socket.once('error', (err) => {
// These client side errors can happen if the client dies while we are reading
// We don't need to surface these in our logs.
if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') {
return;
}
console.error(err);
});
this.agent.createConnection({}, (err, conn) => {
this.debug('< [up] %s', req.url);
// any errors getting a connection mean we cannot service this request
if (err) {
socket.end();
return;
}
// socket met have disconnected while we waiting for a socket
if (!socket.readable || !socket.writable) {
conn.destroy();
socket.end();
return;
}
// websocket requests are special in that we simply re-create the header info
// then directly pipe the socket data
// avoids having to rebuild the request and handle upgrades via the http client
const arr = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
for (let i=0 ; i < (req.rawHeaders.length-1) ; i+=2) {
arr.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}`);
}
arr.push('');
arr.push('');
// using pump is deliberate - see the pump docs for why
pump(conn, socket);
pump(socket, conn);
conn.write(arr.join('\r\n'));
});
}
}
export default Client;