Advertisement
Guest User

Untitled

a guest
Nov 3rd, 2016
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.44 KB | None | 0 0
  1. /**
  2. * UCAPI Client: Create the UCAPI Traverson API client.
  3. */
  4.  
  5. // initialize service name
  6. var serviceName = 'UCAPI';
  7.  
  8. // load helpers
  9. var helpers = require('./components/helpers');
  10.  
  11. // load logger
  12. var logger = require('./components/log');
  13.  
  14. // load constants
  15. var constants = require('./components/constants');
  16.  
  17. // used to create urls
  18. var url = require('url');
  19.  
  20. // used to make requests
  21. var requestLib = require('request');
  22.  
  23. // define errors
  24. var errors;
  25.  
  26. // define config
  27. var config;
  28.  
  29. // define error handler
  30. var handleUcapiErrorResponse;
  31.  
  32. // init UCAPI Cookie; cached and used for subsequent requests; the purpose of this, is to avoid authenticating
  33. // with username and password for every request, as the logging operation has a high overhead cost
  34. var ucapiCookie = {};
  35.  
  36. // load traverson
  37. var traverson = require('traverson');
  38. var JsonHalAdapter = require('traverson-hal');
  39. traverson.registerMediaType(JsonHalAdapter.mediaType, JsonHalAdapter);
  40.  
  41. /**
  42. * Initialize config
  43. * @param initConfig Configuration for UCAPI Connection
  44. * @param initErrors Errors Module Instance
  45. */
  46. var init = function (initConfig, initErrors) {
  47. config = initConfig;
  48. errors = initErrors;
  49. // load ucapi error handler
  50. var ucapiErrorHandler = require('./components/ucapiErrorHandler');
  51. // init error handler
  52. ucapiErrorHandler.init(__dirname + '/config/ucapiClientErrorMap.json', errors);
  53. // get error handling function
  54. handleUcapiErrorResponse = ucapiErrorHandler.handleUcapiErrorResponse;
  55. };
  56.  
  57. /**
  58. * UCAPIClient: create UCAPI Traverson Client
  59. * @param instanceConfig Object with instance configuration. It should contain at least credentials and log properties
  60. * credentials: {
  61. * username: 'exampleUser'
  62. * password: '3x@mpleP@5sw0rd'
  63. * }
  64. * If not sent the config object from initialization will be used
  65. * @constructor
  66. */
  67. var UCAPIClient = function (instanceConfig, dataSet) {
  68. // get instance config merging the one received with the config that module was initialized with
  69. instanceConfig = helpers.mergeObject(instanceConfig || {}, config.ucapi);
  70.  
  71. // cache instance logger
  72. var log = instanceConfig.log;
  73.  
  74. // client options
  75. var clientOptions = {
  76. timeout: instanceConfig.requestTimeout || 10000,
  77. headers: {
  78. // Add Auth Header
  79. 'Authorization': 'Basic ' + new Buffer(instanceConfig.credentials.username + ':' + instanceConfig.credentials.password).toString('base64'),
  80. // Send transaction / request IDs to UCAPI (they can collect it in their logs too)
  81. 'Transaction-Id': log.data.transactionId,
  82. 'Parent-Request-Id': log.data.requestId
  83. },
  84. // this info will be used by logger
  85. // we could send logger itself, but traverson destroys it when making clone of data
  86. '_loggerData': log.data,
  87. // set username as cookie id on each instance of Request to prevent event's callback to reference closure properties
  88. '_cookieId': instanceConfig.credentials.username
  89. };
  90.  
  91. // add SSL settings
  92. if (instanceConfig.https && instanceConfig.https.enabled) {
  93. clientOptions = helpers.mergeObject(clientOptions, instanceConfig.https);
  94. }
  95.  
  96. // create ucapi resource path
  97. var ucapiPath = instanceConfig.relativeUrl || instanceConfig.path;
  98.  
  99. // set UCAPI base url, if a relativeUrl is not set then the entry point will be the base of the traversal set in config
  100. var UCAPIBaseURL = url.format({
  101. protocol: (instanceConfig.https && instanceConfig.https.enabled) ? 'https' : 'http',
  102. host: `${instanceConfig.hostname}:${instanceConfig.port}`,
  103. pathname: ucapiPath
  104. });
  105.  
  106. // initialize traversal
  107. var traversal = traverson
  108. .from(UCAPIBaseURL)
  109. .preferEmbeddedResources()
  110. .jsonHal()
  111. .withRequestOptions(clientOptions);
  112.  
  113. // Modify request module's prototype to add logging to all requests and responses
  114. // Because traverson uses 'request' module to make calls, this will add logging to all requests
  115. // NOTE: this adds listeners on all requests using 'request' module, e.g. Logging helper
  116. // however, since this._loggerData is not populated in Logging helper's, it's not affected
  117. var request = traversal.getRequestLibrary();
  118. var requestPrototype = request.Request.prototype;
  119. // proto is modified only once
  120. if (!requestPrototype._init) {
  121. // override init function
  122. requestPrototype._init = requestPrototype.init;
  123. requestPrototype.init = function () {
  124. // HTTP request is initiated
  125. this.on('request', function (req) {
  126. if (this._loggerData) {
  127. var requestLogger = logger.getLogger('client-requests', this._loggerData);
  128. // copy body into request
  129. req.body = this.body;
  130. requestLogger.info('Sending HTTP request', {'http.req': req});
  131. }
  132. });
  133. // Response is completed
  134. this.on('complete', function (res) {
  135. if (this._loggerData) {
  136. // check if UCAPI sends the login cookie, and cache it
  137. // in case the cookie is malformed, UCAPI will send another one, which will then be re-cached
  138. if(res.headers['set-cookie']) {
  139. ucapiCookie[this._cookieId] = res.headers['set-cookie'][0];
  140. }
  141.  
  142. var responseLogger = logger.getLogger('client-responses', this._loggerData);
  143. responseLogger.info('Received HTTP response', {'http.res': res});
  144. }
  145. });
  146. // Response is redirected
  147. this.on('redirect', function () {
  148. if (this._loggerData) {
  149. var responseLogger = logger.getLogger('client-requests', this._loggerData);
  150. responseLogger.info('Redirected HTTP response');
  151. }
  152. });
  153. // Request failed (not sent)
  154. this.on('error', function (e) {
  155. if (this._loggerData) {
  156. var log = logger.getLogger(null, this._loggerData);
  157. // copy body into request
  158. this.req.body = this.body;
  159. log.error('Failed to send request: ' + e.stack, { 'http.req': this.req });
  160. }
  161. });
  162. return requestPrototype._init.apply(this, arguments);
  163. };
  164. }
  165.  
  166. // overwrite newRequest function
  167. traversal.newRequest = function () {
  168. // call the function that we overwrite
  169. var newRequestResult = Object.getPrototypeOf(traversal).newRequest.apply(traversal);
  170. if(ucapiCookie[instanceConfig.credentials.username]) {
  171. // init request with the (cached) UCAPI cookie, to avoid re-authentication (which is a costly operation)
  172. newRequestResult.addRequestOptions({
  173. headers: { 'Cookie': ucapiCookie[instanceConfig.credentials.username] }
  174. });
  175. }
  176.  
  177. /**
  178. * Analize traverson error and build a standard error
  179. * @param error
  180. * @returns {Object}
  181. */
  182. function getHandledError(error) {
  183. var response = {};
  184.  
  185. // check for timeout
  186. // for timeout traverson doesn't return a comprehensive error... currently just checking for aborted flag
  187. if (error.aborted) {
  188. response.statusCode = 504;
  189. response.body = errors.getError(
  190. 'EXTERNAL_API_REQUEST_TIMEOUT',
  191. {
  192. 'serviceName': serviceName,
  193. 'timeout': clientOptions.timeout
  194. }
  195. );
  196. }
  197. // check for host unreachable or refused connection
  198. else if (error.code == 'EHOSTUNREACH' || error.code == 'ENOTFOUND' || error.code == 'ECONNREFUSED') {
  199. response.statusCode = 502;
  200. response.body = errors.getError(
  201. 'EXTERNAL_API_CONNECTION_ERROR',
  202. {
  203. 'serviceName': serviceName,
  204. 'message': error.code
  205. }
  206. );
  207. }
  208. // check correct http response as traverson might return it as an error
  209. else if (error.name == 'HTTPError' && error.httpStatus) {
  210. response.statusCode = error.httpStatus;
  211. response.body = error.body;
  212. }
  213. // other error which is not handled separately
  214. else {
  215. response.statusCode = 502;
  216. response.body = errors.getError(
  217. 'EXTERNAL_API_SERVER_ERROR',
  218. {
  219. 'serviceName': serviceName,
  220. 'message': error.message ? error.message : (error.code ? error.code : 'null response')
  221. }
  222. );
  223. }
  224.  
  225. return response;
  226. }
  227.  
  228. // overwrite getResource function (for logging)
  229. newRequestResult.getResource = function (callback) {
  230. // pre execution callback
  231. var preCallback = function (err, response, traversal) {
  232. var link = newRequestResult.links[newRequestResult.links.length - 1].value;
  233. // check error
  234. if (err) {
  235. log.warn('Failed to retrieve resource from link', link, ':', err.message);
  236. // handled error
  237. err = getHandledError(err);
  238. } else {
  239. // log the response of the function
  240. log.info('Retrieved resource from', link);
  241. }
  242.  
  243. // execute the initial callback
  244. callback(err, response, traversal);
  245. };
  246.  
  247. // call the function that we overwrite
  248. Object.getPrototypeOf(newRequestResult).getResource.call(newRequestResult, preCallback);
  249. };
  250.  
  251. // overwrite http methods function
  252. var httpMethods = ['get', 'put', 'post', 'delete'];
  253.  
  254. httpMethods.forEach(function (method) {
  255. newRequestResult[method] = function () {
  256. // get function arguments
  257. var args = Array.prototype.slice.call(arguments);
  258.  
  259. // callback is expected to be last argument of the function
  260. // cache it to use in preCallback
  261. var callback = args.pop();
  262.  
  263. // pre execution callback
  264. var preCallback = function (error, response, traversal) {
  265. // check for error
  266. if (error) {
  267. // handled error
  268. response = getHandledError(error);
  269. } else {
  270. // parse the response.body to JSON
  271. if (response.body) {
  272. try {
  273. response.jsonBody = JSON.parse(response.body);
  274.  
  275. // check if the response contains an error
  276. if (response.jsonBody.error) {
  277. response = handleUcapiErrorResponse(response.jsonBody.error, dataSet);
  278. }
  279. } catch (e) {
  280. // return parse error response in case json parsing fails
  281. response.body = errors.getError(
  282. 'EXTERNAL_API_PARSE_ERROR',
  283. {
  284. 'serviceName': serviceName,
  285. 'message': e.message
  286. }
  287. );
  288. response.statusCode = 500;
  289.  
  290. // log the parsing problem
  291. log.error('Error parsing UCAPI JSON response:', e.message, { 'http.res': response });
  292. }
  293. }
  294. }
  295.  
  296. // call the callback
  297. callback(error, response, traversal);
  298. };
  299.  
  300. // add preCallback as the callback to traverson function
  301. args.push(preCallback);
  302.  
  303. // call the function that we overwrite and return the result of it
  304. return Object.getPrototypeOf(newRequestResult)[method].apply(newRequestResult, args);
  305. };
  306. });
  307.  
  308. return newRequestResult;
  309. };
  310.  
  311. /**
  312. * Used for following an embedded resource based on a given array of resource names.
  313. * It traverses only embedded resources.
  314. * It works with array resources too, for example:
  315. * array[$all] gets all items from an embedded array resource and stops
  316. * array[0] gets first item from an embedded array and continues to the next resource name in the array
  317. * @param source Source object to traverse and look for desired resource
  318. * @param links Array of resource names to traverse and fetch the resource
  319. * @returns {*} Returns null in case the resource is not found, otherwise returns the resource reference
  320. */
  321. this.followResource = function (source, links) {
  322. if (!source.hasOwnProperty('_embedded')) {
  323. return null;
  324. }
  325.  
  326. // double check if links property is array
  327. links = Array.isArray(links) ? links : [links];
  328.  
  329. // target is source object at first iteration over link entries, if source doesn't have any embedded the search stops
  330. var target = source;
  331.  
  332. // traverse source
  333. for (var i = 0; i < links.length; i++) {
  334. // at every link entry iteration we check precedent target resource for the desired embedded resource
  335. var embedded = target._embedded;
  336. var linkEntry = links[i];
  337.  
  338. // check if link entry is an array
  339. var arrayIndex = linkEntry.indexOf('[');
  340. if (arrayIndex !== -1) {
  341. // cache array reference
  342. var arrayRef = embedded[linkEntry.slice(0, arrayIndex)] || [];
  343.  
  344. // check if is an array of all elements
  345. if (linkEntry.indexOf('[$all]') !== -1) {
  346. // fetch the desired resource and return the function immediately
  347. target = arrayRef;
  348. break;
  349. }
  350.  
  351. // if the resource is an item of the array update target resource and continue to the next link
  352. var arrayItem = arrayRef[linkEntry.slice(arrayIndex + 1, linkEntry.indexOf(']'))];
  353. if (!arrayItem) {
  354. return null;
  355. }
  356.  
  357. // update target resource
  358. target = arrayItem;
  359. } else {
  360. // look for desired resource in embedded
  361. var objectRef = embedded[linkEntry];
  362. if (!objectRef) {
  363. return null;
  364. }
  365.  
  366. // update target resource
  367. target = objectRef;
  368. }
  369. }
  370.  
  371. return target;
  372. };
  373.  
  374. /**
  375. * Send Media File multipart request to UCAPI
  376. * @param options {*} containing the following properties:
  377. * requestBody (any properties that will be mapped in the multipart content,
  378. * mediaFileId (id to use for searching the file in media file service and stream it to UCAPI)
  379. * method (request's method)
  380. * @param callback Callback to execute after the request has been completed (err, response, responseBody)
  381. */
  382. this.sendMediaFileMultipartRequest = function (options, callback) {
  383. options = options || {};
  384.  
  385. // merge client options with the options given as function parameter
  386. options = helpers.mergeObject(options, clientOptions);
  387.  
  388. // create media file url
  389. var mediaFileUrl = url.format({
  390. protocol: (config.https && config.https.enabled) ? 'https' : 'http',
  391. host: `${config.gateway.hostname}:${config.gateway.port}`,
  392. pathname: `${constants.urls.mediaFile}/uploads`
  393. });
  394.  
  395. // create request options
  396. var requestOptions = helpers.mergeObject(clientOptions, {
  397. url: UCAPIBaseURL,
  398. formData: options.requestBody
  399. });
  400.  
  401. // add the media file in the request body and set it to response writable stream from media file service request
  402. requestOptions.formData.mediaFile = requestLib.get(`${mediaFileUrl}/${options.mediaFileId}`, {
  403. auth: {
  404. bearer: require('./authValidationClient').generateJWTBearerSimpleToken(config.authentication, instanceConfig.log)
  405. }
  406. });
  407.  
  408. // send multipart request to UCAPI with the file contents streaming from media file service
  409. requestLib[options.method.toLowerCase()](requestOptions, callback);
  410. };
  411.  
  412. // set prototype of traversal
  413. Object.setPrototypeOf(this, traversal);
  414. };
  415.  
  416. /**
  417. * @function getInstance Return an instance of the UCAPI Traverson Client
  418. * @returns {UCAPI Traverson Client}
  419. */
  420. function getInstance(instanceConfig, dataset) {
  421. dataset = dataset || {};
  422. return new UCAPIClient(instanceConfig, dataset);
  423. }
  424.  
  425. // Exports creating a Wit eFax client
  426. module.exports = {
  427. init: init,
  428. getInstance: getInstance
  429. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement