elastic-apm-node
Version:
The official Elastic APM agent for Node.js
176 lines (158 loc) • 4.47 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
;
const URL = require('url').URL;
const { httpRequest } = require('../http-request');
const DEFAULT_BASE_URL = new URL('/', 'http://169.254.169.254:80');
function sanitizeHttpHeaderValue(value) {
// no newlines, carriage returns, or ascii characters outside of 32 (\x20) - 127 (\x7F)
const newValue = value.replace(/[\r\n]/g, '').replace(/[^\x20-\x7F]/g, '');
return newValue;
}
/**
* Logic for making request to /latest/dynamic/instance-identity/document
*
* The headers parameter allow us to, if needed, set the IMDSv2 token
*/
function getMetadataAwsWithHeaders(
baseUrl,
headers,
connectTimeoutMs,
resTimeoutMs,
logger,
cb,
) {
const options = {
method: 'GET',
timeout: resTimeoutMs,
connectTimeout: connectTimeoutMs,
};
if (headers) {
options.headers = headers;
}
const url = baseUrl + 'latest/dynamic/instance-identity/document';
const req = httpRequest(url, options, function (res) {
const finalData = [];
res.on('data', function (data) {
finalData.push(data);
});
res.on('end', function (data) {
let result;
try {
result = formatMetadataStringIntoObject(finalData.join(''));
} catch (err) {
logger.trace(
'aws metadata server responded, but there was an ' +
'error parsing the result: %o',
err,
);
cb(err);
return;
}
cb(null, result);
});
});
req.on('timeout', function () {
req.destroy(new Error('request to metadata server timed out'));
});
req.on('connectTimeout', function () {
req.destroy(new Error('could not ping metadata server'));
});
req.on('error', function (err) {
cb(err);
});
req.end();
}
/**
* Fetches metadata from either an IMDSv2 or IMDSv1 endpoint
*
* Attempts to fetch a token for an IMDSv2 service call. If this call
* is a success, then we call the instance endpoint with the token. If
* this call fails, then we call the instance endpoint without the token.
*
* A "success" to the token endpoint means an HTTP status code of 200
* and a non-zero-length return value for the token.
*
* https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
*/
function getMetadataAws(
connectTimeoutMs,
resTimeoutMs,
logger,
baseUrlOverride,
cb,
) {
const baseUrl = baseUrlOverride || DEFAULT_BASE_URL;
const url = baseUrl + 'latest/api/token';
const options = {
method: 'PUT',
headers: {
'X-aws-ec2-metadata-token-ttl-seconds': 300,
},
timeout: resTimeoutMs,
connectTimeout: connectTimeoutMs,
};
const req = httpRequest(url, options, function (res) {
const finalData = [];
res.on('data', function (data) {
finalData.push(data);
});
res.on('end', function () {
const token = sanitizeHttpHeaderValue(finalData.join(''));
const headers = {};
if (token && res.statusCode === 200) {
// uses return value from call to latest/api/token as token,
// and takes extra steps to ensure characters are valid
headers['X-aws-ec2-metadata-token'] = token;
}
getMetadataAwsWithHeaders(
baseUrl,
headers,
connectTimeoutMs,
resTimeoutMs,
logger,
cb,
);
});
});
req.on('timeout', function () {
req.destroy(new Error('request for metadata token timed out'));
});
req.on('connectTimeout', function () {
req.destroy(
new Error('socket connection to metadata token server timed out'),
);
});
req.on('error', function (err) {
cb(err);
});
req.end();
}
/**
* Builds metadata object
*
* Takes the response from a /latest/dynamic/instance-identity/document
* service request and formats it into the cloud metadata object
*/
function formatMetadataStringIntoObject(string) {
const data = JSON.parse(string);
const metadata = {
account: {
id: String(data.accountId),
},
instance: {
id: String(data.instanceId),
},
availability_zone: String(data.availabilityZone),
machine: {
type: String(data.instanceType),
},
provider: 'aws',
region: String(data.region),
};
return metadata;
}
module.exports = { getMetadataAws };