280 lines
8.9 KiB
JavaScript
280 lines
8.9 KiB
JavaScript
var AWS = require('./core');
|
|
require('./http');
|
|
var inherit = AWS.util.inherit;
|
|
var getMetadataServiceEndpoint = require('./metadata_service/get_metadata_service_endpoint');
|
|
var URL = require('url').URL;
|
|
|
|
/**
|
|
* Represents a metadata service available on EC2 instances. Using the
|
|
* {request} method, you can receieve metadata about any available resource
|
|
* on the metadata service.
|
|
*
|
|
* You can disable the use of the IMDS by setting the AWS_EC2_METADATA_DISABLED
|
|
* environment variable to a truthy value.
|
|
*
|
|
* @!attribute [r] httpOptions
|
|
* @return [map] a map of options to pass to the underlying HTTP request:
|
|
*
|
|
* * **timeout** (Number) — a timeout value in milliseconds to wait
|
|
* before aborting the connection. Set to 0 for no timeout.
|
|
*
|
|
* @!macro nobrowser
|
|
*/
|
|
AWS.MetadataService = inherit({
|
|
/**
|
|
* @return [String] the endpoint of the instance metadata service
|
|
*/
|
|
endpoint: getMetadataServiceEndpoint(),
|
|
|
|
/**
|
|
* @!ignore
|
|
*/
|
|
|
|
/**
|
|
* Default HTTP options. By default, the metadata service is set to not
|
|
* timeout on long requests. This means that on non-EC2 machines, this
|
|
* request will never return. If you are calling this operation from an
|
|
* environment that may not always run on EC2, set a `timeout` value so
|
|
* the SDK will abort the request after a given number of milliseconds.
|
|
*/
|
|
httpOptions: { timeout: 0 },
|
|
|
|
/**
|
|
* when enabled, metadata service will not fetch token
|
|
*/
|
|
disableFetchToken: false,
|
|
|
|
/**
|
|
* 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:
|
|
*
|
|
* * **timeout** (Number) — a timeout value in milliseconds to wait
|
|
* before aborting the connection. Set to 0 for no timeout.
|
|
* @option options maxRetries [Integer] the maximum number of retries to
|
|
* perform for timeout errors
|
|
* @option options retryDelayOptions [map] A set of options to configure the
|
|
* retry delay on retryable errors. See AWS.Config for details.
|
|
* @option options ec2MetadataV1Disabled [boolean] Whether to block IMDS v1 fallback.
|
|
* @option options profile [string] A profile to check for IMDSv1 fallback settings.
|
|
* @option options filename [string] Optional filename for the config file.
|
|
*/
|
|
constructor: function MetadataService(options) {
|
|
if (options && options.host) {
|
|
options.endpoint = 'http://' + options.host;
|
|
delete options.host;
|
|
}
|
|
this.profile = options && options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
|
|
this.ec2MetadataV1Disabled = !!(options && options.ec2MetadataV1Disabled);
|
|
this.filename = options && options.filename;
|
|
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
|
|
*
|
|
* @param options [map] an optional map used to make request
|
|
*
|
|
* * **method** (String) — HTTP request method
|
|
*
|
|
* * **headers** (map<String,String>) — a map of response header keys and their respective values
|
|
*
|
|
* @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, options, callback) {
|
|
if (arguments.length === 2) {
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
|
|
if (process.env[AWS.util.imdsDisabledEnv]) {
|
|
callback(new Error('EC2 Instance Metadata Service access disabled'));
|
|
return;
|
|
}
|
|
|
|
path = path || '/';
|
|
|
|
// Verify that host is a valid URL
|
|
if (URL) { new URL(this.endpoint); }
|
|
|
|
var httpRequest = new AWS.HttpRequest(this.endpoint + path);
|
|
httpRequest.method = options.method || 'GET';
|
|
if (options.headers) {
|
|
httpRequest.headers = options.headers;
|
|
}
|
|
AWS.util.handleRequestWithRetries(httpRequest, this, callback);
|
|
},
|
|
|
|
/**
|
|
* @api private
|
|
*/
|
|
loadCredentialsCallbacks: [],
|
|
|
|
/**
|
|
* Fetches metadata token used for authenticating against the instance metadata service.
|
|
*
|
|
* @callback callback function(err, token)
|
|
* Called when token is loaded from the resource
|
|
*/
|
|
fetchMetadataToken: function fetchMetadataToken(callback) {
|
|
var self = this;
|
|
var tokenFetchPath = '/latest/api/token';
|
|
self.request(
|
|
tokenFetchPath,
|
|
{
|
|
'method': 'PUT',
|
|
'headers': {
|
|
'x-aws-ec2-metadata-token-ttl-seconds': '21600'
|
|
}
|
|
},
|
|
callback
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Fetches credentials
|
|
*
|
|
* @api private
|
|
* @callback cb function(err, creds)
|
|
* Called when credentials are loaded from the resource
|
|
*/
|
|
fetchCredentials: function fetchCredentials(options, cb) {
|
|
var self = this;
|
|
var basePath = '/latest/meta-data/iam/security-credentials/';
|
|
|
|
var isImdsV1Fallback = self.disableFetchToken
|
|
|| !(options && options.headers && options.headers['x-aws-ec2-metadata-token']);
|
|
|
|
if (isImdsV1Fallback && !(process.env.AWS_EC2_METADATA_DISABLED)) {
|
|
try {
|
|
var profiles = AWS.util.getProfilesFromSharedConfig(AWS.util.iniLoader, this.filename);
|
|
var profileSettings = profiles[this.profile] || {};
|
|
} catch (e) {
|
|
profileSettings = {};
|
|
}
|
|
|
|
if (profileSettings.ec2_metadata_v1_disabled && profileSettings.ec2_metadata_v1_disabled !== 'false') {
|
|
return cb(AWS.util.error(
|
|
new Error('AWS EC2 Metadata v1 fallback has been blocked by AWS config file profile.')
|
|
));
|
|
}
|
|
|
|
if (self.ec2MetadataV1Disabled) {
|
|
return cb(AWS.util.error(
|
|
new Error('AWS EC2 Metadata v1 fallback has been blocked by AWS.MetadataService::options.ec2MetadataV1Disabled=true.')
|
|
));
|
|
}
|
|
|
|
if (process.env.AWS_EC2_METADATA_V1_DISABLED && process.env.AWS_EC2_METADATA_V1_DISABLED !== 'false') {
|
|
return cb(AWS.util.error(
|
|
new Error('AWS EC2 Metadata v1 fallback has been blocked by process.env.AWS_EC2_METADATA_V1_DISABLED.')
|
|
));
|
|
}
|
|
}
|
|
|
|
self.request(basePath, options, function (err, roleName) {
|
|
if (err) {
|
|
self.disableFetchToken = !(err.statusCode === 401);
|
|
cb(AWS.util.error(
|
|
err,
|
|
{
|
|
message: 'EC2 Metadata roleName request returned error'
|
|
}
|
|
));
|
|
return;
|
|
}
|
|
roleName = roleName.split('\n')[0]; // grab first (and only) role
|
|
self.request(basePath + roleName, options, function (credErr, credData) {
|
|
if (credErr) {
|
|
self.disableFetchToken = !(credErr.statusCode === 401);
|
|
cb(AWS.util.error(
|
|
credErr,
|
|
{
|
|
message: 'EC2 Metadata creds request returned error'
|
|
}
|
|
));
|
|
return;
|
|
}
|
|
try {
|
|
var credentials = JSON.parse(credData);
|
|
cb(null, credentials);
|
|
} catch (parseError) {
|
|
cb(parseError);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
self.loadCredentialsCallbacks.push(callback);
|
|
if (self.loadCredentialsCallbacks.length > 1) { return; }
|
|
|
|
function callbacks(err, creds) {
|
|
var cb;
|
|
while ((cb = self.loadCredentialsCallbacks.shift()) !== undefined) {
|
|
cb(err, creds);
|
|
}
|
|
}
|
|
|
|
if (self.disableFetchToken) {
|
|
self.fetchCredentials({}, callbacks);
|
|
} else {
|
|
self.fetchMetadataToken(function(tokenError, token) {
|
|
if (tokenError) {
|
|
if (tokenError.code === 'TimeoutError') {
|
|
self.disableFetchToken = true;
|
|
} else if (tokenError.retryable === true) {
|
|
callbacks(AWS.util.error(
|
|
tokenError,
|
|
{
|
|
message: 'EC2 Metadata token request returned error'
|
|
}
|
|
));
|
|
return;
|
|
} else if (tokenError.statusCode === 400) {
|
|
callbacks(AWS.util.error(
|
|
tokenError,
|
|
{
|
|
message: 'EC2 Metadata token request returned 400'
|
|
}
|
|
));
|
|
return;
|
|
}
|
|
}
|
|
var options = {};
|
|
if (token) {
|
|
options.headers = {
|
|
'x-aws-ec2-metadata-token': token
|
|
};
|
|
}
|
|
self.fetchCredentials(options, callbacks);
|
|
});
|
|
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @api private
|
|
*/
|
|
module.exports = AWS.MetadataService;
|