UNPKG

@omneedia/socketcluster

Version:

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

937 lines (817 loc) 31.2 kB
var SCServerSocket = require('./scserversocket'); var AuthEngine = require('sc-auth').AuthEngine; var formatter = require('sc-formatter'); var EventEmitter = require('events').EventEmitter; var Emitter = require('component-emitter'); var base64id = require('base64id'); var async = require('async'); var url = require('url'); var crypto = require('crypto'); var uuid = require('uuid'); var SCSimpleBroker = require('sc-simple-broker').SCSimpleBroker; var scErrors = require('sc-errors'); var AuthTokenExpiredError = scErrors.AuthTokenExpiredError; var AuthTokenInvalidError = scErrors.AuthTokenInvalidError; var AuthTokenNotBeforeError = scErrors.AuthTokenNotBeforeError; var AuthTokenError = scErrors.AuthTokenError; var SilentMiddlewareBlockedError = scErrors.SilentMiddlewareBlockedError; var InvalidArgumentsError = scErrors.InvalidArgumentsError; var InvalidOptionsError = scErrors.InvalidOptionsError; var InvalidActionError = scErrors.InvalidActionError; var BrokerError = scErrors.BrokerError; var ServerProtocolError = scErrors.ServerProtocolError; var SCServer = function (options) { var self = this; EventEmitter.call(this); var opts = { brokerEngine: new SCSimpleBroker(), wsEngine: 'ws', wsEngineServerOptions: {}, maxPayload: null, allowClientPublish: true, ackTimeout: 10000, handshakeTimeout: 10000, pingTimeout: 20000, pingTimeoutDisabled: false, pingInterval: 8000, origins: '*:*', appName: uuid.v4(), path: '/socketcluster/', authDefaultExpiry: 86400, authSignAsync: false, authVerifyAsync: true, pubSubBatchDuration: null, middlewareEmitWarnings: true }; for (var i in options) { if (options.hasOwnProperty(i)) { opts[i] = options[i]; } } this.options = opts; this.MIDDLEWARE_HANDSHAKE_WS = 'handshakeWS'; this.MIDDLEWARE_HANDSHAKE_SC = 'handshakeSC'; this.MIDDLEWARE_EMIT = 'emit'; this.MIDDLEWARE_SUBSCRIBE = 'subscribe'; this.MIDDLEWARE_PUBLISH_IN = 'publishIn'; this.MIDDLEWARE_PUBLISH_OUT = 'publishOut'; this.MIDDLEWARE_AUTHENTICATE = 'authenticate'; // Deprecated this.MIDDLEWARE_PUBLISH = this.MIDDLEWARE_PUBLISH_IN; this._middleware = {}; this._middleware[this.MIDDLEWARE_HANDSHAKE_WS] = []; this._middleware[this.MIDDLEWARE_HANDSHAKE_SC] = []; this._middleware[this.MIDDLEWARE_EMIT] = []; this._middleware[this.MIDDLEWARE_SUBSCRIBE] = []; this._middleware[this.MIDDLEWARE_PUBLISH_IN] = []; this._middleware[this.MIDDLEWARE_PUBLISH_OUT] = []; this._middleware[this.MIDDLEWARE_AUTHENTICATE] = []; this.origins = opts.origins; this._allowAllOrigins = this.origins.indexOf('*:*') !== -1; this.ackTimeout = opts.ackTimeout; this.handshakeTimeout = opts.handshakeTimeout; this.pingInterval = opts.pingInterval; this.pingTimeout = opts.pingTimeout; this.pingTimeoutDisabled = opts.pingTimeoutDisabled; this.allowClientPublish = opts.allowClientPublish; this.perMessageDeflate = opts.perMessageDeflate; this.httpServer = opts.httpServer; this.socketChannelLimit = opts.socketChannelLimit; this.brokerEngine = opts.brokerEngine; this.appName = opts.appName || ''; this.middlewareEmitWarnings = opts.middlewareEmitWarnings; // Make sure there is always a leading and a trailing slash in the WS path. this._path = opts.path.replace(/\/?$/, '/').replace(/^\/?/, '/'); this.isReady = false; this.brokerEngine.once('ready', function () { self.isReady = true; EventEmitter.prototype.emit.call(self, 'ready'); }); var wsEngine = typeof opts.wsEngine === 'string' ? require(opts.wsEngine) : opts.wsEngine; if (!wsEngine || !wsEngine.Server) { throw new InvalidOptionsError('The wsEngine option must be a path or module name which points ' + 'to a valid WebSocket engine module with a compatible interface'); } var WSServer = wsEngine.Server; if (opts.authPrivateKey != null || opts.authPublicKey != null) { if (opts.authPrivateKey == null) { throw new InvalidOptionsError('The authPrivateKey option must be specified if authPublicKey is specified'); } else if (opts.authPublicKey == null) { throw new InvalidOptionsError('The authPublicKey option must be specified if authPrivateKey is specified'); } this.signatureKey = opts.authPrivateKey; this.verificationKey = opts.authPublicKey; } else { if (opts.authKey == null) { opts.authKey = crypto.randomBytes(32).toString('hex'); } this.signatureKey = opts.authKey; this.verificationKey = opts.authKey; } this.authVerifyAsync = opts.authVerifyAsync; this.authSignAsync = opts.authSignAsync; this.defaultVerificationOptions = { async: this.authVerifyAsync }; if (opts.authVerifyAlgorithms != null) { this.defaultVerificationOptions.algorithms = opts.authVerifyAlgorithms; } else if (opts.authAlgorithm != null) { this.defaultVerificationOptions.algorithms = [opts.authAlgorithm]; } this.defaultSignatureOptions = { expiresIn: opts.authDefaultExpiry, async: this.authSignAsync }; if (opts.authAlgorithm != null) { this.defaultSignatureOptions.algorithm = opts.authAlgorithm; } if (opts.authEngine) { this.auth = opts.authEngine; } else { // Default authentication engine this.auth = new AuthEngine(); } if (opts.codecEngine) { this.codec = opts.codecEngine; } else { // Default codec engine this.codec = formatter; } this.clients = {}; this.clientsCount = 0; this.pendingClients = {}; this.pendingClientsCount = 0; this.exchange = this.brokerEngine.exchange(); var wsServerOptions = opts.wsEngineServerOptions || {}; wsServerOptions.server = this.httpServer; wsServerOptions.verifyClient = this.verifyHandshake.bind(this); if (wsServerOptions.path == null && this._path != null) { wsServerOptions.path = this._path; } if (wsServerOptions.perMessageDeflate == null && this.perMessageDeflate != null) { wsServerOptions.perMessageDeflate = this.perMessageDeflate; } if (wsServerOptions.handleProtocols == null && opts.handleProtocols != null) { wsServerOptions.handleProtocols = opts.handleProtocols; } if (wsServerOptions.maxPayload == null && opts.maxPayload != null) { wsServerOptions.maxPayload = opts.maxPayload; } if (wsServerOptions.clientTracking == null) { wsServerOptions.clientTracking = false; } this.wsServer = new WSServer(wsServerOptions); this.wsServer.on('error', this._handleServerError.bind(this)); this.wsServer.on('connection', this._handleSocketConnection.bind(this)); }; SCServer.prototype = Object.create(EventEmitter.prototype); SCServer.prototype.setAuthEngine = function (authEngine) { this.auth = authEngine; }; SCServer.prototype.setCodecEngine = function (codecEngine) { this.codec = codecEngine; }; SCServer.prototype._handleServerError = function (error) { if (typeof error === 'string') { error = new ServerProtocolError(error); } this.emit('error', error); }; SCServer.prototype._handleSocketError = function (error) { // We don't want to crash the entire worker on socket error // so we emit it as a warning instead. this.emit('warning', error); }; SCServer.prototype._handleHandshakeTimeout = function (scSocket) { scSocket.disconnect(4005); }; SCServer.prototype._subscribeSocket = function (socket, channelOptions, callback) { var self = this; if (!channelOptions) { callback && callback('Socket ' + socket.id + ' provided a malformated channel payload'); return; } if (this.socketChannelLimit && socket.channelSubscriptionsCount >= this.socketChannelLimit) { callback && callback('Socket ' + socket.id + ' tried to exceed the channel subscription limit of ' + this.socketChannelLimit); return; } var channelName = channelOptions.channel; if (typeof channelName !== 'string') { callback && callback('Socket ' + socket.id + ' provided an invalid channel name'); return; } if (socket.channelSubscriptionsCount == null) { socket.channelSubscriptionsCount = 0; } if (socket.channelSubscriptions[channelName] == null) { socket.channelSubscriptions[channelName] = true; socket.channelSubscriptionsCount++; } this.brokerEngine.subscribeSocket(socket, channelName, function (err) { if (err) { delete socket.channelSubscriptions[channelName]; socket.channelSubscriptionsCount--; } else { Emitter.prototype.emit.call(socket, 'subscribe', channelName, channelOptions); self.emit('subscription', socket, channelName, channelOptions); } callback && callback(err); }); }; SCServer.prototype._unsubscribeSocketFromAllChannels = function (socket) { var channels = []; for (var channel in socket.channelSubscriptions) { if (socket.channelSubscriptions.hasOwnProperty(channel)) { channels.push(channel); } } var len = channels.length; for (var i = 0; i < len; i++) { this._unsubscribeSocket(socket, channels[i]); } }; SCServer.prototype._unsubscribeSocket = function (socket, channel) { if (typeof channel !== 'string') { throw new InvalidActionError('Socket ' + socket.id + ' tried to unsubscribe from an invalid channel name'); } if (!socket.channelSubscriptions[channel]) { throw new InvalidActionError('Socket ' + socket.id + ' tried to unsubscribe from a channel which it is not subscribed to'); } delete socket.channelSubscriptions[channel]; if (socket.channelSubscriptionsCount != null) { socket.channelSubscriptionsCount--; } this.brokerEngine.unsubscribeSocket(socket, channel); Emitter.prototype.emit.call(socket, 'unsubscribe', channel); this.emit('unsubscription', socket, channel); }; SCServer.prototype._processTokenError = function (err) { var authError = null; var isBadToken = true; if (err) { if (err.name === 'TokenExpiredError') { authError = new AuthTokenExpiredError(err.message, err.expiredAt); } else if (err.name === 'JsonWebTokenError') { authError = new AuthTokenInvalidError(err.message); } else if (err.name === 'NotBeforeError') { authError = new AuthTokenNotBeforeError(err.message, err.date); // In this case, the token is good; it's just not active yet. isBadToken = false; } else { authError = new AuthTokenError(err.message); } } return { authError: authError, isBadToken: isBadToken }; }; SCServer.prototype._emitBadAuthTokenError = function (scSocket, error, signedAuthToken) { var badAuthStatus = { authError: error, signedAuthToken: signedAuthToken }; Emitter.prototype.emit.call(scSocket, 'badAuthToken', badAuthStatus); this.emit('badSocketAuthToken', scSocket, badAuthStatus); }; SCServer.prototype._processAuthToken = function (scSocket, signedAuthToken, callback) { var self = this; var verificationOptions = Object.assign({socket: scSocket}, this.defaultVerificationOptions); this.auth.verifyToken(signedAuthToken, this.verificationKey, verificationOptions, function (err, authToken) { var oldState = scSocket.authState; if (authToken) { scSocket.signedAuthToken = signedAuthToken; scSocket.authToken = authToken; scSocket.authState = scSocket.AUTHENTICATED; } else { scSocket.signedAuthToken = null; scSocket.authToken = null; scSocket.authState = scSocket.UNAUTHENTICATED; } // If the socket is authenticated, pass it through the MIDDLEWARE_AUTHENTICATE middleware. // If the token is bad, we will tell the client to remove it. // If there is an error but the token is good, then we will send back a 'quiet' error instead // (as part of the status object only). if (scSocket.authToken) { self._passThroughAuthenticateMiddleware({ socket: scSocket, signedAuthToken: scSocket.signedAuthToken, authToken: scSocket.authToken }, function (middlewareError, isBadToken) { if (middlewareError) { scSocket.authToken = null; scSocket.authState = scSocket.UNAUTHENTICATED; if (isBadToken) { self._emitBadAuthTokenError(scSocket, middlewareError, signedAuthToken); } } // If an error is passed back from the authenticate middleware, it will be treated as a // server warning and not a socket error. callback(middlewareError, isBadToken || false, oldState); }); } else { var errorData = self._processTokenError(err); // If the error is related to the JWT being badly formatted, then we will // treat the error as a socket error. if (err && signedAuthToken != null) { Emitter.prototype.emit.call(scSocket, 'error', errorData.authError); if (errorData.isBadToken) { self._emitBadAuthTokenError(scSocket, errorData.authError, signedAuthToken); } } callback(errorData.authError, errorData.isBadToken, oldState); } }); }; SCServer.prototype._handleSocketConnection = function (wsSocket, upgradeReq) { var self = this; if (this.options.wsEngine === 'ws') { // Normalize ws module to match sc-uws module. wsSocket.upgradeReq = upgradeReq; } var id = this.generateId(); var scSocket = new SCServerSocket(id, this, wsSocket); scSocket.exchange = self.exchange; scSocket.on('error', function (err) { self._handleSocketError(err); }); self.pendingClients[id] = scSocket; self.pendingClientsCount++; scSocket.on('#authenticate', function (signedAuthToken, respond) { self._processAuthToken(scSocket, signedAuthToken, function (err, isBadToken, oldState) { if (err) { if (isBadToken) { scSocket.deauthenticate(); } } else { scSocket.triggerAuthenticationEvents(oldState); } var authStatus = { isAuthenticated: !!scSocket.authToken, authError: scErrors.dehydrateError(err) }; if (err && isBadToken) { respond(err, authStatus); } else { respond(null, authStatus); } }); }); scSocket.on('#removeAuthToken', function () { scSocket.deauthenticateSelf(); }); scSocket.on('#subscribe', function (channelOptions, res) { if (!channelOptions) { channelOptions = {}; } else if (typeof channelOptions === 'string') { channelOptions = { channel: channelOptions }; } // This is an invalid state; it means the client tried to subscribe before // having completed the handshake. if (scSocket.state === scSocket.OPEN) { self._subscribeSocket(scSocket, channelOptions, function (err) { if (err) { var error = new BrokerError('Failed to subscribe socket to the ' + channelOptions.channel + ' channel - ' + err); res(error); Emitter.prototype.emit.call(scSocket, 'error', error); } else { if (channelOptions.batch) { res(undefined, undefined, {batch: true}); } else { res(); } } }); } else { var error = new InvalidActionError('Cannot subscribe socket to a channel before it has completed the handshake'); res(error); self.emit('warning', error); } }); scSocket.on('#unsubscribe', function (channel, res) { var error; try { self._unsubscribeSocket(scSocket, channel); } catch (err) { error = new BrokerError('Failed to unsubscribe socket from the ' + channel + ' channel - ' + err.message); } if (error) { res(error); Emitter.prototype.emit.call(scSocket, 'error', error); } else { res(); } }); var cleanupSocket = function (type, code, data) { clearTimeout(scSocket._handshakeTimeoutRef); scSocket.off('#handshake'); scSocket.off('#authenticate'); scSocket.off('#removeAuthToken'); scSocket.off('#subscribe'); scSocket.off('#unsubscribe'); scSocket.off('authenticate'); scSocket.off('authStateChange'); scSocket.off('deauthenticate'); scSocket.off('_disconnect'); scSocket.off('_connectAbort'); var isClientFullyConnected = !!self.clients[id]; if (isClientFullyConnected) { delete self.clients[id]; self.clientsCount--; } var isClientPending = !!self.pendingClients[id]; if (isClientPending) { delete self.pendingClients[id]; self.pendingClientsCount--; } self._unsubscribeSocketFromAllChannels(scSocket); if (type === 'disconnect') { self.emit('_disconnection', scSocket, code, data); self.emit('disconnection', scSocket, code, data); } else if (type === 'abort') { self.emit('_connectionAbort', scSocket, code, data); self.emit('connectionAbort', scSocket, code, data); } self.emit('_closure', scSocket, code, data); self.emit('closure', scSocket, code, data); }; scSocket.once('_disconnect', cleanupSocket.bind(scSocket, 'disconnect')); scSocket.once('_connectAbort', cleanupSocket.bind(scSocket, 'abort')); scSocket._handshakeTimeoutRef = setTimeout(this._handleHandshakeTimeout.bind(this, scSocket), this.handshakeTimeout); scSocket.once('#handshake', function (data, respond) { if (!data) { data = {}; } var signedAuthToken = data.authToken || null; clearTimeout(scSocket._handshakeTimeoutRef); self._passThroughHandshakeSCMiddleware({ socket: scSocket }, function (err, statusCode) { if (err) { var clientSocketErrorStatus = { code: statusCode }; respond(err, clientSocketErrorStatus); scSocket.disconnect(statusCode); return; } self._processAuthToken(scSocket, signedAuthToken, function (err, isBadToken, oldState) { if (scSocket.state === scSocket.CLOSED) { return; } var clientSocketStatus = { id: scSocket.id, pingTimeout: self.pingTimeout }; var serverSocketStatus = { id: scSocket.id, pingTimeout: self.pingTimeout }; if (err) { if (signedAuthToken != null) { // Because the token is optional as part of the handshake, we don't count // it as an error if the token wasn't provided. clientSocketStatus.authError = scErrors.dehydrateError(err); serverSocketStatus.authError = err; if (isBadToken) { scSocket.deauthenticate(); } } } clientSocketStatus.isAuthenticated = !!scSocket.authToken; serverSocketStatus.isAuthenticated = clientSocketStatus.isAuthenticated; if (self.pendingClients[id]) { delete self.pendingClients[id]; self.pendingClientsCount--; } self.clients[id] = scSocket; self.clientsCount++; scSocket.state = scSocket.OPEN; Emitter.prototype.emit.call(scSocket, 'connect', serverSocketStatus); Emitter.prototype.emit.call(scSocket, '_connect', serverSocketStatus); self.emit('_connection', scSocket, serverSocketStatus); self.emit('connection', scSocket, serverSocketStatus); if (clientSocketStatus.isAuthenticated) { scSocket.triggerAuthenticationEvents(oldState); } // Treat authentication failure as a 'soft' error respond(null, clientSocketStatus); }); }); }); // Emit event to signal that a socket handshake has been initiated. // The _handshake event is for internal use (including third-party plugins) this.emit('_handshake', scSocket); this.emit('handshake', scSocket); }; SCServer.prototype.close = function () { this.isReady = false; this.wsServer.close.apply(this.wsServer, arguments); }; SCServer.prototype.getPath = function () { return this._path; }; SCServer.prototype.generateId = function () { return base64id.generateId(); }; SCServer.prototype.addMiddleware = function (type, middleware) { if (!this._middleware[type]) { throw new InvalidArgumentsError(`Middleware type "${type}" is not supported`); // Read more: https://socketcluster.io/#!/docs/middleware-and-authorization } this._middleware[type].push(middleware); }; SCServer.prototype.removeMiddleware = function (type, middleware) { var middlewareFunctions = this._middleware[type]; this._middleware[type] = middlewareFunctions.filter(function (fn) { return fn !== middleware; }); }; SCServer.prototype.verifyHandshake = function (info, cb) { var self = this; var req = info.req; var origin = info.origin; if (origin === 'null' || origin == null) { origin = '*'; } var ok = false; if (this._allowAllOrigins) { ok = true; } else { try { var parts = url.parse(origin); parts.port = parts.port || 80; ok = ~this.origins.indexOf(parts.hostname + ':' + parts.port) || ~this.origins.indexOf(parts.hostname + ':*') || ~this.origins.indexOf('*:' + parts.port); } catch (e) {} } if (ok) { var handshakeMiddleware = this._middleware[this.MIDDLEWARE_HANDSHAKE_WS]; if (handshakeMiddleware.length) { var callbackInvoked = false; async.applyEachSeries(handshakeMiddleware, req, function (err) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_HANDSHAKE_WS + ' middleware was already invoked')); } else { callbackInvoked = true; if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_HANDSHAKE_WS + ' middleware', self.MIDDLEWARE_HANDSHAKE_WS); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } cb(false, 401, typeof err === 'string' ? err : err.message); } else { cb(true); } } }); } else { cb(true); } } else { var err = new ServerProtocolError('Failed to authorize socket handshake - Invalid origin: ' + origin); this.emit('warning', err); cb(false, 403, err.message); } }; SCServer.prototype._isPrivateTransmittedEvent = function (event) { return typeof event === 'string' && event.indexOf('#') === 0; }; SCServer.prototype.verifyInboundEvent = function (socket, eventName, eventData, cb) { var request = { socket: socket, event: eventName, data: eventData }; var token = socket.getAuthToken(); if (this.isAuthTokenExpired(token)) { request.authTokenExpiredError = new AuthTokenExpiredError('The socket auth token has expired', token.exp); socket.deauthenticate(); } this._passThroughMiddleware(request, cb); }; SCServer.prototype.isAuthTokenExpired = function (token) { if (token && token.exp != null) { var currentTime = Date.now(); var expiryMilliseconds = token.exp * 1000; return currentTime > expiryMilliseconds; } return false; }; SCServer.prototype._passThroughMiddleware = function (options, cb) { var self = this; var callbackInvoked = false; var request = { socket: options.socket }; if (options.authTokenExpiredError != null) { request.authTokenExpiredError = options.authTokenExpiredError; } var event = options.event; if (this._isPrivateTransmittedEvent(event)) { if (event === '#subscribe') { var eventData = options.data || {}; request.channel = eventData.channel; request.waitForAuth = eventData.waitForAuth; request.data = eventData.data; if (request.waitForAuth && request.authTokenExpiredError) { // If the channel has the waitForAuth flag set, then we will handle the expiry quietly // and we won't pass this request through the subscribe middleware. cb(request.authTokenExpiredError, eventData); } else { async.applyEachSeries(this._middleware[this.MIDDLEWARE_SUBSCRIBE], request, function (err) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_SUBSCRIBE + ' middleware was already invoked')); } else { callbackInvoked = true; if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_SUBSCRIBE + ' middleware', self.MIDDLEWARE_SUBSCRIBE); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } } if (request.data !== undefined) { eventData.data = request.data; } cb(err, eventData); } } ); } } else if (event === '#publish') { if (this.allowClientPublish) { var eventData = options.data || {}; request.channel = eventData.channel; request.data = eventData.data; async.applyEachSeries(this._middleware[this.MIDDLEWARE_PUBLISH_IN], request, function (err) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_PUBLISH_IN + ' middleware was already invoked')); } else { callbackInvoked = true; if (request.data !== undefined) { eventData.data = request.data; } if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_PUBLISH_IN + ' middleware', self.MIDDLEWARE_PUBLISH_IN); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } cb(err, eventData, request.ackData); } else { if (typeof request.channel !== 'string') { err = new BrokerError('Socket ' + request.socket.id + ' tried to publish to an invalid ' + request.channel + ' channel'); self.emit('warning', err); cb(err, eventData, request.ackData); return; } self.exchange.publish(request.channel, request.data, function (err) { if (err) { self.emit('warning', err); } cb(err, eventData, request.ackData); }); } } } ); } else { var noPublishError = new InvalidActionError('Client publish feature is disabled'); self.emit('warning', noPublishError); cb(noPublishError, options.data); } } else { // Do not allow blocking other reserved events or it could interfere with SC behaviour cb(null, options.data); } } else { request.event = event; request.data = options.data; async.applyEachSeries(this._middleware[this.MIDDLEWARE_EMIT], request, function (err) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_EMIT + ' middleware was already invoked')); } else { callbackInvoked = true; if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_EMIT + ' middleware', self.MIDDLEWARE_EMIT); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } } cb(err, request.data); } } ); } }; SCServer.prototype._passThroughAuthenticateMiddleware = function (options, cb) { var self = this; var callbackInvoked = false; var request = { socket: options.socket, authToken: options.authToken }; async.applyEachSeries(this._middleware[this.MIDDLEWARE_AUTHENTICATE], request, function (err, results) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_AUTHENTICATE + ' middleware was already invoked')); } else { callbackInvoked = true; var isBadToken = false; if (results.length) { isBadToken = results[results.length - 1] || false; } if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_AUTHENTICATE + ' middleware', self.MIDDLEWARE_AUTHENTICATE); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } } cb(err, isBadToken); } } ); }; SCServer.prototype._passThroughHandshakeSCMiddleware = function (options, cb) { var self = this; var callbackInvoked = false; var request = { socket: options.socket }; async.applyEachSeries(this._middleware[this.MIDDLEWARE_HANDSHAKE_SC], request, function (err, results) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_HANDSHAKE_SC + ' middleware was already invoked')); } else { callbackInvoked = true; var statusCode; if (results.length) { statusCode = results[results.length - 1] || 4008; } else { statusCode = 4008; } if (err) { if (err.statusCode != null) { statusCode = err.statusCode; } if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_HANDSHAKE_SC + ' middleware', self.MIDDLEWARE_HANDSHAKE_SC); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } } cb(err, statusCode); } } ); }; SCServer.prototype.verifyOutboundEvent = function (socket, eventName, eventData, options, cb) { var self = this; var callbackInvoked = false; if (eventName === '#publish') { var request = { socket: socket, channel: eventData.channel, data: eventData.data }; async.applyEachSeries(this._middleware[this.MIDDLEWARE_PUBLISH_OUT], request, function (err) { if (callbackInvoked) { self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_PUBLISH_OUT + ' middleware was already invoked')); } else { callbackInvoked = true; if (request.data !== undefined) { eventData.data = request.data; } if (err) { if (err === true || err.silent) { err = new SilentMiddlewareBlockedError('Action was silently blocked by ' + self.MIDDLEWARE_PUBLISH_OUT + ' middleware', self.MIDDLEWARE_PUBLISH_OUT); } else if (self.middlewareEmitWarnings) { self.emit('warning', err); } cb(err, eventData); } else { if (options && request.useCache) { options.useCache = true; } cb(null, eventData); } } } ); } else { cb(null, eventData); } }; module.exports = SCServer;