/*
* 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();
}
}
}