3 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
11 var Transport = require('../../transport')
12 , EventEmitter = process.EventEmitter
13 , crypto = require('crypto')
14 , parser = require('../../parser');
17 * Export the constructor.
20 exports = module.exports = WebSocket;
23 * HTTP interface constructor. Interface compatible with all transports that
24 * depend on request-response cycles.
29 function WebSocket (mng, data, req) {
33 this.parser = new Parser();
34 this.parser.on('data', function (packet) {
35 self.log.debug(self.name + ' received data packet', packet);
36 self.onMessage(parser.decodePacket(packet));
38 this.parser.on('close', function () {
41 this.parser.on('error', function () {
45 Transport.call(this, mng, data, req);
49 * Inherits from Transport.
52 WebSocket.prototype.__proto__ = Transport.prototype;
60 WebSocket.prototype.name = 'websocket';
63 * Websocket draft version
68 WebSocket.prototype.protocolVersion = 'hixie-76';
71 * Called when the socket connects.
76 WebSocket.prototype.onSocketConnect = function () {
79 this.socket.setNoDelay(true);
84 if (this.req.headers.upgrade !== 'WebSocket') {
85 this.log.warn(this.name + ' connection invalid');
90 var origin = this.req.headers['origin']
91 , waitingForNonce = false;
92 if(this.manager.settings['match origin protocol']){
93 location = (origin.indexOf('https')>-1 ? 'wss' : 'ws') + '://' + this.req.headers.host + this.req.url;
94 }else if(this.socket.encrypted){
95 location = 'wss://' + this.req.headers.host + this.req.url;
97 location = 'ws://' + this.req.headers.host + this.req.url;
100 if (this.req.headers['sec-websocket-key1']) {
101 // If we don't have the nonce yet, wait for it (HAProxy compatibility).
102 if (! (this.req.head && this.req.head.length >= 8)) {
103 waitingForNonce = true;
107 'HTTP/1.1 101 WebSocket Protocol Handshake'
108 , 'Upgrade: WebSocket'
109 , 'Connection: Upgrade'
110 , 'Sec-WebSocket-Origin: ' + origin
111 , 'Sec-WebSocket-Location: ' + location
114 if (this.req.headers['sec-websocket-protocol']){
115 headers.push('Sec-WebSocket-Protocol: '
116 + this.req.headers['sec-websocket-protocol']);
120 'HTTP/1.1 101 Web Socket Protocol Handshake'
121 , 'Upgrade: WebSocket'
122 , 'Connection: Upgrade'
123 , 'WebSocket-Origin: ' + origin
124 , 'WebSocket-Location: ' + location
129 this.socket.write(headers.concat('', '').join('\r\n'));
130 this.socket.setTimeout(0);
131 this.socket.setNoDelay(true);
132 this.socket.setEncoding('utf8');
138 if (waitingForNonce) {
139 this.socket.setEncoding('binary');
140 } else if (this.proveReception(headers)) {
146 this.socket.on('data', function (data) {
147 if (waitingForNonce) {
150 if (headBuffer.length < 8) {
154 // Restore the connection to utf8 encoding after receiving the nonce
155 self.socket.setEncoding('utf8');
156 waitingForNonce = false;
158 // Stuff the nonce into the location where it's expected to be
159 self.req.head = headBuffer.substr(0, 8);
162 if (self.proveReception(headers)) {
169 self.parser.add(data);
174 * Writes to the socket.
179 WebSocket.prototype.write = function (data) {
181 this.drained = false;
184 this.buffered.push(data);
188 var length = Buffer.byteLength(data)
189 , buffer = new Buffer(2 + length);
191 buffer.write('\x00', 'binary');
192 buffer.write(data, 1, 'utf8');
193 buffer.write('\xff', 1 + length, 'binary');
196 if (this.socket.write(buffer)) {
203 this.log.debug(this.name + ' writing', data);
208 * Flushes the internal buffer
213 WebSocket.prototype.flush = function () {
216 for (var i = 0, l = this.buffered.length; i < l; i++) {
217 this.write(this.buffered.splice(0, 1)[0]);
222 * Finishes the handshake.
227 WebSocket.prototype.proveReception = function (headers) {
229 , k1 = this.req.headers['sec-websocket-key1']
230 , k2 = this.req.headers['sec-websocket-key2'];
233 var md5 = crypto.createHash('md5');
235 [k1, k2].forEach(function (k) {
236 var n = parseInt(k.replace(/[^\d]/g, ''))
237 , spaces = k.replace(/[^ ]/g, '').length;
239 if (spaces === 0 || n % spaces !== 0){
240 self.log.warn('Invalid ' + self.name + ' key: "' + k + '".');
247 md5.update(String.fromCharCode(
254 md5.update(this.req.head.toString('binary'));
257 this.socket.write(md5.digest('binary'), 'binary');
272 WebSocket.prototype.payload = function (msgs) {
273 for (var i = 0, l = msgs.length; i < l; i++) {
281 * Closes the connection.
286 WebSocket.prototype.doClose = function () {
302 * Inherits from EventEmitter.
305 Parser.prototype.__proto__ = EventEmitter.prototype;
308 * Adds data to the buffer.
313 Parser.prototype.add = function (data) {
324 Parser.prototype.parse = function () {
325 for (var i = this.i, chr, l = this.buffer.length; i < l; i++){
326 chr = this.buffer[i];
328 if (this.buffer.length == 2 && this.buffer[1] == '\u0000') {
337 this.error('Bad framing. Expected null byte as first frame');
342 if (chr == '\ufffd'){
343 this.emit('data', this.buffer.substr(1, i - 1));
344 this.buffer = this.buffer.substr(i + 1);
357 Parser.prototype.error = function (reason) {
360 this.emit('error', reason);