UNPKG

drp-mesh

Version:
493 lines (430 loc) 17.1 kB
'use strict'; // Had to remove this so we don't have a circular eval problem //const DRP_Node = require('./node'); //const DRP_SubscribableSource = require('./subscription').DRP_SubscribableSource; const DRP_Subscriber = require('./subscription').DRP_Subscriber; const { DRP_Packet, DRP_Cmd, DRP_Reply, DRP_Reply_Error, DRP_RouteOptions, DRP_CmdError, DRP_ErrorCode } = require('./packet'); const WebSocket = require('ws'); class DRP_Endpoint { /** * * @param {Websocket} wsConn Websocket connection * @param {DRP_Node} drpNode DRP Node * @param {string} endpointID Remote Endpoint ID * @param {string} endpointType Remote Endpoint Type */ constructor(wsConn, drpNode, endpointID, endpointType) { let thisEndpoint = this; /** @type {WebSocket} */ this.wsConn = wsConn || null; /** @type {DRP_Node} */ this.DRPNode = drpNode; if (this.wsConn) { this.wsConn.drpEndpoint = this; } this.EndpointID = endpointID || null; this.EndpointType = endpointType || null; this.EndpointCmds = {}; this.AuthInfo = { type: null, value: null, userInfo: null }; /** @type Object<number,function> */ this.ReplyHandlerQueue = {}; this.TokenNum = 1; /** @type {Object.<string,DRP_Subscriber>} */ this.Subscriptions = {}; /** @type {function} */ this.openCallback; /** @type {function} */ this.closeCallback; this.RegisterMethod("getCmds", "GetCmds"); this.RemoteAddress = this.RemoteAddress; this.RemotePort = this.RemotePort; this.RemoteFamily = this.RemoteFamily; } GetToken() { let token = this.TokenNum; this.TokenNum++; return token; } AddReplyHandler(callback) { let token = this.GetToken(); this.ReplyHandlerQueue[token] = callback; return token; } DeleteReplyHandler(token) { delete this.ReplyHandlerQueue[token]; } /** * Register Endpoint Command * @param {string} methodName Method Name * @param {function(Object.<string,object>, DRP_Endpoint, string)} method Function */ RegisterMethod(methodName, method) { let thisEndpoint = this; // Need to do sanity checks; is the method actually a method? if (typeof method === 'function') { thisEndpoint.EndpointCmds[methodName] = method; } else if (typeof thisEndpoint[method] === 'function') { thisEndpoint.EndpointCmds[methodName] = function (...args) { return thisEndpoint[method](...args); }; } else { thisEndpoint.log("Cannot add EndpointCmds[" + methodName + "]" + " -> sourceObj[" + method + "] of type " + typeof thisEndpoint[method]); } } /** * Send serialized packet data * @param {string} drpPacketString JSON string * @returns {number} Error code (0 good, 1 wsConn not open, 2 send error) */ SendPacketString(drpPacketString) { let thisEndpoint = this; if (thisEndpoint.wsConn.readyState !== WebSocket.OPEN) //return "wsConn not OPEN"; return 1; try { thisEndpoint.wsConn.send(drpPacketString); return 0; } catch (e) { //return e; return 2; } } /** * * @param {string} serviceName DRP Service Name * @param {string} method Service Method * @param {Object} params Method Parameters * @param {boolean} promisify Should we promisify? * @param {function} callback Callback function * @param {DRP_RouteOptions} routeOptions Route Options * @param {string} serviceInstanceID Execute on specific Service Instance ID * @return {Promise} Returned promise */ SendCmd(serviceName, method, params, promisify, callback, routeOptions, serviceInstanceID) { let thisEndpoint = this; let returnVal = null; let token = null; if (promisify) { // We expect a response, using await; add 'resolve' to queue returnVal = new Promise(function (resolve, reject) { token = thisEndpoint.AddReplyHandler((cmdResponse) => { if (cmdResponse.err) { reject(cmdResponse.err) } resolve(cmdResponse.payload); }); }); } else if (callback) { // We expect a response, using callback; add callback to queue if (typeof callback !== 'function') throw { message: 'Callback is not a function', name: 'SendCmd' } token = thisEndpoint.AddReplyHandler(callback); } else { // We don't expect a response; leave reply token null } let packetObj = new DRP_Cmd(serviceName, method, params, token, routeOptions, serviceInstanceID); let packetString = JSON.stringify(packetObj); thisEndpoint.SendPacketString(packetString); return returnVal; } /** * Send reply to received command * @param {string} token Reply token * @param {number} status Reply status [0: fail, 1: success, 2: more data coming] * @param {DRP_Reply_Error} err Reply error object * @param {any} payload Payload to send * @param {DRP_RouteOptions} routeOptions Route options * @returns {number} Error string */ SendReply(token, status, err, payload, routeOptions) { let thisEndpoint = this; let packetString = null; let packetObj = null; try { packetObj = new DRP_Reply(token, status, err, payload, routeOptions); packetString = JSON.stringify(packetObj); } catch (e) { let errObj = new DRP_CmdError("Circular object encountered", DRP_ErrorCode.BADREQUEST, "JSON.stringify"); packetObj = new DRP_Reply(token, 1, errObj, e, null); packetString = JSON.stringify(packetObj); } return thisEndpoint.SendPacketString(packetString); } /** * Process inbound DRP Command * @param {DRP_Cmd} cmdPacket DRP Command */ async ProcessCmd(cmdPacket) { let thisEndpoint = this; var cmdResults = { status: 1, err: null, output: null }; // Make sure params is an Object if (!cmdPacket.params || typeof cmdPacket.params !== 'object') cmdPacket.params = {}; let sendOnly = true; if (typeof cmdPacket.token !== "undefined" && cmdPacket.token !== null) { sendOnly = false; } // If the remote end is a consumer, override authInfo and callerType if (thisEndpoint.AuthInfo && (thisEndpoint.AuthInfo.type === "token" || thisEndpoint.AuthInfo.type === "key")) { cmdPacket.params.__authInfo = thisEndpoint.AuthInfo; cmdPacket.params.__callerType = "RPC"; } // Execute method if (sendOnly) { try { thisEndpoint.DRPNode.ServiceCmd(cmdPacket.serviceName, cmdPacket.method, cmdPacket.params, { targetServiceInstanceID: cmdPacket.serviceInstanceID, limitScope: cmdPacket.limitScope, sendOnly: sendOnly, callingEndpoint: thisEndpoint }) } catch (ex) { // Do nothing, caller is not expecting a response } } else { try { cmdResults.output = await thisEndpoint.DRPNode.ServiceCmd(cmdPacket.serviceName, cmdPacket.method, cmdPacket.params, { targetServiceInstanceID: cmdPacket.serviceInstanceID, limitScope: cmdPacket.limitScope, callingEndpoint: thisEndpoint }); } catch (err) { cmdResults.err = { message: err.message, name: err.name, code: err.code || 500, source: err.source, stack: err.stack, } } // Reply with results let routeOptions = null; if (cmdPacket.routeOptions && cmdPacket.routeOptions.tgtNodeID === thisEndpoint.DRPNode.NodeID) { routeOptions = new DRP_RouteOptions(thisEndpoint.DRPNode.NodeID, cmdPacket.routeOptions.srcNodeID); } thisEndpoint.SendReply(cmdPacket.token, cmdResults.status, cmdResults.err, cmdResults.output, routeOptions); } } /** * Process inbound DRP Reply * @param {DRP_Reply} replyPacket DRP Reply Packet */ async ProcessReply(replyPacket) { let thisEndpoint = this; //console.dir(replyPacket, { "depth": 10 }); // Yes - do we have the token? if (thisEndpoint.ReplyHandlerQueue.hasOwnProperty(replyPacket.token)) { // We have the token - execute the reply callback thisEndpoint.ReplyHandlerQueue[replyPacket.token](replyPacket); // Delete if the status < 2 if (replyPacket.status < 2) { delete thisEndpoint.ReplyHandlerQueue[replyPacket.token]; } } else { // We do not have the token - tell the sender we do not honor this token } } /** * Check whether or not to relay the packet * @param {DRP_Packet} drpPacket DRP Packet * @returns {boolean} Should the packet be relayed? */ ShouldRelay(drpPacket) { let thisEndpoint = this; /* * In order to be relayed, a packet should: * - Have route options * - Specify a tgtNodeID that is not the local Node * - Come from an endpoint that has successfully peered as a Node */ if (drpPacket.routeOptions && drpPacket.routeOptions.tgtNodeID && drpPacket.routeOptions.tgtNodeID !== thisEndpoint.DRPNode.NodeID && thisEndpoint.EndpointType && thisEndpoint.EndpointType === "Node") return true; else return false; } async ReceiveMessage(rawMessage) { let thisEndpoint = this; /** @type {DRP_Packet} */ let drpPacket; try { drpPacket = JSON.parse(rawMessage); } catch (e) { thisEndpoint.log(`Received non-JSON message, disconnecting client endpoint[${thisEndpoint.EndpointID}] @ ${thisEndpoint.wsConn._socket.remoteAddress}`); thisEndpoint.log(rawMessage); thisEndpoint.wsConn.close(); return; } // Should we relay the packet? if (thisEndpoint.ShouldRelay(drpPacket)) { // This is meant for another node thisEndpoint.RelayPacket(drpPacket); return; } // Process locally switch (drpPacket.type) { case 'cmd': thisEndpoint.ProcessCmd(drpPacket); break; case 'reply': thisEndpoint.ProcessReply(drpPacket); break; default: thisEndpoint.log("Invalid message.type; here's the JSON data..." + rawMessage); } } /** * Relay DRP Packet * @param {DRP_Packet} drpPacket Packet to relay */ async RelayPacket(drpPacket) { let thisEndpoint = this; try { // Validate sending endpoint if (!thisEndpoint.EndpointID) { // Sending endpoint has not authenticated throw `sending endpoint has not authenticated`; } // Validate source node if (!thisEndpoint.DRPNode.TopologyTracker.ValidateNodeID(drpPacket.routeOptions.srcNodeID)) { // Source NodeID is invalid throw `srcNodeID ${drpPacket.routeOptions.srcNodeID} not found`; } // Validate destination node if (!thisEndpoint.DRPNode.TopologyTracker.ValidateNodeID(drpPacket.routeOptions.tgtNodeID)) { // Target NodeID is invalid throw `tgtNodeID ${drpPacket.routeOptions.tgtNodeID} not found`; } // Verify whether or not we SHOULD relay the node // if (thisEndpoint.DRPNode.IsRegistry() || thisEndpoint.DRPNode.IsRelay()) let nextHopNodeID = thisEndpoint.DRPNode.TopologyTracker.GetNextHop(drpPacket.routeOptions.tgtNodeID); /** @type DRP_Endpoint */ let targetNodeEndpoint = await thisEndpoint.DRPNode.VerifyNodeConnection(nextHopNodeID); // Add this node to the routing history drpPacket.routeOptions.routeHistory.push(thisEndpoint.DRPNode.NodeID); // We do not need to await the results; any target replies will automatically be routed targetNodeEndpoint.SendPacketString(JSON.stringify(drpPacket)); thisEndpoint.DRPNode.PacketRelayCount++; //thisEndpoint.DRPNode.log(`Relaying packet...`); //console.dir(drpPacket); } catch (ex) { // Either could not get connection to node or command send attempt errored out thisEndpoint.DRPNode.log(`Could not relay message: ${ex}`); } } GetCmds() { return Object.keys(this.EndpointCmds); } async OpenHandler() { if (!this.wsConn) return null; this.RemoteAddressPortFamily = `${this.RemoteAddress()}|${this.RemotePort()}|${this.RemoteFamily()}`; } async CloseHandler() { } async ErrorHandler() { } Close() { if (!this.wsConn) return null; this.wsConn.close(); } RemoveSubscriptions() { if (!this.wsConn) return null; let subscriptionIDList = Object.keys(this.Subscriptions); for (let i = 0; i < subscriptionIDList.length; i++) { let subscriptionID = subscriptionIDList[i]; let subscriptionObject = this.Subscriptions[subscriptionID]; subscriptionObject.Terminate(); delete this.Subscriptions[subscriptionID]; this.DRPNode.SubscriptionManager.Subscribers.delete(subscriptionObject); } } IsReady() { if (!this.wsConn) return null; if (this.wsConn && this.wsConn.readyState === 1) return true; else return false; } IsConnecting() { if (!this.wsConn) return null; if (this.wsConn && this.wsConn.readyState === 0) return true; else return false; } /** * @returns {string} Remote Address */ RemoteAddress() { if (!this.wsConn) return null; let returnVal = null; if (this.wsConn && this.wsConn._socket) { returnVal = this.wsConn._socket.remoteAddress; } return returnVal; } /** * @returns {string} Remote Port */ RemotePort() { if (!this.wsConn) return null; let returnVal = null; if (this.wsConn && this.wsConn._socket) { returnVal = this.wsConn._socket.remotePort; } return returnVal; } /** * @returns {string} Remote Family */ RemoteFamily() { if (!this.wsConn) return null; let returnVal = null; if (this.wsConn && this.wsConn._socket) { returnVal = this.wsConn._socket.remoteFamily; } return returnVal; } /** * @returns {number} Uptime in seconds */ UpTime() { if (!this.wsConn) return null; let currentTime = new Date().getTime(); return Math.floor((currentTime - this.wsConn.openTime) / 1000); } /** * @returns {number} Ping time in milliseconds */ PingTime() { if (!this.wsConn) return null; let returnVal = null; if (this.wsConn._socket) { returnVal = this.wsConn.pingTimeMs; } return returnVal; } ConnectionStats() { if (!this.wsConn) return null; return { pingTimeMs: this.PingTime(), uptimeSeconds: this.UpTime() }; } IsServer() { if (!this.wsConn) return null; return this.wsConn._isServer; } log(logMessage) { let thisEndpoint = this; if (thisEndpoint.DRPNode) { thisEndpoint.DRPNode.log(logMessage); } else { console.log(logMessage); } } } module.exports = DRP_Endpoint;