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