/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading; using Amazon.Runtime; using ThirdParty.Json.LitJson; using System.Globalization; using Amazon.Runtime.Internal.Util; using AWSSDK.Runtime.Internal.Util; using Amazon.Runtime.Internal; namespace Amazon.Util { /// /// Provides access to EC2 instance metadata when running on an EC2 instance. /// If this class is used on a non-EC2 instance, the properties in this class /// will return null. /// /// /// /// Amazon EC2 instances can access instance-specific metadata, as well as data supplied when launching the instances, using a specific URI. /// /// /// You can use this data to build more generic AMIs that can be modified by configuration files supplied at launch time. /// For example, if you run web servers for various small businesses, they can all use the same AMI and retrieve their content from the /// Amazon S3 bucket you specify at launch. To add a new customer at any time, simply create a bucket for the customer, add their content, /// and launch your AMI. /// /// /// More information about EC2 Metadata /// /// public static class EC2InstanceMetadata { [Obsolete("EC2_METADATA_SVC is obsolete, refer to ServiceEndpoint instead to respect environment and profile overrides.")] public static readonly string EC2_METADATA_SVC = "http://169.254.169.254"; [Obsolete("EC2_METADATA_ROOT is obsolete, refer to EC2MetadataRoot instead to respect environment and profile overrides.")] public static readonly string EC2_METADATA_ROOT = EC2_METADATA_SVC + LATEST + "/meta-data"; [Obsolete("EC2_USERDATA_ROOT is obsolete, refer to EC2UserDataRoot instead to respect environment and profile overrides.")] public static readonly string EC2_USERDATA_ROOT = EC2_METADATA_SVC + LATEST + "/user-data"; [Obsolete("EC2_DYNAMICDATA_ROOT is obsolete, refer to EC2DynamicDataRoot instead to respect environment and profile overrides.")] public static readonly string EC2_DYNAMICDATA_ROOT = EC2_METADATA_SVC + LATEST + "/dynamic"; [Obsolete("EC2_APITOKEN_URL is obsolete, refer to EC2ApiTokenUrl instead to respect environment and profile overrides.")] public static readonly string EC2_APITOKEN_URL = EC2_METADATA_SVC + LATEST + "/api/token"; public static readonly string LATEST = "/latest", AWS_EC2_METADATA_DISABLED = "AWS_EC2_METADATA_DISABLED"; private static int DEFAULT_RETRIES = 3, MIN_PAUSE_MS = 250, MAX_RETRIES = 3, DEFAULT_APITOKEN_TTL = 21600; private static Dictionary _cache = new Dictionary(); private static bool useNullToken = false; private static ReaderWriterLockSlim metadataLock = new ReaderWriterLockSlim(); // Lock to control getting metadata across multiple threads. private static readonly TimeSpan metadataLockTimeout = TimeSpan.FromMilliseconds(5000); /// /// Base endpoint of the instance metadata service. Returns the endpoint configured first /// via environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT then the current profile's /// ec2_metadata_service_endpoint value. If a specific endpoint is not configured, it selects a pre-determined /// endpoint based on environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE then the /// current profile's ec2_metadata_service_endpoint_mode setting. /// public static string ServiceEndpoint { get { if (!string.IsNullOrEmpty(FallbackInternalConfigurationFactory.EC2MetadataServiceEndpoint)) { return FallbackInternalConfigurationFactory.EC2MetadataServiceEndpoint; } else if (FallbackInternalConfigurationFactory.EC2MetadataServiceEndpointMode == EC2MetadataServiceEndpointMode.IPv6) { return "http://[fd00:ec2::254]"; } else // either explicit IPv4 or default behavior { return "http://169.254.169.254"; } } } /// /// Root URI to retrieve instance metadata /// public static string EC2MetadataRoot => ServiceEndpoint + LATEST + "/meta-data"; /// /// Root URI to retrieve instance user data /// public static string EC2UserDataRoot => ServiceEndpoint + LATEST + "/user-data"; /// /// Root URI to retrieve dynamic instance data /// public static string EC2DynamicDataRoot => ServiceEndpoint + LATEST + "/dynamic"; /// /// URI to retrieve the IMDS API token /// public static string EC2ApiTokenUrl => ServiceEndpoint + LATEST + "/api/token"; /// /// Returns whether requesting the EC2 Instance Metadata Service is /// enabled via the AWS_EC2_METADATA_DISABLED environment variable. /// public static bool IsIMDSEnabled { get { const string True = "true"; string value = string.Empty; try { value = System.Environment.GetEnvironmentVariable(AWS_EC2_METADATA_DISABLED); } catch { }; return !True.Equals(value, StringComparison.OrdinalIgnoreCase); } } /// /// Allows to configure the proxy used for HTTP requests. The default value is null. /// public static IWebProxy Proxy { get; set; } /// /// The AMI ID used to launch the instance. /// public static string AmiId { get { return FetchData("/ami-id"); } } /// /// The index of this instance in the reservation. /// public static string AmiLaunchIndex { get { return FetchData("/ami-launch-index"); } } /// /// The manifest path of the AMI with which the instance was launched. /// public static string AmiManifestPath { get { return FetchData("/ami-manifest-path"); } } /// /// The AMI IDs of any instances that were rebundled to create this AMI. /// Will only exist if the AMI manifest file contained an ancestor-amis key. /// public static IEnumerable AncestorAmiIds { get { return GetItems("/ancestor-ami-ids"); } } /// /// The private hostname of the instance. /// In cases where multiple network interfaces are present, /// this refers to the eth0 device (the device for which the device number is 0). /// public static string Hostname { get { return FetchData("/hostname"); } } /// /// Notifies the instance that it should reboot in preparation for bundling. /// Valid values: none | shutdown | bundle-pending. /// public static string InstanceAction { get { return FetchData("/instance-action"); } } /// /// The ID of this instance. /// public static string InstanceId { get { return FetchData("/instance-id"); } } /// /// The type of instance. /// public static string InstanceType { get { return FetchData("/instance-type"); } } /// /// The ID of the kernel launched with this instance, if applicable. /// public static string KernelId { get { return GetData("kernel-id"); } } /// /// The local hostname of the instance. In cases where multiple network interfaces are present, /// this refers to the eth0 device (the device for which device-number is 0). /// public static string LocalHostname { get { return FetchData("/local-hostname"); } } /// /// The instance's MAC address. In cases where multiple network interfaces are present, /// this refers to the eth0 device (the device for which device-number is 0). /// public static string MacAddress { get { return FetchData("/mac"); } } /// /// The private IP address of the instance. In cases where multiple network interfaces are present, /// this refers to the eth0 device (the device for which device-number is 0). /// public static string PrivateIpAddress { get { return FetchData("/local-ipv4"); } } /// /// The Availability Zone in which the instance launched. /// public static string AvailabilityZone { get { return FetchData("/placement/availability-zone"); } } /// /// Product codes associated with the instance, if any. /// public static IEnumerable ProductCodes { get { return GetItems("/product-codes"); } } /// /// Public key. Only available if supplied at instance launch time. /// public static string PublicKey { get { return FetchData("/public-keys/0/openssh-key"); } } /// /// The ID of the RAM disk specified at launch time, if applicable. /// public static string RamdiskId { get { return FetchData("/ramdisk-id"); } } /// /// The region in which the instance is running, extracted from the identity /// document data. /// public static RegionEndpoint Region { get { var identityDocument = IdentityDocument; if (!string.IsNullOrEmpty(identityDocument)) { try { var jsonDocument = JsonMapper.ToObject(identityDocument.ToString()); var regionName = jsonDocument["region"]; if (regionName != null) return RegionEndpoint.GetBySystemName(regionName.ToString()); } catch (Exception e) { var logger = Logger.GetLogger(typeof(EC2InstanceMetadata)); logger.Error(e, "Error attempting to read region from instance metadata identity document"); } } return null; } } /// /// ID of the reservation. /// public static string ReservationId { get { return FetchData("/reservation-id"); } } /// /// The names of the security groups applied to the instance. /// public static IEnumerable SecurityGroups { get { return GetItems("/security-groups"); } } /// /// Returns information about the last time the instance profile was updated, /// including the instance's LastUpdated date, InstanceProfileArn, and InstanceProfileId. /// public static IAMInstanceProfileMetadata IAMInstanceProfileInfo { get { var json = GetData("/iam/info"); if (null == json) return null; IAMInstanceProfileMetadata info; try { info = JsonMapper.ToObject(json); } catch { info = new IAMInstanceProfileMetadata { Code = "Failed", Message = "Could not parse response from metadata service." }; } return info; } } /// /// Returns the temporary security credentials (AccessKeyId, SecretAccessKey, SessionToken, and Expiration) /// associated with the IAM roles on the instance. /// public static IDictionary IAMSecurityCredentials { get { var list = GetItems("/iam/security-credentials"); if (list == null) return null; var creds = new Dictionary(); foreach (var item in list) { var json = GetData("/iam/security-credentials/" + item); try { var cred = JsonMapper.ToObject(json); creds[item] = cred; } catch { creds[item] = new IAMSecurityCredentialMetadata { Code = "Failed", Message = "Could not parse response from metadata service." }; } } return creds; } } /// /// The virtual devices associated with the ami, root, ebs, and swap. /// public static IDictionary BlockDeviceMapping { get { var keys = GetItems("/block-device-mapping"); if (keys == null) return null; var mapping = new Dictionary(); foreach (var key in keys) { mapping[key] = GetData("/block-device-mapping/" + key); } return mapping; } } /// /// The network interfaces on the instance. /// public static IEnumerable NetworkInterfaces { get { var macs = GetItems("/network/interfaces/macs/"); if (macs == null) return null; var interfaces = new List(); foreach (var mac in macs) { interfaces.Add(new NetworkInterfaceMetadata(mac.Trim('/'))); } return interfaces; } } /// /// The metadata sent to the instance. /// public static string UserData { get { return GetData(EC2UserDataRoot); } } /// /// Value showing whether the customer has enabled detailed /// one-minute monitoring in CloudWatch. /// public static string InstanceMonitoring { get { return GetData(EC2DynamicDataRoot + "/fws/instance-monitoring"); } } /// /// JSON containing instance attributes, such as instance-id, private IP /// address, etc /// public static string IdentityDocument { get { return GetData(EC2DynamicDataRoot + "/instance-identity/document"); } } /// /// Data that can be used by other parties to verify its origin and authenticity. /// public static string IdentitySignature { get { return GetData(EC2DynamicDataRoot + "/instance-identity/signature"); } } /// /// Used to verify the document's authenticity and content against the signature. /// public static string IdentityPkcs7 { get { return GetData(EC2DynamicDataRoot + "/instance-identity/pkcs7"); } } /// /// Return the list of items in the metadata at path. /// /// Path at which to query the metadata; may be relative or absolute. /// List of items returned by the metadata service public static IEnumerable GetItems(string path) { return GetItems(path, DEFAULT_RETRIES, false); } /// /// Return the metadata at the path /// /// Path at which to query the metadata; may be relative or absolute. /// Data returned by the metadata service public static string GetData(string path) { return GetData(path, DEFAULT_RETRIES); } /// /// Return the metadata at the path /// /// Path at which to query the metadata; may be relative or absolute. /// Number of attempts to make /// Data returned by the metadata service public static string GetData(string path, int tries) { var items = GetItems(path, tries, true); if (items != null && items.Count > 0) return items[0]; return null; } /// /// Return the list of items in the metadata at path. /// /// Path at which to query the metadata; may be relative or absolute. /// Number of attempts to make /// List of items returned by the metadata service public static IEnumerable GetItems(string path, int tries) { return GetItems(path, tries, false); } private static string FetchData(string path) { return FetchData(path, false); } private static string FetchData(string path, bool force) { try { // Try to acquire read lock if there is no need to force get the metadata. The thread would be blocked if another thread has write lock. if (!force) { if (metadataLock.TryEnterReadLock(metadataLockTimeout)) { try { if (_cache.ContainsKey(path)) { return _cache[path]; } } finally { metadataLock.ExitReadLock(); } } else { Logger.GetLogger(typeof(EC2InstanceMetadata)).InfoFormat("Unable to acquire read lock to access cache."); } } // If there is no metadata cached or it needs to force get the metadata. Try to acquire write lock. if (metadataLock.TryEnterWriteLock(metadataLockTimeout)) { try { // Check if metadata is cached again in case other thread might have already fetched it. if (force || !_cache.ContainsKey(path)) { _cache[path] = GetData(path); } } finally { metadataLock.ExitWriteLock(); } } else { Logger.GetLogger(typeof(EC2InstanceMetadata)).InfoFormat("Unable to acquire write lock to modify cache."); } // Try to acquire read lock. The thread would be blocked if another thread has write lock. if (metadataLock.TryEnterReadLock(metadataLockTimeout)) { try { if (_cache.ContainsKey(path)) { return _cache[path]; } else { return null; } } finally { metadataLock.ExitReadLock(); } } else { Logger.GetLogger(typeof(EC2InstanceMetadata)).InfoFormat("Unable to acquire read lock to access cache."); return null; } } catch { return null; } } /// /// Fetches the api token to use with metadata requests. /// /// The API token or null public static string FetchApiToken() { return FetchApiToken(DEFAULT_RETRIES); } /// /// Fetches the api token to use with metadata requests. /// /// The number of tries to fetch the api token before giving up and throwing the web exception /// The API token or null if an API token couldn't be obtained and doesn't need to be used private static string FetchApiToken(int tries) { for (int retry = 1; retry <= tries; retry++) { if (!IsIMDSEnabled || useNullToken) { return null; } try { var uriForToken = new Uri(EC2ApiTokenUrl); var headers = new Dictionary(); headers.Add(HeaderKeys.XAwsEc2MetadataTokenTtlSeconds, DEFAULT_APITOKEN_TTL.ToString(CultureInfo.InvariantCulture)); var content = AWSSDKUtils.ExecuteHttpRequest(uriForToken, "PUT", null, TimeSpan.FromSeconds(5), Proxy, headers); return content.Trim(); } catch (Exception e) { HttpStatusCode? httpStatusCode = ExceptionUtils.DetermineHttpStatusCode(e); if (httpStatusCode == HttpStatusCode.NotFound || httpStatusCode == HttpStatusCode.MethodNotAllowed || httpStatusCode == HttpStatusCode.Forbidden) { useNullToken = true; return null; } if (retry >= tries) { if (httpStatusCode == HttpStatusCode.BadRequest) { Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "Unable to contact EC2 Metadata service to obtain a metadata token."); throw; } Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "Unable to contact EC2 Metadata service to obtain a metadata token. Attempting to access IMDS without a token."); //If there isn't a status code, it was a failure to contact the server which would be //a request failure, a network issue, or a timeout. Cache this response and fallback //to IMDS flow without a token. If the non token IMDS flow returns unauthorized, the //useNullToken flag will be cleared and the IMDS flow will attempt to obtain another //token. if (httpStatusCode == null) { useNullToken = true; } //Return null to fallback to the IMDS flow without using a token. return null; } PauseExponentially(retry - 1); } } return null; } public static void ClearTokenFlag() { useNullToken = false; } private static List GetItems(string relativeOrAbsolutePath, int tries, bool slurp) { return GetItems(relativeOrAbsolutePath, tries, slurp, null); } private static List GetItems(string relativeOrAbsolutePath, int tries, bool slurp, string token) { var items = new List(); //For all meta-data queries we need to fetch an api token to use. In the event a //token cannot be obtained we will fallback to not using a token. Dictionary headers = null; if(token == null) { token = FetchApiToken(DEFAULT_RETRIES); } if (!string.IsNullOrEmpty(token)) { headers = new Dictionary(); headers.Add(HeaderKeys.XAwsEc2MetadataToken, token); } try { if (!IsIMDSEnabled) { throw new IMDSDisabledException(); } // if we are given a relative path, we assume the data we need exists under the // main metadata root var uri = relativeOrAbsolutePath.StartsWith(ServiceEndpoint, StringComparison.Ordinal) ? new Uri(relativeOrAbsolutePath) : new Uri(EC2MetadataRoot + relativeOrAbsolutePath); var content = AWSSDKUtils.ExecuteHttpRequest(uri, "GET", null, TimeSpan.FromSeconds(5), Proxy, headers); using (var stream = new StringReader(content)) { if (slurp) items.Add(stream.ReadToEnd()); else { string line; do { line = stream.ReadLine(); if (line != null) items.Add(line.Trim()); } while (line != null); } } } catch (IMDSDisabledException) { // Keep this behavior identical to when HttpStatusCode.NotFound is returned. return null; } catch (Exception e) { HttpStatusCode? httpStatusCode = ExceptionUtils.DetermineHttpStatusCode(e); if (httpStatusCode == HttpStatusCode.NotFound) { return null; } else if (httpStatusCode == HttpStatusCode.Unauthorized) { ClearTokenFlag(); Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "EC2 Metadata service returned unauthorized for token based secure data flow."); throw; } if (tries <= 1) { Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "Unable to contact EC2 Metadata service."); return null; } PauseExponentially(DEFAULT_RETRIES - tries); return GetItems(relativeOrAbsolutePath, tries - 1, slurp, token); } return items; } /// /// Exponentially sleeps based on the current retry value. A lower /// value will sleep shorter than a larger value /// /// Base 0 retry index private static void PauseExponentially(int retry) { var pause = (int)(Math.Pow(2, retry) * MIN_PAUSE_MS); Thread.Sleep(pause < MIN_PAUSE_MS ? MIN_PAUSE_MS : pause); } #if !NETSTANDARD [Serializable] #endif private class IMDSDisabledException : InvalidOperationException { }; } /// /// Returns information about the last time the instance profile was updated, /// including the instance's LastUpdated date, InstanceProfileArn, and InstanceProfileId. /// public class IAMInstanceProfileMetadata { /// /// The status of the instance profile /// public string Code { get; set; } /// /// Further information about the status of the instance profile /// public string Message { get; set; } /// /// The date and time the instance profile was updated /// public DateTime LastUpdated { get; set; } /// /// The Amazon Resource Name (ARN) of the instance profile /// public string InstanceProfileArn { get; set; } /// /// The Id of the instance profile /// public string InstanceProfileId { get; set; } } /// /// The temporary security credentials (AccessKeyId, SecretAccessKey, SessionToken, and Expiration) associated with the IAM role. /// public class IAMSecurityCredentialMetadata { /// /// The status of the security credential /// public string Code { get; set; } /// /// Further information about the status of the instance profile /// public string Message { get; set; } /// /// The date and time the security credential was last updated /// public DateTime LastUpdated { get; set; } /// /// The type of the security credential /// public string Type { get; set; } /// /// The uniqe id of the security credential /// public string AccessKeyId { get; set; } /// /// The secret key used to sign requests /// public string SecretAccessKey { get; set; } /// /// The security token /// public string Token { get; set; } /// /// The date and time when these credentials expire /// public DateTime Expiration { get; set; } } /// /// All of the metadata associated with a network interface on the instance. /// public class NetworkInterfaceMetadata { private string _path; private string _mac; private IEnumerable _availableKeys; private Dictionary _data = new Dictionary(); private NetworkInterfaceMetadata() { } /// /// Construct an instance of NetworkInterface /// /// public NetworkInterfaceMetadata(string macAddress) { _mac = macAddress; _path = string.Format(CultureInfo.InvariantCulture, "/network/interfaces/macs/{0}/", _mac); } /// /// The interface's Media Access Control (mac) address. /// public string MacAddress { get { return _mac; } } /// /// The ID of the owner of the network interface. /// /// /// In multiple-interface environments, an interface can be attached by a third party, such as Elastic Load Balancing. /// Traffic on an interface is always billed to the interface owner. /// public string OwnerId { get { return GetData("owner-id"); } } /// /// The interface's profile /// public string Profile { get { return GetData("profile"); } } /// /// The interface's local hostname. /// public string LocalHostname { get { return GetData("local-hostname"); } } /// /// The private IP addresses associated with the interface. /// public IEnumerable LocalIPv4s { get { return GetItems("local-ipv4s"); } } /// /// The interface's public hostname. /// public string PublicHostname { get { return GetData("public-hostname"); } } /// /// The elastic IP addresses associated with the interface. /// /// /// There may be multiple IP addresses on an instance. /// public IEnumerable PublicIPv4s { get { return GetItems("public-ipv4s"); } } /// /// Security groups to which the network interface belongs. /// public IEnumerable SecurityGroups { get { return GetItems("security-groups"); } } /// /// IDs of the security groups to which the network interface belongs. Returned only for Amazon EC2 instances launched into a VPC. /// public IEnumerable SecurityGroupIds { get { return GetItems("security-group-ids"); } } /// /// The ID of the Amazon EC2-VPC subnet in which the interface resides. /// /// /// Returned only for Amazon EC2 instances launched into a VPC. /// public string SubnetId { get { return GetData("subnet-id"); } } /// /// The CIDR block of the Amazon EC2-VPC subnet in which the interface resides. /// /// /// Returned only for Amazon EC2 instances launched into a VPC. /// public string SubnetIPv4CidrBlock { get { return GetData("subnet-ipv4-cidr-block"); } } /// /// The CIDR block of the Amazon EC2-VPC subnet in which the interface resides. /// /// /// Returned only for Amazon EC2 instances launched into a VPC. /// public string VpcId { get { return GetData("vpc-id"); } } /// /// Get the private IPv4 address(es) that are associated with the public-ip address and assigned to that interface. /// /// The public IP address /// Private IPv4 address(es) associated with the public IP address public IEnumerable GetIpV4Association(string publicIp) { return EC2InstanceMetadata.GetItems(string.Format(CultureInfo.InvariantCulture, "{0}ipv4-associations/{1}", _path, publicIp)); } private string GetData(string key) { if (_data.ContainsKey(key)) return _data[key]; // Since the keys are variable, cache a list of which ones are available // to prevent unnecessary trips to the service. if (null == _availableKeys) _availableKeys = EC2InstanceMetadata.GetItems(_path); if (_availableKeys.Contains(key)) { _data[key] = EC2InstanceMetadata.GetData(_path + key); return _data[key]; } else return null; } private IEnumerable GetItems(string key) { if (null == _availableKeys) _availableKeys = EC2InstanceMetadata.GetItems(_path); if (_availableKeys.Contains(key)) { return EC2InstanceMetadata.GetItems(_path + key); } else return new List(); } } }