Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #78 from aws/ec2metadata

Load credentials from EC2 instance metadata

Adds a number of changes:

* Refactors Credential loading to support asynchronous requests.
* Support for passing low-level HTTP options to HttpClient through Config

Fixes #5
Fixes #50
  • Loading branch information...
commit 33db81220739c48717e3a4da7dc137062d78009e 2 parents 79c63bf + 40d2159
@lsegal lsegal authored
View
1  features/elasticbeanstalk/elasticbeanstalk.feature
@@ -17,6 +17,7 @@ Feature: AWS Elastic Beanstalk
I want to use AWS Elastic Beanstalk
+ @requiresakid
Scenario: Creating applications and application versions
Given I create an Elastic Beanstalk application with name prefix "aws-js-sdk"
And I create an Elastic Beanstalk application version with label "1.0.0"
View
2  features/importexport/importexport.feature
@@ -12,7 +12,7 @@
# language governing permissions and limitations under the License.
# language: en
-@importexport
+@importexport @requiresakid
Feature: AWS Import/Export
I want to use AWS Import/Export
View
1  features/sts/sts.feature
@@ -17,6 +17,7 @@ Feature: AWS Security Token Service
I want to use AWS Security Token Service
+ @requiresakid
Scenario: Get a session token
Given I get an STS session token with a duration of 900 seconds
Then the result should contain an access key ID and secret access key
View
309 lib/config.js
@@ -16,6 +16,7 @@
var AWS = require('./core');
require('./event_listeners');
require('./event_emitter');
+require('./metadata_service');
var inherit = AWS.util.inherit;
/**
@@ -78,6 +79,9 @@ AWS.Config = inherit({
* @option options credentials [AWS.Credentials] the AWS credentials
* to sign requests with. You can either specify this object, or
* specify the accessKeyId and secretAccessKey options directly.
+ * @option options credentialProvider [AWS.CredentialProviderChain] the
+ * provider chain used to resolve credentials if no static `credentials`
+ * property is set.
* @option options region [String] the region to send service requests to.
* See {region} for more information.
* @option options maxRetries [Integer] the maximum amount of retries to
@@ -91,6 +95,16 @@ AWS.Config = inherit({
* in S3 only)
* @option options s3ForcePathStyle [Boolean] whether to force path
* style URLs for S3 objects.
+ * @option options httpOptions [map] A set of options to pass to the low-level
+ * HTTP request. Currently supported options are:
+ *
+ * * **agent** [http.Agent, https.Agent] — the Agent object to perform
+ * HTTP requests with. Used for connection pooling. Defaults to the global
+ * agent (`http.globalAgent`) for non-SSL connections. Note that for
+ * SSL connections, a special Agent object is used in order to enable
+ * peer certificate verification.
+ * * **timeout** [Integer] — The number of milliseconds to wait before
+ * giving up on a connection attempt. Defaults to no timeout.
*/
constructor: function Config(options) {
if (options === undefined) options = {};
@@ -117,6 +131,60 @@ AWS.Config = inherit({
},
/**
+ * @api private
+ */
+ getCredentials: function getCredentials(callback) {
+ var self = this;
+
+ function finish(err) {
+ callback(err, err ? null : self.credentials);
+ }
+
+ function credError(msg, err) {
+ return new AWS.util.error(err || new Error(), {
+ code: 'CredentialsError', message: msg
+ });
+ }
+
+ function getAsyncCredentials() {
+ self.credentials.get(function(err) {
+ if (err) {
+ var msg = 'Could not load credentials from ' +
+ self.credentials.constructor.name;
+ err = credError(msg, err);
+ }
+ finish(err);
+ });
+ }
+
+ function getStaticCredentials() {
+ var err = null;
+ if (!self.credentials.accessKeyId || !self.credentials.secretAccessKey) {
+ err = credError('Missing credentials');
+ }
+ finish(err);
+ }
+
+ if (self.credentials) {
+ if (typeof self.credentials.get === 'function') {
+ getAsyncCredentials();
+ } else { // static credentials
+ getStaticCredentials();
+ }
+ } else if (self.credentialProvider) {
+ self.credentialProvider.resolve(function(err, creds) {
+ if (err) {
+ err = credError('Could not load credentials from any providers', err);
+ }
+ self.credentials = creds;
+ finish(err);
+ });
+ } else {
+ finish(credError('No credentials to load'));
+ }
+ },
+
+ /**
* Loads configuration data from a JSON file into this config object.
* @note Loading configuration will reset all existing configuration
* on the object.
@@ -130,7 +198,10 @@ AWS.Config = inherit({
var fileSystemCreds = new AWS.FileSystemCredentials(path, this);
var chain = new AWS.CredentialProviderChain();
chain.providers.unshift(fileSystemCreds);
- options.credentials = chain.resolve();
+ chain.resolve(function (err, creds) {
+ if (err) throw err;
+ else options.credentials = creds;
+ });
this.constructor(options);
@@ -150,6 +221,7 @@ AWS.Config = inherit({
// reset credential provider
this.set('credentials', undefined);
+ this.set('credentialProvider', undefined);
},
/**
@@ -180,11 +252,24 @@ AWS.Config = inherit({
*/
keys: {
credentials: function () {
- return new AWS.CredentialProviderChain().resolve();
+ var credentials = null;
+ new AWS.CredentialProviderChain([
+ function () { return new AWS.EnvironmentCredentials('AWS'); },
+ function () { return new AWS.EnvironmentCredentials('AMAZON'); }
+ ]).resolve(function(err, creds) {
+ if (!err) credentials = creds;
+ });
+ return credentials;
+ },
+ credentialProvider: function() {
+ return new AWS.CredentialProviderChain([
+ function() { return new AWS.EC2MetadataCredentials(); }
+ ]);
},
region: function() {
return process.env.AWS_REGION || process.env.AMAZON_REGION;
},
+ httpOptions: {},
maxRetries: undefined,
paramValidation: true,
sslEnabled: true,
@@ -233,6 +318,9 @@ AWS.Config = inherit({
* some network storage. The method should reset the credential attributes
* on the object.
*
+ * @!attribute expired
+ * @return [Boolean] whether the credentials have been expired and
+ * require a refresh
* @!attribute accessKeyId
* @return [String] the AWS access key ID
* @!attribute secretAccessKey
@@ -241,7 +329,6 @@ AWS.Config = inherit({
* @return [String] an optional AWS session token
*/
AWS.Credentials = inherit({
-
/**
* A credentials object can be created using positional arguments or an options
* hash.
@@ -266,6 +353,7 @@ AWS.Credentials = inherit({
* });
*/
constructor: function Credentials() {
+ this.expired = false;
if (arguments.length == 1 && typeof arguments[0] === 'object') {
var creds = arguments[0].credentials || arguments[0];
this.accessKeyId = creds.accessKeyId;
@@ -279,14 +367,58 @@ AWS.Credentials = inherit({
},
/**
- * Refreshes the credentials.
+ * @return [Boolean] whether the credentials object should call {refresh}
+ * @note Subclasses should override this method to provide custom refresh
+ * logic.
+ */
+ needsRefresh: function needsRefresh() {
+ return this.expired || !this.accessKeyId || !this.secretAccessKey;
+ },
+
+ /**
+ * Gets the existing credentials, refreshing them if they are not yet loaded
+ * or have expired. Users should call this method before using {refresh},
+ * as this will not attempt to reload credentials when they are already
+ * loaded into the object.
+ *
+ * @callback callback function(err)
+ * Called when the instance metadata service responds (or fails). When
+ * this callback is called with no error, it means that the credentials
+ * information has been loaded into the object (as the `accessKeyId`,
+ * `secretAccessKey`, and `sessionToken` properties).
+ * @param err [Error] if an error occurred, this value will be filled
+ */
+ get: function get(callback) {
+ var self = this;
+ if (this.needsRefresh()) {
+ this.refresh(function(err) {
+ if (!err) self.expired = false; // reset expired flag
+ callback(err);
+ });
+ } else {
+ callback();
+ }
+ },
+
+ /**
+ * Refreshes the credentials. Users should call {get} before attempting
+ * to forcibly refresh credentials.
*
+ * @callback callback function(err)
+ * Called when the instance metadata service responds (or fails). When
+ * this callback is called with no error, it means that the credentials
+ * information has been loaded into the object (as the `accessKeyId`,
+ * `secretAccessKey`, and `sessionToken` properties).
+ * @param err [Error] if an error occurred, this value will be filled
* @note Subclasses should override this class to reset the
* {accessKeyId}, {secretAccessKey} and optional {sessionToken}
- * on the credentials object.
+ * on the credentials object and then call the callback with
+ * any error information.
+ * @see get
*/
- refresh: function refresh() { }
-
+ refresh: function refresh(callback) {
+ callback();
+ }
});
/**
@@ -323,14 +455,31 @@ AWS.FileSystemCredentials = inherit(AWS.Credentials, {
constructor: function FileSystemCredentials(filename, initialCredentials) {
this.filename = filename;
AWS.Credentials.call(this, initialCredentials);
- if (!this.accessKeyId) this.refresh();
+ this.get(function() {});
},
/**
- * Refreshes the credentials from the {filename} on disk.
+ * Loads the credentials from the {filename} on disk.
+ *
+ * @callback callback function(err)
+ * Called when the instance metadata service responds (or fails). When
+ * this callback is called with no error, it means that the credentials
+ * information has been loaded into the object (as the `accessKeyId`,
+ * `secretAccessKey`, and `sessionToken` properties).
+ * @param err [Error] if an error occurred, this value will be filled
+ * @see get
*/
- refresh: function refresh() {
- AWS.Credentials.call(this, JSON.parse(AWS.util.readFileSync(this.filename)));
+ refresh: function refresh(callback) {
+ if (!callback) callback = function(err) { if (err) throw err; };
+ try {
+ AWS.Credentials.call(this, JSON.parse(AWS.util.readFileSync(this.filename)));
+ if (!this.accessKeyId || !this.secretAccessKey) {
+ throw new Error('Credentials not set in ' + this.filename);
+ }
+ callback();
+ } catch (err) {
+ callback(err);
+ }
}
});
@@ -374,32 +523,98 @@ AWS.EnvironmentCredentials = inherit(AWS.Credentials, {
*/
constructor: function EnvironmentCredentials(envPrefix) {
this.envPrefix = envPrefix;
- this.refresh();
+ this.get(function() {});
},
/**
- * Refreshes credentials from the environment using the prefixed
+ * Loads credentials from the environment using the prefixed
* environment variables.
+ *
+ * @callback callback function(err)
+ * Called when the instance metadata service responds (or fails). When
+ * this callback is called with no error, it means that the credentials
+ * information has been loaded into the object (as the `accessKeyId`,
+ * `secretAccessKey`, and `sessionToken` properties).
+ * @param err [Error] if an error occurred, this value will be filled
+ * @see get
*/
- refresh: function refresh() {
- if (process === undefined) return;
+ refresh: function refresh(callback) {
+ /*jshint maxcomplexity:10*/
+ if (!callback) callback = function(err) { if (err) throw err; };
+
+ if (process === undefined) {
+ callback(new Error('No process info available'));
+ return;
+ }
var keys = ['ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'SESSION_TOKEN'];
var values = [];
- /*jshint forin:false*/
- for (var i in keys) {
+ for (var i = 0; i < keys.length; i++) {
var prefix = '';
if (this.envPrefix) prefix = this.envPrefix + '_';
values[i] = process.env[prefix + keys[i]];
+ if (!values[i] && keys[i] !== 'SESSION_TOKEN') {
+ callback(new Error('Variable ' + prefix + keys[i] + ' not set.'));
+ return;
+ }
}
AWS.Credentials.apply(this, values);
+ callback();
}
});
/**
+ * Represents credentials recieved from the metadata service on an EC2 instance.
+ *
+ * By default, this class will connect to the metadata service using
+ * {AWS.MetadataService} and attempt to load any available credentials. If it
+ * can connect, and credentials are available, these will be used with zero
+ * configuration.
+ */
+AWS.EC2MetadataCredentials = inherit(AWS.Credentials, {
+ constructor: function EC2MetadataCredentials(options) {
+ this.serviceError = null;
+ this.metadataService = new AWS.MetadataService(options);
+ this.metadata = {};
+ },
+
+ /**
+ * Loads the credentials from the instance metadata service
+ *
+ * @callback callback function(err)
+ * Called when the instance metadata service responds (or fails). When
+ * this callback is called with no error, it means that the credentials
+ * information has been loaded into the object (as the `accessKeyId`,
+ * `secretAccessKey`, and `sessionToken` properties).
+ * @param err [Error] if an error occurred, this value will be filled
+ * @see get
+ */
+ refresh: function refresh(callback) {
+ var self = this;
+ if (!callback) callback = function(err) { if (err) throw err; };
+ if (self.serviceError) {
+ callback(self.serviceError);
+ return;
+ }
+
+ self.metadataService.loadCredentials(function (err, creds) {
+ if (err) {
+ self.serviceError = err;
+ } else {
+ self.metadata = creds;
+ self.accessKeyId = creds.AccessKeyId;
+ self.secretAccessKey = creds.SecretAccessKey;
+ self.sessionToken = creds.Token;
+ }
+ callback(err);
+ });
+ }
+});
+
+/**
* Creates a credential provider chain that searches for AWS credentials
* in a list of credential providers specified by the {providers} property.
*
@@ -441,40 +656,67 @@ AWS.EnvironmentCredentials = inherit(AWS.Credentials, {
* {defaultProviders}.
* @see defaultProviders
*/
-AWS.CredentialProviderChain = inherit({
+AWS.CredentialProviderChain = inherit(AWS.Credentials, {
/**
* Creates a new CredentialProviderChain with a default set of providers
* specified by {defaultProviders}.
*/
- constructor: function CredentialProviderChain() {
- this.providers = AWS.CredentialProviderChain.defaultProviders.slice(0);
+ constructor: function CredentialProviderChain(providers) {
+ if (providers) {
+ this.providers = providers;
+ } else {
+ this.providers = AWS.CredentialProviderChain.defaultProviders.slice(0);
+ }
},
/**
* Resolves the provider chain by searching for the first set of
* credentials in {providers}.
*
- * @return [AWS.Credentials] the first set of credentials discovered
- * in the chain of {providers}.
- * @return [null] if no credentials are found.
+ * @callback callback function(err, credentials)
+ * Called when the provider resolves the chain to a credentials object
+ * or null if no credentials can be found.
+ *
+ * @param err [Error] the error object returned if no credentials are
+ * found.
+ * @param credentials [AWS.Credentials] the credentials object resolved
+ * by the provider chain.
+ * @return [AWS.CredentialProviderChain] the provider, for chaining.
*/
- resolve: function resolve() {
- var finalCreds;
- AWS.util.arrayEach(this.providers, function (provider) {
- var creds;
+ resolve: function resolve(callback) {
+ if (this.providers.length === 0) {
+ callback(new Error('No providers'));
+ return;
+ }
+
+ var index = 0;
+ var providers = this.providers.slice(0);
+
+ function resolveNext(err, creds) {
+ if ((!err && creds) || index === providers.length) {
+ callback(err, creds);
+ return;
+ }
+
+ var provider = providers[index++];
if (typeof provider === 'function') {
creds = provider.call();
} else {
creds = provider;
}
- if (creds.accessKeyId) {
- finalCreds = creds;
- return AWS.util.abort;
+ if (creds.get) {
+ creds.get(function(err) {
+ resolveNext(err, err ? null : creds);
+ });
+ } else {
+ resolveNext(null, creds);
}
- });
- return finalCreds ? finalCreds : new AWS.Credentials();
+ }
+
+ resolveNext();
+ return this;
}
});
@@ -484,7 +726,8 @@ AWS.CredentialProviderChain = inherit({
*/
AWS.CredentialProviderChain.defaultProviders = [
function () { return new AWS.EnvironmentCredentials('AWS'); },
- function () { return new AWS.EnvironmentCredentials('AMAZON'); }
+ function () { return new AWS.EnvironmentCredentials('AMAZON'); },
+ function () { return new AWS.EC2MetadataCredentials(); }
];
/**
View
1  lib/core.js
@@ -53,6 +53,7 @@ require('./client');
require('./service');
require('./signers/request_signer');
require('./param_validator');
+require('./metadata_service');
/**
* @readonly
View
2  lib/event_emitter.js
@@ -113,7 +113,7 @@ AWS.EventEmitter = AWS.util.inherit({
} else {
this.callListeners(listeners, args, doneCallback);
}
- };
+ }.bind(this);
listener.apply(this, args.concat([callNextListener]));
} else {
View
75 lib/event_listeners.js
@@ -84,12 +84,15 @@ AWS.EventListeners = {
AWS.EventListeners = {
Core: new AWS.EventEmitter().addNamedListeners(function(add, addAsync) {
- add('VALIDATE_CREDENTIALS', 'validate', function VALIDATE_CREDENTIALS(req) {
- if (!req.client.config.credentials.accessKeyId ||
- !req.client.config.credentials.secretAccessKey) {
- throw AWS.util.error(new Error(),
- {code: 'SigningError', message: 'Missing credentials in config'});
- }
+ addAsync('VALIDATE_CREDENTIALS', 'validate',
+ function VALIDATE_CREDENTIALS(req, doneCallback) {
+ req.client.config.getCredentials(function(err) {
+ if (err) {
+ err = AWS.util.error(err,
+ {code: 'SigningError', message: 'Missing credentials in config'});
+ }
+ doneCallback(err);
+ });
});
add('VALIDATE_REGION', 'validate', function VALIDATE_REGION(req) {
@@ -111,21 +114,29 @@ AWS.EventListeners = {
}
});
- add('SIGN', 'sign', function SIGN(req) {
- var date = AWS.util.date.getDate();
- var sigVersion = req.client.api.signatureVersion;
- var credentials = AWS.util.copy(req.client.config.credentials);
- var SignerClass = AWS.Signers.RequestSigner.getVersion(sigVersion);
- var signer = new SignerClass(req.httpRequest,
- req.client.api.signingName || req.client.api.endpointPrefix);
-
- // 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);
+ addAsync('SIGN', 'sign', function SIGN(req, doneCallback) {
+ req.client.config.getCredentials(function (err, credentials) {
+ try {
+ if (err) return doneCallback(err);
+
+ var date = AWS.util.date.getDate();
+ var sigVersion = req.client.api.signatureVersion;
+ var SignerClass = AWS.Signers.RequestSigner.getVersion(sigVersion);
+ var signer = new SignerClass(req.httpRequest,
+ req.client.api.signingName || req.client.api.endpointPrefix);
+
+ // 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);
+ doneCallback();
+ } catch (e) {
+ doneCallback(e);
+ }
+ });
});
add('SETUP_ERROR', 'extractError', function SETUP_ERROR(resp) {
@@ -146,7 +157,27 @@ AWS.EventListeners = {
});
add('SEND', 'send', function SEND(resp) {
- AWS.HttpClient.getInstance().handleRequest(this, resp);
+ function callback(httpResp) {
+ var headers = [httpResp.statusCode, httpResp.headers, resp];
+ resp.request.emitEvent('httpHeaders', headers);
+
+ httpResp.on('data', function onData(data) {
+ resp.request.emitEvent('httpData', [data, resp]);
+ });
+
+ httpResp.on('end', function onEnd() {
+ resp.request.emitEvent('httpDone', [resp]);
+ });
+ }
+
+ function error(err) {
+ err = AWS.util.error(err, {code: 'NetworkingError', retryable: true});
+ resp.request.emitEvent('httpError', [err, resp]);
+ }
+
+ var http = AWS.HttpClient.getInstance();
+ var httpOptions = resp.request.client.config.httpOptions || {};
+ http.handleRequest(this.httpRequest, httpOptions, callback, error);
});
add('HTTP_HEADERS', 'httpHeaders',
View
103 lib/http.js
@@ -57,6 +57,12 @@ AWS.Endpoint = inherit({
* @param endpoint [String] the URL to construct an endpoint from
*/
constructor: function Endpoint(endpoint, config) {
+ if (typeof endpoint === 'undefined' || endpoint === null) {
+ throw new Error('Invalid endpoint: ' + endpoint);
+ } else if (typeof endpoint !== 'string') {
+ return AWS.util.copy(endpoint);
+ }
+
if (!endpoint.match(/^http/)) {
var useSSL = config && config.sslEnabled !== undefined ?
config.sslEnabled : AWS.config.sslEnabled;
@@ -101,12 +107,13 @@ AWS.HttpRequest = inherit({
* @api private
*/
constructor: function HttpRequest(endpoint, region) {
+ endpoint = new AWS.Endpoint(endpoint);
this.method = 'POST';
- this.path = '/';
+ this.path = endpoint.path || '/';
this.headers = {};
this.headers['User-Agent'] = AWS.util.userAgent();
this.body = '';
- this.endpoint = AWS.util.copy(endpoint);
+ this.endpoint = endpoint;
this.region = region;
},
@@ -155,62 +162,68 @@ AWS.HttpResponse = inherit({
* @api private
*/
AWS.NodeHttpClient = inherit({
- handleRequest: function handleRequest(request, response) {
+ handleRequest: function handleRequest(httpRequest, httpOptions, callback, errCallback) {
+ var useSSL = httpRequest.endpoint.protocol === 'https:';
+ var http = useSSL ? require('https') : require('http');
var options = {
- host: request.httpRequest.endpoint.hostname,
- port: request.httpRequest.endpoint.port,
- method: request.httpRequest.method,
- headers: request.httpRequest.headers,
- path: request.httpRequest.path
+ host: httpRequest.endpoint.hostname,
+ port: httpRequest.endpoint.port,
+ method: httpRequest.method,
+ headers: httpRequest.headers,
+ path: httpRequest.path
};
- var useSSL = request.httpRequest.endpoint.protocol === 'https:';
- var client = useSSL ? require('https') : require('http');
if (useSSL) {
- if (!AWS.NodeHttpClient.sslAgent) {
- // cache certificate bundle
- var bundleLocation = __dirname + '/../ca-bundle.crt';
- AWS.NodeHttpClient.certBundle = AWS.util.readFileSync(bundleLocation);
-
- // cache sslAgent
- AWS.NodeHttpClient.sslAgent = new client.Agent({
- rejectUnauthorized: true,
- cert: AWS.NodeHttpClient.certBundle
- });
- }
-
- options.agent = AWS.NodeHttpClient.sslAgent;
+ options.agent = this.sslAgent(http);
}
- var stream = this.setupEvents(client, options, request, response);
- if (request.httpRequest.body instanceof Stream) {
- request.httpRequest.body.pipe(stream, {end: false});
- } else if (request.httpRequest.body) {
- stream.write(request.httpRequest.body);
+ AWS.util.update(options, httpOptions || {});
+ delete options.timeout; // timeout isn't an HTTP option
+
+ var stream = http.request(options, callback);
+
+ // timeout support
+ stream.setTimeout(httpOptions.timeout || 0);
+ stream.on('timeout', function() {
+ var msg = 'Connection timed out after ' + httpOptions.timeout + 'ms';
+ errCallback(AWS.util.error(new Error(msg), {code: 'TimeoutError'}));
+
+ // HACK - abort the connection without tripping our error handler
+ // since we already raised our TimeoutError. Otherwise the connection
+ // comes back with ECONNRESET, which is not a helpful error message
+ stream.removeListener('error', errCallback);
+ stream.on('error', function() { });
+ stream.abort();
+ });
+
+ stream.on('error', errCallback);
+ this.writeBody(stream, httpRequest);
+ return stream;
+ },
+
+ writeBody: function writeBody(stream, httpRequest) {
+ if (httpRequest.body instanceof Stream) {
+ httpRequest.body.pipe(stream, {end: false});
+ } else if (httpRequest.body) {
+ stream.write(httpRequest.body);
}
stream.end();
},
- setupEvents: function setupEvents(client, options, request, response) {
- var stream = client.request(options, function onResponse(httpResponse) {
- request.emitEvent('httpHeaders', [httpResponse.statusCode,
- httpResponse.headers, response]);
-
- httpResponse.on('data', function onData(data) {
- request.emitEvent('httpData', [data, response]);
- });
+ sslAgent: function sslAgent(http) {
+ if (!AWS.NodeHttpClient.sslAgent) {
+ // cache certificate bundle
+ var bundleLocation = __dirname + '/../ca-bundle.crt';
+ AWS.NodeHttpClient.certBundle = AWS.util.readFileSync(bundleLocation);
- httpResponse.on('end', function onEnd() {
- request.emitEvent('httpDone', [response]);
+ // cache sslAgent
+ AWS.NodeHttpClient.sslAgent = new http.Agent({
+ rejectUnauthorized: true,
+ cert: AWS.NodeHttpClient.certBundle
});
- });
-
- stream.on('error', function (err) {
- var error = AWS.util.error(err, {code: 'NetworkingError', retryable: true});
- request.emitEvent('httpError', [error, response]);
- });
+ }
- return stream;
+ return AWS.NodeHttpClient.sslAgent;
}
});
View
105 lib/metadata_service.js
@@ -0,0 +1,105 @@
+/**
+ * Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You
+ * may not use this file except in compliance with the License. A copy of
+ * the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+ * ANY KIND, either express or implied. See the License for the specific
+ * language governing permissions and limitations under the License.
+ */
+
+var AWS = require('./core');
+require('./http');
+var inherit = AWS.util.inherit;
+
+/**
+ * Represents a metadata service available on EC2 instances. Using the
+ * {request} method, you can receieve metadata about any available resource
+ * on the metadata service.
+ */
+AWS.MetadataService = inherit({
+ /**
+ * @return [String] the hostname of the instance metadata service
+ */
+ host: '169.254.169.254',
+
+ /**
+ * @return [map] a map of options to pass to the underlying HTTP request
+ */
+ httpOptions: { timeout: 1000 },
+
+ /**
+ * Creates a new MetadataService object with a given set of options.
+ *
+ * @option options host [String] the hostname of the instance metadata
+ * service
+ * @option options httpOptions [map] a map of options to pass to the
+ * underlying HTTP request
+ */
+ constructor: function MetadataService(options) {
+ AWS.util.update(this, options);
+ },
+
+ /**
+ * Sends a request to the instance metadata service for a given resource.
+ *
+ * @param path [String] the path of the resource to get
+ * @callback callback function(err, data)
+ * Called when a response is available from the service.
+ * @param err [Error, null] if an error occurred, this value will be set
+ * @param data [String, null] if the request was successful, the body of
+ * the response
+ */
+ request: function request(path, callback) {
+ path = path || '/';
+
+ var data = '';
+ var http = AWS.HttpClient.getInstance();
+ var httpRequest = new AWS.HttpRequest('http://' + this.host + path);
+ httpRequest.method = 'GET';
+
+ http.handleRequest(httpRequest, this.httpOptions, function(httpResponse) {
+ httpResponse.on('data', function(chunk) { data += chunk.toString(); });
+ httpResponse.on('end', function() { callback(null, data); });
+ }, callback);
+ },
+
+ /**
+ * Loads a set of credentials stored in the instance metadata service
+ *
+ * @api private
+ * @callback callback function(err, credentials)
+ * Called when credentials are loaded from the resource
+ * @param err [Error] if an error occurred, this value will be set
+ * @param credentials [Object] the raw JSON object containing all
+ * metadata from the credentials resource
+ */
+ loadCredentials: function loadCredentials(callback) {
+ var self = this;
+ var basePath = '/latest/meta-data/iam/security-credentials/';
+ self.request(basePath, function (err, roleName) {
+ if (err) callback(err);
+ else {
+ roleName = roleName.split('\n')[0]; // grab first (and only) role
+ self.request(basePath + roleName, function (credErr, credData) {
+ if (credErr) callback(credErr);
+ else {
+ try {
+ var credentials = JSON.parse(credData);
+ callback(null, credentials);
+ } catch (parseError) {
+ callback(parseError);
+ }
+ }
+ });
+ }
+ });
+ }
+});
+
+module.exports = AWS.MetadataService;
View
1  lib/service_interface/json.js
@@ -25,6 +25,7 @@ AWS.ServiceInterface.Json = {
var target = api.targetPrefix + '.' + api.operations[req.operation].name;
var version = api.jsonVersion || '1.0';
+ httpRequest.path = '/';
httpRequest.body = JSON.stringify(req.params || {});
httpRequest.headers['Content-Type'] = 'application/x-amz-json-' + version;
httpRequest.headers['X-Amz-Target'] = target;
View
15 lib/service_interface/query.js
@@ -25,6 +25,7 @@ AWS.ServiceInterface.Query = {
buildRequest: function buildRequest(req) {
var operation = req.client.api.operations[req.operation];
var httpRequest = req.httpRequest;
+ httpRequest.path = '/';
httpRequest.headers['Content-Type'] =
'application/x-www-form-urlencoded; charset=utf-8';
httpRequest.params = new AWS.QueryParamList();
@@ -110,15 +111,15 @@ AWS.ServiceInterface.Query = {
AWS.QueryParamList = inherit({
constructor: function QueryParamList() {
- this.params = [];
+ this.params = {};
},
add: function add(name, value) {
- this.params.push(new AWS.QueryParam(name, value));
+ this.params[name] = new AWS.QueryParam(name, value);
},
sortedParams: function sortedParams() {
- return this.params.sort(function (p1, p2) {
+ return AWS.util.values(this.params).sort(function (p1, p2) {
return p1.name < p2.name ? -1 : 1;
});
},
@@ -129,14 +130,6 @@ AWS.QueryParamList = inherit({
params.push(param.toString());
});
return params.join('&');
- },
-
- getValue: function getValue(name) {
- for (var i = 0; i < this.params.length; i++) {
- if (this.params[i].name === name)
- return this.params[i].value;
- }
- return null;
}
});
View
4 lib/services/sqs.js
@@ -22,9 +22,9 @@ AWS.SQS = AWS.Service.defineService('./services/sqs.api', {
},
buildEndpoint: function buildEndpoint(request) {
- var url = request.httpRequest.params.getValue('QueueUrl');
+ var url = request.httpRequest.params.params.QueueUrl;
if (url) {
- request.httpRequest.endpoint = new AWS.Endpoint(url);
+ request.httpRequest.endpoint = new AWS.Endpoint(url.value);
// signature version 4 requires the region name to be set,
// sqs queue urls contain the region name
View
1  lib/signers/v2.js
@@ -34,6 +34,7 @@ AWS.Signers.V2 = inherit(AWS.Signers.RequestSigner, {
if (credentials.sessionToken)
r.params.add('SecurityToken', credentials.sessionToken);
+ delete r.params.Signature; // delete old Signature for re-signing
r.params.add('Signature', this.signature(credentials));
r.body = r.params.toString();
View
8 lib/util.js
@@ -286,6 +286,14 @@ AWS.util = {
}
},
+ values: function values(object) {
+ var list = [];
+ AWS.util.arrayEach(object, function(value) {
+ list.push(value);
+ });
+ return list;
+ },
+
update: function update(obj1, obj2) {
AWS.util.each(obj2, function iterator(key, item) {
obj1[key] = item;
View
64 test/config.spec.coffee
@@ -84,6 +84,10 @@ describe 'AWS.Config', ->
it 'can be set to false', ->
expect(configure(sslEnabled: false).sslEnabled).toEqual(false)
+ describe 'httpOptions', ->
+ it 'defaults to {}', ->
+ expect(configure().httpOptions).toEqual({})
+
describe 'set', ->
it 'should set a default value for a key', ->
config = new AWS.Config()
@@ -104,7 +108,7 @@ describe 'AWS.Config', ->
describe 'clear', ->
it 'should be able to clear all key values from a config object', ->
- config = new AWS.Config(maxRetries: 300, sslEnabled: 'foo')
+ config = new AWS.Config(credentials: {}, maxRetries: 300, sslEnabled: 'foo')
expect(config.maxRetries).toEqual(300)
expect(config.sslEnabled).toEqual('foo')
expect(config.credentials).not.toEqual(undefined)
@@ -113,7 +117,8 @@ describe 'AWS.Config', ->
expect(config.maxRetries).toEqual(undefined)
expect(config.sslEnabled).toEqual(undefined)
- expect(config.credentials).not.toEqual(undefined)
+ expect(config.credentials).not.toBe(undefined)
+ expect(config.credentialProvider).not.toEqual(undefined)
describe 'update', ->
it 'should be able to update keyed values', ->
@@ -137,6 +142,61 @@ describe 'AWS.Config', ->
expect(config.credentials.secretAccessKey).toEqual('secret')
expect(config.credentials.sessionToken).toEqual('session')
+ describe 'getCredentials', ->
+ spy = null
+ config = null
+ beforeEach ->
+ spy = jasmine.createSpy('getCredentials callback')
+
+ expectValid = (options, key) ->
+ if options instanceof AWS.Config
+ config = options
+ else
+ config = new AWS.Config(options)
+ config.getCredentials(spy)
+ expect(spy).toHaveBeenCalled()
+ expect(spy.argsForCall[0][0]).toEqual(null)
+ if key
+ expect(config.credentials.accessKeyId).toEqual(key)
+
+ expectError = (options, message) ->
+ if options instanceof AWS.Config
+ config = options
+ else
+ config = new AWS.Config(options)
+ config.getCredentials(spy)
+ expect(spy).toHaveBeenCalled()
+ expect(spy.argsForCall[0][0].code).toEqual('CredentialsError')
+ expect(spy.argsForCall[0][0].message).toEqual(message)
+
+ it 'should check credentials for static object first', ->
+ expectValid credentials: accessKeyId: '123', secretAccessKey: '456'
+
+ it 'should error if static credentials are not available', ->
+ expectError(credentials: {}, 'Missing credentials')
+
+ it 'should check credentials for async get() method', ->
+ expectValid credentials: get: (cb) -> cb()
+
+ it 'should error if credentials.get() cannot resolve', ->
+ options = credentials:
+ constructor: name: 'CustomCredentials'
+ get: (cb) -> cb(new Error('Error!'), null)
+ expectError options, 'Could not load credentials from CustomCredentials'
+
+ it 'should check credentialProvider if no credentials', ->
+ expectValid credentials: null, credentialProvider:
+ resolve: (cb) -> cb(null, accessKeyId: 'key', secretAccessKey: 'secret')
+
+ it 'should error if credentialProvider fails to resolve', ->
+ options = credentials: null, credentialProvider:
+ resolve: (cb) -> cb(new Error('Error!'), null)
+ expectError options, 'Could not load credentials from any providers'
+
+ it 'should error if no credentials or credentialProvider', ->
+ options = credentials: null, credentialProvider: null
+ expectError options, 'No credentials to load'
+
describe 'AWS.config', ->
it 'should be a default Config object', ->
expect(AWS.config.sslEnabled).toEqual(true)
View
77 test/credential_provider_chain.spec.coffee
@@ -16,23 +16,22 @@ AWS = require('../lib/core')
describe 'AWS.CredentialProviderChain', ->
describe 'resolve', ->
-
chain = null
defaultProviders = AWS.CredentialProviderChain.defaultProviders
beforeEach ->
process.env = {}
- chain = new AWS.CredentialProviderChain()
+ chain = new AWS.CredentialProviderChain [
+ -> new AWS.EnvironmentCredentials('AWS'),
+ -> new AWS.EnvironmentCredentials('AMAZON')
+ ]
- # restore the defaultProviders to the original values
afterEach ->
AWS.CredentialProviderChain.defaultProviders = defaultProviders
- it 'returns an empty credentials object by default', ->
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual(undefined)
- expect(creds.secretAccessKey).toEqual(undefined)
- expect(creds.sessionToken).toEqual(undefined)
+ it 'returns an error by default', ->
+ chain.resolve (err) ->
+ expect(err.message).toEqual('Variable AMAZON_ACCESS_KEY_ID not set.')
it 'returns AWS-prefixed credentials found in ENV', ->
@@ -40,10 +39,10 @@ describe 'AWS.CredentialProviderChain', ->
process.env['AWS_SECRET_ACCESS_KEY'] = 'secret'
process.env['AWS_SESSION_TOKEN'] = 'session'
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual('akid')
- expect(creds.secretAccessKey).toEqual('secret')
- expect(creds.sessionToken).toEqual('session')
+ chain.resolve (err, creds) ->
+ expect(creds.accessKeyId).toEqual('akid')
+ expect(creds.secretAccessKey).toEqual('secret')
+ expect(creds.sessionToken).toEqual('session')
it 'returns AMAZON-prefixed credentials found in ENV', ->
@@ -51,10 +50,10 @@ describe 'AWS.CredentialProviderChain', ->
process.env['AMAZON_SECRET_ACCESS_KEY'] = 'secret'
process.env['AMAZON_SESSION_TOKEN'] = 'session'
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual('akid')
- expect(creds.secretAccessKey).toEqual('secret')
- expect(creds.sessionToken).toEqual('session')
+ chain.resolve (err, creds) ->
+ expect(creds.accessKeyId).toEqual('akid')
+ expect(creds.secretAccessKey).toEqual('secret')
+ expect(creds.sessionToken).toEqual('session')
it 'prefers AWS credentials to AMAZON credentials', ->
@@ -66,10 +65,10 @@ describe 'AWS.CredentialProviderChain', ->
process.env['AMAZON_SECRET_ACCESS_KEY'] = 'secret2'
process.env['AMAZON_SESSION_TOKEN'] = 'session2'
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual('akid')
- expect(creds.secretAccessKey).toEqual('secret')
- expect(creds.sessionToken).toEqual('session')
+ chain.resolve (err, creds) ->
+ expect(creds.accessKeyId).toEqual('akid')
+ expect(creds.secretAccessKey).toEqual('secret')
+ expect(creds.sessionToken).toEqual('session')
it 'uses the defaultProviders property on the constructor', ->
@@ -82,32 +81,22 @@ describe 'AWS.CredentialProviderChain', ->
process.env['AWS_SESSION_TOKEN'] = 'session'
chain = new AWS.CredentialProviderChain()
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual(undefined)
- expect(creds.secretAccessKey).toEqual(undefined)
- expect(creds.sessionToken).toEqual(undefined)
+ chain.resolve (err) ->
+ expect(err.message).toEqual('No providers')
it 'calls resolve on each provider in the chain, stopping for akid', ->
-
- staticCreds = { accessKeyId: 'abc', secretAccessKey: 'xyz' }
-
- AWS.CredentialProviderChain.defaultProviders.unshift(staticCreds)
-
- chain = new AWS.CredentialProviderChain()
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual('abc')
- expect(creds.secretAccessKey).toEqual('xyz')
- expect(creds.sessionToken).toEqual(undefined)
+ staticCreds = accessKeyId: 'abc', secretAccessKey: 'xyz'
+ chain = new AWS.CredentialProviderChain([staticCreds])
+ chain.resolve (err, creds) ->
+ expect(creds.accessKeyId).toEqual('abc')
+ expect(creds.secretAccessKey).toEqual('xyz')
+ expect(creds.sessionToken).toEqual(undefined)
it 'accepts providers as functions, elavuating them during resolution', ->
-
provider = ->
- { accessKeyId: 'abc', secretAccessKey: 'xyz' }
-
- AWS.CredentialProviderChain.defaultProviders.unshift(provider)
-
- chain = new AWS.CredentialProviderChain()
- creds = chain.resolve()
- expect(creds.accessKeyId).toEqual('abc')
- expect(creds.secretAccessKey).toEqual('xyz')
- expect(creds.sessionToken).toEqual(undefined)
+ accessKeyId: 'abc', secretAccessKey: 'xyz'
+ chain = new AWS.CredentialProviderChain([provider])
+ chain.resolve (err, creds) ->
+ expect(creds.accessKeyId).toEqual('abc')
+ expect(creds.secretAccessKey).toEqual('xyz')
+ expect(creds.sessionToken).toEqual(undefined)
View
121 test/credentials.spec.coffee
@@ -19,26 +19,65 @@ validateCredentials = (creds, key, secret, session) ->
expect(creds.sessionToken).toEqual(session || 'session')
describe 'AWS.Credentials', ->
- it 'should allow setting of credentials with keys', ->
- config = new AWS.Config(
- accessKeyId: 'akid'
- secretAccessKey: 'secret'
- sessionToken: 'session'
- )
- validateCredentials(config.credentials)
-
- it 'should allow setting of credentials as object', ->
- creds =
- accessKeyId: 'akid'
- secretAccessKey: 'secret'
- sessionToken: 'session'
- validateCredentials(new AWS.Credentials(creds))
-
- it 'defaults credentials to undefined when not passed', ->
- creds = new AWS.Credentials()
- expect(creds.accessKeyId).toBe(undefined)
- expect(creds.secretAccessKey).toBe(undefined)
- expect(creds.sessionToken).toBe(undefined)
+ describe 'constructor', ->
+ it 'should allow setting of credentials with keys', ->
+ config = new AWS.Config(
+ accessKeyId: 'akid'
+ secretAccessKey: 'secret'
+ sessionToken: 'session'
+ )
+ validateCredentials(config.credentials)
+
+ it 'should allow setting of credentials as object', ->
+ creds =
+ accessKeyId: 'akid'
+ secretAccessKey: 'secret'
+ sessionToken: 'session'
+ validateCredentials(new AWS.Credentials(creds))
+
+ it 'defaults credentials to undefined when not passed', ->
+ creds = new AWS.Credentials()
+ expect(creds.accessKeyId).toBe(undefined)
+ expect(creds.secretAccessKey).toBe(undefined)
+ expect(creds.sessionToken).toBe(undefined)
+
+ describe 'needsRefresh', ->
+ it 'needs refresh if credentials are not set', ->
+ creds = new AWS.Credentials()
+ expect(creds.needsRefresh()).toEqual(true)
+ creds = new AWS.Credentials('akid')
+ expect(creds.needsRefresh()).toEqual(true)
+
+ it 'does not need refresh if credentials are set', ->
+ creds = new AWS.Credentials('akid', 'secret')
+ expect(creds.needsRefresh()).toEqual(false)
+
+ it 'needs refresh if creds are expired', ->
+ creds = new AWS.Credentials('akid', 'secret')
+ creds.expired = true
+ expect(creds.needsRefresh()).toEqual(true)
+
+ describe 'get', ->
+ it 'does not call refresh if not needsRefresh', ->
+ spy = jasmine.createSpy('done callback')
+ creds = new AWS.Credentials('akid', 'secret')
+ refresh = spyOn(creds, 'refresh')
+ creds.get(spy)
+ expect(refresh).not.toHaveBeenCalled()
+ expect(spy).toHaveBeenCalled()
+ expect(spy.argsForCall[0][0]).toEqual(null)
+ expect(creds.expired).toEqual(false)
+
+ it 'calls refresh only if needsRefresh', ->
+ spy = jasmine.createSpy('done callback')
+ creds = new AWS.Credentials('akid', 'secret')
+ creds.expired = true
+ refresh = spyOn(creds, 'refresh').andCallThrough()
+ creds.get(spy)
+ expect(refresh).toHaveBeenCalled()
+ expect(spy).toHaveBeenCalled()
+ expect(spy.argsForCall[0][0]).toEqual(null)
+ expect(creds.expired).toEqual(false)
describe 'AWS.EnvironmentCredentials', ->
beforeEach ->
@@ -62,6 +101,7 @@ describe 'AWS.EnvironmentCredentials', ->
describe 'refresh', ->
it 'can refresh credentials', ->
process.env.AWS_ACCESS_KEY_ID = 'akid'
+ process.env.AWS_SECRET_ACCESS_KEY = 'secret'
creds = new AWS.EnvironmentCredentials('AWS')
expect(creds.accessKeyId).toEqual('akid')
creds.accessKeyId = 'not_akid'
@@ -111,3 +151,44 @@ describe 'AWS.FileSystemCredentials', ->
creds.refresh()
validateCredentials(creds, 'RELOADED', 'RELOADED', 'RELOADED')
+
+ it 'fails if credentials are not in the file', ->
+ mock = '{"credentials":{}}'
+ spyOn(AWS.util, 'readFileSync').andReturn(mock)
+
+ values =
+ accessKeyId: "akid"
+ secretAccessKey: "secret"
+ sessionToken: "session"
+ creds = new AWS.FileSystemCredentials('foo', values)
+ validateCredentials(creds)
+ expect(-> creds.refresh()).toThrow('Credentials not set in foo')
+
+describe 'AWS.EC2MetadataCredentials', ->
+ describe 'constructor', ->
+ it 'allows passing of AWS.MetadataService options', ->
+ creds = new AWS.EC2MetadataCredentials(host: 'host')
+ expect(creds.metadataService.host).toEqual('host')
+
+ describe 'refresh', ->
+ it 'loads credentials from EC2 Metadata service', ->
+ creds = new AWS.EC2MetadataCredentials(host: 'host')
+ spy = spyOn(creds.metadataService, 'loadCredentials').andCallFake (cb) ->
+ cb(null, Code:"Success",AccessKeyId:"KEY",SecretAccessKey:"SECRET",Token:"TOKEN")
+ creds.refresh(->)
+ expect(creds.metadata.Code).toEqual('Success')
+ expect(creds.accessKeyId).toEqual('KEY')
+ expect(creds.secretAccessKey).toEqual('SECRET')
+ expect(creds.sessionToken).toEqual('TOKEN')
+
+ it 'does not try to load creds second time if Metadata service failed', ->
+ creds = new AWS.EC2MetadataCredentials(host: 'host')
+ spy = spyOn(creds.metadataService, 'loadCredentials').andCallFake (cb) ->
+ cb(new Error('INVALID SERVICE'))
+
+ creds.refresh (err) ->
+ expect(err.message).toEqual('INVALID SERVICE')
+ creds.refresh ->
+ creds.refresh ->
+ creds.refresh ->
+ expect(spy.calls.length).toEqual(1)
View
10 test/endpoint.spec.coffee
@@ -14,6 +14,16 @@
AWS = require('../lib/core')
describe 'AWS.Endpoint', ->
+ it 'throws error if parameter is null/undefined', ->
+ expect(-> new AWS.Endpoint(null)).toThrow('Invalid endpoint: null')
+ expect(-> new AWS.Endpoint(undefined)).toThrow('Invalid endpoint: undefined')
+
+ it 'copy constructs Endpoint', ->
+ origEndpoint = new AWS.Endpoint('http://domain.com')
+ endpoint = new AWS.Endpoint(origEndpoint)
+ expect(endpoint).not.toBe(origEndpoint)
+ expect(endpoint.host).toEqual('domain.com')
+
it 'retains the entire endpoint as the endpoint href', ->
href = 'http://domain.com/'
endpoint = new AWS.Endpoint(href)
View
13 test/event_listeners.spec.coffee
@@ -67,10 +67,11 @@ describe 'AWS.EventListeners', ->
expect(response.error).toEqual("ERROR")
it 'sends error event if credentials are not set', ->
- errorHandler = createSpy()
+ errorHandler = createSpy('errorHandler')
request = makeRequest()
request.on('error', errorHandler)
+ client.config.credentialProvider = null
client.config.credentials.accessKeyId = null
request.send()
@@ -159,6 +160,16 @@ describe 'AWS.EventListeners', ->
response = request.send()
expect(response.error).toEqual('mockservice')
+ describe 'send', ->
+ it 'passes httpOptions from config', ->
+ options = {}
+ spyOn(AWS.HttpClient, 'getInstance').andReturn handleRequest: (req, opts) ->
+ options = opts
+ client.config.httpOptions = timeout: 15
+ client.config.maxRetries = 0
+ makeRequest(->)
+ expect(options.timeout).toEqual(15)
+
describe 'httpData', ->
beforeEach ->
helpers.mockHttpResponse 200, {}, ['FOO', 'BAR', 'BAZ', 'QUX']
View
45 test/helpers.coffee
@@ -12,6 +12,7 @@
# language governing permissions and limitations under the License.
AWS = require('../lib/aws')
+EventEmitter = require('events').EventEmitter
# Mock credentials
AWS.config.update
@@ -29,7 +30,7 @@ AWS.EventListeners.Core.removeListener 'validate',
# TODO: refactor this out.
`setTimeout = function(fn, delay) { fn(); }`
-AWS.HttpClient.getInstance = -> throw new Error('Unmocked HTTP request')
+#AWS.HttpClient.getInstance = -> throw new Error('Unmocked HTTP request')
flattenXML = (xml) ->
if (!xml)
@@ -62,29 +63,41 @@ MockService = AWS.util.inherit AWS.Service,
MockService.Client = MockClient
+mockHttpSuccessfulResponse = (status, headers, data, cb) ->
+ httpResp = new EventEmitter()
+ httpResp.statusCode = status
+ httpResp.headers = headers
+
+ cb(httpResp)
+
+ if !Array.isArray(data)
+ data = [data]
+ AWS.util.arrayEach data, (str) ->
+ httpResp.emit('data', new Buffer(str))
+
+ httpResp.emit('end')
+
mockHttpResponse = (status, headers, data) ->
+ stream = new EventEmitter()
spyOn(AWS.HttpClient, 'getInstance')
- AWS.HttpClient.getInstance.andReturn handleRequest: (req, resp) ->
+ AWS.HttpClient.getInstance.andReturn handleRequest: (req, opts, cb, errCb) ->
if typeof status == 'number'
- req.emit('httpHeaders', [status, headers, resp])
- str = str instanceof Array ? str : [str]
- AWS.util.arrayEach data, (str) ->
- req.emit('httpData', [new Buffer(str), resp])
- req.emit('httpDone', [resp])
+ mockHttpSuccessfulResponse status, headers, data, cb
else
- req.emit('httpError', [status, resp])
+ errCb(status)
+
+ return stream
mockIntermittentFailureResponse = (numFailures, status, headers, data) ->
+ retryCount = 0
spyOn(AWS.HttpClient, 'getInstance')
- AWS.HttpClient.getInstance.andReturn handleRequest: (req, resp) ->
- if resp.retryCount < numFailures
- req.emit('httpError', [{code: 'NetworkingError', message: 'FAIL!'}, resp])
+ AWS.HttpClient.getInstance.andReturn handleRequest: (req, opts, cb, errCb) ->
+ if retryCount < numFailures
+ retryCount += 1
+ errCb code: 'NetworkingError', message: 'FAIL!'
else
- req.emit('httpHeaders', [(resp.retryCount < numFailures ? 500 : status), headers, resp])
- str = str instanceof Array ? str : [str]
- AWS.util.arrayEach data, (str) ->
- req.emit('httpData', [new Buffer(str), resp])
- req.emit('httpDone', [resp])
+ statusCode = retryCount < numFailures ? 500 : status
+ mockHttpSuccessfulResponse statusCode, headers, data, cb
module.exports =
AWS: AWS
View
9 test/http_request.spec.coffee
@@ -17,7 +17,7 @@ describe 'AWS.HttpRequest', ->
request = null
beforeEach ->
- request = new AWS.HttpRequest()
+ request = new AWS.HttpRequest('http://domain.com')
describe 'constructor', ->
@@ -33,12 +33,13 @@ describe 'AWS.HttpRequest', ->
it 'defaults body to empty string', ->
expect(request.body).toEqual('')
- it 'defaults endpoint to undefined', ->
- expect(request.endpoint).toEqual(undefined)
-
it 'defaults endpointPrefix to undefined', ->
expect(request.endpointPrefix).toEqual(undefined)
+ it 'uses the path from the endpoint if provided', ->
+ request = new AWS.HttpRequest('http://domain.com/path')
+ expect(request.path).toEqual('/path')
+
describe 'pathname', ->
it 'defaults to /', ->
View
66 test/metadata_service.spec.coffee
@@ -0,0 +1,66 @@
+# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+
+helpers = require('./helpers')
+url = require('url')
+http = require('http')
+AWS = helpers.AWS
+
+describe 'AWS.MetadataService', ->
+ describe 'loadCredentials', ->
+ [server, port, service] = [null, 1024 + parseInt(Math.random() * 100), null]
+
+ beforeEach ->
+ service = new AWS.MetadataService(host: '127.0.0.1:' + port)
+ server = http.createServer (req, res) ->
+ re = new RegExp('^/latest/meta-data/iam/security-credentials/(.*)$')
+ match = url.parse(req.url).pathname.match(re)
+ if match
+ res.writeHead(200, 'Content-Type': 'text/plain')
+ if match[1] == ''
+ res.write('TestingRole\n')
+ res.write('TestingRole2\n')
+ else
+ data = '{"Code":"Success","AccessKeyId":"KEY","SecretAccessKey":"SECRET","Token":"TOKEN"}'
+ res.write(data)
+ else
+ res.writeHead(404, {})
+ res.end()
+
+ server.listen(port)
+
+ afterEach -> server.close() if server
+
+ it 'should load credentials from metadata service', ->
+ [err, data] = [null, null]
+ runs ->
+ service.loadCredentials (e, d) -> [err, data] = [e, d]
+ waitsFor -> err || data
+ runs ->
+ expect(err).toBe(null)
+ expect(data.Code).toEqual('Success')
+ expect(data.AccessKeyId).toEqual('KEY')
+ expect(data.SecretAccessKey).toEqual('SECRET')
+ expect(data.Token).toEqual('TOKEN')
+
+ it 'should fail if server is not up', ->
+ server.close(); server = null
+ service = new AWS.MetadataService(host: '255.255.255.255')
+ service.httpOptions.timeout = 10
+ [err, data] = [null, null]
+ runs ->
+ service.loadCredentials (e, d) -> [err, data] = [e, d]
+ waitsFor -> err || data
+ runs ->
+ expect(err instanceof Error).toBe(true)
+ expect(data).toEqual(null)
View
37 test/node_http_client.spec.coffee
@@ -13,21 +13,38 @@
helpers = require('./helpers')
AWS = helpers.AWS
-MockClient = helpers.MockClient
describe 'AWS.NodeHttpClient', ->
http = new AWS.NodeHttpClient()
describe 'handleRequest', ->
- it 'emits httpError in error event', ->
+ it 'loads certificate bundle from disk in SSL request (once)', ->
+ readSpy = spyOn(AWS.util, 'readFileSync').andCallThrough()
done = false
- endpoint = new AWS.Endpoint('http://invalid')
- req = new AWS.Request(endpoint: endpoint, config: region: 'empty')
- resp = new AWS.Response(req)
- req.on 'httpError', (cbErr, cbResp) ->
- expect(cbErr instanceof Error).toBeTruthy()
- expect(cbResp).toBe(resp)
+ req = new AWS.HttpRequest 'https://invalid'
+ runs -> http.handleRequest req, {}, null, ->
done = true
-
- runs -> http.handleRequest(req, resp)
+ expect(AWS.NodeHttpClient.sslAgent).not.toEqual(null)
+ expect(readSpy.callCount).toEqual(1)
waitsFor -> done
+
+ it 'emits error event', ->
+ error = null
+ req = new AWS.HttpRequest 'http://invalid'
+ runs ->
+ http.handleRequest req, {}, null, (err) ->
+ error = err
+ waitsFor -> error
+ runs ->
+ expect(error.code).toEqual 'ENOTFOUND'
+
+ it 'supports timeout in httpOptions', ->
+ error = null
+ req = new AWS.HttpRequest 'http://1.1.1.1'
+ runs ->
+ http.handleRequest req, {timeout: 12}, null, (err) ->
+ error = err
+ waitsFor -> error
+ runs ->
+ expect(error.code).toEqual 'TimeoutError'
+ expect(error.message).toEqual 'Connection timed out after 12ms'
View
33 test/request.spec.coffee
@@ -12,6 +12,7 @@
# language governing permissions and limitations under the License.
helpers = require('./helpers')
+EventEmitter = require('events').EventEmitter
AWS = helpers.AWS
MockClient = helpers.MockClient
@@ -65,11 +66,14 @@ describe 'AWS.Request', ->
it 'streams partial data and raises an error', ->
data = ''; error = null; reqError = null; done = false
spyOn(AWS.HttpClient, 'getInstance')
- AWS.HttpClient.getInstance.andReturn handleRequest: (req, resp) ->
- req.emit('httpHeaders', [200, {}, resp])
+ AWS.HttpClient.getInstance.andReturn handleRequest: (req, opts, cb, errCb) ->
+ req = new EventEmitter()
+ req.statusCode = 200
+ req.headers = {}
+ cb(req)
AWS.util.arrayEach ['FOO', 'BAR', 'BAZ'], (str) ->
- req.emit('httpData', [new Buffer(str), resp])
- req.emit('httpError', [new Error('fail'), resp])
+ req.emit 'data', new Buffer(str)
+ errCb new Error('fail')
runs ->
request = client.makeRequest('mockMethod')
@@ -86,21 +90,24 @@ describe 'AWS.Request', ->
it 'fails if retry occurs in the middle of a failing stream', ->
data = ''; error = null; reqError = null; resp = null
+ retryCount = 0
spyOn(AWS.HttpClient, 'getInstance')
- AWS.HttpClient.getInstance.andReturn handleRequest: (req, resp) ->
+ AWS.HttpClient.getInstance.andReturn handleRequest: (req, opts, cb, errCb) ->
+ req = new EventEmitter()
+ req.statusCode = 200
+ req.headers = {}
process.nextTick ->
- req.emit('httpHeaders', [200, {}, resp])
+ cb(req)
AWS.util.arrayEach ['FOO', 'BAR', 'BAZ', 'QUX'], (str) ->
- if str == 'BAZ' and resp.retryCount < 1
+ if str == 'BAZ' and retryCount < 1
process.nextTick ->
- req.emit('httpError', [{code: 'NetworkingError', message: 'FAIL!', retryable: true}, resp])
+ retryCount += 1
+ errCb code: 'NetworkingError', message: 'FAIL!', retryable: true
return AWS.util.abort
else
- process.nextTick ->
- req.emit('httpData', [new Buffer(str), resp])
- if resp.retryCount >= 1
- process.nextTick ->
- req.emit('httpDone', [resp])
+ process.nextTick -> req.emit 'data', new Buffer(str)
+ if retryCount >= 1
+ process.nextTick -> req.emit('end')
runs ->
request = client.makeRequest('mockMethod')
View
2  test/signers/s3.spec.coffee
@@ -40,7 +40,7 @@ describe 'AWS.Signers.S3', ->
sessionToken = null
buildRequest = () ->
- req = new AWS.HttpRequest()
+ req = new AWS.HttpRequest('https://s3.amazonaws.com')
req.method = method
req.path = path
req.headers = headers
View
4 test/util.spec.coffee
@@ -257,6 +257,10 @@ describe 'AWS.util.arrayEach', ->
expect(total).toEqual(1)
+describe 'AWS.util.values', ->
+ it 'returns values of a key-value map', ->
+ expect(AWS.util.values(a: 1, b: 2, c: 3)).toEqual([1, 2, 3])
+
describe 'AWS.util.copy', ->
it 'does not copy null or undefined', ->
expect(AWS.util.copy(null)).toEqual(null)
Please sign in to comment.
Something went wrong with that request. Please try again.