360 lines
13 KiB
JavaScript
360 lines
13 KiB
JavaScript
|
var AWS = require('./core');
|
||
|
var util = require('./util');
|
||
|
var endpointDiscoveryEnabledEnvs = ['AWS_ENABLE_ENDPOINT_DISCOVERY', 'AWS_ENDPOINT_DISCOVERY_ENABLED'];
|
||
|
|
||
|
/**
|
||
|
* Generate key (except resources and operation part) to index the endpoints in the cache
|
||
|
* If input shape has endpointdiscoveryid trait then use
|
||
|
* accessKey + operation + resources + region + service as cache key
|
||
|
* If input shape doesn't have endpointdiscoveryid trait then use
|
||
|
* accessKey + region + service as cache key
|
||
|
* @return [map<String,String>] object with keys to index endpoints.
|
||
|
* @api private
|
||
|
*/
|
||
|
function getCacheKey(request) {
|
||
|
var service = request.service;
|
||
|
var api = service.api || {};
|
||
|
var operations = api.operations;
|
||
|
var identifiers = {};
|
||
|
if (service.config.region) {
|
||
|
identifiers.region = service.config.region;
|
||
|
}
|
||
|
if (api.serviceId) {
|
||
|
identifiers.serviceId = api.serviceId;
|
||
|
}
|
||
|
if (service.config.credentials.accessKeyId) {
|
||
|
identifiers.accessKeyId = service.config.credentials.accessKeyId;
|
||
|
}
|
||
|
return identifiers;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Recursive helper for marshallCustomIdentifiers().
|
||
|
* Looks for required string input members that have 'endpointdiscoveryid' trait.
|
||
|
* @api private
|
||
|
*/
|
||
|
function marshallCustomIdentifiersHelper(result, params, shape) {
|
||
|
if (!shape || params === undefined || params === null) return;
|
||
|
if (shape.type === 'structure' && shape.required && shape.required.length > 0) {
|
||
|
util.arrayEach(shape.required, function(name) {
|
||
|
var memberShape = shape.members[name];
|
||
|
if (memberShape.endpointDiscoveryId === true) {
|
||
|
var locationName = memberShape.isLocationName ? memberShape.name : name;
|
||
|
result[locationName] = String(params[name]);
|
||
|
} else {
|
||
|
marshallCustomIdentifiersHelper(result, params[name], memberShape);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get custom identifiers for cache key.
|
||
|
* Identifies custom identifiers by checking each shape's `endpointDiscoveryId` trait.
|
||
|
* @param [object] request object
|
||
|
* @param [object] input shape of the given operation's api
|
||
|
* @api private
|
||
|
*/
|
||
|
function marshallCustomIdentifiers(request, shape) {
|
||
|
var identifiers = {};
|
||
|
marshallCustomIdentifiersHelper(identifiers, request.params, shape);
|
||
|
return identifiers;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call endpoint discovery operation when it's optional.
|
||
|
* When endpoint is available in cache then use the cached endpoints. If endpoints
|
||
|
* are unavailable then use regional endpoints and call endpoint discovery operation
|
||
|
* asynchronously. This is turned off by default.
|
||
|
* @param [object] request object
|
||
|
* @api private
|
||
|
*/
|
||
|
function optionalDiscoverEndpoint(request) {
|
||
|
var service = request.service;
|
||
|
var api = service.api;
|
||
|
var operationModel = api.operations ? api.operations[request.operation] : undefined;
|
||
|
var inputShape = operationModel ? operationModel.input : undefined;
|
||
|
|
||
|
var identifiers = marshallCustomIdentifiers(request, inputShape);
|
||
|
var cacheKey = getCacheKey(request);
|
||
|
if (Object.keys(identifiers).length > 0) {
|
||
|
cacheKey = util.update(cacheKey, identifiers);
|
||
|
if (operationModel) cacheKey.operation = operationModel.name;
|
||
|
}
|
||
|
var endpoints = AWS.endpointCache.get(cacheKey);
|
||
|
if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
|
||
|
//endpoint operation is being made but response not yet received
|
||
|
//or endpoint operation just failed in 1 minute
|
||
|
return;
|
||
|
} else if (endpoints && endpoints.length > 0) {
|
||
|
//found endpoint record from cache
|
||
|
request.httpRequest.updateEndpoint(endpoints[0].Address);
|
||
|
} else {
|
||
|
//endpoint record not in cache or outdated. make discovery operation
|
||
|
var endpointRequest = service.makeRequest(api.endpointOperation, {
|
||
|
Operation: operationModel.name,
|
||
|
Identifiers: identifiers,
|
||
|
});
|
||
|
addApiVersionHeader(endpointRequest);
|
||
|
endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
|
||
|
endpointRequest.removeListener('retry', AWS.EventListeners.Core.RETRY_CHECK);
|
||
|
//put in a placeholder for endpoints already requested, prevent
|
||
|
//too much in-flight calls
|
||
|
AWS.endpointCache.put(cacheKey, [{
|
||
|
Address: '',
|
||
|
CachePeriodInMinutes: 1
|
||
|
}]);
|
||
|
endpointRequest.send(function(err, data) {
|
||
|
if (data && data.Endpoints) {
|
||
|
AWS.endpointCache.put(cacheKey, data.Endpoints);
|
||
|
} else if (err) {
|
||
|
AWS.endpointCache.put(cacheKey, [{
|
||
|
Address: '',
|
||
|
CachePeriodInMinutes: 1 //not to make more endpoint operation in next 1 minute
|
||
|
}]);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var requestQueue = {};
|
||
|
|
||
|
/**
|
||
|
* Call endpoint discovery operation when it's required.
|
||
|
* When endpoint is available in cache then use cached ones. If endpoints are
|
||
|
* unavailable then SDK should call endpoint operation then use returned new
|
||
|
* endpoint for the api call. SDK will automatically attempt to do endpoint
|
||
|
* discovery. This is turned off by default
|
||
|
* @param [object] request object
|
||
|
* @api private
|
||
|
*/
|
||
|
function requiredDiscoverEndpoint(request, done) {
|
||
|
var service = request.service;
|
||
|
var api = service.api;
|
||
|
var operationModel = api.operations ? api.operations[request.operation] : undefined;
|
||
|
var inputShape = operationModel ? operationModel.input : undefined;
|
||
|
|
||
|
var identifiers = marshallCustomIdentifiers(request, inputShape);
|
||
|
var cacheKey = getCacheKey(request);
|
||
|
if (Object.keys(identifiers).length > 0) {
|
||
|
cacheKey = util.update(cacheKey, identifiers);
|
||
|
if (operationModel) cacheKey.operation = operationModel.name;
|
||
|
}
|
||
|
var cacheKeyStr = AWS.EndpointCache.getKeyString(cacheKey);
|
||
|
var endpoints = AWS.endpointCache.get(cacheKeyStr); //endpoint cache also accepts string keys
|
||
|
if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
|
||
|
//endpoint operation is being made but response not yet received
|
||
|
//push request object to a pending queue
|
||
|
if (!requestQueue[cacheKeyStr]) requestQueue[cacheKeyStr] = [];
|
||
|
requestQueue[cacheKeyStr].push({request: request, callback: done});
|
||
|
return;
|
||
|
} else if (endpoints && endpoints.length > 0) {
|
||
|
request.httpRequest.updateEndpoint(endpoints[0].Address);
|
||
|
done();
|
||
|
} else {
|
||
|
var endpointRequest = service.makeRequest(api.endpointOperation, {
|
||
|
Operation: operationModel.name,
|
||
|
Identifiers: identifiers,
|
||
|
});
|
||
|
endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
|
||
|
addApiVersionHeader(endpointRequest);
|
||
|
|
||
|
//put in a placeholder for endpoints already requested, prevent
|
||
|
//too much in-flight calls
|
||
|
AWS.endpointCache.put(cacheKeyStr, [{
|
||
|
Address: '',
|
||
|
CachePeriodInMinutes: 60 //long-live cache
|
||
|
}]);
|
||
|
endpointRequest.send(function(err, data) {
|
||
|
if (err) {
|
||
|
var errorParams = {
|
||
|
code: 'EndpointDiscoveryException',
|
||
|
message: 'Request cannot be fulfilled without specifying an endpoint',
|
||
|
retryable: false
|
||
|
};
|
||
|
request.response.error = util.error(err, errorParams);
|
||
|
AWS.endpointCache.remove(cacheKey);
|
||
|
|
||
|
//fail all the pending requests in batch
|
||
|
if (requestQueue[cacheKeyStr]) {
|
||
|
var pendingRequests = requestQueue[cacheKeyStr];
|
||
|
util.arrayEach(pendingRequests, function(requestContext) {
|
||
|
requestContext.request.response.error = util.error(err, errorParams);
|
||
|
requestContext.callback();
|
||
|
});
|
||
|
delete requestQueue[cacheKeyStr];
|
||
|
}
|
||
|
} else if (data) {
|
||
|
AWS.endpointCache.put(cacheKeyStr, data.Endpoints);
|
||
|
request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
|
||
|
|
||
|
//update the endpoint for all the pending requests in batch
|
||
|
if (requestQueue[cacheKeyStr]) {
|
||
|
var pendingRequests = requestQueue[cacheKeyStr];
|
||
|
util.arrayEach(pendingRequests, function(requestContext) {
|
||
|
requestContext.request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
|
||
|
requestContext.callback();
|
||
|
});
|
||
|
delete requestQueue[cacheKeyStr];
|
||
|
}
|
||
|
}
|
||
|
done();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* add api version header to endpoint operation
|
||
|
* @api private
|
||
|
*/
|
||
|
function addApiVersionHeader(endpointRequest) {
|
||
|
var api = endpointRequest.service.api;
|
||
|
var apiVersion = api.apiVersion;
|
||
|
if (apiVersion && !endpointRequest.httpRequest.headers['x-amz-api-version']) {
|
||
|
endpointRequest.httpRequest.headers['x-amz-api-version'] = apiVersion;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If api call gets invalid endpoint exception, SDK should attempt to remove the invalid
|
||
|
* endpoint from cache.
|
||
|
* @api private
|
||
|
*/
|
||
|
function invalidateCachedEndpoints(response) {
|
||
|
var error = response.error;
|
||
|
var httpResponse = response.httpResponse;
|
||
|
if (error &&
|
||
|
(error.code === 'InvalidEndpointException' || httpResponse.statusCode === 421)
|
||
|
) {
|
||
|
var request = response.request;
|
||
|
var operations = request.service.api.operations || {};
|
||
|
var inputShape = operations[request.operation] ? operations[request.operation].input : undefined;
|
||
|
var identifiers = marshallCustomIdentifiers(request, inputShape);
|
||
|
var cacheKey = getCacheKey(request);
|
||
|
if (Object.keys(identifiers).length > 0) {
|
||
|
cacheKey = util.update(cacheKey, identifiers);
|
||
|
if (operations[request.operation]) cacheKey.operation = operations[request.operation].name;
|
||
|
}
|
||
|
AWS.endpointCache.remove(cacheKey);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If endpoint is explicitly configured, SDK should not do endpoint discovery in anytime.
|
||
|
* @param [object] client Service client object.
|
||
|
* @api private
|
||
|
*/
|
||
|
function hasCustomEndpoint(client) {
|
||
|
//if set endpoint is set for specific client, enable endpoint discovery will raise an error.
|
||
|
if (client._originalConfig && client._originalConfig.endpoint && client._originalConfig.endpointDiscoveryEnabled === true) {
|
||
|
throw util.error(new Error(), {
|
||
|
code: 'ConfigurationException',
|
||
|
message: 'Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.'
|
||
|
});
|
||
|
};
|
||
|
var svcConfig = AWS.config[client.serviceIdentifier] || {};
|
||
|
return Boolean(AWS.config.endpoint || svcConfig.endpoint || (client._originalConfig && client._originalConfig.endpoint));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @api private
|
||
|
*/
|
||
|
function isFalsy(value) {
|
||
|
return ['false', '0'].indexOf(value) >= 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If endpoint discovery should perform for this request when endpoint discovery is optional.
|
||
|
* SDK performs config resolution in order like below:
|
||
|
* 1. If turned on client configuration(default to off) then turn on endpoint discovery.
|
||
|
* 2. If turned on in env AWS_ENABLE_ENDPOINT_DISCOVERY then turn on endpoint discovery.
|
||
|
* 3. If turned on in shared ini config file with key 'endpoint_discovery_enabled', then
|
||
|
* turn on endpoint discovery.
|
||
|
* @param [object] request request object.
|
||
|
* @api private
|
||
|
*/
|
||
|
function isEndpointDiscoveryApplicable(request) {
|
||
|
var service = request.service || {};
|
||
|
if (service.config.endpointDiscoveryEnabled === true) return true;
|
||
|
|
||
|
//shared ini file is only available in Node
|
||
|
//not to check env in browser
|
||
|
if (util.isBrowser()) return false;
|
||
|
|
||
|
for (var i = 0; i < endpointDiscoveryEnabledEnvs.length; i++) {
|
||
|
var env = endpointDiscoveryEnabledEnvs[i];
|
||
|
if (Object.prototype.hasOwnProperty.call(process.env, env)) {
|
||
|
if (process.env[env] === '' || process.env[env] === undefined) {
|
||
|
throw util.error(new Error(), {
|
||
|
code: 'ConfigurationException',
|
||
|
message: 'environmental variable ' + env + ' cannot be set to nothing'
|
||
|
});
|
||
|
}
|
||
|
if (!isFalsy(process.env[env])) return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var configFile = {};
|
||
|
try {
|
||
|
configFile = AWS.util.iniLoader ? AWS.util.iniLoader.loadFrom({
|
||
|
isConfig: true,
|
||
|
filename: process.env[AWS.util.sharedConfigFileEnv]
|
||
|
}) : {};
|
||
|
} catch (e) {}
|
||
|
var sharedFileConfig = configFile[
|
||
|
process.env.AWS_PROFILE || AWS.util.defaultProfile
|
||
|
] || {};
|
||
|
if (Object.prototype.hasOwnProperty.call(sharedFileConfig, 'endpoint_discovery_enabled')) {
|
||
|
if (sharedFileConfig.endpoint_discovery_enabled === undefined) {
|
||
|
throw util.error(new Error(), {
|
||
|
code: 'ConfigurationException',
|
||
|
message: 'config file entry \'endpoint_discovery_enabled\' cannot be set to nothing'
|
||
|
});
|
||
|
}
|
||
|
if (!isFalsy(sharedFileConfig.endpoint_discovery_enabled)) return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* attach endpoint discovery logic to request object
|
||
|
* @param [object] request
|
||
|
* @api private
|
||
|
*/
|
||
|
function discoverEndpoint(request, done) {
|
||
|
var service = request.service || {};
|
||
|
if (hasCustomEndpoint(service) || request.isPresigned()) return done();
|
||
|
|
||
|
if (!isEndpointDiscoveryApplicable(request)) return done();
|
||
|
|
||
|
request.httpRequest.appendToUserAgent('endpoint-discovery');
|
||
|
|
||
|
var operations = service.api.operations || {};
|
||
|
var operationModel = operations[request.operation];
|
||
|
var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL';
|
||
|
switch (isEndpointDiscoveryRequired) {
|
||
|
case 'OPTIONAL':
|
||
|
optionalDiscoverEndpoint(request);
|
||
|
request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
|
||
|
done();
|
||
|
break;
|
||
|
case 'REQUIRED':
|
||
|
request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
|
||
|
requiredDiscoverEndpoint(request, done);
|
||
|
break;
|
||
|
case 'NULL':
|
||
|
default:
|
||
|
done();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
discoverEndpoint: discoverEndpoint,
|
||
|
requiredDiscoverEndpoint: requiredDiscoverEndpoint,
|
||
|
optionalDiscoverEndpoint: optionalDiscoverEndpoint,
|
||
|
marshallCustomIdentifiers: marshallCustomIdentifiers,
|
||
|
getCacheKey: getCacheKey,
|
||
|
invalidateCachedEndpoint: invalidateCachedEndpoints,
|
||
|
};
|