diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs | 726 |
1 files changed, 726 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs new file mode 100644 index 0000000..aa72869 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs @@ -0,0 +1,726 @@ +//----------------------------------------------------------------------- +// <copyright file="UriIdentifier.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Reflection; + using System.Security; + using System.Text; + using System.Text.RegularExpressions; + using System.Web.UI.HtmlControls; + using System.Xml; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Xrds; + using DotNetOpenAuth.Yadis; + + /// <summary> + /// A URI style of OpenID Identifier. + /// </summary> + [Serializable] + [Pure] + public sealed class UriIdentifier : Identifier { + /// <summary> + /// The allowed protocol schemes in a URI Identifier. + /// </summary> + private static readonly string[] allowedSchemes = { "http", "https" }; + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// A value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + private static bool schemeSubstitution; + + /// <summary> + /// Initializes static members of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <remarks> + /// This method attempts to workaround the .NET Uri class parsing bug described here: + /// https://connect.microsoft.com/VisualStudio/feedback/details/386695/system-uri-incorrectly-strips-trailing-dots?wa=wsignin1.0#tabs + /// since some identifiers (like some of the pseudonymous identifiers from Yahoo) include path segments + /// that end with periods, which the Uri class will typically trim off. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")] + static UriIdentifier() { + // Our first attempt to handle trailing periods in path segments is to leverage + // full trust if it's available to rewrite the rules. + // In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send + // outbound HTTP requests with trailing periods, so it's the only way to perform + // discovery on such an identifier. + try { + UriParser.Register(roundTrippingHttpParser, "dnoarthttp", 80); + UriParser.Register(roundTrippingHttpsParser, "dnoarthttps", 443); + UriParser.Register(publishableHttpParser, "dnoahttp", 80); + UriParser.Register(publishableHttpsParser, "dnoahttps", 443); + roundTrippingHttpParser.Initialize(false); + roundTrippingHttpsParser.Initialize(false); + publishableHttpParser.Initialize(true); + publishableHttpsParser.Initialize(true); + schemeSubstitution = true; + Logger.OpenId.Debug(".NET Uri class path compression overridden."); + Reporting.RecordFeatureUse("FullTrust"); + } catch (SecurityException) { + // We must be running in partial trust. Nothing more we can do. + Logger.OpenId.Warn("Unable to coerce .NET to stop compressing URI paths due to partial trust limitations. Some URL identifiers may be unable to complete login."); + Reporting.RecordFeatureUse("PartialTrust"); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + internal UriIdentifier(string uri) + : this(uri, false) { + Requires.NotNullOrEmpty(uri, "uri"); + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> + internal UriIdentifier(string uri, bool requireSslDiscovery) + : base(uri, requireSslDiscovery) { + Requires.NotNullOrEmpty(uri, "uri"); + Uri canonicalUri; + bool schemePrepended; + if (!TryCanonicalize(uri, out canonicalUri, requireSslDiscovery, out schemePrepended)) { + throw new UriFormatException(); + } + if (requireSslDiscovery && canonicalUri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException(OpenIdStrings.ExplicitHttpUriSuppliedWithSslRequirement); + } + this.Uri = canonicalUri; + this.SchemeImplicitlyPrepended = schemePrepended; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + internal UriIdentifier(Uri uri) + : this(uri, false) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> + internal UriIdentifier(Uri uri, bool requireSslDiscovery) + : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { + Requires.NotNull(uri, "uri"); + + string uriAsString = uri.OriginalString; + if (schemeSubstitution) { + uriAsString = NormalSchemeToSpecialRoundTrippingScheme(uriAsString); + } + + if (!TryCanonicalize(uriAsString, out uri)) { + throw new UriFormatException(); + } + if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException(OpenIdStrings.ExplicitHttpUriSuppliedWithSslRequirement); + } + this.Uri = uri; + this.SchemeImplicitlyPrepended = false; + } + + /// <summary> + /// Gets or sets a value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + internal static bool SchemeSubstitutionTestHook { + get { return schemeSubstitution; } + set { schemeSubstitution = value; } + } + + /// <summary> + /// Gets the URI this instance represents. + /// </summary> + internal Uri Uri { get; private set; } + + /// <summary> + /// Gets a value indicating whether the scheme was missing when this + /// Identifier was created and added automatically as part of the + /// normalization process. + /// </summary> + internal bool SchemeImplicitlyPrepended { get; private set; } + + /// <summary> + /// Gets a value indicating whether this Identifier has characters or patterns that + /// the <see cref="Uri"/> class normalizes away and invalidating the Identifier. + /// </summary> + internal bool ProblematicNormalization { + get { + if (schemeSubstitution) { + // With full trust, we have no problematic URIs + return false; + } + + var simpleUri = new SimpleUri(this.OriginalString); + if (simpleUri.Path.EndsWith(".", StringComparison.Ordinal) || simpleUri.Path.Contains("./")) { + return true; + } + + return false; + } + } + + /// <summary> + /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance. + /// </summary> + /// <param name="identifier">The identifier to convert to an ordinary <see cref="Uri"/> instance.</param> + /// <returns>The result of the conversion.</returns> + public static implicit operator Uri(UriIdentifier identifier) { + if (identifier == null) { + return null; + } + return identifier.Uri; + } + + /// <summary> + /// Converts a <see cref="Uri"/> instance to a <see cref="UriIdentifier"/> instance. + /// </summary> + /// <param name="identifier">The <see cref="Uri"/> instance to turn into a <see cref="UriIdentifier"/>.</param> + /// <returns>The result of the conversion.</returns> + public static implicit operator UriIdentifier(Uri identifier) { + if (identifier == null) { + return null; + } + return new UriIdentifier(identifier); + } + + /// <summary> + /// Tests equality between this URI and another URI. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param> + /// <returns> + /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + UriIdentifier other = obj as UriIdentifier; + if (obj != null && other == null && Identifier.EqualityOnStrings) { // test hook to enable MockIdentifier comparison + other = Identifier.Parse(obj.ToString()) as UriIdentifier; + } + if (other == null) { + return false; + } + + if (this.ProblematicNormalization || other.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).Equals(new SimpleUri(other.OriginalString)); + } else { + return this.Uri == other.Uri; + } + } + + /// <summary> + /// Returns the hash code of this XRI. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return Uri.GetHashCode(); + } + + /// <summary> + /// Returns the string form of the URI. + /// </summary> + /// <returns> + /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. + /// </returns> + public override string ToString() { + if (this.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).ToString(); + } else { + return this.Uri.AbsoluteUri; + } + } + + /// <summary> + /// Determines whether a URI is a valid OpenID Identifier (of any kind). + /// </summary> + /// <param name="uri">The URI to test for OpenID validity.</param> + /// <returns> + /// <c>true</c> if the identifier is valid; otherwise, <c>false</c>. + /// </returns> + /// <remarks> + /// A valid URI is absolute (not relative) and uses an http(s) scheme. + /// </remarks> + internal static bool IsValidUri(string uri) { + Uri normalized; + bool schemePrepended; + return TryCanonicalize(uri, out normalized, false, out schemePrepended); + } + + /// <summary> + /// Determines whether a URI is a valid OpenID Identifier (of any kind). + /// </summary> + /// <param name="uri">The URI to test for OpenID validity.</param> + /// <returns> + /// <c>true</c> if the identifier is valid; otherwise, <c>false</c>. + /// </returns> + /// <remarks> + /// A valid URI is absolute (not relative) and uses an http(s) scheme. + /// </remarks> + internal static bool IsValidUri(Uri uri) { + if (uri == null) { + return false; + } + if (!uri.IsAbsoluteUri) { + return false; + } + if (!IsAllowedScheme(uri)) { + return false; + } + return true; + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns> + /// A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance.. + /// </returns> + internal override Identifier TrimFragment() { + // If there is no fragment, we have no need to rebuild the Identifier. + if (Uri.Fragment == null || Uri.Fragment.Length == 0) { + return this; + } + + // Strip the fragment. + return new UriIdentifier(this.OriginalString.Substring(0, this.OriginalString.IndexOf('#'))); + } + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier">The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints.</param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + // If this Identifier is already secure, reuse it. + if (IsDiscoverySecureEndToEnd) { + secureIdentifier = this; + return true; + } + + // If this identifier already uses SSL for initial discovery, return one + // that guarantees it will be used throughout the discovery process. + if (String.Equals(Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + secureIdentifier = new UriIdentifier(this.Uri, true); + return true; + } + + // Otherwise, try to make this Identifier secure by normalizing to HTTPS instead of HTTP. + if (this.SchemeImplicitlyPrepended) { + UriBuilder newIdentifierUri = new UriBuilder(this.Uri); + newIdentifierUri.Scheme = Uri.UriSchemeHttps; + if (newIdentifierUri.Port == 80) { + newIdentifierUri.Port = 443; + } + secureIdentifier = new UriIdentifier(newIdentifierUri.Uri, true); + return true; + } + + // This identifier is explicitly NOT https, so we cannot change it. + secureIdentifier = new NoDiscoveryIdentifier(this, true); + return false; + } + + /// <summary> + /// Determines whether the given URI is using a scheme in the list of allowed schemes. + /// </summary> + /// <param name="uri">The URI whose scheme is to be checked.</param> + /// <returns> + /// <c>true</c> if the scheme is allowed; otherwise, <c>false</c>. + /// <c>false</c> is also returned if <paramref name="uri"/> is null. + /// </returns> + private static bool IsAllowedScheme(string uri) { + if (string.IsNullOrEmpty(uri)) { + return false; + } + return Array.FindIndex( + allowedSchemes, + s => uri.StartsWith(s + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase)) >= 0; + } + + /// <summary> + /// Determines whether the given URI is using a scheme in the list of allowed schemes. + /// </summary> + /// <param name="uri">The URI whose scheme is to be checked.</param> + /// <returns> + /// <c>true</c> if the scheme is allowed; otherwise, <c>false</c>. + /// <c>false</c> is also returned if <paramref name="uri"/> is null. + /// </returns> + private static bool IsAllowedScheme(Uri uri) { + if (uri == null) { + return false; + } + return Array.FindIndex( + allowedSchemes, + s => uri.Scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) >= 0; + } + + /// <summary> + /// Tries to canonicalize a user-supplied identifier. + /// This does NOT convert a user-supplied identifier to a Claimed Identifier! + /// </summary> + /// <param name="uri">The user-supplied identifier.</param> + /// <param name="canonicalUri">The resulting canonical URI.</param> + /// <param name="forceHttpsDefaultScheme">If set to <c>true</c> and the user-supplied identifier lacks a scheme, the "https://" scheme will be prepended instead of the standard "http://" one.</param> + /// <param name="schemePrepended">if set to <c>true</c> [scheme prepended].</param> + /// <returns> + /// <c>true</c> if the identifier was valid and could be canonicalized. + /// <c>false</c> if the identifier is outside the scope of allowed inputs and should be rejected. + /// </returns> + /// <remarks> + /// Canonicalization is done by adding a scheme in front of an + /// identifier if it isn't already present. Other trivial changes that do not + /// require network access are also done, such as lower-casing the hostname in the URI. + /// </remarks> + private static bool TryCanonicalize(string uri, out Uri canonicalUri, bool forceHttpsDefaultScheme, out bool schemePrepended) { + Requires.NotNullOrEmpty(uri, "uri"); + + canonicalUri = null; + try { + uri = DoSimpleCanonicalize(uri, forceHttpsDefaultScheme, out schemePrepended); + if (schemeSubstitution) { + uri = NormalSchemeToSpecialRoundTrippingScheme(uri); + } + + // Use a UriBuilder because it helps to normalize the URL as well. + return TryCanonicalize(uri, out canonicalUri); + } catch (UriFormatException) { + // We try not to land here with checks in the try block, but just in case. + schemePrepended = false; + return false; + } + } + + /// <summary> + /// Fixes up the scheme if appropriate. + /// </summary> + /// <param name="uri">The URI, already in legal form (with http(s):// prepended if necessary).</param> + /// <param name="canonicalUri">The resulting canonical URI.</param> + /// <returns><c>true</c> if the canonicalization was successful; <c>false</c> otherwise.</returns> + /// <remarks> + /// This does NOT standardize an OpenID URL for storage in a database, as + /// it does nothing to convert the URL to a Claimed Identifier, besides the fact + /// that it only deals with URLs whereas OpenID 2.0 supports XRIs. + /// For this, you should lookup the value stored in IAuthenticationResponse.ClaimedIdentifier. + /// </remarks> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "The user will see the result of this operation and they want to see it in lower case.")] + private static bool TryCanonicalize(string uri, out Uri canonicalUri) { + Requires.NotNull(uri, "uri"); + + if (schemeSubstitution) { + UriBuilder uriBuilder = new UriBuilder(uri); + + // Swap out our round-trippable scheme for the publishable (hidden) scheme. + uriBuilder.Scheme = uriBuilder.Scheme == roundTrippingHttpParser.RegisteredScheme ? publishableHttpParser.RegisteredScheme : publishableHttpsParser.RegisteredScheme; + canonicalUri = uriBuilder.Uri; + } else { + canonicalUri = new Uri(uri); + } + + return true; + } + + /// <summary> + /// Gets the special non-compressing scheme or URL for a standard scheme or URL. + /// </summary> + /// <param name="normal">The ordinary URL or scheme name.</param> + /// <returns>The non-compressing equivalent scheme or URL for the given value.</returns> + private static string NormalSchemeToSpecialRoundTrippingScheme(string normal) { + Requires.NotNullOrEmpty(normal, "normal"); + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + ErrorUtilities.VerifyInternal(schemeSubstitution, "Wrong schemeSubstitution value."); + + int delimiterIndex = normal.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + string normalScheme = delimiterIndex < 0 ? normal : normal.Substring(0, delimiterIndex); + string nonCompressingScheme; + if (string.Equals(normalScheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpParser.RegisteredScheme; + } else if (string.Equals(normalScheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpsParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpsParser.RegisteredScheme; + } else { + throw new NotSupportedException(); + } + + return delimiterIndex < 0 ? nonCompressingScheme : nonCompressingScheme + normal.Substring(delimiterIndex); + } + + /// <summary> + /// Performs the minimal URL normalization to allow a string to be passed to the <see cref="Uri"/> constructor. + /// </summary> + /// <param name="uri">The user-supplied identifier URI to normalize.</param> + /// <param name="forceHttpsDefaultScheme">if set to <c>true</c>, a missing scheme should result in HTTPS being prepended instead of HTTP.</param> + /// <param name="schemePrepended">if set to <c>true</c>, the scheme was prepended during normalization.</param> + /// <returns>The somewhat normalized URL.</returns> + private static string DoSimpleCanonicalize(string uri, bool forceHttpsDefaultScheme, out bool schemePrepended) { + Requires.NotNullOrEmpty(uri, "uri"); + + schemePrepended = false; + uri = uri.Trim(); + + // Assume http:// scheme if an allowed scheme isn't given, and strip + // fragments off. Consistent with spec section 7.2#3 + if (!IsAllowedScheme(uri)) { + uri = (forceHttpsDefaultScheme ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) + + Uri.SchemeDelimiter + uri; + schemePrepended = true; + } + + return uri; + } + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.Uri != null); + Contract.Invariant(this.Uri.AbsoluteUri != null); + } +#endif + + /// <summary> + /// A simple URI class that doesn't suffer from the parsing problems of the <see cref="Uri"/> class. + /// </summary> + internal class SimpleUri { + /// <summary> + /// URI characters that separate the URI Path from subsequent elements. + /// </summary> + private static readonly char[] PathEndingCharacters = new char[] { '?', '#' }; + + /// <summary> + /// Initializes a new instance of the <see cref="SimpleUri"/> class. + /// </summary> + /// <param name="value">The value.</param> + internal SimpleUri(string value) { + Requires.NotNullOrEmpty(value, "value"); + + bool schemePrepended; + value = DoSimpleCanonicalize(value, false, out schemePrepended); + + // Leverage the Uri class's parsing where we can. + Uri uri = new Uri(value); + this.Scheme = uri.Scheme; + this.Authority = uri.Authority; + this.Query = uri.Query; + this.Fragment = uri.Fragment; + + // Get the Path out ourselves, since the default Uri parser compresses it too much for OpenID. + int schemeLength = value.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + Contract.Assume(schemeLength > 0); + int hostStart = schemeLength + Uri.SchemeDelimiter.Length; + int hostFinish = value.IndexOf('/', hostStart); + if (hostFinish < 0) { + this.Path = "/"; + } else { + int pathFinish = value.IndexOfAny(PathEndingCharacters, hostFinish); + Contract.Assume(pathFinish >= hostFinish || pathFinish < 0); + if (pathFinish < 0) { + this.Path = value.Substring(hostFinish); + } else { + this.Path = value.Substring(hostFinish, pathFinish - hostFinish); + } + } + + this.Path = NormalizePathEscaping(this.Path); + } + + /// <summary> + /// Gets the scheme. + /// </summary> + /// <value>The scheme.</value> + public string Scheme { get; private set; } + + /// <summary> + /// Gets the authority. + /// </summary> + /// <value>The authority.</value> + public string Authority { get; private set; } + + /// <summary> + /// Gets the path of the URI. + /// </summary> + /// <value>The path from the URI.</value> + public string Path { get; private set; } + + /// <summary> + /// Gets the query. + /// </summary> + /// <value>The query.</value> + public string Query { get; private set; } + + /// <summary> + /// Gets the fragment. + /// </summary> + /// <value>The fragment.</value> + public string Fragment { get; private set; } + + /// <summary> + /// Returns a <see cref="System.String"/> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="System.String"/> that represents this instance. + /// </returns> + public override string ToString() { + return this.Scheme + Uri.SchemeDelimiter + this.Authority + this.Path + this.Query + this.Fragment; + } + + /// <summary> + /// Determines whether the specified <see cref="System.Object"/> is equal to this instance. + /// </summary> + /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + SimpleUri other = obj as SimpleUri; + if (other == null) { + return false; + } + + // Note that this equality check is intentionally leaving off the Fragment part + // to match Uri behavior, and is intentionally being case sensitive and insensitive + // for different parts. + return string.Equals(this.Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Authority, other.Authority, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Path, other.Path, StringComparison.Ordinal) && + string.Equals(this.Query, other.Query, StringComparison.Ordinal); + } + + /// <summary> + /// Returns a hash code for this instance. + /// </summary> + /// <returns> + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// </returns> + public override int GetHashCode() { + int hashCode = 0; + hashCode += StringComparer.OrdinalIgnoreCase.GetHashCode(this.Scheme); + hashCode += StringComparer.OrdinalIgnoreCase.GetHashCode(this.Authority); + hashCode += StringComparer.Ordinal.GetHashCode(this.Path); + hashCode += StringComparer.Ordinal.GetHashCode(this.Query); + return hashCode; + } + + /// <summary> + /// Normalizes the characters that are escaped in the given URI path. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <returns>The given path, with exactly those characters escaped which should be.</returns> + private static string NormalizePathEscaping(string path) { + Requires.NotNull(path, "path"); + + string[] segments = path.Split('/'); + for (int i = 0; i < segments.Length; i++) { + segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i])); + } + + return string.Join("/", segments); + } + } + + /// <summary> + /// A URI parser that does not compress paths, such as trimming trailing periods from path segments. + /// </summary> + private class NonPathCompressingUriParser : GenericUriParser { + /// <summary> + /// The field that stores the scheme that this parser is registered under. + /// </summary> + private static FieldInfo schemeField; + + /// <summary> + /// The standard "http" or "https" scheme that this parser is subverting. + /// </summary> + private string standardScheme; + + /// <summary> + /// Initializes a new instance of the <see cref="NonPathCompressingUriParser"/> class. + /// </summary> + /// <param name="standardScheme">The standard scheme that this parser will be subverting.</param> + public NonPathCompressingUriParser(string standardScheme) + : base(GenericUriParserOptions.DontCompressPath | GenericUriParserOptions.IriParsing | GenericUriParserOptions.Idn) { + Requires.NotNullOrEmpty(standardScheme, "standardScheme"); + this.standardScheme = standardScheme; + } + + /// <summary> + /// Gets the scheme this parser is registered under. + /// </summary> + /// <value>The registered scheme.</value> + internal string RegisteredScheme { get; private set; } + + /// <summary> + /// Initializes this parser with the actual scheme it should appear to be. + /// </summary> + /// <param name="hideNonStandardScheme">if set to <c>true</c> Uris using this scheme will look like they're using the original standard scheme.</param> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Schemes are traditionally displayed in lowercase.")] + internal void Initialize(bool hideNonStandardScheme) { + if (schemeField == null) { + schemeField = typeof(UriParser).GetField("m_Scheme", BindingFlags.NonPublic | BindingFlags.Instance); + } + + this.RegisteredScheme = (string)schemeField.GetValue(this); + + if (hideNonStandardScheme) { + schemeField.SetValue(this, this.standardScheme.ToLowerInvariant()); + } + } + } + } +} |