123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- /*!
- * socket.io-node
- * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
- * 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';
- /**
- * Websocket draft version
- *
- * @api public
- */
- WebSocket.prototype.protocolVersion = 'hixie-76';
- /**
- * 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']
- , waitingForNonce = false;
- if(this.manager.settings['match origin protocol']){
- location = (origin.indexOf('https')>-1 ? 'wss' : 'ws') + '://' + this.req.headers.host + this.req.url;
- }else if(this.socket.encrypted){
- location = 'wss://' + this.req.headers.host + this.req.url;
- }else{
- location = 'ws://' + this.req.headers.host + this.req.url;
- }
- 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('\x00', 'binary');
- buffer.write(data, 1, 'utf8');
- buffer.write('\xff', 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;
- };
|