'use strict'; var propertyProvider = require('@smithy/property-provider'); var url = require('url'); var buffer = require('buffer'); var http = require('http'); var nodeConfigProvider = require('@smithy/node-config-provider'); var urlParser = require('@smithy/url-parser'); function httpRequest(options) { return new Promise((resolve, reject) => { const req = http.request({ method: "GET", ...options, hostname: options.hostname?.replace(/^\[(.+)\]$/, "$1"), }); req.on("error", (err) => { reject(Object.assign(new propertyProvider.ProviderError("Unable to connect to instance metadata service"), err)); req.destroy(); }); req.on("timeout", () => { reject(new propertyProvider.ProviderError("TimeoutError from instance metadata service")); req.destroy(); }); req.on("response", (res) => { const { statusCode = 400 } = res; if (statusCode < 200 || 300 <= statusCode) { reject(Object.assign(new propertyProvider.ProviderError("Error response received from instance metadata service"), { statusCode })); req.destroy(); } const chunks = []; res.on("data", (chunk) => { chunks.push(chunk); }); res.on("end", () => { resolve(buffer.Buffer.concat(chunks)); req.destroy(); }); }); req.end(); }); } const isImdsCredentials = (arg) => Boolean(arg) && typeof arg === "object" && typeof arg.AccessKeyId === "string" && typeof arg.SecretAccessKey === "string" && typeof arg.Token === "string" && typeof arg.Expiration === "string"; const fromImdsCredentials = (creds) => ({ accessKeyId: creds.AccessKeyId, secretAccessKey: creds.SecretAccessKey, sessionToken: creds.Token, expiration: new Date(creds.Expiration), ...(creds.AccountId && { accountId: creds.AccountId }), }); const DEFAULT_TIMEOUT = 1000; const DEFAULT_MAX_RETRIES = 0; const providerConfigFromInit = ({ maxRetries = DEFAULT_MAX_RETRIES, timeout = DEFAULT_TIMEOUT, }) => ({ maxRetries, timeout }); const retry = (toRetry, maxRetries) => { let promise = toRetry(); for (let i = 0; i < maxRetries; i++) { promise = promise.catch(toRetry); } return promise; }; const ENV_CMDS_FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI"; const ENV_CMDS_RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; const ENV_CMDS_AUTH_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; const fromContainerMetadata = (init = {}) => { const { timeout, maxRetries } = providerConfigFromInit(init); return () => retry(async () => { const requestOptions = await getCmdsUri({ logger: init.logger }); const credsResponse = JSON.parse(await requestFromEcsImds(timeout, requestOptions)); if (!isImdsCredentials(credsResponse)) { throw new propertyProvider.CredentialsProviderError("Invalid response received from instance metadata service.", { logger: init.logger, }); } return fromImdsCredentials(credsResponse); }, maxRetries); }; const requestFromEcsImds = async (timeout, options) => { if (process.env[ENV_CMDS_AUTH_TOKEN]) { options.headers = { ...options.headers, Authorization: process.env[ENV_CMDS_AUTH_TOKEN], }; } const buffer = await httpRequest({ ...options, timeout, }); return buffer.toString(); }; const CMDS_IP = "169.254.170.2"; const GREENGRASS_HOSTS = { localhost: true, "127.0.0.1": true, }; const GREENGRASS_PROTOCOLS = { "http:": true, "https:": true, }; const getCmdsUri = async ({ logger }) => { if (process.env[ENV_CMDS_RELATIVE_URI]) { return { hostname: CMDS_IP, path: process.env[ENV_CMDS_RELATIVE_URI], }; } if (process.env[ENV_CMDS_FULL_URI]) { const parsed = url.parse(process.env[ENV_CMDS_FULL_URI]); if (!parsed.hostname || !(parsed.hostname in GREENGRASS_HOSTS)) { throw new propertyProvider.CredentialsProviderError(`${parsed.hostname} is not a valid container metadata service hostname`, { tryNextLink: false, logger, }); } if (!parsed.protocol || !(parsed.protocol in GREENGRASS_PROTOCOLS)) { throw new propertyProvider.CredentialsProviderError(`${parsed.protocol} is not a valid container metadata service protocol`, { tryNextLink: false, logger, }); } return { ...parsed, port: parsed.port ? parseInt(parsed.port, 10) : undefined, }; } throw new propertyProvider.CredentialsProviderError("The container metadata credential provider cannot be used unless" + ` the ${ENV_CMDS_RELATIVE_URI} or ${ENV_CMDS_FULL_URI} environment` + " variable is set", { tryNextLink: false, logger, }); }; class InstanceMetadataV1FallbackError extends propertyProvider.CredentialsProviderError { tryNextLink; name = "InstanceMetadataV1FallbackError"; constructor(message, tryNextLink = true) { super(message, tryNextLink); this.tryNextLink = tryNextLink; Object.setPrototypeOf(this, InstanceMetadataV1FallbackError.prototype); } } exports.Endpoint = void 0; (function (Endpoint) { Endpoint["IPv4"] = "http://169.254.169.254"; Endpoint["IPv6"] = "http://[fd00:ec2::254]"; })(exports.Endpoint || (exports.Endpoint = {})); const ENV_ENDPOINT_NAME = "AWS_EC2_METADATA_SERVICE_ENDPOINT"; const CONFIG_ENDPOINT_NAME = "ec2_metadata_service_endpoint"; const ENDPOINT_CONFIG_OPTIONS = { environmentVariableSelector: (env) => env[ENV_ENDPOINT_NAME], configFileSelector: (profile) => profile[CONFIG_ENDPOINT_NAME], default: undefined, }; var EndpointMode; (function (EndpointMode) { EndpointMode["IPv4"] = "IPv4"; EndpointMode["IPv6"] = "IPv6"; })(EndpointMode || (EndpointMode = {})); const ENV_ENDPOINT_MODE_NAME = "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"; const CONFIG_ENDPOINT_MODE_NAME = "ec2_metadata_service_endpoint_mode"; const ENDPOINT_MODE_CONFIG_OPTIONS = { environmentVariableSelector: (env) => env[ENV_ENDPOINT_MODE_NAME], configFileSelector: (profile) => profile[CONFIG_ENDPOINT_MODE_NAME], default: EndpointMode.IPv4, }; const getInstanceMetadataEndpoint = async () => urlParser.parseUrl((await getFromEndpointConfig()) || (await getFromEndpointModeConfig())); const getFromEndpointConfig = async () => nodeConfigProvider.loadConfig(ENDPOINT_CONFIG_OPTIONS)(); const getFromEndpointModeConfig = async () => { const endpointMode = await nodeConfigProvider.loadConfig(ENDPOINT_MODE_CONFIG_OPTIONS)(); switch (endpointMode) { case EndpointMode.IPv4: return exports.Endpoint.IPv4; case EndpointMode.IPv6: return exports.Endpoint.IPv6; default: throw new Error(`Unsupported endpoint mode: ${endpointMode}.` + ` Select from ${Object.values(EndpointMode)}`); } }; const STATIC_STABILITY_REFRESH_INTERVAL_SECONDS = 5 * 60; const STATIC_STABILITY_REFRESH_INTERVAL_JITTER_WINDOW_SECONDS = 5 * 60; const STATIC_STABILITY_DOC_URL = "https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html"; const getExtendedInstanceMetadataCredentials = (credentials, logger) => { const refreshInterval = STATIC_STABILITY_REFRESH_INTERVAL_SECONDS + Math.floor(Math.random() * STATIC_STABILITY_REFRESH_INTERVAL_JITTER_WINDOW_SECONDS); const newExpiration = new Date(Date.now() + refreshInterval * 1000); logger.warn("Attempting credential expiration extension due to a credential service availability issue. A refresh of these " + `credentials will be attempted after ${new Date(newExpiration)}.\nFor more information, please visit: ` + STATIC_STABILITY_DOC_URL); const originalExpiration = credentials.originalExpiration ?? credentials.expiration; return { ...credentials, ...(originalExpiration ? { originalExpiration } : {}), expiration: newExpiration, }; }; const staticStabilityProvider = (provider, options = {}) => { const logger = options?.logger || console; let pastCredentials; return async () => { let credentials; try { credentials = await provider(); if (credentials.expiration && credentials.expiration.getTime() < Date.now()) { credentials = getExtendedInstanceMetadataCredentials(credentials, logger); } } catch (e) { if (pastCredentials) { logger.warn("Credential renew failed: ", e); credentials = getExtendedInstanceMetadataCredentials(pastCredentials, logger); } else { throw e; } } pastCredentials = credentials; return credentials; }; }; const IMDS_PATH = "/latest/meta-data/iam/security-credentials/"; const IMDS_TOKEN_PATH = "/latest/api/token"; const AWS_EC2_METADATA_V1_DISABLED = "AWS_EC2_METADATA_V1_DISABLED"; const PROFILE_AWS_EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled"; const X_AWS_EC2_METADATA_TOKEN = "x-aws-ec2-metadata-token"; const fromInstanceMetadata = (init = {}) => staticStabilityProvider(getInstanceMetadataProvider(init), { logger: init.logger }); const getInstanceMetadataProvider = (init = {}) => { let disableFetchToken = false; const { logger, profile } = init; const { timeout, maxRetries } = providerConfigFromInit(init); const getCredentials = async (maxRetries, options) => { const isImdsV1Fallback = disableFetchToken || options.headers?.[X_AWS_EC2_METADATA_TOKEN] == null; if (isImdsV1Fallback) { let fallbackBlockedFromProfile = false; let fallbackBlockedFromProcessEnv = false; const configValue = await nodeConfigProvider.loadConfig({ environmentVariableSelector: (env) => { const envValue = env[AWS_EC2_METADATA_V1_DISABLED]; fallbackBlockedFromProcessEnv = !!envValue && envValue !== "false"; if (envValue === undefined) { throw new propertyProvider.CredentialsProviderError(`${AWS_EC2_METADATA_V1_DISABLED} not set in env, checking config file next.`, { logger: init.logger }); } return fallbackBlockedFromProcessEnv; }, configFileSelector: (profile) => { const profileValue = profile[PROFILE_AWS_EC2_METADATA_V1_DISABLED]; fallbackBlockedFromProfile = !!profileValue && profileValue !== "false"; return fallbackBlockedFromProfile; }, default: false, }, { profile, })(); if (init.ec2MetadataV1Disabled || configValue) { const causes = []; if (init.ec2MetadataV1Disabled) causes.push("credential provider initialization (runtime option ec2MetadataV1Disabled)"); if (fallbackBlockedFromProfile) causes.push(`config file profile (${PROFILE_AWS_EC2_METADATA_V1_DISABLED})`); if (fallbackBlockedFromProcessEnv) causes.push(`process environment variable (${AWS_EC2_METADATA_V1_DISABLED})`); throw new InstanceMetadataV1FallbackError(`AWS EC2 Metadata v1 fallback has been blocked by AWS SDK configuration in the following: [${causes.join(", ")}].`); } } const imdsProfile = (await retry(async () => { let profile; try { profile = await getProfile(options); } catch (err) { if (err.statusCode === 401) { disableFetchToken = false; } throw err; } return profile; }, maxRetries)).trim(); return retry(async () => { let creds; try { creds = await getCredentialsFromProfile(imdsProfile, options, init); } catch (err) { if (err.statusCode === 401) { disableFetchToken = false; } throw err; } return creds; }, maxRetries); }; return async () => { const endpoint = await getInstanceMetadataEndpoint(); if (disableFetchToken) { logger?.debug("AWS SDK Instance Metadata", "using v1 fallback (no token fetch)"); return getCredentials(maxRetries, { ...endpoint, timeout }); } else { let token; try { token = (await getMetadataToken({ ...endpoint, timeout })).toString(); } catch (error) { if (error?.statusCode === 400) { throw Object.assign(error, { message: "EC2 Metadata token request returned error", }); } else if (error.message === "TimeoutError" || [403, 404, 405].includes(error.statusCode)) { disableFetchToken = true; } logger?.debug("AWS SDK Instance Metadata", "using v1 fallback (initial)"); return getCredentials(maxRetries, { ...endpoint, timeout }); } return getCredentials(maxRetries, { ...endpoint, headers: { [X_AWS_EC2_METADATA_TOKEN]: token, }, timeout, }); } }; }; const getMetadataToken = async (options) => httpRequest({ ...options, path: IMDS_TOKEN_PATH, method: "PUT", headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600", }, }); const getProfile = async (options) => (await httpRequest({ ...options, path: IMDS_PATH })).toString(); const getCredentialsFromProfile = async (profile, options, init) => { const credentialsResponse = JSON.parse((await httpRequest({ ...options, path: IMDS_PATH + profile, })).toString()); if (!isImdsCredentials(credentialsResponse)) { throw new propertyProvider.CredentialsProviderError("Invalid response received from instance metadata service.", { logger: init.logger, }); } return fromImdsCredentials(credentialsResponse); }; exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES; exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT; exports.ENV_CMDS_AUTH_TOKEN = ENV_CMDS_AUTH_TOKEN; exports.ENV_CMDS_FULL_URI = ENV_CMDS_FULL_URI; exports.ENV_CMDS_RELATIVE_URI = ENV_CMDS_RELATIVE_URI; exports.fromContainerMetadata = fromContainerMetadata; exports.fromInstanceMetadata = fromInstanceMetadata; exports.getInstanceMetadataEndpoint = getInstanceMetadataEndpoint; exports.httpRequest = httpRequest; exports.providerConfigFromInit = providerConfigFromInit;