/*******************************************************************************
* 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.
* *****************************************************************************
* __ _ _ ___
* ( )( \/\/ )/ __)
* /__\ \ / \__ \
* (_)(_) \/\/ (___/
*
* AWS SDK for .NET
*
*/
using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace Amazon.S3.Util
{
///
/// Uri wrapper that can parse out information (bucket, key, region, style) from an
/// S3 URI.
///
public class AmazonS3Uri
{
private static readonly Regex EndpointRegexMatch = new Regex(@"^(.+\.)?s3[.-]([a-z0-9-]+)\.", RegexOptions.Compiled);
///
/// True if the URI contains the bucket in the path, false if it contains the bucket in the authority.
///
public bool IsPathStyle { get; private set; }
///
/// The bucket name parsed from the URI (or null if no bucket specified).
///
public string Bucket { get; private set; }
///
/// The key parsed from the URI (or null if no key specified).
///
public string Key { get; private set; }
///
/// The region parsed from the URI (or null if no region specified).
///
public RegionEndpoint Region { get; set; }
///
/// Constructs a parser for the S3 URI specified as a string.
/// An encoded URI is expected.
///
/// The S3 URI to be parsed.
public AmazonS3Uri(string uri)
: this(new Uri(uri))
{
}
///
/// Constructs a parser for the S3 URI specified as a Uri instance.
/// An encoded URI is expected.
///
/// The S3 URI to be parsed.
public AmazonS3Uri(Uri uri)
{
if (uri == null)
throw new ArgumentNullException("uri");
if (string.IsNullOrEmpty(uri.Host))
throw new ArgumentException("Invalid URI - no hostname present");
if (uri.Scheme == "s3")
{
this.Region = null;
this.IsPathStyle = false;
this.Bucket = uri.Authority;
if (this.Bucket == null)
{
throw new ArgumentException("Invalid S3 URI - no bucket present");
}
this.Key = uri.AbsolutePath.Equals("/") ? null : Decode(uri.AbsolutePath.Substring(1));
return;
}
var match = EndpointRegexMatch.Match(uri.Host);
if (!match.Success)
throw new ArgumentException("Invalid S3 URI - hostname does not appear to be a valid S3 endpoint");
// for host style urls:
// group 0 is bucketname plus 's3' prefix and possible region code
// group 1 is bucket name
// group 2 will be region or 'amazonaws' if US Classic region
// for path style urls:
// group 0 will be s3 prefix plus possible region code
// group 1 will be empty
// group 2 will be region or 'amazonaws' if US Classic region
var bucketNameGroup = match.Groups[1];
if (string.IsNullOrEmpty(bucketNameGroup.Value))
{
// no bucket name in the authority, parse it from the path
this.IsPathStyle = true;
// grab the encoded path so we don't run afoul of '/'s in the bucket name
var path = uri.AbsolutePath;
if (path.Equals("/"))
{
this.Bucket = null;
this.Key = null;
}
else
{
var index = path.IndexOf('/', 1);
if (index == -1)
{
// https://s3.amazonaws.com/bucket
this.Bucket = Decode(path.Substring(1));
this.Key = null;
}
else if (index == (path.Length - 1))
{
// https://s3.amazonaws.com/bucket/
this.Bucket = Decode(path.Substring(1, index)).TrimEnd('/');
this.Key = null;
}
else
{
// https://s3.amazonaws.com/bucket/key
this.Bucket = Decode(path.Substring(1, index)).TrimEnd('/');
this.Key = Decode(path.Substring(index + 1));
}
}
}
else
{
// bucket name in the host, path is the object key
this.IsPathStyle = false;
// remove any trailing '.' from the prefix to get the bucket name
this.Bucket = bucketNameGroup.Value.TrimEnd('.');
this.Key = uri.AbsolutePath.Equals("/") ? null : Decode(uri.AbsolutePath.Substring(1));
}
if (match.Groups.Count > 2)
{
// US 'classic' urls will not have a region code in the endpoint
var regionGroupValue = match.Groups[2].Value;
if (regionGroupValue.Equals("amazonaws", StringComparison.Ordinal)
|| regionGroupValue.Equals("external-1", StringComparison.Ordinal))
this.Region = RegionEndpoint.USEast1;
else
{
try
{
this.Region = RegionEndpoint.GetBySystemName(regionGroupValue);
}
catch (Amazon.Runtime.AmazonClientException)
{
// in cases where endpoints such as "s3-accelerate" matches,
// just set the region to null and move on.
this.Region = null;
}
}
}
}
///
/// Constructs a parser for the S3 URI specified as a string.
/// An encoded URI is expected.
///
/// The S3 URI to be parsed.
/// Flag indicating if URI string should be preprocessed to decode certain characters.
public AmazonS3Uri(string uri, bool decode)
: this(decode ? EscapeSpecialControlCharacters(uri) : uri)
{
}
///
/// If the given string is an AmazonS3Endpoint return true and set the AmazonS3Uri out parameter.
///
///
///
/// true if the string is an AmazonS3Endpoint, and the out paramter has been filled in, false otherwise
public static bool TryParseAmazonS3Uri(string uri, out AmazonS3Uri amazonS3Uri)
{
try
{
return TryParseAmazonS3Uri(new Uri(uri), out amazonS3Uri);
}
catch (Exception)
{
amazonS3Uri = null;
return false;
}
}
///
/// If the given string is an AmazonS3Endpoint return true and set the AmazonS3Uri out parameter.
///
///
/// Flag indicating if URI string should be preprocessed to decode certain characters.
///
/// true if the string is an AmazonS3Endpoint, and the out paramter has been filled in, false otherwise
public static bool TryParseAmazonS3Uri(string uri, bool decode, out AmazonS3Uri amazonS3Uri)
{
if (decode)
uri = EscapeSpecialControlCharacters(uri);
return TryParseAmazonS3Uri(new Uri(uri), out amazonS3Uri);
}
///
/// If the given Uri is an AmazonS3Endpoint return true and set the AmazonS3Uri out parameter.
///
///
///
/// true if the Uri is an AmazonS3Endpoint, and the out paramter has been filled in, false otherwise
public static bool TryParseAmazonS3Uri(Uri uri, out AmazonS3Uri amazonS3Uri)
{
try
{
if (IsAmazonS3Endpoint(uri))
{
amazonS3Uri = new AmazonS3Uri(uri);
return true;
}
}
// intentionally suppress all exceptions, because this is a try method
#pragma warning disable CA1031 // Do not catch general exception types
catch { }
#pragma warning restore CA1031 // Do not catch general exception types
amazonS3Uri = null;
return false;
}
///
/// Checks whether the given URI is a Amazon S3 URI.
///
/// The S3 URI to be checked.
/// true if the URI is a Amazon S3 URI, false; otherwise.
public static bool IsAmazonS3Endpoint(string uri)
{
if (uri == null)
throw new ArgumentNullException("uri");
return IsAmazonS3Endpoint(new Uri(uri));
}
///
/// Checks whether the given URI is a Amazon S3 URI.
///
/// The S3 URI to be checked.
/// true if the URI is a Amazon S3 URI, false; otherwise.
public static bool IsAmazonS3Endpoint(Uri uri)
{
if (uri == null)
throw new ArgumentNullException("uri");
if (uri.IsAbsoluteUri && (uri.Host.EndsWith("amazonaws.com", StringComparison.OrdinalIgnoreCase) || uri.Host.EndsWith("amazonaws.com.cn", StringComparison.OrdinalIgnoreCase)))
{
return EndpointRegexMatch.Match(uri.Host).Success;
}
else if (uri.IsAbsoluteUri && uri.Scheme == "s3") // For S3 scheme URI, URI Authority is bucket name
{
return !string.IsNullOrEmpty(uri.Authority);
}
else
{
return false;
}
}
///
/// Percent-decodes the given string, with a fast path for strings that are not
/// percent-encoded.
///
/// The string to decode
/// The decoded string
static string Decode(string s)
{
if (s == null)
return null;
for (var i = 0; i < s.Length; ++i)
{
if (s[i] == '%')
return Decode(s, i);
}
return s;
}
///
/// Percent-decodes the given string.
///
/// The string to decode
/// The index of the first '%' in the string
/// The decoded string
static string Decode(string s, int firstPercent)
{
var sb = new StringBuilder(s.Substring(0, firstPercent));
AppendDecoded(sb, s, firstPercent);
for (var i = firstPercent + 3; i < s.Length; ++i)
{
if (s[i] == '%')
{
AppendDecoded(sb, s, i);
i += 2;
}
else
sb.Append(s[i]);
}
return sb.ToString();
}
///
/// Decodes the percent-encoded character at the given index in the string
/// and appends the decoded value to the string under construction.
///
///
/// The string under construction to which the decoded character will be
/// appended.
///
/// The string being decoded.
/// The index of the '%' character in the string.
static void AppendDecoded(StringBuilder builder, string s, int index)
{
if (index > s.Length - 3)
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Invalid percent-encoded string '{0}'",
s));
var first = s[index + 1];
var second = s[index + 2];
var decoded = (char) (FromHex(first) << 4 | FromHex(second));
builder.Append(decoded);
}
///
/// Converts a hex character (0-9A-Fa-f) into its corresponding quad value.
///
/// The hex character
/// The quad value
static int FromHex(char c)
{
if (c < '0')
{
throw new InvalidOperationException(
"Invalid percent-encoded string: bad character '" + c + "' in "
+ "escape sequence.");
}
if (c <= '9')
{
return (c - '0');
}
if (c < 'A')
{
throw new InvalidOperationException(
"Invalid percent-encoded string: bad character '" + c + "' in "
+ "escape sequence.");
}
if (c <= 'F')
{
return (c - 'A') + 10;
}
if (c < 'a')
{
throw new InvalidOperationException(
"Invalid percent-encoded string: bad character '" + c + "' in "
+ "escape sequence.");
}
if (c <= 'f')
{
return (c - 'a') + 10;
}
throw new InvalidOperationException(
"Invalid percent-encoded string: bad character '" + c + "' in "
+ "escape sequence.");
}
private static string EscapeSpecialControlCharacters(string uri)
{
// Addresses https://github.com/aws/aws-sdk-net/issues/1894. Ports the logic from AmazonS3Uri class in Java V1 SDK.
return uri?.Replace("%3A", ":")
.Replace("%2F", "/")
.Replace("+", "%20");
}
}
}