/*
* 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.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Amazon.Internal;
using Amazon.Runtime.Internal.Util;
namespace Amazon.Util.Internal
{
///
/// Finds region string in the endpoint string using predefined rules
/// If predefined rules fail to match the region, regular expression strings provided in
/// endpoints.json are used to find the region.
/// If regular expressions also fail, then a default region is returned.
///
public class RegionFinder : IDisposable
{
internal class EndpointSegment
{
public string Value { get; set; }
public IRegionEndpoint RegionEndpoint { get; set; }
public bool UseThisValue { get; set; }
public List Children { get; set; }
}
#region Constants
private const string DefaultRegion = "us-east-1";
private const string DefaultGovRegion = "us-gov-west-1";
#endregion
#region Members
private readonly EndpointSegment _root;
private readonly Logger _logger;
private readonly Dictionary _regionEndpoints;
private readonly RegionEndpointProviderV3 _regionEndpointProviderV3;
#endregion
#region Constructors
internal RegionFinder()
{
_regionEndpointProviderV3 = new RegionEndpointProviderV3();
_regionEndpoints = BuildRegionEndpoints();
_root = BuildRoot();
_logger = Logger.GetLogger(typeof(RegionFinder));
}
#endregion
#region Public methods
///
/// Finds the region in the provided endpoint parsing from right to left
/// Try to find exact match of the region in endpoints.json
/// If there doesn't exist an exact match, find a fuzzy match
/// Else return default region
///
/// Endpoint string
/// First successfully parsed region from right to left in the given endpoint or default region
public IRegionEndpoint FindRegion(string endpoint)
{
if (string.IsNullOrEmpty(endpoint))
{
return _root.RegionEndpoint;
}
endpoint = GetAuthority(endpoint.ToLower());
var exactRegion = FindExactRegion(endpoint);
if (exactRegion != null && exactRegion.UseThisValue)
{
return exactRegion.RegionEndpoint;
}
_logger.InfoFormat($"Unable to find exact matched region in endpoint {endpoint}");
var fuzzyRegion = FindFuzzyRegion(endpoint);
if (fuzzyRegion != null)
{
_logger.InfoFormat($"{fuzzyRegion.RegionName} fuzzy region found in endpoint {endpoint}");
return fuzzyRegion;
}
_logger.InfoFormat($"Unable to find fuzzy matched region in endpoint {endpoint}");
// Return the default region
return _root.RegionEndpoint;
}
///
/// Returns the Domain Name System host name
///
/// URL string
/// A String containing the authority component of the URL
public static string GetAuthority(string url)
{
if (string.IsNullOrEmpty(url))
{
return null;
}
var schemeEndIndex = url.IndexOf("://", StringComparison.Ordinal);
if (schemeEndIndex != -1)
{
url = url.Substring(schemeEndIndex + 3);
}
var hostEndIndex = url.IndexOf("/", StringComparison.Ordinal);
if (hostEndIndex != -1)
{
url = url.Substring(0, hostEndIndex);
}
return url;
}
///
/// Find region in the endpoint using endpoints.json region regexs
/// If there doesn't exist a match, return null
///
///
/// First matched region from right to left in the given endpoint or null
public IRegionEndpoint FindFuzzyRegion(string endpoint)
{
foreach (var regionRegex in _regionEndpointProviderV3.AllRegionRegex)
{
// A typical region regex looks like "^(us|eu|ap|sa|ca|me|af)\\-\\w+\\-\\d+$"
// Remove the start (^) and end ($) keyword to allow regex matching without defined start and end pattern
var trimmedRegionRegex = regionRegex.Trim('^', '$');
var match = Regex.Match(endpoint, trimmedRegionRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft);
if (match.Success)
{
return new RegionEndpointProviderV2.RegionEndpoint(match.Value, "Unknown");
}
}
return null;
}
#endregion
#region Internal methods
///
/// Find endpoint segment in the endpoint parsing right to left
/// If there exists an exception such as us-gov, return exception value
/// Else return null
///
/// Endpoint string
/// First parsed region from right to left in the given endpoint or null
internal EndpointSegment FindExactRegion(string endpoint)
{
var segments = endpoint.Split('.');
return FindExactRegion(segments, segments.Length - 1, _root);
}
#endregion
#region Private methods
private Dictionary BuildRegionEndpoints()
{
var allRegionEndpoints = new Dictionary();
foreach (var regionEndpoint in _regionEndpointProviderV3.AllRegionEndpoints)
{
allRegionEndpoints[regionEndpoint.RegionName] = regionEndpoint;
}
return allRegionEndpoints;
}
///
/// Builds an exception tree root that is used to handle the exception cases for an endpoint to determine the region.
/// New exceptions must be added as a child to the root.
/// If there exists a sub-exception that depends on the parent exception, it must be added as a child to the parent node
/// For example, us-gov followed by s-accelerate from right to left, then us-gov must have s3-accelerate as a Child
///
/// Root of exception tree
private EndpointSegment BuildRoot()
{
return new EndpointSegment()
{
Children = new List()
{
new EndpointSegment()
{
Value = "s3-accelerate",
RegionEndpoint = null,
UseThisValue = true,
},
new EndpointSegment()
{
Value = "us-gov",
RegionEndpoint = _regionEndpoints[DefaultGovRegion],
UseThisValue = true
}
},
RegionEndpoint = _regionEndpoints[DefaultRegion]
};
}
private EndpointSegment FindExactRegion(IList segments, int segmentIndex, EndpointSegment currentEndpointSegment)
{
// Return null if there doesn't exist a matching region
if (segmentIndex < 0)
{
return null;
}
var segment = segments[segmentIndex];
// Move down in the tree, if there exists a child node
var nextEndpointSegment = currentEndpointSegment.Children.FirstOrDefault(endpointSegment => endpointSegment.Value.Equals(segment));
if (nextEndpointSegment != null)
{
currentEndpointSegment = nextEndpointSegment;
}
// Return the value of node if exception is configured with return value
if (currentEndpointSegment.UseThisValue)
{
return currentEndpointSegment;
}
// Check for the region
var valueToCheck = string.Empty;
var dashedSegments = segment.Split('-');
for (var dashedSegmentIndex = dashedSegments.Length - 1; dashedSegmentIndex >= 0; dashedSegmentIndex--)
{
valueToCheck = string.IsNullOrEmpty(valueToCheck) ? dashedSegments[dashedSegmentIndex] : $"{dashedSegments[dashedSegmentIndex]}-{valueToCheck}";
if (_regionEndpoints.ContainsKey(valueToCheck))
{
return new EndpointSegment()
{
RegionEndpoint = _regionEndpoints[valueToCheck],
UseThisValue = true
};
}
}
return FindExactRegion(segments, segmentIndex - 1, currentEndpointSegment);
}
#endregion
private static readonly RegionFinder _instance = new RegionFinder();
private bool disposedValue;
///
/// Gets the singleton.
///
public static RegionFinder Instance
{
get
{
return _instance;
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if(_regionEndpointProviderV3 != null)
{
_regionEndpointProviderV3.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}