UNPKG

22.1 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) 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'});
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) 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 if (req.httpRequest.headers['Content-Length'] === undefined) {
160 try {
161 var length = AWS.util.string.byteLength(req.httpRequest.body);
162 req.httpRequest.headers['Content-Length'] = length;
163 } catch (err) {
164 if (authtype.indexOf('unsigned-body') === -1) {
165 throw err;
166 } else {
167 // Body isn't signed and may not need content length (lex)
168 return;
169 }
170 }
171 }
172 });
173
174 add('SET_HTTP_HOST', 'afterBuild', function SET_HTTP_HOST(req) {
175 req.httpRequest.headers['Host'] = req.httpRequest.endpoint.host;
176 });
177
178 add('RESTART', 'restart', function RESTART() {
179 var err = this.response.error;
180 if (!err || !err.retryable) return;
181
182 this.httpRequest = new AWS.HttpRequest(
183 this.service.endpoint,
184 this.service.region
185 );
186
187 if (this.response.retryCount < this.service.config.maxRetries) {
188 this.response.retryCount++;
189 } else {
190 this.response.error = null;
191 }
192 });
193
194 var addToHead = true;
195 addAsync('DISCOVER_ENDPOINT', 'sign', DISCOVER_ENDPOINT, addToHead);
196
197 addAsync('SIGN', 'sign', function SIGN(req, done) {
198 var service = req.service;
199 var operations = req.service.api.operations || {};
200 var operation = operations[req.operation];
201 var authtype = operation ? operation.authtype : '';
202 if (!service.api.signatureVersion && !authtype) return done(); // none
203
204 service.config.getCredentials(function (err, credentials) {
205 if (err) {
206 req.response.error = err;
207 return done();
208 }
209
210 try {
211 var date = service.getSkewCorrectedDate();
212 var SignerClass = service.getSignerClass(req);
213 var signer = new SignerClass(req.httpRequest,
214 service.api.signingName || service.api.endpointPrefix,
215 {
216 signatureCache: service.config.signatureCache,
217 operation: operation,
218 signatureVersion: service.api.signatureVersion
219 });
220 signer.setServiceClientId(service._clientId);
221
222 // clear old authorization headers
223 delete req.httpRequest.headers['Authorization'];
224 delete req.httpRequest.headers['Date'];
225 delete req.httpRequest.headers['X-Amz-Date'];
226
227 // add new authorization
228 signer.addAuthorization(credentials, date);
229 req.signedAt = date;
230 } catch (e) {
231 req.response.error = e;
232 }
233 done();
234 });
235 });
236
237 add('VALIDATE_RESPONSE', 'validateResponse', function VALIDATE_RESPONSE(resp) {
238 if (this.service.successfulResponse(resp, this)) {
239 resp.data = {};
240 resp.error = null;
241 } else {
242 resp.data = null;
243 resp.error = AWS.util.error(new Error(),
244 {code: 'UnknownError', message: 'An unknown error occurred.'});
245 }
246 });
247
248 addAsync('SEND', 'send', function SEND(resp, done) {
249 resp.httpResponse._abortCallback = done;
250 resp.error = null;
251 resp.data = null;
252
253 function callback(httpResp) {
254 resp.httpResponse.stream = httpResp;
255 var stream = resp.request.httpRequest.stream;
256 var service = resp.request.service;
257 var api = service.api;
258 var operationName = resp.request.operation;
259 var operation = api.operations[operationName] || {};
260
261 httpResp.on('headers', function onHeaders(statusCode, headers, statusMessage) {
262 resp.request.emit(
263 'httpHeaders',
264 [statusCode, headers, resp, statusMessage]
265 );
266
267 if (!resp.httpResponse.streaming) {
268 if (AWS.HttpClient.streamsApiVersion === 2) { // streams2 API check
269 // if we detect event streams, we're going to have to
270 // return the stream immediately
271 if (operation.hasEventOutput && service.successfulResponse(resp)) {
272 // skip reading the IncomingStream
273 resp.request.emit('httpDone');
274 done();
275 return;
276 }
277
278 httpResp.on('readable', function onReadable() {
279 var data = httpResp.read();
280 if (data !== null) {
281 resp.request.emit('httpData', [data, resp]);
282 }
283 });
284 } else { // legacy streams API
285 httpResp.on('data', function onData(data) {
286 resp.request.emit('httpData', [data, resp]);
287 });
288 }
289 }
290 });
291
292 httpResp.on('end', function onEnd() {
293 if (!stream || !stream.didCallback) {
294 if (AWS.HttpClient.streamsApiVersion === 2 && (operation.hasEventOutput && service.successfulResponse(resp))) {
295 // don't concatenate response chunks when streaming event stream data when response is successful
296 return;
297 }
298 resp.request.emit('httpDone');
299 done();
300 }
301 });
302 }
303
304 function progress(httpResp) {
305 httpResp.on('sendProgress', function onSendProgress(value) {
306 resp.request.emit('httpUploadProgress', [value, resp]);
307 });
308
309 httpResp.on('receiveProgress', function onReceiveProgress(value) {
310 resp.request.emit('httpDownloadProgress', [value, resp]);
311 });
312 }
313
314 function error(err) {
315 if (err.code !== 'RequestAbortedError') {
316 var errCode = err.code === 'TimeoutError' ? err.code : 'NetworkingError';
317 err = AWS.util.error(err, {
318 code: errCode,
319 region: resp.request.httpRequest.region,
320 hostname: resp.request.httpRequest.endpoint.hostname,
321 retryable: true
322 });
323 }
324 resp.error = err;
325 resp.request.emit('httpError', [resp.error, resp], function() {
326 done();
327 });
328 }
329
330 function executeSend() {
331 var http = AWS.HttpClient.getInstance();
332 var httpOptions = resp.request.service.config.httpOptions || {};
333 try {
334 var stream = http.handleRequest(resp.request.httpRequest, httpOptions,
335 callback, error);
336 progress(stream);
337 } catch (err) {
338 error(err);
339 }
340 }
341 var timeDiff = (resp.request.service.getSkewCorrectedDate() - this.signedAt) / 1000;
342 if (timeDiff >= 60 * 10) { // if we signed 10min ago, re-sign
343 this.emit('sign', [this], function(err) {
344 if (err) done(err);
345 else executeSend();
346 });
347 } else {
348 executeSend();
349 }
350 });
351
352 add('HTTP_HEADERS', 'httpHeaders',
353 function HTTP_HEADERS(statusCode, headers, resp, statusMessage) {
354 resp.httpResponse.statusCode = statusCode;
355 resp.httpResponse.statusMessage = statusMessage;
356 resp.httpResponse.headers = headers;
357 resp.httpResponse.body = new AWS.util.Buffer('');
358 resp.httpResponse.buffers = [];
359 resp.httpResponse.numBytes = 0;
360 var dateHeader = headers.date || headers.Date;
361 var service = resp.request.service;
362 if (dateHeader) {
363 var serverTime = Date.parse(dateHeader);
364 if (service.config.correctClockSkew
365 && service.isClockSkewed(serverTime)) {
366 service.applyClockOffset(serverTime);
367 }
368 }
369 });
370
371 add('HTTP_DATA', 'httpData', function HTTP_DATA(chunk, resp) {
372 if (chunk) {
373 if (AWS.util.isNode()) {
374 resp.httpResponse.numBytes += chunk.length;
375
376 var total = resp.httpResponse.headers['content-length'];
377 var progress = { loaded: resp.httpResponse.numBytes, total: total };
378 resp.request.emit('httpDownloadProgress', [progress, resp]);
379 }
380
381 resp.httpResponse.buffers.push(new AWS.util.Buffer(chunk));
382 }
383 });
384
385 add('HTTP_DONE', 'httpDone', function HTTP_DONE(resp) {
386 // convert buffers array into single buffer
387 if (resp.httpResponse.buffers && resp.httpResponse.buffers.length > 0) {
388 var body = AWS.util.buffer.concat(resp.httpResponse.buffers);
389 resp.httpResponse.body = body;
390 }
391 delete resp.httpResponse.numBytes;
392 delete resp.httpResponse.buffers;
393 });
394
395 add('FINALIZE_ERROR', 'retry', function FINALIZE_ERROR(resp) {
396 if (resp.httpResponse.statusCode) {
397 resp.error.statusCode = resp.httpResponse.statusCode;
398 if (resp.error.retryable === undefined) {
399 resp.error.retryable = this.service.retryableError(resp.error, this);
400 }
401 }
402 });
403
404 add('INVALIDATE_CREDENTIALS', 'retry', function INVALIDATE_CREDENTIALS(resp) {
405 if (!resp.error) return;
406 switch (resp.error.code) {
407 case 'RequestExpired': // EC2 only
408 case 'ExpiredTokenException':
409 case 'ExpiredToken':
410 resp.error.retryable = true;
411 resp.request.service.config.credentials.expired = true;
412 }
413 });
414
415 add('EXPIRED_SIGNATURE', 'retry', function EXPIRED_SIGNATURE(resp) {
416 var err = resp.error;
417 if (!err) return;
418 if (typeof err.code === 'string' && typeof err.message === 'string') {
419 if (err.code.match(/Signature/) && err.message.match(/expired/)) {
420 resp.error.retryable = true;
421 }
422 }
423 });
424
425 add('CLOCK_SKEWED', 'retry', function CLOCK_SKEWED(resp) {
426 if (!resp.error) return;
427 if (this.service.clockSkewError(resp.error)
428 && this.service.config.correctClockSkew) {
429 resp.error.retryable = true;
430 }
431 });
432
433 add('REDIRECT', 'retry', function REDIRECT(resp) {
434 if (resp.error && resp.error.statusCode >= 300 &&
435 resp.error.statusCode < 400 && resp.httpResponse.headers['location']) {
436 this.httpRequest.endpoint =
437 new AWS.Endpoint(resp.httpResponse.headers['location']);
438 this.httpRequest.headers['Host'] = this.httpRequest.endpoint.host;
439 resp.error.redirect = true;
440 resp.error.retryable = true;
441 }
442 });
443
444 add('RETRY_CHECK', 'retry', function RETRY_CHECK(resp) {
445 if (resp.error) {
446 if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
447 resp.error.retryDelay = 0;
448 } else if (resp.retryCount < resp.maxRetries) {
449 resp.error.retryDelay = this.service.retryDelays(resp.retryCount) || 0;
450 }
451 }
452 });
453
454 addAsync('RESET_RETRY_STATE', 'afterRetry', function RESET_RETRY_STATE(resp, done) {
455 var delay, willRetry = false;
456
457 if (resp.error) {
458 delay = resp.error.retryDelay || 0;
459 if (resp.error.retryable && resp.retryCount < resp.maxRetries) {
460 resp.retryCount++;
461 willRetry = true;
462 } else if (resp.error.redirect && resp.redirectCount < resp.maxRedirects) {
463 resp.redirectCount++;
464 willRetry = true;
465 }
466 }
467
468 if (willRetry) {
469 resp.error = null;
470 setTimeout(done, delay);
471 } else {
472 done();
473 }
474 });
475 }),
476
477 CorePost: new SequentialExecutor().addNamedListeners(function(add) {
478 add('EXTRACT_REQUEST_ID', 'extractData', AWS.util.extractRequestId);
479 add('EXTRACT_REQUEST_ID', 'extractError', AWS.util.extractRequestId);
480
481 add('ENOTFOUND_ERROR', 'httpError', function ENOTFOUND_ERROR(err) {
482 if (err.code === 'NetworkingError' && err.errno === 'ENOTFOUND') {
483 var message = 'Inaccessible host: `' + err.hostname +
484 '\'. This service may not be available in the `' + err.region +
485 '\' region.';
486 this.response.error = AWS.util.error(new Error(message), {
487 code: 'UnknownEndpoint',
488 region: err.region,
489 hostname: err.hostname,
490 retryable: true,
491 originalError: err
492 });
493 }
494 });
495 }),
496
497 Logger: new SequentialExecutor().addNamedListeners(function(add) {
498 add('LOG_REQUEST', 'complete', function LOG_REQUEST(resp) {
499 var req = resp.request;
500 var logger = req.service.config.logger;
501 if (!logger) return;
502 function filterSensitiveLog(inputShape, shape) {
503 if (!shape) {
504 return shape;
505 }
506 switch (inputShape.type) {
507 case 'structure':
508 var struct = {};
509 AWS.util.each(shape, function(subShapeName, subShape) {
510 if (Object.prototype.hasOwnProperty.call(inputShape.members, subShapeName)) {
511 struct[subShapeName] = filterSensitiveLog(inputShape.members[subShapeName], subShape);
512 } else {
513 struct[subShapeName] = subShape;
514 }
515 });
516 return struct;
517 case 'list':
518 var list = [];
519 AWS.util.arrayEach(shape, function(subShape, index) {
520 list.push(filterSensitiveLog(inputShape.member, subShape));
521 });
522 return list;
523 case 'map':
524 var map = {};
525 AWS.util.each(shape, function(key, value) {
526 map[key] = filterSensitiveLog(inputShape.value, value);
527 });
528 return map;
529 default:
530 if (inputShape.isSensitive) {
531 return '***SensitiveInformation***';
532 } else {
533 return shape;
534 }
535 }
536 }
537
538 function buildMessage() {
539 var time = resp.request.service.getSkewCorrectedDate().getTime();
540 var delta = (time - req.startTime.getTime()) / 1000;
541 var ansi = logger.isTTY ? true : false;
542 var status = resp.httpResponse.statusCode;
543 var censoredParams = req.params;
544 if (
545 req.service.api.operations &&
546 req.service.api.operations[req.operation] &&
547 req.service.api.operations[req.operation].input
548 ) {
549 var inputShape = req.service.api.operations[req.operation].input;
550 censoredParams = filterSensitiveLog(inputShape, req.params);
551 }
552 var params = require('util').inspect(censoredParams, true, null);
553 var message = '';
554 if (ansi) message += '\x1B[33m';
555 message += '[AWS ' + req.service.serviceIdentifier + ' ' + status;
556 message += ' ' + delta.toString() + 's ' + resp.retryCount + ' retries]';
557 if (ansi) message += '\x1B[0;1m';
558 message += ' ' + AWS.util.string.lowerFirst(req.operation);
559 message += '(' + params + ')';
560 if (ansi) message += '\x1B[0m';
561 return message;
562 }
563
564 var line = buildMessage();
565 if (typeof logger.log === 'function') {
566 logger.log(line);
567 } else if (typeof logger.write === 'function') {
568 logger.write(line + '\n');
569 }
570 });
571 }),
572
573 Json: new SequentialExecutor().addNamedListeners(function(add) {
574 var svc = require('./protocol/json');
575 add('BUILD', 'build', svc.buildRequest);
576 add('EXTRACT_DATA', 'extractData', svc.extractData);
577 add('EXTRACT_ERROR', 'extractError', svc.extractError);
578 }),
579
580 Rest: new SequentialExecutor().addNamedListeners(function(add) {
581 var svc = require('./protocol/rest');
582 add('BUILD', 'build', svc.buildRequest);
583 add('EXTRACT_DATA', 'extractData', svc.extractData);
584 add('EXTRACT_ERROR', 'extractError', svc.extractError);
585 }),
586
587 RestJson: new SequentialExecutor().addNamedListeners(function(add) {
588 var svc = require('./protocol/rest_json');
589 add('BUILD', 'build', svc.buildRequest);
590 add('EXTRACT_DATA', 'extractData', svc.extractData);
591 add('EXTRACT_ERROR', 'extractError', svc.extractError);
592 }),
593
594 RestXml: new SequentialExecutor().addNamedListeners(function(add) {
595 var svc = require('./protocol/rest_xml');
596 add('BUILD', 'build', svc.buildRequest);
597 add('EXTRACT_DATA', 'extractData', svc.extractData);
598 add('EXTRACT_ERROR', 'extractError', svc.extractError);
599 }),
600
601 Query: new SequentialExecutor().addNamedListeners(function(add) {
602 var svc = require('./protocol/query');
603 add('BUILD', 'build', svc.buildRequest);
604 add('EXTRACT_DATA', 'extractData', svc.extractData);
605 add('EXTRACT_ERROR', 'extractError', svc.extractError);
606 })
607};