/*
* 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 Amazon.Runtime;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using ThirdParty.Json.LitJson;
namespace Amazon.Internal
{
[Obsolete("This class is obsoleted because as of version 3.7.100 endpoint is resolved using a newer system that uses request level parameters to resolve the endpoint.")]
public class RegionEndpointV3 : IRegionEndpoint
{
private ServiceMap _serviceMap = new ServiceMap();
public string RegionName { get; private set; }
public string DisplayName { get; private set; }
public string PartitionName
{
get
{
return (string)_partitionJsonData["partition"];
}
}
public string PartitionDnsSuffix
{
get
{
return (string)_partitionJsonData["dnsSuffix"];
}
}
private JsonData _partitionJsonData;
private JsonData _servicesJsonData;
private bool _servicesLoaded = false;
public RegionEndpointV3(string regionName, string displayName, JsonData partition, JsonData services)
{
RegionName = regionName;
DisplayName = displayName;
_partitionJsonData = partition;
_servicesJsonData = services;
}
///
/// Retrieves the endpoint for the given service in the current region
///
/// Name of the service in endpoints.json
/// Whether to retrieve the dual-stack variant
/// Matching endpoint from endpoints.json, or a computed endpoint if possible
[Obsolete("Use GetEndpointForService(string serviceName, GetEndpointForServiceOptions options) instead", error: false)]
public RegionEndpoint.Endpoint GetEndpointForService(string serviceName, bool dualStack)
{
return GetEndpointForService(serviceName, new GetEndpointForServiceOptions { DualStack = dualStack });
}
///
/// Retrieves the endpoint for the given service in the current region
///
/// Name of the service in endpoints.json
/// Specify additional requirements on the to be returned.
/// Matching endpoint from endpoints.json, or a computed endpoint if possible
public RegionEndpoint.Endpoint GetEndpointForService(string serviceName, GetEndpointForServiceOptions options)
{
var variants = BuildVariantHashSet(options);
return GetEndpointForService(serviceName, variants);
}
///
/// Retrieves the endpoint for the given service in the current region
///
/// Name of the service in endpoints.json
/// Set of tags describing an endpoint variant
/// Matching endpoint from endpoints.json, or a computed endpoint if possible
public RegionEndpoint.Endpoint GetEndpointForService(string serviceName, HashSet variants)
{
RegionEndpoint.Endpoint endpointObject = null;
// lock on _partitionJsonData because:
// a) ParseAllServices() will mutate _partitionJsonData, so it needs to be run inside a critical section.
// b) RegionEndpointV3 objects are exclusively built by RegionEndpointProviderV3, which will
// constructor inject the _same instance_ of _servicesJsonData and _partitionJsonData into all
// RegionEndpointProviderV3.
// c) This provides thread-safety if multiple RegionEndpointV3 instances were to be initialized at
// the same time: https://github.com/aws/aws-sdk-net/issues/1939
lock (_partitionJsonData)
{
if (!_servicesLoaded)
{
ParseAllServices();
_servicesLoaded = true;
}
if (!_serviceMap.TryGetEndpoint(serviceName, variants, out endpointObject))
{
// For all current variants (dual-stack and FIPS) SDKs cannot
// fall back to normal endpoints and must raise an error.
if (variants?.Count > 0)
{
throw new AmazonClientException($"Requested endpoint for {serviceName} with variants [{string.Join(", ", variants.ToArray())}] could not be found.");
}
// Do a fallback of creating an unknown endpoint based on the
// current region's hostname template.
endpointObject = CreateUnknownEndpoint(serviceName);
}
}
return endpointObject;
}
private RegionEndpoint.Endpoint CreateUnknownEndpoint(string serviceName)
{
string template = (string)_partitionJsonData["defaults"]["hostname"];
string dnsSuffix = (string)_partitionJsonData["dnsSuffix"];
string hostname = template.Replace("{service}", serviceName)
.Replace("{region}", RegionName)
.Replace("{dnsSuffix}", dnsSuffix);
return new RegionEndpoint.Endpoint(hostname, null, null, dnsSuffix, deprecated: false);
}
private void ParseAllServices()
{
foreach (string serviceName in _servicesJsonData.PropertyNames)
{
if (_servicesJsonData[serviceName] != null && _servicesJsonData[serviceName].Count > 0)
{
AddServiceToMap(_servicesJsonData[serviceName], serviceName);
}
}
}
private void AddServiceToMap(JsonData service, string serviceName)
{
var partitionEndpoint = service["partitionEndpoint"] != null ? (string)service["partitionEndpoint"] : "";
var isRegionalized = service["isRegionalized"] != null ? (bool)service["isRegionalized"] : true;
var regionKey = RegionName;
// Use the partition's default endpoint if the service is not regionalized, like Route53, and there is no
// endpoint defined for the this service name.
if (!isRegionalized && service["endpoints"][regionKey] == null && !string.IsNullOrEmpty(partitionEndpoint))
{
regionKey = partitionEndpoint;
}
var regionEndpoint = service["endpoints"][regionKey];
var mergedEndpoint = new JsonData();
var variantJsonData = new Dictionary, JsonData>(HashSet.CreateSetComparer());
// Create the merged endpoint definitions for both the normal endpoint and variants
MergeJsonData(mergedEndpoint, regionEndpoint, variantJsonData); // first prioritizing the service+region object
MergeJsonData(mergedEndpoint, service["defaults"], variantJsonData); // then service-level defaults
MergeJsonData(mergedEndpoint, _partitionJsonData["defaults"], variantJsonData); // then partition-level defaults
// Preserve existing behavior of short circuiting the normal endpoint if there isn't a region-specific entry
if (regionEndpoint != null)
{
AddNormalEndpointToServiceMap(mergedEndpoint, RegionName, serviceName);
}
AddVariantEndpointsToServiceMap(mergedEndpoint, RegionName, serviceName, variantJsonData);
}
private static void MergeJsonData(JsonData target, JsonData source, Dictionary, JsonData> variants)
{
if (source == null || target == null)
{
return;
}
foreach (var propertyName in source.PropertyNames)
{
if (propertyName != "variants")
{
if (target[propertyName] == null)
{
target[propertyName] = source[propertyName];
}
}
else // Variants need special handling because they are identified by the "tags" within the
{ // variant object. First build the key, and then merge the rest of the json properties for a given variant.
foreach (JsonData variant in source["variants"])
{
var tagsKey = new HashSet();
foreach (JsonData label in variant["tags"])
{
tagsKey.Add((string)label);
}
if (variants.ContainsKey(tagsKey))
{
// We've encountered this variant at a lower level in the hierarchy
// so only merge properties which are still null
foreach (var variantProperty in variant.PropertyNames)
{
if (variants[tagsKey][variantProperty] == null)
{
variants[tagsKey][variantProperty] = variant[variantProperty];
}
}
}
else // First time encountering this variant, so merge the entire object
{
variants[tagsKey] = variant;
}
}
}
}
}
private void AddNormalEndpointToServiceMap(JsonData mergedEndpoint, string regionName, string serviceName)
{
string template = (string)mergedEndpoint["hostname"];
string dnsSuffix = (string)_partitionJsonData["dnsSuffix"];
string hostname = template.Replace("{service}", serviceName)
.Replace("{region}", regionName)
.Replace("{dnsSuffix}", dnsSuffix);
string authRegion = null;
JsonData credentialScope = mergedEndpoint["credentialScope"];
if (credentialScope != null)
{
authRegion = DetermineAuthRegion(credentialScope);
}
JsonData deprecatedJson = mergedEndpoint["deprecated"];
var deprecated = deprecatedJson?.IsBoolean == true ? (bool) deprecatedJson : false;
var signatureOverride = DetermineSignatureOverride(mergedEndpoint, serviceName);
RegionEndpoint.Endpoint endpoint = new RegionEndpoint.Endpoint(hostname, authRegion, signatureOverride, dnsSuffix, deprecated);
_serviceMap.Add(serviceName, endpoint);
}
private void AddVariantEndpointsToServiceMap(JsonData mergedEndpoint, string regionName, string serviceName, Dictionary, JsonData> mergedVariants)
{
string authRegion = null;
JsonData credentialScope = mergedEndpoint["credentialScope"];
if (credentialScope != null)
{
authRegion = DetermineAuthRegion(credentialScope);
}
JsonData deprecatedJson = mergedEndpoint["deprecated"];
var deprecated = deprecatedJson?.IsBoolean == true ? (bool)deprecatedJson : false;
string signatureOverride = DetermineSignatureOverride(mergedEndpoint, serviceName);
foreach (var tagsKey in mergedVariants.Keys)
{
var variantHostnameTemplate = (string)mergedVariants[tagsKey]["hostname"];
if (string.IsNullOrEmpty(variantHostnameTemplate))
{
throw new AmazonClientException($"Unable to determine the hostname for {serviceName} with variants [{string.Join(", ", tagsKey.ToArray())}].");
}
if (variantHostnameTemplate.Contains("{region}") && string.IsNullOrEmpty(regionName))
{
throw new AmazonClientException($"Unable to determine the region for {serviceName} with variants [{string.Join(", ", tagsKey.ToArray())}].");
}
var variantDnsSuffix = mergedVariants[tagsKey]["dnsSuffix"] != null ? (string)mergedVariants[tagsKey]["dnsSuffix"] : (string)_partitionJsonData["dnsSuffix"];
if (variantHostnameTemplate.Contains("{dnsSuffix}") && string.IsNullOrEmpty(variantDnsSuffix))
{
throw new AmazonClientException($"Unable to determine the dnsSuffix for {serviceName} with variants [{string.Join(", ", tagsKey.ToArray())}].");
}
var variantHostname = variantHostnameTemplate.Replace("{service}", serviceName)
.Replace("{region}", regionName)
.Replace("{dnsSuffix}", variantDnsSuffix);
_serviceMap.Add(serviceName, new RegionEndpoint.Endpoint(variantHostname, authRegion, signatureOverride, variantDnsSuffix, deprecated), tagsKey);
}
}
private static string DetermineSignatureOverride(JsonData defaults, string serviceName)
{
if (string.Equals(serviceName, "s3", StringComparison.OrdinalIgnoreCase))
{
bool supportsSigV2 = false;
foreach (JsonData element in defaults["signatureVersions"])
{
string sig = (string)element;
if (string.Equals(sig, "s3", StringComparison.OrdinalIgnoreCase))
{
supportsSigV2 = true;
break;
}
}
return (supportsSigV2 ? "2" : "4");
}
return null;
}
private static string DetermineAuthRegion(JsonData credentialScope)
{
string authRegion = null;
if (credentialScope["region"] != null)
{
authRegion = (string)credentialScope["region"];
}
return authRegion;
}
///
/// Builds the set used to identify a specific endpoint variant
///
/// Whether to use a dualstack (IPv6 enabled) endpoint
/// Whether to use a FIPS-compliant endpoint
/// Set used to identify the combined variant in endpoints.json
private static HashSet BuildVariantHashSet(GetEndpointForServiceOptions options)
{
options = options ?? new GetEndpointForServiceOptions();
if (!options.DualStack && !options.FIPS)
{
return null;
}
var variants = new HashSet();
if (options.DualStack)
{
variants.Add("dualstack");
}
if (options.FIPS)
{
variants.Add("fips");
}
return variants;
}
class ServiceMap
{
///
/// Stores the plain endpoints for each service in the current region
///
private Dictionary _serviceMap = new Dictionary();
///
/// Stores the variants for each service in the current region, identified by the set of variant tags.
///
private Dictionary, RegionEndpoint.Endpoint>> _variantMap = new Dictionary, RegionEndpoint.Endpoint>>();
public bool ContainsKey(string serviceName)
{
return _serviceMap.ContainsKey(serviceName);
}
public void Add(string serviceName, RegionEndpoint.Endpoint endpoint, HashSet variants = null)
{
if (variants == null || variants.Count == 0)
{
_serviceMap.Add(serviceName, endpoint);
}
else
{
if (!_variantMap.ContainsKey(serviceName) || _variantMap[serviceName] == null)
{
_variantMap.Add(serviceName, new Dictionary, RegionEndpoint.Endpoint>(HashSet.CreateSetComparer()));
}
_variantMap[serviceName].Add(variants, endpoint);
}
}
public bool TryGetEndpoint(string serviceName, HashSet variants, out RegionEndpoint.Endpoint endpoint)
{
if (variants == null || variants.Count == 0)
{
return _serviceMap.TryGetValue(serviceName, out endpoint);
}
else
{
if (!_variantMap.ContainsKey(serviceName))
{
endpoint = default(RegionEndpoint.Endpoint);
return false;
}
else
{
return _variantMap[serviceName].TryGetValue(variants, out endpoint);
}
}
}
}
}
[Obsolete("This class is obsoleted because as of version 3.7.100 endpoint is resolved using a newer system that uses request level parameters to resolve the endpoint.")]
public class RegionEndpointProviderV3 : IRegionEndpointProvider, IDisposable
{
#if NETSTANDARD
private const string ENDPOINT_JSON_RESOURCE = "Core.endpoints.json";
#else
private const string ENDPOINT_JSON_RESOURCE = "Amazon.endpoints.json";
#endif
private const string ENDPOINT_JSON = "endpoints.json";
private JsonData _root;
private Dictionary _regionEndpointMap = new Dictionary();
private Dictionary _nonStandardRegionNameToObjectMap = new Dictionary();
private ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim();
public RegionEndpointProviderV3()
{
using (var stream = GetEndpointJsonSourceStream())
using (StreamReader reader = new StreamReader(stream))
{
_root = JsonMapper.ToObject(reader);
}
}
public RegionEndpointProviderV3(JsonData root)
{
_root = root;
}
private static Stream GetEndpointJsonSourceStream()
{
//
// If the endpoints.json file has been provided next to the assembly:
//
string assemblyLocation = typeof(RegionEndpointProviderV3).Assembly.Location;
if (!string.IsNullOrEmpty(assemblyLocation))
{
string endpointsPath = Path.Combine(Path.GetDirectoryName(assemblyLocation), ENDPOINT_JSON);
if (File.Exists(endpointsPath))
{
return File.Open(endpointsPath, FileMode.Open, FileAccess.Read);
}
}
//
// Default to endpoints.json file provided in the resource manifest:
//
return Amazon.Util.Internal.TypeFactory.GetTypeInfo(typeof(RegionEndpointProviderV3)).Assembly.GetManifestResourceStream(ENDPOINT_JSON_RESOURCE);
}
private IEnumerable _allRegionEndpoints;
public IEnumerable AllRegionEndpoints
{
get
{
if (_allRegionEndpoints == null)
{
try
{
_readerWriterLock.EnterWriteLock();
if (_allRegionEndpoints == null)
{
JsonData partitions = _root["partitions"];
List endpoints = new List();
foreach (JsonData partition in partitions)
{
JsonData regions = partition["regions"];
foreach (string regionName in regions.PropertyNames)
{
IRegionEndpoint endpoint;
if (!_regionEndpointMap.TryGetValue(regionName, out endpoint))
{
endpoint = new RegionEndpointV3(regionName, (string)regions[regionName]["description"], partition, partition["services"]);
_regionEndpointMap.Add(regionName, endpoint);
}
endpoints.Add(endpoint);
}
}
_allRegionEndpoints = endpoints;
}
}
finally
{
if (_readerWriterLock.IsWriteLockHeld)
{
_readerWriterLock.ExitWriteLock();
}
}
}
return _allRegionEndpoints;
}
}
private object _allRegionRegexLock = new object();
private IEnumerable _allRegionRegex;
public IEnumerable AllRegionRegex
{
get
{
if (_allRegionRegex == null)
{
try
{
_readerWriterLock.EnterWriteLock();
if (_allRegionRegex == null)
{
JsonData partitions = _root["partitions"];
var allRegionRegex = new List();
foreach (JsonData partition in partitions)
{
var regionRegex = (string)partition["regionRegex"];
allRegionRegex.Add(regionRegex);
}
_allRegionRegex = allRegionRegex;
}
}
finally
{
if (_readerWriterLock.IsWriteLockHeld)
{
_readerWriterLock.ExitWriteLock();
}
}
}
return _allRegionRegex;
}
}
private static string GetUnknownRegionDescription(string regionName)
{
if (regionName.StartsWith("cn-", StringComparison.OrdinalIgnoreCase) ||
regionName.EndsWith("cn-global", StringComparison.OrdinalIgnoreCase))
{
return "China (Unknown)";
}
else
{
return "Unknown";
}
}
private static bool IsRegionInPartition(string regionName, JsonData partition, out string description)
{
JsonData regionsData = partition["regions"];
string regionPattern = (string)partition["regionRegex"];
// see if the region name is a real region
if (regionsData[regionName] != null)
{
description = (string)regionsData[regionName]["description"];
return true;
}
// see if the region is global region by concatenating the partition and "-global" to construct the global name
// for the partition
else if (regionName.Equals(string.Concat((string)partition["partition"], "-global"), StringComparison.OrdinalIgnoreCase))
{
description = "Global";
return true;
}
// no region key in the entry, but it matches the pattern in this partition.
// we can try to construct an endpoint based on the heuristics described in endpoints.json
else if (new Regex(regionPattern).Match(regionName).Success)
{
description = GetUnknownRegionDescription(regionName);
return true;
}
else
{
description = GetUnknownRegionDescription(regionName);
return false;
}
}
public IRegionEndpoint GetRegionEndpoint(string regionName)
{
try
{
try
{
_readerWriterLock.EnterReadLock();
IRegionEndpoint endpoint;
if (_regionEndpointMap.TryGetValue(regionName, out endpoint))
{
return endpoint;
}
}
finally
{
if (_readerWriterLock.IsReadLockHeld)
{
_readerWriterLock.ExitReadLock();
}
}
try
{
_readerWriterLock.EnterWriteLock();
IRegionEndpoint endpoint;
// Check again to see if region is in cache in case another thread got the write lock before and filled the cache.
if (_regionEndpointMap.TryGetValue(regionName, out endpoint))
{
return endpoint;
}
JsonData partitions = _root["partitions"];
foreach (JsonData partition in partitions)
{
string description;
if (IsRegionInPartition(regionName, partition, out description))
{
endpoint = new RegionEndpointV3(regionName, description, partition, partition["services"]);
_regionEndpointMap.Add(regionName, endpoint);
return endpoint;
}
}
}
finally
{
if (_readerWriterLock.IsWriteLockHeld)
{
_readerWriterLock.ExitWriteLock();
}
}
}
catch (Exception)
{
throw new AmazonClientException("Invalid endpoint.json format.");
}
return GetNonstandardRegionEndpoint(regionName);
}
///
/// This region name is non-standard. Search the whole endpoints.json file to
/// determine the partition this region is in.
///
private IRegionEndpoint GetNonstandardRegionEndpoint(string regionName)
{
try
{
_readerWriterLock.EnterReadLock();
IRegionEndpoint regionEndpoint;
if (_nonStandardRegionNameToObjectMap.TryGetValue(regionName, out regionEndpoint))
{
return regionEndpoint;
}
}
finally
{
if (_readerWriterLock.IsReadLockHeld)
{
_readerWriterLock.ExitReadLock();
}
}
try
{
_readerWriterLock.EnterWriteLock();
IRegionEndpoint regionEndpoint;
// Check again to see if region is in cache in case another thread got the write lock before and filled the cache.
if (_nonStandardRegionNameToObjectMap.TryGetValue(regionName, out regionEndpoint))
{
return regionEndpoint;
}
// default to "aws" partition
JsonData partitionData = _root["partitions"][0];
string regionDescription = GetUnknownRegionDescription(regionName);
JsonData servicesData = partitionData["services"];
bool foundContainingPartition = false;
const string validRegionRegexStr = @"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$";
var match = Regex.Match(regionName, validRegionRegexStr, RegexOptions.Compiled);
foreach (JsonData partition in _root["partitions"])
{
JsonData partitionServices = partition["services"];
foreach (string service in partitionServices.PropertyNames)
{
if (partitionServices[service] != null && partitionServices[service].Count > 0)
{
JsonData serviceData = partitionServices[service];
if (serviceData != null && serviceData["endpoints"][regionName] != null)
{
partitionData = partition;
servicesData = partitionServices;
foundContainingPartition = true;
break;
}
}
}
}
if (!foundContainingPartition && !match.Success)
{
throw new ArgumentException("Invalid region endpoint provided");
}
regionEndpoint = new RegionEndpointV3(regionName, regionDescription, partitionData, servicesData);
_nonStandardRegionNameToObjectMap.Add(regionName, regionEndpoint);
return regionEndpoint;
}
finally
{
if (_readerWriterLock.IsWriteLockHeld)
{
_readerWriterLock.ExitWriteLock();
}
}
}
private static JsonData _emptyDictionaryJsonData = JsonMapper.ToObject("{}");
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_readerWriterLock.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
/// Returns the DNS suffix for the given partition, or
/// an empty string if a matching partition was not found in endpoints.json
///
/// partition
/// DNS suffix for the given partition, empty string if a matching partition was not found
public string GetDnsSuffixForPartition(string partition)
{
foreach (JsonData currentPartition in _root["partitions"])
{
if ((string)currentPartition["partition"] == partition)
{
return (string)currentPartition["dnsSuffix"];
}
}
return "";
}
}
}