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) — 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 getting credentials * * @api private * @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;