var AWS = require('./core'); var Api = require('./model/api'); var regionConfig = require('./region_config'); var inherit = AWS.util.inherit; var clientCount = 0; /** * The service class representing an AWS service. * * @class_abstract This class is an abstract class. * * @!attribute apiVersions * @return [Array] the list of API versions supported by this service. * @readonly */ AWS.Service = inherit({ /** * Create a new service object with a configuration object * * @param config [map] a map of configuration options */ constructor: function Service(config) { if (!this.loadServiceClass) { throw AWS.util.error(new Error(), 'Service must be constructed with `new\' operator'); } var ServiceClass = this.loadServiceClass(config || {}); if (ServiceClass) { var originalConfig = AWS.util.copy(config); var svc = new ServiceClass(config); Object.defineProperty(svc, '_originalConfig', { get: function() { return originalConfig; }, enumerable: false, configurable: true }); svc._clientId = ++clientCount; return svc; } this.initialize(config); }, /** * @api private */ initialize: function initialize(config) { var svcConfig = AWS.config[this.serviceIdentifier]; this.config = new AWS.Config(AWS.config); if (svcConfig) this.config.update(svcConfig, true); if (config) this.config.update(config, true); this.validateService(); if (!this.config.endpoint) regionConfig(this); this.config.endpoint = this.endpointFromTemplate(this.config.endpoint); this.setEndpoint(this.config.endpoint); //enable attaching listeners to service client AWS.SequentialExecutor.call(this); AWS.Service.addDefaultMonitoringListeners(this); if ((this.config.clientSideMonitoring || AWS.Service._clientSideMonitoring) && this.publisher) { var publisher = this.publisher; this.addNamedListener('PUBLISH_API_CALL', 'apiCall', function PUBLISH_API_CALL(event) { process.nextTick(function() {publisher.eventHandler(event);}); }); this.addNamedListener('PUBLISH_API_ATTEMPT', 'apiCallAttempt', function PUBLISH_API_ATTEMPT(event) { process.nextTick(function() {publisher.eventHandler(event);}); }); } }, /** * @api private */ validateService: function validateService() { }, /** * @api private */ loadServiceClass: function loadServiceClass(serviceConfig) { var config = serviceConfig; if (!AWS.util.isEmpty(this.api)) { return null; } else if (config.apiConfig) { return AWS.Service.defineServiceApi(this.constructor, config.apiConfig); } else if (!this.constructor.services) { return null; } else { config = new AWS.Config(AWS.config); config.update(serviceConfig, true); var version = config.apiVersions[this.constructor.serviceIdentifier]; version = version || config.apiVersion; return this.getLatestServiceClass(version); } }, /** * @api private */ getLatestServiceClass: function getLatestServiceClass(version) { version = this.getLatestServiceVersion(version); if (this.constructor.services[version] === null) { AWS.Service.defineServiceApi(this.constructor, version); } return this.constructor.services[version]; }, /** * @api private */ getLatestServiceVersion: function getLatestServiceVersion(version) { if (!this.constructor.services || this.constructor.services.length === 0) { throw new Error('No services defined on ' + this.constructor.serviceIdentifier); } if (!version) { version = 'latest'; } else if (AWS.util.isType(version, Date)) { version = AWS.util.date.iso8601(version).split('T')[0]; } if (Object.hasOwnProperty(this.constructor.services, version)) { return version; } var keys = Object.keys(this.constructor.services).sort(); var selectedVersion = null; for (var i = keys.length - 1; i >= 0; i--) { // versions that end in "*" are not available on disk and can be // skipped, so do not choose these as selectedVersions if (keys[i][keys[i].length - 1] !== '*') { selectedVersion = keys[i]; } if (keys[i].substr(0, 10) <= version) { return selectedVersion; } } throw new Error('Could not find ' + this.constructor.serviceIdentifier + ' API to satisfy version constraint `' + version + '\''); }, /** * @api private */ api: {}, /** * @api private */ defaultRetryCount: 3, /** * @api private */ customizeRequests: function customizeRequests(callback) { if (!callback) { this.customRequestHandler = null; } else if (typeof callback === 'function') { this.customRequestHandler = callback; } else { throw new Error('Invalid callback type \'' + typeof callback + '\' provided in customizeRequests'); } }, /** * Calls an operation on a service with the given input parameters. * * @param operation [String] the name of the operation to call on the service. * @param params [map] a map of input options for the operation * @callback callback function(err, data) * If a callback is supplied, it is called when a response is returned * from the service. * @param err [Error] the error object returned from the request. * Set to `null` if the request is successful. * @param data [Object] the de-serialized data returned from * the request. Set to `null` if a request error occurs. */ makeRequest: function makeRequest(operation, params, callback) { if (typeof params === 'function') { callback = params; params = null; } params = params || {}; if (this.config.params) { // copy only toplevel bound params var rules = this.api.operations[operation]; if (rules) { params = AWS.util.copy(params); AWS.util.each(this.config.params, function(key, value) { if (rules.input.members[key]) { if (params[key] === undefined || params[key] === null) { params[key] = value; } } }); } } var request = new AWS.Request(this, operation, params); this.addAllRequestListeners(request); this.attachMonitoringEmitter(request); if (callback) request.send(callback); return request; }, /** * Calls an operation on a service with the given input parameters, without * any authentication data. This method is useful for "public" API operations. * * @param operation [String] the name of the operation to call on the service. * @param params [map] a map of input options for the operation * @callback callback function(err, data) * If a callback is supplied, it is called when a response is returned * from the service. * @param err [Error] the error object returned from the request. * Set to `null` if the request is successful. * @param data [Object] the de-serialized data returned from * the request. Set to `null` if a request error occurs. */ makeUnauthenticatedRequest: function makeUnauthenticatedRequest(operation, params, callback) { if (typeof params === 'function') { callback = params; params = {}; } var request = this.makeRequest(operation, params).toUnauthenticated(); return callback ? request.send(callback) : request; }, /** * Waits for a given state * * @param state [String] the state on the service to wait for * @param params [map] a map of parameters to pass with each request * @option params $waiter [map] a map of configuration options for the waiter * @option params $waiter.delay [Number] The number of seconds to wait between * requests * @option params $waiter.maxAttempts [Number] The maximum number of requests * to send while waiting * @callback callback function(err, data) * If a callback is supplied, it is called when a response is returned * from the service. * @param err [Error] the error object returned from the request. * Set to `null` if the request is successful. * @param data [Object] the de-serialized data returned from * the request. Set to `null` if a request error occurs. */ waitFor: function waitFor(state, params, callback) { var waiter = new AWS.ResourceWaiter(this, state); return waiter.wait(params, callback); }, /** * @api private */ addAllRequestListeners: function addAllRequestListeners(request) { var list = [AWS.events, AWS.EventListeners.Core, this.serviceInterface(), AWS.EventListeners.CorePost]; for (var i = 0; i < list.length; i++) { if (list[i]) request.addListeners(list[i]); } // disable parameter validation if (!this.config.paramValidation) { request.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS); } if (this.config.logger) { // add logging events request.addListeners(AWS.EventListeners.Logger); } this.setupRequestListeners(request); // call prototype's customRequestHandler if (typeof this.constructor.prototype.customRequestHandler === 'function') { this.constructor.prototype.customRequestHandler(request); } // call instance's customRequestHandler if (Object.prototype.hasOwnProperty.call(this, 'customRequestHandler') && typeof this.customRequestHandler === 'function') { this.customRequestHandler(request); } }, /** * Event recording metrics for a whole API call. * @returns {object} a subset of api call metrics * @api private */ apiCallEvent: function apiCallEvent(request) { var api = request.service.api.operations[request.operation]; var monitoringEvent = { Type: 'ApiCall', Api: api ? api.name : request.operation, Version: 1, Service: request.service.api.serviceId || request.service.api.endpointPrefix, Region: request.httpRequest.region, MaxRetriesExceeded: 0, UserAgent: request.httpRequest.getUserAgent(), }; var response = request.response; if (response.httpResponse.statusCode) { monitoringEvent.FinalHttpStatusCode = response.httpResponse.statusCode; } if (response.error) { var error = response.error; var statusCode = response.httpResponse.statusCode; if (statusCode > 299) { if (error.code) monitoringEvent.FinalAwsException = error.code; if (error.message) monitoringEvent.FinalAwsExceptionMessage = error.message; } else { if (error.code || error.name) monitoringEvent.FinalSdkException = error.code || error.name; if (error.message) monitoringEvent.FinalSdkExceptionMessage = error.message; } } return monitoringEvent; }, /** * Event recording metrics for an API call attempt. * @returns {object} a subset of api call attempt metrics * @api private */ apiAttemptEvent: function apiAttemptEvent(request) { var api = request.service.api.operations[request.operation]; var monitoringEvent = { Type: 'ApiCallAttempt', Api: api ? api.name : request.operation, Version: 1, Service: request.service.api.serviceId || request.service.api.endpointPrefix, Fqdn: request.httpRequest.endpoint.hostname, UserAgent: request.httpRequest.getUserAgent(), }; var response = request.response; if (response.httpResponse.statusCode) { monitoringEvent.HttpStatusCode = response.httpResponse.statusCode; } if ( !request._unAuthenticated && request.service.config.credentials && request.service.config.credentials.accessKeyId ) { monitoringEvent.AccessKey = request.service.config.credentials.accessKeyId; } if (!response.httpResponse.headers) return monitoringEvent; if (request.httpRequest.headers['x-amz-security-token']) { monitoringEvent.SessionToken = request.httpRequest.headers['x-amz-security-token']; } if (response.httpResponse.headers['x-amzn-requestid']) { monitoringEvent.XAmznRequestId = response.httpResponse.headers['x-amzn-requestid']; } if (response.httpResponse.headers['x-amz-request-id']) { monitoringEvent.XAmzRequestId = response.httpResponse.headers['x-amz-request-id']; } if (response.httpResponse.headers['x-amz-id-2']) { monitoringEvent.XAmzId2 = response.httpResponse.headers['x-amz-id-2']; } return monitoringEvent; }, /** * Add metrics of failed request. * @api private */ attemptFailEvent: function attemptFailEvent(request) { var monitoringEvent = this.apiAttemptEvent(request); var response = request.response; var error = response.error; if (response.httpResponse.statusCode > 299 ) { if (error.code) monitoringEvent.AwsException = error.code; if (error.message) monitoringEvent.AwsExceptionMessage = error.message; } else { if (error.code || error.name) monitoringEvent.SdkException = error.code || error.name; if (error.message) monitoringEvent.SdkExceptionMessage = error.message; } return monitoringEvent; }, /** * Attach listeners to request object to fetch metrics of each request * and emit data object through \'ApiCall\' and \'ApiCallAttempt\' events. * @api private */ attachMonitoringEmitter: function attachMonitoringEmitter(request) { var attemptTimestamp; //timestamp marking the beginning of a request attempt var attemptStartRealTime; //Start time of request attempt. Used to calculating attemptLatency var attemptLatency; //latency from request sent out to http response reaching SDK var callStartRealTime; //Start time of API call. Used to calculating API call latency var attemptCount = 0; //request.retryCount is not reliable here var region; //region cache region for each attempt since it can be updated in plase (e.g. s3) var callTimestamp; //timestamp when the request is created var self = this; var addToHead = true; request.on('validate', function () { callStartRealTime = AWS.util.realClock.now(); callTimestamp = Date.now(); }, addToHead); request.on('sign', function () { attemptStartRealTime = AWS.util.realClock.now(); attemptTimestamp = Date.now(); region = request.httpRequest.region; attemptCount++; }, addToHead); request.on('validateResponse', function() { attemptLatency = Math.round(AWS.util.realClock.now() - attemptStartRealTime); }); request.addNamedListener('API_CALL_ATTEMPT', 'success', function API_CALL_ATTEMPT() { var apiAttemptEvent = self.apiAttemptEvent(request); apiAttemptEvent.Timestamp = attemptTimestamp; apiAttemptEvent.AttemptLatency = attemptLatency >= 0 ? attemptLatency : 0; apiAttemptEvent.Region = region; self.emit('apiCallAttempt', [apiAttemptEvent]); }); request.addNamedListener('API_CALL_ATTEMPT_RETRY', 'retry', function API_CALL_ATTEMPT_RETRY() { var apiAttemptEvent = self.attemptFailEvent(request); apiAttemptEvent.Timestamp = attemptTimestamp; //attemptLatency may not be available if fail before response attemptLatency = attemptLatency || Math.round(AWS.util.realClock.now() - attemptStartRealTime); apiAttemptEvent.AttemptLatency = attemptLatency >= 0 ? attemptLatency : 0; apiAttemptEvent.Region = region; self.emit('apiCallAttempt', [apiAttemptEvent]); }); request.addNamedListener('API_CALL', 'complete', function API_CALL() { var apiCallEvent = self.apiCallEvent(request); apiCallEvent.AttemptCount = attemptCount; if (apiCallEvent.AttemptCount <= 0) return; apiCallEvent.Timestamp = callTimestamp; var latency = Math.round(AWS.util.realClock.now() - callStartRealTime); apiCallEvent.Latency = latency >= 0 ? latency : 0; var response = request.response; if ( typeof response.retryCount === 'number' && typeof response.maxRetries === 'number' && (response.retryCount >= response.maxRetries) ) { apiCallEvent.MaxRetriesExceeded = 1; } self.emit('apiCall', [apiCallEvent]); }); }, /** * Override this method to setup any custom request listeners for each * new request to the service. * * @method_abstract This is an abstract method. */ setupRequestListeners: function setupRequestListeners(request) { }, /** * Gets the signer class for a given request * @api private */ getSignerClass: function getSignerClass(request) { var version; // get operation authtype if present var operation = null; var authtype = ''; if (request) { var operations = request.service.api.operations || {}; operation = operations[request.operation] || null; authtype = operation ? operation.authtype : ''; } if (this.config.signatureVersion) { version = this.config.signatureVersion; } else if (authtype === 'v4' || authtype === 'v4-unsigned-body') { version = 'v4'; } else { version = this.api.signatureVersion; } return AWS.Signers.RequestSigner.getVersion(version); }, /** * @api private */ serviceInterface: function serviceInterface() { switch (this.api.protocol) { case 'ec2': return AWS.EventListeners.Query; case 'query': return AWS.EventListeners.Query; case 'json': return AWS.EventListeners.Json; case 'rest-json': return AWS.EventListeners.RestJson; case 'rest-xml': return AWS.EventListeners.RestXml; } if (this.api.protocol) { throw new Error('Invalid service `protocol\' ' + this.api.protocol + ' in API config'); } }, /** * @api private */ successfulResponse: function successfulResponse(resp) { return resp.httpResponse.statusCode < 300; }, /** * How many times a failed request should be retried before giving up. * the defaultRetryCount can be overriden by service classes. * * @api private */ numRetries: function numRetries() { if (this.config.maxRetries !== undefined) { return this.config.maxRetries; } else { return this.defaultRetryCount; } }, /** * @api private */ retryDelays: function retryDelays(retryCount) { return AWS.util.calculateRetryDelay(retryCount, this.config.retryDelayOptions); }, /** * @api private */ retryableError: function retryableError(error) { if (this.timeoutError(error)) return true; if (this.networkingError(error)) return true; if (this.expiredCredentialsError(error)) return true; if (this.throttledError(error)) return true; if (error.statusCode >= 500) return true; return false; }, /** * @api private */ networkingError: function networkingError(error) { return error.code === 'NetworkingError'; }, /** * @api private */ timeoutError: function timeoutError(error) { return error.code === 'TimeoutError'; }, /** * @api private */ expiredCredentialsError: function expiredCredentialsError(error) { // TODO : this only handles *one* of the expired credential codes return (error.code === 'ExpiredTokenException'); }, /** * @api private */ clockSkewError: function clockSkewError(error) { switch (error.code) { case 'RequestTimeTooSkewed': case 'RequestExpired': case 'InvalidSignatureException': case 'SignatureDoesNotMatch': case 'AuthFailure': case 'RequestInTheFuture': return true; default: return false; } }, /** * @api private */ getSkewCorrectedDate: function getSkewCorrectedDate() { return new Date(Date.now() + this.config.systemClockOffset); }, /** * @api private */ applyClockOffset: function applyClockOffset(newServerTime) { if (newServerTime) { this.config.systemClockOffset = newServerTime - Date.now(); } }, /** * @api private */ isClockSkewed: function isClockSkewed(newServerTime) { if (newServerTime) { return Math.abs(this.getSkewCorrectedDate().getTime() - newServerTime) >= 30000; } }, /** * @api private */ throttledError: function throttledError(error) { // this logic varies between services switch (error.code) { case 'ProvisionedThroughputExceededException': case 'Throttling': case 'ThrottlingException': case 'RequestLimitExceeded': case 'RequestThrottled': case 'RequestThrottledException': case 'TooManyRequestsException': case 'TransactionInProgressException': //dynamodb return true; default: return false; } }, /** * @api private */ endpointFromTemplate: function endpointFromTemplate(endpoint) { if (typeof endpoint !== 'string') return endpoint; var e = endpoint; e = e.replace(/\{service\}/g, this.api.endpointPrefix); e = e.replace(/\{region\}/g, this.config.region); e = e.replace(/\{scheme\}/g, this.config.sslEnabled ? 'https' : 'http'); return e; }, /** * @api private */ setEndpoint: function setEndpoint(endpoint) { this.endpoint = new AWS.Endpoint(endpoint, this.config); }, /** * @api private */ paginationConfig: function paginationConfig(operation, throwException) { var paginator = this.api.operations[operation].paginator; if (!paginator) { if (throwException) { var e = new Error(); throw AWS.util.error(e, 'No pagination configuration for ' + operation); } return null; } return paginator; } }); AWS.util.update(AWS.Service, { /** * Adds one method for each operation described in the api configuration * * @api private */ defineMethods: function defineMethods(svc) { AWS.util.each(svc.prototype.api.operations, function iterator(method) { if (svc.prototype[method]) return; var operation = svc.prototype.api.operations[method]; if (operation.authtype === 'none') { svc.prototype[method] = function (params, callback) { return this.makeUnauthenticatedRequest(method, params, callback); }; } else { svc.prototype[method] = function (params, callback) { return this.makeRequest(method, params, callback); }; } }); }, /** * Defines a new Service class using a service identifier and list of versions * including an optional set of features (functions) to apply to the class * prototype. * * @param serviceIdentifier [String] the identifier for the service * @param versions [Array] a list of versions that work with this * service * @param features [Object] an object to attach to the prototype * @return [Class] the service class defined by this function. */ defineService: function defineService(serviceIdentifier, versions, features) { AWS.Service._serviceMap[serviceIdentifier] = true; if (!Array.isArray(versions)) { features = versions; versions = []; } var svc = inherit(AWS.Service, features || {}); if (typeof serviceIdentifier === 'string') { AWS.Service.addVersions(svc, versions); var identifier = svc.serviceIdentifier || serviceIdentifier; svc.serviceIdentifier = identifier; } else { // defineService called with an API svc.prototype.api = serviceIdentifier; AWS.Service.defineMethods(svc); } AWS.SequentialExecutor.call(this.prototype); //util.clientSideMonitoring is only available in node if (!this.prototype.publisher && AWS.util.clientSideMonitoring) { var Publisher = AWS.util.clientSideMonitoring.Publisher; var configProvider = AWS.util.clientSideMonitoring.configProvider; var publisherConfig = configProvider(); this.prototype.publisher = new Publisher(publisherConfig); if (publisherConfig.enabled) { //if csm is enabled in environment, SDK should send all metrics AWS.Service._clientSideMonitoring = true; } } AWS.SequentialExecutor.call(svc.prototype); AWS.Service.addDefaultMonitoringListeners(svc.prototype); return svc; }, /** * @api private */ addVersions: function addVersions(svc, versions) { if (!Array.isArray(versions)) versions = [versions]; svc.services = svc.services || {}; for (var i = 0; i < versions.length; i++) { if (svc.services[versions[i]] === undefined) { svc.services[versions[i]] = null; } } svc.apiVersions = Object.keys(svc.services).sort(); }, /** * @api private */ defineServiceApi: function defineServiceApi(superclass, version, apiConfig) { var svc = inherit(superclass, { serviceIdentifier: superclass.serviceIdentifier }); function setApi(api) { if (api.isApi) { svc.prototype.api = api; } else { svc.prototype.api = new Api(api); } } if (typeof version === 'string') { if (apiConfig) { setApi(apiConfig); } else { try { setApi(AWS.apiLoader(superclass.serviceIdentifier, version)); } catch (err) { throw AWS.util.error(err, { message: 'Could not find API configuration ' + superclass.serviceIdentifier + '-' + version }); } } if (!Object.prototype.hasOwnProperty.call(superclass.services, version)) { superclass.apiVersions = superclass.apiVersions.concat(version).sort(); } superclass.services[version] = svc; } else { setApi(version); } AWS.Service.defineMethods(svc); return svc; }, /** * @api private */ hasService: function(identifier) { return Object.prototype.hasOwnProperty.call(AWS.Service._serviceMap, identifier); }, /** * @param attachOn attach default monitoring listeners to object * * Each monitoring event should be emitted from service client to service constructor prototype and then * to global service prototype like bubbling up. These default monitoring events listener will transfer * the monitoring events to the upper layer. * @api private */ addDefaultMonitoringListeners: function addDefaultMonitoringListeners(attachOn) { attachOn.addNamedListener('MONITOR_EVENTS_BUBBLE', 'apiCallAttempt', function EVENTS_BUBBLE(event) { var baseClass = Object.getPrototypeOf(attachOn); if (baseClass._events) baseClass.emit('apiCallAttempt', [event]); }); attachOn.addNamedListener('CALL_EVENTS_BUBBLE', 'apiCall', function CALL_EVENTS_BUBBLE(event) { var baseClass = Object.getPrototypeOf(attachOn); if (baseClass._events) baseClass.emit('apiCall', [event]); }); }, /** * @api private */ _serviceMap: {} }); AWS.util.mixin(AWS.Service, AWS.SequentialExecutor); /** * @api private */ module.exports = AWS.Service;