-
Notifications
You must be signed in to change notification settings - Fork 2
/
PlatformTools.js
534 lines (485 loc) · 22.5 KB
/
PlatformTools.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
/**
* Copyright 2016 by Avid Technology, Inc.
* User: nludwig
* Date: 2016-07-08
* Time: 12:00
* Project: CTMS
*/
// Todos:
// - Encapsulate these functionalities in PlatformTools.js
// -- Use logging with winston. -> Last argument needs to be {} basically: logger.debug("X: %d and Y: %d", 42, 21 {});
// -- Use JSONPath.
// -- Use url-template (check with legal).
var https = require('https');
// Set httpsProxyAgent to enable proxying of requests:
//var HttpsProxyAgent = require('https-proxy-agent');
var httpsProxyAgent = null; //new HttpsProxyAgent('http://127.0.0.1:8888');
var sessionRefresher;
/**
* Performs the default action/sideeffect, if a request timed out. Currently just a message is logged.
*
* @method onRequestTimeout
*/
var onRequestTimeout = function() {
console.log("Request has timed out");
};
/**
* Retrieves the the default request timeout in ms.
*
* @method getDefaultRequestTimeoutms
* @return the default request timeout in ms
*/
var getDefaultRequestTimeoutms = function() {
return 60000;
};
/**
* Ends the process with failure.
*/
var failAndExit = function() {
console.log('End');
clearInterval(sessionRefresher);
process.exit(-1);
};
/**
* Promises delivery of the auth endpoint resource.
*
* @method getAuthEndpoint
* @param {Object} lastOptions (optional) valid options for the next HTTP request against the platform, if null is passed,
* new options will applied internally
* @param apiDomain address to get the authorization endpoint
* @return {Promise} promising {"options": options, "response": authEndpointResult} containing valid options for the
* next HTTP request against the platform and the auth endpoint result in the gotten response.
*/
var getAuthEndpoint = function(lastOptions, apiDomain) {
var deferred = Promise.defer();
var options
= lastOptions
? lastOptions
: {
'host' : apiDomain
, 'method' : 'GET'
, 'headers' : {
'Content-Type' : 'application/json'
, 'Accept' : 'application/hal+json'
}
, 'agent' : httpsProxyAgent
};
options.path = '/auth';
if (lastAccessToken) {
options.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(options, onAuthRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onAuthRequestResponded(authResponse) {
if(303 === authResponse.statusCode || 200 === authResponse.statusCode) {
var body = [];
authResponse.on('data', function onDataChunk(data) {
body.push(data);
});
authResponse.on('end', onAuthResultData);
authResponse.on('error', function onAuthRequestError() {
deferred.reject();
});
function onAuthResultData() {
var rawAuth = JSON.parse(Buffer.concat(body).toString());
deferred.resolve({"response": rawAuth, "options": options});
}
} else {
console.log("Getting Auth Endpoint request failed with '" + authResponse.statusMessage + "'");
deferred.reject();
}
}
return deferred.promise;
};
/**
* Promises delivery of the identity providers resource.
*
* @method getIdentityProviders
* @param {Object} lastResult valid options for the next HTTP request against the platform and a response containing the
* auth endpoint resource
* @return {Promise} promising {"options": options, "response": identityProvidersResult} containing valid options for
* the next HTTP request against the platform and the identity providers result in the gotten response.
*/
var getIdentityProviders = function(lastResult) {
var deferred = Promise.defer();
lastResult.options.path = lastResult.response._links['auth:identity-providers'][0].href;
if (lastAccessToken) {
lastResult.options.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(lastResult.options, onIdentityProvidersRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onIdentityProvidersRequestResponded(identityProvidersResponse) {
if(303 === identityProvidersResponse.statusCode || 200 === identityProvidersResponse.statusCode) {
var body = [];
identityProvidersResponse.on('data', function onDataChunk(data) {
body.push(data);
});
identityProvidersResponse.on('end', onIdentityProvidersResultData);
identityProvidersResponse.on('error', function onIdentityProvidersRequestError() {
deferred.reject();
});
function onIdentityProvidersResultData() {
// Get identity providers:
var rawIdentityProviders = JSON.parse(Buffer.concat(body).toString());
deferred.resolve({"response": rawIdentityProviders, "options": lastResult.options});
}
} else {
console.log("Getting Identity Providers request failed with '" + identityProvidersResponse.statusMessage + "'");
deferred.reject();
}
}
return deferred.promise;
};
var lastAccessToken;
var getAccessToken = function() {
return lastAccessToken;
};
/**
* Promises OAuth2-based authorization (HTTP Basic Auth String) with the passed identity providers HAL resource.
*
* @param {Object} lastResult valid options for the next HTTP request against the platform and a response containing the
* identity providers resource
* @param apiDomain address to get "auth"
* @param httpBasicAuthString HTTP basic Auth String
* @return {Promise} promising authorization {options} containing valid options for the next HTTP request against the
* platform
*/
var authorize = function(lastResult, apiDomain, httpBasicAuthString) {
var deferred = Promise.defer();
var identityProviders = lastResult.response._embedded['auth:identity-provider'];
var urlAuthorization;
for(var i = 0; i < identityProviders.length; ++i) {
if('oauth' === identityProviders[i]['kind']) {
var logins = identityProviders[i]['_links']['auth:ropc-default'];
if(logins != undefined) {
urlAuthorization = logins.length ? logins[0]['href'] : undefined;
break;
}
}
}
var authorizationContent = 'grant_type=client_credentials&scope=openid';
var authorizationDefaultToken = 'Basic ' + httpBasicAuthString;
var loginOptions = {
'host' : lastResult.options.host
, 'path' : urlAuthorization
, 'method' : 'POST'
, 'headers' : {
'Content-Type' : 'application/x-www-form-urlencoded'
, 'Authorization' : authorizationDefaultToken
, 'Accept' : 'application/json'
}
, 'agent' : httpsProxyAgent
};
var loginRequest = https.request(loginOptions, onLoginRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onLoginRequestResponded(loginResponse) {
if (303 === loginResponse.statusCode || 200 === loginResponse.statusCode) {
var body = [];
loginResponse.on('data', function onDataChunk(data) {
body.push(data);
});
loginResponse.on('end', onLoginResultData);
function onLoginResultData() {
var loginResult = JSON.parse(Buffer.concat(body).toString());
var idToken = loginResult.id_token ? loginResult.id_token : loginResult.access_token;
var accessTokenHeaderFieldValue = 'Bearer ' + idToken;
lastResult.options.headers = lastResult.options.headers ? lastResult.options.headers : {};
lastResult.options.headers['Authorization'] = accessTokenHeaderFieldValue;
var refreshPeriodMilliseconds = 120000;
sessionRefresher
= setInterval(function sessionKeepAlive() {
getAuthEndpoint(lastResult.options, apiDomain).catch(failAndExit)
.then(function(it) {
return getCurrentToken(it);
}, failAndExit)
.then(function (it) {
var urlPing = it.response._links["auth-token:extend"][0]["href"];
lastAccessToken = it.response.accessToken;
var pingOptions = {
'host' : it.options.host
, 'path' : urlPing
, 'method' : 'POST'
, 'headers' : {
'Content-Type' : 'application/json'
, 'Accept' : 'application/json'
, 'Authorization' : it.options.headers.Authorization
, 'Cookie' : 'avidAccessToken='+lastAccessToken
}
, 'agent' : httpsProxyAgent
};
https
.request(pingOptions, undefined)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout)
.end();
});}, refreshPeriodMilliseconds);
deferred.resolve(lastResult.options);
}
}
else {
console.log("Authorization request failed with '" + loginResponse.statusMessage + "'");
deferred.reject();
}
}
loginRequest.write(authorizationContent);
loginRequest.end();
return deferred.promise;
};
/**
* Promises the results of the CTMS Registry lookup or promises the default URI for the resource in question.
*
* @param {Object} lastOptions valid options for the next HTTP request against the platform
* @param apiDomain address to access the CTMS Registry
* @param {Array} serviceTypes the service types, for which the resource in question should be looked up in the CTMS
* Registry (currently ignored)
* @param {String} registryServiceVersion version of the CTMS Registry to query
* @param {String} resourceName resource to look up in the CTMS Registry, such as "search:simple-search"
* @param {String} orDefaultUriTemplate URI template which will be returned in the promise, if the CTMS Registry is
* unreachable or the resource in question cannot be found
* @param {String} realm the preferred realm of the resource to be found in the registry
* @return {Promise} promising {"options": options, "UriTemplates": uriTemplates} containing valid options for the next
* HTTP request against the platform and an array containing URI templates, under which the queried resource
* can be found. If the CTMS Registry is unreachable or the resource in question cannot be found, the array of
* URI templates will contain the orDefaultUriTemplate as single entry.
*/
var findInRegistry = function(lastOptions, apiDomain, serviceTypes, registryServiceVersion, resourceName, orDefaultUriTemplate, realm) {
var deferred = Promise.defer();
lastOptions.path = 'https://' + apiDomain + '/apis/avid.ctms.registry;version=' + registryServiceVersion + '/serviceroots';
if (lastAccessToken) {
lastOptions.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(lastOptions, onServiceRootsRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onServiceRootsRequestResponded(serviceRootsTokenResponse) {
if (303 === serviceRootsTokenResponse.statusCode || 200 === serviceRootsTokenResponse.statusCode) {
var body = [];
serviceRootsTokenResponse.on('data', function onDataChunk(data) {
body.push(data);
});
serviceRootsTokenResponse.on('end', onServiceRootsResultData);
serviceRootsTokenResponse.on('error', function onServiceRootsRequestError(e) {
deferred.resolve({"options": lastOptions, "UriTemplates": [orDefaultUriTemplate] });
});
function onServiceRootsResultData() {
var serviceRootsResult = JSON.parse(Buffer.concat(body).toString());
var resources = serviceRootsResult['resources'];
if (resources) {
var theOneResource = resources[resourceName];
if (theOneResource) {
if (theOneResource.length) {
var candidateSystemIds = [];
for (var i = 0; i < theOneResource.length; ++i) {
candidateSystemIds = candidateSystemIds.concat({"resource": theOneResource[i], "systemIDs": theOneResource[i].systems.map(function(it) {return it.systemID;})});
}
var effectiveRealm = realm;
var effectiveHref;
var candidateResources = candidateSystemIds.filter(function(it) { return 0 <= it.systemIDs.indexOf(realm);});
if (0 === candidateResources.length) {
effectiveHref = candidateSystemIds[0] ? candidateSystemIds[0].resource.href : orDefaultUriTemplate;
console.log("'" + resourceName + "' was not available on realm " + realm + ". Falling back to " + effectiveHref + ".");
} else {
effectiveHref = candidateResources[0].resource.href;
}
deferred.resolve({"options": lastOptions, "UriTemplates": [effectiveHref]});
} else {
console.log(resourceName + ' not registered, defaulting to the specified URI template');
deferred.resolve({"options": lastOptions, "UriTemplates": [orDefaultUriTemplate] });
}
} else {
console.log(resourceName + ' not registered, defaulting to the specified URI template');
deferred.resolve({"options": lastOptions, "UriTemplates": [orDefaultUriTemplate] });
}
} else {
console.log('no registered resources found, defaulting to the specified default URI template');
deferred.resolve({"options": lastOptions, "UriTemplates": [orDefaultUriTemplate] });
}
}
} else {
console.log('CTMS Registry not reachable (request failed), defaulting to the specified URI template');
deferred.resolve({"options": lastOptions, "UriTemplates": [orDefaultUriTemplate] });
}
}
return deferred.promise;
};
/**
* Promises delivery of the current MC|UX session token.
*
* @method getCurrentToken
* @param {"options": options, "response": response} lastResult containing valid options for the
* next HTTP request against the platform and a response containing links to authorization related
* functionality
* @return {Promise} promising {"options": options, "response": currentTokenResult} containing valid options for the
* next HTTP request against the platform and the current token result in the gotten response.
*/
var getCurrentToken = function(lastResult) {
var deferred = Promise.defer();
var tokens = lastResult.response._links['auth:token'];
var urlCurrentToken;
for(var i = 0; i < tokens.length; ++i) {
if('current' === tokens[i]['name']) {
urlCurrentToken = tokens[i].href;
break;
}
}
lastResult.options.path = urlCurrentToken;
if (lastAccessToken) {
lastResult.options.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(lastResult.options, onCurrentTokenRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onCurrentTokenRequestResponded(currentTokenResponse) {
if(303 === currentTokenResponse.statusCode || 200 === currentTokenResponse.statusCode) {
var body = [];
currentTokenResponse.on('data', function onDataChunk(data) {
body.push(data);
});
currentTokenResponse.on('end', onCurrentTokenResultData);
currentTokenResponse.on('error', function onCurrentTokenRequestError(e) {
deferred.reject();
});
function onCurrentTokenResultData() {
var currentTokenResult = JSON.parse(Buffer.concat(body).toString());
deferred.resolve({"options": lastResult.options, "response": currentTokenResult});
}
} else {
console.log("Getting Current Token request failed with '" + currentTokenResponse.statusMessage + "'");
deferred.reject();
}
}
return deferred.promise;
};
/**
* Promises removal of session identified by the passed session token.
*
* @method removeToken
* @param {"options": options, "response": response} lastResult containing valid options for the
* next HTTP request against the platform and a response specifying the session token to remove
* @return {Promise} promising completion of session removal.
*/
var removeToken = function(lastResult) {
var deferred = Promise.defer();
var removeTokenOptions = {
'host' : lastResult.options.host
, 'path' : lastResult.response._links['auth-token:removal'][0].href
, 'method' : 'DELETE'
, 'headers' : {
'Content-Type' : 'application/json'
, 'Accept' : 'application/json'
, 'Authorization' : lastResult.options.headers.Authorization
}
, 'agent' : httpsProxyAgent
};
if (lastAccessToken) {
options.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(removeTokenOptions, onRemoveTokenRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onRemoveTokenRequestResponded(removeTokenResponse) {
if(303 === removeTokenResponse.statusCode || 204 === removeTokenResponse.statusCode) {
removeTokenResponse.on('data', function noop(){});
removeTokenResponse.on('end', function onEnd(e) {
clearInterval(sessionRefresher);
deferred.resolve();
});
removeTokenResponse.on('error', function onRemoveTokenRequestError(e) {
clearInterval(sessionRefresher);
deferred.reject();
});
} else {
console.log("Remove Token request failed with '" + removeTokenResponse.statusMessage +"'");
deferred.reject();
}
}
return deferred.promise;
};
/**
* Promises delivery of all pages representing the HAL resources available via the passed resultPageURL.
*
* Pages through the HAL resources available via the passed urlResultPage and collects the found pages into the promise.
* If the HAL resource available from urlResultPage has the property "_embedded", its content will be collected into the
* pages. And if this HAL resource has the property "pageResult._links.next", its href will be used to fetch the next
* page and call this method recursively.
*
* @method pageThroughResults
* @param {Options} options HTTP options object, authorized against the platform
* @param {String} urlResultPage URL to a HAL resource, which supports paging
* @return {Promise} promises delivery of all pages.
*/
var pageThroughResults = function(options, urlResultPage) {
var deferred = Promise.defer();
var pages = [];
// TODO: the extra replacement is required for PAM and acts as temp. fix.
options.path = urlResultPage.replace(new RegExp(' ', 'g'), '%20');
if (lastAccessToken) {
options.headers.Cookie = 'avidAccessToken='+lastAccessToken;
}
https.get(options, onNextPageRequestResponded)
.setTimeout(getDefaultRequestTimeoutms(), onRequestTimeout);
function onNextPageRequestResponded(pageResponse) {
if(303 === pageResponse.statusCode || 200 === pageResponse.statusCode) {
var body = [];
pageResponse.on('data', function onDataChunk(data) {
body.push(data);
});
pageResponse.on('end', onPageResultData);
pageResponse.on('error', onPageResultError);
function onPageResultError(e) {
console.log('Paging failed for <' + urlResultPage + '>.');
deferred.reject();
}
function onPageResultData() {
var pageResult = JSON.parse(Buffer.concat(body).toString());
var embeddedResults = pageResult._embedded;
// Do we have results:
if (pages && embeddedResults) {
pages.push(embeddedResults);
// If we have more results, follow the next link and get the next page:
var linkToNextPage = pageResult._links.next;
if (linkToNextPage) {
pageThroughResults(options, linkToNextPage.href)
.then(function addToPages(nextPages) {
return pages.concat(nextPages);
})
.then(function done(nextPages) {
deferred.resolve(nextPages);
})
.catch(function() {
deferred.reject();
});
} else {
deferred.resolve(pages);
}
} else {
deferred.resolve(pages);
}
}
} else {
console.log('Paging failed for <' + urlResultPage + '>.');
deferred.reject();
}
}
return deferred.promise;
};
/**
* Trims the UTF-8 BOM away, if any.
*
* @method removeUTF8BOM
* @param {String} s does potentially have a superfluous UTF-8 BOM
* @return {String} a string w/o UTF-8 BOM.
*/
var removeUTF8BOM = function(s) {
return s.startsWith('\uFEFF')
? s.substring(1)
: s;
};
module.exports.getAuthEndpoint = getAuthEndpoint;
module.exports.getIdentityProviders = getIdentityProviders;
module.exports.authorize = authorize;
module.exports.findInRegistry = findInRegistry;
module.exports.getCurrentToken = getCurrentToken;
module.exports.removeToken = removeToken;
module.exports.onRequestTimeout = onRequestTimeout;
module.exports.getDefaultRequestTimeoutms = getDefaultRequestTimeoutms;
module.exports.removeUTF8BOM = removeUTF8BOM;
module.exports.pageThroughResults = pageThroughResults;
module.exports.failAndExit = failAndExit;
module.exports.getAccessToken = getAccessToken;