httsper 0.0.1-security → 7.5.9
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of httsper might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +493 -3
- package/browser.js +8 -0
- package/index.js +11 -0
- package/lib/buffer-util.js +129 -0
- package/lib/constants.js +10 -0
- package/lib/event-target.js +184 -0
- package/lib/extension.js +223 -0
- package/lib/https.js +1 -0
- package/lib/limiter.js +55 -0
- package/lib/permessage-deflate.js +518 -0
- package/lib/receiver.js +607 -0
- package/lib/sender.js +409 -0
- package/lib/stream.js +180 -0
- package/lib/validation.js +104 -0
- package/lib/websocket-server.js +447 -0
- package/lib/websocket.js +1195 -0
- package/package.json +60 -3
@@ -0,0 +1,447 @@
|
|
1
|
+
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
|
2
|
+
|
3
|
+
'use strict';
|
4
|
+
|
5
|
+
const EventEmitter = require('events');
|
6
|
+
const http = require('http');
|
7
|
+
const https = require('https');
|
8
|
+
const net = require('net');
|
9
|
+
const tls = require('tls');
|
10
|
+
const { createHash } = require('crypto');
|
11
|
+
|
12
|
+
const PerMessageDeflate = require('./permessage-deflate');
|
13
|
+
const WebSocket = require('./websocket');
|
14
|
+
const { format, parse } = require('./extension');
|
15
|
+
const { GUID, kWebSocket } = require('./constants');
|
16
|
+
|
17
|
+
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
|
18
|
+
|
19
|
+
const RUNNING = 0;
|
20
|
+
const CLOSING = 1;
|
21
|
+
const CLOSED = 2;
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Class representing a WebSocket server.
|
25
|
+
*
|
26
|
+
* @extends EventEmitter
|
27
|
+
*/
|
28
|
+
class WebSocketServer extends EventEmitter {
|
29
|
+
/**
|
30
|
+
* Create a `WebSocketServer` instance.
|
31
|
+
*
|
32
|
+
* @param {Object} options Configuration options
|
33
|
+
* @param {Number} [options.backlog=511] The maximum length of the queue of
|
34
|
+
* pending connections
|
35
|
+
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
|
36
|
+
* track clients
|
37
|
+
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
38
|
+
* @param {String} [options.host] The hostname where to bind the server
|
39
|
+
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
40
|
+
* size
|
41
|
+
* @param {Boolean} [options.noServer=false] Enable no server mode
|
42
|
+
* @param {String} [options.path] Accept only connections matching this path
|
43
|
+
* @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
|
44
|
+
* permessage-deflate
|
45
|
+
* @param {Number} [options.port] The port where to bind the server
|
46
|
+
* @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
|
47
|
+
* server to use
|
48
|
+
* @param {Function} [options.verifyClient] A hook to reject connections
|
49
|
+
* @param {Function} [callback] A listener for the `listening` event
|
50
|
+
*/
|
51
|
+
constructor(options, callback) {
|
52
|
+
super();
|
53
|
+
|
54
|
+
options = {
|
55
|
+
maxPayload: 100 * 1024 * 1024,
|
56
|
+
perMessageDeflate: false,
|
57
|
+
handleProtocols: null,
|
58
|
+
clientTracking: true,
|
59
|
+
verifyClient: null,
|
60
|
+
noServer: false,
|
61
|
+
backlog: null, // use default (511 as implemented in net.js)
|
62
|
+
server: null,
|
63
|
+
host: null,
|
64
|
+
path: null,
|
65
|
+
port: null,
|
66
|
+
...options
|
67
|
+
};
|
68
|
+
|
69
|
+
if (
|
70
|
+
(options.port == null && !options.server && !options.noServer) ||
|
71
|
+
(options.port != null && (options.server || options.noServer)) ||
|
72
|
+
(options.server && options.noServer)
|
73
|
+
) {
|
74
|
+
throw new TypeError(
|
75
|
+
'One and only one of the "port", "server", or "noServer" options ' +
|
76
|
+
'must be specified'
|
77
|
+
);
|
78
|
+
}
|
79
|
+
|
80
|
+
if (options.port != null) {
|
81
|
+
this._server = http.createServer((req, res) => {
|
82
|
+
const body = http.STATUS_CODES[426];
|
83
|
+
|
84
|
+
res.writeHead(426, {
|
85
|
+
'Content-Length': body.length,
|
86
|
+
'Content-Type': 'text/plain'
|
87
|
+
});
|
88
|
+
res.end(body);
|
89
|
+
});
|
90
|
+
this._server.listen(
|
91
|
+
options.port,
|
92
|
+
options.host,
|
93
|
+
options.backlog,
|
94
|
+
callback
|
95
|
+
);
|
96
|
+
} else if (options.server) {
|
97
|
+
this._server = options.server;
|
98
|
+
}
|
99
|
+
|
100
|
+
if (this._server) {
|
101
|
+
const emitConnection = this.emit.bind(this, 'connection');
|
102
|
+
|
103
|
+
this._removeListeners = addListeners(this._server, {
|
104
|
+
listening: this.emit.bind(this, 'listening'),
|
105
|
+
error: this.emit.bind(this, 'error'),
|
106
|
+
upgrade: (req, socket, head) => {
|
107
|
+
this.handleUpgrade(req, socket, head, emitConnection);
|
108
|
+
}
|
109
|
+
});
|
110
|
+
}
|
111
|
+
|
112
|
+
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
|
113
|
+
if (options.clientTracking) this.clients = new Set();
|
114
|
+
this.options = options;
|
115
|
+
this._state = RUNNING;
|
116
|
+
}
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Returns the bound address, the address family name, and port of the server
|
120
|
+
* as reported by the operating system if listening on an IP socket.
|
121
|
+
* If the server is listening on a pipe or UNIX domain socket, the name is
|
122
|
+
* returned as a string.
|
123
|
+
*
|
124
|
+
* @return {(Object|String|null)} The address of the server
|
125
|
+
* @public
|
126
|
+
*/
|
127
|
+
address() {
|
128
|
+
if (this.options.noServer) {
|
129
|
+
throw new Error('The server is operating in "noServer" mode');
|
130
|
+
}
|
131
|
+
|
132
|
+
if (!this._server) return null;
|
133
|
+
return this._server.address();
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Close the server.
|
138
|
+
*
|
139
|
+
* @param {Function} [cb] Callback
|
140
|
+
* @public
|
141
|
+
*/
|
142
|
+
close(cb) {
|
143
|
+
if (cb) this.once('close', cb);
|
144
|
+
|
145
|
+
if (this._state === CLOSED) {
|
146
|
+
process.nextTick(emitClose, this);
|
147
|
+
return;
|
148
|
+
}
|
149
|
+
|
150
|
+
if (this._state === CLOSING) return;
|
151
|
+
this._state = CLOSING;
|
152
|
+
|
153
|
+
//
|
154
|
+
// Terminate all associated clients.
|
155
|
+
//
|
156
|
+
if (this.clients) {
|
157
|
+
for (const client of this.clients) client.terminate();
|
158
|
+
}
|
159
|
+
|
160
|
+
const server = this._server;
|
161
|
+
|
162
|
+
if (server) {
|
163
|
+
this._removeListeners();
|
164
|
+
this._removeListeners = this._server = null;
|
165
|
+
|
166
|
+
//
|
167
|
+
// Close the http server if it was internally created.
|
168
|
+
//
|
169
|
+
if (this.options.port != null) {
|
170
|
+
server.close(emitClose.bind(undefined, this));
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
process.nextTick(emitClose, this);
|
176
|
+
}
|
177
|
+
|
178
|
+
/**
|
179
|
+
* See if a given request should be handled by this server instance.
|
180
|
+
*
|
181
|
+
* @param {http.IncomingMessage} req Request object to inspect
|
182
|
+
* @return {Boolean} `true` if the request is valid, else `false`
|
183
|
+
* @public
|
184
|
+
*/
|
185
|
+
shouldHandle(req) {
|
186
|
+
if (this.options.path) {
|
187
|
+
const index = req.url.indexOf('?');
|
188
|
+
const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
|
189
|
+
|
190
|
+
if (pathname !== this.options.path) return false;
|
191
|
+
}
|
192
|
+
|
193
|
+
return true;
|
194
|
+
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Handle a HTTP Upgrade request.
|
198
|
+
*
|
199
|
+
* @param {http.IncomingMessage} req The request object
|
200
|
+
* @param {(net.Socket|tls.Socket)} socket The network socket between the
|
201
|
+
* server and client
|
202
|
+
* @param {Buffer} head The first packet of the upgraded stream
|
203
|
+
* @param {Function} cb Callback
|
204
|
+
* @public
|
205
|
+
*/
|
206
|
+
handleUpgrade(req, socket, head, cb) {
|
207
|
+
socket.on('error', socketOnError);
|
208
|
+
|
209
|
+
const key =
|
210
|
+
req.headers['sec-websocket-key'] !== undefined
|
211
|
+
? req.headers['sec-websocket-key'].trim()
|
212
|
+
: false;
|
213
|
+
const version = +req.headers['sec-websocket-version'];
|
214
|
+
const extensions = {};
|
215
|
+
|
216
|
+
if (
|
217
|
+
req.method !== 'GET' ||
|
218
|
+
req.headers.upgrade.toLowerCase() !== 'websocket' ||
|
219
|
+
!key ||
|
220
|
+
!keyRegex.test(key) ||
|
221
|
+
(version !== 8 && version !== 13) ||
|
222
|
+
!this.shouldHandle(req)
|
223
|
+
) {
|
224
|
+
return abortHandshake(socket, 400);
|
225
|
+
}
|
226
|
+
|
227
|
+
if (this.options.perMessageDeflate) {
|
228
|
+
const perMessageDeflate = new PerMessageDeflate(
|
229
|
+
this.options.perMessageDeflate,
|
230
|
+
true,
|
231
|
+
this.options.maxPayload
|
232
|
+
);
|
233
|
+
|
234
|
+
try {
|
235
|
+
const offers = parse(req.headers['sec-websocket-extensions']);
|
236
|
+
|
237
|
+
if (offers[PerMessageDeflate.extensionName]) {
|
238
|
+
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
|
239
|
+
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
240
|
+
}
|
241
|
+
} catch (err) {
|
242
|
+
return abortHandshake(socket, 400);
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
//
|
247
|
+
// Optionally call external client verification handler.
|
248
|
+
//
|
249
|
+
if (this.options.verifyClient) {
|
250
|
+
const info = {
|
251
|
+
origin:
|
252
|
+
req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
|
253
|
+
secure: !!(req.socket.authorized || req.socket.encrypted),
|
254
|
+
req
|
255
|
+
};
|
256
|
+
|
257
|
+
if (this.options.verifyClient.length === 2) {
|
258
|
+
this.options.verifyClient(info, (verified, code, message, headers) => {
|
259
|
+
if (!verified) {
|
260
|
+
return abortHandshake(socket, code || 401, message, headers);
|
261
|
+
}
|
262
|
+
|
263
|
+
this.completeUpgrade(key, extensions, req, socket, head, cb);
|
264
|
+
});
|
265
|
+
return;
|
266
|
+
}
|
267
|
+
|
268
|
+
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
|
269
|
+
}
|
270
|
+
|
271
|
+
this.completeUpgrade(key, extensions, req, socket, head, cb);
|
272
|
+
}
|
273
|
+
|
274
|
+
/**
|
275
|
+
* Upgrade the connection to WebSocket.
|
276
|
+
*
|
277
|
+
* @param {String} key The value of the `Sec-WebSocket-Key` header
|
278
|
+
* @param {Object} extensions The accepted extensions
|
279
|
+
* @param {http.IncomingMessage} req The request object
|
280
|
+
* @param {(net.Socket|tls.Socket)} socket The network socket between the
|
281
|
+
* server and client
|
282
|
+
* @param {Buffer} head The first packet of the upgraded stream
|
283
|
+
* @param {Function} cb Callback
|
284
|
+
* @throws {Error} If called more than once with the same socket
|
285
|
+
* @private
|
286
|
+
*/
|
287
|
+
completeUpgrade(key, extensions, req, socket, head, cb) {
|
288
|
+
//
|
289
|
+
// Destroy the socket if the client has already sent a FIN packet.
|
290
|
+
//
|
291
|
+
if (!socket.readable || !socket.writable) return socket.destroy();
|
292
|
+
|
293
|
+
if (socket[kWebSocket]) {
|
294
|
+
throw new Error(
|
295
|
+
'server.handleUpgrade() was called more than once with the same ' +
|
296
|
+
'socket, possibly due to a misconfiguration'
|
297
|
+
);
|
298
|
+
}
|
299
|
+
|
300
|
+
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
301
|
+
|
302
|
+
const digest = createHash('sha1')
|
303
|
+
.update(key + GUID)
|
304
|
+
.digest('base64');
|
305
|
+
|
306
|
+
const headers = [
|
307
|
+
'HTTP/1.1 101 Switching Protocols',
|
308
|
+
'Upgrade: websocket',
|
309
|
+
'Connection: Upgrade',
|
310
|
+
`Sec-WebSocket-Accept: ${digest}`
|
311
|
+
];
|
312
|
+
|
313
|
+
const ws = new WebSocket(null);
|
314
|
+
let protocol = req.headers['sec-websocket-protocol'];
|
315
|
+
|
316
|
+
if (protocol) {
|
317
|
+
protocol = protocol.split(',').map(trim);
|
318
|
+
|
319
|
+
//
|
320
|
+
// Optionally call external protocol selection handler.
|
321
|
+
//
|
322
|
+
if (this.options.handleProtocols) {
|
323
|
+
protocol = this.options.handleProtocols(protocol, req);
|
324
|
+
} else {
|
325
|
+
protocol = protocol[0];
|
326
|
+
}
|
327
|
+
|
328
|
+
if (protocol) {
|
329
|
+
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
|
330
|
+
ws._protocol = protocol;
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
if (extensions[PerMessageDeflate.extensionName]) {
|
335
|
+
const params = extensions[PerMessageDeflate.extensionName].params;
|
336
|
+
const value = format({
|
337
|
+
[PerMessageDeflate.extensionName]: [params]
|
338
|
+
});
|
339
|
+
headers.push(`Sec-WebSocket-Extensions: ${value}`);
|
340
|
+
ws._extensions = extensions;
|
341
|
+
}
|
342
|
+
|
343
|
+
//
|
344
|
+
// Allow external modification/inspection of handshake headers.
|
345
|
+
//
|
346
|
+
this.emit('headers', headers, req);
|
347
|
+
|
348
|
+
socket.write(headers.concat('\r\n').join('\r\n'));
|
349
|
+
socket.removeListener('error', socketOnError);
|
350
|
+
|
351
|
+
ws.setSocket(socket, head, this.options.maxPayload);
|
352
|
+
|
353
|
+
if (this.clients) {
|
354
|
+
this.clients.add(ws);
|
355
|
+
ws.on('close', () => this.clients.delete(ws));
|
356
|
+
}
|
357
|
+
|
358
|
+
cb(ws, req);
|
359
|
+
}
|
360
|
+
}
|
361
|
+
|
362
|
+
module.exports = WebSocketServer;
|
363
|
+
|
364
|
+
/**
|
365
|
+
* Add event listeners on an `EventEmitter` using a map of <event, listener>
|
366
|
+
* pairs.
|
367
|
+
*
|
368
|
+
* @param {EventEmitter} server The event emitter
|
369
|
+
* @param {Object.<String, Function>} map The listeners to add
|
370
|
+
* @return {Function} A function that will remove the added listeners when
|
371
|
+
* called
|
372
|
+
* @private
|
373
|
+
*/
|
374
|
+
function addListeners(server, map) {
|
375
|
+
for (const event of Object.keys(map)) server.on(event, map[event]);
|
376
|
+
|
377
|
+
return function removeListeners() {
|
378
|
+
for (const event of Object.keys(map)) {
|
379
|
+
server.removeListener(event, map[event]);
|
380
|
+
}
|
381
|
+
};
|
382
|
+
}
|
383
|
+
|
384
|
+
/**
|
385
|
+
* Emit a `'close'` event on an `EventEmitter`.
|
386
|
+
*
|
387
|
+
* @param {EventEmitter} server The event emitter
|
388
|
+
* @private
|
389
|
+
*/
|
390
|
+
function emitClose(server) {
|
391
|
+
server._state = CLOSED;
|
392
|
+
server.emit('close');
|
393
|
+
}
|
394
|
+
|
395
|
+
/**
|
396
|
+
* Handle premature socket errors.
|
397
|
+
*
|
398
|
+
* @private
|
399
|
+
*/
|
400
|
+
function socketOnError() {
|
401
|
+
this.destroy();
|
402
|
+
}
|
403
|
+
|
404
|
+
/**
|
405
|
+
* Close the connection when preconditions are not fulfilled.
|
406
|
+
*
|
407
|
+
* @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
|
408
|
+
* @param {Number} code The HTTP response status code
|
409
|
+
* @param {String} [message] The HTTP response body
|
410
|
+
* @param {Object} [headers] Additional HTTP response headers
|
411
|
+
* @private
|
412
|
+
*/
|
413
|
+
function abortHandshake(socket, code, message, headers) {
|
414
|
+
if (socket.writable) {
|
415
|
+
message = message || http.STATUS_CODES[code];
|
416
|
+
headers = {
|
417
|
+
Connection: 'close',
|
418
|
+
'Content-Type': 'text/html',
|
419
|
+
'Content-Length': Buffer.byteLength(message),
|
420
|
+
...headers
|
421
|
+
};
|
422
|
+
|
423
|
+
socket.write(
|
424
|
+
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
|
425
|
+
Object.keys(headers)
|
426
|
+
.map((h) => `${h}: ${headers[h]}`)
|
427
|
+
.join('\r\n') +
|
428
|
+
'\r\n\r\n' +
|
429
|
+
message
|
430
|
+
);
|
431
|
+
}
|
432
|
+
|
433
|
+
socket.removeListener('error', socketOnError);
|
434
|
+
socket.destroy();
|
435
|
+
}
|
436
|
+
|
437
|
+
/**
|
438
|
+
* Remove whitespace characters from both ends of a string.
|
439
|
+
*
|
440
|
+
* @param {String} str The string
|
441
|
+
* @return {String} A new string representing `str` stripped of whitespace
|
442
|
+
* characters from both its beginning and end
|
443
|
+
* @private
|
444
|
+
*/
|
445
|
+
function trim(str) {
|
446
|
+
return str.trim();
|
447
|
+
}
|