/******************************************************************************* * 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.Runtime.Internal.Util { /// /// Uri wrapper that can parse out information (bucket, key, region, style) from an /// S3 URI. /// public class S3Uri { private const string S3EndpointPattern = @"^(.+\.)?s3[.-]([a-z0-9-]+)\."; //s3-control has a similar pattern to s3-region host names, so we explicitly exclude it private const string S3ControlExlusionPattern = @"^(.+\.)?s3-control\."; private static readonly Regex S3EndpointRegex = new Regex(S3EndpointPattern, RegexOptions.Compiled); private static readonly Regex S3ControlExlusionRegex = new Regex(S3ControlExlusionPattern, 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. /// /// The S3 URI to be parsed. public S3Uri(string uri) : this(new Uri(uri)) { } /// /// Constructs a parser for the S3 URI specified as a Uri instance. /// /// The S3 URI to be parsed. public S3Uri(Uri uri) { if (uri == null) throw new ArgumentNullException("uri"); if (string.IsNullOrEmpty(uri.Host)) throw new ArgumentException("Invalid URI - no hostname present"); var match = S3EndpointRegex.Match(uri.Host); if (!match.Success && !S3ControlExlusionRegex.Match(uri.Host).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 : 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 this.Region = RegionEndpoint.GetBySystemName(regionGroupValue); } } public static bool IsS3Uri(Uri uri) { return S3EndpointRegex.Match(uri.Host).Success && !S3ControlExlusionRegex.Match(uri.Host).Success; } /// /// 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."); } } }