UNPKG

22.7 kBJavaScriptView Raw
1var AWS = require('./core');
2var SequentialExecutor = require('./sequential_executor');
3var DISCOVER_ENDPOINT = require('./discover_endpoint').discoverEndpoint;
4/**
5 * The namespace used to register global event listeners for request building
6 * and sending.
7 */
8AWS.EventListeners = {
9 /**
10 * @!attribute VALIDATE_CREDENTIALS
11 * A request listener that validates whether the request is being
12 * sent with credentials.
13 * Handles the {AWS.Request~validate 'validate' Request event}
14 * @example Sending a request without validating credentials
15 * var listener = AWS.EventListeners.Core.VALIDATE_CREDENTIALS;
16 * request.removeListener('validate', listener);
17 * @readonly
18 * @return [Function]
19 * @!attribute VALIDATE_REGION
20 * A request listener that validates whether the region is set
21 * for a request.
22 * Handles the {AWS.Request~validate 'validate' Request event}
23 * @example Sending a request without validating region configuration
24 * var listener = AWS.EventListeners.Core.VALIDATE_REGION;
25 * request.removeListener('validate', listener);
26 * @readonly
27 * @return [Function]
28 * @!attribute VALIDATE_PARAMETERS
29 * A request listener that validates input parameters in a request.
30 * Handles the {AWS.Request~validate 'validate' Request event}
31 * @example Sending a request without validating parameters
32 * var listener = AWS.EventListeners.Core.VALIDATE_PARAMETERS;
33 * request.removeListener('validate', listener);
34 * @example Disable parameter validation globally
35 * AWS.EventListeners.Core.removeListener('validate',
36 * AWS.EventListeners.Core.VALIDATE_REGION);
37 * @readonly
38 * @return [Function]
39 * @!attribute SEND
40 * A request listener that initiates the HTTP connection for a
41 * request being sent. Handles the {AWS.Request~send 'send' Request event}
42 * @example Replacing the HTTP handler
43 * var listener = AWS.EventListeners.Core.SEND;
44 * request.removeListener('send', listener);
45 * request.on('send', function(response) {
46 * customHandler.send(response);
47 * });
48 * @return [Function]
49 * @readonly
50 * @!attribute HTTP_DATA
51 * A request listener that reads data from the HTTP connection in order
52 * to build the response data.
53 * Handles the {AWS.Request~httpData 'httpData' Request event}.
54 * Remove this handler if you are overriding the 'httpData' event and
55 * do not want extra data processing and buffering overhead.
56 * @example Disabling default data processing
57 * var listener = AWS.EventListeners.Core.HTTP_DATA;
58 * request.removeListener('httpData', listener);
59 * @return [Function]
60 * @readonly
61 */
62 Core: {} /* doc hack */
63};
64
65/**
66 * @api private
67 */
68function getOperationAuthtype(req) {
69 if (!req.service.api.operations) {
70 return '';
71 }
72 var operation = req.service.api.operations[req.operation];
73 return operation ? operation.authtype : '';
74}
75
76AWS.EventListeners = {
77 Core: new SequentialExecutor().addNamedListeners(function(add, addAsync) {
78 addAsync('VALIDATE_CREDENTIALS', 'validate',
79 function VALIDATE_CREDENTIALS(req, done) {
80 if (!req.service.api.signatureVersion && !req.service.config.signatureVersion) return done(); // none
81 req.service.config.getCredentials(function(err) {
82 if (err) {
83 req.response.error = AWS.util.error(err,
84 {code: 'CredentialsError', message: 'Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1'});
85 }
86 done();
87 });
88 });
89
90 add('VALIDATE_REGION', 'validate', function VALIDATE_REGION(req) {
91 if (!req.service.config.region && !req.service.isGlobalEndpoint) {
92 req.response.error = AWS.util.error(new Error(),
93 {code: 'ConfigError', message: 'Missing region in config'});
94 }
95 });
96
97 add('BUILD_IDEMPOTENCY_TOKENS', 'validate', function BUILD_IDEMPOTENCY_TOKENS(req) {
98 if (!req.service.api.operations) {
99 return;
100 }
101 var operation = req.service.api.operations[req.operation];
102 if (!operation) {
103 return;
104 }
105 var idempotentMembers = operation.idempotentMembers;
106 if (!idempotentMembers.length) {
107 return;
108 }
109 // creates a copy of params so user's param object isn't mutated
110 var params = AWS.util.copy(req.params);
111 for (var i = 0, iLen = idempotentMembers.length; i < iLen; i++) {
112 if (!params[idempotentMembers[i]]) {
113 // add the member
114 params[idempotentMembers[i]] = AWS.util.uuid.v4();
115 }
116 }
117 req.params = params;
118 });
119
120 add('VALIDATE_PARAMETERS', 'validate', function VALIDATE_PARAMETERS(req) {
121 if (!req.service.api.operations) {
122 return;
123 }
124 var rules = req.service.api.operations[req.operation].input;
125 var validation = req.service.config.paramValidation;
126 new AWS.ParamValidator(validation).validate(rules, req.params);
127 });
128
129 addAsync('COMPUTE_SHA256', 'afterBuild', function COMPUTE_SHA256(req, done) {
130 req.haltHandlersOnError();
131 if (!req.service.api.operations) {
132 return;
133 }
134 var operation = req.service.api.operations[req.operation];
135 var authtype = operation ? operation.authtype : '';
136 if (!req.service.api.signatureVersion && !authtype && !req.service.config.signatureVersion) return done(); // none
137 if (req.service.getSignerClass(req) === AWS.Signers.V4) {
138 var body = req.httpRequest.body || '';
139 if (authtype.indexOf('unsigned-body') >= 0) {
140 req.httpRequest.headers['X-Amz-Content-Sha256'] = 'UNSIGNED-PAYLOAD';
141 return done();
142 }
143 AWS.util.computeSha256(body, function(err, sha) {
144 if (err) {
145 done(err);
146 }
147 else {
148 req.httpRequest.headers['X-Amz-Content-Sha256'] = sha;
149 done();
150 }
151 });
152 } else {
153 done();
154 }
155 });
156
157 add('SET_CONTENT_LENGTH', 'afterBuild', function SET_CONTENT_LENGTH(req) {
158 var authtype = getOperationAuthtype(req);
159 var payloadMember = AWS.util.getRequestPayloadShape(req);
160 if (req.httpRequest.headers['Content-Length'] === undefined) {
161 try {
162 var length = AWS.util.string.byteLength(req.httpRequest.body);
163 req.httpRequest.headers['Content-Length'] = length;
164 } catch (err) {
165 if (payloadMember && payloadMember.isStreaming) {
166 if (payloadMember.requiresLength) {
167 //streaming payload requires length(s3, glacier)
168 throw err;
169 } else if (authtype.indexOf('unsigned-body') >= 0) {
170 //unbounded streaming payload(lex, mediastore)
171 req.httpRequest.headers['Transfer-Encoding'] = 'chunked';
172 return;
173 } else {
174 throw err;
175 }
176 }
177 throw err;
178 }
179 }
180 });
181
182 add('SET_HTTP_HOST', 'afterBuild', function SET_HTTP_HOST(req) {
183 req.httpRequest.headers['Host'] = req.httpRequest.endpoint.host;
184 });
185
186 add('RESTART', 'restart', function RESTART() {
187 var err = this.response.error;
188 if (!err || !err.retryable) return;
189
190 this.httpRequest = new AWS.HttpRequest(
191 this.service.endpoint,
192 this.service.region
193 );
194
195 if (this.response.retryCount < this.service.config.maxRetries) {
196 this.response.retryCount++;
197 } else {
198 this.response.error = null;
199 }
200 });
201
202 var addToHead = true;
203 addAsync('DISCOVER_ENDPOINT', 'sign', DISCOVER_ENDPOINT, addToHead);
204
205 addAsync('SIGN', 'sign', function SIGN(req, done) {
206 var service = req.service;
207 var operations = req.service.api.operations || {};
208 var operation = operations[req.operation];
209 var authtype = operation ? operation.authtype : '';
210 if (!service.api.signatureVersion && !authtype && !service.config.signatureVersion) return done(); // none
211
212 service.config.getCredentials(function (err, credentials) {
213 if (err) {
214 req.response.error = err;
215 return done();
216 }
217
218 try {
219 var date = service.getSkewCorrectedDate();
220 var SignerClass = service.getSignerClass(req);
221 var signer = new SignerClass(req.httpRequest,
222 service.api.signingName || service.api.endpointPrefix,
223 {
224 signatureCache: service.config.signatureCache,
225 operation: operation,
226 signatureVersion: service.api.signatureVersion
227 });
228 signer.setServiceClientId(service._clientId);
229
230 // clear old authorization headers
231 delete req.httpRequest.headers['Authorization'];
232 delete req.httpRequest.headers['Date'];
233 delete req.httpRequest.headers['X-Amz-Date'];
234
235 // add new authorization
236 signer.addAuthorization(credentials, date);
237 req.signedAt = date;
238 } catch (e) {
239 req.response.error = e;
240 }
241 done();
242 });
243 });
244
245 add('VALIDATE_RESPONSE', 'validateResponse', function VALIDATE_RESPONSE(resp) {
246 if (this.service.successfulResponse(resp, this)) {
247 resp.data = {};
248 resp.error = null;
249 } else {
250 resp.data = null;
251 resp.error = AWS.util.error(new Error(),
252 {code: 'UnknownError', message: 'An unknown error occurred.'});
253 }
254 });
255
256 addAsync('SEND', 'send', function SEND(resp, done) {
257 resp.httpResponse._abortCallback = done;
258 resp.error = null;
259 resp.data = null;
260
261 function callback(httpResp) {
262 resp.httpResponse.stream = httpResp;
263 var stream = resp.request.httpRequest.stream;
264 var service = resp.request.service;
265 var api = service.api;
266 var operationName = resp.request.operation;
267 var operation = api.operations[operationName] || {};
268
269 httpResp.on('headers', function onHeaders(statusCode, headers, statusMessage) {
270 resp.request.emit(
271 'httpHeaders',
272 [statusCode, headers, resp, statusMessage]
273 );
274
275 if (!resp.httpResponse.streaming) {
276 if (AWS.HttpClient.streamsApiVersion === 2) { // streams2 API check
277 // if we detect event streams, we're going to have to
278 // return the stream immediately
279 if (operation.hasEventOutput && service.successfulResponse(resp)) {
280 // skip reading the IncomingStream
281 resp.request.emit('httpDone');
282 done();
283 return;
284 }
285
286 httpResp.on('readable', function onReadable() {
287 var data = httpResp.read();
288 if (data !== null) {
289 resp.request.emit('httpData', [data, resp]);
290 }
291 });
292 } else { // legacy streams API
293 httpResp.on('data', function onData(data) {
294 resp.request.emit('httpData', [data, resp]);
295 });
296 }
297 }
298 });
299
300 httpResp.on('end', function onEnd() {
301 if (!stream || !stream.didCallback) {
302 if (AWS.HttpClient.streamsApiVersion === 2 && (operation.hasEventOutput && service.successfulResponse(resp))) {
303 // don't concatenate response chunks when streaming event stream data when response is successful
304 return;
305 }
306 resp.request.emit('httpDone');
307 done();
308 }
309 });
310 }
311
312 function progress(httpResp) {
313 httpResp.on('sendProgress', function onSendProgress(value) {
314 resp.request.emit('httpUploadProgress', [value, resp]);
315 });
316
317 httpResp.on('receiveProgress', function onReceiveProgress(value) {
318 resp.request.emit('httpDownloadProgress', [value, resp]);
319 });
320 }
321
322 function error(err) {
323 if (err.code !== 'RequestAbortedError') {
324 var errCode = err.code === 'TimeoutError' ? err.code : 'NetworkingError';
325 err = AWS.util.error(err, {
326 code: errCode,
327 region: resp.request.httpRequest.region,
328 hostname: resp.request.httpRequest.endpoint.hostname,
329 retryable: true
330 });
331 }
332 resp.error = err;
333 resp.request.emit('httpError', [resp.error, resp], function() {
334 done();
335 });
336 }
337
338 function executeSend() {
339 var http = AWS.HttpClient.getInstance();
340 var httpOptions = resp.request.service.config.httpOptions || {};
341 try {
342 var stream = http.handleRequest(resp.request.httpRequest, httpOptions,
343 callback, error);
344 progress(stream);
345 } catch (err) {
346 error(err);
347 }
348 }
349 var timeDiff = (resp.request.service.getSkewCorrectedDate() - this.signedAt) / 1000;
350 if (timeDiff >= 60 * 10) { // if we signed 10min ago, re-sign
351 this.emit('sign', [this], function(err) {
352 if (err) done(err);
353 else executeSend();
354 });
355 } else {
356 executeSend();
357 }
358 });
359
360 add('HTTP_HEADERS', 'httpHeaders',
361 function HTTP_HEADERS(statusCode, headers, resp, statusMessage) {
362 resp.httpResponse.statusCode = statusCode;
363 resp.httpResponse.statusMessage = statusMessage;
364 resp.httpResponse.headers = headers;
365 resp.httpResponse.body = AWS.util.buffer.toBuffer('');
366 resp.httpResponse.buffers = [];
367 resp.httpResponse.numBytes = 0;
368 var dateHeader = headers.date || headers.Date;
369 var service = resp.request.service;
370 if (dateHeader) {
371 var serverTime = Date.parse(dateHeader);
372 if (service.config.correctClockSkew
373 && service.isClockSkewed(serverTime)) {
374 service.applyClockOffset(serverTime);
375 }
376 }
377 });
378
379 add('HTTP_DATA', 'httpData', function HTTP_DATA(chunk, resp) {
380 if (chunk) {
381 if (AWS.util.isNode()) {
382 resp.httpResponse.numBytes += chunk.length;
383
384 var total = resp.httpResponse.headers['content-length'];
385 var progress = { loaded: resp.httpResponse.numBytes, total: total };
386 resp.request.emit('httpDownloadProgress', [progress, resp]);
387 }
388
389 resp.httpResponse.buffers.push(AWS.util.buffer.toBuffer(chunk));
390 }
391 });
392
393 add('HTTP_DONE', 'httpDone', function HTTP_DONE(resp) {
394 // convert buffers array into single buffer
395 if (resp.httpResponse.buffers && resp.httpResponse.buffers.length > 0) {
396 var body = AWS.util.buffer.concat(resp.httpResponse.buffers);
397 resp.httpResponse.body = body;
398 }
399 delete resp.httpResponse.numBytes;
400 delete resp.httpResponse.buffers;
401 });
402
403 add('FINALIZE_ERROR', 'retry', function FINALIZE_ERROR(resp) {
404 if (resp.httpResponse.statusCode) {
405 resp.error.statusCode = resp.httpResponse.statusCode;
406 if (resp.error.retryable === undefined) {
407 resp.error.retryable = this.service.retryableError(resp.error, this);
408 }
409 }
410 });
411
412 add('INVALIDATE_CREDENTIALS', 'retry', function INVALIDATE_CREDENTIALS(resp) {
413 if (!resp.error) return;
414 switch (resp.error.code) {
415 case 'RequestExpired': // EC2 only
416 case 'ExpiredTokenException':
417 case 'ExpiredToken':
418 resp.error.retryable = true;
419 resp.request.service.config.credentials.expired = true;
420 }
421 });
422
423 add('EXPIRED_SIGNATURE', 'retry', function EXPIRED_SIGNATURE(resp) {
424 var err = resp.error;
425 if (!err) return;
426 if (typeof err.code === 'string' && typeof err.message === 'string') {
427 if (err.code.match(/Signature/) && err.message.match(/expired/)) {
428 resp.error.retryable = true;
429 }
430 }
431 });
432
433 add('CLOCK_SKEWED', 'retry', function CLOCK_SKEWED(resp) {
434 if (!resp.error) return;
435 if (this.service.clockSkewError(resp.error)
436 && this.service.config.correctClockSkew) {
437 resp.error.retryable = true;
438 }
439 });
440
441 add('REDIRECT', 'retry', function REDIRECT(resp) {
442 if (resp.error && resp.error.statusCode >= 300 &&
443 resp.error.statusCode < 400 && resp.httpResponse.headers['location']) {
444 this.httpRequest.endpoint =
445 new AWS.Endpoint(resp.httpResponse.headers['location']);
446 this.httpRequest.headers['Host'] = this.httpRequest.endpoint.host;
447 resp.error.redirect = true;
448 resp.error.retryable = true;
449 }
450 });
451
452 add('RETRY_CHECK', 'retry', function RETRY_CHECK(resp) {
453 if (resp.error) {
454 if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
455 resp.error.retryDelay = 0;
456 } else if (resp.retryCount < resp.maxRetries) {
457 resp.error.retryDelay = this.service.retryDelays(resp.retryCount, resp.error) || 0;
458 }
459 }
460 });
461
462 addAsync('RESET_RETRY_STATE', 'afterRetry', function RESET_RETRY_STATE(resp, done) {
463 var delay, willRetry = false;
464
465 if (resp.error) {
466 delay = resp.error.retryDelay || 0;
467 if (resp.error.retryable && resp.retryCount < resp.maxRetries) {
468 resp.retryCount++;
469 willRetry = true;
470 } else if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
471 resp.redirectCount++;
472 willRetry = true;
473 }
474 }
475
476 // delay < 0 is a signal from customBackoff to skip retries
477 if (willRetry && delay >= 0) {
478 resp.error = null;
479 setTimeout(done, delay);
480 } else {
481 done();
482 }
483 });
484 }),
485
486 CorePost: new SequentialExecutor().addNamedListeners(function(add) {
487 add('EXTRACT_REQUEST_ID', 'extractData', AWS.util.extractRequestId);
488 add('EXTRACT_REQUEST_ID', 'extractError', AWS.util.extractRequestId);
489
490 add('ENOTFOUND_ERROR', 'httpError', function ENOTFOUND_ERROR(err) {
491 if (err.code === 'NetworkingError' && err.errno === 'ENOTFOUND') {
492 var message = 'Inaccessible host: `' + err.hostname +
493 '\'. This service may not be available in the `' + err.region +
494 '\' region.';
495 this.response.error = AWS.util.error(new Error(message), {
496 code: 'UnknownEndpoint',
497 region: err.region,
498 hostname: err.hostname,
499 retryable: true,
500 originalError: err
501 });
502 }
503 });
504 }),
505
506 Logger: new SequentialExecutor().addNamedListeners(function(add) {
507 add('LOG_REQUEST', 'complete', function LOG_REQUEST(resp) {
508 var req = resp.request;
509 var logger = req.service.config.logger;
510 if (!logger) return;
511 function filterSensitiveLog(inputShape, shape) {
512 if (!shape) {
513 return shape;
514 }
515 switch (inputShape.type) {
516 case 'structure':
517 var struct = {};
518 AWS.util.each(shape, function(subShapeName, subShape) {
519 if (Object.prototype.hasOwnProperty.call(inputShape.members, subShapeName)) {
520 struct[subShapeName] = filterSensitiveLog(inputShape.members[subShapeName], subShape);
521 } else {
522 struct[subShapeName] = subShape;
523 }
524 });
525 return struct;
526 case 'list':
527 var list = [];
528 AWS.util.arrayEach(shape, function(subShape, index) {
529 list.push(filterSensitiveLog(inputShape.member, subShape));
530 });
531 return list;
532 case 'map':
533 var map = {};
534 AWS.util.each(shape, function(key, value) {
535 map[key] = filterSensitiveLog(inputShape.value, value);
536 });
537 return map;
538 default:
539 if (inputShape.isSensitive) {
540 return '***SensitiveInformation***';
541 } else {
542 return shape;
543 }
544 }
545 }
546
547 function buildMessage() {
548 var time = resp.request.service.getSkewCorrectedDate().getTime();
549 var delta = (time - req.startTime.getTime()) / 1000;
550 var ansi = logger.isTTY ? true : false;
551 var status = resp.httpResponse.statusCode;
552 var censoredParams = req.params;
553 if (
554 req.service.api.operations &&
555 req.service.api.operations[req.operation] &&
556 req.service.api.operations[req.operation].input
557 ) {
558 var inputShape = req.service.api.operations[req.operation].input;
559 censoredParams = filterSensitiveLog(inputShape, req.params);
560 }
561 var params = require('util').inspect(censoredParams, true, null);
562 var message = '';
563 if (ansi) message += '\x1B[33m';
564 message += '[AWS ' + req.service.serviceIdentifier + ' ' + status;
565 message += ' ' + delta.toString() + 's ' + resp.retryCount + ' retries]';
566 if (ansi) message += '\x1B[0;1m';
567 message += ' ' + AWS.util.string.lowerFirst(req.operation);
568 message += '(' + params + ')';
569 if (ansi) message += '\x1B[0m';
570 return message;
571 }
572
573 var line = buildMessage();
574 if (typeof logger.log === 'function') {
575 logger.log(line);
576 } else if (typeof logger.write === 'function') {
577 logger.write(line + '\n');
578 }
579 });
580 }),
581
582 Json: new SequentialExecutor().addNamedListeners(function(add) {
583 var svc = require('./protocol/json');
584 add('BUILD', 'build', svc.buildRequest);
585 add('EXTRACT_DATA', 'extractData', svc.extractData);
586 add('EXTRACT_ERROR', 'extractError', svc.extractError);
587 }),
588
589 Rest: new SequentialExecutor().addNamedListeners(function(add) {
590 var svc = require('./protocol/rest');
591 add('BUILD', 'build', svc.buildRequest);
592 add('EXTRACT_DATA', 'extractData', svc.extractData);
593 add('EXTRACT_ERROR', 'extractError', svc.extractError);
594 }),
595
596 RestJson: new SequentialExecutor().addNamedListeners(function(add) {
597 var svc = require('./protocol/rest_json');
598 add('BUILD', 'build', svc.buildRequest);
599 add('EXTRACT_DATA', 'extractData', svc.extractData);
600 add('EXTRACT_ERROR', 'extractError', svc.extractError);
601 }),
602
603 RestXml: new SequentialExecutor().addNamedListeners(function(add) {
604 var svc = require('./protocol/rest_xml');
605 add('BUILD', 'build', svc.buildRequest);
606 add('EXTRACT_DATA', 'extractData', svc.extractData);
607 add('EXTRACT_ERROR', 'extractError', svc.extractError);
608 }),
609
610 Query: new SequentialExecutor().addNamedListeners(function(add) {
611 var svc = require('./protocol/query');
612 add('BUILD', 'build', svc.buildRequest);
613 add('EXTRACT_DATA', 'extractData', svc.extractData);
614 add('EXTRACT_ERROR', 'extractError', svc.extractError);
615 })
616};