aws-sdk
Version:
AWS SDK for JavaScript
734 lines (670 loc) • 26.5 kB
JavaScript
var AWS = require('./core');
var SequentialExecutor = require('./sequential_executor');
var DISCOVER_ENDPOINT = require('./discover_endpoint').discoverEndpoint;
/**
* The namespace used to register global event listeners for request building
* and sending.
*/
AWS.EventListeners = {
/**
* @!attribute VALIDATE_CREDENTIALS
* A request listener that validates whether the request is being
* sent with credentials.
* Handles the {AWS.Request~validate 'validate' Request event}
* @example Sending a request without validating credentials
* var listener = AWS.EventListeners.Core.VALIDATE_CREDENTIALS;
* request.removeListener('validate', listener);
* @readonly
* @return [Function]
* @!attribute VALIDATE_REGION
* A request listener that validates whether the region is set
* for a request.
* Handles the {AWS.Request~validate 'validate' Request event}
* @example Sending a request without validating region configuration
* var listener = AWS.EventListeners.Core.VALIDATE_REGION;
* request.removeListener('validate', listener);
* @readonly
* @return [Function]
* @!attribute VALIDATE_PARAMETERS
* A request listener that validates input parameters in a request.
* Handles the {AWS.Request~validate 'validate' Request event}
* @example Sending a request without validating parameters
* var listener = AWS.EventListeners.Core.VALIDATE_PARAMETERS;
* request.removeListener('validate', listener);
* @example Disable parameter validation globally
* AWS.EventListeners.Core.removeListener('validate',
* AWS.EventListeners.Core.VALIDATE_REGION);
* @readonly
* @return [Function]
* @!attribute SEND
* A request listener that initiates the HTTP connection for a
* request being sent. Handles the {AWS.Request~send 'send' Request event}
* @example Replacing the HTTP handler
* var listener = AWS.EventListeners.Core.SEND;
* request.removeListener('send', listener);
* request.on('send', function(response) {
* customHandler.send(response);
* });
* @return [Function]
* @readonly
* @!attribute HTTP_DATA
* A request listener that reads data from the HTTP connection in order
* to build the response data.
* Handles the {AWS.Request~httpData 'httpData' Request event}.
* Remove this handler if you are overriding the 'httpData' event and
* do not want extra data processing and buffering overhead.
* @example Disabling default data processing
* var listener = AWS.EventListeners.Core.HTTP_DATA;
* request.removeListener('httpData', listener);
* @return [Function]
* @readonly
*/
Core: {} /* doc hack */
};
/**
* @api private
*/
function getOperationAuthtype(req) {
if (!req.service.api.operations) {
return '';
}
var operation = req.service.api.operations[req.operation];
return operation ? operation.authtype : '';
}
/**
* @api private
*/
function getIdentityType(req) {
var service = req.service;
if (service.config.signatureVersion) {
return service.config.signatureVersion;
}
if (service.api.signatureVersion) {
return service.api.signatureVersion;
}
return getOperationAuthtype(req);
}
AWS.EventListeners = {
Core: new SequentialExecutor().addNamedListeners(function(add, addAsync) {
addAsync(
'VALIDATE_CREDENTIALS', 'validate',
function VALIDATE_CREDENTIALS(req, done) {
if (!req.service.api.signatureVersion && !req.service.config.signatureVersion) return done(); // none
var identityType = getIdentityType(req);
if (identityType === 'bearer') {
req.service.config.getToken(function(err) {
if (err) {
req.response.error = AWS.util.error(err, {code: 'TokenError'});
}
done();
});
return;
}
req.service.config.getCredentials(function(err) {
if (err) {
req.response.error = AWS.util.error(err,
{
code: 'CredentialsError',
message: 'Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1'
}
);
}
done();
});
});
add('VALIDATE_REGION', 'validate', function VALIDATE_REGION(req) {
if (!req.service.isGlobalEndpoint) {
var dnsHostRegex = new RegExp(/^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$/);
if (!req.service.config.region) {
req.response.error = AWS.util.error(new Error(),
{code: 'ConfigError', message: 'Missing region in config'});
} else if (!dnsHostRegex.test(req.service.config.region)) {
req.response.error = AWS.util.error(new Error(),
{code: 'ConfigError', message: 'Invalid region in config'});
}
}
});
add('BUILD_IDEMPOTENCY_TOKENS', 'validate', function BUILD_IDEMPOTENCY_TOKENS(req) {
if (!req.service.api.operations) {
return;
}
var operation = req.service.api.operations[req.operation];
if (!operation) {
return;
}
var idempotentMembers = operation.idempotentMembers;
if (!idempotentMembers.length) {
return;
}
// creates a copy of params so user's param object isn't mutated
var params = AWS.util.copy(req.params);
for (var i = 0, iLen = idempotentMembers.length; i < iLen; i++) {
if (!params[idempotentMembers[i]]) {
// add the member
params[idempotentMembers[i]] = AWS.util.uuid.v4();
}
}
req.params = params;
});
add('VALIDATE_PARAMETERS', 'validate', function VALIDATE_PARAMETERS(req) {
if (!req.service.api.operations) {
return;
}
var rules = req.service.api.operations[req.operation].input;
var validation = req.service.config.paramValidation;
new AWS.ParamValidator(validation).validate(rules, req.params);
});
add('COMPUTE_CHECKSUM', 'afterBuild', function COMPUTE_CHECKSUM(req) {
if (!req.service.api.operations) {
return;
}
var operation = req.service.api.operations[req.operation];
if (!operation) {
return;
}
var body = req.httpRequest.body;
var isNonStreamingPayload = body && (AWS.util.Buffer.isBuffer(body) || typeof body === 'string');
var headers = req.httpRequest.headers;
if (
operation.httpChecksumRequired &&
req.service.config.computeChecksums &&
isNonStreamingPayload &&
!headers['Content-MD5']
) {
var md5 = AWS.util.crypto.md5(body, 'base64');
headers['Content-MD5'] = md5;
}
});
addAsync('COMPUTE_SHA256', 'afterBuild', function COMPUTE_SHA256(req, done) {
req.haltHandlersOnError();
if (!req.service.api.operations) {
return;
}
var operation = req.service.api.operations[req.operation];
var authtype = operation ? operation.authtype : '';
if (!req.service.api.signatureVersion && !authtype && !req.service.config.signatureVersion) return done(); // none
if (req.service.getSignerClass(req) === AWS.Signers.V4) {
var body = req.httpRequest.body || '';
if (authtype.indexOf('unsigned-body') >= 0) {
req.httpRequest.headers['X-Amz-Content-Sha256'] = 'UNSIGNED-PAYLOAD';
return done();
}
AWS.util.computeSha256(body, function(err, sha) {
if (err) {
done(err);
}
else {
req.httpRequest.headers['X-Amz-Content-Sha256'] = sha;
done();
}
});
} else {
done();
}
});
add('SET_CONTENT_LENGTH', 'afterBuild', function SET_CONTENT_LENGTH(req) {
var authtype = getOperationAuthtype(req);
var payloadMember = AWS.util.getRequestPayloadShape(req);
if (req.httpRequest.headers['Content-Length'] === undefined) {
try {
var length = AWS.util.string.byteLength(req.httpRequest.body);
req.httpRequest.headers['Content-Length'] = length;
} catch (err) {
if (payloadMember && payloadMember.isStreaming) {
if (payloadMember.requiresLength) {
//streaming payload requires length(s3, glacier)
throw err;
} else if (authtype.indexOf('unsigned-body') >= 0) {
//unbounded streaming payload(lex, mediastore)
req.httpRequest.headers['Transfer-Encoding'] = 'chunked';
return;
} else {
throw err;
}
}
throw err;
}
}
});
add('SET_HTTP_HOST', 'afterBuild', function SET_HTTP_HOST(req) {
req.httpRequest.headers['Host'] = req.httpRequest.endpoint.host;
});
add('SET_TRACE_ID', 'afterBuild', function SET_TRACE_ID(req) {
var traceIdHeaderName = 'X-Amzn-Trace-Id';
if (AWS.util.isNode() && !Object.hasOwnProperty.call(req.httpRequest.headers, traceIdHeaderName)) {
var ENV_LAMBDA_FUNCTION_NAME = 'AWS_LAMBDA_FUNCTION_NAME';
var ENV_TRACE_ID = '_X_AMZN_TRACE_ID';
var functionName = process.env[ENV_LAMBDA_FUNCTION_NAME];
var traceId = process.env[ENV_TRACE_ID];
if (
typeof functionName === 'string' &&
functionName.length > 0 &&
typeof traceId === 'string' &&
traceId.length > 0
) {
req.httpRequest.headers[traceIdHeaderName] = traceId;
}
}
});
add('RESTART', 'restart', function RESTART() {
var err = this.response.error;
if (!err || !err.retryable) return;
this.httpRequest = new AWS.HttpRequest(
this.service.endpoint,
this.service.region
);
if (this.response.retryCount < this.service.config.maxRetries) {
this.response.retryCount++;
} else {
this.response.error = null;
}
});
var addToHead = true;
addAsync('DISCOVER_ENDPOINT', 'sign', DISCOVER_ENDPOINT, addToHead);
addAsync('SIGN', 'sign', function SIGN(req, done) {
var service = req.service;
var identityType = getIdentityType(req);
if (!identityType || identityType.length === 0) return done(); // none
if (identityType === 'bearer') {
service.config.getToken(function (err, token) {
if (err) {
req.response.error = err;
return done();
}
try {
var SignerClass = service.getSignerClass(req);
var signer = new SignerClass(req.httpRequest);
signer.addAuthorization(token);
} catch (e) {
req.response.error = e;
}
done();
});
} else {
service.config.getCredentials(function (err, credentials) {
if (err) {
req.response.error = err;
return done();
}
try {
var date = service.getSkewCorrectedDate();
var SignerClass = service.getSignerClass(req);
var operations = req.service.api.operations || {};
var operation = operations[req.operation];
var signer = new SignerClass(req.httpRequest,
service.getSigningName(req),
{
signatureCache: service.config.signatureCache,
operation: operation,
signatureVersion: service.api.signatureVersion
});
signer.setServiceClientId(service._clientId);
// clear old authorization headers
delete req.httpRequest.headers['Authorization'];
delete req.httpRequest.headers['Date'];
delete req.httpRequest.headers['X-Amz-Date'];
// add new authorization
signer.addAuthorization(credentials, date);
req.signedAt = date;
} catch (e) {
req.response.error = e;
}
done();
});
}
});
add('VALIDATE_RESPONSE', 'validateResponse', function VALIDATE_RESPONSE(resp) {
if (this.service.successfulResponse(resp, this)) {
resp.data = {};
resp.error = null;
} else {
resp.data = null;
resp.error = AWS.util.error(new Error(),
{code: 'UnknownError', message: 'An unknown error occurred.'});
}
});
add('ERROR', 'error', function ERROR(err, resp) {
var awsQueryCompatible = resp.request.service.api.awsQueryCompatible;
if (awsQueryCompatible) {
var headers = resp.httpResponse.headers;
var queryErrorCode = headers ? headers['x-amzn-query-error'] : undefined;
if (queryErrorCode && queryErrorCode.includes(';')) {
resp.error.code = queryErrorCode.split(';')[0];
}
}
}, true);
addAsync('SEND', 'send', function SEND(resp, done) {
resp.httpResponse._abortCallback = done;
resp.error = null;
resp.data = null;
function callback(httpResp) {
resp.httpResponse.stream = httpResp;
var stream = resp.request.httpRequest.stream;
var service = resp.request.service;
var api = service.api;
var operationName = resp.request.operation;
var operation = api.operations[operationName] || {};
httpResp.on('headers', function onHeaders(statusCode, headers, statusMessage) {
resp.request.emit(
'httpHeaders',
[statusCode, headers, resp, statusMessage]
);
if (!resp.httpResponse.streaming) {
if (AWS.HttpClient.streamsApiVersion === 2) { // streams2 API check
// if we detect event streams, we're going to have to
// return the stream immediately
if (operation.hasEventOutput && service.successfulResponse(resp)) {
// skip reading the IncomingStream
resp.request.emit('httpDone');
done();
return;
}
httpResp.on('readable', function onReadable() {
var data = httpResp.read();
if (data !== null) {
resp.request.emit('httpData', [data, resp]);
}
});
} else { // legacy streams API
httpResp.on('data', function onData(data) {
resp.request.emit('httpData', [data, resp]);
});
}
}
});
httpResp.on('end', function onEnd() {
if (!stream || !stream.didCallback) {
if (AWS.HttpClient.streamsApiVersion === 2 && (operation.hasEventOutput && service.successfulResponse(resp))) {
// don't concatenate response chunks when streaming event stream data when response is successful
return;
}
resp.request.emit('httpDone');
done();
}
});
}
function progress(httpResp) {
httpResp.on('sendProgress', function onSendProgress(value) {
resp.request.emit('httpUploadProgress', [value, resp]);
});
httpResp.on('receiveProgress', function onReceiveProgress(value) {
resp.request.emit('httpDownloadProgress', [value, resp]);
});
}
function error(err) {
if (err.code !== 'RequestAbortedError') {
var errCode = err.code === 'TimeoutError' ? err.code : 'NetworkingError';
err = AWS.util.error(err, {
code: errCode,
region: resp.request.httpRequest.region,
hostname: resp.request.httpRequest.endpoint.hostname,
retryable: true
});
}
resp.error = err;
resp.request.emit('httpError', [resp.error, resp], function() {
done();
});
}
function executeSend() {
var http = AWS.HttpClient.getInstance();
var httpOptions = resp.request.service.config.httpOptions || {};
try {
var stream = http.handleRequest(resp.request.httpRequest, httpOptions,
callback, error);
progress(stream);
} catch (err) {
error(err);
}
}
var timeDiff = (resp.request.service.getSkewCorrectedDate() - this.signedAt) / 1000;
if (timeDiff >= 60 * 10) { // if we signed 10min ago, re-sign
this.emit('sign', [this], function(err) {
if (err) done(err);
else executeSend();
});
} else {
executeSend();
}
});
add('HTTP_HEADERS', 'httpHeaders',
function HTTP_HEADERS(statusCode, headers, resp, statusMessage) {
resp.httpResponse.statusCode = statusCode;
resp.httpResponse.statusMessage = statusMessage;
resp.httpResponse.headers = headers;
resp.httpResponse.body = AWS.util.buffer.toBuffer('');
resp.httpResponse.buffers = [];
resp.httpResponse.numBytes = 0;
var dateHeader = headers.date || headers.Date;
var service = resp.request.service;
if (dateHeader) {
var serverTime = Date.parse(dateHeader);
if (service.config.correctClockSkew
&& service.isClockSkewed(serverTime)) {
service.applyClockOffset(serverTime);
}
}
});
add('HTTP_DATA', 'httpData', function HTTP_DATA(chunk, resp) {
if (chunk) {
if (AWS.util.isNode()) {
resp.httpResponse.numBytes += chunk.length;
var total = resp.httpResponse.headers['content-length'];
var progress = { loaded: resp.httpResponse.numBytes, total: total };
resp.request.emit('httpDownloadProgress', [progress, resp]);
}
resp.httpResponse.buffers.push(AWS.util.buffer.toBuffer(chunk));
}
});
add('HTTP_DONE', 'httpDone', function HTTP_DONE(resp) {
// convert buffers array into single buffer
if (resp.httpResponse.buffers && resp.httpResponse.buffers.length > 0) {
var body = AWS.util.buffer.concat(resp.httpResponse.buffers);
resp.httpResponse.body = body;
}
delete resp.httpResponse.numBytes;
delete resp.httpResponse.buffers;
});
add('FINALIZE_ERROR', 'retry', function FINALIZE_ERROR(resp) {
if (resp.httpResponse.statusCode) {
resp.error.statusCode = resp.httpResponse.statusCode;
if (resp.error.retryable === undefined) {
resp.error.retryable = this.service.retryableError(resp.error, this);
}
}
});
add('INVALIDATE_CREDENTIALS', 'retry', function INVALIDATE_CREDENTIALS(resp) {
if (!resp.error) return;
switch (resp.error.code) {
case 'RequestExpired': // EC2 only
case 'ExpiredTokenException':
case 'ExpiredToken':
resp.error.retryable = true;
resp.request.service.config.credentials.expired = true;
}
});
add('EXPIRED_SIGNATURE', 'retry', function EXPIRED_SIGNATURE(resp) {
var err = resp.error;
if (!err) return;
if (typeof err.code === 'string' && typeof err.message === 'string') {
if (err.code.match(/Signature/) && err.message.match(/expired/)) {
resp.error.retryable = true;
}
}
});
add('CLOCK_SKEWED', 'retry', function CLOCK_SKEWED(resp) {
if (!resp.error) return;
if (this.service.clockSkewError(resp.error)
&& this.service.config.correctClockSkew) {
resp.error.retryable = true;
}
});
add('REDIRECT', 'retry', function REDIRECT(resp) {
if (resp.error && resp.error.statusCode >= 300 &&
resp.error.statusCode < 400 && resp.httpResponse.headers['location']) {
this.httpRequest.endpoint =
new AWS.Endpoint(resp.httpResponse.headers['location']);
this.httpRequest.headers['Host'] = this.httpRequest.endpoint.host;
this.httpRequest.path = this.httpRequest.endpoint.path;
resp.error.redirect = true;
resp.error.retryable = true;
}
});
add('RETRY_CHECK', 'retry', function RETRY_CHECK(resp) {
if (resp.error) {
if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
resp.error.retryDelay = 0;
} else if (resp.retryCount < resp.maxRetries) {
resp.error.retryDelay = this.service.retryDelays(resp.retryCount, resp.error) || 0;
}
}
});
addAsync('RESET_RETRY_STATE', 'afterRetry', function RESET_RETRY_STATE(resp, done) {
var delay, willRetry = false;
if (resp.error) {
delay = resp.error.retryDelay || 0;
if (resp.error.retryable && resp.retryCount < resp.maxRetries) {
resp.retryCount++;
willRetry = true;
} else if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
resp.redirectCount++;
willRetry = true;
}
}
// delay < 0 is a signal from customBackoff to skip retries
if (willRetry && delay >= 0) {
resp.error = null;
setTimeout(done, delay);
} else {
done();
}
});
}),
CorePost: new SequentialExecutor().addNamedListeners(function(add) {
add('EXTRACT_REQUEST_ID', 'extractData', AWS.util.extractRequestId);
add('EXTRACT_REQUEST_ID', 'extractError', AWS.util.extractRequestId);
add('ENOTFOUND_ERROR', 'httpError', function ENOTFOUND_ERROR(err) {
function isDNSError(err) {
return err.errno === 'ENOTFOUND' ||
typeof err.errno === 'number' &&
typeof AWS.util.getSystemErrorName === 'function' &&
['EAI_NONAME', 'EAI_NODATA'].indexOf(AWS.util.getSystemErrorName(err.errno) >= 0);
}
if (err.code === 'NetworkingError' && isDNSError(err)) {
var message = 'Inaccessible host: `' + err.hostname + '\' at port `' + err.port +
'\'. This service may not be available in the `' + err.region +
'\' region.';
this.response.error = AWS.util.error(new Error(message), {
code: 'UnknownEndpoint',
region: err.region,
hostname: err.hostname,
retryable: true,
originalError: err
});
}
});
}),
Logger: new SequentialExecutor().addNamedListeners(function(add) {
add('LOG_REQUEST', 'complete', function LOG_REQUEST(resp) {
var req = resp.request;
var logger = req.service.config.logger;
if (!logger) return;
function filterSensitiveLog(inputShape, shape) {
if (!shape) {
return shape;
}
if (inputShape.isSensitive) {
return '***SensitiveInformation***';
}
switch (inputShape.type) {
case 'structure':
var struct = {};
AWS.util.each(shape, function(subShapeName, subShape) {
if (Object.prototype.hasOwnProperty.call(inputShape.members, subShapeName)) {
struct[subShapeName] = filterSensitiveLog(inputShape.members[subShapeName], subShape);
} else {
struct[subShapeName] = subShape;
}
});
return struct;
case 'list':
var list = [];
AWS.util.arrayEach(shape, function(subShape, index) {
list.push(filterSensitiveLog(inputShape.member, subShape));
});
return list;
case 'map':
var map = {};
AWS.util.each(shape, function(key, value) {
map[key] = filterSensitiveLog(inputShape.value, value);
});
return map;
default:
return shape;
}
}
function buildMessage() {
var time = resp.request.service.getSkewCorrectedDate().getTime();
var delta = (time - req.startTime.getTime()) / 1000;
var ansi = logger.isTTY ? true : false;
var status = resp.httpResponse.statusCode;
var censoredParams = req.params;
if (
req.service.api.operations &&
req.service.api.operations[req.operation] &&
req.service.api.operations[req.operation].input
) {
var inputShape = req.service.api.operations[req.operation].input;
censoredParams = filterSensitiveLog(inputShape, req.params);
}
var params = require('util').inspect(censoredParams, true, null);
var message = '';
if (ansi) message += '\x1B[33m';
message += '[AWS ' + req.service.serviceIdentifier + ' ' + status;
message += ' ' + delta.toString() + 's ' + resp.retryCount + ' retries]';
if (ansi) message += '\x1B[0;1m';
message += ' ' + AWS.util.string.lowerFirst(req.operation);
message += '(' + params + ')';
if (ansi) message += '\x1B[0m';
return message;
}
var line = buildMessage();
if (typeof logger.log === 'function') {
logger.log(line);
} else if (typeof logger.write === 'function') {
logger.write(line + '\n');
}
});
}),
Json: new SequentialExecutor().addNamedListeners(function(add) {
var svc = require('./protocol/json');
add('BUILD', 'build', svc.buildRequest);
add('EXTRACT_DATA', 'extractData', svc.extractData);
add('EXTRACT_ERROR', 'extractError', svc.extractError);
}),
Rest: new SequentialExecutor().addNamedListeners(function(add) {
var svc = require('./protocol/rest');
add('BUILD', 'build', svc.buildRequest);
add('EXTRACT_DATA', 'extractData', svc.extractData);
add('EXTRACT_ERROR', 'extractError', svc.extractError);
}),
RestJson: new SequentialExecutor().addNamedListeners(function(add) {
var svc = require('./protocol/rest_json');
add('BUILD', 'build', svc.buildRequest);
add('EXTRACT_DATA', 'extractData', svc.extractData);
add('EXTRACT_ERROR', 'extractError', svc.extractError);
add('UNSET_CONTENT_LENGTH', 'afterBuild', svc.unsetContentLength);
}),
RestXml: new SequentialExecutor().addNamedListeners(function(add) {
var svc = require('./protocol/rest_xml');
add('BUILD', 'build', svc.buildRequest);
add('EXTRACT_DATA', 'extractData', svc.extractData);
add('EXTRACT_ERROR', 'extractError', svc.extractError);
}),
Query: new SequentialExecutor().addNamedListeners(function(add) {
var svc = require('./protocol/query');
add('BUILD', 'build', svc.buildRequest);
add('EXTRACT_DATA', 'extractData', svc.extractData);
add('EXTRACT_ERROR', 'extractError', svc.extractError);
})
};