X-Git-Url: https://git.josue.xyz/?a=blobdiff_plain;f=node_modules%2Fwebsocket%2Flib%2FWebSocketConnection.js;fp=node_modules%2Fwebsocket%2Flib%2FWebSocketConnection.js;h=9f2750cc785a294c63585f2c99f16fae669a9c23;hb=0f4e331e6d75c244e978860b62a6e1aed8d446e0;hp=0000000000000000000000000000000000000000;hpb=0cb383f1c0646575a831f4f812cd85c9e24d9a18;p=VSoRC%2F.git diff --git a/node_modules/websocket/lib/WebSocketConnection.js b/node_modules/websocket/lib/WebSocketConnection.js new file mode 100644 index 0000000..9f2750c --- /dev/null +++ b/node_modules/websocket/lib/WebSocketConnection.js @@ -0,0 +1,896 @@ +/************************************************************************ + * Copyright 2010-2015 Brian McKelvey. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***********************************************************************/ + +var util = require('util'); +var utils = require('./utils'); +var EventEmitter = require('events').EventEmitter; +var WebSocketFrame = require('./WebSocketFrame'); +var BufferList = require('../vendor/FastBufferList'); +var Validation = require('./Validation').Validation; +var bufferAllocUnsafe = utils.bufferAllocUnsafe; +var bufferFromString = utils.bufferFromString; + +// Connected, fully-open, ready to send and receive frames +const STATE_OPEN = 'open'; +// Received a close frame from the remote peer +const STATE_PEER_REQUESTED_CLOSE = 'peer_requested_close'; +// Sent close frame to remote peer. No further data can be sent. +const STATE_ENDING = 'ending'; +// Connection is fully closed. No further data can be sent or received. +const STATE_CLOSED = 'closed'; + +var setImmediateImpl = ('setImmediate' in global) ? + global.setImmediate.bind(global) : + process.nextTick.bind(process); + +var idCounter = 0; + +function WebSocketConnection(socket, extensions, protocol, maskOutgoingPackets, config) { + this._debug = utils.BufferingLogger('websocket:connection', ++idCounter); + this._debug('constructor'); + + if (this._debug.enabled) { + instrumentSocketForDebugging(this, socket); + } + + // Superclass Constructor + EventEmitter.call(this); + + this._pingListenerCount = 0; + this.on('newListener', function(ev) { + if (ev === 'ping'){ + this._pingListenerCount++; + } + }).on('removeListener', function(ev) { + if (ev === 'ping') { + this._pingListenerCount--; + } + }); + + this.config = config; + this.socket = socket; + this.protocol = protocol; + this.extensions = extensions; + this.remoteAddress = socket.remoteAddress; + this.closeReasonCode = -1; + this.closeDescription = null; + this.closeEventEmitted = false; + + // We have to mask outgoing packets if we're acting as a WebSocket client. + this.maskOutgoingPackets = maskOutgoingPackets; + + // We re-use the same buffers for the mask and frame header for all frames + // received on each connection to avoid a small memory allocation for each + // frame. + this.maskBytes = bufferAllocUnsafe(4); + this.frameHeader = bufferAllocUnsafe(10); + + // the BufferList will handle the data streaming in + this.bufferList = new BufferList(); + + // Prepare for receiving first frame + this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + this.fragmentationSize = 0; // data received so far... + this.frameQueue = []; + + // Various bits of connection state + this.connected = true; + this.state = STATE_OPEN; + this.waitingForCloseResponse = false; + // Received TCP FIN, socket's readable stream is finished. + this.receivedEnd = false; + + this.closeTimeout = this.config.closeTimeout; + this.assembleFragments = this.config.assembleFragments; + this.maxReceivedMessageSize = this.config.maxReceivedMessageSize; + + this.outputBufferFull = false; + this.inputPaused = false; + this.receivedDataHandler = this.processReceivedData.bind(this); + this._closeTimerHandler = this.handleCloseTimer.bind(this); + + // Disable nagle algorithm? + this.socket.setNoDelay(this.config.disableNagleAlgorithm); + + // Make sure there is no socket inactivity timeout + this.socket.setTimeout(0); + + if (this.config.keepalive && !this.config.useNativeKeepalive) { + if (typeof(this.config.keepaliveInterval) !== 'number') { + throw new Error('keepaliveInterval must be specified and numeric ' + + 'if keepalive is true.'); + } + this._keepaliveTimerHandler = this.handleKeepaliveTimer.bind(this); + this.setKeepaliveTimer(); + + if (this.config.dropConnectionOnKeepaliveTimeout) { + if (typeof(this.config.keepaliveGracePeriod) !== 'number') { + throw new Error('keepaliveGracePeriod must be specified and ' + + 'numeric if dropConnectionOnKeepaliveTimeout ' + + 'is true.'); + } + this._gracePeriodTimerHandler = this.handleGracePeriodTimer.bind(this); + } + } + else if (this.config.keepalive && this.config.useNativeKeepalive) { + if (!('setKeepAlive' in this.socket)) { + throw new Error('Unable to use native keepalive: unsupported by ' + + 'this version of Node.'); + } + this.socket.setKeepAlive(true, this.config.keepaliveInterval); + } + + // The HTTP Client seems to subscribe to socket error events + // and re-dispatch them in such a way that doesn't make sense + // for users of our client, so we want to make sure nobody + // else is listening for error events on the socket besides us. + this.socket.removeAllListeners('error'); +} + +WebSocketConnection.CLOSE_REASON_NORMAL = 1000; +WebSocketConnection.CLOSE_REASON_GOING_AWAY = 1001; +WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR = 1002; +WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT = 1003; +WebSocketConnection.CLOSE_REASON_RESERVED = 1004; // Reserved value. Undefined meaning. +WebSocketConnection.CLOSE_REASON_NOT_PROVIDED = 1005; // Not to be used on the wire +WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006; // Not to be used on the wire +WebSocketConnection.CLOSE_REASON_INVALID_DATA = 1007; +WebSocketConnection.CLOSE_REASON_POLICY_VIOLATION = 1008; +WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG = 1009; +WebSocketConnection.CLOSE_REASON_EXTENSION_REQUIRED = 1010; +WebSocketConnection.CLOSE_REASON_INTERNAL_SERVER_ERROR = 1011; +WebSocketConnection.CLOSE_REASON_TLS_HANDSHAKE_FAILED = 1015; // Not to be used on the wire + +WebSocketConnection.CLOSE_DESCRIPTIONS = { + 1000: 'Normal connection closure', + 1001: 'Remote peer is going away', + 1002: 'Protocol error', + 1003: 'Unprocessable input', + 1004: 'Reserved', + 1005: 'Reason not provided', + 1006: 'Abnormal closure, no further detail available', + 1007: 'Invalid data received', + 1008: 'Policy violation', + 1009: 'Message too big', + 1010: 'Extension requested by client is required', + 1011: 'Internal Server Error', + 1015: 'TLS Handshake Failed' +}; + +function validateCloseReason(code) { + if (code < 1000) { + // Status codes in the range 0-999 are not used + return false; + } + if (code >= 1000 && code <= 2999) { + // Codes from 1000 - 2999 are reserved for use by the protocol. Only + // a few codes are defined, all others are currently illegal. + return [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].indexOf(code) !== -1; + } + if (code >= 3000 && code <= 3999) { + // Reserved for use by libraries, frameworks, and applications. + // Should be registered with IANA. Interpretation of these codes is + // undefined by the WebSocket protocol. + return true; + } + if (code >= 4000 && code <= 4999) { + // Reserved for private use. Interpretation of these codes is + // undefined by the WebSocket protocol. + return true; + } + if (code >= 5000) { + return false; + } +} + +util.inherits(WebSocketConnection, EventEmitter); + +WebSocketConnection.prototype._addSocketEventListeners = function() { + this.socket.on('error', this.handleSocketError.bind(this)); + this.socket.on('end', this.handleSocketEnd.bind(this)); + this.socket.on('close', this.handleSocketClose.bind(this)); + this.socket.on('drain', this.handleSocketDrain.bind(this)); + this.socket.on('pause', this.handleSocketPause.bind(this)); + this.socket.on('resume', this.handleSocketResume.bind(this)); + this.socket.on('data', this.handleSocketData.bind(this)); +}; + +// set or reset the keepalive timer when data is received. +WebSocketConnection.prototype.setKeepaliveTimer = function() { + this._debug('setKeepaliveTimer'); + if (!this.config.keepalive || this.config.useNativeKeepalive) { return; } + this.clearKeepaliveTimer(); + this.clearGracePeriodTimer(); + this._keepaliveTimeoutID = setTimeout(this._keepaliveTimerHandler, this.config.keepaliveInterval); +}; + +WebSocketConnection.prototype.clearKeepaliveTimer = function() { + if (this._keepaliveTimeoutID) { + clearTimeout(this._keepaliveTimeoutID); + } +}; + +// No data has been received within config.keepaliveTimeout ms. +WebSocketConnection.prototype.handleKeepaliveTimer = function() { + this._debug('handleKeepaliveTimer'); + this._keepaliveTimeoutID = null; + this.ping(); + + // If we are configured to drop connections if the client doesn't respond + // then set the grace period timer. + if (this.config.dropConnectionOnKeepaliveTimeout) { + this.setGracePeriodTimer(); + } + else { + // Otherwise reset the keepalive timer to send the next ping. + this.setKeepaliveTimer(); + } +}; + +WebSocketConnection.prototype.setGracePeriodTimer = function() { + this._debug('setGracePeriodTimer'); + this.clearGracePeriodTimer(); + this._gracePeriodTimeoutID = setTimeout(this._gracePeriodTimerHandler, this.config.keepaliveGracePeriod); +}; + +WebSocketConnection.prototype.clearGracePeriodTimer = function() { + if (this._gracePeriodTimeoutID) { + clearTimeout(this._gracePeriodTimeoutID); + } +}; + +WebSocketConnection.prototype.handleGracePeriodTimer = function() { + this._debug('handleGracePeriodTimer'); + // If this is called, the client has not responded and is assumed dead. + this._gracePeriodTimeoutID = null; + this.drop(WebSocketConnection.CLOSE_REASON_ABNORMAL, 'Peer not responding.', true); +}; + +WebSocketConnection.prototype.handleSocketData = function(data) { + this._debug('handleSocketData'); + // Reset the keepalive timer when receiving data of any kind. + this.setKeepaliveTimer(); + + // Add received data to our bufferList, which efficiently holds received + // data chunks in a linked list of Buffer objects. + this.bufferList.write(data); + + this.processReceivedData(); +}; + +WebSocketConnection.prototype.processReceivedData = function() { + this._debug('processReceivedData'); + // If we're not connected, we should ignore any data remaining on the buffer. + if (!this.connected) { return; } + + // Receiving/parsing is expected to be halted when paused. + if (this.inputPaused) { return; } + + var frame = this.currentFrame; + + // WebSocketFrame.prototype.addData returns true if all data necessary to + // parse the frame was available. It returns false if we are waiting for + // more data to come in on the wire. + if (!frame.addData(this.bufferList)) { this._debug('-- insufficient data for frame'); return; } + + var self = this; + + // Handle possible parsing errors + if (frame.protocolError) { + // Something bad happened.. get rid of this client. + this._debug('-- protocol error'); + process.nextTick(function() { + self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, frame.dropReason); + }); + return; + } + else if (frame.frameTooLarge) { + this._debug('-- frame too large'); + process.nextTick(function() { + self.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, frame.dropReason); + }); + return; + } + + // For now since we don't support extensions, all RSV bits are illegal + if (frame.rsv1 || frame.rsv2 || frame.rsv3) { + this._debug('-- illegal rsv flag'); + process.nextTick(function() { + self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, + 'Unsupported usage of rsv bits without negotiated extension.'); + }); + return; + } + + if (!this.assembleFragments) { + this._debug('-- emitting frame'); + process.nextTick(function() { self.emit('frame', frame); }); + } + + process.nextTick(function() { self.processFrame(frame); }); + + this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + + // If there's data remaining, schedule additional processing, but yield + // for now so that other connections have a chance to have their data + // processed. We use setImmediate here instead of process.nextTick to + // explicitly indicate that we wish for other I/O to be handled first. + if (this.bufferList.length > 0) { + setImmediateImpl(this.receivedDataHandler); + } +}; + +WebSocketConnection.prototype.handleSocketError = function(error) { + this._debug('handleSocketError: %j', error); + if (this.state === STATE_CLOSED) { + // See https://github.com/theturtle32/WebSocket-Node/issues/288 + this._debug(' --- Socket \'error\' after \'close\''); + return; + } + this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL; + this.closeDescription = 'Socket Error: ' + error.syscall + ' ' + error.code; + this.connected = false; + this.state = STATE_CLOSED; + this.fragmentationSize = 0; + if (utils.eventEmitterListenerCount(this, 'error') > 0) { + this.emit('error', error); + } + this.socket.destroy(error); + this._debug.printOutput(); +}; + +WebSocketConnection.prototype.handleSocketEnd = function() { + this._debug('handleSocketEnd: received socket end. state = %s', this.state); + this.receivedEnd = true; + if (this.state === STATE_CLOSED) { + // When using the TLS module, sometimes the socket will emit 'end' + // after it emits 'close'. I don't think that's correct behavior, + // but we should deal with it gracefully by ignoring it. + this._debug(' --- Socket \'end\' after \'close\''); + return; + } + if (this.state !== STATE_PEER_REQUESTED_CLOSE && + this.state !== STATE_ENDING) { + this._debug(' --- UNEXPECTED socket end.'); + this.socket.end(); + } +}; + +WebSocketConnection.prototype.handleSocketClose = function(hadError) { + this._debug('handleSocketClose: received socket close'); + this.socketHadError = hadError; + this.connected = false; + this.state = STATE_CLOSED; + // If closeReasonCode is still set to -1 at this point then we must + // not have received a close frame!! + if (this.closeReasonCode === -1) { + this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL; + this.closeDescription = 'Connection dropped by remote peer.'; + } + this.clearCloseTimer(); + this.clearKeepaliveTimer(); + this.clearGracePeriodTimer(); + if (!this.closeEventEmitted) { + this.closeEventEmitted = true; + this._debug('-- Emitting WebSocketConnection close event'); + this.emit('close', this.closeReasonCode, this.closeDescription); + } +}; + +WebSocketConnection.prototype.handleSocketDrain = function() { + this._debug('handleSocketDrain: socket drain event'); + this.outputBufferFull = false; + this.emit('drain'); +}; + +WebSocketConnection.prototype.handleSocketPause = function() { + this._debug('handleSocketPause: socket pause event'); + this.inputPaused = true; + this.emit('pause'); +}; + +WebSocketConnection.prototype.handleSocketResume = function() { + this._debug('handleSocketResume: socket resume event'); + this.inputPaused = false; + this.emit('resume'); + this.processReceivedData(); +}; + +WebSocketConnection.prototype.pause = function() { + this._debug('pause: pause requested'); + this.socket.pause(); +}; + +WebSocketConnection.prototype.resume = function() { + this._debug('resume: resume requested'); + this.socket.resume(); +}; + +WebSocketConnection.prototype.close = function(reasonCode, description) { + if (this.connected) { + this._debug('close: Initating clean WebSocket close sequence.'); + if ('number' !== typeof reasonCode) { + reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; + } + if (!validateCloseReason(reasonCode)) { + throw new Error('Close code ' + reasonCode + ' is not valid.'); + } + if ('string' !== typeof description) { + description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode]; + } + this.closeReasonCode = reasonCode; + this.closeDescription = description; + this.setCloseTimer(); + this.sendCloseFrame(this.closeReasonCode, this.closeDescription); + this.state = STATE_ENDING; + this.connected = false; + } +}; + +WebSocketConnection.prototype.drop = function(reasonCode, description, skipCloseFrame) { + this._debug('drop'); + if (typeof(reasonCode) !== 'number') { + reasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; + } + + if (typeof(description) !== 'string') { + // If no description is provided, try to look one up based on the + // specified reasonCode. + description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode]; + } + + this._debug('Forcefully dropping connection. skipCloseFrame: %s, code: %d, description: %s', + skipCloseFrame, reasonCode, description + ); + + this.closeReasonCode = reasonCode; + this.closeDescription = description; + this.frameQueue = []; + this.fragmentationSize = 0; + if (!skipCloseFrame) { + this.sendCloseFrame(reasonCode, description); + } + this.connected = false; + this.state = STATE_CLOSED; + this.clearCloseTimer(); + this.clearKeepaliveTimer(); + this.clearGracePeriodTimer(); + + if (!this.closeEventEmitted) { + this.closeEventEmitted = true; + this._debug('Emitting WebSocketConnection close event'); + this.emit('close', this.closeReasonCode, this.closeDescription); + } + + this._debug('Drop: destroying socket'); + this.socket.destroy(); +}; + +WebSocketConnection.prototype.setCloseTimer = function() { + this._debug('setCloseTimer'); + this.clearCloseTimer(); + this._debug('Setting close timer'); + this.waitingForCloseResponse = true; + this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout); +}; + +WebSocketConnection.prototype.clearCloseTimer = function() { + this._debug('clearCloseTimer'); + if (this.closeTimer) { + this._debug('Clearing close timer'); + clearTimeout(this.closeTimer); + this.waitingForCloseResponse = false; + this.closeTimer = null; + } +}; + +WebSocketConnection.prototype.handleCloseTimer = function() { + this._debug('handleCloseTimer'); + this.closeTimer = null; + if (this.waitingForCloseResponse) { + this._debug('Close response not received from client. Forcing socket end.'); + this.waitingForCloseResponse = false; + this.state = STATE_CLOSED; + this.socket.end(); + } +}; + +WebSocketConnection.prototype.processFrame = function(frame) { + this._debug('processFrame'); + this._debug(' -- frame: %s', frame); + + // Any non-control opcode besides 0x00 (continuation) received in the + // middle of a fragmented message is illegal. + if (this.frameQueue.length !== 0 && (frame.opcode > 0x00 && frame.opcode < 0x08)) { + this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, + 'Illegal frame opcode 0x' + frame.opcode.toString(16) + ' ' + + 'received in middle of fragmented message.'); + return; + } + + switch(frame.opcode) { + case 0x02: // WebSocketFrame.BINARY_FRAME + this._debug('-- Binary Frame'); + if (this.assembleFragments) { + if (frame.fin) { + // Complete single-frame message received + this._debug('---- Emitting \'message\' event'); + this.emit('message', { + type: 'binary', + binaryData: frame.binaryPayload + }); + } + else { + // beginning of a fragmented message + this.frameQueue.push(frame); + this.fragmentationSize = frame.length; + } + } + break; + case 0x01: // WebSocketFrame.TEXT_FRAME + this._debug('-- Text Frame'); + if (this.assembleFragments) { + if (frame.fin) { + if (!Validation.isValidUTF8(frame.binaryPayload)) { + this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA, + 'Invalid UTF-8 Data Received'); + return; + } + // Complete single-frame message received + this._debug('---- Emitting \'message\' event'); + this.emit('message', { + type: 'utf8', + utf8Data: frame.binaryPayload.toString('utf8') + }); + } + else { + // beginning of a fragmented message + this.frameQueue.push(frame); + this.fragmentationSize = frame.length; + } + } + break; + case 0x00: // WebSocketFrame.CONTINUATION + this._debug('-- Continuation Frame'); + if (this.assembleFragments) { + if (this.frameQueue.length === 0) { + this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, + 'Unexpected Continuation Frame'); + return; + } + + this.fragmentationSize += frame.length; + + if (this.fragmentationSize > this.maxReceivedMessageSize) { + this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, + 'Maximum message size exceeded.'); + return; + } + + this.frameQueue.push(frame); + + if (frame.fin) { + // end of fragmented message, so we process the whole + // message now. We also have to decode the utf-8 data + // for text frames after combining all the fragments. + var bytesCopied = 0; + var binaryPayload = bufferAllocUnsafe(this.fragmentationSize); + var opcode = this.frameQueue[0].opcode; + this.frameQueue.forEach(function (currentFrame) { + currentFrame.binaryPayload.copy(binaryPayload, bytesCopied); + bytesCopied += currentFrame.binaryPayload.length; + }); + this.frameQueue = []; + this.fragmentationSize = 0; + + switch (opcode) { + case 0x02: // WebSocketOpcode.BINARY_FRAME + this.emit('message', { + type: 'binary', + binaryData: binaryPayload + }); + break; + case 0x01: // WebSocketOpcode.TEXT_FRAME + if (!Validation.isValidUTF8(binaryPayload)) { + this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA, + 'Invalid UTF-8 Data Received'); + return; + } + this.emit('message', { + type: 'utf8', + utf8Data: binaryPayload.toString('utf8') + }); + break; + default: + this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, + 'Unexpected first opcode in fragmentation sequence: 0x' + opcode.toString(16)); + return; + } + } + } + break; + case 0x09: // WebSocketFrame.PING + this._debug('-- Ping Frame'); + + if (this._pingListenerCount > 0) { + // logic to emit the ping frame: this is only done when a listener is known to exist + // Expose a function allowing the user to override the default ping() behavior + var cancelled = false; + var cancel = function() { + cancelled = true; + }; + this.emit('ping', cancel, frame.binaryPayload); + + // Only send a pong if the client did not indicate that he would like to cancel + if (!cancelled) { + this.pong(frame.binaryPayload); + } + } + else { + this.pong(frame.binaryPayload); + } + + break; + case 0x0A: // WebSocketFrame.PONG + this._debug('-- Pong Frame'); + this.emit('pong', frame.binaryPayload); + break; + case 0x08: // WebSocketFrame.CONNECTION_CLOSE + this._debug('-- Close Frame'); + if (this.waitingForCloseResponse) { + // Got response to our request to close the connection. + // Close is complete, so we just hang up. + this._debug('---- Got close response from peer. Completing closing handshake.'); + this.clearCloseTimer(); + this.waitingForCloseResponse = false; + this.state = STATE_CLOSED; + this.socket.end(); + return; + } + + this._debug('---- Closing handshake initiated by peer.'); + // Got request from other party to close connection. + // Send back acknowledgement and then hang up. + this.state = STATE_PEER_REQUESTED_CLOSE; + var respondCloseReasonCode; + + // Make sure the close reason provided is legal according to + // the protocol spec. Providing no close status is legal. + // WebSocketFrame sets closeStatus to -1 by default, so if it + // is still -1, then no status was provided. + if (frame.invalidCloseFrameLength) { + this.closeReasonCode = 1005; // 1005 = No reason provided. + respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; + } + else if (frame.closeStatus === -1 || validateCloseReason(frame.closeStatus)) { + this.closeReasonCode = frame.closeStatus; + respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; + } + else { + this.closeReasonCode = frame.closeStatus; + respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; + } + + // If there is a textual description in the close frame, extract it. + if (frame.binaryPayload.length > 1) { + if (!Validation.isValidUTF8(frame.binaryPayload)) { + this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA, + 'Invalid UTF-8 Data Received'); + return; + } + this.closeDescription = frame.binaryPayload.toString('utf8'); + } + else { + this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode]; + } + this._debug( + '------ Remote peer %s - code: %d - %s - close frame payload length: %d', + this.remoteAddress, this.closeReasonCode, + this.closeDescription, frame.length + ); + this._debug('------ responding to remote peer\'s close request.'); + this.sendCloseFrame(respondCloseReasonCode, null); + this.connected = false; + break; + default: + this._debug('-- Unrecognized Opcode %d', frame.opcode); + this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, + 'Unrecognized Opcode: 0x' + frame.opcode.toString(16)); + break; + } +}; + +WebSocketConnection.prototype.send = function(data, cb) { + this._debug('send'); + if (Buffer.isBuffer(data)) { + this.sendBytes(data, cb); + } + else if (typeof(data['toString']) === 'function') { + this.sendUTF(data, cb); + } + else { + throw new Error('Data provided must either be a Node Buffer or implement toString()'); + } +}; + +WebSocketConnection.prototype.sendUTF = function(data, cb) { + data = bufferFromString(data.toString(), 'utf8'); + this._debug('sendUTF: %d bytes', data.length); + var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME + frame.binaryPayload = data; + this.fragmentAndSend(frame, cb); +}; + +WebSocketConnection.prototype.sendBytes = function(data, cb) { + this._debug('sendBytes'); + if (!Buffer.isBuffer(data)) { + throw new Error('You must pass a Node Buffer object to WebSocketConnection.prototype.sendBytes()'); + } + var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME + frame.binaryPayload = data; + this.fragmentAndSend(frame, cb); +}; + +WebSocketConnection.prototype.ping = function(data) { + this._debug('ping'); + var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + frame.opcode = 0x09; // WebSocketOpcode.PING + frame.fin = true; + if (data) { + if (!Buffer.isBuffer(data)) { + data = bufferFromString(data.toString(), 'utf8'); + } + if (data.length > 125) { + this._debug('WebSocket: Data for ping is longer than 125 bytes. Truncating.'); + data = data.slice(0,124); + } + frame.binaryPayload = data; + } + this.sendFrame(frame); +}; + +// Pong frames have to echo back the contents of the data portion of the +// ping frame exactly, byte for byte. +WebSocketConnection.prototype.pong = function(binaryPayload) { + this._debug('pong'); + var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + frame.opcode = 0x0A; // WebSocketOpcode.PONG + if (Buffer.isBuffer(binaryPayload) && binaryPayload.length > 125) { + this._debug('WebSocket: Data for pong is longer than 125 bytes. Truncating.'); + binaryPayload = binaryPayload.slice(0,124); + } + frame.binaryPayload = binaryPayload; + frame.fin = true; + this.sendFrame(frame); +}; + +WebSocketConnection.prototype.fragmentAndSend = function(frame, cb) { + this._debug('fragmentAndSend'); + if (frame.opcode > 0x07) { + throw new Error('You cannot fragment control frames.'); + } + + var threshold = this.config.fragmentationThreshold; + var length = frame.binaryPayload.length; + + // Send immediately if fragmentation is disabled or the message is not + // larger than the fragmentation threshold. + if (!this.config.fragmentOutgoingMessages || (frame.binaryPayload && length <= threshold)) { + frame.fin = true; + this.sendFrame(frame, cb); + return; + } + + var numFragments = Math.ceil(length / threshold); + var sentFragments = 0; + var sentCallback = function fragmentSentCallback(err) { + if (err) { + if (typeof cb === 'function') { + // pass only the first error + cb(err); + cb = null; + } + return; + } + ++sentFragments; + if ((sentFragments === numFragments) && (typeof cb === 'function')) { + cb(); + } + }; + for (var i=1; i <= numFragments; i++) { + var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + + // continuation opcode except for first frame. + currentFrame.opcode = (i === 1) ? frame.opcode : 0x00; + + // fin set on last frame only + currentFrame.fin = (i === numFragments); + + // length is likely to be shorter on the last fragment + var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold; + var sliceStart = threshold * (i-1); + + // Slice the right portion of the original payload + currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength); + + this.sendFrame(currentFrame, sentCallback); + } +}; + +WebSocketConnection.prototype.sendCloseFrame = function(reasonCode, description, cb) { + if (typeof(reasonCode) !== 'number') { + reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; + } + + this._debug('sendCloseFrame state: %s, reasonCode: %d, description: %s', this.state, reasonCode, description); + + if (this.state !== STATE_OPEN && this.state !== STATE_PEER_REQUESTED_CLOSE) { return; } + + var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); + frame.fin = true; + frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE + frame.closeStatus = reasonCode; + if (typeof(description) === 'string') { + frame.binaryPayload = bufferFromString(description, 'utf8'); + } + + this.sendFrame(frame, cb); + this.socket.end(); +}; + +WebSocketConnection.prototype.sendFrame = function(frame, cb) { + this._debug('sendFrame'); + frame.mask = this.maskOutgoingPackets; + var flushed = this.socket.write(frame.toBuffer(), cb); + this.outputBufferFull = !flushed; + return flushed; +}; + +module.exports = WebSocketConnection; + + + +function instrumentSocketForDebugging(connection, socket) { + /* jshint loopfunc: true */ + if (!connection._debug.enabled) { return; } + + var originalSocketEmit = socket.emit; + socket.emit = function(event) { + connection._debug('||| Socket Event \'%s\'', event); + originalSocketEmit.apply(this, arguments); + }; + + for (var key in socket) { + if ('function' !== typeof(socket[key])) { continue; } + if (['emit'].indexOf(key) !== -1) { continue; } + (function(key) { + var original = socket[key]; + if (key === 'on') { + socket[key] = function proxyMethod__EventEmitter__On() { + connection._debug('||| Socket method called: %s (%s)', key, arguments[0]); + return original.apply(this, arguments); + }; + return; + } + socket[key] = function proxyMethod() { + connection._debug('||| Socket method called: %s', key); + return original.apply(this, arguments); + }; + })(key); + } +}