2 * ws: a node.js websocket client
3 * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
7 var util = require('util')
8 , events = require('events')
9 , http = require('http')
10 , crypto = require('crypto')
11 , Options = require('options')
12 , WebSocket = require('./WebSocket')
13 , tls = require('tls')
14 , url = require('url');
17 * WebSocket Server implementation
20 function WebSocketServer(options, callback) {
21 options = new Options({
26 handleProtocols: null,
33 if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
34 throw new TypeError('`port` or a `server` must be provided');
39 if (options.isDefinedAndNonNull('port')) {
40 this._server = http.createServer(function (req, res) {
41 res.writeHead(200, {'Content-Type': 'text/plain'});
42 res.end('Not implemented');
44 this._server.listen(options.value.port, options.value.host, callback);
45 this._closeServer = function() { if (self._server) self._server.close(); };
47 else if (options.value.server) {
48 this._server = options.value.server;
49 if (options.value.path) {
50 // take note of the path, to avoid collisions when multiple websocket servers are
51 // listening on the same http server
52 if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) {
53 throw new Error('two instances of WebSocketServer cannot listen on the same http server path');
55 if (typeof this._server._webSocketPaths !== 'object') {
56 this._server._webSocketPaths = {};
58 this._server._webSocketPaths[options.value.path] = 1;
61 if (this._server) this._server.once('listening', function() { self.emit('listening'); });
63 if (typeof this._server != 'undefined') {
64 this._server.on('error', function(error) {
65 self.emit('error', error)
67 this._server.on('upgrade', function(req, socket, upgradeHead) {
68 //copy upgradeHead to avoid retention of large slab buffers used in node core
69 var head = new Buffer(upgradeHead.length);
70 upgradeHead.copy(head);
72 self.handleUpgrade(req, socket, head, function(client) {
73 self.emit('connection'+req.url, client);
74 self.emit('connection', client);
79 this.options = options.value;
80 this.path = options.value.path;
85 * Inherits from EventEmitter.
88 util.inherits(WebSocketServer, events.EventEmitter);
91 * Immediately shuts down the connection.
96 WebSocketServer.prototype.close = function() {
97 // terminate all associated clients
100 for (var i = 0, l = this.clients.length; i < l; ++i) {
101 this.clients[i].terminate();
108 // remove path descriptor, if any
109 if (this.path && this._server._webSocketPaths) {
110 delete this._server._webSocketPaths[this.path];
111 if (Object.keys(this._server._webSocketPaths).length == 0) {
112 delete this._server._webSocketPaths;
116 // close the http server if it was internally created
118 if (typeof this._closeServer !== 'undefined') {
125 if (error) throw error;
129 * Handle a HTTP Upgrade request.
134 WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) {
135 // check for wrong path
136 if (this.options.path) {
137 var u = url.parse(req.url);
138 if (u && u.pathname !== this.options.path) return;
141 if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') {
142 abortConnection(socket, 400, 'Bad Request');
146 if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments);
147 else handleHybiUpgrade.apply(this, arguments);
150 module.exports = WebSocketServer;
153 * Entirely private apis,
154 * which may or may not be bound to a sepcific WebSocket instance.
157 function handleHybiUpgrade(req, socket, upgradeHead, cb) {
158 // handle premature socket errors
159 var errorHandler = function() {
160 try { socket.destroy(); } catch (e) {}
162 socket.on('error', errorHandler);
164 // verify key presence
165 if (!req.headers['sec-websocket-key']) {
166 abortConnection(socket, 400, 'Bad Request');
171 var version = parseInt(req.headers['sec-websocket-version']);
172 if ([8, 13].indexOf(version) === -1) {
173 abortConnection(socket, 400, 'Bad Request');
178 var protocols = req.headers['sec-websocket-protocol'];
181 var origin = version < 13 ?
182 req.headers['sec-websocket-origin'] :
183 req.headers['origin'];
185 // handler to call when the connection sequence completes
187 var completeHybiUpgrade2 = function(protocol) {
190 var key = req.headers['sec-websocket-key'];
191 var shasum = crypto.createHash('sha1');
192 shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
193 key = shasum.digest('base64');
196 'HTTP/1.1 101 Switching Protocols'
197 , 'Upgrade: websocket'
198 , 'Connection: Upgrade'
199 , 'Sec-WebSocket-Accept: ' + key
202 if (typeof protocol != 'undefined') {
203 headers.push('Sec-WebSocket-Protocol: ' + protocol);
206 // allows external modification/inspection of handshake headers
207 self.emit('headers', headers);
209 socket.setTimeout(0);
210 socket.setNoDelay(true);
212 socket.write(headers.concat('', '').join('\r\n'));
215 // if the upgrade write fails, shut the connection down hard
216 try { socket.destroy(); } catch (e) {}
220 var client = new WebSocket([req, socket, upgradeHead], {
221 protocolVersion: version,
225 if (self.options.clientTracking) {
226 self.clients.push(client);
227 client.on('close', function() {
228 var index = self.clients.indexOf(client);
230 self.clients.splice(index, 1);
235 // signal upgrade complete
236 socket.removeListener('error', errorHandler);
240 // optionally call external protocol selection handler before
241 // calling completeHybiUpgrade2
242 var completeHybiUpgrade1 = function() {
243 // choose from the sub-protocols
244 if (typeof self.options.handleProtocols == 'function') {
245 var protList = (protocols || "").split(/, */);
246 var callbackCalled = false;
247 var res = self.options.handleProtocols(protList, function(result, protocol) {
248 callbackCalled = true;
249 if (!result) abortConnection(socket, 404, 'Unauthorized')
250 else completeHybiUpgrade2(protocol);
252 if (!callbackCalled) {
253 // the handleProtocols handler never called our callback
254 abortConnection(socket, 501, 'Could not process protocols');
258 if (typeof protocols !== 'undefined') {
259 completeHybiUpgrade2(protocols.split(/, */)[0]);
262 completeHybiUpgrade2();
267 // optionally call external client verification handler
268 if (typeof this.options.verifyClient == 'function') {
271 secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
274 if (this.options.verifyClient.length == 2) {
275 this.options.verifyClient(info, function(result, code, name) {
276 if (typeof code === 'undefined') code = 401;
277 if (typeof name === 'undefined') name = http.STATUS_CODES[code];
279 if (!result) abortConnection(socket, code, name);
280 else completeHybiUpgrade1();
284 else if (!this.options.verifyClient(info)) {
285 abortConnection(socket, 401, 'Unauthorized');
290 completeHybiUpgrade1();
293 function handleHixieUpgrade(req, socket, upgradeHead, cb) {
294 // handle premature socket errors
295 var errorHandler = function() {
296 try { socket.destroy(); } catch (e) {}
298 socket.on('error', errorHandler);
300 // bail if options prevent hixie
301 if (this.options.disableHixie) {
302 abortConnection(socket, 401, 'Hixie support disabled');
306 // verify key presence
307 if (!req.headers['sec-websocket-key2']) {
308 abortConnection(socket, 400, 'Bad Request');
312 var origin = req.headers['origin']
315 // setup handshake completion to run after client has been verified
316 var onClientVerified = function() {
318 if (!req.headers['x-forwarded-host'])
319 wshost = req.headers.host;
321 wshost = req.headers['x-forwarded-host'];
322 var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
323 , protocol = req.headers['sec-websocket-protocol'];
325 // handshake completion code to run once nonce has been successfully retrieved
326 var completeHandshake = function(nonce, rest) {
328 var k1 = req.headers['sec-websocket-key1']
329 , k2 = req.headers['sec-websocket-key2']
330 , md5 = crypto.createHash('md5');
332 [k1, k2].forEach(function (k) {
333 var n = parseInt(k.replace(/[^\d]/g, ''))
334 , spaces = k.replace(/[^ ]/g, '').length;
335 if (spaces === 0 || n % spaces !== 0){
336 abortConnection(socket, 400, 'Bad Request');
340 md5.update(String.fromCharCode(
346 md5.update(nonce.toString('binary'));
349 'HTTP/1.1 101 Switching Protocols'
350 , 'Upgrade: WebSocket'
351 , 'Connection: Upgrade'
352 , 'Sec-WebSocket-Location: ' + location
354 if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol);
355 if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin);
357 socket.setTimeout(0);
358 socket.setNoDelay(true);
360 // merge header and hash buffer
361 var headerBuffer = new Buffer(headers.concat('', '').join('\r\n'));
362 var hashBuffer = new Buffer(md5.digest('binary'), 'binary');
363 var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length);
364 headerBuffer.copy(handshakeBuffer, 0);
365 hashBuffer.copy(handshakeBuffer, headerBuffer.length);
367 // do a single write, which - upon success - causes a new client websocket to be setup
368 socket.write(handshakeBuffer, 'binary', function(err) {
369 if (err) return; // do not create client if an error happens
370 var client = new WebSocket([req, socket, rest], {
371 protocolVersion: 'hixie-76',
374 if (self.options.clientTracking) {
375 self.clients.push(client);
376 client.on('close', function() {
377 var index = self.clients.indexOf(client);
379 self.clients.splice(index, 1);
384 // signal upgrade complete
385 socket.removeListener('error', errorHandler);
390 try { socket.destroy(); } catch (e) {}
397 if (upgradeHead && upgradeHead.length >= nonceLength) {
398 var nonce = upgradeHead.slice(0, nonceLength);
399 var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null;
400 completeHandshake.call(self, nonce, rest);
403 // nonce not present in upgradeHead, so we must wait for enough data
404 // data to arrive before continuing
405 var nonce = new Buffer(nonceLength);
406 upgradeHead.copy(nonce, 0);
407 var received = upgradeHead.length;
409 var handler = function (data) {
410 var toRead = Math.min(data.length, nonceLength - received);
411 if (toRead === 0) return;
412 data.copy(nonce, received, 0, toRead);
414 if (received == nonceLength) {
415 socket.removeListener('data', handler);
416 if (toRead < data.length) rest = data.slice(toRead);
417 completeHandshake.call(self, nonce, rest);
420 socket.on('data', handler);
425 if (typeof this.options.verifyClient == 'function') {
428 secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
431 if (this.options.verifyClient.length == 2) {
433 this.options.verifyClient(info, function(result, code, name) {
434 if (typeof code === 'undefined') code = 401;
435 if (typeof name === 'undefined') name = http.STATUS_CODES[code];
437 if (!result) abortConnection(socket, code, name);
438 else onClientVerified.apply(self);
442 else if (!this.options.verifyClient(info)) {
443 abortConnection(socket, 401, 'Unauthorized');
448 // no client verification required
452 function abortConnection(socket, code, name) {
455 'HTTP/1.1 ' + code + ' ' + name,
456 'Content-type: text/html'
458 socket.write(response.concat('', '').join('\r\n'));
460 catch (e) { /* ignore errors - we've aborted this connection */ }
462 // ensure that an early aborted connection is shut down completely
463 try { socket.destroy(); } catch (e) {}