xref: /unit/src/nodejs/unit-http/websocket_server.js (revision 1132:9ac5b5f33ed9)
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