UNPKG

drp-mesh

Version:
1,178 lines (1,054 loc) 208 kB
'use strict'; const os = require('os'); const fs = require('fs'); const util = require('util'); const tcpp = require('tcp-ping'); const tcpPing = util.promisify(tcpp.ping); const ping = require('ping'); const dns = require('dns').promises; const express = require('express'); const bodyParser = require('body-parser'); const DRP_Endpoint = require("./endpoint"); const DRP_Client = require("./client"); const DRP_Service = require("./service"); const { DRP_TopicManager, DRP_TopicManager_Topic } = require("./topicmanager"); const DRP_RouteHandler = require("./routehandler"); const { DRP_MethodParams, DRP_GetParams } = require("./params"); const { DRP_WebServer, DRP_WebServerConfig } = require("./webserver"); const { DRP_SubscribableSource, DRP_Subscriber } = require('./subscription'); const { DRP_AuthRequest, DRP_AuthResponse, DRP_AuthFunction, DRP_AuthInfo } = require('./auth'); const { DRP_Packet, DRP_Cmd, DRP_Reply, DRP_Stream, DRP_RouteOptions, DRP_CmdError, DRP_ErrorCode } = require('./packet'); const { DRP_Permission, DRP_PermissionSet, DRP_Securable, DRP_VirtualFunction, DRP_VirtualDirectory } = require('./securable'); const Express_Request = express.request; const Express_Response = express.response; const { v4: uuidv4 } = require('uuid'); const DRPVersion = "1.0.5" class DRP_RemotePath { /** * Create a reference to a remote path * @param {DRP_Node} localNode * @param {string} targetNodeID * @param {DRP_MethodParams} params */ constructor(localNode, targetNodeID) { this.localNode = localNode; this.targetNodeID = targetNodeID; } /** * Send pathCmd to remote node * @param {DRP_MethodParams} params */ async CallPath(params) { let returnObj = await this.localNode.SendPathCmdToNode(this.targetNodeID, params); return returnObj; } } class DRP_NodeDeclaration { /** * * @param {string} nodeID Node ID * @param {string[]} nodeRoles Functional Roles ['Registry','Broker','Portal','Provider','Producer','Sidecar','Logger'] * @param {string} hostID Host Identifier * @param {string} nodeURL Listening URL (optional) * @param {string} domainName Domain Name * @param {string} meshKey Domain Key * @param {string} zoneName Zone Name * @param {string} scope Scope */ constructor(nodeID, nodeRoles, hostID, nodeURL, domainName, meshKey, zoneName, scope) { this.NodeID = nodeID; this.NodeRoles = nodeRoles; this.HostID = hostID; this.NodeURL = nodeURL; this.DomainName = domainName; this.MeshKey = meshKey; this.Zone = zoneName; this.Scope = scope; } } class DRP_Node extends DRP_Securable { #MeshKey #mTLS #debug #registrySet #useSwagger #rejectUnreachable /** * * @param {string[]} nodeRoles Functional Roles ['Registry','Broker','Provider','Producer','Logger'] * @param {string} hostID Host Identifier * @param {string} domainName DRP Domain Name * @param {string} meshKey DRP Mesh Key * @param {string} zone DRP Zone Name (optional) * @param {DRP_WebServerConfig} webServerConfig Web server config (optional) * @param {string} drpRoute DRP WS Route (optional) * @param {DRP_PermissionSet} permissionSet DRP Permission Set */ constructor(nodeRoles, hostID, domainName, meshKey, zone, webServerConfig, drpRoute, permissionSet) { super(permissionSet); let thisNode = this; this.NodeID = `${os.hostname()}-${process.pid}`; this.HostID = hostID; /** @type {DRP_WebServer} */ this.WebServer = null; this.WebServerConfig = webServerConfig || null; this.drpRoute = drpRoute || "/"; this.DomainName = domainName; this.#MeshKey = meshKey || null; this.#mTLS = null; //if (!this.#MeshKey) this.Die("No MeshKey provided"); this.Zone = zone || "default"; /** @type{string} */ this.RegistryUrl = null; this.Debug = false; this.RegistrySet = null; this.UseSwagger = false; /** @type{string} */ this.AuthenticationServiceName = null; this.HasConnectedToMesh = false; /** @type{function} */ this.onControlPlaneConnect = null; this.ListeningURL = null; this.PendingRegistryConnections = new Set(); /** @type{string[]} */ this.NodeRoles = nodeRoles || []; /** @type{string} */ this.WebProxyURL = null; // If we have a web server config, start listening if (this.WebServerConfig && this.WebServerConfig.ListeningURL) { this.WebServer = new DRP_WebServer(webServerConfig); this.WebServer.start(); this.ListeningURL = this.WebServer.config.ListeningURL; } // By default, Registry nodes are "connected" to the Control Plane and non-Registry nodes aren't this.isConnectedToControlPlane = thisNode.IsRegistry(); // True if this node is currently attempting to connect to the control plane this.isConnectingToControlPlane = false; // Wait time for Registry reconnect attempts this.ReconnectWaitTimeSeconds = 0; /** @type {Object.<string,DRP_NodeClient>} */ this.NodeEndpoints = {}; /** @type {Object.<string,DRP_NodeClient>} */ this.ConsumerEndpoints = {}; // Create topic manager - Handles stream messaging this.TopicManager = new DRP_TopicManager(thisNode); // Create subscription manager - Handles client subscriptions this.SubscriptionManager = new DRP_SubscriptionManager(thisNode); // Create topology tracker - Processes changes in mesh topology this.TopologyTracker = new DRP_TopologyTracker(thisNode); // Add this node to TopologyTracker let newNodeEntry = new DRP_NodeTableEntry(thisNode.NodeID, null, nodeRoles, this.ListeningURL, "global", this.Zone, this.NodeID, this.HostID); let addNodePacket = new DRP_TopologyPacket(newNodeEntry.NodeID, "add", "node", newNodeEntry.NodeID, newNodeEntry.Scope, newNodeEntry.Zone, newNodeEntry); thisNode.TopologyTracker.ProcessPacket(addNodePacket, this.NodeID); /** @type Object.<string,DRP_Service> */ this.Services = {}; /** @type Object.<string,DRP_AuthResponse> */ this.ConsumerTokens = {}; // Add a route handler even if we don't have an Express server (needed for stream relays) this.RouteHandler = new DRP_RouteHandler(this, this.drpRoute); this.PacketRelayCount = 0; this.TCPPing = this.TCPPing; this.GetServiceDefinitions = this.GetServiceDefinitions; this.ListClassInstances = this.ListClassInstances; this.ToggleDebug = this.ToggleDebug; this.GetDebug = () => { return this.Debug; } this.Evacuate = this.Evacuate; this.Mesh = async (params, callingEndpoint) => { let pathData = await thisNode.PathCmd(params, thisNode.GetBaseObj().Mesh, callingEndpoint); return pathData; } let localDRPEndpoint = new DRP_Endpoint(null, this, "Local"); this.ApplyNodeEndpointMethods(localDRPEndpoint); let DRPService = new DRP_Service("DRP", this, "DRP", null, false, 10, 10, this.Zone, "local", null, ["Console", "TopologyTracker"], 1, DRPVersion); DRPService.ClientCmds = localDRPEndpoint.EndpointCmds; DRPService.Streams['Console'].MaxHistoryLength = 1000; DRPService.Streams['TopologyTracker'].MaxHistoryLength = 1000; // Add hook for watching topology for Subscription Manager let topologySubscription = new DRP_Subscriber("DRP", "TopologyTracker", "local", null, null, false, (topologyPacket) => { this.SubscriptionManager.ProcessTopologyPacket(topologyPacket.Message); }, null); this.TopicManager.SubscribeToTopic(topologySubscription); this.AddService(DRPService); } /** @type boolean */ get Debug() { return this.#debug } set Debug(val) { this.#debug = this.IsTrue(val) } /** @type Object.<string,Object> */ get RegistrySet() { return this.#registrySet } set RegistrySet(val) { if (!val) { this.#registrySet = null; return; } this.#registrySet = val.split(/,/).reduce((map, registryEntry) => { let [host, port] = registryEntry.split(/:/); if (!host || !port) { throw new Error(`Invalid registry entry [${registryEntry}], expecting "{host}:{port}"`); } let key = `${host}-${port}`; map[key] = { name: host, port: port, pingInfo: null }; return map; }, {}); } get mTLS() { return this.#mTLS } set mTLS(val) { if (!val || !val.certFile || !val.keyFile) { this.#mTLS = null; return; } this.#mTLS = { cert: fs.readFileSync(val.certFile), key: fs.readFileSync(val.keyFile) } } /** @type boolean */ get UseSwagger() { return this.#useSwagger } set UseSwagger(val) { this.#useSwagger = this.IsTrue(val) } /** @type boolean */ get RejectUnreachable() { return this.#rejectUnreachable } set RejectUnreachable(val) { this.#rejectUnreachable = this.IsTrue(val) } /** * Print message to stdout * @param {string} message Message to output * @param {boolean} isDebugMsg Is it a debug message? */ log(message, isDebugMsg) { // If it's a debug message and we don't have debugging turned on, return if (!this.Debug && isDebugMsg) return; let paddedNodeID = this.NodeID.padEnd(14, ' '); let outputMsg = `${this.getTimestamp()} [${paddedNodeID}] -> ${message}`; console.log(outputMsg); if (this.TopicManager) { this.TopicManager.SendToTopic("DRP", "Console", outputMsg); } } getTimestamp() { let date = new Date(); let hour = date.getHours(); hour = (hour < 10 ? "0" : "") + hour; let min = date.getMinutes(); min = (min < 10 ? "0" : "") + min; let sec = date.getSeconds(); sec = (sec < 10 ? "0" : "") + sec; let ms = date.getMilliseconds(); if (ms < 10) { ms = "00" + ms; } else if (ms < 100) { ms = "0" + ms; } let year = date.getFullYear(); let month = date.getMonth() + 1; month = (month < 10 ? "0" : "") + month; let day = date.getDate(); day = (day < 10 ? "0" : "") + day; return year + "" + month + "" + day + "" + hour + "" + min + "" + sec + "." + ms; } ToggleDebug() { if (this.Debug) { this.Debug = false; } else { this.Debug = true; } return this.Debug; } __GetNodeDeclaration() { let thisNode = this; return new DRP_NodeDeclaration(thisNode.NodeID, thisNode.NodeRoles, thisNode.HostID, thisNode.ListeningURL, thisNode.DomainName, thisNode.#MeshKey, thisNode.Zone); } async GetConsumerToken(username, password) { let thisNode = this; let returnToken = null; /** @type {DRP_AuthResponse} */ let authResults = await thisNode.Authenticate(username, password, null); if (authResults) { thisNode.ConsumerTokens[authResults.Token] = authResults; // If this Node is a Broker, relay to other Brokers in Zone if (thisNode.IsBroker()) { let nodeIDList = Object.keys(thisNode.TopologyTracker.NodeTable); for (let i = 0; i < nodeIDList.length; i++) { let checkNode = thisNode.TopologyTracker.NodeTable[nodeIDList[i]]; if (checkNode.NodeID !== thisNode.NodeID && checkNode.IsBroker() && checkNode.Zone === thisNode.Zone) { // Send command to remote Broker thisNode.ServiceCmd("DRP", "addConsumerToken", { tokenPacket: authResults }, { targetNodeID: checkNode.NodeID, sendOnly: true }); } } } returnToken = authResults.Token; } return returnToken; } async GetConsumerTokenAnon() { let thisNode = this; let returnToken = null; /** @type {DRP_AuthResponse} */ let authResults = new DRP_AuthResponse(uuidv4(), "Anonymous", "Anonymous", [], null, "Anonymous", thisNode.getTimestamp()); thisNode.ConsumerTokens[authResults.Token] = authResults; returnToken = authResults.Token; return returnToken; } GetLastTokenForUser(userName) { let thisNode = this; let tokenList = Object.keys(thisNode.ConsumerTokens).reverse(); for (let i = 0; i < tokenList.length; i++) { let checkTokenID = tokenList[i]; let thisToken = thisNode.ConsumerTokens[tokenList[i]]; if (userName === thisToken.UserName) return checkTokenID; } return null; } /** * * @param {string} restRoute Route to listen for Node REST requests * @param {string} basePath Base path list * @param {boolean} writeToLogger If true, output REST Logs to Logger * @returns {number} Failure code */ EnableREST(webServer, restRoute, basePath, writeToLogger) { let thisNode = this; if (!webServer || !webServer.expressApp) return 1; // Set Consumer Token Cleanup Interval - 60 seconds let checkFrequencyMs = 60000; setInterval(() => { let iCurrentTimestamp = parseInt(thisNode.getTimestamp()); let consumerTokenIDList = Object.keys(thisNode.ConsumerTokens); // Collect tokens from ConsumerEndpoints let connectedTokenList = []; let consumerEndpointIDList = Object.keys(thisNode.ConsumerEndpoints); for (let i = 0; i < consumerEndpointIDList.length; i++) { let consumerEndpointID = consumerEndpointIDList[i]; connectedTokenList.push(thisNode.ConsumerEndpoints[consumerEndpointID].AuthInfo.value); } // TO DO - if clients automatically time out, we need to add a keepalive so that // tokens of consumers connected to this Broker are not removed from other Brokers for (let i = 0; i < consumerTokenIDList.length; i++) { let checkToken = consumerTokenIDList[i]; let checkTokenObj = thisNode.ConsumerTokens[checkToken]; let iCheckTimestamp = parseInt(checkTokenObj.AuthTimestamp); // Skip if currently connected if (connectedTokenList.includes(checkToken)) continue; // Expire after 30 minutes let maxAgeSeconds = 60 * 30; if (iCurrentTimestamp > iCheckTimestamp + maxAgeSeconds) { // The token has expired delete thisNode.ConsumerTokens[checkToken]; } } }, checkFrequencyMs); let tmpBasePath = basePath || ""; let basePathArray = tmpBasePath.replace(/^\/|\/$/g, '').split('/'); // Get a list of valid HTTP codes, convert to numeric values for later comparison let validHttpCodes = Object.keys(require('http').STATUS_CODES).map(statusCode => parseInt(statusCode)); /** * * @param {Express_Request} req Request * @param {Express_Response} res Response * @param {function} next Next step */ let nodeRestHandler = async function (req, res, next) { OUTERTRY: try { // Get Auth Key let authInfo = { type: null, value: null }; // Check for authInfo: // x-api-key - apps (static) // x-api-token - users (dynamic) let xapikey = null; let xapitoken = null; if (req.headers['x-api-key']) { xapikey = req.headers['x-api-key']; } else if (req.query['x-api-key']) { xapikey = req.query['x-api-key']; } else if (req.headers.cookie && /^x-api-key=.*$/.test(req.headers.cookie)) { xapikey = req.headers.cookie.match(/^x-api-key=(.*)$/)[1]; } else if (req.headers['x-api-token']) { xapitoken = req.headers['x-api-token']; } else if (req.headers.cookie && /^x-api-token=.*$/.test(req.headers.cookie)) { xapitoken = req.headers.cookie.match(/^x-api-token=(.*)$/)[1]; } else if (req.headers['x-gitlab-token']) { xapikey = req.headers['x-gitlab-token'] } if (thisNode.IsSidecar()) { // No auth required from client, pass through sidecar creds authInfo.type = 'sidecar'; authInfo.value = null; } else if (xapikey) { authInfo.type = 'key'; authInfo.value = xapikey; } else if (xapitoken) { authInfo.type = 'token'; authInfo.value = xapitoken; // Make sure the token is current if (!thisNode.ConsumerTokens[authInfo.value]) { // We don't recognize this token res.status(401).send("Invalid token"); return; } authInfo.userInfo = thisNode.ConsumerTokens[authInfo.value]; } else { // Unauthorized res.status(401).send("No x-api-token or x-api-key provided"); return; } // Turn path into list, remove first element let decodedPath = decodeURIComponent(req.path); let remainingPath = decodedPath.replace(/^\/|\/$/g, '').split('/'); remainingPath.shift(); // Init vars let format = null; let verb = null; let resultString = ""; let resCode = 200; // Reserved - format JSON output if (req.query.__format) format = thisNode.IsTrue(req.query.__format); // Reserved - override verb if (req.query.__verb) verb = req.query.__verb; // HTTP Verb Map let httpMethodToVerbMap = { GET: "GetItem", POST: "SetItem", PUT: "SetItem", DELETE: "RemoveItem" }; RUNTRY: try { // Client did not specify verb if (!verb) { // Get verb from HTTP method verb = httpMethodToVerbMap[req.method]; if (!verb) { resultString = `Invalid method: ${req.method}`; resCode = DRP_ErrorCode.BADREQUEST; break RUNTRY; } } let params = new DRP_MethodParams(verb, basePathArray.concat(remainingPath), req.body, req.query, "REST", authInfo); let resultObj = await thisNode.PathCmd(params, thisNode.GetBaseObj()); try { // The res.send() method cannot accept numbers. Convert the result if it's not already a string. let objectType = typeof resultObj; switch (objectType) { case "string": resultString = resultObj; break; case "number": case "boolean": resultString = resultObj.toString(); break; default: resultString = JSON.stringify(resultObj, null, format); } } catch { resultString = "Could not convert result to string"; resCode = DRP_ErrorCode.BADREQUEST; } } catch (ex) { // An exception was thrown at some point in the call. If the code isn't a valid HTTP code, use 500. if (validHttpCodes.includes(ex.code)) { resCode = ex.code; } else { resCode = 500; } resultString = ex.message; } // Send response to client res.status(resCode).send(resultString); // Create message for logging let logMessage = { req: { hostname: req.hostname, ip: req.ip, method: req.method, protocol: req.protocol, path: req.path, headers: Object.assign({}, req.headers), query: req.query, baseUrl: req.baseUrl, body: req.body }, res: { code: resCode, length: resultString.length } }; // Remove authorization header for security purposes delete logMessage.req.headers["authorization"]; // Send log to RESTLogs topic thisNode.TopicManager.SendToTopic("DRP", "RESTLogs", logMessage); // Write to Logger service if flag is set if (writeToLogger) { thisNode.ServiceCmd("Logger", "writeLog", { serviceName: "REST", logData: logMessage }, { sendOnly: true }); } return; } catch (ex) { console.log(`Could not respond to REST request:`); console.dir(ex); console.dir({ url: req.url, method: req.method, headers: req.headers, body: req.body, ip: req.ip }); } }; // Assign REST handler webServer.expressApp.all(`${restRoute}`, bodyParser.urlencoded({ extended: true }), bodyParser.json(), nodeRestHandler); webServer.expressApp.all(`${restRoute}/*`, bodyParser.urlencoded({ extended: true }), bodyParser.json(), nodeRestHandler); // Get Token webServer.expressApp.post('/token', async (req, res) => { // Get an auth token let username = req.body.username; let password = req.body.password; let userToken = await GetConsumerToken(username, password); if (userToken) { res.send(`x-api-token: ${userToken}`); } else { res.status(401).send("Bad credentials"); } return; }); return 0; } /** * Return a dictionary of class names, services and providers * @param {DRP_MethodParams} params Parameters * @param {object} callingEndpoint Calling Endpoint * @param {token} token Command token * @returns {Object.<string,Object.<string,{providers:string[]}>>} Class instances */ async ListClassInstances(params, callingEndpoint, token) { let thisNode = this; let results = {}; let findClassName = null; if (params && params.className) findClassName = params.className; else if (params && params.__pathList && params.__pathList.length > 0) findClassName = params.__pathList.shift(); // Get best instance for each service let serviceDefinitions = await thisNode.GetServiceDefinitions(params, callingEndpoint); // Get best instance of each service let serviceList = Object.keys(serviceDefinitions); for (let i = 0; i < serviceList.length; i++) { let serviceName = serviceList[i]; let serviceInstanceObj = serviceDefinitions[serviceName]; let classList = Object.keys(serviceInstanceObj.Classes); for (let k = 0; k < classList.length; k++) { let className = classList[k]; if (!findClassName || findClassName === className) { if (!results[className]) { results[className] = {}; } if (!results[className][serviceName]) { results[className][serviceName] = { providers: [] }; } results[className][serviceName].providers.push(serviceInstanceObj.NodeID); } } } return results; } GetServiceDefinition(params) { /* * We need to return: * { * "TestService": {ClientCmds: {}, Classes:{}, Streams:{}} * } */ let thisNode = this; let targetService = thisNode.Services[params.serviceName]; let serviceDefinition = targetService.GetDefinition(); return serviceDefinition; } async GetServiceDefinitions(params, callingEndpoint) { /* * We need to return: * { * "TestService": {ClientCmds: {}, Classes:{}, Streams:{}} * } */ let thisNode = this; let serviceDefinitions = {}; let serviceNameList = thisNode.TopologyTracker.ListServices(); for (let i = 0; i < serviceNameList.length; i++) { let serviceName = serviceNameList[i]; // Unlike before, we don't distribute the actual service definitions in // the declarations. We need to go out to each service and get the info let bestInstance = thisNode.TopologyTracker.FindInstanceOfService(serviceName); let response = await thisNode.ServiceCmd("DRP", "getServiceDefinition", { serviceName: serviceName }, { targetNodeID: bestInstance.NodeID, targetServiceInstanceID: bestInstance.InstanceID, useControlPlane: true, callingEndpoint: callingEndpoint }); response.NodeID = bestInstance.NodeID; serviceDefinitions[serviceName] = response; } return serviceDefinitions; } /** * Get service definitions for this DRP_Node * @param {DRP_MethodParams} params * @param {any} callingEndpoint */ GetLocalServiceDefinitions(params, callingEndpoint) { /* * We need to return: * { * "TestService": {ClientCmds: {}, Classes:{}, Streams:{}} * } */ let thisNode = this; let serviceDefinitions = {}; let checkServiceName = null; if (params) { if (params.serviceName) checkServiceName = params.serviceName; else if (params.__pathList && params.__pathList.length > 0) checkServiceName = params.__pathList.shift(); } let serviceNameList = Object.keys(thisNode.Services); for (let i = 0; i < serviceNameList.length; i++) { let serviceName = serviceNameList[i]; if (checkServiceName && checkServiceName !== serviceName) continue; let serviceDefinition = thisNode.Services[serviceName].GetDefinition(); serviceDefinitions[serviceName] = serviceDefinition; } return serviceDefinitions; } /** * Return class records * @param {DRP_MethodParams} params Parameters * @returns {Object} Class records */ async GetClassRecords(params) { let thisNode = this; let results = {}; // If user didn't supply the className, return null if (!params || !params.className) return null; let thisClassName = params.className; // We need to get a list of all distinct INSTANCES for this class along with the best source for each let classInstances = await thisNode.ListClassInstances(params); // If we don't have data for this class, return null if (!classInstances[thisClassName]) return null; let thisClassObj = classInstances[thisClassName]; // Loop over Class for this service let serviceNames = Object.keys(thisClassObj); for (let j = 0; j < serviceNames.length; j++) { let serviceName = serviceNames[j]; if (params.serviceName && params.serviceName !== serviceName) continue; let recordPath = ["Mesh", "Services", serviceName, "Classes", thisClassName, "cache"]; // Get data let pathParams = new DRP_MethodParams("GetItem", recordPath); let pathData = await thisNode.PathCmd(pathParams, thisNode.GetBaseObj()); results[serviceName] = pathData; } return results; } /** * Send PathCmd to Remote Node * @param {string} targetNodeID * @param {Object} params * @returns {any} */ async SendPathCmdToNode(targetNodeID, params) { let thisNode = this; let oReturnObject = null; if (targetNodeID === thisNode.NodeID) { // The target NodeID is local oReturnObject = thisNode.PathCmd(params, thisNode.GetBaseObj()); } else { oReturnObject = await thisNode.ServiceCmd("DRP", "pathCmd", params, { targetNodeID: targetNodeID }); } return oReturnObject; } GetBaseObj() { let thisNode = this; let pathFuncs = { Nodes: { /** * Get dictionary of available nodes in Mesh, override type as DRP_RemotePath * @param {DRP_MethodParams} params * @param {string} zoneName */ List: async (params, zoneName) => { let nodeList = thisNode.TopologyTracker.ListNodes(zoneName); let returnObj = nodeList.reduce((map, nodeID) => { map[nodeID] = new DRP_RemotePath(); return map; }, {}); return returnObj; }, /** * Find node and return a DRP_RemotePath command object * @param {DRP_MethodParams} params * @param {string} zoneName */ Get: async (params, zoneName) => { let targetNodeID = params.__pathList.shift(); let checkEntry = thisNode.TopologyTracker.NodeTable[targetNodeID]; if (!checkEntry) { throw new DRP_CmdError(`Node [${targetNodeID}] does not exist`, DRP_ErrorCode.NOTFOUND, "PathCmd"); } if (zoneName && checkEntry.Zone !== zoneName) { throw new DRP_CmdError(`Node [${targetNodeID}] is in Zone [${checkEntry.Zone}], not Zone [${zoneName}]`, DRP_ErrorCode.NOTFOUND, "PathCmd"); } let returnObj = {} returnObj = new DRP_RemotePath(thisNode, targetNodeID, params); return returnObj; }, }, Services: { /** * Get dictionary of available services in Mesh, override type as DRP_RemotePath * @param {DRP_MethodParams} params * @param {string} zoneName */ List: async (params, zoneName) => { let serviceList = Object.keys(thisNode.TopologyTracker.GetServicesWithProviders(zoneName)); let returnObj = serviceList.reduce((map, serviceName) => { map[serviceName] = new DRP_RemotePath(); return map; }, {}); return returnObj; }, /** * Find an instance of the service and send a command to that path * @param {DRP_MethodParams} params * @param {string} zoneName */ Get: async (params, zoneName) => { let serviceName = params.__pathList.shift(); params.__pathList = ['Services', serviceName].concat(params.__pathList); let limitScope = zoneName ? "zone" : null; let targetServiceEntry = thisNode.TopologyTracker.FindInstanceOfService(serviceName, null, zoneName, null, limitScope); if (!targetServiceEntry) { throw new DRP_CmdError("Service not found", DRP_ErrorCode.NOTFOUND, "PathCmd"); }; let targetNodeID = targetServiceEntry.NodeID; let returnObj = {} returnObj = new DRP_RemotePath(thisNode, targetNodeID, params); return returnObj; } }, Streams: { /** * List available streams in Mesh * @param {DRP_MethodParams} params * @param {string} zoneName */ List: async (params, zoneName) => { let returnObj = {}; let streamProviders = thisNode.TopologyTracker.GetStreamsWithProviders(zoneName); for (let streamName in streamProviders) { returnObj[streamName] = {} streamProviders[streamName].reduce((map, streamName) => { let newParams = Object.assign({}, params); newParams.__pathList = ['DRPNode', 'TopicManager', 'Topics', streamName].concat(params.__pathList.slice(1)); map[streamName] = new DRP_RemotePath(thisNode, streamName, newParams); return map; }, returnObj[streamName]); } return returnObj; }, /** * Get dictionary of available services in Mesh, override type as DRP_RemotePath * @param {DRP_MethodParams} params * @param {string} zoneName */ Get: async (params, zoneName) => { let streamName = params.__pathList.shift(); let returnObj = {}; let streamProviders = thisNode.TopologyTracker.GetStreamsWithProviders(zoneName); if (!streamProviders[streamName]) { throw new DRP_CmdError(`Stream [${streamName}] not found`, DRP_ErrorCode.NOTFOUND, "PathCmd"); } returnObj = streamProviders[streamName].reduce((map, providerNodeID) => { let newParams = Object.assign({}, params); newParams.__pathList = ['DRPNode', 'TopicManager', 'Topics', streamName].concat(params.__pathList.slice(1)); map[providerNodeID] = new DRP_RemotePath(thisNode, providerNodeID, newParams); return map; }, returnObj); return returnObj; } } } return { NodeID: thisNode.NodeID, NodeURL: thisNode.ListeningURL, DRPNode: thisNode, Services: thisNode.Services, Streams: thisNode.TopicManager.Topics, Endpoints: { /** * Return Node endpoints * @param {DRP_MethodParams} params */ Nodes: async function (params) { let remainingChildPath = params.__pathList; let oReturnObject = null; if (remainingChildPath && remainingChildPath.length > 0) { let targetNodeID = remainingChildPath.shift(); // Need to send command to remote Node with remaining tree data params.__pathList = remainingChildPath; oReturnObject = await thisNode.SendPathCmdToNode(targetNodeID, params); } else { // Return list of NodeEndpoints oReturnObject = {}; let aNodeKeys = Object.keys(thisNode.NodeEndpoints); for (let i = 0; i < aNodeKeys.length; i++) { oReturnObject[aNodeKeys[i]] = { "ConsumerType": "SomeType1", "Status": "Unknown" }; } } return oReturnObject; }, /** * Return Consumer endpoints * @param {DRP_MethodParams} params */ Consumers: async function (params) { let remainingChildPath = params.__pathList; let oReturnObject = null; if (remainingChildPath && remainingChildPath.length > 0) { let agentID = remainingChildPath.shift(); // Need to send command to consumer with remaining tree data params.__pathList = remainingChildPath; let targetEndpoint = await thisNode.VerifyConsumerConnection(agentID); if (targetEndpoint) { // Await for command from consumer let results = await targetEndpoint.SendCmd("DRP", "pathCmd", params, true, null); if (results) { oReturnObject = results; } } else { thisNode.log(`Could not verify consumer connection for [${agentID}]`, true); } } else { // Return list of consumers oReturnObject = {}; let aConsumerKeys = Object.keys(thisNode.ConsumerEndpoints); for (let i = 0; i < aConsumerKeys.length; i++) { oReturnObject[aConsumerKeys[i]] = { "ConsumerType": "SomeType1", "Status": "Unknown" }; } } return oReturnObject; } }, Mesh: { Topology: thisNode.TopologyTracker, Nodes: new DRP_VirtualDirectory( /** * List Nodes * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Nodes.List(params); }, /** * Get Node * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Nodes.Get(params); }, // Permission set null ), Services: new DRP_VirtualDirectory( /** * List Services * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Services.List(params); }, /** * Get Service * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Services.Get(params); }, // Permission set null ), Streams: new DRP_VirtualDirectory( /** * List Streams * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Streams.List(params); }, /** * Get Stream * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Streams.Get(params); }, // Permission set null ), Zones: new DRP_VirtualDirectory( /** * List Zones * @param {DRP_MethodParams} params */ async (params) => { // Return list of Zones let returnObj = thisNode.TopologyTracker.ListZones().reduce((map, zoneName) => { map[zoneName] = new DRP_RemotePath(); return map; }, {}); return returnObj; }, /** * Get Zone * @param {DRP_MethodParams} params */ async (params) => { let returnObj = {}; // Get Zone let zoneName = params.__pathList.shift(); // Make sure zone exists if (!(thisNode.TopologyTracker.ListZones().includes(zoneName))) { throw new DRP_CmdError(`Zone [${zoneName}] not found`, DRP_ErrorCode.NOTFOUND, "PathCmd"); } // If the local DRPNode is not in the specified zone, we need to route the call to a Registry in the target zone if (zoneName !== thisNode.Zone) { let targetZoneRegistries = thisNode.TopologyTracker.FindRegistriesInZone(zoneName); if (targetZoneRegistries.length == 0) { throw new DRP_CmdError('No registries for zone', DRP_ErrorCode.UNAVAILABLE, "PathCmd"); } let targetNodeID = targetZoneRegistries[0].NodeID; params.__pathList = ['Mesh', 'Zones', zoneName].concat(params.__pathList); returnObj = new DRP_RemotePath(thisNode, targetNodeID, params); return returnObj; } // The local DRPNode is in the specified zone, proceed let levelDefinitions = { Nodes: new DRP_VirtualDirectory( /** * List Nodes * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Nodes.List(params, zoneName); }, /** * Get Node * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Nodes.Get(params, zoneName); }, // Permission set null ), Services: new DRP_VirtualDirectory( /** * List Services * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Services.List(params, zoneName); }, /** * Get Service * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Services.Get(params, zoneName); }, // Permission set null ), Streams: new DRP_VirtualDirectory( /** * List Streams * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Streams.List(params, zoneName); }, /** * Get Stream * @param {DRP_MethodParams} params */ async (params) => { return await pathFuncs.Streams.Get(params, zoneName); }, // Permission set null ) } returnObj = new DRP_VirtualDirectory( /** * List items * @param {DRP_MethodParams} params */ async (params) => { // Return list of target types in Zone let returnObj = Object.keys(levelDefinitions).reduce((map, targetType) => { map[targetType] = new DRP_VirtualDirectory(); return map; }, {}); return returnObj; }, /** * Get item * @param {DRP_MethodParams} params */ async (params) => { let returnObj = {}; let targetType = params.__pathList.shift(); if (!levelDefinitions[targetType]) { throw new DRP_CmdError(`Invalid path`, DRP_ErrorCode.NOTFOUND, "PathCmd"); } returnObj = await levelDefinitions[targetType]; return returnObj; }, // Permission set null ) return returnObj; }, // Permission set null ) } }; } /** * @param {DRP_MethodParams} params Remaining path * @param {Boolean} baseObj Flag to return list of children * @param {object} callingEndpoint Endpoint making request * @returns {object} oReturnObject Return object */ async PathCmd(params, oCurrentObject, callingEndpoint) { /* * REST Calls * params.__method = ('GetItem'|'GetChildItems'|'SetItem') * * RPC Calls * params.__method = ('cat'|'ls'|'exec') * * DRP_VirtualFunction accepts methods 'execute' and 'SetItem' * * Using a subset of PowerShell NavigationCmdle