control.js

  1. const http = require('axios');
  2. const https = require('https');
  3. const ACCOUNT_ACTIVATE_URL = '/AccountActivate';
  4. const SERVICE_LOOKUP_URL = '/ServiceLookup';
  5. const ACCESS_SECRET_URL = '/AccessSecret';
  6. const SERVICE_REGISTER_URL = '/ServiceRegister';
  7. const SERVICE_REREGISTER_URL = '/ServiceReregister';
  8. const SERVICE_UNREGISTER_URL = '/ServiceUnregister';
  9. /**
  10. * @class Control
  11. *
  12. * @description Establishes a PxGrid Control connection. Generally passed to a PxGrid REST Client session.
  13. * @constructor
  14. * @param {Object} options Options for the PxGrid Control instance. See examples for more information.
  15. * @param {string} options.host The IP or URL of the PxGrid Controller. Deprecated in v1.3.0, please use `hosts` array.
  16. * @param {Object} options.hosts An array of PxGrid controllers to attempt connecting to. The first successful connection will be used.
  17. * @param {number} [options.port] The host port to connect to the PxGrid Controller on.
  18. * @param {string} options.client The desired name of the client for the client.
  19. * @param {Buffer} options.clientCert A byte stream of the client public key certificate file to use.
  20. * @param {Buffer} options.clientKey A byte stream of the client private key file to use.
  21. * @param {Buffer} options.caBundle A byte stream of the CA Bundle used to verify the PxGrid Controller's identity.
  22. * @param {Boolean} [options.verifySSL=true] If true, verify server's SSL certificate.
  23. * @param {number} [options.httpTimeout=1000] Value, in milliseconds, to consider a server unavailable.
  24. * @param {string} [options.clientKeyPassword] The password to unlock the client private key file.
  25. * @param {string} [options.secret] The secret to help authenticate a newly registered service.
  26. *
  27. *
  28. * @see {@link https://github.com/cisco-pxgrid/pxgrid-rest-ws/wiki/ Cisco PxGrid 2.0 GitHub Wiki} for more information on the Cisco PxGrid 2.0 implementation.
  29. * @see Client
  30. * @example
  31. *
  32. * const fs = require('fs');
  33. * certs = [];
  34. * certs.clientCert = fs.readFileSync('./certs/publiccert.cer');
  35. * certs.clientKey = fs.readFileSync('./certs/key.pem');
  36. * certs.caBundle = fs.readFileSync('./certs/caBundle.cer');
  37. *
  38. * const Pxgrid = require('pxgrid-node');
  39. *
  40. * const pxgridControlOptions = {
  41. * host: 'my-ise-server.domain.com',
  42. * hosts: ['ise01.domain.com', 'ise02.domain.com']
  43. * client: 'my-node-app',
  44. * clientCert: certs.clientCert,
  45. * clientKey: certs.clientKey,
  46. * caBundle: certs.caBundle,
  47. * clientKeyPassword: false,
  48. * secret: '',
  49. * port: '8910',
  50. * verifySSL: false,
  51. * httpTimeout: 3000
  52. * }
  53. *
  54. * const pxgrid = new Pxgrid.Control(pxgridControlOptions);
  55. */
  56. class Control {
  57. constructor({
  58. host,
  59. hosts,
  60. client,
  61. clientCert,
  62. clientKey,
  63. caBundle,
  64. clientKeyPassword,
  65. secret,
  66. port,
  67. verifySSL,
  68. httpTimeout
  69. }) {
  70. this.config = {
  71. hostname: host,
  72. hosts: hosts,
  73. client,
  74. port: port || '8910',
  75. secret: secret || '',
  76. clientCert,
  77. clientKey,
  78. clientKeyPassword: clientKeyPassword || false,
  79. caBundle,
  80. verifySSL,
  81. httpTimeout: httpTimeout || 1000
  82. };
  83. this.hosts = [];
  84. if (
  85. Array.isArray(this.config.hosts) &&
  86. typeof this.config.hosts !== 'undefined'
  87. ) {
  88. this.hosts = this.config.hosts;
  89. } else {
  90. this.hosts = [];
  91. }
  92. // Maintaining handling of 'hostname' config attr for backwards compatability after supporting HA.
  93. if (
  94. this.config.hostname &&
  95. typeof this.config.hostname === 'string' &&
  96. !this.config.hosts
  97. )
  98. this.hosts[0] = this.config.hostname;
  99. // Further backwards compatability, if host/hostname value is given as array, just place in hosts (alternative is throwing error).
  100. if (
  101. this.config.hostname &&
  102. typeof this.config.hostname.length > 0 &&
  103. !this.config.hosts
  104. )
  105. this.hosts = this.config.hostname;
  106. this.config.verifySSL =
  107. typeof this.config.verifySSL === 'undefined'
  108. ? true
  109. : this.config.verifySSL;
  110. this.httpOptions = {
  111. timeout: this.config.httpTimeout
  112. };
  113. this.httpsOptions = {
  114. cert: this.config.clientCert,
  115. key: this.config.clientKey,
  116. ca: this.config.caBundle,
  117. rejectUnauthorized: this.config.verifySSL
  118. };
  119. if (this.config.clientKeyPassword)
  120. this.httpsOptions.passphrase = clientKeyPassword;
  121. this.registeredServices = [];
  122. if ((!this.config.hostname && !this.config.hosts) || !this.config.client) {
  123. throw new Error(
  124. 'Please define hostname and a Pxgrid client name before connecting to the pxGrid server.'
  125. );
  126. }
  127. }
  128. async _post(url, body, debug = false) {
  129. for (let i = 0; i < this.hosts.length; i++) {
  130. const baseUrl = `https://${this.hosts[i]}:${this.config.port}/pxgrid/control`;
  131. this.basicAuth = Buffer.from(
  132. `${this.config.client}:${this.config.secret}`
  133. ).toString('base64');
  134. const session = http.create({
  135. baseURL: baseUrl,
  136. headers: {
  137. Authorization: `Basic ${this.basicAuth}`,
  138. 'Content-Type': 'application/json',
  139. Accept: 'application/json'
  140. },
  141. httpsAgent: new https.Agent(this.httpsOptions)
  142. });
  143. try {
  144. return await session
  145. .post(url, body, this.httpOptions)
  146. .then(response => {
  147. if (debug) {
  148. console.debug(`URL: ${url}`);
  149. console.debug(`DATA: ${JSON.stringify(response.data)}`);
  150. }
  151. if (response.status == 200) {
  152. return response.data;
  153. } else {
  154. return response.status;
  155. }
  156. })
  157. .catch(error => {
  158. const err = new Error();
  159. err.message = 'Error in POST request from pxGrid client.';
  160. err.status = error.response.status;
  161. err.statusText = error.response.statusText;
  162. throw err;
  163. });
  164. } catch (e) {
  165. if (debug) {
  166. console.debug(`Connection to ${this.hosts[i]} failed, trying next..`);
  167. }
  168. continue;
  169. }
  170. }
  171. throw new Error('None of the provided hosts responded to requests.');
  172. }
  173. _delay(time = 5000) {
  174. return new Promise(function(resolve) {
  175. setTimeout(() => {
  176. resolve();
  177. }, time);
  178. });
  179. }
  180. /**
  181. * @description
  182. * Activate your client pxGrid account on the controller.
  183. *
  184. * This needs to happen one time before using your new client, as well as anytime its state changes to PENDING or DISABLED on the controller.
  185. *
  186. * For simplicity, this is handled *automatically* by Client#connect, but can be done manually, as well.
  187. *
  188. * If the client is not activated, you will fail to interact with pxGrid.
  189. *
  190. * Sometimes, the account will return with a PENDING status which will induce a backoff timer of 60 seconds before retrying. This is normally because the account needs to be activated on the pxGrid Controller (either manually, or automatically). If configured for auto-approval, the activation should work in the next attempt.
  191. *
  192. * @param {string} [accountDesc='pxgrid-node'] - A description for the client you are registering.
  193. * @param {number} [retryInterval=60000] - Retry interval in milliseconds.
  194. * @param {number} [maxRetries=10] - Maximum retries that will be attempted.
  195. * @param {number} [retryAttempt=1] - Which attempt we are on. This is necessary since we use recursion for retries.
  196. * @return {Promise} True if the PxGrid account has been activated on the upstream PxGrid controller.
  197. * @see {@link https://github.com/cisco-pxgrid/pxgrid-rest-ws/wiki/pxGrid-Consumer#accountactivate Cisco PxGrid 2.0 GitHub Wiki - AccountActivate}
  198. * @memberof Control
  199. */
  200. activate(
  201. accountDesc = 'pxgrid-node',
  202. retryInterval = 60000,
  203. maxRetries = 10,
  204. retryAttempt = 1
  205. ) {
  206. const payload = { description: accountDesc };
  207. return (
  208. this._post(ACCOUNT_ACTIVATE_URL, payload)
  209. // eslint-disable-next-line consistent-return
  210. .then(response => {
  211. const state = response.accountState;
  212. if (state === 'ENABLED') {
  213. return true;
  214. // eslint-disable-next-line no-else-return
  215. } else if (state === 'PENDING') {
  216. if (retryAttempt > maxRetries) {
  217. throw new Error(
  218. `Account state is PENDING (likely requires approval). Hit max number of retries (${maxRetries}).`
  219. );
  220. } else {
  221. console.log(
  222. `Account state is PENDING. Retrying in ${retryInterval /
  223. 1000} seconds (attempt: ${retryAttempt}/${maxRetries}). The account may need to be approved on the pxGrid controller.`
  224. );
  225. return this._delay(retryInterval).then(() =>
  226. this.activate(
  227. accountDesc,
  228. retryInterval,
  229. maxRetries,
  230. retryAttempt + 1
  231. )
  232. );
  233. }
  234. } else if (state === 'DISABLED') {
  235. throw new Error(
  236. 'Client failed to activate because the account state is DISABLED! Please enable the account on the pxGrid controller and try again.'
  237. );
  238. }
  239. })
  240. );
  241. }
  242. /**
  243. * @description
  244. * Looks up any nodes publishing the serviceName.
  245. *
  246. * If no nodes are publishing, response is empty. Therefore, if subscribing, your subscription will fail until a publisher is registered for the service/topic.
  247. *
  248. * Will retry every `retryInterval` if no publishers are registered and activated.
  249. * @memberof Control
  250. * @param {string} serviceName - Name of the service to lookup.
  251. * @param {number} [retryInterval=30000] - Retry interval in milliseconds.
  252. * @param {number} [maxRetries=10] - Maximum retries that will be attempted.
  253. * @param {number} [retryAttempt=1] - Which attempt we are on. This is necessary since we use recursion for retries.
  254. * @return {Promise} Returns a list of nodes providing the specified service, as well as their properties. Empty if no publishers registered.
  255. */
  256. serviceLookup(
  257. serviceName,
  258. retryInterval = 30000,
  259. maxRetries = 10,
  260. retryAttempt = 1
  261. ) {
  262. const payload = { name: serviceName };
  263. return this._post(SERVICE_LOOKUP_URL, payload)
  264. .then(response => {
  265. if (!response.services[0]) {
  266. // If no publishers found, retry every 30 seconds.
  267. if (retryAttempt > maxRetries) {
  268. throw new Error(
  269. `No registered publisher(s) for service/topic. Hit max number of retries (${maxRetries}).`
  270. );
  271. } else {
  272. console.log(
  273. `No publishing nodes registered for service ${serviceName}, retrying in ${retryInterval /
  274. 1000} seconds (attempt: ${retryAttempt})...`
  275. );
  276. return this._delay(retryInterval).then(() =>
  277. this.serviceLookup(
  278. serviceName,
  279. retryInterval,
  280. maxRetries,
  281. retryAttempt + 1
  282. )
  283. );
  284. }
  285. } else {
  286. return response.services[0];
  287. }
  288. })
  289. .catch(error => {
  290. let message;
  291. switch (error.status) {
  292. case 401:
  293. // 401 Unauthorized
  294. message =
  295. 'pxGrid client is not registered to the controller. ' +
  296. 'Please ensure that Control#activate() is run at least once ' +
  297. 'before using your client.';
  298. break;
  299. case 403:
  300. // 403 Forbidden
  301. message =
  302. 'pxGrid client is forbidden from performing task. ' +
  303. 'This is likely because the client registered, but still in a PENDING status. ' +
  304. 'Please approve the client on the pxGrid controller.';
  305. break;
  306. default:
  307. message = `Response status: ${error.status} ${error.statusText}`;
  308. }
  309. throw new Error(message);
  310. });
  311. }
  312. /**
  313. * @description Register as a publisher to a service. This could be a new service, or an existing service.
  314. * @memberof Control
  315. * @param {string} serviceName - Name of the service to register for.
  316. * @param {Object} properties - Properties of the service you are registering.
  317. * @return {Promise} The id and reregisterTimeMillis for the newly registered service.
  318. */
  319. serviceRegister(serviceName, properties) {
  320. const payload = { name: serviceName, properties };
  321. return this._post(SERVICE_REGISTER_URL, payload)
  322. .then(response => {
  323. this.registeredServices[serviceName] = response.id;
  324. return response;
  325. })
  326. .catch(error => console.log(error));
  327. }
  328. /**
  329. * @description Reregister your node for a service. Services must reregister within the reregisterTimeMillis interval provided when initially registering.
  330. * @memberof Control
  331. * @see Control#serviceRegister
  332. * @param {string} serviceId - The ID of the service to reregister.
  333. * @return {Promise} Empty response from controller, if successful.
  334. */
  335. serviceReregister(serviceId) {
  336. return this._post(SERVICE_REREGISTER_URL, { id: serviceId });
  337. }
  338. /**
  339. * @description Automatically reregister a service at a given interval. Reregistration will occur 5 seconds prior to provided interval to prevent inadvertent timeouts.
  340. * @memberof Control
  341. * @see Control#serviceReregister
  342. * @param {string} serviceId - The ID of the service to reregister.
  343. * @param {number} interval - Interval to reregister (milliseconds).
  344. */
  345. autoServiceReregister(serviceId, interval) {
  346. setInterval(() => this.serviceReregister(serviceId), interval - 5000);
  347. }
  348. /**
  349. * @description Unregisters a service.
  350. * @memberof Control
  351. * @param {string} serviceId - The ID of the service to unregister.
  352. * @return {Promise} Empty response from controller, if successful.
  353. */
  354. serviceUnregister(serviceId) {
  355. return this._post(SERVICE_UNREGISTER_URL, { id: serviceId });
  356. }
  357. /**
  358. * @description Unregisters all services that have been registered by the instance.
  359. * @memberof Control
  360. */
  361. serviceUnregisterAll() {
  362. this.registeredServices.forEach(service => {
  363. this.serviceUnregister(service.id).then(response =>
  364. console.log(response)
  365. );
  366. });
  367. }
  368. /**
  369. * @description Gets the Access Secret for a given node. AccessSecret is a unique secret between a Consumer and Provider pair.
  370. * @memberof Control
  371. * @param {string} nodeName - The node name to get the secret for.
  372. * @return {Promise} Access Secret for node.
  373. */
  374. getAccessSecret(nodeName) {
  375. const payload = { peerNodeName: nodeName };
  376. return this._post(ACCESS_SECRET_URL, payload);
  377. }
  378. /**
  379. * @description Returns the control configuration.
  380. * @memberof Control
  381. * @return {Object} Control configuration.
  382. */
  383. getConfig() {
  384. return this.config;
  385. }
  386. }
  387. module.exports = Control;