From 48dadd8e10e1015a87ef77e27f397a97c08a1b79 Mon Sep 17 00:00:00 2001 From: einaros Date: Mon, 29 Aug 2011 07:56:40 +0200 Subject: [PATCH] joined compatible hybi protocol handlers and updated test reference --- lib/transports/websocket/8.js | 461 ------------------ .../websocket/{7.js => hybi-07-12.js} | 0 lib/transports/websocket/index.js | 4 +- ...sports.websocket.hybi07-12.parser.test.js} | 2 +- ...transports.websocket.hybi10-parser.test.js | 271 ---------- 5 files changed, 3 insertions(+), 735 deletions(-) delete mode 100644 lib/transports/websocket/8.js rename lib/transports/websocket/{7.js => hybi-07-12.js} (100%) rename test/{transports.websocket.hybi07-parser.test.js => transports.websocket.hybi07-12.parser.test.js} (99%) delete mode 100644 test/transports.websocket.hybi10-parser.test.js diff --git a/lib/transports/websocket/8.js b/lib/transports/websocket/8.js deleted file mode 100644 index 38c6e058..00000000 --- a/lib/transports/websocket/8.js +++ /dev/null @@ -1,461 +0,0 @@ - -/*! - * socket.io-node - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -/** - * Module requirements. - */ - -var Transport = require('../../transport') - , EventEmitter = process.EventEmitter - , crypto = require('crypto') - , parser = require('../../parser') - , util = require('../../util'); - -/** - * Export the constructor. - */ - -exports = module.exports = WebSocket; -exports.Parser = Parser; - -/** - * HTTP interface constructor. Interface compatible with all transports that - * depend on request-response cycles. - * - * @api public - */ - -function WebSocket (mng, data, req) { - // parser - var self = this; - - this.parser = new Parser(); - this.parser.on('data', function (packet) { - self.onMessage(parser.decodePacket(packet)); - }); - this.parser.on('ping', function () { - // version 8 ping => pong - this.socket.write('\u008a\u0000'); - }); - this.parser.on('close', function () { - self.end(); - }); - this.parser.on('error', function () { - self.end(); - }); - - Transport.call(this, mng, data, req); -}; - -/** - * Inherits from Transport. - */ - -WebSocket.prototype.__proto__ = Transport.prototype; - -/** - * Transport name - * - * @api public - */ - -WebSocket.prototype.name = 'websocket'; - -/** - * Called when the socket connects. - * - * @api private - */ - -WebSocket.prototype.onSocketConnect = function () { - var self = this; - - if (this.req.headers.upgrade !== 'websocket') { - this.log.warn(this.name + ' connection invalid'); - this.end(); - return; - } - - var origin = this.req.headers.origin - , location = (this.socket.encrypted ? 'wss' : 'ws') - + '://' + this.req.headers.host + this.req.url; - - if (!this.req.headers['sec-websocket-key']) { - this.log.warn(this.name + ' connection invalid: received no key'); - this.end(); - return; - } - - // calc key - var key = this.req.headers['sec-websocket-key']; - var shasum = crypto.createHash('sha1'); - shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); - key = shasum.digest('base64'); - - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: websocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Accept: ' + key - ]; - - try { - this.socket.write(headers.concat('', '').join('\r\n')); - this.socket.setTimeout(0); - this.socket.setNoDelay(true); - } catch (e) { - this.end(); - return; - } - - this.socket.on('data', function (data) { - self.parser.add(data); - }); -}; - -/** - * Writes to the socket. - * - * @api private - */ - -WebSocket.prototype.write = function (data) { - if (this.open) { - var buf = this.frame(0x81, data); - this.socket.write(buf, 'binary'); - this.log.debug(this.name + ' writing', data); - } -}; - -/** - * Frame server-to-client output as a text packet. - * - * @api private - */ - -WebSocket.prototype.frame = function (opcode, data) { - var startOffset = 2, secondByte = data.length, buf; - if (data.length > 125) { - // opcode = 0x81; - startOffset += 2; - secondByte = 126; - } - if (data.length > 65536) { - startOffset += 6; - secondByte = 127; - } - buf = new Buffer(data.length + startOffset, 'utf8'); - buf[0] = opcode; - buf[1] = secondByte; - switch (secondByte) { - case 126: - buf[2] = data.length >>> 8; - buf[3] = data.length % 256; - break; - case 127: - for (var i = 6; i > 2; i--) { - buf[i] = secondByte % 256; - secondByte >>>= 8; - } - } - buf.write(data, startOffset, 'utf8'); - return buf; -}; - -/** - * Closes the connection. - * - * @api private - */ - -WebSocket.prototype.doClose = function () { - this.socket.end(); -}; - -/** - * WebSocket parser - * - * @api public - */ - -function Parser () { - this.state = { - activeFragmentedOperation: null, - lastFragment: false, - masked: false, - opcode: 0 - }; - this.overflow = null; - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - this.currentMessage = ''; - - var self = this; - this.opcodeHandlers = { - // text - '1': function(data) { - var finish = function(mask, data) { - self.currentMessage += self.unmask(mask, data); - if (self.state.lastFragment) { - self.emit('data', self.currentMessage); - self.currentMessage = ''; - } - self.endPacket(); - } - - var expectData = function(length) { - if (self.state.masked) { - self.expect('Mask', 4, function(data) { - var mask = data; - self.expect('Data', length, function(data) { - finish(mask, data); - }); - }); - } - else { - self.expect('Data', length, function(data) { - finish(null, data); - }); - } - } - - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - expectData(firstLength); - } - else if (firstLength == 126) { - self.expect('Length', 2, function(data) { - expectData(util.unpack(data)); - }); - } - else if (firstLength == 127) { - self.expect('Length', 8, function(data) { - if (util.unpack(data.slice(0, 4)) != 0) { - self.error('packets with length spanning more than 32 bit is currently not supported'); - return; - } - var lengthBytes = data.slice(4); // note: cap to 32 bit length - expectData(util.unpack(data)); - }); - } - }, - // close - '8': function(data) { - self.emit('close'); - self.reset(); - }, - // ping - '9': function(data) { - if (self.state.lastFragment == false) { - self.error('fragmented ping is not supported'); - return; - } - - var finish = function(mask, data) { - self.emit('ping', self.unmask(mask, data)); - self.endPacket(); - } - - var expectData = function(length) { - if (self.state.masked) { - self.expect('Mask', 4, function(data) { - var mask = data; - self.expect('Data', length, function(data) { - finish(mask, data); - }); - }); - } - else { - self.expect('Data', length, function(data) { - finish(null, data); - }); - } - } - - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength == 0) { - finish(null, null); - } - else if (firstLength < 126) { - expectData(firstLength); - } - else if (firstLength == 126) { - self.expect('Length', 2, function(data) { - expectData(util.unpack(data)); - }); - } - else if (firstLength == 127) { - self.expect('Length', 8, function(data) { - expectData(util.unpack(data)); - }); - } - } - } - - this.expect('Opcode', 2, this.processPacket); -}; - -/** - * Inherits from EventEmitter. - */ - -Parser.prototype.__proto__ = EventEmitter.prototype; - -/** - * Add new data to the parser. - * - * @api public - */ - -Parser.prototype.add = function(data) { - if (this.expectBuffer == null) { - this.addToOverflow(data); - return; - } - var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset); - data.copy(this.expectBuffer, this.expectOffset, 0, toRead); - this.expectOffset += toRead; - if (toRead < data.length) { - // at this point the overflow buffer shouldn't at all exist - this.overflow = new Buffer(data.length - toRead); - data.copy(this.overflow, 0, toRead, toRead + this.overflow.length); - } - if (this.expectOffset == this.expectBuffer.length) { - var bufferForHandler = this.expectBuffer; - this.expectBuffer = null; - this.expectOffset = 0; - this.expectHandler.call(this, bufferForHandler); - } -} - -/** - * Adds a piece of data to the overflow. - * - * @api private - */ - -Parser.prototype.addToOverflow = function(data) { - if (this.overflow == null) this.overflow = data; - else { - var prevOverflow = this.overflow; - this.overflow = new Buffer(this.overflow.length + data.length); - prevOverflow.copy(this.overflow, 0); - data.copy(this.overflow, prevOverflow.length); - } -} - -/** - * Waits for a certain amount of bytes to be available, then fires a callback. - * - * @api private - */ - -Parser.prototype.expect = function(what, length, handler) { - this.expectBuffer = new Buffer(length); - this.expectOffset = 0; - this.expectHandler = handler; - if (this.overflow != null) { - var toOverflow = this.overflow; - this.overflow = null; - this.add(toOverflow); - } -} - -/** - * Start processing a new packet. - * - * @api private - */ - -Parser.prototype.processPacket = function (data) { - if ((data[0] & 0x70) != 0) this.error('reserved fields not empty'); - this.state.lastFragment = (data[0] & 0x80) == 0x80; - this.state.masked = (data[1] & 0x80) == 0x80; - var opcode = data[0] & 0xf; - if (opcode == 0) { - // continuation frame - if (this.state.opcode != 1 || this.state.opcode != 2) { - this.error('continuation frame cannot follow current opcode') - return; - } - } - else this.state.opcode = opcode; - this.state.opcode = data[0] & 0xf; - var handler = this.opcodeHandlers[this.state.opcode]; - if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode); - else handler(data); -} - -/** - * Endprocessing a packet. - * - * @api private - */ - -Parser.prototype.endPacket = function() { - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) { - // end current fragmented operation - this.state.activeFragmentedOperation = null; - } - this.state.lastFragment = false; - this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; - this.state.masked = false; - this.expect('Opcode', 2, this.processPacket); -} - -/** - * Reset the parser state. - * - * @api private - */ - -Parser.prototype.reset = function() { - this.state = { - activeFragmentedOperation: null, - lastFragment: false, - masked: false, - opcode: 0 - }; - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - this.overflow = null; - this.currentMessage = ''; -} - -/** - * Unmask received data. - * - * @api private - */ - -Parser.prototype.unmask = function (mask, buf) { - if (mask != null) { - for (var i = 0, ll = buf.length; i < ll; i++) { - buf[i] ^= mask[i % 4]; - } - } - return buf != null ? buf.toString('utf8') : ''; -} - -/** - * Handles an error - * - * @api private - */ - -Parser.prototype.error = function (reason) { - this.reset(); - this.emit('error', reason); - return this; -}; diff --git a/lib/transports/websocket/7.js b/lib/transports/websocket/hybi-07-12.js similarity index 100% rename from lib/transports/websocket/7.js rename to lib/transports/websocket/hybi-07-12.js diff --git a/lib/transports/websocket/index.js b/lib/transports/websocket/index.js index 59dc5dd5..5c615b29 100644 --- a/lib/transports/websocket/index.js +++ b/lib/transports/websocket/index.js @@ -4,7 +4,7 @@ */ module.exports = { - 7: require('./7'), - 8: require('./8'), + 7: require('./hybi-07-12'), + 8: require('./hybi-07-12'), default: require('./default') }; diff --git a/test/transports.websocket.hybi07-parser.test.js b/test/transports.websocket.hybi07-12.parser.test.js similarity index 99% rename from test/transports.websocket.hybi07-parser.test.js rename to test/transports.websocket.hybi07-12.parser.test.js index 8c53ea9a..94efaa66 100644 --- a/test/transports.websocket.hybi07-parser.test.js +++ b/test/transports.websocket.hybi07-12.parser.test.js @@ -3,7 +3,7 @@ */ var assert = require('assert'); -var Parser = require('../lib/transports/websocket/7.js').Parser; +var Parser = require('../lib/transports/websocket/hybi-07-12.js').Parser; /** * Returns a Buffer from a "ff 00 ff"-type hex string. diff --git a/test/transports.websocket.hybi10-parser.test.js b/test/transports.websocket.hybi10-parser.test.js deleted file mode 100644 index e7bc4a90..00000000 --- a/test/transports.websocket.hybi10-parser.test.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Test dependencies. - */ - -var assert = require('assert'); -var Parser = require('../lib/transports/websocket/8.js').Parser; - -/** - * Returns a Buffer from a "ff 00 ff"-type hex string. - */ - -function makeBufferFromHexString(byteStr) { - var bytes = byteStr.split(' '); - var buf = new Buffer(bytes.length); - for (var i = 0; i < bytes.length; ++i) { - buf[i] = parseInt(bytes[i], 16); - } - return buf; -} - -/** - * Splits a buffer in two parts. - */ - -function splitBuffer(buffer) { - var b1 = new Buffer(Math.ceil(buffer.length / 2)); - buffer.copy(b1, 0, 0, b1.length); - var b2 = new Buffer(Math.floor(buffer.length / 2)); - buffer.copy(b2, 0, b1.length, b1.length + b2.length); - return [b1, b2]; -} - -/** - * Performs hybi07+ type masking on a hex string. - */ - -function mask(str, maskString) { - var buf = new Buffer(str); - var mask = makeBufferFromHexString(maskString || '34 83 a8 68'); - for (var i = 0; i < buf.length; ++i) { - buf[i] ^= mask[i % 4]; - } - return buf; -} - -/** - * Unpacks a Buffer into a number. - */ - -function unpack(buffer) { - var n = 0; - for (var i = 0; i < buffer.length; ++i) { - n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; - } - return n; -} - -/** - * Returns a hex string, representing a specific byte count 'length', from a number. - */ - -function pack(length, number) { - return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim(); -} - -/** - * Left pads the string 's' to a total length of 'n' with char 'c'. - */ - -function padl(s, n, c) { - return new Array(1 + n - s.length).join(c) + s; -} - -/** - * Returns a hex string from a Buffer. - */ - -function dump(data) { - var s = ''; - for (var i = 0; i < data.length; ++i) { - s += padl(data[i].toString(16), 2, '0') + ' '; - } - return s.trim(); -} - -/** - * Tests. - */ - -module.exports = { - 'can parse unmasked text message': function() { - var p = new Parser(); - var packet = '81 05 48 65 6c 6c 6f'; - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal('Hello', data); - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotData); - }, - 'can parse close message': function() { - var p = new Parser(); - var packet = '88 00'; - - var gotClose = false; - p.on('close', function(data) { - gotClose = true; - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotClose); - }, - 'can parse masked text message': function() { - var p = new Parser(); - var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5'; - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal('5:::{"name":"echo"}', data); - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotData); - }, - 'can parse a masked text message longer than 125 bytes': function() { - var p = new Parser(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68')); - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal(message, data); - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotData); - }, - 'can parse a really long masked text message': function() { - var p = new Parser(); - var message = 'A'; - for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString(); - var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68')); - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal(message, data); - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotData); - }, - 'can parse a fragmented masked text message of 300 bytes': function() { - var p = new Parser(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - var msgpiece1 = message.substr(0, 150); - var msgpiece2 = message.substr(150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68')); - var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal(message, data); - }); - - p.add(makeBufferFromHexString(packet1)); - p.add(makeBufferFromHexString(packet2)); - assert.ok(gotData); - }, - 'can parse a ping message': function() { - var p = new Parser(); - var message = 'Hello'; - var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68')); - - var gotPing = false; - p.on('ping', function(data) { - gotPing = true; - assert.equal(message, data); - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotPing); - }, - 'can parse a ping with no data': function() { - var p = new Parser(); - var packet = '89 00'; - - var gotPing = false; - p.on('ping', function(data) { - gotPing = true; - }); - - p.add(makeBufferFromHexString(packet)); - assert.ok(gotPing); - }, - 'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() { - var p = new Parser(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - - var msgpiece1 = message.substr(0, 150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68')); - - var pingMessage = 'Hello'; - var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + dump(mask(pingMessage, '34 83 a8 68')); - - var msgpiece2 = message.substr(150); - var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal(message, data); - }); - var gotPing = false; - p.on('ping', function(data) { - gotPing = true; - assert.equal(pingMessage, data); - }); - - p.add(makeBufferFromHexString(packet1)); - p.add(makeBufferFromHexString(pingPacket)); - p.add(makeBufferFromHexString(packet2)); - assert.ok(gotData); - assert.ok(gotPing); - }, - 'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() { - var p = new Parser(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - - var msgpiece1 = message.substr(0, 150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68')); - - var pingMessage = 'Hello'; - var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + dump(mask(pingMessage, '34 83 a8 68')); - - var msgpiece2 = message.substr(150); - var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.on('data', function(data) { - gotData = true; - assert.equal(message, data); - }); - var gotPing = false; - p.on('ping', function(data) { - gotPing = true; - assert.equal(pingMessage, data); - }); - - var buffers = []; - buffers = buffers.concat(splitBuffer(makeBufferFromHexString(packet1))); - buffers = buffers.concat(splitBuffer(makeBufferFromHexString(pingPacket))); - buffers = buffers.concat(splitBuffer(makeBufferFromHexString(packet2))); - for (var i = 0; i < buffers.length; ++i) { - p.add(buffers[i]); - } - assert.ok(gotData); - assert.ok(gotPing); - }, -}; -