UNPKG

@omneedia/socketcluster

Version:

SocketCluster - A Highly parallelized WebSocket server cluster to make the most of multi-core machines/instances.

536 lines (463 loc) 15.8 kB
var cloneDeep = require('lodash.clonedeep'); var Emitter = require('component-emitter'); var Response = require('./response').Response; var scErrors = require('sc-errors'); var InvalidArgumentsError = scErrors.InvalidArgumentsError; var SocketProtocolError = scErrors.SocketProtocolError; var TimeoutError = scErrors.TimeoutError; var InvalidActionError = scErrors.InvalidActionError; var SCServerSocket = function (id, server, socket) { var self = this; Emitter.call(this); this._localEvents = { 'subscribe': 1, 'unsubscribe': 1, 'connect': 1, '_connect': 1, 'disconnect': 1, '_disconnect': 1, 'connectAbort': 1, '_connectAbort': 1, 'close': 1, '_close': 1, 'message': 1, 'error': 1, 'authStateChange': 1, 'authTokenSigned': 1, 'authenticate': 1, 'deauthenticate': 1, 'badAuthToken': 1, 'raw': 1 }; this._autoAckEvents = { '#publish': 1 }; this.id = id; this.server = server; this.socket = socket; this.state = this.CONNECTING; this.authState = this.UNAUTHENTICATED; this.active = true; this.request = this.socket.upgradeReq || {}; var wsEngine = this.server.options.wsEngine; if (wsEngine === 'sc-uws' || wsEngine === 'uws') { this.request.connection = this.socket._socket; } if (this.request.connection) { this.remoteAddress = this.request.connection.remoteAddress; this.remoteFamily = this.request.connection.remoteFamily; this.remotePort = this.request.connection.remotePort; } else { this.remoteAddress = this.request.remoteAddress; this.remoteFamily = this.request.remoteFamily; this.remotePort = this.request.remotePort; } if (this.request.forwardedForAddress) { this.forwardedForAddress = this.request.forwardedForAddress; } this._cid = 1; this._callbackMap = {}; this._batchSendList = []; this.channelSubscriptions = {}; this.channelSubscriptionsCount = 0; this.socket.on('error', function (err) { Emitter.prototype.emit.call(self, 'error', err); }); this.socket.on('close', function (code, data) { self._onSCClose(code, data); }); if (!this.server.pingTimeoutDisabled) { this._pingIntervalTicker = setInterval(this._sendPing.bind(this), this.server.pingInterval); } this._resetPongTimeout(); // Receive incoming raw messages this.socket.on('message', function (message, flags) { self._resetPongTimeout(); Emitter.prototype.emit.call(self, 'message', message); var obj; try { obj = self.decode(message); } catch (err) { if (err.name === 'Error') { err.name = 'InvalidMessageError'; } Emitter.prototype.emit.call(self, 'error', err); return; } // If pong if (obj === '#2') { var token = self.getAuthToken(); if (self.server.isAuthTokenExpired(token)) { self.deauthenticate(); } } else { if (Array.isArray(obj)) { var len = obj.length; for (var i = 0; i < len; i++) { self._handleEventObject(obj[i], message); } } else { self._handleEventObject(obj, message); } } }); }; SCServerSocket.prototype = Object.create(Emitter.prototype); SCServerSocket.CONNECTING = SCServerSocket.prototype.CONNECTING = 'connecting'; SCServerSocket.OPEN = SCServerSocket.prototype.OPEN = 'open'; SCServerSocket.CLOSED = SCServerSocket.prototype.CLOSED = 'closed'; SCServerSocket.AUTHENTICATED = SCServerSocket.prototype.AUTHENTICATED = 'authenticated'; SCServerSocket.UNAUTHENTICATED = SCServerSocket.prototype.UNAUTHENTICATED = 'unauthenticated'; SCServerSocket.ignoreStatuses = scErrors.socketProtocolIgnoreStatuses; SCServerSocket.errorStatuses = scErrors.socketProtocolErrorStatuses; SCServerSocket.prototype._sendPing = function () { if (this.state !== this.CLOSED) { this.sendObject('#1'); } }; SCServerSocket.prototype._handleEventObject = function (obj, message) { var self = this; if (obj && obj.event != null) { var eventName = obj.event; if (self._localEvents[eventName] == null) { var response = new Response(self, obj.cid); self.server.verifyInboundEvent(self, eventName, obj.data, function (err, newEventData, ackData) { if (err) { response.error(err, ackData); } else { if (eventName === '#disconnect') { var disconnectData = newEventData || {}; self._onSCClose(disconnectData.code, disconnectData.data); } else { if (self._autoAckEvents[eventName]) { if (ackData !== undefined) { response.end(ackData); } else { response.end(); } Emitter.prototype.emit.call(self, eventName, newEventData); } else { Emitter.prototype.emit.call(self, eventName, newEventData, response.callback.bind(response)); } } } }); } } else if (obj && obj.rid != null) { // If incoming message is a response to a previously sent message var ret = self._callbackMap[obj.rid]; if (ret) { clearTimeout(ret.timeout); delete self._callbackMap[obj.rid]; var rehydratedError = scErrors.hydrateError(obj.error); ret.callback(rehydratedError, obj.data); } } else { // The last remaining case is to treat the message as raw Emitter.prototype.emit.call(self, 'raw', message); } }; SCServerSocket.prototype._resetPongTimeout = function () { if (this.server.pingTimeoutDisabled) { return; } var self = this; clearTimeout(this._pingTimeoutTicker); this._pingTimeoutTicker = setTimeout(function() { self._onSCClose(4001); self.socket.close(4001); }, this.server.pingTimeout); }; SCServerSocket.prototype._nextCallId = function () { return this._cid++; }; SCServerSocket.prototype.getState = function () { return this.state; }; SCServerSocket.prototype.getBytesReceived = function () { return this.socket.bytesReceived; }; SCServerSocket.prototype._onSCClose = function (code, data) { clearInterval(this._pingIntervalTicker); clearTimeout(this._pingTimeoutTicker); if (this.state !== this.CLOSED) { var prevState = this.state; this.state = this.CLOSED; if (prevState === this.CONNECTING) { // Private connectAbort event for internal use only Emitter.prototype.emit.call(this, '_connectAbort', code, data); Emitter.prototype.emit.call(this, 'connectAbort', code, data); } else { // Private disconnect event for internal use only Emitter.prototype.emit.call(this, '_disconnect', code, data); Emitter.prototype.emit.call(this, 'disconnect', code, data); } // Private close event for internal use only Emitter.prototype.emit.call(this, '_close', code, data); Emitter.prototype.emit.call(this, 'close', code, data); if (!SCServerSocket.ignoreStatuses[code]) { var closeMessage; if (data) { var reasonString; if (typeof data === 'object') { try { reasonString = JSON.stringify(data); } catch (error) { reasonString = data.toString(); } } else { reasonString = data; } closeMessage = 'Socket connection closed with status code ' + code + ' and reason: ' + reasonString; } else { closeMessage = 'Socket connection closed with status code ' + code; } var err = new SocketProtocolError(SCServerSocket.errorStatuses[code] || closeMessage, code); Emitter.prototype.emit.call(this, 'error', err); } } }; SCServerSocket.prototype.disconnect = function (code, data) { code = code || 1000; if (typeof code !== 'number') { var err = new InvalidArgumentsError('If specified, the code argument must be a number'); Emitter.prototype.emit.call(this, 'error', err); } if (this.state !== this.CLOSED) { var packet = { code: code, data: data }; this.emit('#disconnect', packet); this._onSCClose(code, data); this.socket.close(code); } }; SCServerSocket.prototype.destroy = function (code, data) { this.active = false; this.disconnect(code, data); }; SCServerSocket.prototype.terminate = function () { this.socket.terminate(); }; SCServerSocket.prototype.send = function (data, options) { var self = this; this.socket.send(data, options, function (err) { if (err) { self._onSCClose(1006, err.toString()); } }); }; SCServerSocket.prototype.decode = function (message) { return this.server.codec.decode(message); }; SCServerSocket.prototype.encode = function (object) { return this.server.codec.encode(object); }; SCServerSocket.prototype.sendObjectBatch = function (object) { var self = this; this._batchSendList.push(object); if (this._batchTimeout) { return; } this._batchTimeout = setTimeout(function () { delete self._batchTimeout; if (self._batchSendList.length) { var str; try { str = self.encode(self._batchSendList); } catch (err) { Emitter.prototype.emit.call(self, 'error', err); } if (str != null) { self.send(str); } self._batchSendList = []; } }, this.server.options.pubSubBatchDuration || 0); }; SCServerSocket.prototype.sendObjectSingle = function (object) { var str; try { str = this.encode(object); } catch (err) { Emitter.prototype.emit.call(this, 'error', err); } if (str != null) { this.send(str); } }; SCServerSocket.prototype.sendObject = function (object, options) { if (options && options.batch) { this.sendObjectBatch(object); } else { this.sendObjectSingle(object); } }; SCServerSocket.prototype.emit = function (event, data, callback, options) { var self = this; if (this._localEvents[event] == null) { this.server.verifyOutboundEvent(this, event, data, options, function (err, newData) { var eventObject = { event: event }; if (newData !== undefined) { eventObject.data = newData; } if (err) { if (callback) { eventObject.cid = self._nextCallId(); callback(err, eventObject); } } else { if (callback) { eventObject.cid = self._nextCallId(); var timeout = setTimeout(function () { var error = new TimeoutError("Event response for '" + event + "' timed out"); delete self._callbackMap[eventObject.cid]; callback(error, eventObject); }, self.server.ackTimeout); self._callbackMap[eventObject.cid] = {callback: callback, timeout: timeout}; } if (options && options.useCache && options.stringifiedData != null) { // Optimized self.send(options.stringifiedData); } else { self.sendObject(eventObject); } } }); } else if (event === 'error') { Emitter.prototype.emit.call(this, event, data); } else { var error = new InvalidActionError('The "' + event + '" event is reserved and cannot be emitted on a server socket'); Emitter.prototype.emit.call(this, 'error', error); } }; SCServerSocket.prototype.triggerAuthenticationEvents = function (oldState) { if (oldState !== this.AUTHENTICATED) { var stateChangeData = { oldState: oldState, newState: this.authState, authToken: this.authToken }; Emitter.prototype.emit.call(this, 'authStateChange', stateChangeData); this.server.emit('authenticationStateChange', this, stateChangeData); } Emitter.prototype.emit.call(this, 'authenticate', this.authToken); this.server.emit('authentication', this, this.authToken); }; SCServerSocket.prototype.setAuthToken = function (data, options, callback) { var self = this; var authToken = cloneDeep(data); var oldState = this.authState; this.authState = this.AUTHENTICATED; if (options == null) { options = {}; } else { options = cloneDeep(options); if (options.algorithm != null) { delete options.algorithm; var err = new InvalidArgumentsError('Cannot change auth token algorithm at runtime - It must be specified as a config option on launch'); Emitter.prototype.emit.call(this, 'error', err); } } options.mutatePayload = true; var defaultSignatureOptions = this.server.defaultSignatureOptions; // We cannot have the exp claim on the token and the expiresIn option // set at the same time or else auth.signToken will throw an error. var expiresIn; if (options.expiresIn == null) { expiresIn = defaultSignatureOptions.expiresIn; } else { expiresIn = options.expiresIn; } if (authToken) { if (authToken.exp == null) { options.expiresIn = expiresIn; } else { delete options.expiresIn; } } else { options.expiresIn = expiresIn; } // Always use the default sync/async signing mode since it cannot be changed at runtime. if (defaultSignatureOptions.async != null) { options.async = defaultSignatureOptions.async; } // Always use the default algorithm since it cannot be changed at runtime. if (defaultSignatureOptions.algorithm != null) { options.algorithm = defaultSignatureOptions.algorithm; } this.authToken = authToken; this.server.auth.signToken(authToken, this.server.signatureKey, options, function (err, signedToken) { if (err) { Emitter.prototype.emit.call(self, 'error', err); self._onSCClose(4002, err.toString()); self.socket.close(4002); callback && callback(err); } else { var tokenData = { token: signedToken }; if (self.authToken === authToken) { self.signedAuthToken = signedToken; Emitter.prototype.emit.call(self, 'authTokenSigned', signedToken); } self.emit('#setAuthToken', tokenData, callback); } }); this.triggerAuthenticationEvents(oldState); }; SCServerSocket.prototype.getAuthToken = function () { return this.authToken; }; SCServerSocket.prototype.deauthenticateSelf = function () { var oldState = this.authState; var oldToken = this.authToken; this.signedAuthToken = null; this.authToken = null; this.authState = this.UNAUTHENTICATED; if (oldState !== this.UNAUTHENTICATED) { var stateChangeData = { oldState: oldState, newState: this.authState }; Emitter.prototype.emit.call(this, 'authStateChange', stateChangeData); this.server.emit('authenticationStateChange', this, stateChangeData); } Emitter.prototype.emit.call(this, 'deauthenticate', oldToken); this.server.emit('deauthentication', this, oldToken); }; SCServerSocket.prototype.deauthenticate = function (callback) { this.deauthenticateSelf(); this.emit('#removeAuthToken', null, callback); }; SCServerSocket.prototype.kickOut = function (channel, message, callback) { var self = this; if (channel == null) { Object.keys(this.channelSubscriptions).forEach(function (channelName) { delete self.channelSubscriptions[channelName]; self.channelSubscriptionsCount--; self.emit('#kickOut', {message: message, channel: channelName}); }); } else { delete this.channelSubscriptions[channel]; this.channelSubscriptionsCount--; this.emit('#kickOut', {message: message, channel: channel}); } this.server.brokerEngine.unsubscribeSocket(this, channel, callback); }; SCServerSocket.prototype.subscriptions = function () { var subs = []; for (var i in this.channelSubscriptions) { if (this.channelSubscriptions.hasOwnProperty(i)) { subs.push(i); } } return subs; }; SCServerSocket.prototype.isSubscribed = function (channel) { return !!this.channelSubscriptions[channel]; }; module.exports = SCServerSocket;