1 /************************************************************************
2 * Copyright 2010-2015 Brian McKelvey.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 ***********************************************************************/
17 var util = require('util');
18 var utils = require('./utils');
19 var EventEmitter = require('events').EventEmitter;
20 var WebSocketFrame = require('./WebSocketFrame');
21 var BufferList = require('../vendor/FastBufferList');
22 var Validation = require('./Validation').Validation;
23 var bufferAllocUnsafe = utils.bufferAllocUnsafe;
24 var bufferFromString = utils.bufferFromString;
26 // Connected, fully-open, ready to send and receive frames
27 const STATE_OPEN = 'open';
28 // Received a close frame from the remote peer
29 const STATE_PEER_REQUESTED_CLOSE = 'peer_requested_close';
30 // Sent close frame to remote peer. No further data can be sent.
31 const STATE_ENDING = 'ending';
32 // Connection is fully closed. No further data can be sent or received.
33 const STATE_CLOSED = 'closed';
35 var setImmediateImpl = ('setImmediate' in global) ?
36 global.setImmediate.bind(global) :
37 process.nextTick.bind(process);
41 function WebSocketConnection(socket, extensions, protocol, maskOutgoingPackets, config) {
42 this._debug = utils.BufferingLogger('websocket:connection', ++idCounter);
43 this._debug('constructor');
45 if (this._debug.enabled) {
46 instrumentSocketForDebugging(this, socket);
49 // Superclass Constructor
50 EventEmitter.call(this);
52 this._pingListenerCount = 0;
53 this.on('newListener', function(ev) {
55 this._pingListenerCount++;
57 }).on('removeListener', function(ev) {
59 this._pingListenerCount--;
65 this.protocol = protocol;
66 this.extensions = extensions;
67 this.remoteAddress = socket.remoteAddress;
68 this.closeReasonCode = -1;
69 this.closeDescription = null;
70 this.closeEventEmitted = false;
72 // We have to mask outgoing packets if we're acting as a WebSocket client.
73 this.maskOutgoingPackets = maskOutgoingPackets;
75 // We re-use the same buffers for the mask and frame header for all frames
76 // received on each connection to avoid a small memory allocation for each
78 this.maskBytes = bufferAllocUnsafe(4);
79 this.frameHeader = bufferAllocUnsafe(10);
81 // the BufferList will handle the data streaming in
82 this.bufferList = new BufferList();
84 // Prepare for receiving first frame
85 this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
86 this.fragmentationSize = 0; // data received so far...
89 // Various bits of connection state
90 this.connected = true;
91 this.state = STATE_OPEN;
92 this.waitingForCloseResponse = false;
93 // Received TCP FIN, socket's readable stream is finished.
94 this.receivedEnd = false;
96 this.closeTimeout = this.config.closeTimeout;
97 this.assembleFragments = this.config.assembleFragments;
98 this.maxReceivedMessageSize = this.config.maxReceivedMessageSize;
100 this.outputBufferFull = false;
101 this.inputPaused = false;
102 this.receivedDataHandler = this.processReceivedData.bind(this);
103 this._closeTimerHandler = this.handleCloseTimer.bind(this);
105 // Disable nagle algorithm?
106 this.socket.setNoDelay(this.config.disableNagleAlgorithm);
108 // Make sure there is no socket inactivity timeout
109 this.socket.setTimeout(0);
111 if (this.config.keepalive && !this.config.useNativeKeepalive) {
112 if (typeof(this.config.keepaliveInterval) !== 'number') {
113 throw new Error('keepaliveInterval must be specified and numeric ' +
114 'if keepalive is true.');
116 this._keepaliveTimerHandler = this.handleKeepaliveTimer.bind(this);
117 this.setKeepaliveTimer();
119 if (this.config.dropConnectionOnKeepaliveTimeout) {
120 if (typeof(this.config.keepaliveGracePeriod) !== 'number') {
121 throw new Error('keepaliveGracePeriod must be specified and ' +
122 'numeric if dropConnectionOnKeepaliveTimeout ' +
125 this._gracePeriodTimerHandler = this.handleGracePeriodTimer.bind(this);
128 else if (this.config.keepalive && this.config.useNativeKeepalive) {
129 if (!('setKeepAlive' in this.socket)) {
130 throw new Error('Unable to use native keepalive: unsupported by ' +
131 'this version of Node.');
133 this.socket.setKeepAlive(true, this.config.keepaliveInterval);
136 // The HTTP Client seems to subscribe to socket error events
137 // and re-dispatch them in such a way that doesn't make sense
138 // for users of our client, so we want to make sure nobody
139 // else is listening for error events on the socket besides us.
140 this.socket.removeAllListeners('error');
143 WebSocketConnection.CLOSE_REASON_NORMAL = 1000;
144 WebSocketConnection.CLOSE_REASON_GOING_AWAY = 1001;
145 WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR = 1002;
146 WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT = 1003;
147 WebSocketConnection.CLOSE_REASON_RESERVED = 1004; // Reserved value. Undefined meaning.
148 WebSocketConnection.CLOSE_REASON_NOT_PROVIDED = 1005; // Not to be used on the wire
149 WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006; // Not to be used on the wire
150 WebSocketConnection.CLOSE_REASON_INVALID_DATA = 1007;
151 WebSocketConnection.CLOSE_REASON_POLICY_VIOLATION = 1008;
152 WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG = 1009;
153 WebSocketConnection.CLOSE_REASON_EXTENSION_REQUIRED = 1010;
154 WebSocketConnection.CLOSE_REASON_INTERNAL_SERVER_ERROR = 1011;
155 WebSocketConnection.CLOSE_REASON_TLS_HANDSHAKE_FAILED = 1015; // Not to be used on the wire
157 WebSocketConnection.CLOSE_DESCRIPTIONS = {
158 1000: 'Normal connection closure',
159 1001: 'Remote peer is going away',
160 1002: 'Protocol error',
161 1003: 'Unprocessable input',
163 1005: 'Reason not provided',
164 1006: 'Abnormal closure, no further detail available',
165 1007: 'Invalid data received',
166 1008: 'Policy violation',
167 1009: 'Message too big',
168 1010: 'Extension requested by client is required',
169 1011: 'Internal Server Error',
170 1015: 'TLS Handshake Failed'
173 function validateCloseReason(code) {
175 // Status codes in the range 0-999 are not used
178 if (code >= 1000 && code <= 2999) {
179 // Codes from 1000 - 2999 are reserved for use by the protocol. Only
180 // a few codes are defined, all others are currently illegal.
181 return [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014].indexOf(code) !== -1;
183 if (code >= 3000 && code <= 3999) {
184 // Reserved for use by libraries, frameworks, and applications.
185 // Should be registered with IANA. Interpretation of these codes is
186 // undefined by the WebSocket protocol.
189 if (code >= 4000 && code <= 4999) {
190 // Reserved for private use. Interpretation of these codes is
191 // undefined by the WebSocket protocol.
199 util.inherits(WebSocketConnection, EventEmitter);
201 WebSocketConnection.prototype._addSocketEventListeners = function() {
202 this.socket.on('error', this.handleSocketError.bind(this));
203 this.socket.on('end', this.handleSocketEnd.bind(this));
204 this.socket.on('close', this.handleSocketClose.bind(this));
205 this.socket.on('drain', this.handleSocketDrain.bind(this));
206 this.socket.on('pause', this.handleSocketPause.bind(this));
207 this.socket.on('resume', this.handleSocketResume.bind(this));
208 this.socket.on('data', this.handleSocketData.bind(this));
211 // set or reset the keepalive timer when data is received.
212 WebSocketConnection.prototype.setKeepaliveTimer = function() {
213 this._debug('setKeepaliveTimer');
214 if (!this.config.keepalive || this.config.useNativeKeepalive) { return; }
215 this.clearKeepaliveTimer();
216 this.clearGracePeriodTimer();
217 this._keepaliveTimeoutID = setTimeout(this._keepaliveTimerHandler, this.config.keepaliveInterval);
220 WebSocketConnection.prototype.clearKeepaliveTimer = function() {
221 if (this._keepaliveTimeoutID) {
222 clearTimeout(this._keepaliveTimeoutID);
226 // No data has been received within config.keepaliveTimeout ms.
227 WebSocketConnection.prototype.handleKeepaliveTimer = function() {
228 this._debug('handleKeepaliveTimer');
229 this._keepaliveTimeoutID = null;
232 // If we are configured to drop connections if the client doesn't respond
233 // then set the grace period timer.
234 if (this.config.dropConnectionOnKeepaliveTimeout) {
235 this.setGracePeriodTimer();
238 // Otherwise reset the keepalive timer to send the next ping.
239 this.setKeepaliveTimer();
243 WebSocketConnection.prototype.setGracePeriodTimer = function() {
244 this._debug('setGracePeriodTimer');
245 this.clearGracePeriodTimer();
246 this._gracePeriodTimeoutID = setTimeout(this._gracePeriodTimerHandler, this.config.keepaliveGracePeriod);
249 WebSocketConnection.prototype.clearGracePeriodTimer = function() {
250 if (this._gracePeriodTimeoutID) {
251 clearTimeout(this._gracePeriodTimeoutID);
255 WebSocketConnection.prototype.handleGracePeriodTimer = function() {
256 this._debug('handleGracePeriodTimer');
257 // If this is called, the client has not responded and is assumed dead.
258 this._gracePeriodTimeoutID = null;
259 this.drop(WebSocketConnection.CLOSE_REASON_ABNORMAL, 'Peer not responding.', true);
262 WebSocketConnection.prototype.handleSocketData = function(data) {
263 this._debug('handleSocketData');
264 // Reset the keepalive timer when receiving data of any kind.
265 this.setKeepaliveTimer();
267 // Add received data to our bufferList, which efficiently holds received
268 // data chunks in a linked list of Buffer objects.
269 this.bufferList.write(data);
271 this.processReceivedData();
274 WebSocketConnection.prototype.processReceivedData = function() {
275 this._debug('processReceivedData');
276 // If we're not connected, we should ignore any data remaining on the buffer.
277 if (!this.connected) { return; }
279 // Receiving/parsing is expected to be halted when paused.
280 if (this.inputPaused) { return; }
282 var frame = this.currentFrame;
284 // WebSocketFrame.prototype.addData returns true if all data necessary to
285 // parse the frame was available. It returns false if we are waiting for
286 // more data to come in on the wire.
287 if (!frame.addData(this.bufferList)) { this._debug('-- insufficient data for frame'); return; }
291 // Handle possible parsing errors
292 if (frame.protocolError) {
293 // Something bad happened.. get rid of this client.
294 this._debug('-- protocol error');
295 process.nextTick(function() {
296 self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, frame.dropReason);
300 else if (frame.frameTooLarge) {
301 this._debug('-- frame too large');
302 process.nextTick(function() {
303 self.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG, frame.dropReason);
308 // For now since we don't support extensions, all RSV bits are illegal
309 if (frame.rsv1 || frame.rsv2 || frame.rsv3) {
310 this._debug('-- illegal rsv flag');
311 process.nextTick(function() {
312 self.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
313 'Unsupported usage of rsv bits without negotiated extension.');
318 if (!this.assembleFragments) {
319 this._debug('-- emitting frame');
320 process.nextTick(function() { self.emit('frame', frame); });
323 process.nextTick(function() { self.processFrame(frame); });
325 this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
327 // If there's data remaining, schedule additional processing, but yield
328 // for now so that other connections have a chance to have their data
329 // processed. We use setImmediate here instead of process.nextTick to
330 // explicitly indicate that we wish for other I/O to be handled first.
331 if (this.bufferList.length > 0) {
332 setImmediateImpl(this.receivedDataHandler);
336 WebSocketConnection.prototype.handleSocketError = function(error) {
337 this._debug('handleSocketError: %j', error);
338 if (this.state === STATE_CLOSED) {
339 // See https://github.com/theturtle32/WebSocket-Node/issues/288
340 this._debug(' --- Socket \'error\' after \'close\'');
343 this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL;
344 this.closeDescription = 'Socket Error: ' + error.syscall + ' ' + error.code;
345 this.connected = false;
346 this.state = STATE_CLOSED;
347 this.fragmentationSize = 0;
348 if (utils.eventEmitterListenerCount(this, 'error') > 0) {
349 this.emit('error', error);
351 this.socket.destroy(error);
352 this._debug.printOutput();
355 WebSocketConnection.prototype.handleSocketEnd = function() {
356 this._debug('handleSocketEnd: received socket end. state = %s', this.state);
357 this.receivedEnd = true;
358 if (this.state === STATE_CLOSED) {
359 // When using the TLS module, sometimes the socket will emit 'end'
360 // after it emits 'close'. I don't think that's correct behavior,
361 // but we should deal with it gracefully by ignoring it.
362 this._debug(' --- Socket \'end\' after \'close\'');
365 if (this.state !== STATE_PEER_REQUESTED_CLOSE &&
366 this.state !== STATE_ENDING) {
367 this._debug(' --- UNEXPECTED socket end.');
372 WebSocketConnection.prototype.handleSocketClose = function(hadError) {
373 this._debug('handleSocketClose: received socket close');
374 this.socketHadError = hadError;
375 this.connected = false;
376 this.state = STATE_CLOSED;
377 // If closeReasonCode is still set to -1 at this point then we must
378 // not have received a close frame!!
379 if (this.closeReasonCode === -1) {
380 this.closeReasonCode = WebSocketConnection.CLOSE_REASON_ABNORMAL;
381 this.closeDescription = 'Connection dropped by remote peer.';
383 this.clearCloseTimer();
384 this.clearKeepaliveTimer();
385 this.clearGracePeriodTimer();
386 if (!this.closeEventEmitted) {
387 this.closeEventEmitted = true;
388 this._debug('-- Emitting WebSocketConnection close event');
389 this.emit('close', this.closeReasonCode, this.closeDescription);
393 WebSocketConnection.prototype.handleSocketDrain = function() {
394 this._debug('handleSocketDrain: socket drain event');
395 this.outputBufferFull = false;
399 WebSocketConnection.prototype.handleSocketPause = function() {
400 this._debug('handleSocketPause: socket pause event');
401 this.inputPaused = true;
405 WebSocketConnection.prototype.handleSocketResume = function() {
406 this._debug('handleSocketResume: socket resume event');
407 this.inputPaused = false;
409 this.processReceivedData();
412 WebSocketConnection.prototype.pause = function() {
413 this._debug('pause: pause requested');
417 WebSocketConnection.prototype.resume = function() {
418 this._debug('resume: resume requested');
419 this.socket.resume();
422 WebSocketConnection.prototype.close = function(reasonCode, description) {
423 if (this.connected) {
424 this._debug('close: Initating clean WebSocket close sequence.');
425 if ('number' !== typeof reasonCode) {
426 reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
428 if (!validateCloseReason(reasonCode)) {
429 throw new Error('Close code ' + reasonCode + ' is not valid.');
431 if ('string' !== typeof description) {
432 description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode];
434 this.closeReasonCode = reasonCode;
435 this.closeDescription = description;
436 this.setCloseTimer();
437 this.sendCloseFrame(this.closeReasonCode, this.closeDescription);
438 this.state = STATE_ENDING;
439 this.connected = false;
443 WebSocketConnection.prototype.drop = function(reasonCode, description, skipCloseFrame) {
445 if (typeof(reasonCode) !== 'number') {
446 reasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
449 if (typeof(description) !== 'string') {
450 // If no description is provided, try to look one up based on the
451 // specified reasonCode.
452 description = WebSocketConnection.CLOSE_DESCRIPTIONS[reasonCode];
455 this._debug('Forcefully dropping connection. skipCloseFrame: %s, code: %d, description: %s',
456 skipCloseFrame, reasonCode, description
459 this.closeReasonCode = reasonCode;
460 this.closeDescription = description;
461 this.frameQueue = [];
462 this.fragmentationSize = 0;
463 if (!skipCloseFrame) {
464 this.sendCloseFrame(reasonCode, description);
466 this.connected = false;
467 this.state = STATE_CLOSED;
468 this.clearCloseTimer();
469 this.clearKeepaliveTimer();
470 this.clearGracePeriodTimer();
472 if (!this.closeEventEmitted) {
473 this.closeEventEmitted = true;
474 this._debug('Emitting WebSocketConnection close event');
475 this.emit('close', this.closeReasonCode, this.closeDescription);
478 this._debug('Drop: destroying socket');
479 this.socket.destroy();
482 WebSocketConnection.prototype.setCloseTimer = function() {
483 this._debug('setCloseTimer');
484 this.clearCloseTimer();
485 this._debug('Setting close timer');
486 this.waitingForCloseResponse = true;
487 this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout);
490 WebSocketConnection.prototype.clearCloseTimer = function() {
491 this._debug('clearCloseTimer');
492 if (this.closeTimer) {
493 this._debug('Clearing close timer');
494 clearTimeout(this.closeTimer);
495 this.waitingForCloseResponse = false;
496 this.closeTimer = null;
500 WebSocketConnection.prototype.handleCloseTimer = function() {
501 this._debug('handleCloseTimer');
502 this.closeTimer = null;
503 if (this.waitingForCloseResponse) {
504 this._debug('Close response not received from client. Forcing socket end.');
505 this.waitingForCloseResponse = false;
506 this.state = STATE_CLOSED;
511 WebSocketConnection.prototype.processFrame = function(frame) {
512 this._debug('processFrame');
513 this._debug(' -- frame: %s', frame);
515 // Any non-control opcode besides 0x00 (continuation) received in the
516 // middle of a fragmented message is illegal.
517 if (this.frameQueue.length !== 0 && (frame.opcode > 0x00 && frame.opcode < 0x08)) {
518 this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
519 'Illegal frame opcode 0x' + frame.opcode.toString(16) + ' ' +
520 'received in middle of fragmented message.');
524 switch(frame.opcode) {
525 case 0x02: // WebSocketFrame.BINARY_FRAME
526 this._debug('-- Binary Frame');
527 if (this.assembleFragments) {
529 // Complete single-frame message received
530 this._debug('---- Emitting \'message\' event');
531 this.emit('message', {
533 binaryData: frame.binaryPayload
537 // beginning of a fragmented message
538 this.frameQueue.push(frame);
539 this.fragmentationSize = frame.length;
543 case 0x01: // WebSocketFrame.TEXT_FRAME
544 this._debug('-- Text Frame');
545 if (this.assembleFragments) {
547 if (!Validation.isValidUTF8(frame.binaryPayload)) {
548 this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
549 'Invalid UTF-8 Data Received');
552 // Complete single-frame message received
553 this._debug('---- Emitting \'message\' event');
554 this.emit('message', {
556 utf8Data: frame.binaryPayload.toString('utf8')
560 // beginning of a fragmented message
561 this.frameQueue.push(frame);
562 this.fragmentationSize = frame.length;
566 case 0x00: // WebSocketFrame.CONTINUATION
567 this._debug('-- Continuation Frame');
568 if (this.assembleFragments) {
569 if (this.frameQueue.length === 0) {
570 this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
571 'Unexpected Continuation Frame');
575 this.fragmentationSize += frame.length;
577 if (this.fragmentationSize > this.maxReceivedMessageSize) {
578 this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_BIG,
579 'Maximum message size exceeded.');
583 this.frameQueue.push(frame);
586 // end of fragmented message, so we process the whole
587 // message now. We also have to decode the utf-8 data
588 // for text frames after combining all the fragments.
590 var binaryPayload = bufferAllocUnsafe(this.fragmentationSize);
591 var opcode = this.frameQueue[0].opcode;
592 this.frameQueue.forEach(function (currentFrame) {
593 currentFrame.binaryPayload.copy(binaryPayload, bytesCopied);
594 bytesCopied += currentFrame.binaryPayload.length;
596 this.frameQueue = [];
597 this.fragmentationSize = 0;
600 case 0x02: // WebSocketOpcode.BINARY_FRAME
601 this.emit('message', {
603 binaryData: binaryPayload
606 case 0x01: // WebSocketOpcode.TEXT_FRAME
607 if (!Validation.isValidUTF8(binaryPayload)) {
608 this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
609 'Invalid UTF-8 Data Received');
612 this.emit('message', {
614 utf8Data: binaryPayload.toString('utf8')
618 this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
619 'Unexpected first opcode in fragmentation sequence: 0x' + opcode.toString(16));
625 case 0x09: // WebSocketFrame.PING
626 this._debug('-- Ping Frame');
628 if (this._pingListenerCount > 0) {
629 // logic to emit the ping frame: this is only done when a listener is known to exist
630 // Expose a function allowing the user to override the default ping() behavior
631 var cancelled = false;
632 var cancel = function() {
635 this.emit('ping', cancel, frame.binaryPayload);
637 // Only send a pong if the client did not indicate that he would like to cancel
639 this.pong(frame.binaryPayload);
643 this.pong(frame.binaryPayload);
647 case 0x0A: // WebSocketFrame.PONG
648 this._debug('-- Pong Frame');
649 this.emit('pong', frame.binaryPayload);
651 case 0x08: // WebSocketFrame.CONNECTION_CLOSE
652 this._debug('-- Close Frame');
653 if (this.waitingForCloseResponse) {
654 // Got response to our request to close the connection.
655 // Close is complete, so we just hang up.
656 this._debug('---- Got close response from peer. Completing closing handshake.');
657 this.clearCloseTimer();
658 this.waitingForCloseResponse = false;
659 this.state = STATE_CLOSED;
664 this._debug('---- Closing handshake initiated by peer.');
665 // Got request from other party to close connection.
666 // Send back acknowledgement and then hang up.
667 this.state = STATE_PEER_REQUESTED_CLOSE;
668 var respondCloseReasonCode;
670 // Make sure the close reason provided is legal according to
671 // the protocol spec. Providing no close status is legal.
672 // WebSocketFrame sets closeStatus to -1 by default, so if it
673 // is still -1, then no status was provided.
674 if (frame.invalidCloseFrameLength) {
675 this.closeReasonCode = 1005; // 1005 = No reason provided.
676 respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
678 else if (frame.closeStatus === -1 || validateCloseReason(frame.closeStatus)) {
679 this.closeReasonCode = frame.closeStatus;
680 respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
683 this.closeReasonCode = frame.closeStatus;
684 respondCloseReasonCode = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR;
687 // If there is a textual description in the close frame, extract it.
688 if (frame.binaryPayload.length > 1) {
689 if (!Validation.isValidUTF8(frame.binaryPayload)) {
690 this.drop(WebSocketConnection.CLOSE_REASON_INVALID_DATA,
691 'Invalid UTF-8 Data Received');
694 this.closeDescription = frame.binaryPayload.toString('utf8');
697 this.closeDescription = WebSocketConnection.CLOSE_DESCRIPTIONS[this.closeReasonCode];
700 '------ Remote peer %s - code: %d - %s - close frame payload length: %d',
701 this.remoteAddress, this.closeReasonCode,
702 this.closeDescription, frame.length
704 this._debug('------ responding to remote peer\'s close request.');
705 this.sendCloseFrame(respondCloseReasonCode, null);
706 this.connected = false;
709 this._debug('-- Unrecognized Opcode %d', frame.opcode);
710 this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR,
711 'Unrecognized Opcode: 0x' + frame.opcode.toString(16));
716 WebSocketConnection.prototype.send = function(data, cb) {
718 if (Buffer.isBuffer(data)) {
719 this.sendBytes(data, cb);
721 else if (typeof(data['toString']) === 'function') {
722 this.sendUTF(data, cb);
725 throw new Error('Data provided must either be a Node Buffer or implement toString()');
729 WebSocketConnection.prototype.sendUTF = function(data, cb) {
730 data = bufferFromString(data.toString(), 'utf8');
731 this._debug('sendUTF: %d bytes', data.length);
732 var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
733 frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME
734 frame.binaryPayload = data;
735 this.fragmentAndSend(frame, cb);
738 WebSocketConnection.prototype.sendBytes = function(data, cb) {
739 this._debug('sendBytes');
740 if (!Buffer.isBuffer(data)) {
741 throw new Error('You must pass a Node Buffer object to WebSocketConnection.prototype.sendBytes()');
743 var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
744 frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME
745 frame.binaryPayload = data;
746 this.fragmentAndSend(frame, cb);
749 WebSocketConnection.prototype.ping = function(data) {
751 var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
752 frame.opcode = 0x09; // WebSocketOpcode.PING
755 if (!Buffer.isBuffer(data)) {
756 data = bufferFromString(data.toString(), 'utf8');
758 if (data.length > 125) {
759 this._debug('WebSocket: Data for ping is longer than 125 bytes. Truncating.');
760 data = data.slice(0,124);
762 frame.binaryPayload = data;
764 this.sendFrame(frame);
767 // Pong frames have to echo back the contents of the data portion of the
768 // ping frame exactly, byte for byte.
769 WebSocketConnection.prototype.pong = function(binaryPayload) {
771 var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
772 frame.opcode = 0x0A; // WebSocketOpcode.PONG
773 if (Buffer.isBuffer(binaryPayload) && binaryPayload.length > 125) {
774 this._debug('WebSocket: Data for pong is longer than 125 bytes. Truncating.');
775 binaryPayload = binaryPayload.slice(0,124);
777 frame.binaryPayload = binaryPayload;
779 this.sendFrame(frame);
782 WebSocketConnection.prototype.fragmentAndSend = function(frame, cb) {
783 this._debug('fragmentAndSend');
784 if (frame.opcode > 0x07) {
785 throw new Error('You cannot fragment control frames.');
788 var threshold = this.config.fragmentationThreshold;
789 var length = frame.binaryPayload.length;
791 // Send immediately if fragmentation is disabled or the message is not
792 // larger than the fragmentation threshold.
793 if (!this.config.fragmentOutgoingMessages || (frame.binaryPayload && length <= threshold)) {
795 this.sendFrame(frame, cb);
799 var numFragments = Math.ceil(length / threshold);
800 var sentFragments = 0;
801 var sentCallback = function fragmentSentCallback(err) {
803 if (typeof cb === 'function') {
804 // pass only the first error
811 if ((sentFragments === numFragments) && (typeof cb === 'function')) {
815 for (var i=1; i <= numFragments; i++) {
816 var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
818 // continuation opcode except for first frame.
819 currentFrame.opcode = (i === 1) ? frame.opcode : 0x00;
821 // fin set on last frame only
822 currentFrame.fin = (i === numFragments);
824 // length is likely to be shorter on the last fragment
825 var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold;
826 var sliceStart = threshold * (i-1);
828 // Slice the right portion of the original payload
829 currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength);
831 this.sendFrame(currentFrame, sentCallback);
835 WebSocketConnection.prototype.sendCloseFrame = function(reasonCode, description, cb) {
836 if (typeof(reasonCode) !== 'number') {
837 reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL;
840 this._debug('sendCloseFrame state: %s, reasonCode: %d, description: %s', this.state, reasonCode, description);
842 if (this.state !== STATE_OPEN && this.state !== STATE_PEER_REQUESTED_CLOSE) { return; }
844 var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config);
846 frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE
847 frame.closeStatus = reasonCode;
848 if (typeof(description) === 'string') {
849 frame.binaryPayload = bufferFromString(description, 'utf8');
852 this.sendFrame(frame, cb);
856 WebSocketConnection.prototype.sendFrame = function(frame, cb) {
857 this._debug('sendFrame');
858 frame.mask = this.maskOutgoingPackets;
859 var flushed = this.socket.write(frame.toBuffer(), cb);
860 this.outputBufferFull = !flushed;
864 module.exports = WebSocketConnection;
868 function instrumentSocketForDebugging(connection, socket) {
869 /* jshint loopfunc: true */
870 if (!connection._debug.enabled) { return; }
872 var originalSocketEmit = socket.emit;
873 socket.emit = function(event) {
874 connection._debug('||| Socket Event \'%s\'', event);
875 originalSocketEmit.apply(this, arguments);
878 for (var key in socket) {
879 if ('function' !== typeof(socket[key])) { continue; }
880 if (['emit'].indexOf(key) !== -1) { continue; }
882 var original = socket[key];
884 socket[key] = function proxyMethod__EventEmitter__On() {
885 connection._debug('||| Socket method called: %s (%s)', key, arguments[0]);
886 return original.apply(this, arguments);
890 socket[key] = function proxyMethod() {
891 connection._debug('||| Socket method called: %s', key);
892 return original.apply(this, arguments);