UNPKG

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