UNPKG

socketcluster-server

Version:
1,585 lines (1,360 loc) 46 kB
const cloneDeep = require('clone-deep'); const WritableConsumableStream = require('writable-consumable-stream'); const StreamDemux = require('stream-demux'); const AsyncStreamEmitter = require('async-stream-emitter'); const AGAction = require('./action'); const AGRequest = require('ag-request'); const scErrors = require('sc-errors'); const InvalidArgumentsError = scErrors.InvalidArgumentsError; const SocketProtocolError = scErrors.SocketProtocolError; const TimeoutError = scErrors.TimeoutError; const BadConnectionError = scErrors.BadConnectionError; const InvalidActionError = scErrors.InvalidActionError; const AuthError = scErrors.AuthError; const AuthTokenExpiredError = scErrors.AuthTokenExpiredError; const AuthTokenInvalidError = scErrors.AuthTokenInvalidError; const AuthTokenNotBeforeError = scErrors.AuthTokenNotBeforeError; const AuthTokenError = scErrors.AuthTokenError; const BrokerError = scErrors.BrokerError; const HANDSHAKE_REJECTION_STATUS_CODE = 4008; const PONG_RESET_FREQUENCY_DIVISOR = 4; function AGServerSocket(id, server, socket, protocolVersion) { AsyncStreamEmitter.call(this); this.id = id; this.server = server; this.socket = socket; this.state = this.CONNECTING; this.authState = this.UNAUTHENTICATED; this.protocolVersion = protocolVersion; this._receiverDemux = new StreamDemux(); this._procedureDemux = new StreamDemux(); this.request = this.socket.upgradeReq; this.inboundReceivedMessageCount = 0; this.inboundProcessedMessageCount = 0; this.outboundPreparedMessageCount = 0; this.outboundSentMessageCount = 0; this.createRequest = this.server.options.requestCreator || this.defaultRequestCreator; this.cloneData = this.server.options.cloneData; this.inboundMessageStream = new WritableConsumableStream(); this.outboundPacketStream = new WritableConsumableStream(); this.middlewareHandshakeStream = this.request[this.server.SYMBOL_MIDDLEWARE_HANDSHAKE_STREAM]; this.middlewareInboundRawStream = new WritableConsumableStream(); this.middlewareInboundRawStream.type = this.server.MIDDLEWARE_INBOUND_RAW; this.middlewareInboundStream = new WritableConsumableStream(); this.middlewareInboundStream.type = this.server.MIDDLEWARE_INBOUND; this.middlewareOutboundStream = new WritableConsumableStream(); this.middlewareOutboundStream.type = this.server.MIDDLEWARE_OUTBOUND; 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.isBufferingBatch = false; this.isBatching = false; this.batchOnHandshake = this.server.options.batchOnHandshake; this.batchOnHandshakeDuration = this.server.options.batchOnHandshakeDuration; this.batchInterval = this.server.options.batchInterval; this._batchBuffer = []; this._batchingIntervalId = null; this._cid = 1; this._callbackMap = {}; this._lastPongResetTime = 0; this.channelSubscriptions = {}; this.channelSubscriptionsCount = 0; this.socket.on('error', (err) => { this.emitError(err); }); this.socket.on('close', (code, reasonBuffer) => { let reason = reasonBuffer.toString(); this._destroy(code, reason); }); let pongMessage; if (this.protocolVersion === 1) { pongMessage = '#2'; this._sendPing = () => { if (this.state !== this.CLOSED) { this.send('#1'); } }; } else { pongMessage = ''; this._sendPing = () => { if (this.state !== this.CLOSED) { this.send(''); } }; } if (!this.server.pingTimeoutDisabled) { this._pingIntervalTicker = setInterval(() => { this._sendPing(); }, this.server.pingInterval); } this._resetPongTimeout(); this._handshakeTimeoutRef = setTimeout(() => { this._handleHandshakeTimeout(); }, this.server.handshakeTimeout); this.server.pendingClients[this.id] = this; this.server.pendingClientsCount++; this._handleInboundMessageStream(pongMessage); this._handleOutboundPacketStream(); // Receive incoming raw messages this.socket.on('message', async (messageBuffer, isBinary) => { let message = isBinary ? messageBuffer : messageBuffer.toString(); this.inboundReceivedMessageCount++; let isPong = message === pongMessage; if (isPong) { this._resetPongTimeout(); } else { // Reset pong timeout for regular messages if enough time has passed let now = Date.now(); if (now - this._lastPongResetTime > this.server.pingTimeout / PONG_RESET_FREQUENCY_DIVISOR) { this._resetPongTimeout(); } } if (this.server.hasMiddleware(this.server.MIDDLEWARE_INBOUND_RAW)) { let action = new AGAction(); action.socket = this; action.type = AGAction.MESSAGE; action.data = message; try { let {data} = await this.server._processMiddlewareAction(this.middlewareInboundRawStream, action, this); message = data; } catch (error) { this.inboundProcessedMessageCount++; return; } } this.inboundMessageStream.write(message); this.emit('message', {message}); }); } AGServerSocket.prototype = Object.create(AsyncStreamEmitter.prototype); AGServerSocket.CONNECTING = AGServerSocket.prototype.CONNECTING = 'connecting'; AGServerSocket.OPEN = AGServerSocket.prototype.OPEN = 'open'; AGServerSocket.CLOSED = AGServerSocket.prototype.CLOSED = 'closed'; AGServerSocket.AUTHENTICATED = AGServerSocket.prototype.AUTHENTICATED = 'authenticated'; AGServerSocket.UNAUTHENTICATED = AGServerSocket.prototype.UNAUTHENTICATED = 'unauthenticated'; AGServerSocket.ignoreStatuses = scErrors.socketProtocolIgnoreStatuses; AGServerSocket.errorStatuses = scErrors.socketProtocolErrorStatuses; AGServerSocket.prototype.getBackpressure = function () { return Math.max( this.getInboundBackpressure(), this.getOutboundBackpressure(), this.getAllListenersBackpressure(), this.getAllReceiversBackpressure(), this.getAllProceduresBackpressure() ); }; AGServerSocket.prototype.getInboundBackpressure = function () { return this.inboundReceivedMessageCount - this.inboundProcessedMessageCount; }; AGServerSocket.prototype.getOutboundBackpressure = function () { return this.outboundPreparedMessageCount - this.outboundSentMessageCount; }; AGServerSocket.prototype._startBatchOnHandshake = function () { this._startBatching(); setTimeout(() => { if (!this.isBatching) { this._stopBatching(); } }, this.batchOnHandshakeDuration); }; AGServerSocket.prototype.defaultRequestCreator = function (socket, id, procedureName, data) { return new AGRequest(socket, id, procedureName, data); }; // ---- Receiver logic ---- AGServerSocket.prototype.receiver = function (receiverName) { return this._receiverDemux.stream(receiverName); }; AGServerSocket.prototype.closeReceiver = function (receiverName) { this._receiverDemux.close(receiverName); }; AGServerSocket.prototype.closeAllReceivers = function () { this._receiverDemux.closeAll(); }; AGServerSocket.prototype.killReceiver = function (receiverName) { this._receiverDemux.kill(receiverName); }; AGServerSocket.prototype.killAllReceivers = function () { this._receiverDemux.killAll(); }; AGServerSocket.prototype.killReceiverConsumer = function (consumerId) { this._receiverDemux.killConsumer(consumerId); }; AGServerSocket.prototype.getReceiverConsumerStats = function (consumerId) { return this._receiverDemux.getConsumerStats(consumerId); }; AGServerSocket.prototype.getReceiverConsumerStatsList = function (receiverName) { return this._receiverDemux.getConsumerStatsList(receiverName); }; AGServerSocket.prototype.getAllReceiversConsumerStatsList = function () { return this._receiverDemux.getConsumerStatsListAll(); }; AGServerSocket.prototype.getReceiverBackpressure = function (receiverName) { return this._receiverDemux.getBackpressure(receiverName); }; AGServerSocket.prototype.getAllReceiversBackpressure = function () { return this._receiverDemux.getBackpressureAll(); }; AGServerSocket.prototype.getReceiverConsumerBackpressure = function (consumerId) { return this._receiverDemux.getConsumerBackpressure(consumerId); }; AGServerSocket.prototype.hasReceiverConsumer = function (receiverName, consumerId) { return this._receiverDemux.hasConsumer(receiverName, consumerId); }; AGServerSocket.prototype.hasAnyReceiverConsumer = function (consumerId) { return this._receiverDemux.hasConsumerAll(consumerId); }; // ---- Procedure logic ---- AGServerSocket.prototype.procedure = function (procedureName) { return this._procedureDemux.stream(procedureName); }; AGServerSocket.prototype.closeProcedure = function (procedureName) { this._procedureDemux.close(procedureName); }; AGServerSocket.prototype.closeAllProcedures = function () { this._procedureDemux.closeAll(); }; AGServerSocket.prototype.killProcedure = function (procedureName) { this._procedureDemux.kill(procedureName); }; AGServerSocket.prototype.killAllProcedures = function () { this._procedureDemux.killAll(); }; AGServerSocket.prototype.killProcedureConsumer = function (consumerId) { this._procedureDemux.killConsumer(consumerId); }; AGServerSocket.prototype.getProcedureConsumerStats = function (consumerId) { return this._procedureDemux.getConsumerStats(consumerId); }; AGServerSocket.prototype.getProcedureConsumerStatsList = function (procedureName) { return this._procedureDemux.getConsumerStatsList(procedureName); }; AGServerSocket.prototype.getAllProceduresConsumerStatsList = function () { return this._procedureDemux.getConsumerStatsListAll(); }; AGServerSocket.prototype.getProcedureBackpressure = function (procedureName) { return this._procedureDemux.getBackpressure(procedureName); }; AGServerSocket.prototype.getAllProceduresBackpressure = function () { return this._procedureDemux.getBackpressureAll(); }; AGServerSocket.prototype.getProcedureConsumerBackpressure = function (consumerId) { return this._procedureDemux.getConsumerBackpressure(consumerId); }; AGServerSocket.prototype.hasProcedureConsumer = function (procedureName, consumerId) { return this._procedureDemux.hasConsumer(procedureName, consumerId); }; AGServerSocket.prototype.hasAnyProcedureConsumer = function (consumerId) { return this._procedureDemux.hasConsumerAll(consumerId); }; AGServerSocket.prototype._handleInboundMessageStream = async function (pongMessage) { for await (let message of this.inboundMessageStream) { this.inboundProcessedMessageCount++; let isPong = message === pongMessage; if (isPong) { if (this.server.strictHandshake && this.state === this.CONNECTING) { this._destroy(4009); this.socket.close(4009); continue; } let token = this.getAuthToken(); if (this.isAuthTokenExpired(token)) { this.deauthenticate(); } continue; } let packet; try { packet = this.decode(message); } catch (error) { if (error.name === 'Error') { error.name = 'InvalidMessageError'; } this.emitError(error); if (this.server.strictHandshake && this.state === this.CONNECTING) { this._destroy(4009); this.socket.close(4009); } continue; } if (Array.isArray(packet)) { let len = packet.length; for (let i = 0; i < len; i++) { await this._processInboundPacket(packet[i], message); } } else { await this._processInboundPacket(packet, message); } } }; AGServerSocket.prototype._handleHandshakeTimeout = function () { this.disconnect(4005); }; AGServerSocket.prototype._processHandshakeRequest = async function (request) { let data = request.data || {}; let signedAuthToken = data.authToken || null; clearTimeout(this._handshakeTimeoutRef); let authInfo = await this._validateAuthToken(signedAuthToken); let action = new AGAction(); action.request = this.request; action.socket = this; action.type = AGAction.HANDSHAKE_SC; action.data = authInfo; try { await this.server._processMiddlewareAction(this.middlewareHandshakeStream, action); } catch (error) { if (error.statusCode == null) { error.statusCode = HANDSHAKE_REJECTION_STATUS_CODE; } request.error(error); this.disconnect(error.statusCode); return; } let clientSocketStatus = { id: this.id, pingTimeout: this.server.pingTimeout }; let serverSocketStatus = { id: this.id, pingTimeout: this.server.pingTimeout }; let oldAuthState = this.authState; try { await this._processAuthentication(authInfo); if (this.state === this.CLOSED) { return; } } catch (error) { 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(error); serverSocketStatus.authError = error; if (error.isBadToken) { this.deauthenticate(); } } } clientSocketStatus.isAuthenticated = !!this.authToken; serverSocketStatus.isAuthenticated = clientSocketStatus.isAuthenticated; if (this.server.pendingClients[this.id]) { delete this.server.pendingClients[this.id]; this.server.pendingClientsCount--; } this.server.clients[this.id] = this; this.server.clientsCount++; this.state = this.OPEN; if (clientSocketStatus.isAuthenticated) { // Needs to be executed after the connection event to allow // consumers to be setup from inside the connection loop. (async () => { await this.listener('connect').once(); this.triggerAuthenticationEvents(oldAuthState); })(); } // Treat authentication failure as a 'soft' error request.end(clientSocketStatus); if (this.batchOnHandshake) { this._startBatchOnHandshake(); } this.emit('connect', serverSocketStatus); this.server.emit('connection', {socket: this, ...serverSocketStatus}); this.middlewareHandshakeStream.close(); }; AGServerSocket.prototype._processAuthenticateRequest = async function (request) { let signedAuthToken = request.data; let oldAuthState = this.authState; let authInfo = await this._validateAuthToken(signedAuthToken); try { await this._processAuthentication(authInfo); } catch (error) { if (error.isBadToken) { this.deauthenticate(); request.error(error); return; } request.end({ isAuthenticated: !!this.authToken, authError: signedAuthToken == null ? null : scErrors.dehydrateError(error) }); return; } this.triggerAuthenticationEvents(oldAuthState); request.end({ isAuthenticated: !!this.authToken, authError: null }); }; AGServerSocket.prototype._subscribeSocket = async function (channelName, subscriptionOptions) { if (this.server.socketChannelLimit && this.channelSubscriptionsCount >= this.server.socketChannelLimit) { throw new InvalidActionError( `Socket ${this.id} tried to exceed the channel subscription limit of ${this.server.socketChannelLimit}` ); } if (this.channelSubscriptionsCount == null) { this.channelSubscriptionsCount = 0; } if (this.channelSubscriptions[channelName] == null) { this.channelSubscriptions[channelName] = true; this.channelSubscriptionsCount++; } try { await this.server.brokerEngine.subscribeSocket(this, channelName); } catch (error) { delete this.channelSubscriptions[channelName]; this.channelSubscriptionsCount--; throw error; } this.emit('subscribe', { channel: channelName, subscriptionOptions }); this.server.emit('subscription', { socket: this, channel: channelName, subscriptionOptions }); }; AGServerSocket.prototype._processSubscribeRequest = async function (request) { if (this.state === this.OPEN) { let subscriptionOptions = Object.assign({}, request.data); let channelName = subscriptionOptions.channel; delete subscriptionOptions.channel; try { await this._subscribeSocket(channelName, subscriptionOptions); } catch (err) { let error = new BrokerError(`Failed to subscribe socket to the ${channelName} channel - ${err}`); this.emitError(error); request.error(error); return; } request.end(); return; } // This is an invalid state; it means the client tried to subscribe before // having completed the handshake. let error = new InvalidActionError('Cannot subscribe socket to a channel before it has completed the handshake'); this.emitError(error); request.error(error); }; AGServerSocket.prototype._unsubscribeFromAllChannels = function () { const channels = Object.keys(this.channelSubscriptions); return Promise.all(channels.map((channel) => this._unsubscribe(channel))); }; AGServerSocket.prototype._unsubscribe = async function (channel) { if (!this.channelSubscriptions[channel]) { throw new InvalidActionError( `Socket ${this.id} tried to unsubscribe from a channel which it is not subscribed to` ); } try { await this.server.brokerEngine.unsubscribeSocket(this, channel); delete this.channelSubscriptions[channel]; if (this.channelSubscriptionsCount != null) { this.channelSubscriptionsCount--; } this.emit('unsubscribe', {channel}); this.server.emit('unsubscription', {socket: this, channel}); } catch (err) { const error = new BrokerError( `Failed to unsubscribe socket from the ${channel} channel - ${err}` ); this.emitError(error); } }; AGServerSocket.prototype._processUnsubscribePacket = async function (packet) { let channel = packet.data; try { await this._unsubscribe(channel); } catch (err) { let error = new BrokerError( `Failed to unsubscribe socket from the ${channel} channel - ${err}` ); this.emitError(error); } }; AGServerSocket.prototype._processUnsubscribeRequest = async function (request) { let channel = request.data; try { await this._unsubscribe(channel); } catch (err) { let error = new BrokerError( `Failed to unsubscribe socket from the ${channel} channel - ${err}` ); this.emitError(error); request.error(error); return; } request.end(); }; AGServerSocket.prototype._processInboundPublishPacket = async function (packet) { try { await this.server.exchange.invokePublish(packet.data.channel, packet.data.data); } catch (error) { this.emitError(error); } }; AGServerSocket.prototype._processInboundPublishRequest = async function (request) { try { await this.server.exchange.invokePublish(request.data.channel, request.data.data); } catch (error) { this.emitError(error); request.error(error); return; } request.end(); }; AGServerSocket.prototype._processInboundPacket = async function (packet, message) { if (packet && typeof packet.event === 'string') { let eventName = packet.event; let isRPC = typeof packet.cid === 'number'; if (eventName === '#handshake') { if (!isRPC) { let error = new InvalidActionError('Handshake request was malformatted'); this.emitError(error); this._destroy(HANDSHAKE_REJECTION_STATUS_CODE); this.socket.close(HANDSHAKE_REJECTION_STATUS_CODE); return; } let request = this.createRequest(this, packet.cid, eventName, packet.data); await this._processHandshakeRequest(request); this._procedureDemux.write(eventName, request); return; } if (this.server.strictHandshake && this.state === this.CONNECTING) { this._destroy(4009); this.socket.close(4009); return; } if (eventName === '#authenticate') { if (!isRPC) { let error = new InvalidActionError('Authenticate request was malformatted'); this.emitError(error); this._destroy(HANDSHAKE_REJECTION_STATUS_CODE); this.socket.close(HANDSHAKE_REJECTION_STATUS_CODE); return; } // Let AGServer handle these events. let request = this.createRequest(this, packet.cid, eventName, packet.data); await this._processAuthenticateRequest(request); this._procedureDemux.write(eventName, request); return; } if (eventName === '#removeAuthToken') { this.deauthenticateSelf(); this._receiverDemux.write(eventName, packet.data); return; } let action = new AGAction(); action.socket = this; let tokenExpiredError = this._processAuthTokenExpiry(); if (tokenExpiredError) { action.authTokenExpiredError = tokenExpiredError; } let isPublish = eventName === '#publish'; let isSubscribe = eventName === '#subscribe'; let isUnsubscribe = eventName === '#unsubscribe'; if (isPublish) { if (!this.server.allowClientPublish) { let error = new InvalidActionError('Client publish feature is disabled'); this.emitError(error); if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); request.error(error); } return; } if (!packet.data || typeof packet.data.channel !== 'string') { let error = new InvalidActionError('Publish channel name was malformatted'); this.emitError(error); if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); request.error(error); } return; } action.type = AGAction.PUBLISH_IN; action.channel = packet.data.channel; action.data = packet.data.data; } else if (isSubscribe) { if (!packet.data || typeof packet.data.channel !== 'string') { let error = new InvalidActionError('Subscribe channel name was malformatted'); this.emitError(error); if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); request.error(error); } return; } action.type = AGAction.SUBSCRIBE; action.channel = packet.data.channel; action.data = packet.data.data; } else if (isUnsubscribe) { if (typeof packet.data !== 'string') { let error = new InvalidActionError('Unsubscribe channel name was malformatted'); this.emitError(error); if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); request.error(error); } return; } if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); await this._processUnsubscribeRequest(request); this._procedureDemux.write(eventName, request); return; } await this._processUnsubscribePacket(packet); this._receiverDemux.write(eventName, packet.data); return; } else { if (isRPC) { action.type = AGAction.INVOKE; action.procedure = packet.event; if (packet.data !== undefined) { action.data = packet.data; } } else { action.type = AGAction.TRANSMIT; action.receiver = packet.event; if (packet.data !== undefined) { action.data = packet.data; } } } let newData; if (isRPC) { let request = this.createRequest(this, packet.cid, eventName, packet.data); try { let {data} = await this.server._processMiddlewareAction(this.middlewareInboundStream, action, this); newData = data; } catch (error) { request.error(error); return; } if (isSubscribe) { request.data.data = newData; await this._processSubscribeRequest(request); } else if (isPublish) { request.data.data = newData; await this._processInboundPublishRequest(request); } else { request.data = newData; } this._procedureDemux.write(eventName, request); return; } try { let {data} = await this.server._processMiddlewareAction(this.middlewareInboundStream, action, this); newData = data; } catch (error) { return; } if (isPublish) { packet.data.data = newData; await this._processInboundPublishPacket(packet); } this._receiverDemux.write(eventName, newData); return; } if (this.server.strictHandshake && this.state === this.CONNECTING) { this._destroy(4009); this.socket.close(4009); return; } if (packet && typeof packet.rid === 'number') { // If incoming message is a response to a previously sent message let ret = this._callbackMap[packet.rid]; if (ret) { clearTimeout(ret.timeout); delete this._callbackMap[packet.rid]; let rehydratedError = scErrors.hydrateError(packet.error); ret.callback(rehydratedError, packet.data); } return; } // The last remaining case is to treat the message as raw this.emit('raw', {message}); }; AGServerSocket.prototype._resetPongTimeout = function () { if (this.server.pingTimeoutDisabled) { return; } clearTimeout(this._pingTimeoutTicker); this._pingTimeoutTicker = setTimeout(() => { this._destroy(4001); this.socket.close(4001); }, this.server.pingTimeout); this._lastPongResetTime = Date.now(); }; AGServerSocket.prototype._nextCallId = function () { return this._cid++; }; AGServerSocket.prototype.getState = function () { return this.state; }; AGServerSocket.prototype.getBytesReceived = function () { return this.socket.bytesReceived; }; AGServerSocket.prototype.emitError = function (error) { this.emit('error', {error}); this.server.emitWarning(error); }; AGServerSocket.prototype._abortAllPendingEventsDueToBadConnection = function (failureType, code, reason) { Object.keys(this._callbackMap || {}).forEach((i) => { let eventObject = this._callbackMap[i]; delete this._callbackMap[i]; clearTimeout(eventObject.timeout); delete eventObject.timeout; let errorMessage = `Event ${eventObject.event} was aborted due to a bad connection`; let badConnectionError = new BadConnectionError(errorMessage, failureType, code, reason); let callback = eventObject.callback; delete eventObject.callback; callback.call(eventObject, badConnectionError, eventObject); }); }; AGServerSocket.prototype.closeAllMiddlewares = function () { this.middlewareHandshakeStream.close(); this.middlewareInboundRawStream.close(); this.middlewareInboundStream.close(); this.middlewareOutboundStream.close(); }; AGServerSocket.prototype.closeInput = function () { this.inboundMessageStream.close(); }; AGServerSocket.prototype.closeOutput = function () { this.outboundPacketStream.close(); }; AGServerSocket.prototype.closeIO = function () { this.closeInput(); this.closeOutput(); }; AGServerSocket.prototype.closeAllStreams = function () { this.closeAllMiddlewares(); this.closeIO(); this.closeAllReceivers(); this.closeAllProcedures(); this.closeAllListeners(); }; AGServerSocket.prototype.killAllMiddlewares = function () { this.middlewareHandshakeStream.kill(); this.middlewareInboundRawStream.kill(); this.middlewareInboundStream.kill(); this.middlewareOutboundStream.kill(); }; AGServerSocket.prototype.killInput = function () { this.inboundMessageStream.kill(); }; AGServerSocket.prototype.killOutput = function () { this.outboundPacketStream.kill(); }; AGServerSocket.prototype.killIO = function () { this.killInput(); this.killOutput(); }; AGServerSocket.prototype.killAllStreams = function () { this.killAllMiddlewares(); this.killIO(); this.killAllReceivers(); this.killAllProcedures(); this.killAllListeners(); }; AGServerSocket.prototype._destroy = async function (code, reason) { clearInterval(this._pingIntervalTicker); clearTimeout(this._pingTimeoutTicker); this._cancelBatching(); if (this.state === this.CLOSED) { this._abortAllPendingEventsDueToBadConnection('connectAbort', code, reason); } else { if (!reason && AGServerSocket.errorStatuses[code]) { reason = AGServerSocket.errorStatuses[code]; } let prevState = this.state; this.state = this.CLOSED; if (prevState === this.CONNECTING) { this._abortAllPendingEventsDueToBadConnection('connectAbort', code, reason); this.emit('connectAbort', {code, reason}); this.server.emit('connectionAbort', { socket: this, code, reason }); } else { this._abortAllPendingEventsDueToBadConnection('disconnect', code, reason); this.emit('disconnect', {code, reason}); this.server.emit('disconnection', { socket: this, code, reason }); } this.emit('close', {code, reason}); this.server.emit('closure', { socket: this, code, reason }); clearTimeout(this._handshakeTimeoutRef); let isClientFullyConnected = !!this.server.clients[this.id]; if (isClientFullyConnected) { delete this.server.clients[this.id]; this.server.clientsCount--; } let isClientPending = !!this.server.pendingClients[this.id]; if (isClientPending) { delete this.server.pendingClients[this.id]; this.server.pendingClientsCount--; } if (!AGServerSocket.ignoreStatuses[code]) { let closeMessage; if (typeof reason === 'string') { closeMessage = `Socket connection closed with status code ${code} and reason: ${reason}`; } else { closeMessage = `Socket connection closed with status code ${code}`; } let err = new SocketProtocolError(AGServerSocket.errorStatuses[code] || closeMessage, code); this.emitError(err); } await this._unsubscribeFromAllChannels(); let cleanupMode = this.server.options.socketStreamCleanupMode; if (cleanupMode === 'kill') { (async () => { await this.listener('end').once(); this.killAllStreams(); })(); } else if (cleanupMode === 'close') { (async () => { await this.listener('end').once(); this.closeAllStreams(); })(); } this.emit('end'); } }; AGServerSocket.prototype.disconnect = async function (code, reason) { code = code || 1000; if (typeof code !== 'number') { let err = new InvalidArgumentsError('If specified, the code argument must be a number'); this.emitError(err); } if (this.state !== this.CLOSED) { this._destroy(code, reason); this.socket.close(code, reason); } }; AGServerSocket.prototype.terminate = function () { this.socket.terminate(); }; AGServerSocket.prototype.send = function (data, options) { this.socket.send(data, options, (error) => { if (error) { this.emitError(error); this._destroy(1006, error.toString()); } }); }; AGServerSocket.prototype.decode = function (message) { return this.server.codec.decode(message); }; AGServerSocket.prototype.encode = function (object) { return this.server.codec.encode(object); }; AGServerSocket.prototype.startBatch = function () { this.isBufferingBatch = true; this._batchBuffer = []; }; AGServerSocket.prototype.flushBatch = function () { this.isBufferingBatch = false; if (!this._batchBuffer.length) { return; } let serializedBatch = this.serializeObject(this._batchBuffer); this._batchBuffer = []; this.send(serializedBatch); }; AGServerSocket.prototype.cancelBatch = function () { this.isBufferingBatch = false; this._batchBuffer = []; }; AGServerSocket.prototype._startBatching = function () { if (this._batchingIntervalId != null) { return; } this.startBatch(); this._batchingIntervalId = setInterval(() => { this.flushBatch(); this.startBatch(); }, this.batchInterval); }; AGServerSocket.prototype.startBatching = function () { this.isBatching = true; this._startBatching(); }; AGServerSocket.prototype._stopBatching = function () { if (this._batchingIntervalId != null) { clearInterval(this._batchingIntervalId); } this._batchingIntervalId = null; this.flushBatch(); }; AGServerSocket.prototype.stopBatching = function () { this.isBatching = false; this._stopBatching(); }; AGServerSocket.prototype._cancelBatching = function () { if (this._batchingIntervalId != null) { clearInterval(this._batchingIntervalId); } this._batchingIntervalId = null; this.cancelBatch(); }; AGServerSocket.prototype.cancelBatching = function () { this.isBatching = false; this._cancelBatching(); }; AGServerSocket.prototype.serializeObject = function (object) { let str; try { str = this.encode(object); } catch (error) { this.emitError(error); return null; } return str; }; AGServerSocket.prototype.sendObject = function (object) { if (this.isBufferingBatch) { this._batchBuffer.push(object); return; } let str = this.serializeObject(object); if (str != null) { this.send(str); } }; AGServerSocket.prototype._handleOutboundPacketStream = async function () { for await (let packet of this.outboundPacketStream) { if (packet.resolve) { // Invoke has no middleware, so there is no need to await here. (async () => { let result; try { result = await this._invoke(packet.event, packet.data, packet.options); } catch (error) { packet.reject(error); return; } packet.resolve(result); })(); this.outboundSentMessageCount++; continue; } await this._processTransmit(packet.event, packet.data, packet.options); this.outboundSentMessageCount++; } }; AGServerSocket.prototype._transmit = async function (event, data, options) { if (this.cloneData) { data = cloneDeep(data); } this.outboundPreparedMessageCount++; this.outboundPacketStream.write({ event, data, options }); }; AGServerSocket.prototype.transmit = async function (event, data, options) { if (this.state !== this.OPEN) { let error = new BadConnectionError( `Socket transmit ${event} event was aborted due to a bad connection`, 'connectAbort' ); this.emitError(error); return; } this._transmit(event, data, options); }; AGServerSocket.prototype.invoke = async function (event, data, options) { if (this.state !== this.OPEN) { let error = new BadConnectionError( `Socket invoke ${event} event was aborted due to a bad connection`, 'connectAbort' ); this.emitError(error); throw error; } if (this.cloneData) { data = cloneDeep(data); } this.outboundPreparedMessageCount++; return new Promise((resolve, reject) => { this.outboundPacketStream.write({ event, data, options, resolve, reject }); }); }; AGServerSocket.prototype._processTransmit = async function (event, data, options) { let newData; let useCache = options ? options.useCache : false; let packet = {event, data}; let isPublish = event === '#publish'; if (isPublish) { let action = new AGAction(); action.socket = this; action.type = AGAction.PUBLISH_OUT; if (data !== undefined) { action.channel = data.channel; action.data = data.data; } useCache = !this.server.hasMiddleware(this.middlewareOutboundStream.type); try { let {data, options} = await this.server._processMiddlewareAction(this.middlewareOutboundStream, action, this); newData = data; useCache = options == null ? useCache : options.useCache; } catch (error) { return; } } else { newData = packet.data; } if (options && useCache && options.stringifiedData != null && !this.isBufferingBatch) { // Optimized this.send(options.stringifiedData); } else { let eventObject = { event }; if (isPublish) { eventObject.data = data || {}; eventObject.data.data = newData; } else { eventObject.data = newData; } this.sendObject(eventObject); } }; AGServerSocket.prototype._invoke = async function (event, data, options) { options = options || {}; return new Promise((resolve, reject) => { let eventObject = { event, cid: this._nextCallId() }; if (data !== undefined) { eventObject.data = data; } let ackTimeout = options.ackTimeout == null ? this.server.ackTimeout : options.ackTimeout; let timeout = setTimeout(() => { let error = new TimeoutError(`Event response for ${event} event timed out`); delete this._callbackMap[eventObject.cid]; reject(error); }, ackTimeout); this._callbackMap[eventObject.cid] = { event, callback: (err, result) => { if (err) { reject(err); return; } resolve(result); }, timeout }; if (options.useCache && options.stringifiedData != null && !this.isBufferingBatch) { // Optimized this.send(options.stringifiedData); } else { this.sendObject(eventObject); } }); }; AGServerSocket.prototype.triggerAuthenticationEvents = function (oldAuthState) { if (oldAuthState !== this.AUTHENTICATED) { let stateChangeData = { oldAuthState, newAuthState: this.authState, authToken: this.authToken }; this.emit('authStateChange', stateChangeData); this.server.emit('authenticationStateChange', { socket: this, ...stateChangeData }); } this.emit('authenticate', {authToken: this.authToken}); this.server.emit('authentication', { socket: this, authToken: this.authToken }); }; AGServerSocket.prototype.setAuthToken = async function (data, options) { if (this.state === this.CONNECTING) { let err = new InvalidActionError( 'Cannot call setAuthToken before completing the handshake' ); this.emitError(err); throw err; } let authToken = cloneDeep(data); let oldAuthState = this.authState; this.authState = this.AUTHENTICATED; if (options == null) { options = {}; } else { options = {...options}; if (options.algorithm != null) { delete options.algorithm; let err = new InvalidArgumentsError( 'Cannot change auth token algorithm at runtime - It must be specified as a config option on launch' ); this.emitError(err); } } options.mutatePayload = true; let rejectOnFailedDelivery = options.rejectOnFailedDelivery; delete options.rejectOnFailedDelivery; let 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. let 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 algorithm since it cannot be changed at runtime. if (defaultSignatureOptions.algorithm != null) { options.algorithm = defaultSignatureOptions.algorithm; } this.authToken = authToken; let signedAuthToken; try { signedAuthToken = await this.server.auth.signToken(authToken, this.server.signatureKey, options); } catch (error) { this.emitError(error); this._destroy(4002, error.toString()); this.socket.close(4002); throw error; } if (this.authToken === authToken) { this.signedAuthToken = signedAuthToken; this.emit('authTokenSigned', {signedAuthToken}); } this.triggerAuthenticationEvents(oldAuthState); let tokenData = { token: signedAuthToken }; if (rejectOnFailedDelivery) { try { await this.invoke('#setAuthToken', tokenData); } catch (err) { let error; if (err && typeof err.message === 'string') { error = new AuthError(`Failed to deliver auth token to client - ${err.message}`); } else { error = new AuthError( 'Failed to confirm delivery of auth token to client due to malformatted error response' ); } this.emitError(error); throw error; } return; } this.transmit('#setAuthToken', tokenData); }; AGServerSocket.prototype.getAuthToken = function () { return this.authToken; }; AGServerSocket.prototype.deauthenticateSelf = function () { let oldAuthState = this.authState; let oldAuthToken = this.authToken; this.signedAuthToken = null; this.authToken = null; this.authState = this.UNAUTHENTICATED; if (oldAuthState !== this.UNAUTHENTICATED) { let stateChangeData = { oldAuthState, newAuthState: this.authState }; this.emit('authStateChange', stateChangeData); this.server.emit('authenticationStateChange', { socket: this, ...stateChangeData }); } this.emit('deauthenticate', {oldAuthToken}); this.server.emit('deauthentication', { socket: this, oldAuthToken }); }; AGServerSocket.prototype.deauthenticate = async function (options) { this.deauthenticateSelf(); if (options && options.rejectOnFailedDelivery) { try { await this.invoke('#removeAuthToken'); } catch (error) { this.emitError(error); if (options && options.rejectOnFailedDelivery) { throw error; } } return; } this._transmit('#removeAuthToken'); }; AGServerSocket.prototype.kickOut = function (channel, message) { let channels = channel; if (!channels) { channels = Object.keys(this.channelSubscriptions); } if (!Array.isArray(channels)) { channels = [channel]; } return Promise.all(channels.map((channelName) => { this.transmit('#kickOut', {channel: channelName, message}); return this._unsubscribe(channelName); })); }; AGServerSocket.prototype.subscriptions = function () { return Object.keys(this.channelSubscriptions); }; AGServerSocket.prototype.isSubscribed = function (channel) { return !!this.channelSubscriptions[channel]; }; AGServerSocket.prototype._processAuthTokenExpiry = function () { let token = this.getAuthToken(); if (this.isAuthTokenExpired(token)) { this.deauthenticate(); return new AuthTokenExpiredError( 'The socket auth token has expired', token.exp ); } return null; }; AGServerSocket.prototype.isAuthTokenExpired = function (token) { if (token && token.exp != null) { let currentTime = Date.now(); let expiryMilliseconds = token.exp * 1000; return currentTime > expiryMilliseconds; } return false; }; AGServerSocket.prototype._processTokenError = function (err) { if (err) { if (err.name === 'TokenExpiredError') { let authError = new AuthTokenExpiredError(err.message, err.expiredAt); authError.isBadToken = true; return authError; } if (err.name === 'JsonWebTokenError') { let authError = new AuthTokenInvalidError(err.message); authError.isBadToken = true; return authError; } if (err.name === 'NotBeforeError') { let authError = new AuthTokenNotBeforeError(err.message, err.date); // In this case, the token is good; it's just not active yet. authError.isBadToken = false; return authError; } let authError = new AuthTokenError(err.message); authError.isBadToken = true; return authError; } return null; }; AGServerSocket.prototype._emitBadAuthTokenError = function (error, signedAuthToken) { this.emit('badAuthToken', { authError: error, signedAuthToken }); this.server.emit('badSocketAuthToken', { socket: this, authError: error, signedAuthToken }); }; AGServerSocket.prototype._validateAuthToken = async function (signedAuthToken) { let verificationOptions = Object.assign({}, this.server.defaultVerificationOptions, { socket: this }); let authToken; try { authToken = await this.server.auth.verifyToken(signedAuthToken, this.server.verificationKey, verificationOptions); } catch (error) { let authTokenError = this._processTokenError(error); return { signedAuthToken, authTokenError, authToken: null, authState: this.UNAUTHENTICATED }; } return { signedAuthToken, authTokenError: null, authToken, authState: this.AUTHENTICATED }; }; AGServerSocket.prototype._processAuthentication = async function ({signedAuthToken, authTokenError, authToken, authState}) { if (authTokenError) { this.signedAuthToken = null; this.authToken = null; this.authState = this.UNAUTHENTICATED; // If the error is related to the JWT being badly formatted, then we will // treat the error as a socket error. if (signedAuthToken != null) { this.emitError(authTokenError); if (authTokenError.isBadToken) { this._emitBadAuthTokenError(authTokenError, signedAuthToken); } } throw authTokenError; } this.signedAuthToken = signedAuthToken; this.authToken = authToken; this.authState = this.AUTHENTICATED; let action = new AGAction(); action.socket = this; action.type = AGAction.AUTHENTICATE; action.signedAuthToken = this.signedAuthToken; action.authToken = this.authToken; try { await this.server._processMiddlewareAction(this.middlewareInboundStream, action, this); } catch (error) { this.authToken = null; this.authState = this.UNAUTHENTICATED; if (error.isBadToken) { this._emitBadAuthTokenError(error, signedAuthToken); } throw error; } }; module.exports = AGServerSocket;