diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2008-11-23 06:41:29 -0800 |
---|---|---|
committer | Andrew <andrewarnott@gmail.com> | 2008-11-23 06:41:29 -0800 |
commit | 328caf109416c6d06effd937ed5303d94b334ddc (patch) | |
tree | 4274f09a6e259eb7bbebdd8979e4b4f5710194c8 /src | |
parent | e9379729042a5ca7312df00d81f42370a83df0d1 (diff) | |
download | DotNetOpenAuth-328caf109416c6d06effd937ed5303d94b334ddc.zip DotNetOpenAuth-328caf109416c6d06effd937ed5303d94b334ddc.tar.gz DotNetOpenAuth-328caf109416c6d06effd937ed5303d94b334ddc.tar.bz2 |
Added Identifier classes.
Discovery is still #ifdef'd out.
Diffstat (limited to 'src')
-rw-r--r-- | src/DotNetOpenAuth/DotNetOpenAuth.csproj | 4 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Identifier.cs | 164 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs | 4 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs | 44 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs | 27 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/OpenIdStrings.resx | 9 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Realm.cs | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/UriIdentifier.cs | 284 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/XriIdentifier.cs | 139 |
9 files changed, 674 insertions, 3 deletions
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 0d143eb..fa97e8d 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -163,7 +163,9 @@ <Compile Include="OpenId\ChannelElements\OpenIdChannel.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdMessageFactory.cs" /> <Compile Include="OpenId\Configuration.cs" /> + <Compile Include="OpenId\Identifier.cs" /> <Compile Include="OpenId\Messages\CheckIdRequest.cs" /> + <Compile Include="OpenId\NoDiscoveryIdentifier.cs" /> <Compile Include="OpenId\Realm.cs" /> <Compile Include="OpenId\RelyingPartyDescription.cs" /> <Compile Include="OpenId\DiffieHellmanUtilities.cs" /> @@ -202,6 +204,8 @@ <Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" /> <Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" /> <Compile Include="OpenId\SecuritySettings.cs" /> + <Compile Include="OpenId\UriIdentifier.cs" /> + <Compile Include="OpenId\XriIdentifier.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="OAuth\Messages\UnauthorizedTokenRequest.cs" /> <Compile Include="OAuth\ChannelElements\RsaSha1SigningBindingElement.cs" /> diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs new file mode 100644 index 0000000..2eff359 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -0,0 +1,164 @@ +//----------------------------------------------------------------------- +// <copyright file="Identifier.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An Identifier is either a "http" or "https" URI, or an XRI. + /// </summary> + [Serializable] + public abstract class Identifier { + /// <summary> + /// Constructs an <see cref="Identifier"/>. + /// </summary> + /// <param name="isDiscoverySecureEndToEnd"> + /// Whether the derived class is prepared to guarantee end-to-end discovery + /// and initial redirect for authentication is performed using SSL. + /// </param> + protected Identifier(bool isDiscoverySecureEndToEnd) { + IsDiscoverySecureEndToEnd = isDiscoverySecureEndToEnd; + } + + /// <summary> + /// Whether this Identifier will ensure SSL is used throughout the discovery phase + /// and initial redirect of authentication. + /// </summary> + /// <remarks> + /// If this is False, a value of True may be obtained by calling <see cref="TryRequireSsl"/>. + /// </remarks> + protected internal bool IsDiscoverySecureEndToEnd { get; private set; } + + /// <summary> + /// Converts the string representation of an Identifier to its strong type. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates"), SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")] + public static implicit operator Identifier(string identifier) { + if (identifier == null) return null; + return Parse(identifier); + } + /// <summary> + /// Returns a strongly-typed Identifier for a given Uri. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates")] + public static implicit operator Identifier(Uri identifier) { + if (identifier == null) return null; + return new UriIdentifier(identifier); + } + /// <summary> + /// Converts an Identifier to its string representation. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates")] + public static implicit operator string(Identifier identifier) { + if (identifier == null) return null; + return identifier.ToString(); + } + /// <summary> + /// Parses an identifier string and automatically determines + /// whether it is an XRI or URI. + /// </summary> + /// <param name="identifier">Either a URI or XRI identifier.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] + public static Identifier Parse(string identifier) { + if (string.IsNullOrEmpty(identifier)) throw new ArgumentNullException("identifier"); + if (XriIdentifier.IsValidXri(identifier)) { + return new XriIdentifier(identifier); + } else { + return new UriIdentifier(identifier); + } + } + /// <summary> + /// Attempts to parse a string for an OpenId Identifier. + /// </summary> + /// <param name="value">The string to be parsed.</param> + /// <param name="result">The parsed Identifier form.</param> + /// <returns> + /// True if the operation was successful. False if the string was not a valid OpenId Identifier. + /// </returns> + public static bool TryParse(string value, out Identifier result) { + if (IsValid(value)) { + result = Parse(value); + return true; + } else { + result = null; + return false; + } + } + /// <summary> + /// Gets whether a given string represents a valid Identifier format. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] + public static bool IsValid(string identifier) { + return XriIdentifier.IsValidXri(identifier) || UriIdentifier.IsValidUri(identifier); + } + +#if DISCOVERY // TODO: Add discovery and then re-enable this code block + /// <summary> + /// Performs discovery on the Identifier. + /// </summary> + /// <returns> + /// An initialized structure containing the discovered provider endpoint information. + /// </returns> + internal abstract IEnumerable<ServiceEndpoint> Discover(); +#endif + + /// <summary> + /// Tests equality between two <see cref="Identifier"/>s. + /// </summary> + public static bool operator ==(Identifier id1, Identifier id2) { + if ((object)id1 == null ^ (object)id2 == null) return false; + if ((object)id1 == null) return true; + return id1.Equals(id2); + } + /// <summary> + /// Tests inequality between two <see cref="Identifier"/>s. + /// </summary> + public static bool operator !=(Identifier id1, Identifier id2) { + return !(id1 == id2); + } + /// <summary> + /// Tests equality between two <see cref="Identifier"/>s. + /// </summary> + public override bool Equals(object obj) { + Debug.Fail("This should be overridden in every derived class."); + return base.Equals(obj); + } + /// <summary> + /// Gets the hash code for an <see cref="Identifier"/> for storage in a hashtable. + /// </summary> + public override int GetHashCode() { + Debug.Fail("This should be overridden in every derived class."); + return base.GetHashCode(); + } + + /// <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> + internal abstract Identifier TrimFragment(); + + /// <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 abstract bool TryRequireSsl(out Identifier secureIdentifier); + } +} diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs index 034dd65..46a32f6 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs @@ -43,7 +43,7 @@ namespace DotNetOpenAuth.OpenId.Messages { /// <para>It is RECOMMENDED that OPs accept XRI identifiers with or without the "xri://" prefix, as specified in the Normalization (Normalization) section. </para> /// </remarks> [MessagePart("openid.claimed_id", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] - internal string ClaimedIdentifier { get; set; } + internal Identifier ClaimedIdentifier { get; set; } /// <summary> /// Gets or sets the OP Local Identifier. @@ -60,7 +60,7 @@ namespace DotNetOpenAuth.OpenId.Messages { /// see openid.claimed_id above). </para> /// </remarks> [MessagePart("openid.identity", IsRequired = false, AllowEmpty = false)] - internal string LocalIdentifier { get; set; } + internal Identifier LocalIdentifier { get; set; } /// <summary> /// Gets or sets the handle of the association the RP would like the Provider diff --git a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs new file mode 100644 index 0000000..9a6d0ac --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs @@ -0,0 +1,44 @@ +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Wraps an existing Identifier and prevents it from performing discovery. + /// </summary> + class NoDiscoveryIdentifier : Identifier { + Identifier wrappedIdentifier; + internal NoDiscoveryIdentifier(Identifier wrappedIdentifier) + : base(false) { + if (wrappedIdentifier == null) throw new ArgumentNullException("wrappedIdentifier"); + + this.wrappedIdentifier = wrappedIdentifier; + } + +#if DISCOVERY // TODO: Add discovery and then re-enable this code block + internal override IEnumerable<ServiceEndpoint> Discover() { + return new ServiceEndpoint[0]; + } +#endif + + internal override Identifier TrimFragment() { + return new NoDiscoveryIdentifier(wrappedIdentifier.TrimFragment()); + } + + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + return wrappedIdentifier.TryRequireSsl(out secureIdentifier); + } + + public override string ToString() { + return wrappedIdentifier.ToString(); + } + + public override bool Equals(object obj) { + return wrappedIdentifier.Equals(obj); + } + + public override int GetHashCode() { + return wrappedIdentifier.GetHashCode(); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index d6b6de0..9dca2a8 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -106,6 +106,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to URI is not SSL yet requireSslDiscovery is set to true.. + /// </summary> + internal static string ExplicitHttpUriSuppliedWithSslRequirement { + get { + return ResourceManager.GetString("ExplicitHttpUriSuppliedWithSslRequirement", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}'). /// </summary> internal static string InvalidCharacterInKeyValueFormInput { @@ -133,6 +142,24 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to The value '{0}' is not a valid URI.. + /// </summary> + internal static string InvalidUri { + get { + return ResourceManager.GetString("InvalidUri", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Not a recognized XRI format: '{0}'.. + /// </summary> + internal static string InvalidXri { + get { + return ResourceManager.GetString("InvalidXri", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The list of keys do not match the provided dictionary.. /// </summary> internal static string KeysListAndDictionaryDoNotMatch { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index aae4bad..706d7e4 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -132,6 +132,9 @@ <data name="DiffieHellmanRequiredPropertiesNotSet" xml:space="preserve"> <value>The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}</value> </data> + <data name="ExplicitHttpUriSuppliedWithSslRequirement" xml:space="preserve"> + <value>URI is not SSL yet requireSslDiscovery is set to true.</value> + </data> <data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve"> <value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}')</value> </data> @@ -141,6 +144,12 @@ <data name="InvalidScheme" xml:space="preserve"> <value>The scheme must be http or https but was '{0}'.</value> </data> + <data name="InvalidUri" xml:space="preserve"> + <value>The value '{0}' is not a valid URI.</value> + </data> + <data name="InvalidXri" xml:space="preserve"> + <value>Not a recognized XRI format: '{0}'.</value> + </data> <data name="KeysListAndDictionaryDoNotMatch" xml:space="preserve"> <value>The list of keys do not match the provided dictionary.</value> </data> diff --git a/src/DotNetOpenAuth/OpenId/Realm.cs b/src/DotNetOpenAuth/OpenId/Realm.cs index 80c0188..ff38291 100644 --- a/src/DotNetOpenAuth/OpenId/Realm.cs +++ b/src/DotNetOpenAuth/OpenId/Realm.cs @@ -273,7 +273,7 @@ namespace DotNetOpenAuth.OpenId { || url.PathAndQuery[path_len] == '/'; } -#if TODO // TODO: Add discovery and then re-enable this code block +#if DISCOVERY // TODO: Add discovery and then re-enable this code block /// <summary> /// Searches for an XRDS document at the realm URL, and if found, searches /// for a description of a relying party endpoints (OpenId login pages). diff --git a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs new file mode 100644 index 0000000..d4cba3a --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs @@ -0,0 +1,284 @@ +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using System.Web.UI.HtmlControls; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A URI style of OpenID Identifier. + /// </summary> + [Serializable] + public sealed class UriIdentifier : Identifier { + static readonly string[] allowedSchemes = { "http", "https" }; + /// <summary> + /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance. + /// </summary> + 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> + public static implicit operator UriIdentifier(Uri identifier) { + if (identifier == null) return null; + return new UriIdentifier(identifier); + } + + internal UriIdentifier(string uri) : this(uri, false) { } + internal UriIdentifier(string uri, bool requireSslDiscovery) + : base(requireSslDiscovery) { + if (string.IsNullOrEmpty(uri)) throw new ArgumentNullException("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); + } + Uri = canonicalUri; + SchemeImplicitlyPrepended = schemePrepended; + } + internal UriIdentifier(Uri uri) : this(uri, false) { } + internal UriIdentifier(Uri uri, bool requireSslDiscovery) + : base(requireSslDiscovery) { + if (uri == null) throw new ArgumentNullException("uri"); + if (!TryCanonicalize(new UriBuilder(uri), out uri)) + throw new UriFormatException(); + if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException(OpenIdStrings.ExplicitHttpUriSuppliedWithSslRequirement); + } + Uri = uri; + SchemeImplicitlyPrepended = false; + } + + internal Uri Uri { get; private set; } + /// <summary> + /// Gets 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; } + + static bool isAllowedScheme(string uri) { + if (string.IsNullOrEmpty(uri)) return false; + return Array.FindIndex(allowedSchemes, s => uri.StartsWith( + s + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase)) >= 0; + } + static bool isAllowedScheme(Uri uri) { + if (uri == null) return false; + return Array.FindIndex(allowedSchemes, s => + uri.Scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) >= 0; + } + static bool TryCanonicalize(string uri, out Uri canonicalUri, bool forceHttpsDefaultScheme, out bool schemePrepended) { + canonicalUri = null; + schemePrepended = false; + try { + // 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; + } + // Use a UriBuilder because it helps to normalize the URL as well. + return TryCanonicalize(new UriBuilder(uri), out canonicalUri); + } catch (UriFormatException) { + // We try not to land here with checks in the try block, but just in case. + return false; + } + } +#if UNUSED + static bool TryCanonicalize(string uri, out string canonicalUri) { + Uri normalizedUri; + bool result = TryCanonicalize(uri, out normalizedUri); + canonicalUri = normalizedUri.AbsoluteUri; + return result; + } +#endif + /// <summary> + /// Removes the fragment from a URL and sets the host to lowercase. + /// </summary> + /// <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> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + static bool TryCanonicalize(UriBuilder uriBuilder, out Uri canonicalUri) { + uriBuilder.Host = uriBuilder.Host.ToLowerInvariant(); + canonicalUri = uriBuilder.Uri; + return true; + } + internal static bool IsValidUri(string uri) { + Uri normalized; + bool schemePrepended; + return TryCanonicalize(uri, out normalized, false, out schemePrepended); + } + internal static bool IsValidUri(Uri uri) { + if (uri == null) return false; + if (!uri.IsAbsoluteUri) return false; + if (!isAllowedScheme(uri)) return false; + return true; + } + +#if DISCOVERY // TODO: Add discovery and then re-enable this code block + /// <summary> + /// Searches HTML for the HEAD META tags that describe OpenID provider services. + /// </summary> + /// <param name="claimedIdentifier"> + /// The final URL that provided this HTML document. + /// This may not be the same as (this) userSuppliedIdentifier if the + /// userSuppliedIdentifier pointed to a 301 Redirect. + /// </param> + /// <param name="html">The HTML that was downloaded and should be searched.</param> + /// <returns> + /// An initialized ServiceEndpoint if the OpenID Provider information was + /// found. Otherwise null. + /// </returns> + /// <remarks> + /// OpenID 2.0 tags are always used if they are present, otherwise + /// OpenID 1.x tags are used if present. + /// </remarks> + private static ServiceEndpoint DiscoverFromHtml(Uri claimedIdentifier, string html) { + Uri providerEndpoint = null; + Protocol discoveredProtocol = null; + Identifier providerLocalIdentifier = null; + var linkTags = new List<HtmlLink>(Yadis.HtmlParser.HeadTags<HtmlLink>(html)); + foreach (var protocol in Protocol.AllVersions) { + foreach (var linkTag in linkTags) { + // rel attributes are supposed to be interpreted with case INsensitivity, + // and is a space-delimited list of values. (http://www.htmlhelp.com/reference/html40/values.html#linktypes) + if (Regex.IsMatch(linkTag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryProviderKey) + @"\b", RegexOptions.IgnoreCase)) { + if (Uri.TryCreate(linkTag.Href, UriKind.Absolute, out providerEndpoint)) { + discoveredProtocol = protocol; + break; + } + } + } + if (providerEndpoint != null) break; + } + if (providerEndpoint == null) + return null; // html did not contain openid.server link + // See if a LocalId tag of the discovered version exists + foreach (var linkTag in linkTags) { + if (Regex.IsMatch(linkTag.Attributes["rel"], @"\b" + Regex.Escape(discoveredProtocol.HtmlDiscoveryLocalIdKey) + @"\b", RegexOptions.IgnoreCase)) { + if (Identifier.IsValid(linkTag.Href)) { + providerLocalIdentifier = linkTag.Href; + break; + } else { + Logger.WarnFormat("Skipping endpoint data because local id is badly formed ({0}).", linkTag.Href); + return null; // badly formed URL used as LocalId + } + } + } + + // Choose the TypeURI to match the OpenID version detected. + string[] typeURIs = { discoveredProtocol.ClaimedIdentifierServiceTypeURI }; + return ServiceEndpoint.CreateForClaimedIdentifier(claimedIdentifier, providerLocalIdentifier, + providerEndpoint, typeURIs, (int?)null, (int?)null); + } + + internal override IEnumerable<ServiceEndpoint> Discover() { + List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>(); + // Attempt YADIS discovery + DiscoveryResult yadisResult = Yadis.Yadis.Discover(this, IsDiscoverySecureEndToEnd); + if (yadisResult != null) { + if (yadisResult.IsXrds) { + XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); + var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri); + // Filter out insecure endpoints if high security is required. + if (IsDiscoverySecureEndToEnd) { + xrdsEndpoints = Util.Where(xrdsEndpoints, se => se.IsSecure); + } + endpoints.AddRange(xrdsEndpoints); + } + // Failing YADIS discovery of an XRDS document, we try HTML discovery. + if (endpoints.Count == 0) { + ServiceEndpoint ep = DiscoverFromHtml(yadisResult.NormalizedUri, yadisResult.ResponseText); + if (ep != null) { + Logger.Debug("HTML discovery found a service endpoint."); + Logger.Debug(ep); + if (!IsDiscoverySecureEndToEnd || ep.IsSecure) { + endpoints.Add(ep); + } else { + Logger.Info("Skipping HTML discovered endpoint because it is not secure."); + } + } else { + Logger.Debug("HTML discovery failed to find any endpoints."); + } + } else { + Logger.Debug("Skipping HTML discovery because XRDS contained service endpoints."); + } + } + return endpoints; + } +#endif + + 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. + UriBuilder builder = new UriBuilder(Uri); + builder.Fragment = null; + return builder.Uri; + } + + 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 (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); + return false; + } + + /// <summary> + /// Tests equality between this URI and another URI. + /// </summary> + public override bool Equals(object obj) { + UriIdentifier other = obj as UriIdentifier; + if (other == null) return false; + return this.Uri == other.Uri; + } + + /// <summary> + /// Returns the hash code of this XRI. + /// </summary> + public override int GetHashCode() { + return Uri.GetHashCode(); + } + + /// <summary> + /// Returns the string form of the URI. + /// </summary> + public override string ToString() { + return Uri.AbsoluteUri; + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs new file mode 100644 index 0000000..663bb49 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs @@ -0,0 +1,139 @@ +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Xml; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An XRI style of OpenID Identifier. + /// </summary> + [Serializable] + public sealed class XriIdentifier : Identifier { + internal static readonly char[] GlobalContextSymbols = { '=', '@', '+', '$', '!' }; + const string xriScheme = "xri://"; + + internal XriIdentifier(string xri) : this(xri, false) { } + internal XriIdentifier(string xri, bool requireSsl) + : base(requireSsl) { + if (!IsValidXri(xri)) + throw new FormatException(string.Format(CultureInfo.CurrentCulture, + OpenIdStrings.InvalidXri, xri)); + xriResolverProxy = xriResolverProxyTemplate; + if (requireSsl) { + // Indicate to xri.net that we require SSL to be used for delegated resolution + // of community i-names. + xriResolverProxy += ";https=true"; + } + OriginalXri = xri; + CanonicalXri = canonicalizeXri(xri); + } + + /// <summary> + /// The original XRI supplied to the constructor. + /// </summary> + internal string OriginalXri { get; private set; } + /// <summary> + /// The canonical form of the XRI string. + /// </summary> + internal string CanonicalXri { get; private set; } + + /// <summary> + /// Tests whether a given string represents a valid XRI format. + /// </summary> + internal static bool IsValidXri(string xri) { + if (string.IsNullOrEmpty(xri)) throw new ArgumentNullException("xri"); + // TODO: better validation code here + return xri.IndexOfAny(GlobalContextSymbols) == 0 + || xri.StartsWith("(", StringComparison.Ordinal) + || xri.StartsWith(xriScheme, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Takes any valid form of XRI string and returns the canonical form of the same XRI. + /// </summary> + static string canonicalizeXri(string xri) { + xri = xri.Trim(); + if (xri.StartsWith(xriScheme, StringComparison.OrdinalIgnoreCase)) + xri = xri.Substring(xriScheme.Length); + return xri; + } + + /// <summary> + /// The magic URL that will provide us an XRDS document for a given XRI identifier. + /// </summary> + /// <remarks> + /// We use application/xrd+xml instead of application/xrds+xml because it gets + /// xri.net to automatically give us exactly the right XRD element for community i-names + /// automatically, saving us having to choose which one to use out of the result. + /// The ssl=true parameter tells the proxy resolver to accept only SSL connections + /// when resolving community i-names. + /// </remarks> + const string xriResolverProxyTemplate = "https://xri.net/{0}?_xrd_r=application/xrd%2Bxml;sep=false"; + readonly string xriResolverProxy; + /// <summary> + /// Resolves the XRI to a URL from which an XRDS document may be downloaded. + /// </summary> + private Uri XrdsUrl { + get { + return new Uri(string.Format(CultureInfo.InvariantCulture, + xriResolverProxy, this)); + } + } + +#if DISCOVERY // TODO: Add discovery and then re-enable this code block + XrdsDocument downloadXrds() { + var xrdsResponse = UntrustedWebRequest.Request(XrdsUrl); + XrdsDocument doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream)); + if (!doc.IsXrdResolutionSuccessful) { + throw new OpenIdException(Strings.XriResolutionFailed); + } + return doc; + } + + internal override IEnumerable<ServiceEndpoint> Discover() { + return downloadXrds().CreateServiceEndpoints(this); + } + + /// <summary> + /// Performs discovery on THIS identifier, but generates <see cref="ServiceEndpoint"/> + /// instances that treat another given identifier as the user-supplied identifier. + /// </summary> + internal IEnumerable<ServiceEndpoint> Discover(XriIdentifier userSuppliedIdentifier) { + return downloadXrds().CreateServiceEndpoints(userSuppliedIdentifier); + } +#endif + + internal override Identifier TrimFragment() { + return this; + } + + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + secureIdentifier = IsDiscoverySecureEndToEnd ? this : new XriIdentifier(this, true); + return true; + } + + /// <summary> + /// Tests equality between this XRI and another XRI. + /// </summary> + public override bool Equals(object obj) { + XriIdentifier other = obj as XriIdentifier; + if (other == null) return false; + return this.CanonicalXri == other.CanonicalXri; + } + + /// <summary> + /// Returns the hash code of this XRI. + /// </summary> + public override int GetHashCode() { + return CanonicalXri.GetHashCode(); + } + + /// <summary> + /// Returns the canonical string form of the XRI. + /// </summary> + public override string ToString() { + return CanonicalXri; + } + } +} |