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