discover_endpoint.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. var AWS = require('./core');
  2. var util = require('./util');
  3. var endpointDiscoveryEnabledEnvs = ['AWS_ENABLE_ENDPOINT_DISCOVERY', 'AWS_ENDPOINT_DISCOVERY_ENABLED'];
  4. /**
  5. * Generate key (except resources and operation part) to index the endpoints in the cache
  6. * If input shape has endpointdiscoveryid trait then use
  7. * accessKey + operation + resources + region + service as cache key
  8. * If input shape doesn't have endpointdiscoveryid trait then use
  9. * accessKey + region + service as cache key
  10. * @return [map<String,String>] object with keys to index endpoints.
  11. * @api private
  12. */
  13. function getCacheKey(request) {
  14. var service = request.service;
  15. var api = service.api || {};
  16. var operations = api.operations;
  17. var identifiers = {};
  18. if (service.config.region) {
  19. identifiers.region = service.config.region;
  20. }
  21. if (api.serviceId) {
  22. identifiers.serviceId = api.serviceId;
  23. }
  24. if (service.config.credentials.accessKeyId) {
  25. identifiers.accessKeyId = service.config.credentials.accessKeyId;
  26. }
  27. return identifiers;
  28. }
  29. /**
  30. * Recursive helper for marshallCustomIdentifiers().
  31. * Looks for required string input members that have 'endpointdiscoveryid' trait.
  32. * @api private
  33. */
  34. function marshallCustomIdentifiersHelper(result, params, shape) {
  35. if (!shape || params === undefined || params === null) return;
  36. if (shape.type === 'structure' && shape.required && shape.required.length > 0) {
  37. util.arrayEach(shape.required, function(name) {
  38. var memberShape = shape.members[name];
  39. if (memberShape.endpointDiscoveryId === true) {
  40. var locationName = memberShape.isLocationName ? memberShape.name : name;
  41. result[locationName] = String(params[name]);
  42. } else {
  43. marshallCustomIdentifiersHelper(result, params[name], memberShape);
  44. }
  45. });
  46. }
  47. }
  48. /**
  49. * Get custom identifiers for cache key.
  50. * Identifies custom identifiers by checking each shape's `endpointDiscoveryId` trait.
  51. * @param [object] request object
  52. * @param [object] input shape of the given operation's api
  53. * @api private
  54. */
  55. function marshallCustomIdentifiers(request, shape) {
  56. var identifiers = {};
  57. marshallCustomIdentifiersHelper(identifiers, request.params, shape);
  58. return identifiers;
  59. }
  60. /**
  61. * Call endpoint discovery operation when it's optional.
  62. * When endpoint is available in cache then use the cached endpoints. If endpoints
  63. * are unavailable then use regional endpoints and call endpoint discovery operation
  64. * asynchronously. This is turned off by default.
  65. * @param [object] request object
  66. * @api private
  67. */
  68. function optionalDiscoverEndpoint(request) {
  69. var service = request.service;
  70. var api = service.api;
  71. var operationModel = api.operations ? api.operations[request.operation] : undefined;
  72. var inputShape = operationModel ? operationModel.input : undefined;
  73. var identifiers = marshallCustomIdentifiers(request, inputShape);
  74. var cacheKey = getCacheKey(request);
  75. if (Object.keys(identifiers).length > 0) {
  76. cacheKey = util.update(cacheKey, identifiers);
  77. if (operationModel) cacheKey.operation = operationModel.name;
  78. }
  79. var endpoints = AWS.endpointCache.get(cacheKey);
  80. if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
  81. //endpoint operation is being made but response not yet received
  82. //or endpoint operation just failed in 1 minute
  83. return;
  84. } else if (endpoints && endpoints.length > 0) {
  85. //found endpoint record from cache
  86. request.httpRequest.updateEndpoint(endpoints[0].Address);
  87. } else {
  88. //endpoint record not in cache or outdated. make discovery operation
  89. var endpointRequest = service.makeRequest(api.endpointOperation, {
  90. Operation: operationModel.name,
  91. Identifiers: identifiers,
  92. });
  93. addApiVersionHeader(endpointRequest);
  94. endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
  95. endpointRequest.removeListener('retry', AWS.EventListeners.Core.RETRY_CHECK);
  96. //put in a placeholder for endpoints already requested, prevent
  97. //too much in-flight calls
  98. AWS.endpointCache.put(cacheKey, [{
  99. Address: '',
  100. CachePeriodInMinutes: 1
  101. }]);
  102. endpointRequest.send(function(err, data) {
  103. if (data && data.Endpoints) {
  104. AWS.endpointCache.put(cacheKey, data.Endpoints);
  105. } else if (err) {
  106. AWS.endpointCache.put(cacheKey, [{
  107. Address: '',
  108. CachePeriodInMinutes: 1 //not to make more endpoint operation in next 1 minute
  109. }]);
  110. }
  111. });
  112. }
  113. }
  114. var requestQueue = {};
  115. /**
  116. * Call endpoint discovery operation when it's required.
  117. * When endpoint is available in cache then use cached ones. If endpoints are
  118. * unavailable then SDK should call endpoint operation then use returned new
  119. * endpoint for the api call. SDK will automatically attempt to do endpoint
  120. * discovery. This is turned off by default
  121. * @param [object] request object
  122. * @api private
  123. */
  124. function requiredDiscoverEndpoint(request, done) {
  125. var service = request.service;
  126. var api = service.api;
  127. var operationModel = api.operations ? api.operations[request.operation] : undefined;
  128. var inputShape = operationModel ? operationModel.input : undefined;
  129. var identifiers = marshallCustomIdentifiers(request, inputShape);
  130. var cacheKey = getCacheKey(request);
  131. if (Object.keys(identifiers).length > 0) {
  132. cacheKey = util.update(cacheKey, identifiers);
  133. if (operationModel) cacheKey.operation = operationModel.name;
  134. }
  135. var cacheKeyStr = AWS.EndpointCache.getKeyString(cacheKey);
  136. var endpoints = AWS.endpointCache.get(cacheKeyStr); //endpoint cache also accepts string keys
  137. if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
  138. //endpoint operation is being made but response not yet received
  139. //push request object to a pending queue
  140. if (!requestQueue[cacheKeyStr]) requestQueue[cacheKeyStr] = [];
  141. requestQueue[cacheKeyStr].push({request: request, callback: done});
  142. return;
  143. } else if (endpoints && endpoints.length > 0) {
  144. request.httpRequest.updateEndpoint(endpoints[0].Address);
  145. done();
  146. } else {
  147. var endpointRequest = service.makeRequest(api.endpointOperation, {
  148. Operation: operationModel.name,
  149. Identifiers: identifiers,
  150. });
  151. endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
  152. addApiVersionHeader(endpointRequest);
  153. //put in a placeholder for endpoints already requested, prevent
  154. //too much in-flight calls
  155. AWS.endpointCache.put(cacheKeyStr, [{
  156. Address: '',
  157. CachePeriodInMinutes: 60 //long-live cache
  158. }]);
  159. endpointRequest.send(function(err, data) {
  160. if (err) {
  161. request.response.error = util.error(err, { retryable: false });
  162. AWS.endpointCache.remove(cacheKey);
  163. //fail all the pending requests in batch
  164. if (requestQueue[cacheKeyStr]) {
  165. var pendingRequests = requestQueue[cacheKeyStr];
  166. util.arrayEach(pendingRequests, function(requestContext) {
  167. requestContext.request.response.error = util.error(err, { retryable: false });
  168. requestContext.callback();
  169. });
  170. delete requestQueue[cacheKeyStr];
  171. }
  172. } else if (data) {
  173. AWS.endpointCache.put(cacheKeyStr, data.Endpoints);
  174. request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
  175. //update the endpoint for all the pending requests in batch
  176. if (requestQueue[cacheKeyStr]) {
  177. var pendingRequests = requestQueue[cacheKeyStr];
  178. util.arrayEach(pendingRequests, function(requestContext) {
  179. requestContext.request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
  180. requestContext.callback();
  181. });
  182. delete requestQueue[cacheKeyStr];
  183. }
  184. }
  185. done();
  186. });
  187. }
  188. }
  189. /**
  190. * add api version header to endpoint operation
  191. * @api private
  192. */
  193. function addApiVersionHeader(endpointRequest) {
  194. var api = endpointRequest.service.api;
  195. var apiVersion = api.apiVersion;
  196. if (apiVersion && !endpointRequest.httpRequest.headers['x-amz-api-version']) {
  197. endpointRequest.httpRequest.headers['x-amz-api-version'] = apiVersion;
  198. }
  199. }
  200. /**
  201. * If api call gets invalid endpoint exception, SDK should attempt to remove the invalid
  202. * endpoint from cache.
  203. * @api private
  204. */
  205. function invalidateCachedEndpoints(response) {
  206. var error = response.error;
  207. var httpResponse = response.httpResponse;
  208. if (error &&
  209. (error.code === 'InvalidEndpointException' || httpResponse.statusCode === 421)
  210. ) {
  211. var request = response.request;
  212. var operations = request.service.api.operations || {};
  213. var inputShape = operations[request.operation] ? operations[request.operation].input : undefined;
  214. var identifiers = marshallCustomIdentifiers(request, inputShape);
  215. var cacheKey = getCacheKey(request);
  216. if (Object.keys(identifiers).length > 0) {
  217. cacheKey = util.update(cacheKey, identifiers);
  218. if (operations[request.operation]) cacheKey.operation = operations[request.operation].name;
  219. }
  220. AWS.endpointCache.remove(cacheKey);
  221. }
  222. }
  223. /**
  224. * If endpoint is explicitly configured, SDK should not do endpoint discovery in anytime.
  225. * @param [object] client Service client object.
  226. * @api private
  227. */
  228. function hasCustomEndpoint(client) {
  229. //if set endpoint is set for specific client, enable endpoint discovery will raise an error.
  230. if (client._originalConfig && client._originalConfig.endpoint && client._originalConfig.endpointDiscoveryEnabled === true) {
  231. throw util.error(new Error(), {
  232. code: 'ConfigurationException',
  233. message: 'Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.'
  234. });
  235. };
  236. var svcConfig = AWS.config[client.serviceIdentifier] || {};
  237. return Boolean(AWS.config.endpoint || svcConfig.endpoint || (client._originalConfig && client._originalConfig.endpoint));
  238. }
  239. /**
  240. * @api private
  241. */
  242. function isFalsy(value) {
  243. return ['false', '0'].indexOf(value) >= 0;
  244. }
  245. /**
  246. * If endpoint discovery should perform for this request when no operation requires endpoint
  247. * discovery for the given service.
  248. * SDK performs config resolution in order like below:
  249. * 1. If set in client configuration.
  250. * 2. If set in env AWS_ENABLE_ENDPOINT_DISCOVERY.
  251. * 3. If set in shared ini config file with key 'endpoint_discovery_enabled'.
  252. * @param [object] request request object.
  253. * @returns [boolean|undefined] if endpoint discovery config is not set in any source, this
  254. * function returns undefined
  255. * @api private
  256. */
  257. function resolveEndpointDiscoveryConfig(request) {
  258. var service = request.service || {};
  259. if (service.config.endpointDiscoveryEnabled !== undefined) {
  260. return service.config.endpointDiscoveryEnabled;
  261. }
  262. //shared ini file is only available in Node
  263. //not to check env in browser
  264. if (util.isBrowser()) return undefined;
  265. // If any of recognized endpoint discovery config env is set
  266. for (var i = 0; i < endpointDiscoveryEnabledEnvs.length; i++) {
  267. var env = endpointDiscoveryEnabledEnvs[i];
  268. if (Object.prototype.hasOwnProperty.call(process.env, env)) {
  269. if (process.env[env] === '' || process.env[env] === undefined) {
  270. throw util.error(new Error(), {
  271. code: 'ConfigurationException',
  272. message: 'environmental variable ' + env + ' cannot be set to nothing'
  273. });
  274. }
  275. return !isFalsy(process.env[env]);
  276. }
  277. }
  278. var configFile = {};
  279. try {
  280. configFile = AWS.util.iniLoader ? AWS.util.iniLoader.loadFrom({
  281. isConfig: true,
  282. filename: process.env[AWS.util.sharedConfigFileEnv]
  283. }) : {};
  284. } catch (e) {}
  285. var sharedFileConfig = configFile[
  286. process.env.AWS_PROFILE || AWS.util.defaultProfile
  287. ] || {};
  288. if (Object.prototype.hasOwnProperty.call(sharedFileConfig, 'endpoint_discovery_enabled')) {
  289. if (sharedFileConfig.endpoint_discovery_enabled === undefined) {
  290. throw util.error(new Error(), {
  291. code: 'ConfigurationException',
  292. message: 'config file entry \'endpoint_discovery_enabled\' cannot be set to nothing'
  293. });
  294. }
  295. return !isFalsy(sharedFileConfig.endpoint_discovery_enabled);
  296. }
  297. return undefined;
  298. }
  299. /**
  300. * attach endpoint discovery logic to request object
  301. * @param [object] request
  302. * @api private
  303. */
  304. function discoverEndpoint(request, done) {
  305. var service = request.service || {};
  306. if (hasCustomEndpoint(service) || request.isPresigned()) return done();
  307. var operations = service.api.operations || {};
  308. var operationModel = operations[request.operation];
  309. var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL';
  310. var isEnabled = resolveEndpointDiscoveryConfig(request);
  311. var hasRequiredEndpointDiscovery = service.api.hasRequiredEndpointDiscovery;
  312. if (isEnabled || hasRequiredEndpointDiscovery) {
  313. // Once a customer enables endpoint discovery, the SDK should start appending
  314. // the string endpoint-discovery to the user-agent on all requests.
  315. request.httpRequest.appendToUserAgent('endpoint-discovery');
  316. }
  317. switch (isEndpointDiscoveryRequired) {
  318. case 'OPTIONAL':
  319. if (isEnabled || hasRequiredEndpointDiscovery) {
  320. // For a given service; if at least one operation requires endpoint discovery then the SDK must enable endpoint discovery
  321. // by default for all operations of that service, including operations where endpoint discovery is optional.
  322. optionalDiscoverEndpoint(request);
  323. request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
  324. }
  325. done();
  326. break;
  327. case 'REQUIRED':
  328. if (isEnabled === false) {
  329. // For a given operation; if endpoint discovery is required and it has been disabled on the SDK client,
  330. // then the SDK must return a clear and actionable exception.
  331. request.response.error = util.error(new Error(), {
  332. code: 'ConfigurationException',
  333. message: 'Endpoint Discovery is disabled but ' + service.api.className + '.' + request.operation +
  334. '() requires it. Please check your configurations.'
  335. });
  336. done();
  337. break;
  338. }
  339. request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
  340. requiredDiscoverEndpoint(request, done);
  341. break;
  342. case 'NULL':
  343. default:
  344. done();
  345. break;
  346. }
  347. }
  348. module.exports = {
  349. discoverEndpoint: discoverEndpoint,
  350. requiredDiscoverEndpoint: requiredDiscoverEndpoint,
  351. optionalDiscoverEndpoint: optionalDiscoverEndpoint,
  352. marshallCustomIdentifiers: marshallCustomIdentifiers,
  353. getCacheKey: getCacheKey,
  354. invalidateCachedEndpoint: invalidateCachedEndpoints,
  355. };