11132Smax.romanov@nginx.com/************************************************************************ 21132Smax.romanov@nginx.com * Copyright 2010-2015 Brian McKelvey. 31132Smax.romanov@nginx.com * 41132Smax.romanov@nginx.com * Licensed under the Apache License, Version 2.0 (the "License"); 51132Smax.romanov@nginx.com * you may not use this file except in compliance with the License. 61132Smax.romanov@nginx.com * You may obtain a copy of the License at 71132Smax.romanov@nginx.com * 81132Smax.romanov@nginx.com * http://www.apache.org/licenses/LICENSE-2.0 91132Smax.romanov@nginx.com * 101132Smax.romanov@nginx.com * Unless required by applicable law or agreed to in writing, software 111132Smax.romanov@nginx.com * distributed under the License is distributed on an "AS IS" BASIS, 121132Smax.romanov@nginx.com * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 131132Smax.romanov@nginx.com * See the License for the specific language governing permissions and 141132Smax.romanov@nginx.com * limitations under the License. 151132Smax.romanov@nginx.com ***********************************************************************/ 161132Smax.romanov@nginx.com 171132Smax.romanov@nginx.comvar util = require('util'); 181132Smax.romanov@nginx.comvar utils = require('./utils'); 191132Smax.romanov@nginx.comvar unit_lib = require('./build/Release/unit-http'); 201132Smax.romanov@nginx.comvar EventEmitter = require('events').EventEmitter; 211132Smax.romanov@nginx.comvar WebSocketFrame = require('./websocket_frame'); 221132Smax.romanov@nginx.comvar bufferAllocUnsafe = utils.bufferAllocUnsafe; 231132Smax.romanov@nginx.comvar bufferFromString = utils.bufferFromString; 241132Smax.romanov@nginx.com 251132Smax.romanov@nginx.com// Connected, fully-open, ready to send and receive frames 261132Smax.romanov@nginx.comconst STATE_OPEN = 'open'; 271132Smax.romanov@nginx.com// Received a close frame from the remote peer 281132Smax.romanov@nginx.comconst STATE_PEER_REQUESTED_CLOSE = 'peer_requested_close'; 291132Smax.romanov@nginx.com// Sent close frame to remote peer. No further data can be sent. 301132Smax.romanov@nginx.comconst STATE_ENDING = 'ending'; 311132Smax.romanov@nginx.com// Connection is fully closed. No further data can be sent or received. 321132Smax.romanov@nginx.comconst STATE_CLOSED = 'closed'; 331132Smax.romanov@nginx.com 341132Smax.romanov@nginx.comvar idCounter = 0; 351132Smax.romanov@nginx.com 361132Smax.romanov@nginx.comfunction WebSocketConnection(socket, extensions, protocol, maskOutgoingPackets, config) { 371132Smax.romanov@nginx.com this._debug = utils.BufferingLogger('websocket:connection', ++idCounter); 381132Smax.romanov@nginx.com this._debug('constructor'); 39*2617Szelenkov@nginx.com 401132Smax.romanov@nginx.com if (this._debug.enabled) { 411132Smax.romanov@nginx.com instrumentSocketForDebugging(this, socket); 421132Smax.romanov@nginx.com } 43*2617Szelenkov@nginx.com 441132Smax.romanov@nginx.com // Superclass Constructor 451132Smax.romanov@nginx.com EventEmitter.call(this); 461132Smax.romanov@nginx.com 471132Smax.romanov@nginx.com this._pingListenerCount = 0; 481132Smax.romanov@nginx.com this.on('newListener', function(ev) { 491132Smax.romanov@nginx.com if (ev === 'ping'){ 501132Smax.romanov@nginx.com this._pingListenerCount++; 511132Smax.romanov@nginx.com } 521132Smax.romanov@nginx.com }).on('removeListener', function(ev) { 531132Smax.romanov@nginx.com if (ev === 'ping') { 541132Smax.romanov@nginx.com this._pingListenerCount--; 551132Smax.romanov@nginx.com } 561132Smax.romanov@nginx.com }); 571132Smax.romanov@nginx.com 581132Smax.romanov@nginx.com this.config = config; 591132Smax.romanov@nginx.com this.socket = socket; 601132Smax.romanov@nginx.com this.protocol = protocol; 611132Smax.romanov@nginx.com this.extensions = extensions; 621132Smax.romanov@nginx.com this.remoteAddress = socket.remoteAddress; 631132Smax.romanov@nginx.com this.closeReasonCode = -1; 641132Smax.romanov@nginx.com this.closeDescription = null; 651132Smax.romanov@nginx.com this.closeEventEmitted = false; 661132Smax.romanov@nginx.com 671132Smax.romanov@nginx.com // We have to mask outgoing packets if we're acting as a WebSocket client. 681132Smax.romanov@nginx.com this.maskOutgoingPackets = maskOutgoingPackets; 691132Smax.romanov@nginx.com 701132Smax.romanov@nginx.com this.fragmentationSize = 0; // data received so far... 711132Smax.romanov@nginx.com this.frameQueue = []; 721132Smax.romanov@nginx.com 731132Smax.romanov@nginx.com // Various bits of connection state 741132Smax.romanov@nginx.com this.connected = true; 751132Smax.romanov@nginx.com this.state = STATE_OPEN; 761132Smax.romanov@nginx.com this.waitingForCloseResponse = false; 771132Smax.romanov@nginx.com // Received TCP FIN, socket's readable stream is finished. 781132Smax.romanov@nginx.com this.receivedEnd = false; 791132Smax.romanov@nginx.com 801132Smax.romanov@nginx.com this.closeTimeout = this.config.closeTimeout; 811132Smax.romanov@nginx.com this.assembleFragments = this.config.assembleFragments; 821132Smax.romanov@nginx.com this.maxReceivedMessageSize = this.config.maxReceivedMessageSize; 831132Smax.romanov@nginx.com 841132Smax.romanov@nginx.com this.outputBufferFull = false; 851132Smax.romanov@nginx.com this.inputPaused = false; 861132Smax.romanov@nginx.com this._closeTimerHandler = this.handleCloseTimer.bind(this); 871132Smax.romanov@nginx.com 881132Smax.romanov@nginx.com // Disable nagle algorithm? 891132Smax.romanov@nginx.com this.socket.setNoDelay(this.config.disableNagleAlgorithm); 901132Smax.romanov@nginx.com 911132Smax.romanov@nginx.com // Make sure there is no socket inactivity timeout 921132Smax.romanov@nginx.com this.socket.setTimeout(0); 931132Smax.romanov@nginx.com 941132Smax.romanov@nginx.com // The HTTP Client seems to subscribe to socket error events 951132Smax.romanov@nginx.com // and re-dispatch them in such a way that doesn't make sense 961132Smax.romanov@nginx.com // for users of our client, so we want to make sure nobody 971132Smax.romanov@nginx.com // else is listening for error events on the socket besides us. 981132Smax.romanov@nginx.com this.socket.removeAllListeners('error'); 991132Smax.romanov@nginx.com 1001132Smax.romanov@nginx.com this._set_sock(this.socket); 1011132Smax.romanov@nginx.com} 1021132Smax.romanov@nginx.com 1031132Smax.romanov@nginx.comWebSocketConnection.prototype._set_sock = unit_lib.websocket_set_sock; 1041132Smax.romanov@nginx.comWebSocketConnection.prototype._end = unit_lib.response_end; 1051132Smax.romanov@nginx.com 1061132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_NORMAL = 1000; 1071132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_GOING_AWAY = 1001; 1081132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR = 1002; 1091132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT = 1003; 1101132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_RESERVED = 1004; // Reserved value. Undefined meaning. 1111132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_NOT_PROVIDED = 1005; // Not to be used on the wire 1121132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_ABNORMAL = 1006; // Not to be used on the wire 1131132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_INVALID_DATA = 1007; 1141132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_POLICY_VIOLATION = 1008; 1151132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG = 1009; 1161132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_EXTENSION_REQUIRED = 1010; 1171132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_INTERNAL_SERVER_ERROR = 1011; 1181132Smax.romanov@nginx.comWebSocketConnection.CLOSE_REASON_TLS_HANDSHAKE_FAILED = 1015; // Not to be used on the wire 1191132Smax.romanov@nginx.com 1201132Smax.romanov@nginx.comWebSocketConnection.CLOSE_DESCRIPTIONS = { 1211132Smax.romanov@nginx.com 1000: 'Normal connection closure', 1221132Smax.romanov@nginx.com 1001: 'Remote peer is going away', 1231132Smax.romanov@nginx.com 1002: 'Protocol error', 1241132Smax.romanov@nginx.com 1003: 'Unprocessable input', 1251132Smax.romanov@nginx.com 1004: 'Reserved', 1261132Smax.romanov@nginx.com 1005: 'Reason not provided', 1271132Smax.romanov@nginx.com 1006: 'Abnormal closure, no further detail available', 1281132Smax.romanov@nginx.com 1007: 'Invalid data received', 1291132Smax.romanov@nginx.com 1008: 'Policy violation', 1301132Smax.romanov@nginx.com 1009: 'Message too big', 1311132Smax.romanov@nginx.com 1010: 'Extension requested by client is required', 1321132Smax.romanov@nginx.com 1011: 'Internal Server Error', 1331132Smax.romanov@nginx.com 1015: 'TLS Handshake Failed' 1341132Smax.romanov@nginx.com}; 1351132Smax.romanov@nginx.com 1361132Smax.romanov@nginx.comfunction validateCloseReason(code) { 1371132Smax.romanov@nginx.com if (code < 1000) { 1381132Smax.romanov@nginx.com // Status codes in the range 0-999 are not used 1391132Smax.romanov@nginx.com return false; 1401132Smax.romanov@nginx.com } 1411132Smax.romanov@nginx.com if (code >= 1000 && code <= 2999) { 1421132Smax.romanov@nginx.com // Codes from 1000 - 2999 are reserved for use by the protocol. Only 1431132Smax.romanov@nginx.com // a few codes are defined, all others are currently illegal. 1441132Smax.romanov@nginx.com return [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].indexOf(code) !== -1; 1451132Smax.romanov@nginx.com } 1461132Smax.romanov@nginx.com if (code >= 3000 && code <= 3999) { 1471132Smax.romanov@nginx.com // Reserved for use by libraries, frameworks, and applications. 1481132Smax.romanov@nginx.com // Should be registered with IANA. Interpretation of these codes is 1491132Smax.romanov@nginx.com // undefined by the WebSocket protocol. 1501132Smax.romanov@nginx.com return true; 1511132Smax.romanov@nginx.com } 1521132Smax.romanov@nginx.com if (code >= 4000 && code <= 4999) { 1531132Smax.romanov@nginx.com // Reserved for private use. Interpretation of these codes is 1541132Smax.romanov@nginx.com // undefined by the WebSocket protocol. 1551132Smax.romanov@nginx.com return true; 1561132Smax.romanov@nginx.com } 1571132Smax.romanov@nginx.com if (code >= 5000) { 1581132Smax.romanov@nginx.com return false; 1591132Smax.romanov@nginx.com } 1601132Smax.romanov@nginx.com} 1611132Smax.romanov@nginx.com 1621132Smax.romanov@nginx.comutil.inherits(WebSocketConnection, EventEmitter); 1631132Smax.romanov@nginx.com 1641132Smax.romanov@nginx.comWebSocketConnection.prototype._addSocketEventListeners = function() { 1651132Smax.romanov@nginx.com this.socket.on('error', this.handleSocketError.bind(this)); 1661132Smax.romanov@nginx.com this.socket.on('end', this.handleSocketEnd.bind(this)); 1671132Smax.romanov@nginx.com this.socket.on('close', this.handleSocketClose.bind(this)); 1681132Smax.romanov@nginx.com}; 1691132Smax.romanov@nginx.com 1701132Smax.romanov@nginx.comWebSocketConnection.prototype.handleSocketError = function(error) { 1711132Smax.romanov@nginx.com this._debug('handleSocketError: %j', error); 1721132Smax.romanov@nginx.com if (this.state === STATE_CLOSED) { 1731132Smax.romanov@nginx.com // See https://github.com/theturtle32/WebSocket-Node/issues/288 1741132Smax.romanov@nginx.com this._debug(' --- Socket \'error\' after \'close\''); 1751132Smax.romanov@nginx.com return; 1761132Smax.romanov@nginx.com } 1771132Smax.romanov@nginx.com this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL; 1781132Smax.romanov@nginx.com this.closeDescription = 'Socket Error: ' + error.syscall + ' ' + error.code; 1791132Smax.romanov@nginx.com this.connected = false; 1801132Smax.romanov@nginx.com this.state = STATE_CLOSED; 1811132Smax.romanov@nginx.com this.fragmentationSize = 0; 1821132Smax.romanov@nginx.com if (utils.eventEmitterListenerCount(this, 'error') > 0) { 1831132Smax.romanov@nginx.com this.emit('error', error); 1841132Smax.romanov@nginx.com } 1851132Smax.romanov@nginx.com this.socket.destroy(error); 1861132Smax.romanov@nginx.com this._debug.printOutput(); 1871132Smax.romanov@nginx.com 1881132Smax.romanov@nginx.com this._end(); 1891132Smax.romanov@nginx.com}; 1901132Smax.romanov@nginx.com 1911132Smax.romanov@nginx.comWebSocketConnection.prototype.handleSocketEnd = function() { 1921132Smax.romanov@nginx.com this._debug('handleSocketEnd: received socket end. state = %s', this.state); 1931132Smax.romanov@nginx.com this.receivedEnd = true; 1941132Smax.romanov@nginx.com if (this.state === STATE_CLOSED) { 1951132Smax.romanov@nginx.com // When using the TLS module, sometimes the socket will emit 'end' 1961132Smax.romanov@nginx.com // after it emits 'close'. I don't think that's correct behavior, 1971132Smax.romanov@nginx.com // but we should deal with it gracefully by ignoring it. 1981132Smax.romanov@nginx.com this._debug(' --- Socket \'end\' after \'close\''); 1991132Smax.romanov@nginx.com return; 2001132Smax.romanov@nginx.com } 2011132Smax.romanov@nginx.com if (this.state !== STATE_PEER_REQUESTED_CLOSE && 2021132Smax.romanov@nginx.com this.state !== STATE_ENDING) { 2031132Smax.romanov@nginx.com this._debug(' --- UNEXPECTED socket end.'); 2041132Smax.romanov@nginx.com this.socket.end(); 2051132Smax.romanov@nginx.com 2061132Smax.romanov@nginx.com this._end(); 2071132Smax.romanov@nginx.com } 2081132Smax.romanov@nginx.com}; 2091132Smax.romanov@nginx.com 2101132Smax.romanov@nginx.comWebSocketConnection.prototype.handleSocketClose = function(hadError) { 2111132Smax.romanov@nginx.com this._debug('handleSocketClose: received socket close'); 2121132Smax.romanov@nginx.com this.socketHadError = hadError; 2131132Smax.romanov@nginx.com this.connected = false; 2141132Smax.romanov@nginx.com this.state = STATE_CLOSED; 2151132Smax.romanov@nginx.com // If closeReasonCode is still set to -1 at this point then we must 2161132Smax.romanov@nginx.com // not have received a close frame!! 2171132Smax.romanov@nginx.com if (this.closeReasonCode === -1) { 2181132Smax.romanov@nginx.com this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL; 2191132Smax.romanov@nginx.com this.closeDescription = 'Connection dropped by remote peer.'; 2201132Smax.romanov@nginx.com } 2211132Smax.romanov@nginx.com this.clearCloseTimer(); 2221132Smax.romanov@nginx.com if (!this.closeEventEmitted) { 2231132Smax.romanov@nginx.com this.closeEventEmitted = true; 2241132Smax.romanov@nginx.com this._debug('-- Emitting WebSocketConnection close event'); 2251132Smax.romanov@nginx.com this.emit('close', this.closeReasonCode, this.closeDescription); 2261132Smax.romanov@nginx.com } 2271132Smax.romanov@nginx.com}; 2281132Smax.romanov@nginx.com 2291132Smax.romanov@nginx.comWebSocketConnection.prototype.close = function(reasonCode, description) { 2301132Smax.romanov@nginx.com if (this.connected) { 2311132Smax.romanov@nginx.com this._debug('close: Initating clean WebSocket close sequence.'); 2321132Smax.romanov@nginx.com if ('number' !== typeof reasonCode) { 2331132Smax.romanov@nginx.com reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; 2341132Smax.romanov@nginx.com } 2351132Smax.romanov@nginx.com if (!validateCloseReason(reasonCode)) { 2361132Smax.romanov@nginx.com throw new Error('Close code ' + reasonCode + ' is not valid.'); 2371132Smax.romanov@nginx.com } 2381132Smax.romanov@nginx.com if ('string' !== typeof description) { 2391132Smax.romanov@nginx.com description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode]; 2401132Smax.romanov@nginx.com } 2411132Smax.romanov@nginx.com this.closeReasonCode = reasonCode; 2421132Smax.romanov@nginx.com this.closeDescription = description; 2431132Smax.romanov@nginx.com this.setCloseTimer(); 2441132Smax.romanov@nginx.com this.sendCloseFrame(this.closeReasonCode, this.closeDescription); 2451132Smax.romanov@nginx.com this.state = STATE_ENDING; 2461132Smax.romanov@nginx.com this.connected = false; 2471132Smax.romanov@nginx.com } 2481132Smax.romanov@nginx.com}; 2491132Smax.romanov@nginx.com 2501132Smax.romanov@nginx.comWebSocketConnection.prototype.drop = function(reasonCode, description, skipCloseFrame) { 2511132Smax.romanov@nginx.com this._debug('drop'); 2521132Smax.romanov@nginx.com if (typeof(reasonCode) !== 'number') { 2531132Smax.romanov@nginx.com reasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; 2541132Smax.romanov@nginx.com } 2551132Smax.romanov@nginx.com 2561132Smax.romanov@nginx.com if (typeof(description) !== 'string') { 2571132Smax.romanov@nginx.com // If no description is provided, try to look one up based on the 2581132Smax.romanov@nginx.com // specified reasonCode. 2591132Smax.romanov@nginx.com description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode]; 2601132Smax.romanov@nginx.com } 2611132Smax.romanov@nginx.com 2621132Smax.romanov@nginx.com this._debug('Forcefully dropping connection. skipCloseFrame: %s, code: %d, description: %s', 2631132Smax.romanov@nginx.com skipCloseFrame, reasonCode, description 2641132Smax.romanov@nginx.com ); 2651132Smax.romanov@nginx.com 2661132Smax.romanov@nginx.com this.closeReasonCode = reasonCode; 2671132Smax.romanov@nginx.com this.closeDescription = description; 2681132Smax.romanov@nginx.com this.frameQueue = []; 2691132Smax.romanov@nginx.com this.fragmentationSize = 0; 2701132Smax.romanov@nginx.com if (!skipCloseFrame) { 2711132Smax.romanov@nginx.com this.sendCloseFrame(reasonCode, description); 2721132Smax.romanov@nginx.com } 2731132Smax.romanov@nginx.com this.connected = false; 2741132Smax.romanov@nginx.com this.state = STATE_CLOSED; 2751132Smax.romanov@nginx.com this.clearCloseTimer(); 2761132Smax.romanov@nginx.com 2771132Smax.romanov@nginx.com if (!this.closeEventEmitted) { 2781132Smax.romanov@nginx.com this.closeEventEmitted = true; 2791132Smax.romanov@nginx.com this._debug('Emitting WebSocketConnection close event'); 2801132Smax.romanov@nginx.com this.emit('close', this.closeReasonCode, this.closeDescription); 2811132Smax.romanov@nginx.com } 2821132Smax.romanov@nginx.com 2831132Smax.romanov@nginx.com this._debug('Drop: destroying socket'); 2841132Smax.romanov@nginx.com this.socket.destroy(); 2851132Smax.romanov@nginx.com 2861132Smax.romanov@nginx.com this._end(); 2871132Smax.romanov@nginx.com}; 2881132Smax.romanov@nginx.com 2891132Smax.romanov@nginx.comWebSocketConnection.prototype.setCloseTimer = function() { 2901132Smax.romanov@nginx.com this._debug('setCloseTimer'); 2911132Smax.romanov@nginx.com this.clearCloseTimer(); 2921132Smax.romanov@nginx.com this._debug('Setting close timer'); 2931132Smax.romanov@nginx.com this.waitingForCloseResponse = true; 2941132Smax.romanov@nginx.com this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout); 2951132Smax.romanov@nginx.com}; 2961132Smax.romanov@nginx.com 2971132Smax.romanov@nginx.comWebSocketConnection.prototype.clearCloseTimer = function() { 2981132Smax.romanov@nginx.com this._debug('clearCloseTimer'); 2991132Smax.romanov@nginx.com if (this.closeTimer) { 3001132Smax.romanov@nginx.com this._debug('Clearing close timer'); 3011132Smax.romanov@nginx.com clearTimeout(this.closeTimer); 3021132Smax.romanov@nginx.com this.waitingForCloseResponse = false; 3031132Smax.romanov@nginx.com this.closeTimer = null; 3041132Smax.romanov@nginx.com } 3051132Smax.romanov@nginx.com}; 3061132Smax.romanov@nginx.com 3071132Smax.romanov@nginx.comWebSocketConnection.prototype.handleCloseTimer = function() { 3081132Smax.romanov@nginx.com this._debug('handleCloseTimer'); 3091132Smax.romanov@nginx.com this.closeTimer = null; 3101132Smax.romanov@nginx.com if (this.waitingForCloseResponse) { 3111132Smax.romanov@nginx.com this._debug('Close response not received from client. Forcing socket end.'); 3121132Smax.romanov@nginx.com this.waitingForCloseResponse = false; 3131132Smax.romanov@nginx.com this.state = STATE_CLOSED; 3141132Smax.romanov@nginx.com this.socket.end(); 3151132Smax.romanov@nginx.com 3161132Smax.romanov@nginx.com this._end(); 3171132Smax.romanov@nginx.com } 3181132Smax.romanov@nginx.com}; 3191132Smax.romanov@nginx.com 3201132Smax.romanov@nginx.comWebSocketConnection.prototype.processFrame = function(frame) { 3211132Smax.romanov@nginx.com if (!this.connected) { 3221132Smax.romanov@nginx.com return; 3231132Smax.romanov@nginx.com } 3241132Smax.romanov@nginx.com 3251132Smax.romanov@nginx.com this._debug('processFrame'); 3261132Smax.romanov@nginx.com this._debug(' -- frame: %s', frame); 3271132Smax.romanov@nginx.com 3281132Smax.romanov@nginx.com // Any non-control opcode besides 0x00 (continuation) received in the 3291132Smax.romanov@nginx.com // middle of a fragmented message is illegal. 3301132Smax.romanov@nginx.com if (this.frameQueue.length !== 0 && (frame.opcode > 0x00 && frame.opcode < 0x08)) { 3311132Smax.romanov@nginx.com this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, 3321132Smax.romanov@nginx.com 'Illegal frame opcode 0x' + frame.opcode.toString(16) + ' ' + 3331132Smax.romanov@nginx.com 'received in middle of fragmented message.'); 3341132Smax.romanov@nginx.com return; 3351132Smax.romanov@nginx.com } 3361132Smax.romanov@nginx.com 3371132Smax.romanov@nginx.com switch(frame.opcode) { 3381132Smax.romanov@nginx.com case 0x02: // WebSocketFrame.BINARY_FRAME 3391132Smax.romanov@nginx.com this._debug('-- Binary Frame'); 3401132Smax.romanov@nginx.com if (this.assembleFragments) { 3411132Smax.romanov@nginx.com if (frame.fin) { 3421132Smax.romanov@nginx.com // Complete single-frame message received 3431132Smax.romanov@nginx.com this._debug('---- Emitting \'message\' event'); 3441132Smax.romanov@nginx.com this.emit('message', { 3451132Smax.romanov@nginx.com type: 'binary', 3461132Smax.romanov@nginx.com binaryData: frame.binaryPayload 3471132Smax.romanov@nginx.com }); 3481132Smax.romanov@nginx.com } 3491132Smax.romanov@nginx.com else { 3501132Smax.romanov@nginx.com // beginning of a fragmented message 3511132Smax.romanov@nginx.com this.frameQueue.push(frame); 3521132Smax.romanov@nginx.com this.fragmentationSize = frame.length; 3531132Smax.romanov@nginx.com } 3541132Smax.romanov@nginx.com } 3551132Smax.romanov@nginx.com break; 3561132Smax.romanov@nginx.com case 0x01: // WebSocketFrame.TEXT_FRAME 3571132Smax.romanov@nginx.com this._debug('-- Text Frame'); 3581132Smax.romanov@nginx.com if (this.assembleFragments) { 3591132Smax.romanov@nginx.com if (frame.fin) { 3601132Smax.romanov@nginx.com // Complete single-frame message received 3611132Smax.romanov@nginx.com this._debug('---- Emitting \'message\' event'); 3621132Smax.romanov@nginx.com this.emit('message', { 3631132Smax.romanov@nginx.com type: 'utf8', 3641132Smax.romanov@nginx.com utf8Data: frame.binaryPayload.toString('utf8') 3651132Smax.romanov@nginx.com }); 3661132Smax.romanov@nginx.com } 3671132Smax.romanov@nginx.com else { 3681132Smax.romanov@nginx.com // beginning of a fragmented message 3691132Smax.romanov@nginx.com this.frameQueue.push(frame); 3701132Smax.romanov@nginx.com this.fragmentationSize = frame.length; 3711132Smax.romanov@nginx.com } 3721132Smax.romanov@nginx.com } 3731132Smax.romanov@nginx.com break; 3741132Smax.romanov@nginx.com case 0x00: // WebSocketFrame.CONTINUATION 3751132Smax.romanov@nginx.com this._debug('-- Continuation Frame'); 3761132Smax.romanov@nginx.com if (this.assembleFragments) { 3771132Smax.romanov@nginx.com if (this.frameQueue.length === 0) { 3781132Smax.romanov@nginx.com this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, 3791132Smax.romanov@nginx.com 'Unexpected Continuation Frame'); 3801132Smax.romanov@nginx.com return; 3811132Smax.romanov@nginx.com } 3821132Smax.romanov@nginx.com 3831132Smax.romanov@nginx.com this.fragmentationSize += frame.length; 3841132Smax.romanov@nginx.com 3851132Smax.romanov@nginx.com if (this.fragmentationSize > this.maxReceivedMessageSize) { 3861132Smax.romanov@nginx.com this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, 3871132Smax.romanov@nginx.com 'Maximum message size exceeded.'); 3881132Smax.romanov@nginx.com return; 3891132Smax.romanov@nginx.com } 3901132Smax.romanov@nginx.com 3911132Smax.romanov@nginx.com this.frameQueue.push(frame); 3921132Smax.romanov@nginx.com 3931132Smax.romanov@nginx.com if (frame.fin) { 3941132Smax.romanov@nginx.com // end of fragmented message, so we process the whole 3951132Smax.romanov@nginx.com // message now. We also have to decode the utf-8 data 3961132Smax.romanov@nginx.com // for text frames after combining all the fragments. 3971132Smax.romanov@nginx.com var bytesCopied = 0; 3981132Smax.romanov@nginx.com var binaryPayload = bufferAllocUnsafe(this.fragmentationSize); 3991132Smax.romanov@nginx.com var opcode = this.frameQueue[0].opcode; 4001132Smax.romanov@nginx.com this.frameQueue.forEach(function (currentFrame) { 4011132Smax.romanov@nginx.com currentFrame.binaryPayload.copy(binaryPayload, bytesCopied); 4021132Smax.romanov@nginx.com bytesCopied += currentFrame.binaryPayload.length; 4031132Smax.romanov@nginx.com }); 4041132Smax.romanov@nginx.com this.frameQueue = []; 4051132Smax.romanov@nginx.com this.fragmentationSize = 0; 4061132Smax.romanov@nginx.com 4071132Smax.romanov@nginx.com switch (opcode) { 4081132Smax.romanov@nginx.com case 0x02: // WebSocketOpcode.BINARY_FRAME 4091132Smax.romanov@nginx.com this.emit('message', { 4101132Smax.romanov@nginx.com type: 'binary', 4111132Smax.romanov@nginx.com binaryData: binaryPayload 4121132Smax.romanov@nginx.com }); 4131132Smax.romanov@nginx.com break; 4141132Smax.romanov@nginx.com case 0x01: // WebSocketOpcode.TEXT_FRAME 4151132Smax.romanov@nginx.com this.emit('message', { 4161132Smax.romanov@nginx.com type: 'utf8', 4171132Smax.romanov@nginx.com utf8Data: binaryPayload.toString('utf8') 4181132Smax.romanov@nginx.com }); 4191132Smax.romanov@nginx.com break; 4201132Smax.romanov@nginx.com default: 4211132Smax.romanov@nginx.com this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, 4221132Smax.romanov@nginx.com 'Unexpected first opcode in fragmentation sequence: 0x' + opcode.toString(16)); 4231132Smax.romanov@nginx.com return; 4241132Smax.romanov@nginx.com } 4251132Smax.romanov@nginx.com } 4261132Smax.romanov@nginx.com } 4271132Smax.romanov@nginx.com break; 4281132Smax.romanov@nginx.com case 0x09: // WebSocketFrame.PING 4291132Smax.romanov@nginx.com this._debug('-- Ping Frame'); 4301132Smax.romanov@nginx.com 4311132Smax.romanov@nginx.com if (this._pingListenerCount > 0) { 4321132Smax.romanov@nginx.com // logic to emit the ping frame: this is only done when a listener is known to exist 4331132Smax.romanov@nginx.com // Expose a function allowing the user to override the default ping() behavior 4341132Smax.romanov@nginx.com var cancelled = false; 435*2617Szelenkov@nginx.com var cancel = function() { 436*2617Szelenkov@nginx.com cancelled = true; 4371132Smax.romanov@nginx.com }; 4381132Smax.romanov@nginx.com this.emit('ping', cancel, frame.binaryPayload); 4391132Smax.romanov@nginx.com 4401132Smax.romanov@nginx.com // Only send a pong if the client did not indicate that he would like to cancel 4411132Smax.romanov@nginx.com if (!cancelled) { 4421132Smax.romanov@nginx.com this.pong(frame.binaryPayload); 4431132Smax.romanov@nginx.com } 4441132Smax.romanov@nginx.com } 4451132Smax.romanov@nginx.com else { 4461132Smax.romanov@nginx.com this.pong(frame.binaryPayload); 4471132Smax.romanov@nginx.com } 4481132Smax.romanov@nginx.com 4491132Smax.romanov@nginx.com break; 4501132Smax.romanov@nginx.com case 0x0A: // WebSocketFrame.PONG 4511132Smax.romanov@nginx.com this._debug('-- Pong Frame'); 4521132Smax.romanov@nginx.com this.emit('pong', frame.binaryPayload); 4531132Smax.romanov@nginx.com break; 4541132Smax.romanov@nginx.com case 0x08: // WebSocketFrame.CONNECTION_CLOSE 4551132Smax.romanov@nginx.com this._debug('-- Close Frame'); 4561132Smax.romanov@nginx.com if (this.waitingForCloseResponse) { 4571132Smax.romanov@nginx.com // Got response to our request to close the connection. 4581132Smax.romanov@nginx.com // Close is complete, so we just hang up. 4591132Smax.romanov@nginx.com this._debug('---- Got close response from peer. Completing closing handshake.'); 4601132Smax.romanov@nginx.com this.clearCloseTimer(); 4611132Smax.romanov@nginx.com this.waitingForCloseResponse = false; 4621132Smax.romanov@nginx.com this.state = STATE_CLOSED; 4631132Smax.romanov@nginx.com this.socket.end(); 4641132Smax.romanov@nginx.com 4651132Smax.romanov@nginx.com this._end(); 4661132Smax.romanov@nginx.com return; 4671132Smax.romanov@nginx.com } 4681132Smax.romanov@nginx.com 4691132Smax.romanov@nginx.com this._debug('---- Closing handshake initiated by peer.'); 4701132Smax.romanov@nginx.com // Got request from other party to close connection. 4711132Smax.romanov@nginx.com // Send back acknowledgement and then hang up. 4721132Smax.romanov@nginx.com this.state = STATE_PEER_REQUESTED_CLOSE; 4731132Smax.romanov@nginx.com var respondCloseReasonCode; 4741132Smax.romanov@nginx.com 4751132Smax.romanov@nginx.com // Make sure the close reason provided is legal according to 4761132Smax.romanov@nginx.com // the protocol spec. Providing no close status is legal. 4771132Smax.romanov@nginx.com // WebSocketFrame sets closeStatus to -1 by default, so if it 4781132Smax.romanov@nginx.com // is still -1, then no status was provided. 4791132Smax.romanov@nginx.com if (frame.invalidCloseFrameLength) { 4801132Smax.romanov@nginx.com this.closeReasonCode = 1005; // 1005 = No reason provided. 4811132Smax.romanov@nginx.com respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; 4821132Smax.romanov@nginx.com } 4831132Smax.romanov@nginx.com else if (frame.closeStatus === -1 || validateCloseReason(frame.closeStatus)) { 4841132Smax.romanov@nginx.com this.closeReasonCode = frame.closeStatus; 4851132Smax.romanov@nginx.com respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; 4861132Smax.romanov@nginx.com } 4871132Smax.romanov@nginx.com else { 4881132Smax.romanov@nginx.com this.closeReasonCode = frame.closeStatus; 4891132Smax.romanov@nginx.com respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; 4901132Smax.romanov@nginx.com } 4911132Smax.romanov@nginx.com 4921132Smax.romanov@nginx.com // If there is a textual description in the close frame, extract it. 4931132Smax.romanov@nginx.com if (frame.binaryPayload.length > 1) { 4941132Smax.romanov@nginx.com this.closeDescription = frame.binaryPayload.toString('utf8'); 4951132Smax.romanov@nginx.com } 4961132Smax.romanov@nginx.com else { 4971132Smax.romanov@nginx.com this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode]; 4981132Smax.romanov@nginx.com } 4991132Smax.romanov@nginx.com this._debug( 5001132Smax.romanov@nginx.com '------ Remote peer %s - code: %d - %s - close frame payload length: %d', 5011132Smax.romanov@nginx.com this.remoteAddress, this.closeReasonCode, 5021132Smax.romanov@nginx.com this.closeDescription, frame.length 5031132Smax.romanov@nginx.com ); 5041132Smax.romanov@nginx.com this._debug('------ responding to remote peer\'s close request.'); 5051132Smax.romanov@nginx.com this.drop(respondCloseReasonCode, null); 5061132Smax.romanov@nginx.com this.connected = false; 5071132Smax.romanov@nginx.com break; 5081132Smax.romanov@nginx.com default: 5091132Smax.romanov@nginx.com this._debug('-- Unrecognized Opcode %d', frame.opcode); 5101132Smax.romanov@nginx.com this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, 5111132Smax.romanov@nginx.com 'Unrecognized Opcode: 0x' + frame.opcode.toString(16)); 5121132Smax.romanov@nginx.com break; 5131132Smax.romanov@nginx.com } 5141132Smax.romanov@nginx.com}; 5151132Smax.romanov@nginx.com 5161132Smax.romanov@nginx.comWebSocketConnection.prototype.send = function(data, cb) { 5171132Smax.romanov@nginx.com this._debug('send'); 5181132Smax.romanov@nginx.com if (Buffer.isBuffer(data)) { 5191132Smax.romanov@nginx.com this.sendBytes(data, cb); 5201132Smax.romanov@nginx.com } 5211132Smax.romanov@nginx.com else if (typeof(data['toString']) === 'function') { 5221132Smax.romanov@nginx.com this.sendUTF(data, cb); 5231132Smax.romanov@nginx.com } 5241132Smax.romanov@nginx.com else { 5251132Smax.romanov@nginx.com throw new Error('Data provided must either be a Node Buffer or implement toString()'); 5261132Smax.romanov@nginx.com } 5271132Smax.romanov@nginx.com}; 5281132Smax.romanov@nginx.com 5291132Smax.romanov@nginx.comWebSocketConnection.prototype.sendUTF = function(data, cb) { 5301132Smax.romanov@nginx.com data = bufferFromString(data.toString(), 'utf8'); 5311132Smax.romanov@nginx.com this._debug('sendUTF: %d bytes', data.length); 5321132Smax.romanov@nginx.com 5331132Smax.romanov@nginx.com var frame = new WebSocketFrame(); 5341132Smax.romanov@nginx.com frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME 5351132Smax.romanov@nginx.com frame.binaryPayload = data; 5361132Smax.romanov@nginx.com 5371132Smax.romanov@nginx.com this.fragmentAndSend(frame, cb); 5381132Smax.romanov@nginx.com}; 5391132Smax.romanov@nginx.com 5401132Smax.romanov@nginx.comWebSocketConnection.prototype.sendBytes = function(data, cb) { 5411132Smax.romanov@nginx.com this._debug('sendBytes'); 5421132Smax.romanov@nginx.com if (!Buffer.isBuffer(data)) { 5431132Smax.romanov@nginx.com throw new Error('You must pass a Node Buffer object to WebSocketConnection.prototype.sendBytes()'); 5441132Smax.romanov@nginx.com } 5451132Smax.romanov@nginx.com 5461132Smax.romanov@nginx.com var frame = new WebSocketFrame(); 5471132Smax.romanov@nginx.com frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME 5481132Smax.romanov@nginx.com frame.binaryPayload = data; 5491132Smax.romanov@nginx.com 5501132Smax.romanov@nginx.com this.fragmentAndSend(frame, cb); 5511132Smax.romanov@nginx.com}; 5521132Smax.romanov@nginx.com 5531132Smax.romanov@nginx.comWebSocketConnection.prototype.ping = function(data) { 5541132Smax.romanov@nginx.com this._debug('ping'); 5551132Smax.romanov@nginx.com 5561132Smax.romanov@nginx.com var frame = new WebSocketFrame(); 5571132Smax.romanov@nginx.com frame.opcode = 0x09; // WebSocketOpcode.PING 5581132Smax.romanov@nginx.com frame.fin = true; 5591132Smax.romanov@nginx.com 5601132Smax.romanov@nginx.com if (data) { 5611132Smax.romanov@nginx.com if (!Buffer.isBuffer(data)) { 5621132Smax.romanov@nginx.com data = bufferFromString(data.toString(), 'utf8'); 5631132Smax.romanov@nginx.com } 5641132Smax.romanov@nginx.com if (data.length > 125) { 5651132Smax.romanov@nginx.com this._debug('WebSocket: Data for ping is longer than 125 bytes. Truncating.'); 5661132Smax.romanov@nginx.com data = data.slice(0,124); 5671132Smax.romanov@nginx.com } 5681132Smax.romanov@nginx.com frame.binaryPayload = data; 5691132Smax.romanov@nginx.com } 5701132Smax.romanov@nginx.com 5711132Smax.romanov@nginx.com this.sendFrame(frame); 5721132Smax.romanov@nginx.com}; 5731132Smax.romanov@nginx.com 5741132Smax.romanov@nginx.com// Pong frames have to echo back the contents of the data portion of the 5751132Smax.romanov@nginx.com// ping frame exactly, byte for byte. 5761132Smax.romanov@nginx.comWebSocketConnection.prototype.pong = function(binaryPayload) { 5771132Smax.romanov@nginx.com this._debug('pong'); 5781132Smax.romanov@nginx.com 5791132Smax.romanov@nginx.com var frame = new WebSocketFrame(); 5801132Smax.romanov@nginx.com frame.opcode = 0x0A; // WebSocketOpcode.PONG 5811132Smax.romanov@nginx.com if (Buffer.isBuffer(binaryPayload) && binaryPayload.length > 125) { 5821132Smax.romanov@nginx.com this._debug('WebSocket: Data for pong is longer than 125 bytes. Truncating.'); 5831132Smax.romanov@nginx.com binaryPayload = binaryPayload.slice(0,124); 5841132Smax.romanov@nginx.com } 5851132Smax.romanov@nginx.com frame.binaryPayload = binaryPayload; 5861132Smax.romanov@nginx.com frame.fin = true; 5871132Smax.romanov@nginx.com 5881132Smax.romanov@nginx.com this.sendFrame(frame); 5891132Smax.romanov@nginx.com}; 5901132Smax.romanov@nginx.com 5911132Smax.romanov@nginx.comWebSocketConnection.prototype.fragmentAndSend = function(frame, cb) { 5921132Smax.romanov@nginx.com this._debug('fragmentAndSend'); 5931132Smax.romanov@nginx.com if (frame.opcode > 0x07) { 5941132Smax.romanov@nginx.com throw new Error('You cannot fragment control frames.'); 5951132Smax.romanov@nginx.com } 5961132Smax.romanov@nginx.com 5971132Smax.romanov@nginx.com var threshold = this.config.fragmentationThreshold; 5981132Smax.romanov@nginx.com var length = frame.binaryPayload.length; 5991132Smax.romanov@nginx.com 6001132Smax.romanov@nginx.com // Send immediately if fragmentation is disabled or the message is not 6011132Smax.romanov@nginx.com // larger than the fragmentation threshold. 6021132Smax.romanov@nginx.com if (!this.config.fragmentOutgoingMessages || (frame.binaryPayload && length <= threshold)) { 6031132Smax.romanov@nginx.com frame.fin = true; 6041132Smax.romanov@nginx.com this.sendFrame(frame, cb); 6051132Smax.romanov@nginx.com return; 6061132Smax.romanov@nginx.com } 6071132Smax.romanov@nginx.com 6081132Smax.romanov@nginx.com var numFragments = Math.ceil(length / threshold); 6091132Smax.romanov@nginx.com var sentFragments = 0; 6101132Smax.romanov@nginx.com var sentCallback = function fragmentSentCallback(err) { 6111132Smax.romanov@nginx.com if (err) { 6121132Smax.romanov@nginx.com if (typeof cb === 'function') { 6131132Smax.romanov@nginx.com // pass only the first error 6141132Smax.romanov@nginx.com cb(err); 6151132Smax.romanov@nginx.com cb = null; 6161132Smax.romanov@nginx.com } 6171132Smax.romanov@nginx.com return; 6181132Smax.romanov@nginx.com } 6191132Smax.romanov@nginx.com ++sentFragments; 6201132Smax.romanov@nginx.com if ((sentFragments === numFragments) && (typeof cb === 'function')) { 6211132Smax.romanov@nginx.com cb(); 6221132Smax.romanov@nginx.com } 6231132Smax.romanov@nginx.com }; 6241132Smax.romanov@nginx.com for (var i=1; i <= numFragments; i++) { 6251132Smax.romanov@nginx.com var currentFrame = new WebSocketFrame(); 6261132Smax.romanov@nginx.com 6271132Smax.romanov@nginx.com // continuation opcode except for first frame. 6281132Smax.romanov@nginx.com currentFrame.opcode = (i === 1) ? frame.opcode : 0x00; 6291132Smax.romanov@nginx.com 6301132Smax.romanov@nginx.com // fin set on last frame only 6311132Smax.romanov@nginx.com currentFrame.fin = (i === numFragments); 6321132Smax.romanov@nginx.com 6331132Smax.romanov@nginx.com // length is likely to be shorter on the last fragment 6341132Smax.romanov@nginx.com var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold; 6351132Smax.romanov@nginx.com var sliceStart = threshold * (i-1); 6361132Smax.romanov@nginx.com 6371132Smax.romanov@nginx.com // Slice the right portion of the original payload 6381132Smax.romanov@nginx.com currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength); 6391132Smax.romanov@nginx.com 6401132Smax.romanov@nginx.com this.sendFrame(currentFrame, sentCallback); 6411132Smax.romanov@nginx.com } 6421132Smax.romanov@nginx.com}; 6431132Smax.romanov@nginx.com 6441132Smax.romanov@nginx.comWebSocketConnection.prototype.sendCloseFrame = function(reasonCode, description, cb) { 6451132Smax.romanov@nginx.com if (typeof(reasonCode) !== 'number') { 6461132Smax.romanov@nginx.com reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; 6471132Smax.romanov@nginx.com } 6481132Smax.romanov@nginx.com 6491132Smax.romanov@nginx.com this._debug('sendCloseFrame state: %s, reasonCode: %d, description: %s', this.state, reasonCode, description); 6501132Smax.romanov@nginx.com 6511132Smax.romanov@nginx.com if (this.state !== STATE_OPEN && this.state !== STATE_PEER_REQUESTED_CLOSE) { return; } 6521132Smax.romanov@nginx.com 6531132Smax.romanov@nginx.com var frame = new WebSocketFrame(); 6541132Smax.romanov@nginx.com frame.fin = true; 6551132Smax.romanov@nginx.com frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE 6561132Smax.romanov@nginx.com frame.closeStatus = reasonCode; 6571132Smax.romanov@nginx.com if (typeof(description) === 'string') { 6581132Smax.romanov@nginx.com frame.binaryPayload = bufferFromString(description, 'utf8'); 6591132Smax.romanov@nginx.com } 6601132Smax.romanov@nginx.com 6611132Smax.romanov@nginx.com this.sendFrame(frame, cb); 6621132Smax.romanov@nginx.com this.socket.end(); 6631132Smax.romanov@nginx.com}; 6641132Smax.romanov@nginx.com 6651132Smax.romanov@nginx.comWebSocketConnection.prototype._send_frame = unit_lib.websocket_send_frame; 6661132Smax.romanov@nginx.com 6671132Smax.romanov@nginx.comWebSocketConnection.prototype.sendFrame = function(frame, cb) { 6681132Smax.romanov@nginx.com this._debug('sendFrame'); 6691132Smax.romanov@nginx.com 6701132Smax.romanov@nginx.com frame.mask = this.maskOutgoingPackets; 6711132Smax.romanov@nginx.com 6721132Smax.romanov@nginx.com this._send_frame(frame); 6731132Smax.romanov@nginx.com 6741132Smax.romanov@nginx.com if (typeof cb === 'function') { 6751132Smax.romanov@nginx.com cb(); 6761132Smax.romanov@nginx.com } 6771132Smax.romanov@nginx.com 6781132Smax.romanov@nginx.com var flushed = 0; // this.socket.write(frame.toBuffer(), cb); 6791132Smax.romanov@nginx.com this.outputBufferFull = !flushed; 6801132Smax.romanov@nginx.com return flushed; 6811132Smax.romanov@nginx.com}; 6821132Smax.romanov@nginx.com 6831132Smax.romanov@nginx.commodule.exports = WebSocketConnection; 684