diff --git a/lib/manager.js b/lib/manager.js index b5f32dbb10..ee2bf49271 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -866,7 +866,7 @@ Manager.prototype.handshakeData = function (data) { */ Manager.prototype.verifyOrigin = function (request) { - var origin = request.headers.origin + var origin = request.headers.origin || request.headers.referer , origins = this.get('origins'); if (origin === 'null') origin = '*'; @@ -878,14 +878,19 @@ Manager.prototype.verifyOrigin = function (request) { if (origin) { try { var parts = url.parse(origin); - - return - ~origins.indexOf(parts.host + ':' + parts.port) || - ~origins.indexOf(parts.host + ':*') || + var ok = + ~origins.indexOf(parts.hostname + ':' + parts.port) || + ~origins.indexOf(parts.hostname + ':*') || ~origins.indexOf('*:' + parts.port); - } catch (ex) {} + if (!ok) this.log.warn('illegal origin: ' + origin); + return ok; + } catch (ex) { + this.log.warn('error parsing origin'); + } + } + else { + this.log.warn('origin missing from handshake, yet required by config'); } - return false; }; diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 48b3d95906..16768196ac 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -24,7 +24,7 @@ exports = module.exports = FlashSocket; */ function FlashSocket (mng, data, req) { - WebSocket.call(this, mng, data, req); + return WebSocket.call(this, mng, data, req); } /** diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 41f90dc15d..2082bd6fa1 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -9,10 +9,7 @@ * Module requirements. */ -var Transport = require('../transport') - , EventEmitter = process.EventEmitter - , crypto = require('crypto') - , parser = require('../parser'); +var protocolVersions = require('./wsver'); /** * Export the constructor. @@ -28,323 +25,9 @@ exports = module.exports = WebSocket; */ function WebSocket (mng, data, req) { - // parser - var self = this; - - this.parser = new Parser(); - this.parser.on('data', function (packet) { - self.log.debug(self.name + ' received data packet', packet); - self.onMessage(parser.decodePacket(packet)); - }); - 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; - - this.socket.setNoDelay(true); - - this.buffer = true; - this.buffered = []; - - if (this.req.headers.upgrade !== 'WebSocket') { - this.log.warn(this.name + ' connection invalid'); - this.end(); - return; + var version = req.headers['sec-websocket-version']; + if (typeof version !== 'undefined' && typeof protocolVersions[version] !== 'undefined') { + return new protocolVersions[version](mng, data, req); } - - var origin = this.req.headers.origin - , location = (this.socket.encrypted ? 'wss' : 'ws') - + '://' + this.req.headers.host + this.req.url - , waitingForNonce = false; - - if (this.req.headers['sec-websocket-key1']) { - // If we don't have the nonce yet, wait for it (HAProxy compatibility). - if (! (this.req.head && this.req.head.length >= 8)) { - waitingForNonce = true; - } - - var headers = [ - 'HTTP/1.1 101 WebSocket Protocol Handshake' - , 'Upgrade: WebSocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Origin: ' + origin - , 'Sec-WebSocket-Location: ' + location - ]; - - if (this.req.headers['sec-websocket-protocol']){ - headers.push('Sec-WebSocket-Protocol: ' - + this.req.headers['sec-websocket-protocol']); - } - } else { - var headers = [ - 'HTTP/1.1 101 Web Socket Protocol Handshake' - , 'Upgrade: WebSocket' - , 'Connection: Upgrade' - , 'WebSocket-Origin: ' + origin - , 'WebSocket-Location: ' + location - ]; - } - - try { - this.socket.write(headers.concat('', '').join('\r\n')); - this.socket.setTimeout(0); - this.socket.setNoDelay(true); - this.socket.setEncoding('utf8'); - } catch (e) { - this.end(); - return; - } - - if (waitingForNonce) { - this.socket.setEncoding('binary'); - } else if (this.proveReception(headers)) { - self.flush(); - } - - var headBuffer = ''; - - this.socket.on('data', function (data) { - if (waitingForNonce) { - headBuffer += data; - - if (headBuffer.length < 8) { - return; - } - - // Restore the connection to utf8 encoding after receiving the nonce - self.socket.setEncoding('utf8'); - waitingForNonce = false; - - // Stuff the nonce into the location where it's expected to be - self.req.head = headBuffer.substr(0, 8); - headBuffer = ''; - - if (self.proveReception(headers)) { - self.flush(); - } - - return; - } - - self.parser.add(data); - }); -}; - -/** - * Writes to the socket. - * - * @api private - */ - -WebSocket.prototype.write = function (data) { - if (this.open) { - this.drained = false; - - if (this.buffer) { - this.buffered.push(data); - return this; - } - - var length = Buffer.byteLength(data) - , buffer = new Buffer(2 + length); - - buffer.write('\u0000', 'binary'); - buffer.write(data, 1, 'utf8'); - buffer.write('\uffff', 1 + length, 'binary'); - - try { - if (this.socket.write(buffer)) { - this.drained = true; - } - } catch (e) { - this.end(); - } - - this.log.debug(this.name + ' writing', data); - } -}; - -/** - * Flushes the internal buffer - * - * @api private - */ - -WebSocket.prototype.flush = function () { - this.buffer = false; - - for (var i = 0, l = this.buffered.length; i < l; i++) { - this.write(this.buffered.splice(0, 1)[0]); - } -}; - -/** - * Finishes the handshake. - * - * @api private - */ - -WebSocket.prototype.proveReception = function (headers) { - var self = this - , k1 = this.req.headers['sec-websocket-key1'] - , k2 = this.req.headers['sec-websocket-key2']; - - if (k1 && k2){ - var md5 = crypto.createHash('md5'); - - [k1, k2].forEach(function (k) { - var n = parseInt(k.replace(/[^\d]/g, '')) - , spaces = k.replace(/[^ ]/g, '').length; - - if (spaces === 0 || n % spaces !== 0){ - self.log.warn('Invalid ' + self.name + ' key: "' + k + '".'); - self.end(); - return false; - } - - n /= spaces; - - md5.update(String.fromCharCode( - n >> 24 & 0xFF, - n >> 16 & 0xFF, - n >> 8 & 0xFF, - n & 0xFF)); - }); - - md5.update(this.req.head.toString('binary')); - - try { - this.socket.write(md5.digest('binary'), 'binary'); - } catch (e) { - this.end(); - } - } - - return true; -}; - -/** - * Writes a payload. - * - * @api private - */ - -WebSocket.prototype.payload = function (msgs) { - for (var i = 0, l = msgs.length; i < l; i++) { - this.write(msgs[i]); - } - - return this; -}; - -/** - * Closes the connection. - * - * @api private - */ - -WebSocket.prototype.doClose = function () { - this.socket.end(); -}; - -/** - * WebSocket parser - * - * @api public - */ - -function Parser () { - this.buffer = ''; - this.i = 0; -}; - -/** - * Inherits from EventEmitter. - */ - -Parser.prototype.__proto__ = EventEmitter.prototype; - -/** - * Adds data to the buffer. - * - * @api public - */ - -Parser.prototype.add = function (data) { - this.buffer += data; - this.parse(); -}; - -/** - * Parses the buffer. - * - * @api private - */ - -Parser.prototype.parse = function () { - for (var i = this.i, chr, l = this.buffer.length; i < l; i++){ - chr = this.buffer[i]; - - if (this.buffer.length == 2 && this.buffer[1] == '\u0000') { - this.emit('close'); - this.buffer = ''; - this.i = 0; - return; - } - - if (i === 0){ - if (chr != '\u0000') - this.error('Bad framing. Expected null byte as first frame'); - else - continue; - } - - if (chr == '\ufffd'){ - this.emit('data', this.buffer.substr(1, i - 1)); - this.buffer = this.buffer.substr(i + 1); - this.i = 0; - return this.parse(); - } - } -}; - -/** - * Handles an error - * - * @api private - */ - -Parser.prototype.error = function (reason) { - this.buffer = ''; - this.i = 0; - this.emit('error', reason); - return this; + return new protocolVersions['default'](mng, data, req); }; diff --git a/lib/transports/wsver/8.js b/lib/transports/wsver/8.js new file mode 100644 index 0000000000..467d8d9b2c --- /dev/null +++ b/lib/transports/wsver/8.js @@ -0,0 +1,451 @@ + +/*! + * 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; + + this.socket.setNoDelay(true); + + 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); + } +}; + +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) { + expectData(util.unpack(data)); + }); + } + }, + '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/wsver/default.js b/lib/transports/wsver/default.js new file mode 100644 index 0000000000..092729d923 --- /dev/null +++ b/lib/transports/wsver/default.js @@ -0,0 +1,350 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var Transport = require('../../transport') + , EventEmitter = process.EventEmitter + , crypto = require('crypto') + , parser = require('../../parser'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; + +/** + * 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.log.debug(self.name + ' received data packet', packet); + self.onMessage(parser.decodePacket(packet)); + }); + 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; + + this.socket.setNoDelay(true); + + this.buffer = true; + this.buffered = []; + + 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 + , waitingForNonce = false; + + if (this.req.headers['sec-websocket-key1']) { + // If we don't have the nonce yet, wait for it (HAProxy compatibility). + if (! (this.req.head && this.req.head.length >= 8)) { + waitingForNonce = true; + } + + var headers = [ + 'HTTP/1.1 101 WebSocket Protocol Handshake' + , 'Upgrade: WebSocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Origin: ' + origin + , 'Sec-WebSocket-Location: ' + location + ]; + + if (this.req.headers['sec-websocket-protocol']){ + headers.push('Sec-WebSocket-Protocol: ' + + this.req.headers['sec-websocket-protocol']); + } + } else { + var headers = [ + 'HTTP/1.1 101 Web Socket Protocol Handshake' + , 'Upgrade: WebSocket' + , 'Connection: Upgrade' + , 'WebSocket-Origin: ' + origin + , 'WebSocket-Location: ' + location + ]; + } + + try { + this.socket.write(headers.concat('', '').join('\r\n')); + this.socket.setTimeout(0); + this.socket.setNoDelay(true); + this.socket.setEncoding('utf8'); + } catch (e) { + this.end(); + return; + } + + if (waitingForNonce) { + this.socket.setEncoding('binary'); + } else if (this.proveReception(headers)) { + self.flush(); + } + + var headBuffer = ''; + + this.socket.on('data', function (data) { + if (waitingForNonce) { + headBuffer += data; + + if (headBuffer.length < 8) { + return; + } + + // Restore the connection to utf8 encoding after receiving the nonce + self.socket.setEncoding('utf8'); + waitingForNonce = false; + + // Stuff the nonce into the location where it's expected to be + self.req.head = headBuffer.substr(0, 8); + headBuffer = ''; + + if (self.proveReception(headers)) { + self.flush(); + } + + return; + } + + self.parser.add(data); + }); +}; + +/** + * Writes to the socket. + * + * @api private + */ + +WebSocket.prototype.write = function (data) { + if (this.open) { + this.drained = false; + + if (this.buffer) { + this.buffered.push(data); + return this; + } + + var length = Buffer.byteLength(data) + , buffer = new Buffer(2 + length); + + buffer.write('\u0000', 'binary'); + buffer.write(data, 1, 'utf8'); + buffer.write('\uffff', 1 + length, 'binary'); + + try { + if (this.socket.write(buffer)) { + this.drained = true; + } + } catch (e) { + this.end(); + } + + this.log.debug(this.name + ' writing', data); + } +}; + +/** + * Flushes the internal buffer + * + * @api private + */ + +WebSocket.prototype.flush = function () { + this.buffer = false; + + for (var i = 0, l = this.buffered.length; i < l; i++) { + this.write(this.buffered.splice(0, 1)[0]); + } +}; + +/** + * Finishes the handshake. + * + * @api private + */ + +WebSocket.prototype.proveReception = function (headers) { + var self = this + , k1 = this.req.headers['sec-websocket-key1'] + , k2 = this.req.headers['sec-websocket-key2']; + + if (k1 && k2){ + var md5 = crypto.createHash('md5'); + + [k1, k2].forEach(function (k) { + var n = parseInt(k.replace(/[^\d]/g, '')) + , spaces = k.replace(/[^ ]/g, '').length; + + if (spaces === 0 || n % spaces !== 0){ + self.log.warn('Invalid ' + self.name + ' key: "' + k + '".'); + self.end(); + return false; + } + + n /= spaces; + + md5.update(String.fromCharCode( + n >> 24 & 0xFF, + n >> 16 & 0xFF, + n >> 8 & 0xFF, + n & 0xFF)); + }); + + md5.update(this.req.head.toString('binary')); + + try { + this.socket.write(md5.digest('binary'), 'binary'); + } catch (e) { + this.end(); + } + } + + return true; +}; + +/** + * Writes a payload. + * + * @api private + */ + +WebSocket.prototype.payload = function (msgs) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.write(msgs[i]); + } + + return this; +}; + +/** + * Closes the connection. + * + * @api private + */ + +WebSocket.prototype.doClose = function () { + this.socket.end(); +}; + +/** + * WebSocket parser + * + * @api public + */ + +function Parser () { + this.buffer = ''; + this.i = 0; +}; + +/** + * Inherits from EventEmitter. + */ + +Parser.prototype.__proto__ = EventEmitter.prototype; + +/** + * Adds data to the buffer. + * + * @api public + */ + +Parser.prototype.add = function (data) { + this.buffer += data; + this.parse(); +}; + +/** + * Parses the buffer. + * + * @api private + */ + +Parser.prototype.parse = function () { + for (var i = this.i, chr, l = this.buffer.length; i < l; i++){ + chr = this.buffer[i]; + + if (this.buffer.length == 2 && this.buffer[1] == '\u0000') { + this.emit('close'); + this.buffer = ''; + this.i = 0; + return; + } + + if (i === 0){ + if (chr != '\u0000') + this.error('Bad framing. Expected null byte as first frame'); + else + continue; + } + + if (chr == '\ufffd'){ + this.emit('data', this.buffer.substr(1, i - 1)); + this.buffer = this.buffer.substr(i + 1); + this.i = 0; + return this.parse(); + } + } +}; + +/** + * Handles an error + * + * @api private + */ + +Parser.prototype.error = function (reason) { + this.buffer = ''; + this.i = 0; + this.emit('error', reason); + return this; +}; diff --git a/lib/transports/wsver/index.js b/lib/transports/wsver/index.js new file mode 100644 index 0000000000..6f94f5975a --- /dev/null +++ b/lib/transports/wsver/index.js @@ -0,0 +1,9 @@ + +/** + * Export websocket versions. + */ + +module.exports = { + 8: require('./8') + , default: require('./default') +}; diff --git a/lib/util.js b/lib/util.js index 25f31f2bd8..f7d9f2b4b4 100644 --- a/lib/util.js +++ b/lib/util.js @@ -23,3 +23,28 @@ exports.toArray = function (enu) { return arr; }; + +/** + * Unpacks a buffer to a number. + * + * @api public + */ + +exports.unpack = function (buffer) { + var n = 0; + for (var i = 0; i < buffer.length; ++i) { + n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; + } + return n; +} + +/** + * Left pads a string. + * + * @api public + */ + +exports.padl = function (s,n,c) { + return new Array(1 + n - s.length).join(c) + s; +} + diff --git a/test/manager.test.js b/test/manager.test.js index 6b8319f85c..93922d38db 100644 --- a/test/manager.test.js +++ b/test/manager.test.js @@ -472,6 +472,91 @@ module.exports = { }); }, + 'test that a referer is accepted for *:* origin': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + io.set('origins', '*:*'); + }); + + cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:82/something' } }, function (res, data) { + res.statusCode.should.eql(200); + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that valid referer is accepted for addr:* origin': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + io.set('origins', 'foo.bar.com:*'); + }); + + cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) { + res.statusCode.should.eql(200); + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that erroneous referer is denied for addr:* origin': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + io.set('origins', 'foo.bar.com:*'); + }); + + cl.get('/socket.io/{protocol}', { headers: { referer: 'http://baz.bar.com/something' } }, function (res, data) { + res.statusCode.should.eql(403); + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that valid referer port is accepted for addr:port origin': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + io.set('origins', 'foo.bar.com:81'); + }); + + cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:81/something' } }, function (res, data) { + res.statusCode.should.eql(200); + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that erroneous referer port is denied for addr:port origin': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.configure(function () { + io.set('origins', 'foo.bar.com:81'); + }); + + cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) { + res.statusCode.should.eql(403); + cl.end(); + io.server.close(); + done(); + }); + }, + 'test limiting the supported transports for a manager': function (done) { var port = ++ports , io = sio.listen(port) diff --git a/test/transports.websocket.hybi10-parser.test.js b/test/transports.websocket.hybi10-parser.test.js new file mode 100644 index 0000000000..78f451f6cb --- /dev/null +++ b/test/transports.websocket.hybi10-parser.test.js @@ -0,0 +1,220 @@ +var assert = require('assert'); +var Parser = require('../lib/transports/wsver/8.js').Parser; + +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; +} + +function splitBuffer(buffer) { + var b1 = new Buffer(Math.ceil(buffer.length / 2)); + var b2 = new Buffer(Math.floor(buffer.length / 2)); + buffer.copy(b1, 0, 0, b1.length); + buffer.copy(b2, 0, b1.length, b1.length + b2.length); + return [b1, b2]; +} + +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; +} + +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; +} + +function pack(length, number) { + return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim(); +} + +function padl(s,n,c) { + return new Array(1 + n - s.length).join(c) + s; +} + +function dump(data) { + var s = ''; + for (var i = 0; i < data.length; ++i) { + s += padl(data[i].toString(16), 2, '0') + ' '; + } + return s.trim(); +} + +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 of 300 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 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); + }, +}; +