forked from dwmkerr/wait-port
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwait-port.js
250 lines (215 loc) · 8.33 KB
/
wait-port.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
const debug = require('debug')('wait-port');
const net = require('net');
const outputFunctions = require('./output-functions');
const validateParameters = require('./validate-parameters');
const ConnectionError = require('./errors/connection-error');
let IPv6enabled = true;
function createConnectionWithTimeout({ host, port, ipVersion }, timeout, callback) {
// Variable to hold the timer we'll use to kill the socket if we don't
// connect in time.
let timer = null;
// Try and open the socket, with the params and callback.
const socket = net.createConnection({ host, port, family: ipVersion, autoSelectFamily: true }, (err) => {
if (!err) clearTimeout(timer);
return callback(err);
});
// TODO: Check for the socket ECONNREFUSED event.
socket.on('error', (error) => {
debug(`Socket error: ${error}`);
clearTimeout(timer);
socket.destroy();
callback(error);
});
// Kill the socket if we don't open in time.
timer = setTimeout(() => {
socket.destroy();
const error = new Error(`Timeout trying to open socket to ${host}:${port}, IPv${ipVersion}`);
error.code = 'ECONNTIMEOUT';
callback(error);
}, timeout);
// Return the socket.
return socket;
}
function checkHttp(socket, params, timeout, callback) {
// Create the HTTP request.
const request = `GET ${params.path} HTTP/1.1\r\nHost: ${params.host}\r\n\r\n`;
let timer = null;
timer = setTimeout(() => {
socket.destroy();
const error = new Error(`Timeout waiting for data from ${params.host}:${params.port}, IPv${params.ipVersion}`);
error.code = 'EREQTIMEOUT';
callback(error);
}, timeout);
// Get ready for a response.
socket.on('data', function(data) {
// Get the response as text.
const response = data.toString();
const statusLine = response.split('\n')[0];
// Stop the timer.
clearTimeout(timer);
// Check the data. Remember an HTTP response is:
// HTTP/1.1 XXX Stuff
const statusLineParts = statusLine.split(' ');
if (statusLineParts < 2 || statusLineParts[1].startsWith('2') === false) {
debug(`Invalid HTTP status line: ${statusLine}`);
const error = new Error('Invalid response from server');
error.code = 'ERESPONSE';
callback(error);
}
// ALL good!
debug(`Successful HTTP status line: ${statusLine}`);
callback();
});
// Send the request.
socket.write(request);
}
// This function attempts to open a connection, given a limited time window.
// This is the function which we will run repeatedly until we connect.
function tryConnect(options, timeout) {
return new Promise((resolve, reject) => {
try {
const socket = createConnectionWithTimeout(options, timeout, (err) => {
if (err) {
if (err.code === 'ECONNREFUSED' || err.code === 'EACCES') {
// We successfully *tried* to connect, so resolve with false so
// that we try again.
debug(`Socket not open: ${err.code}`);
socket.destroy();
return resolve(false);
} else if (err.code === 'ECONNTIMEOUT') {
// We've successfully *tried* to connect, but we're timing out
// establishing the connection. This is not ideal (either
// the port is open or it ain't).
debug('Socket not open: ECONNTIMEOUT');
socket.destroy();
return resolve(false);
} else if (err.code === 'ECONNRESET') {
// This can happen if the target server kills its connection before
// we can read from it, we can normally just try again.
debug('Socket not open: ECONNRESET');
socket.destroy();
return resolve(false);
} else if (options.ipVersion === 6 && (err.code === 'EADDRNOTAVAIL' || err.code === 'ENOTFOUND')) {
// This will occur if the IP address we are trying to connect to does not exist
// This can happen for ::1 or other IPv6 addresses if the IPv6 stack is not enabled.
// In this case we disable the IPv6 lookup
debug(`Socket cannot be opened for IPv6: ${err.code}`);
debug('Disabling IPv6 lookup');
IPv6enabled = false;
socket.destroy();
return resolve(false);
} else if (err.code === 'ENOTFOUND') {
// This will occur if the address is not found, i.e. due to a dns
// lookup fail (normally a problem if the domain is wrong).
debug('Socket cannot be opened: ENOTFOUND');
socket.destroy();
// If we are going to wait for DNS records, we can actually just try
// again...
if (options.waitForDns === true) return resolve(false);
// ...otherwise, we will explicitly fail with a meaningful error for
// the user.
return reject(new ConnectionError(`The address '${options.host}' cannot be found`));
}
// Trying to open the socket has resulted in an error we don't
// understand. Better give up.
debug(`Unexpected error trying to open socket: ${err}`);
socket.destroy();
// If we are currently checking for IPv6 we ignore this error and disable IPv6
if (options.ipVersion === 6) {
IPv6enabled = false;
return resolve(false);
}
return reject(err);
}
// Boom, we connected!
debug('Socket connected!');
// If we are not dealing with http, we're done.
if (options.protocol !== 'http') {
// Disconnect, stop the timer and resolve.
socket.destroy();
return resolve(true);
}
// TODO: we should only use the portion of the timeout for this interval which is still left to us.
// Now we've got to wait for a HTTP response.
checkHttp(socket, options, timeout, (err) => {
if (err) {
if (err.code === 'EREQTIMEOUT') {
debug('HTTP error: EREQTIMEOUT');
socket.destroy();
return resolve(false);
} else if (err.code === 'ERESPONSE') {
debug('HTTP error: ERESPONSE');
socket.destroy();
return resolve(false);
}
debug(`Unexpected error checking http response: ${err}`);
socket.destroy();
return reject(err);
}
socket.destroy();
return resolve(true);
});
});
} catch (err) {
// Trying to open the socket has resulted in an exception we don't
// understand. Better give up.
debug(`Unexpected exception trying to open socket: ${err}`);
return reject(err);
}
});
}
function waitPort(params) {
return new Promise((resolve, reject) => {
const {
protocol,
host,
port,
path,
interval,
timeout,
output,
waitForDns,
} = validateParameters(params);
// Keep track of the start time (needed for timeout calcs).
const startTime = new Date();
// Don't wait for more than connectTimeout to try and connect.
const connectTimeout = 1000;
// Grab the object for output.
const outputFunction = outputFunctions[output];
outputFunction.starting({ host, port });
// Start trying to connect.
const loop = (ipVersion = 4) => {
outputFunction.tryConnect();
tryConnect({ protocol, host, port, path, waitForDns, ipVersion }, connectTimeout)
.then((open) => {
debug(`Socket status is: ${open}`);
// The socket is open, we're done.
if (open) {
outputFunction.connected();
return resolve({ open: true, ipVersion });
}
// If we have a timeout, and we've passed it, we're done.
if (timeout && (new Date() - startTime) > timeout) {
outputFunction.timeout();
return resolve({ open: false });
}
// Check for IPv6 next
if (IPv6enabled && ipVersion === 4 && !net.isIP(host)) {
return loop(6);
}
// Run the loop again.
return setTimeout(loop, interval);
})
.catch((err) => {
debug(`Unhandled error occured trying to connect: ${err}`);
return reject(err);
});
};
// Start the loop.
loop();
});
}
module.exports = waitPort;
waitPort({
port: 3000
})