1/************************************************************************ 2 * Copyright 2010-2015 Brian McKelvey. 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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 ***********************************************************************/ 16 17var extend = require('./utils').extend; 18var utils = require('./utils'); 19var util = require('util'); 20var EventEmitter = require('events').EventEmitter; 21var WebSocketRequest = require('./websocket_request'); 22 23var WebSocketServer = function WebSocketServer(config) { 24 // Superclass Constructor 25 EventEmitter.call(this); 26 27 this._handlers = { 28 upgrade: this.handleUpgrade.bind(this), 29 requestAccepted: this.handleRequestAccepted.bind(this), 30 requestResolved: this.handleRequestResolved.bind(this) 31 }; 32 this.connections = []; 33 this.pendingRequests = []; 34 if (config) { 35 this.mount(config); 36 } 37}; 38 39util.inherits(WebSocketServer, EventEmitter); 40 41WebSocketServer.prototype.mount = function(config) { 42 this.config = { 43 // The http server instance to attach to. Required. 44 httpServer: null, 45 46 // 64KiB max frame size. 47 maxReceivedFrameSize: 0x10000, 48 49 // 1MiB max message size, only applicable if 50 // assembleFragments is true 51 maxReceivedMessageSize: 0x100000, 52 53 // Outgoing messages larger than fragmentationThreshold will be 54 // split into multiple fragments. 55 fragmentOutgoingMessages: true, 56 57 // Outgoing frames are fragmented if they exceed this threshold. 58 // Default is 16KiB 59 fragmentationThreshold: 0x4000, 60 61 // If true, fragmented messages will be automatically assembled 62 // and the full message will be emitted via a 'message' event. 63 // If false, each frame will be emitted via a 'frame' event and 64 // the application will be responsible for aggregating multiple 65 // fragmented frames. Single-frame messages will emit a 'message' 66 // event in addition to the 'frame' event. 67 // Most users will want to leave this set to 'true' 68 assembleFragments: true, 69 70 // If this is true, websocket connections will be accepted 71 // regardless of the path and protocol specified by the client. 72 // The protocol accepted will be the first that was requested 73 // by the client. Clients from any origin will be accepted. 74 // This should only be used in the simplest of cases. You should 75 // probably leave this set to 'false' and inspect the request 76 // object to make sure it's acceptable before accepting it. 77 autoAcceptConnections: false, 78 79 // Whether or not the X-Forwarded-For header should be respected. 80 // It's important to set this to 'true' when accepting connections 81 // from untrusted clients, as a malicious client could spoof its 82 // IP address by simply setting this header. It's meant to be added 83 // by a trusted proxy or other intermediary within your own 84 // infrastructure. 85 // See: http://en.wikipedia.org/wiki/X-Forwarded-For 86 ignoreXForwardedFor: false, 87 88 // The Nagle Algorithm makes more efficient use of network resources 89 // by introducing a small delay before sending small packets so that 90 // multiple messages can be batched together before going onto the 91 // wire. This however comes at the cost of latency, so the default 92 // is to disable it. If you don't need low latency and are streaming 93 // lots of small messages, you can change this to 'false' 94 disableNagleAlgorithm: true, 95 96 // The number of milliseconds to wait after sending a close frame 97 // for an acknowledgement to come back before giving up and just 98 // closing the socket. 99 closeTimeout: 5000 100 }; 101 extend(this.config, config); 102 103 if (this.config.httpServer) { 104 if (!Array.isArray(this.config.httpServer)) { 105 this.config.httpServer = [this.config.httpServer]; 106 } 107 var upgradeHandler = this._handlers.upgrade; 108 this.config.httpServer.forEach(function(httpServer) { 109 httpServer.on('upgrade', upgradeHandler); 110 }); 111 } 112 else { 113 throw new Error('You must specify an httpServer on which to mount the WebSocket server.'); 114 } 115}; 116 117WebSocketServer.prototype.unmount = function() { 118 var upgradeHandler = this._handlers.upgrade; 119 this.config.httpServer.forEach(function(httpServer) { 120 httpServer.removeListener('upgrade', upgradeHandler); 121 }); 122}; 123 124WebSocketServer.prototype.closeAllConnections = function() { 125 this.connections.forEach(function(connection) { 126 connection.close(); 127 }); 128 this.pendingRequests.forEach(function(request) { 129 process.nextTick(function() { 130 request.reject(503); // HTTP 503 Service Unavailable 131 }); 132 }); 133}; 134 135WebSocketServer.prototype.broadcast = function(data) { 136 if (Buffer.isBuffer(data)) { 137 this.broadcastBytes(data); 138 } 139 else if (typeof(data.toString) === 'function') { 140 this.broadcastUTF(data); 141 } 142}; 143 144WebSocketServer.prototype.broadcastUTF = function(utfData) { 145 this.connections.forEach(function(connection) { 146 connection.sendUTF(utfData); 147 }); 148}; 149 150WebSocketServer.prototype.broadcastBytes = function(binaryData) { 151 this.connections.forEach(function(connection) { 152 connection.sendBytes(binaryData); 153 }); 154}; 155 156WebSocketServer.prototype.shutDown = function() { 157 this.unmount(); 158 this.closeAllConnections(); 159}; 160 161WebSocketServer.prototype.handleUpgrade = function(request, socket) { 162 var wsRequest = new WebSocketRequest(socket, request, this.config); 163 try { 164 wsRequest.readHandshake(); 165 } 166 catch(e) { 167 wsRequest.reject( 168 e.httpCode ? e.httpCode : 400, 169 e.message, 170 e.headers 171 ); 172 return; 173 } 174 175 this.pendingRequests.push(wsRequest); 176 177 wsRequest.once('requestAccepted', this._handlers.requestAccepted); 178 wsRequest.once('requestResolved', this._handlers.requestResolved); 179 180 if (!this.config.autoAcceptConnections && utils.eventEmitterListenerCount(this, 'request') > 0) { 181 this.emit('request', wsRequest); 182 } 183 else if (this.config.autoAcceptConnections) { 184 wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin); 185 } 186 else { 187 wsRequest.reject(404, 'No handler is configured to accept the connection.'); 188 } 189}; 190 191WebSocketServer.prototype.handleRequestAccepted = function(connection) { 192 var self = this; 193 connection.once('close', function(closeReason, description) { 194 self.handleConnectionClose(connection, closeReason, description); 195 }); 196 this.connections.push(connection); 197 this.emit('connect', connection); 198}; 199 200WebSocketServer.prototype.handleConnectionClose = function(connection, closeReason, description) { 201 var index = this.connections.indexOf(connection); 202 if (index !== -1) { 203 this.connections.splice(index, 1); 204 } 205 this.emit('close', connection, closeReason, description); 206}; 207 208WebSocketServer.prototype.handleRequestResolved = function(request) { 209 var index = this.pendingRequests.indexOf(request); 210 if (index !== -1) { this.pendingRequests.splice(index, 1); } 211}; 212 213module.exports = WebSocketServer; 214