metadata_service.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. var AWS = require('./core');
  2. require('./http');
  3. var inherit = AWS.util.inherit;
  4. var getMetadataServiceEndpoint = require('./metadata_service/get_metadata_service_endpoint');
  5. var URL = require('url').URL;
  6. /**
  7. * Represents a metadata service available on EC2 instances. Using the
  8. * {request} method, you can receieve metadata about any available resource
  9. * on the metadata service.
  10. *
  11. * You can disable the use of the IMDS by setting the AWS_EC2_METADATA_DISABLED
  12. * environment variable to a truthy value.
  13. *
  14. * @!attribute [r] httpOptions
  15. * @return [map] a map of options to pass to the underlying HTTP request:
  16. *
  17. * * **timeout** (Number) — a timeout value in milliseconds to wait
  18. * before aborting the connection. Set to 0 for no timeout.
  19. *
  20. * @!macro nobrowser
  21. */
  22. AWS.MetadataService = inherit({
  23. /**
  24. * @return [String] the endpoint of the instance metadata service
  25. */
  26. endpoint: getMetadataServiceEndpoint(),
  27. /**
  28. * @!ignore
  29. */
  30. /**
  31. * Default HTTP options. By default, the metadata service is set to not
  32. * timeout on long requests. This means that on non-EC2 machines, this
  33. * request will never return. If you are calling this operation from an
  34. * environment that may not always run on EC2, set a `timeout` value so
  35. * the SDK will abort the request after a given number of milliseconds.
  36. */
  37. httpOptions: { timeout: 0 },
  38. /**
  39. * when enabled, metadata service will not fetch token
  40. */
  41. disableFetchToken: false,
  42. /**
  43. * Creates a new MetadataService object with a given set of options.
  44. *
  45. * @option options host [String] the hostname of the instance metadata
  46. * service
  47. * @option options httpOptions [map] a map of options to pass to the
  48. * underlying HTTP request:
  49. *
  50. * * **timeout** (Number) — a timeout value in milliseconds to wait
  51. * before aborting the connection. Set to 0 for no timeout.
  52. * @option options maxRetries [Integer] the maximum number of retries to
  53. * perform for timeout errors
  54. * @option options retryDelayOptions [map] A set of options to configure the
  55. * retry delay on retryable errors. See AWS.Config for details.
  56. * @option options ec2MetadataV1Disabled [boolean] Whether to block IMDS v1 fallback.
  57. * @option options profile [string] A profile to check for IMDSv1 fallback settings.
  58. * @option options filename [string] Optional filename for the config file.
  59. */
  60. constructor: function MetadataService(options) {
  61. if (options && options.host) {
  62. options.endpoint = 'http://' + options.host;
  63. delete options.host;
  64. }
  65. this.profile = options && options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile;
  66. this.ec2MetadataV1Disabled = !!(options && options.ec2MetadataV1Disabled);
  67. this.filename = options && options.filename;
  68. AWS.util.update(this, options);
  69. },
  70. /**
  71. * Sends a request to the instance metadata service for a given resource.
  72. *
  73. * @param path [String] the path of the resource to get
  74. *
  75. * @param options [map] an optional map used to make request
  76. *
  77. * * **method** (String) — HTTP request method
  78. *
  79. * * **headers** (map<String,String>) &mdash; a map of response header keys and their respective values
  80. *
  81. * @callback callback function(err, data)
  82. * Called when a response is available from the service.
  83. * @param err [Error, null] if an error occurred, this value will be set
  84. * @param data [String, null] if the request was successful, the body of
  85. * the response
  86. */
  87. request: function request(path, options, callback) {
  88. if (arguments.length === 2) {
  89. callback = options;
  90. options = {};
  91. }
  92. if (process.env[AWS.util.imdsDisabledEnv]) {
  93. callback(new Error('EC2 Instance Metadata Service access disabled'));
  94. return;
  95. }
  96. path = path || '/';
  97. // Verify that host is a valid URL
  98. if (URL) { new URL(this.endpoint); }
  99. var httpRequest = new AWS.HttpRequest(this.endpoint + path);
  100. httpRequest.method = options.method || 'GET';
  101. if (options.headers) {
  102. httpRequest.headers = options.headers;
  103. }
  104. AWS.util.handleRequestWithRetries(httpRequest, this, callback);
  105. },
  106. /**
  107. * @api private
  108. */
  109. loadCredentialsCallbacks: [],
  110. /**
  111. * Fetches metadata token used for getting credentials
  112. *
  113. * @api private
  114. * @callback callback function(err, token)
  115. * Called when token is loaded from the resource
  116. */
  117. fetchMetadataToken: function fetchMetadataToken(callback) {
  118. var self = this;
  119. var tokenFetchPath = '/latest/api/token';
  120. self.request(
  121. tokenFetchPath,
  122. {
  123. 'method': 'PUT',
  124. 'headers': {
  125. 'x-aws-ec2-metadata-token-ttl-seconds': '21600'
  126. }
  127. },
  128. callback
  129. );
  130. },
  131. /**
  132. * Fetches credentials
  133. *
  134. * @api private
  135. * @callback cb function(err, creds)
  136. * Called when credentials are loaded from the resource
  137. */
  138. fetchCredentials: function fetchCredentials(options, cb) {
  139. var self = this;
  140. var basePath = '/latest/meta-data/iam/security-credentials/';
  141. var isImdsV1Fallback = self.disableFetchToken
  142. || !(options && options.headers && options.headers['x-aws-ec2-metadata-token']);
  143. if (isImdsV1Fallback && !(process.env.AWS_EC2_METADATA_DISABLED)) {
  144. try {
  145. var profiles = AWS.util.getProfilesFromSharedConfig(AWS.util.iniLoader, this.filename);
  146. var profileSettings = profiles[this.profile] || {};
  147. } catch (e) {
  148. profileSettings = {};
  149. }
  150. if (profileSettings.ec2_metadata_v1_disabled && profileSettings.ec2_metadata_v1_disabled !== 'false') {
  151. return cb(AWS.util.error(
  152. new Error('AWS EC2 Metadata v1 fallback has been blocked by AWS config file profile.')
  153. ));
  154. }
  155. if (self.ec2MetadataV1Disabled) {
  156. return cb(AWS.util.error(
  157. new Error('AWS EC2 Metadata v1 fallback has been blocked by AWS.MetadataService::options.ec2MetadataV1Disabled=true.')
  158. ));
  159. }
  160. if (process.env.AWS_EC2_METADATA_V1_DISABLED && process.env.AWS_EC2_METADATA_V1_DISABLED !== 'false') {
  161. return cb(AWS.util.error(
  162. new Error('AWS EC2 Metadata v1 fallback has been blocked by process.env.AWS_EC2_METADATA_V1_DISABLED.')
  163. ));
  164. }
  165. }
  166. self.request(basePath, options, function (err, roleName) {
  167. if (err) {
  168. self.disableFetchToken = !(err.statusCode === 401);
  169. cb(AWS.util.error(
  170. err,
  171. {
  172. message: 'EC2 Metadata roleName request returned error'
  173. }
  174. ));
  175. return;
  176. }
  177. roleName = roleName.split('\n')[0]; // grab first (and only) role
  178. self.request(basePath + roleName, options, function (credErr, credData) {
  179. if (credErr) {
  180. self.disableFetchToken = !(credErr.statusCode === 401);
  181. cb(AWS.util.error(
  182. credErr,
  183. {
  184. message: 'EC2 Metadata creds request returned error'
  185. }
  186. ));
  187. return;
  188. }
  189. try {
  190. var credentials = JSON.parse(credData);
  191. cb(null, credentials);
  192. } catch (parseError) {
  193. cb(parseError);
  194. }
  195. });
  196. });
  197. },
  198. /**
  199. * Loads a set of credentials stored in the instance metadata service
  200. *
  201. * @api private
  202. * @callback callback function(err, credentials)
  203. * Called when credentials are loaded from the resource
  204. * @param err [Error] if an error occurred, this value will be set
  205. * @param credentials [Object] the raw JSON object containing all
  206. * metadata from the credentials resource
  207. */
  208. loadCredentials: function loadCredentials(callback) {
  209. var self = this;
  210. self.loadCredentialsCallbacks.push(callback);
  211. if (self.loadCredentialsCallbacks.length > 1) { return; }
  212. function callbacks(err, creds) {
  213. var cb;
  214. while ((cb = self.loadCredentialsCallbacks.shift()) !== undefined) {
  215. cb(err, creds);
  216. }
  217. }
  218. if (self.disableFetchToken) {
  219. self.fetchCredentials({}, callbacks);
  220. } else {
  221. self.fetchMetadataToken(function(tokenError, token) {
  222. if (tokenError) {
  223. if (tokenError.code === 'TimeoutError') {
  224. self.disableFetchToken = true;
  225. } else if (tokenError.retryable === true) {
  226. callbacks(AWS.util.error(
  227. tokenError,
  228. {
  229. message: 'EC2 Metadata token request returned error'
  230. }
  231. ));
  232. return;
  233. } else if (tokenError.statusCode === 400) {
  234. callbacks(AWS.util.error(
  235. tokenError,
  236. {
  237. message: 'EC2 Metadata token request returned 400'
  238. }
  239. ));
  240. return;
  241. }
  242. }
  243. var options = {};
  244. if (token) {
  245. options.headers = {
  246. 'x-aws-ec2-metadata-token': token
  247. };
  248. }
  249. self.fetchCredentials(options, callbacks);
  250. });
  251. }
  252. }
  253. });
  254. /**
  255. * @api private
  256. */
  257. module.exports = AWS.MetadataService;