socketioMEANseed/server/node_modules/ws/lib/WebSocketServer.js
server/node_modules/ws/lib/WebSocketServer.jsRaw
1/*!
2 * ws: a node.js websocket client
3 * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
4 * MIT Licensed
5 */
6
7var util = require('util')
8 , events = require('events')
9 , http = require('http')
10 , crypto = require('crypto')
11 , Options = require('options')
12 , WebSocket = require('./WebSocket')
13 , Extensions = require('./Extensions')
14 , PerMessageDeflate = require('./PerMessageDeflate')
15 , tls = require('tls')
16 , url = require('url');
17
18/**
19 * WebSocket Server implementation
20 */
21
22function WebSocketServer(options, callback) {
23 if (this instanceof WebSocketServer === false) {
24 return new WebSocketServer(options, callback);
25 }
26
27 events.EventEmitter.call(this);
28
29 options = new Options({
30 host: '0.0.0.0',
31 port: null,
32 server: null,
33 verifyClient: null,
34 handleProtocols: null,
35 path: null,
36 noServer: false,
37 disableHixie: false,
38 clientTracking: true,
39 perMessageDeflate: true,
40 maxPayload: 100 * 1024 * 1024
41 }).merge(options);
42
43 if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
44 throw new TypeError('`port` or a `server` must be provided');
45 }
46
47 var self = this;
48
49 if (options.isDefinedAndNonNull('port')) {
50 this._server = http.createServer(function (req, res) {
51 var body = http.STATUS_CODES[426];
52 res.writeHead(426, {
53 'Content-Length': body.length,
54 'Content-Type': 'text/plain'
55 });
56 res.end(body);
57 });
58 this._server.allowHalfOpen = false;
59 this._server.listen(options.value.port, options.value.host, callback);
60 this._closeServer = function() { if (self._server) self._server.close(); };
61 }
62 else if (options.value.server) {
63 this._server = options.value.server;
64 if (options.value.path) {
65 // take note of the path, to avoid collisions when multiple websocket servers are
66 // listening on the same http server
67 if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) {
68 throw new Error('two instances of WebSocketServer cannot listen on the same http server path');
69 }
70 if (typeof this._server._webSocketPaths !== 'object') {
71 this._server._webSocketPaths = {};
72 }
73 this._server._webSocketPaths[options.value.path] = 1;
74 }
75 }
76 if (this._server) {
77 this._onceServerListening = function() { self.emit('listening'); };
78 this._server.once('listening', this._onceServerListening);
79 }
80
81 if (typeof this._server != 'undefined') {
82 this._onServerError = function(error) { self.emit('error', error) };
83 this._server.on('error', this._onServerError);
84 this._onServerUpgrade = function(req, socket, upgradeHead) {
85 //copy upgradeHead to avoid retention of large slab buffers used in node core
86 var head = new Buffer(upgradeHead.length);
87 upgradeHead.copy(head);
88
89 self.handleUpgrade(req, socket, head, function(client) {
90 self.emit('connection'+req.url, client);
91 self.emit('connection', client);
92 });
93 };
94 this._server.on('upgrade', this._onServerUpgrade);
95 }
96
97 this.options = options.value;
98 this.path = options.value.path;
99 this.clients = [];
100}
101
102/**
103 * Inherits from EventEmitter.
104 */
105
106util.inherits(WebSocketServer, events.EventEmitter);
107
108/**
109 * Immediately shuts down the connection.
110 *
111 * @api public
112 */
113
114WebSocketServer.prototype.close = function(callback) {
115 // terminate all associated clients
116 var error = null;
117 try {
118 for (var i = 0, l = this.clients.length; i < l; ++i) {
119 this.clients[i].terminate();
120 }
121 }
122 catch (e) {
123 error = e;
124 }
125
126 // remove path descriptor, if any
127 if (this.path && this._server._webSocketPaths) {
128 delete this._server._webSocketPaths[this.path];
129 if (Object.keys(this._server._webSocketPaths).length == 0) {
130 delete this._server._webSocketPaths;
131 }
132 }
133
134 // close the http server if it was internally created
135 try {
136 if (typeof this._closeServer !== 'undefined') {
137 this._closeServer();
138 }
139 }
140 finally {
141 if (this._server) {
142 this._server.removeListener('listening', this._onceServerListening);
143 this._server.removeListener('error', this._onServerError);
144 this._server.removeListener('upgrade', this._onServerUpgrade);
145 }
146 delete this._server;
147 }
148 if(callback)
149 callback(error);
150 else if(error)
151 throw error;
152}
153
154/**
155 * Handle a HTTP Upgrade request.
156 *
157 * @api public
158 */
159
160WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) {
161 // check for wrong path
162 if (this.options.path) {
163 var u = url.parse(req.url);
164 if (u && u.pathname !== this.options.path) return;
165 }
166
167 if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') {
168 abortConnection(socket, 400, 'Bad Request');
169 return;
170 }
171
172 if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments);
173 else handleHybiUpgrade.apply(this, arguments);
174}
175
176module.exports = WebSocketServer;
177
178/**
179 * Entirely private apis,
180 * which may or may not be bound to a sepcific WebSocket instance.
181 */
182
183function handleHybiUpgrade(req, socket, upgradeHead, cb) {
184 // handle premature socket errors
185 var errorHandler = function() {
186 try { socket.destroy(); } catch (e) {}
187 }
188 socket.on('error', errorHandler);
189
190 // verify key presence
191 if (!req.headers['sec-websocket-key']) {
192 abortConnection(socket, 400, 'Bad Request');
193 return;
194 }
195
196 // verify version
197 var version = parseInt(req.headers['sec-websocket-version']);
198 if ([8, 13].indexOf(version) === -1) {
199 abortConnection(socket, 400, 'Bad Request');
200 return;
201 }
202
203 // verify protocol
204 var protocols = req.headers['sec-websocket-protocol'];
205
206 // verify client
207 var origin = version < 13 ?
208 req.headers['sec-websocket-origin'] :
209 req.headers['origin'];
210
211 // handle extensions offer
212 var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
213
214 // handler to call when the connection sequence completes
215 var self = this;
216 var completeHybiUpgrade2 = function(protocol) {
217
218 // calc key
219 var key = req.headers['sec-websocket-key'];
220 var shasum = crypto.createHash('sha1');
221 shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
222 key = shasum.digest('base64');
223
224 var headers = [
225 'HTTP/1.1 101 Switching Protocols'
226 , 'Upgrade: websocket'
227 , 'Connection: Upgrade'
228 , 'Sec-WebSocket-Accept: ' + key
229 ];
230
231 if (typeof protocol != 'undefined') {
232 headers.push('Sec-WebSocket-Protocol: ' + protocol);
233 }
234
235 var extensions = {};
236 try {
237 extensions = acceptExtensions.call(self, extensionsOffer);
238 } catch (err) {
239 abortConnection(socket, 400, 'Bad Request');
240 return;
241 }
242
243 if (Object.keys(extensions).length) {
244 var serverExtensions = {};
245 Object.keys(extensions).forEach(function(token) {
246 serverExtensions[token] = [extensions[token].params]
247 });
248 headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions));
249 }
250
251 // allows external modification/inspection of handshake headers
252 self.emit('headers', headers);
253
254 socket.setTimeout(0);
255 socket.setNoDelay(true);
256 try {
257 socket.write(headers.concat('', '').join('\r\n'));
258 }
259 catch (e) {
260 // if the upgrade write fails, shut the connection down hard
261 try { socket.destroy(); } catch (e) {}
262 return;
263 }
264
265 var client = new WebSocket([req, socket, upgradeHead], {
266 protocolVersion: version,
267 protocol: protocol,
268 extensions: extensions,
269 maxPayload: self.options.maxPayload
270 });
271
272 if (self.options.clientTracking) {
273 self.clients.push(client);
274 client.on('close', function() {
275 var index = self.clients.indexOf(client);
276 if (index != -1) {
277 self.clients.splice(index, 1);
278 }
279 });
280 }
281
282 // signal upgrade complete
283 socket.removeListener('error', errorHandler);
284 cb(client);
285 }
286
287 // optionally call external protocol selection handler before
288 // calling completeHybiUpgrade2
289 var completeHybiUpgrade1 = function() {
290 // choose from the sub-protocols
291 if (typeof self.options.handleProtocols == 'function') {
292 var protList = (protocols || "").split(/, */);
293 var callbackCalled = false;
294 var res = self.options.handleProtocols(protList, function(result, protocol) {
295 callbackCalled = true;
296 if (!result) abortConnection(socket, 401, 'Unauthorized');
297 else completeHybiUpgrade2(protocol);
298 });
299 if (!callbackCalled) {
300 // the handleProtocols handler never called our callback
301 abortConnection(socket, 501, 'Could not process protocols');
302 }
303 return;
304 } else {
305 if (typeof protocols !== 'undefined') {
306 completeHybiUpgrade2(protocols.split(/, */)[0]);
307 }
308 else {
309 completeHybiUpgrade2();
310 }
311 }
312 }
313
314 // optionally call external client verification handler
315 if (typeof this.options.verifyClient == 'function') {
316 var info = {
317 origin: origin,
318 secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
319 req: req
320 };
321 if (this.options.verifyClient.length == 2) {
322 this.options.verifyClient(info, function(result, code, name) {
323 if (typeof code === 'undefined') code = 401;
324 if (typeof name === 'undefined') name = http.STATUS_CODES[code];
325
326 if (!result) abortConnection(socket, code, name);
327 else completeHybiUpgrade1();
328 });
329 return;
330 }
331 else if (!this.options.verifyClient(info)) {
332 abortConnection(socket, 401, 'Unauthorized');
333 return;
334 }
335 }
336
337 completeHybiUpgrade1();
338}
339
340function handleHixieUpgrade(req, socket, upgradeHead, cb) {
341 // handle premature socket errors
342 var errorHandler = function() {
343 try { socket.destroy(); } catch (e) {}
344 }
345 socket.on('error', errorHandler);
346
347 // bail if options prevent hixie
348 if (this.options.disableHixie) {
349 abortConnection(socket, 401, 'Hixie support disabled');
350 return;
351 }
352
353 // verify key presence
354 if (!req.headers['sec-websocket-key2']) {
355 abortConnection(socket, 400, 'Bad Request');
356 return;
357 }
358
359 var origin = req.headers['origin']
360 , self = this;
361
362 // setup handshake completion to run after client has been verified
363 var onClientVerified = function() {
364 var wshost;
365 if (!req.headers['x-forwarded-host'])
366 wshost = req.headers.host;
367 else
368 wshost = req.headers['x-forwarded-host'];
369 var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
370 , protocol = req.headers['sec-websocket-protocol'];
371
372 // build the response header and return a Buffer
373 var buildResponseHeader = function() {
374 var headers = [
375 'HTTP/1.1 101 Switching Protocols'
376 , 'Upgrade: WebSocket'
377 , 'Connection: Upgrade'
378 , 'Sec-WebSocket-Location: ' + location
379 ];
380 if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol);
381 if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin);
382
383 return new Buffer(headers.concat('', '').join('\r\n'));
384 };
385
386 // send handshake response before receiving the nonce
387 var handshakeResponse = function() {
388
389 socket.setTimeout(0);
390 socket.setNoDelay(true);
391
392 var headerBuffer = buildResponseHeader();
393
394 try {
395 socket.write(headerBuffer, 'binary', function(err) {
396 // remove listener if there was an error
397 if (err) socket.removeListener('data', handler);
398 return;
399 });
400 } catch (e) {
401 try { socket.destroy(); } catch (e) {}
402 return;
403 };
404 };
405
406 // handshake completion code to run once nonce has been successfully retrieved
407 var completeHandshake = function(nonce, rest, headerBuffer) {
408 // calculate key
409 var k1 = req.headers['sec-websocket-key1']
410 , k2 = req.headers['sec-websocket-key2']
411 , md5 = crypto.createHash('md5');
412
413 [k1, k2].forEach(function (k) {
414 var n = parseInt(k.replace(/[^\d]/g, ''))
415 , spaces = k.replace(/[^ ]/g, '').length;
416 if (spaces === 0 || n % spaces !== 0){
417 abortConnection(socket, 400, 'Bad Request');
418 return;
419 }
420 n /= spaces;
421 md5.update(String.fromCharCode(
422 n >> 24 & 0xFF,
423 n >> 16 & 0xFF,
424 n >> 8 & 0xFF,
425 n & 0xFF));
426 });
427 md5.update(nonce.toString('binary'));
428
429 socket.setTimeout(0);
430 socket.setNoDelay(true);
431
432 try {
433 var hashBuffer = new Buffer(md5.digest('binary'), 'binary');
434 var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length);
435 headerBuffer.copy(handshakeBuffer, 0);
436 hashBuffer.copy(handshakeBuffer, headerBuffer.length);
437
438 // do a single write, which - upon success - causes a new client websocket to be setup
439 socket.write(handshakeBuffer, 'binary', function(err) {
440 if (err) return; // do not create client if an error happens
441 var client = new WebSocket([req, socket, rest], {
442 protocolVersion: 'hixie-76',
443 protocol: protocol
444 });
445 if (self.options.clientTracking) {
446 self.clients.push(client);
447 client.on('close', function() {
448 var index = self.clients.indexOf(client);
449 if (index != -1) {
450 self.clients.splice(index, 1);
451 }
452 });
453 }
454
455 // signal upgrade complete
456 socket.removeListener('error', errorHandler);
457 cb(client);
458 });
459 }
460 catch (e) {
461 try { socket.destroy(); } catch (e) {}
462 return;
463 }
464 }
465
466 // retrieve nonce
467 var nonceLength = 8;
468 if (upgradeHead && upgradeHead.length >= nonceLength) {
469 var nonce = upgradeHead.slice(0, nonceLength);
470 var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null;
471 completeHandshake.call(self, nonce, rest, buildResponseHeader());
472 }
473 else {
474 // nonce not present in upgradeHead
475 var nonce = new Buffer(nonceLength);
476 upgradeHead.copy(nonce, 0);
477 var received = upgradeHead.length;
478 var rest = null;
479 var handler = function (data) {
480 var toRead = Math.min(data.length, nonceLength - received);
481 if (toRead === 0) return;
482 data.copy(nonce, received, 0, toRead);
483 received += toRead;
484 if (received == nonceLength) {
485 socket.removeListener('data', handler);
486 if (toRead < data.length) rest = data.slice(toRead);
487
488 // complete the handshake but send empty buffer for headers since they have already been sent
489 completeHandshake.call(self, nonce, rest, new Buffer(0));
490 }
491 }
492
493 // handle additional data as we receive it
494 socket.on('data', handler);
495
496 // send header response before we have the nonce to fix haproxy buffering
497 handshakeResponse();
498 }
499 }
500
501 // verify client
502 if (typeof this.options.verifyClient == 'function') {
503 var info = {
504 origin: origin,
505 secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
506 req: req
507 };
508 if (this.options.verifyClient.length == 2) {
509 var self = this;
510 this.options.verifyClient(info, function(result, code, name) {
511 if (typeof code === 'undefined') code = 401;
512 if (typeof name === 'undefined') name = http.STATUS_CODES[code];
513
514 if (!result) abortConnection(socket, code, name);
515 else onClientVerified.apply(self);
516 });
517 return;
518 }
519 else if (!this.options.verifyClient(info)) {
520 abortConnection(socket, 401, 'Unauthorized');
521 return;
522 }
523 }
524
525 // no client verification required
526 onClientVerified();
527}
528
529function acceptExtensions(offer) {
530 var extensions = {};
531 var options = this.options.perMessageDeflate;
532 var maxPayload = this.options.maxPayload;
533 if (options && offer[PerMessageDeflate.extensionName]) {
534 var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true, maxPayload);
535 perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
536 extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
537 }
538 return extensions;
539}
540
541function abortConnection(socket, code, name) {
542 try {
543 var response = [
544 'HTTP/1.1 ' + code + ' ' + name,
545 'Content-type: text/html'
546 ];
547 socket.write(response.concat('', '').join('\r\n'));
548 }
549 catch (e) { /* ignore errors - we've aborted this connection */ }
550 finally {
551 // ensure that an early aborted connection is shut down completely
552 try { socket.destroy(); } catch (e) {}
553 }
554}