summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth')
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj26
-rw-r--r--src/DotNetOpenAuth/Messaging/Channel.cs6
-rw-r--r--src/DotNetOpenAuth/Messaging/DirectWebResponse.cs73
-rw-r--r--src/DotNetOpenAuth/Messaging/ErrorUtilities.cs2
-rw-r--r--src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs (renamed from src/DotNetOpenAuth/Messaging/IWebRequestHandler.cs)10
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs36
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingStrings.resx12
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs65
-rw-r--r--src/DotNetOpenAuth/Messaging/Response.cs12
-rw-r--r--src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs18
-rw-r--r--src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs405
-rw-r--r--src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Identifier.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs3
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs27
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.resx9
-rw-r--r--src/DotNetOpenAuth/OpenId/ProviderDescription.cs24
-rw-r--r--src/DotNetOpenAuth/OpenId/Realm.cs62
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs50
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs28
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs305
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingPartyDescription.cs32
-rw-r--r--src/DotNetOpenAuth/OpenId/UriIdentifier.cs11
-rw-r--r--src/DotNetOpenAuth/OpenId/XriIdentifier.cs46
-rw-r--r--src/DotNetOpenAuth/Util.cs84
-rw-r--r--src/DotNetOpenAuth/Xrds/ServiceElement.cs87
-rw-r--r--src/DotNetOpenAuth/Xrds/TypeElement.cs19
-rw-r--r--src/DotNetOpenAuth/Xrds/UriElement.cs53
-rw-r--r--src/DotNetOpenAuth/Xrds/XrdElement.cs115
-rw-r--r--src/DotNetOpenAuth/Xrds/XrdsDocument.cs157
-rw-r--r--src/DotNetOpenAuth/Xrds/XrdsNode.cs38
-rw-r--r--src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs99
-rw-r--r--src/DotNetOpenAuth/Xrds/XrdsStrings.resx132
-rw-r--r--src/DotNetOpenAuth/Yadis/ContentTypes.cs32
-rw-r--r--src/DotNetOpenAuth/Yadis/DiscoveryResult.cs88
-rw-r--r--src/DotNetOpenAuth/Yadis/HtmlParser.cs81
-rw-r--r--src/DotNetOpenAuth/Yadis/Yadis.cs139
38 files changed, 2287 insertions, 105 deletions
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index fa97e8d..d6ccb7b 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -72,6 +72,7 @@
<Compile Include="Configuration\UntrustedWebRequestSection.cs" />
<Compile Include="Configuration\HostNameOrRegexCollection.cs" />
<Compile Include="Configuration\HostNameElement.cs" />
+ <Compile Include="Messaging\DirectWebResponse.cs" />
<Compile Include="Messaging\IDirectResponseProtocolMessage.cs" />
<Compile Include="Messaging\EmptyDictionary.cs" />
<Compile Include="Messaging\EmptyEnumerator.cs" />
@@ -105,7 +106,7 @@
<Compile Include="Messaging\Bindings\NonceMemoryStore.cs" />
<Compile Include="OAuth\ChannelElements\SigningBindingElementBase.cs" />
<Compile Include="OAuth\WebConsumer.cs" />
- <Compile Include="Messaging\IWebRequestHandler.cs" />
+ <Compile Include="Messaging\IDirectWebRequestHandler.cs" />
<Compile Include="OAuth\ChannelElements\ITamperResistantOAuthMessage.cs" />
<Compile Include="OAuth\Messages\MessageBase.cs" />
<Compile Include="OAuth\Messages\AuthorizedTokenRequest.cs" />
@@ -193,6 +194,8 @@
<Compile Include="OpenId\Messages\DirectErrorResponse.cs" />
<Compile Include="OpenId\Messages\RequestBase.cs" />
<Compile Include="OpenId\Messages\DirectResponseBase.cs" />
+ <Compile Include="OpenId\RelyingParty\IProviderEndpoint.cs" />
+ <Compile Include="OpenId\RelyingParty\IXrdsProviderEndpoint.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdRelyingParty.cs" />
<Compile Include="OpenId\OpenIdStrings.Designer.cs">
<DependentUpon>OpenIdStrings.resx</DependentUpon>
@@ -203,7 +206,9 @@
<Compile Include="OpenId\ProviderDescription.cs" />
<Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" />
<Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" />
+ <Compile Include="OpenId\RelyingParty\ServiceEndpoint.cs" />
<Compile Include="OpenId\SecuritySettings.cs" />
+ <Compile Include="Messaging\UntrustedWebRequestHandler.cs" />
<Compile Include="OpenId\UriIdentifier.cs" />
<Compile Include="OpenId\XriIdentifier.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@@ -220,6 +225,21 @@
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
<Compile Include="UriUtil.cs" />
+ <Compile Include="Xrds\XrdsStrings.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>XrdsStrings.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Yadis\ContentTypes.cs" />
+ <Compile Include="Yadis\DiscoveryResult.cs" />
+ <Compile Include="Yadis\HtmlParser.cs" />
+ <Compile Include="Xrds\ServiceElement.cs" />
+ <Compile Include="Xrds\TypeElement.cs" />
+ <Compile Include="Xrds\UriElement.cs" />
+ <Compile Include="Xrds\XrdElement.cs" />
+ <Compile Include="Xrds\XrdsDocument.cs" />
+ <Compile Include="Xrds\XrdsNode.cs" />
+ <Compile Include="Yadis\Yadis.cs" />
</ItemGroup>
<ItemGroup>
<None Include="ClassDiagram.cd" />
@@ -246,6 +266,10 @@
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
+ <EmbeddedResource Include="Xrds\XrdsStrings.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>XrdsStrings.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" />
diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs
index 2db6b93..71afb4f 100644
--- a/src/DotNetOpenAuth/Messaging/Channel.cs
+++ b/src/DotNetOpenAuth/Messaging/Channel.cs
@@ -95,7 +95,7 @@ namespace DotNetOpenAuth.Messaging {
/// This defaults to a straightforward implementation, but can be set
/// to a mock object for testing purposes.
/// </remarks>
- public IWebRequestHandler WebRequestHandler { get; set; }
+ public IDirectWebRequestHandler WebRequestHandler { get; set; }
/// <summary>
/// Gets the binding elements used by this channel, in the order they are applied to outgoing messages.
@@ -359,7 +359,7 @@ namespace DotNetOpenAuth.Messaging {
protected virtual IProtocolMessage RequestInternal(IDirectedProtocolMessage request) {
HttpWebRequest webRequest = this.CreateHttpRequest(request);
- Response response = this.WebRequestHandler.GetResponse(webRequest);
+ DirectWebResponse response = this.WebRequestHandler.GetResponse(webRequest);
if (response.ResponseStream == null) {
return null;
}
@@ -522,7 +522,7 @@ namespace DotNetOpenAuth.Messaging {
/// </summary>
/// <param name="response">The response that is anticipated to contain an protocol message.</param>
/// <returns>The deserialized message parts, if found. Null otherwise.</returns>
- protected abstract IDictionary<string, string> ReadFromResponseInternal(Response response);
+ protected abstract IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response);
/// <summary>
/// Prepares an HTTP request that carries a given message.
diff --git a/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs
new file mode 100644
index 0000000..f8dc31e
--- /dev/null
+++ b/src/DotNetOpenAuth/Messaging/DirectWebResponse.cs
@@ -0,0 +1,73 @@
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Globalization;
+ using System.Net.Mime;
+ using System.Net;
+
+ [Serializable]
+ [DebuggerDisplay("{StatusCode} {ContentType.MediaType}: {ReadResponseString().Substring(4,50)}")]
+ public class DirectWebResponse : Response {
+ private const string DefaultContentEncoding = "ISO-8859-1";
+
+ internal DirectWebResponse() {
+ }
+
+ internal DirectWebResponse(Uri requestUri, HttpWebResponse response)
+ : this(requestUri, response, int.MaxValue) {
+ }
+
+ internal DirectWebResponse(Uri requestUri, HttpWebResponse response, int maximumBytesToRead) : base(response, maximumBytesToRead) {
+ ErrorUtilities.VerifyArgumentNotNull(requestUri, "requestUri");
+ ErrorUtilities.VerifyArgumentNotNull(response, "response");
+ this.RequestUri = requestUri;
+ if (!string.IsNullOrEmpty(response.ContentType))
+ ContentType = new ContentType(response.ContentType);
+ ContentEncoding = string.IsNullOrEmpty(response.ContentEncoding) ? DefaultContentEncoding : response.ContentEncoding;
+ FinalUri = response.ResponseUri;
+ }
+
+ /// <summary>
+ /// Constructs a mock web response.
+ /// </summary>
+ internal DirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers,
+ HttpStatusCode statusCode, string contentType, string contentEncoding, Stream responseStream) {
+ ErrorUtilities.VerifyArgumentNotNull(requestUri, "requestUri");
+ ErrorUtilities.VerifyArgumentNotNull(responseStream, "responseStream");
+ this.RequestUri = requestUri;
+ this.ResponseStream = responseStream;
+ this.Status = statusCode;
+ if (!string.IsNullOrEmpty(contentType)) {
+ this.ContentType = new ContentType(contentType);
+ }
+ this.ContentEncoding = string.IsNullOrEmpty(contentEncoding) ? DefaultContentEncoding : contentEncoding;
+ this.Headers = headers;
+ this.FinalUri = responseUri;
+ }
+
+ public ContentType ContentType { get; private set; }
+ public string ContentEncoding { get; private set; }
+ public Uri RequestUri { get; private set; }
+ public Uri FinalUri { get; private set; }
+
+ public override string ToString() {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "RequestUri = {0}", this.RequestUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ResponseUri = {0}", this.FinalUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "StatusCode = {0}", this.Status));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentType = {0}", this.ContentType));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentEncoding = {0}", this.ContentEncoding));
+ sb.AppendLine("Headers:");
+ foreach (string header in this.Headers) {
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "\t{0}: {1}", header, this.Headers[header]));
+ }
+ sb.AppendLine("Response:");
+ sb.AppendLine(this.Body);
+ return sb.ToString();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
index 9068601..552662c 100644
--- a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
@@ -77,7 +77,7 @@ namespace DotNetOpenAuth.Messaging {
/// <param name="condition">The condition that must evaluate to true to avoid an exception.</param>
/// <param name="message">The message to use in the exception if the condition is false.</param>
/// <param name="args">The string formatting arguments, if any.</param>
- internal static void VerifyArgument(bool condition, string message, params string[] args) {
+ internal static void VerifyArgument(bool condition, string message, params object[] args) {
if (!condition) {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, message, args));
}
diff --git a/src/DotNetOpenAuth/Messaging/IWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs
index b2c60be..bf2acfb 100644
--- a/src/DotNetOpenAuth/Messaging/IWebRequestHandler.cs
+++ b/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
-// <copyright file="IWebRequestHandler.cs" company="Andrew Arnott">
+// <copyright file="IDirectWebRequestHandler.cs" company="Andrew Arnott">
// Copyright (c) Andrew Arnott. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
@@ -12,7 +12,7 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// A contract for <see cref="HttpWebRequest"/> handling.
/// </summary>
- public interface IWebRequestHandler {
+ public interface IDirectWebRequestHandler {
/// <summary>
/// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
/// </summary>
@@ -22,10 +22,10 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// Processes an <see cref="HttpWebRequest"/> and converts the
- /// <see cref="HttpWebResponse"/> to a <see cref="Response"/> instance.
+ /// <see cref="HttpWebResponse"/> to a <see cref="DirectWebResponse"/> instance.
/// </summary>
/// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
- /// <returns>An instance of <see cref="Response"/> describing the response.</returns>
- Response GetResponse(HttpWebRequest request);
+ /// <returns>An instance of <see cref="DirectWebResponse"/> describing the response.</returns>
+ DirectWebResponse GetResponse(HttpWebRequest request);
}
}
diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs
index dcd8f4e..7bd3d94 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs
@@ -187,6 +187,15 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Looks up a localized string similar to Insecure web request for &apos;{0}&apos; aborted due to security requirements demanding HTTPS..
+ /// </summary>
+ internal static string InsecureWebRequestWithSslRequired {
+ get {
+ return ResourceManager.GetString("InsecureWebRequestWithSslRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The message required protections {0} but the channel could only apply {1}..
/// </summary>
internal static string InsufficentMessageProtection {
@@ -376,6 +385,15 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Looks up a localized string similar to The maximum allowable number of redirects were exceeded while requesting &apos;{0}&apos;..
+ /// </summary>
+ internal static string TooManyRedirects {
+ get {
+ return ResourceManager.GetString("TooManyRedirects", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The array must not be empty..
/// </summary>
internal static string UnexpectedEmptyArray {
@@ -446,5 +464,23 @@ namespace DotNetOpenAuth.Messaging {
return ResourceManager.GetString("UnrecognizedEnumValue", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to The URL &apos;{0}&apos; is rated unsafe and cannot be requested this way..
+ /// </summary>
+ internal static string UnsafeWebRequestDetected {
+ get {
+ return ResourceManager.GetString("UnsafeWebRequestDetected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Web request to &apos;{0}&apos; failed..
+ /// </summary>
+ internal static string WebRequestFailed {
+ get {
+ return ResourceManager.GetString("WebRequestFailed", resourceCulture);
+ }
+ }
}
}
diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx
index aa0d9d0..767b07f 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx
+++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx
@@ -159,6 +159,9 @@
<data name="IndirectMessagesMustImplementIDirectedProtocolMessage" xml:space="preserve">
<value>Messages that indicate indirect transport must implement the {0} interface.</value>
</data>
+ <data name="InsecureWebRequestWithSslRequired" xml:space="preserve">
+ <value>Insecure web request for '{0}' aborted due to security requirements demanding HTTPS.</value>
+ </data>
<data name="InsufficentMessageProtection" xml:space="preserve">
<value>The message required protections {0} but the channel could only apply {1}.</value>
</data>
@@ -222,6 +225,9 @@
<data name="TooManyBindingsOfferingSameProtection" xml:space="preserve">
<value>Expected at most 1 binding element offering the {0} protection, but found {1}.</value>
</data>
+ <data name="TooManyRedirects" xml:space="preserve">
+ <value>The maximum allowable number of redirects were exceeded while requesting '{0}'.</value>
+ </data>
<data name="UnexpectedEmptyArray" xml:space="preserve">
<value>The array must not be empty.</value>
</data>
@@ -246,4 +252,10 @@
<data name="UnrecognizedEnumValue" xml:space="preserve">
<value>{0} property has unrecognized value {1}.</value>
</data>
+ <data name="UnsafeWebRequestDetected" xml:space="preserve">
+ <value>The URL '{0}' is rated unsafe and cannot be requested this way.</value>
+ </data>
+ <data name="WebRequestFailed" xml:space="preserve">
+ <value>Web request to '{0}' failed.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index f23aea8..59467da 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -115,28 +115,65 @@ namespace DotNetOpenAuth.Messaging {
/// </summary>
/// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param>
/// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param>
+ /// <returns>The total number of bytes copied.</returns>
/// <remarks>
/// Copying begins at the streams' current positions.
/// The positions are NOT reset after copying is complete.
/// </remarks>
- internal static void CopyTo(this Stream copyFrom, Stream copyTo) {
- if (copyFrom == null) {
- throw new ArgumentNullException("copyFrom");
- }
- if (copyTo == null) {
- throw new ArgumentNullException("copyTo");
- }
- if (!copyFrom.CanRead) {
- throw new ArgumentException(MessagingStrings.StreamUnreadable, "copyFrom");
- }
- if (!copyTo.CanWrite) {
- throw new ArgumentException(MessagingStrings.StreamUnwritable, "copyTo");
- }
+ internal static int CopyTo(this Stream copyFrom, Stream copyTo) {
+ return CopyTo(copyFrom, copyTo, int.MaxValue);
+ }
+
+ /// <summary>
+ /// Copies the contents of one stream to another.
+ /// </summary>
+ /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param>
+ /// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param>
+ /// <returns>The total number of bytes copied.</returns>
+ /// <remarks>
+ /// Copying begins at the streams' current positions.
+ /// The positions are NOT reset after copying is complete.
+ /// </remarks>
+ internal static int CopyTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) {
+ ErrorUtilities.VerifyArgumentNotNull(copyFrom, "copyFrom");
+ ErrorUtilities.VerifyArgumentNotNull(copyTo, "copyTo");
+ ErrorUtilities.VerifyArgument(copyFrom.CanRead, MessagingStrings.StreamUnreadable);
+ ErrorUtilities.VerifyArgument(copyTo.CanWrite, MessagingStrings.StreamUnwritable, "copyTo");
byte[] buffer = new byte[1024];
int readBytes;
+ int totalCopiedBytes = 0;
while ((readBytes = copyFrom.Read(buffer, 0, 1024)) > 0) {
- copyTo.Write(buffer, 0, readBytes);
+ int writeBytes = Math.Min(maximumBytesToCopy, readBytes);
+ copyTo.Write(buffer, 0, writeBytes);
+ totalCopiedBytes += writeBytes;
+ maximumBytesToCopy -= writeBytes;
+ }
+
+ return totalCopiedBytes;
+ }
+
+ /// <summary>
+ /// Creates a snapshot of some stream so it is seekable, and the original can be closed.
+ /// </summary>
+ /// <param name="copyFrom">The stream to copy bytes from.</param>
+ /// <returns>A seekable stream with the same contents as the original.</returns>
+ internal static Stream CreateSnapshot(this Stream copyFrom) {
+ ErrorUtilities.VerifyArgumentNotNull(copyFrom, "copyFrom");
+
+ MemoryStream copyTo = new MemoryStream(copyFrom.CanSeek ? (int)copyFrom.Length : 4 * 1024);
+ copyFrom.CopyTo(copyTo);
+ copyTo.Position = 0;
+ return copyTo;
+ }
+
+ internal static Stream CreateSnapshotAndClose(this Stream copyFrom) {
+ ErrorUtilities.VerifyArgumentNotNull(copyFrom, "copyFrom");
+
+ try {
+ return CreateSnapshot(copyFrom);
+ } finally {
+ copyFrom.Dispose();
}
}
diff --git a/src/DotNetOpenAuth/Messaging/Response.cs b/src/DotNetOpenAuth/Messaging/Response.cs
index 02a4a0e..5696d8b 100644
--- a/src/DotNetOpenAuth/Messaging/Response.cs
+++ b/src/DotNetOpenAuth/Messaging/Response.cs
@@ -26,7 +26,7 @@ namespace DotNetOpenAuth.Messaging {
/// can be canceled by calling <see cref="HttpResponse.End"/> after this message
/// is sent on the response stream.</para>
/// </remarks>
- public class Response {
+ public class Response { // TODO: rename this to UserAgentResponse
/// <summary>
/// Initializes a new instance of the <see cref="Response"/> class.
/// </summary>
@@ -40,12 +40,16 @@ namespace DotNetOpenAuth.Messaging {
/// based on the contents of an <see cref="HttpWebResponse"/>.
/// </summary>
/// <param name="response">The <see cref="HttpWebResponse"/> to clone.</param>
- internal Response(HttpWebResponse response) {
+ /// <param name="maximumBytesToRead">The maximum bytes to read from the response stream.</param>
+ protected internal Response(HttpWebResponse response, int maximumBytesToRead) {
+ ErrorUtilities.VerifyArgumentNotNull(response, "response");
+
this.Status = response.StatusCode;
this.Headers = response.Headers;
- this.ResponseStream = new MemoryStream();
+ this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength);
using (Stream responseStream = response.GetResponseStream()) {
- responseStream.CopyTo(this.ResponseStream);
+ // BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here.
+ this.IsResponseTruncated = responseStream.CopyTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead;
this.ResponseStream.Seek(0, SeekOrigin.Begin);
}
}
diff --git a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
index c27234c..8161de9 100644
--- a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
+++ b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
@@ -14,7 +14,7 @@ namespace DotNetOpenAuth.Messaging {
/// The default handler for transmitting <see cref="HttpWebRequest"/> instances
/// and returning the responses.
/// </summary>
- internal class StandardWebRequestHandler : IWebRequestHandler {
+ internal class StandardWebRequestHandler : IDirectWebRequestHandler {
#region IWebRequestHandler Members
/// <summary>
@@ -24,9 +24,7 @@ namespace DotNetOpenAuth.Messaging {
/// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
/// <returns>The stream the caller should write out the entity data to.</returns>
public TextWriter GetRequestStream(HttpWebRequest request) {
- if (request == null) {
- throw new ArgumentNullException("request");
- }
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
try {
return new StreamWriter(request.GetRequestStream());
@@ -37,19 +35,17 @@ namespace DotNetOpenAuth.Messaging {
/// <summary>
/// Processes an <see cref="HttpWebRequest"/> and converts the
- /// <see cref="HttpWebResponse"/> to a <see cref="Response"/> instance.
+ /// <see cref="HttpWebResponse"/> to a <see cref="DirectWebResponse"/> instance.
/// </summary>
/// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
- /// <returns>An instance of <see cref="Response"/> describing the response.</returns>
- public Response GetResponse(HttpWebRequest request) {
- if (request == null) {
- throw new ArgumentNullException("request");
- }
+ /// <returns>An instance of <see cref="DirectWebResponse"/> describing the response.</returns>
+ public DirectWebResponse GetResponse(HttpWebRequest request) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
try {
Logger.DebugFormat("HTTP {0} {1}", request.Method, request.RequestUri);
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
- return new Response(response);
+ return new DirectWebResponse(request.RequestUri, response);
}
} catch (WebException ex) {
if (Logger.IsErrorEnabled) {
diff --git a/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs
new file mode 100644
index 0000000..d173541
--- /dev/null
+++ b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs
@@ -0,0 +1,405 @@
+//-----------------------------------------------------------------------
+// <copyright file="UntrustedWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+#if DEBUG
+#define LONGTIMEOUT
+#endif
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Globalization;
+ using System.IO;
+ using System.Net;
+ using System.Net.Cache;
+ using System.Text.RegularExpressions;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote
+ /// server leaving dangling connections, sending too much data, causing requests against
+ /// internal servers, etc.
+ /// </summary>
+ /// <remarks>
+ /// Protections include:
+ /// * Conservative maximum time to receive the complete response.
+ /// * Only HTTP and HTTPS schemes are permitted.
+ /// * Internal IP address ranges are not permitted: 127.*.*.*, 1::*
+ /// * Internal host names are not permitted (periods must be found in the host name)
+ /// If a particular host would be permitted but is in the blacklist, it is not allowed.
+ /// If a particular host would not be permitted but is in the whitelist, it is allowed.
+ /// </remarks>
+ public class UntrustedWebRequestHandler : IDirectWebRequestHandler {
+ /// <summary>
+ /// Gets or sets the default cache policy to use for HTTP requests.
+ /// </summary>
+ internal readonly static RequestCachePolicy DefaultCachePolicy = HttpWebRequest.DefaultCachePolicy;
+
+ private static DotNetOpenAuth.Configuration.UntrustedWebRequestSection Configuration {
+ get { return UntrustedWebRequestSection.Configuration; }
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int maximumBytesToRead = Configuration.MaximumBytesToRead;
+
+ /// <summary>
+ /// The default maximum bytes to read in any given HTTP request.
+ /// Default is 1MB. Cannot be less than 2KB.
+ /// </summary>
+ public int MaximumBytesToRead {
+ get { return maximumBytesToRead; }
+ set {
+ if (value < 2048) throw new ArgumentOutOfRangeException("value");
+ maximumBytesToRead = value;
+ }
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int maximumRedirections = Configuration.MaximumRedirections;
+
+ /// <summary>
+ /// Backing store for the <see cref="RequireSsl"/> property.
+ /// </summary>
+ private readonly bool requireSsl;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class.
+ /// </summary>
+ /// <param name="requireSsl">if set to <c>true</c> all requests made with this instance must be completed using SSL.</param>
+ public UntrustedWebRequestHandler(bool requireSsl) {
+ this.requireSsl = requireSsl;
+
+ this.ReadWriteTimeout = Configuration.ReadWriteTimeout;
+ this.Timeout = Configuration.Timeout;
+#if LONGTIMEOUT
+ this.ReadWriteTimeout = TimeSpan.FromHours(1);
+ this.Timeout = TimeSpan.FromHours(1);
+#endif
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether all requests (and redirects) will be required
+ /// to use SSL encryption for the request to be completed successfully.
+ /// </summary>
+ /// <remarks>
+ /// Many policies in this class can be configured after the class is instantiated.
+ /// But requiring SSL is an immutable setting and can only be set in the constructor.
+ /// </remarks>
+ public bool RequireSsl {
+ get { return this.requireSsl; }
+ }
+
+ /// <summary>
+ /// Gets or sets the total number of redirections to allow on any one request.
+ /// Default is 10.
+ /// </summary>
+ public int MaximumRedirections {
+ get { return maximumRedirections; }
+ set {
+ if (value < 0) throw new ArgumentOutOfRangeException("value");
+ maximumRedirections = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the time allowed to wait for single read or write operation to complete.
+ /// Default is 500 milliseconds.
+ /// </summary>
+ public TimeSpan ReadWriteTimeout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time allowed for an entire HTTP request.
+ /// Default is 5 seconds.
+ /// </summary>
+ public TimeSpan Timeout { get; set; }
+
+ private ICollection<string> allowableSchemes = new List<string> { "http", "https" };
+ private ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings);
+ /// <summary>
+ /// A collection of host name literals that should be allowed even if they don't
+ /// pass standard security checks.
+ /// </summary>
+ public ICollection<string> WhitelistHosts { get { return whitelistHosts; } }
+ private ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs);
+ /// <summary>
+ /// A collection of host name regular expressions that indicate hosts that should
+ /// be allowed even though they don't pass standard security checks.
+ /// </summary>
+ public ICollection<Regex> WhitelistHostsRegex { get { return whitelistHostsRegex; } }
+ private ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings);
+ /// <summary>
+ /// A collection of host name literals that should be rejected even if they
+ /// pass standard security checks.
+ /// </summary>
+ public ICollection<string> BlacklistHosts { get { return blacklistHosts; } }
+ private ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs);
+ /// <summary>
+ /// A collection of host name regular expressions that indicate hosts that should
+ /// be rjected even if they pass standard security checks.
+ /// </summary>
+ public ICollection<Regex> BlacklistHostsRegex { get { return blacklistHostsRegex; } }
+
+ #region IWebRequestHandler Members
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ public TextWriter GetRequestStream(HttpWebRequest request) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+ this.EnsureAllowableRequestUri(request.RequestUri);
+
+ this.PrepareRequest(request);
+
+ // We don't currently support redirects at URLs where we're POSTing data.
+ // When we want to add this support, we need to be careful to not allow
+ // redirects to non-HTTPS schemes if RequireSsl is true.
+ request.AllowAutoRedirect = false;
+
+ // Submit the request and get the request stream back.
+ try {
+ return new StreamWriter(request.GetRequestStream());
+ } catch (WebException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.ErrorInRequestReplyMessage);
+ }
+ }
+
+ public DirectWebResponse GetResponse(HttpWebRequest request) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+ this.EnsureAllowableRequestUri(request.RequestUri);
+
+ // This request MAY have already been prepared by GetRequestStream, but
+ // we have no guarantee, so do it just to be safe.
+ this.PrepareRequest(request);
+
+ // TODO: Code here
+ throw new NotImplementedException();
+ }
+
+ #endregion
+
+ internal DirectWebResponse RequestWithManagedRedirects(HttpWebRequest request) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+
+ // Since we may require SSL for every redirect, we handle each redirect manually
+ // in order to detect and fail if any redirect sends us to an HTTP url.
+ // We COULD allow automatic redirect in the cases where HTTPS is not required,
+ // but our mock request infrastructure can't do redirects on its own either.
+ Uri originalRequestUri = request.RequestUri;
+ int i;
+ for (i = 0; i < MaximumRedirections; i++) {
+ DirectWebResponse response = this.RequestCore(request, null, originalRequestUri);
+ if (response.Status == HttpStatusCode.MovedPermanently ||
+ response.Status == HttpStatusCode.Redirect ||
+ response.Status == HttpStatusCode.RedirectMethod ||
+ response.Status == HttpStatusCode.RedirectKeepVerb) {
+ Uri redirectUri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]);
+ request = CloneRequestWithNewUrl(request, redirectUri);
+ } else {
+ return response;
+ }
+ }
+ throw new WebException(string.Format(CultureInfo.CurrentCulture, MessagingStrings.TooManyRedirects, originalRequestUri));
+ }
+
+ private static HttpWebRequest CloneRequestWithNewUrl(HttpWebRequest request, Uri newRequestUri) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+ ErrorUtilities.VerifyArgumentNotNull(newRequestUri, "newRequestUri");
+
+ var newRequest = (HttpWebRequest)WebRequest.Create(newRequestUri);
+ newRequest.Accept = request.Accept;
+ newRequest.AllowAutoRedirect = request.AllowAutoRedirect;
+ newRequest.AllowWriteStreamBuffering = request.AllowWriteStreamBuffering;
+ newRequest.AuthenticationLevel = request.AuthenticationLevel;
+ newRequest.AutomaticDecompression = request.AutomaticDecompression;
+ newRequest.CachePolicy = request.CachePolicy;
+ newRequest.ClientCertificates = request.ClientCertificates;
+ newRequest.Connection = request.Connection;
+ newRequest.ConnectionGroupName = request.ConnectionGroupName;
+ newRequest.ContentLength = request.ContentLength;
+ newRequest.ContentType = request.ContentType;
+ newRequest.ContinueDelegate = request.ContinueDelegate;
+ newRequest.CookieContainer = request.CookieContainer;
+ newRequest.Credentials = request.Credentials;
+ newRequest.Expect = request.Expect;
+ newRequest.Headers = request.Headers;
+ newRequest.IfModifiedSince = request.IfModifiedSince;
+ newRequest.ImpersonationLevel = request.ImpersonationLevel;
+ newRequest.KeepAlive = request.KeepAlive;
+ newRequest.MaximumAutomaticRedirections = request.MaximumAutomaticRedirections;
+ newRequest.MaximumResponseHeadersLength = request.MaximumResponseHeadersLength;
+ newRequest.MediaType = request.MediaType;
+ newRequest.Method = request.Method;
+ newRequest.Pipelined = request.Pipelined;
+ newRequest.PreAuthenticate = request.PreAuthenticate;
+ newRequest.ProtocolVersion = request.ProtocolVersion;
+ newRequest.Proxy = request.Proxy;
+ newRequest.ReadWriteTimeout = request.ReadWriteTimeout;
+ newRequest.Referer = request.Referer;
+ newRequest.SendChunked = request.SendChunked;
+ newRequest.Timeout = request.Timeout;
+ newRequest.TransferEncoding = request.TransferEncoding;
+ newRequest.UnsafeAuthenticatedConnectionSharing = request.UnsafeAuthenticatedConnectionSharing;
+ newRequest.UseDefaultCredentials = request.UseDefaultCredentials;
+ newRequest.UserAgent = request.UserAgent;
+
+ return newRequest;
+ }
+
+ private bool isHostWhitelisted(string host) {
+ return isHostInList(host, WhitelistHosts, WhitelistHostsRegex);
+ }
+
+ private bool isHostBlacklisted(string host) {
+ return isHostInList(host, BlacklistHosts, BlacklistHostsRegex);
+ }
+
+ private bool isHostInList(string host, ICollection<string> stringList, ICollection<Regex> regexList) {
+ Debug.Assert(!string.IsNullOrEmpty(host));
+ Debug.Assert(stringList != null);
+ Debug.Assert(regexList != null);
+ foreach (string testHost in stringList) {
+ if (string.Equals(host, testHost, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+ foreach (Regex regex in regexList) {
+ if (regex.IsMatch(host))
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Verify that the request qualifies under our security policies
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ private void EnsureAllowableRequestUri(Uri requestUri) {
+ ErrorUtilities.VerifyArgument(this.isUriAllowable(requestUri), MessagingStrings.UnsafeWebRequestDetected, requestUri);
+ ErrorUtilities.VerifyProtocol(!this.RequireSsl || String.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase), MessagingStrings.InsecureWebRequestWithSslRequired, requestUri);
+ }
+
+ private bool isUriAllowable(Uri uri) {
+ Debug.Assert(uri != null);
+ if (!allowableSchemes.Contains(uri.Scheme)) {
+ Logger.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
+ return false;
+ }
+
+ // Allow for whitelist or blacklist to override our detection.
+ Func<string, bool> failsUnlessWhitelisted = (string reason) => {
+ if (isHostWhitelisted(uri.DnsSafeHost)) return true;
+ Logger.WarnFormat("Rejecting URL {0} because {1}.", uri, reason);
+ return false;
+ };
+
+ // Try to interpret the hostname as an IP address so we can test for internal
+ // IP address ranges. Note that IP addresses can appear in many forms
+ // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1
+ // So we convert them to a canonical IPAddress instance, and test for all
+ // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1
+ // Note that Uri.IsLoopback is very unreliable, not catching many of these variants.
+ IPAddress hostIPAddress;
+ if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress)) {
+ byte[] addressBytes = hostIPAddress.GetAddressBytes();
+ // The host is actually an IP address.
+ switch (hostIPAddress.AddressFamily) {
+ case System.Net.Sockets.AddressFamily.InterNetwork:
+ if (addressBytes[0] == 127 || addressBytes[0] == 10)
+ return failsUnlessWhitelisted("it is a loopback address.");
+ break;
+ case System.Net.Sockets.AddressFamily.InterNetworkV6:
+ if (isIPv6Loopback(hostIPAddress))
+ return failsUnlessWhitelisted("it is a loopback address.");
+ break;
+ default:
+ return failsUnlessWhitelisted("it does not use an IPv4 or IPv6 address.");
+ }
+ } else {
+ // The host is given by name. We require names to contain periods to
+ // help make sure it's not an internal address.
+ if (!uri.Host.Contains(".")) {
+ return failsUnlessWhitelisted("it does not contain a period in the host name.");
+ }
+ }
+ if (isHostBlacklisted(uri.DnsSafeHost)) {
+ Logger.WarnFormat("Rejected URL {0} because it is blacklisted.", uri);
+ return false;
+ }
+ return true;
+ }
+
+ private bool isIPv6Loopback(IPAddress ip) {
+ Debug.Assert(ip != null);
+ byte[] addressBytes = ip.GetAddressBytes();
+ for (int i = 0; i < addressBytes.Length - 1; i++)
+ if (addressBytes[i] != 0) return false;
+ if (addressBytes[addressBytes.Length - 1] != 1) return false;
+ return true;
+ }
+
+ private HttpWebRequest PrepareRequest(HttpWebRequest request) {
+ // Set/override a few properties of the request to apply our policies for untrusted requests.
+ request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
+ request.Timeout = (int)Timeout.TotalMilliseconds;
+ request.KeepAlive = false;
+
+ // If SSL is required throughout, we cannot allow auto redirects because
+ // it may include a pass through an unprotected HTTP request.
+ // We have to follow redirects manually.
+ request.AllowAutoRedirect = false;
+
+ return request;
+ }
+
+ private DirectWebResponse RequestCore(HttpWebRequest request, Stream postEntity, Uri originalRequestUri) {
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+ ErrorUtilities.VerifyArgumentNotNull(originalRequestUri, "originalRequestUri");
+ EnsureAllowableRequestUri(request.RequestUri);
+
+ int postEntityLength = 0;
+ try {
+ if (postEntity != null) {
+ using (Stream outStream = request.GetRequestStream()) {
+ postEntityLength = postEntity.CopyTo(outStream);
+ }
+ }
+
+ using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
+ return new DirectWebResponse(originalRequestUri, response, MaximumBytesToRead);
+ }
+ } catch (WebException e) {
+ using (HttpWebResponse response = (HttpWebResponse)e.Response) {
+ if (response != null) {
+ if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
+ if (request.ServicePoint.Expect100Continue) { // must only try this once more
+ // Some OpenID servers doesn't understand the Expect header and send 417 error back.
+ // If this server just failed from that, we're trying again without sending the
+ // "Expect: 100-Continue" HTTP header. (see Google Code Issue 72)
+ // We don't just set Expect100Continue = !avoidSendingExpect100Continue
+ // so that future requests don't reset this and have to try twice as well.
+ // We don't want to blindly set all ServicePoints to not use the Expect header
+ // as that would be a security hole allowing any visitor to a web site change
+ // the web site's global behavior when calling that host.
+ request.ServicePoint.Expect100Continue = false; // TODO: investigate that CAS may throw here, and we can use request.Expect instead.
+ postEntity.Seek(-postEntityLength, SeekOrigin.Current);
+ request = CloneRequestWithNewUrl(request, request.RequestUri);
+ return RequestCore(request, postEntity, originalRequestUri);
+ }
+ }
+ return new DirectWebResponse(originalRequestUri, response, MaximumBytesToRead);
+ } else {
+ throw ErrorUtilities.Wrap(e, MessagingStrings.WebRequestFailed, originalRequestUri);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs
index d827ed3..2203955 100644
--- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs
+++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs
@@ -158,7 +158,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements {
/// <returns>
/// The deserialized message parts, if found. Null otherwise.
/// </returns>
- protected override IDictionary<string, string> ReadFromResponseInternal(Response response) {
+ protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) {
if (response == null) {
throw new ArgumentNullException("response");
}
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
index 023cce6..1879561 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs
@@ -66,7 +66,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
/// <returns>
/// The deserialized message parts, if found. Null otherwise.
/// </returns>
- protected override IDictionary<string, string> ReadFromResponseInternal(Response response) {
+ protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) {
if (response == null) {
throw new ArgumentNullException("response");
}
diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs
index ae40d4e..26108af 100644
--- a/src/DotNetOpenAuth/OpenId/Identifier.cs
+++ b/src/DotNetOpenAuth/OpenId/Identifier.cs
@@ -124,7 +124,6 @@ namespace DotNetOpenAuth.OpenId {
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>
@@ -132,7 +131,6 @@ namespace DotNetOpenAuth.OpenId {
/// 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.
diff --git a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs
index a6f1384..b563324 100644
--- a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs
+++ b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs
@@ -30,11 +30,10 @@ namespace DotNetOpenAuth.OpenId {
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
+
/// <summary>
/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
/// </summary>
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
index 9dca2a8..d51ec37 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
@@ -205,6 +205,24 @@ namespace DotNetOpenAuth.OpenId {
}
/// <summary>
+ /// Looks up a localized string similar to Unable to determine the version of the OpenID protocol implemented by the Provider at endpoint &apos;{0}&apos;..
+ /// </summary>
+ internal static string ProviderVersionUnrecognized {
+ get {
+ return ResourceManager.GetString("ProviderVersionUnrecognized", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An HTTP request to the realm URL ({0}) resulted in a redirect, which is not allowed during relying party discovery..
+ /// </summary>
+ internal static string RealmCausedRedirectUponDiscovery {
+ get {
+ return ResourceManager.GetString("RealmCausedRedirectUponDiscovery", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to return_to &apos;{0}&apos; not under realm &apos;{1}&apos;..
/// </summary>
internal static string ReturnToNotUnderRealm {
@@ -212,5 +230,14 @@ namespace DotNetOpenAuth.OpenId {
return ResourceManager.GetString("ReturnToNotUnderRealm", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to XRI resolution failed..
+ /// </summary>
+ internal static string XriResolutionFailed {
+ get {
+ return ResourceManager.GetString("XriResolutionFailed", resourceCulture);
+ }
+ }
}
}
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
index 706d7e4..fd3d799 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
@@ -165,7 +165,16 @@
<data name="NoSessionTypeFound" xml:space="preserve">
<value>Diffie-Hellman session type '{0}' not found for OpenID {1}.</value>
</data>
+ <data name="ProviderVersionUnrecognized" xml:space="preserve">
+ <value>Unable to determine the version of the OpenID protocol implemented by the Provider at endpoint '{0}'.</value>
+ </data>
+ <data name="RealmCausedRedirectUponDiscovery" xml:space="preserve">
+ <value>An HTTP request to the realm URL ({0}) resulted in a redirect, which is not allowed during relying party discovery.</value>
+ </data>
<data name="ReturnToNotUnderRealm" xml:space="preserve">
<value>return_to '{0}' not under realm '{1}'.</value>
</data>
+ <data name="XriResolutionFailed" xml:space="preserve">
+ <value>XRI resolution failed.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/OpenId/ProviderDescription.cs b/src/DotNetOpenAuth/OpenId/ProviderDescription.cs
index 48f8c49..0bc6b3d 100644
--- a/src/DotNetOpenAuth/OpenId/ProviderDescription.cs
+++ b/src/DotNetOpenAuth/OpenId/ProviderDescription.cs
@@ -10,6 +10,7 @@ namespace DotNetOpenAuth.OpenId {
using System.Linq;
using System.Text;
using DotNetOpenAuth.Messaging;
+using System.Collections.ObjectModel;
/// <summary>
/// Describes some OpenID Provider endpoint and its capabilities.
@@ -31,6 +32,24 @@ namespace DotNetOpenAuth.OpenId {
this.ProtocolVersion = openIdVersion;
}
+ internal ProviderEndpointDescription(Uri providerEndpoint, IEnumerable<string> serviceTypeURIs) {
+ ErrorUtilities.VerifyArgumentNotNull(providerEndpoint, "providerEndpoint");
+ ErrorUtilities.VerifyArgumentNotNull(serviceTypeURIs, "serviceTypeURIs");
+
+ this.Endpoint = providerEndpoint;
+ this.Capabilities = new ReadOnlyCollection<string>(serviceTypeURIs.ToList());
+
+ Protocol opIdentifierProtocol = Protocol.FindBestVersion(p => p.ClaimedIdentifierForOPIdentifier, serviceTypeURIs);
+ Protocol claimedIdentifierProviderVersion = Protocol.FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, serviceTypeURIs);
+ if (opIdentifierProtocol != null) {
+ this.ProtocolVersion = opIdentifierProtocol.Version;
+ } else if (claimedIdentifierProviderVersion != null) {
+ this.ProtocolVersion = claimedIdentifierProviderVersion.Version;
+ }
+
+ ErrorUtilities.VerifyProtocol(this.ProtocolVersion != null, OpenIdStrings.ProviderVersionUnrecognized, this.Endpoint);
+ }
+
/// <summary>
/// Gets the URL that the OpenID Provider listens for incoming OpenID messages on.
/// </summary>
@@ -44,5 +63,10 @@ namespace DotNetOpenAuth.OpenId {
/// by its own <see cref="ProviderEndpointDescription"/> object.
/// </remarks>
internal Version ProtocolVersion { get; private set; }
+
+ /// <summary>
+ /// Gets the collection of service type URIs found in the XRDS document describing this Provider.
+ /// </summary>
+ internal ReadOnlyCollection<string> Capabilities { get; private set; }
}
}
diff --git a/src/DotNetOpenAuth/OpenId/Realm.cs b/src/DotNetOpenAuth/OpenId/Realm.cs
index 4b0266e..0bdaa33 100644
--- a/src/DotNetOpenAuth/OpenId/Realm.cs
+++ b/src/DotNetOpenAuth/OpenId/Realm.cs
@@ -14,6 +14,9 @@ namespace DotNetOpenAuth.OpenId {
using System.Xml;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.Yadis;
+ using DotNetOpenAuth.Xrds;
+ using System.Linq;
/// <summary>
/// A trust root to validate requests and match return URLs against.
@@ -345,38 +348,33 @@ namespace DotNetOpenAuth.OpenId {
|| url.PathAndQuery[path_len] == '/';
}
-#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).
- /////// </summary>
- /////// <param name="allowRedirects">
- /////// Whether redirects may be followed when discovering the Realm.
- /////// This may be true when creating an unsolicited assertion, but must be
- /////// false when performing return URL verification per 2.0 spec section 9.2.1.
- /////// </param>
- /////// <returns>The details of the endpoints if found, otherwise null.</returns>
- ////internal IEnumerable<DotNetOpenId.Provider.RelyingPartyReceivingEndpoint> Discover(bool allowRedirects) {
- //// // Attempt YADIS discovery
- //// DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false);
- //// if (yadisResult != null) {
- //// if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) {
- //// // Redirect occurred when it was not allowed.
- //// throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- //// Strings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri));
- //// }
- //// if (yadisResult.IsXrds) {
- //// try {
- //// XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
- //// return xrds.FindRelyingPartyReceivingEndpoints();
- //// } catch (XmlException ex) {
- //// throw new OpenIdException(Strings.InvalidXRDSDocument, ex);
- //// }
- //// }
- //// }
- //// return new RelyingPartyReceivingEndpoint[0];
- ////}
-#endif
+ /// <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).
+ /// </summary>
+ /// <param name="allowRedirects">
+ /// Whether redirects may be followed when discovering the Realm.
+ /// This may be true when creating an unsolicited assertion, but must be
+ /// false when performing return URL verification per 2.0 spec section 9.2.1.
+ /// </param>
+ /// <returns>The details of the endpoints if found, otherwise null.</returns>
+ internal IEnumerable<RelyingPartyEndpointDescription> Discover(bool allowRedirects) {
+ // Attempt YADIS discovery
+ DiscoveryResult yadisResult = Yadis.Discover(UriWithWildcardChangedToWww, false);
+ if (yadisResult != null) {
+ // Detect disallowed redirects, since realm discovery never allows them for security.
+ ErrorUtilities.VerifyProtocol(allowRedirects || yadisResult.NormalizedUri == yadisResult.RequestUri, OpenIdStrings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri);
+ if (yadisResult.IsXrds) {
+ try {
+ XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
+ return xrds.FindRelyingPartyReceivingEndpoints();
+ } catch (XmlException ex) {
+ throw ErrorUtilities.Wrap(ex, XrdsStrings.InvalidXRDSDocument);
+ }
+ }
+ }
+ return Enumerable.Empty<RelyingPartyEndpointDescription>();
+ }
/// <summary>
/// Calls <see cref="UriBuilder.ToString"/> if the argument is non-null.
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs
new file mode 100644
index 0000000..310bb72
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ /// <summary>
+ /// Information published about an OpenId Provider by the
+ /// OpenId discovery documents found at a user's Claimed Identifier.
+ /// </summary>
+ /// <remarks>
+ /// Because information provided by this interface is suppplied by a
+ /// user's individually published documents, it may be incomplete or inaccurate.
+ /// </remarks>
+ public interface IProviderEndpoint {
+ /////// <summary>
+ /////// Checks whether the OpenId Identifier claims support for a given extension.
+ /////// </summary>
+ /////// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /////// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /////// <remarks>
+ /////// Note that a true or false return value is no guarantee of a Provider's
+ /////// support for or lack of support for an extension. The return value is
+ /////// determined by how the authenticating user filled out his/her XRDS document only.
+ /////// The only way to be sure of support for a given extension is to include
+ /////// the extension in the request and see if a response comes back for that extension.
+ /////// </remarks>
+ ////[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
+ ////bool IsExtensionSupported<T>() where T : Extensions.IExtension, new();
+ /////// <summary>
+ /////// Checks whether the OpenId Identifier claims support for a given extension.
+ /////// </summary>
+ /////// <param name="extensionType">The extension whose support is being queried.</param>
+ /////// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /////// <remarks>
+ /////// Note that a true or false return value is no guarantee of a Provider's
+ /////// support for or lack of support for an extension. The return value is
+ /////// determined by how the authenticating user filled out his/her XRDS document only.
+ /////// The only way to be sure of support for a given extension is to include
+ /////// the extension in the request and see if a response comes back for that extension.
+ /////// </remarks>
+ ////bool IsExtensionSupported(Type extensionType);
+ /// <summary>
+ /// The detected version of OpenID implemented by the Provider.
+ /// </summary>
+ Version Version { get; }
+ /// <summary>
+ /// The URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ Uri Uri { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs
new file mode 100644
index 0000000..f28e256
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs
@@ -0,0 +1,28 @@
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// An <see cref="IProviderEndpoint"/> interface with additional members for use
+ /// in sorting for most preferred endpoint.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds")]
+ public interface IXrdsProviderEndpoint : IProviderEndpoint {
+ /// <summary>
+ /// Checks for the presence of a given Type URI in an XRDS service.
+ /// </summary>
+ bool IsTypeUriPresent(string typeUri);
+ /// <summary>
+ /// Gets the priority associated with this service that may have been given
+ /// in the XRDS document.
+ /// </summary>
+ int? ServicePriority { get; }
+ /// <summary>
+ /// Gets the priority associated with the service endpoint URL.
+ /// </summary>
+ /// <remarks>
+ /// When sorting by priority, this property should be considered second after
+ /// <see cref="ServicePriority"/>.
+ /// </remarks>
+ int? UriPriority { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs
new file mode 100644
index 0000000..d83d26e
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs
@@ -0,0 +1,305 @@
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Globalization;
+ using System.IO;
+ using System.Text;
+
+ /// <summary>
+ /// Represents information discovered about a user-supplied Identifier.
+ /// </summary>
+ [DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, ProviderEndpoint: {ProviderEndpoint}, OpenId: {Protocol.Version}")]
+ internal class ServiceEndpoint : IXrdsProviderEndpoint {
+ /// <summary>
+ /// The URL which accepts OpenID Authentication protocol messages.
+ /// </summary>
+ /// <remarks>
+ /// Obtained by performing discovery on the User-Supplied Identifier.
+ /// This value MUST be an absolute HTTP or HTTPS URL.
+ /// </remarks>
+ public Uri ProviderEndpoint { get; private set; }
+ /// <summary>
+ /// Returns true if the <see cref="ProviderEndpoint"/> is using an encrypted channel.
+ /// </summary>
+ internal bool IsSecure {
+ get { return string.Equals(ProviderEndpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); }
+ }
+ Uri IProviderEndpoint.Uri { get { return ProviderEndpoint; } }
+ /*
+ /// <summary>
+ /// An Identifier for an OpenID Provider.
+ /// </summary>
+ public Identifier ProviderIdentifier { get; private set; }
+ */
+ /// <summary>
+ /// An Identifier that was presented by the end user to the Relying Party,
+ /// or selected by the user at the OpenID Provider.
+ /// During the initiation phase of the protocol, an end user may enter
+ /// either their own Identifier or an OP Identifier. If an OP Identifier
+ /// is used, the OP may then assist the end user in selecting an Identifier
+ /// to share with the Relying Party.
+ /// </summary>
+ public Identifier UserSuppliedIdentifier { get; private set; }
+ /// <summary>
+ /// The Identifier that the end user claims to own.
+ /// </summary>
+ public Identifier ClaimedIdentifier { get; private set; }
+ /// <summary>
+ /// An alternate Identifier for an end user that is local to a
+ /// particular OP and thus not necessarily under the end user's
+ /// control.
+ /// </summary>
+ public Identifier ProviderLocalIdentifier { get; private set; }
+ string friendlyIdentifierForDisplay;
+ /// <summary>
+ /// Supports the <see cref="IAuthenticationResponse.FriendlyIdentifierForDisplay"/> property.
+ /// </summary>
+ public string FriendlyIdentifierForDisplay {
+ get {
+ if (friendlyIdentifierForDisplay == null) {
+ XriIdentifier xri = ClaimedIdentifier as XriIdentifier;
+ UriIdentifier uri = ClaimedIdentifier as UriIdentifier;
+ if (xri != null) {
+ if (UserSuppliedIdentifier == null || String.Equals(UserSuppliedIdentifier, ClaimedIdentifier, StringComparison.OrdinalIgnoreCase)) {
+ friendlyIdentifierForDisplay = ClaimedIdentifier;
+ } else {
+ friendlyIdentifierForDisplay = UserSuppliedIdentifier;
+ }
+ } else if (uri != null) {
+ if (uri != Protocol.ClaimedIdentifierForOPIdentifier) {
+ string displayUri = uri.Uri.Authority + uri.Uri.PathAndQuery;
+ displayUri = displayUri.TrimEnd('/');
+ // Multi-byte unicode characters get encoded by the Uri class for transit.
+ // Since this is for display purposes, we want to reverse this and display a readable
+ // representation of these foreign characters.
+ friendlyIdentifierForDisplay = Uri.UnescapeDataString(displayUri);
+ }
+ } else {
+ Debug.Fail("Doh! We never should have reached here.");
+ friendlyIdentifierForDisplay = ClaimedIdentifier;
+ }
+ }
+ return friendlyIdentifierForDisplay;
+ }
+ }
+ /// <summary>
+ /// Gets the list of services available at this OP Endpoint for the
+ /// claimed Identifier. May be null.
+ /// </summary>
+ public string[] ProviderSupportedServiceTypeUris { get; private set; }
+
+ ServiceEndpoint(Identifier claimedIdentifier, Identifier userSuppliedIdentifier,
+ Uri providerEndpoint, Identifier providerLocalIdentifier,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+ if (claimedIdentifier == null) throw new ArgumentNullException("claimedIdentifier");
+ if (providerEndpoint == null) throw new ArgumentNullException("providerEndpoint");
+ if (providerSupportedServiceTypeUris == null) throw new ArgumentNullException("providerSupportedServiceTypeUris");
+ ClaimedIdentifier = claimedIdentifier;
+ UserSuppliedIdentifier = userSuppliedIdentifier;
+ ProviderEndpoint = providerEndpoint;
+ ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier;
+ ProviderSupportedServiceTypeUris = providerSupportedServiceTypeUris;
+ this.servicePriority = servicePriority;
+ this.uriPriority = uriPriority;
+ }
+ /// <summary>
+ /// Used for deserializing <see cref="ServiceEndpoint"/> from authentication responses.
+ /// </summary>
+ ServiceEndpoint(Identifier claimedIdentifier, Identifier userSuppliedIdentifier,
+ Uri providerEndpoint, Identifier providerLocalIdentifier, Protocol protocol) {
+ ClaimedIdentifier = claimedIdentifier;
+ UserSuppliedIdentifier = userSuppliedIdentifier;
+ ProviderEndpoint = providerEndpoint;
+ ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier;
+ this.protocol = protocol;
+ }
+
+ internal static ServiceEndpoint CreateForProviderIdentifier(
+ Identifier providerIdentifier, Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+ Protocol protocol = Protocol.Detect(providerSupportedServiceTypeUris);
+
+ return new ServiceEndpoint(protocol.ClaimedIdentifierForOPIdentifier, providerIdentifier,
+ providerEndpoint, protocol.ClaimedIdentifierForOPIdentifier,
+ providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
+ internal static ServiceEndpoint CreateForClaimedIdentifier(
+ Identifier claimedIdentifier, Identifier providerLocalIdentifier,
+ Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+ return CreateForClaimedIdentifier(claimedIdentifier, null, providerLocalIdentifier,
+ providerEndpoint, providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
+ internal static ServiceEndpoint CreateForClaimedIdentifier(
+ Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier,
+ Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+ return new ServiceEndpoint(claimedIdentifier, userSuppliedIdentifier, providerEndpoint,
+ providerLocalIdentifier, providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
+ Protocol protocol;
+ /// <summary>
+ /// Gets the OpenID protocol used by the Provider.
+ /// </summary>
+ public Protocol Protocol {
+ get {
+ if (protocol == null) {
+ protocol = Protocol.Detect(ProviderSupportedServiceTypeUris);
+ }
+ if (protocol != null) return protocol;
+ throw new InvalidOperationException("Unable to determine the version of OpenID the Provider supports.");
+ }
+ }
+
+ public bool IsTypeUriPresent(string typeUri) {
+ return IsExtensionSupported(typeUri);
+ }
+
+ public bool IsExtensionSupported(string extensionUri) {
+ if (ProviderSupportedServiceTypeUris == null)
+ throw new InvalidOperationException("Cannot lookup extension support on a rehydrated ServiceEndpoint.");
+ return Array.IndexOf(ProviderSupportedServiceTypeUris, extensionUri) >= 0;
+ }
+
+ ////public bool IsExtensionSupported(IExtension extension) {
+ //// if (extension == null) throw new ArgumentNullException("extension");
+
+ //// // Consider the primary case.
+ //// if (IsExtensionSupported(extension.TypeUri)) {
+ //// return true;
+ //// }
+ //// // Consider the secondary cases.
+ //// if (extension.AdditionalSupportedTypeUris != null) {
+ //// foreach (string extensionTypeUri in extension.AdditionalSupportedTypeUris) {
+ //// if (IsExtensionSupported(extensionTypeUri)) {
+ //// return true;
+ //// }
+ //// }
+ //// }
+ //// return false;
+ ////}
+
+ ////public bool IsExtensionSupported<T>() where T : Extensions.IExtension, new() {
+ //// T extension = new T();
+ //// return IsExtensionSupported(extension);
+ ////}
+
+ ////public bool IsExtensionSupported(Type extensionType) {
+ //// if (extensionType == null) throw new ArgumentNullException("extensionType");
+ //// if (!typeof(Extensions.IExtension).IsAssignableFrom(extensionType))
+ //// throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ //// Strings.TypeMustImplementX, typeof(Extensions.IExtension).FullName),
+ //// "extensionType");
+ //// var extension = (Extensions.IExtension)Activator.CreateInstance(extensionType);
+ //// return IsExtensionSupported(extension);
+ ////}
+
+ Version IProviderEndpoint.Version { get { return Protocol.Version; } }
+
+ /// <summary>
+ /// Saves the discovered information about this endpoint
+ /// for later comparison to validate assertions.
+ /// </summary>
+ internal void Serialize(TextWriter writer) {
+ writer.WriteLine(ClaimedIdentifier);
+ writer.WriteLine(ProviderLocalIdentifier);
+ writer.WriteLine(UserSuppliedIdentifier);
+ writer.WriteLine(ProviderEndpoint);
+ writer.WriteLine(Protocol.Version);
+ // No reason to serialize priority. We only needed priority to decide whether to use this endpoint.
+ }
+
+ /// <summary>
+ /// Reads previously discovered information about an endpoint
+ /// from a solicited authentication assertion for validation.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="ServiceEndpoint"/> object that has everything
+ /// except the <see cref="ProviderSupportedServiceTypeUris"/>
+ /// deserialized.
+ /// </returns>
+ internal static ServiceEndpoint Deserialize(TextReader reader) {
+ var claimedIdentifier = Identifier.Parse(reader.ReadLine());
+ var providerLocalIdentifier = Identifier.Parse(reader.ReadLine());
+ string userSuppliedIdentifier = reader.ReadLine();
+ if (userSuppliedIdentifier.Length == 0) userSuppliedIdentifier = null;
+ var providerEndpoint = new Uri(reader.ReadLine());
+ var protocol = Protocol.FindBestVersion(p => p.Version, new[] { new Version(reader.ReadLine()) });
+ return new ServiceEndpoint(claimedIdentifier, userSuppliedIdentifier,
+ providerEndpoint, providerLocalIdentifier, protocol);
+ }
+
+ public static bool operator ==(ServiceEndpoint se1, ServiceEndpoint se2) {
+ if ((object)se1 == null ^ (object)se2 == null) return false;
+ if ((object)se1 == null) return true;
+ return se1.Equals(se2);
+ }
+ public static bool operator !=(ServiceEndpoint se1, ServiceEndpoint se2) {
+ return !(se1 == se2);
+ }
+ public override bool Equals(object obj) {
+ ServiceEndpoint other = obj as ServiceEndpoint;
+ if (other == null) return false;
+ // We specifically do not check our ProviderSupportedServiceTypeUris array
+ // or the priority field
+ // as that is not persisted in our tokens, and it is not part of the
+ // important assertion validation that is part of the spec.
+ return
+ this.ClaimedIdentifier == other.ClaimedIdentifier &&
+ this.ProviderEndpoint == other.ProviderEndpoint &&
+ this.ProviderLocalIdentifier == other.ProviderLocalIdentifier &&
+ this.Protocol == other.Protocol;
+ }
+ public override int GetHashCode() {
+ return ClaimedIdentifier.GetHashCode();
+ }
+ public override string ToString() {
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("ClaimedIdentifier: " + ClaimedIdentifier);
+ builder.AppendLine("ProviderLocalIdentifier: " + ProviderLocalIdentifier);
+ builder.AppendLine("ProviderEndpoint: " + ProviderEndpoint.AbsoluteUri);
+ builder.AppendLine("OpenID version: " + Protocol.Version);
+ builder.AppendLine("Service Type URIs:");
+ if (ProviderSupportedServiceTypeUris != null) {
+ foreach (string serviceTypeUri in ProviderSupportedServiceTypeUris) {
+ builder.Append("\t");
+ // TODO: uncomment when we support extensions
+ ////var matchingExtension = Util.FirstOrDefault(ExtensionManager.RequestExtensions, ext => ext.Key.TypeUri == serviceTypeUri);
+ ////if (matchingExtension.Key != null) {
+ //// builder.AppendLine(string.Format(CultureInfo.CurrentCulture, "{0} ({1})", serviceTypeUri, matchingExtension.Value));
+ ////} else {
+ //// builder.AppendLine(serviceTypeUri);
+ ////}
+ }
+ } else {
+ builder.AppendLine("\t(unavailable)");
+ }
+ builder.Length -= Environment.NewLine.Length; // trim last newline
+ return builder.ToString();
+ }
+
+ #region IXrdsProviderEndpoint Members
+
+ private int? servicePriority;
+ /// <summary>
+ /// Gets the priority associated with this service that may have been given
+ /// in the XRDS document.
+ /// </summary>
+ int? IXrdsProviderEndpoint.ServicePriority {
+ get { return servicePriority; }
+ }
+ private int? uriPriority;
+ /// <summary>
+ /// Gets the priority associated with the service endpoint URL.
+ /// </summary>
+ int? IXrdsProviderEndpoint.UriPriority {
+ get { return uriPriority; }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth/OpenId/RelyingPartyDescription.cs b/src/DotNetOpenAuth/OpenId/RelyingPartyDescription.cs
index 263daac..1dd9734 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingPartyDescription.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingPartyDescription.cs
@@ -13,6 +13,38 @@ namespace DotNetOpenAuth.OpenId {
/// <summary>
/// A description of some OpenID Relying Party endpoint.
/// </summary>
+ /// <remarks>
+ /// This is an immutable type.
+ /// </remarks>
internal class RelyingPartyEndpointDescription {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RelyingPartyEndpointDescription"/> class.
+ /// </summary>
+ /// <param name="returnTo">The return to.</param>
+ /// <param name="supportedServiceTypeUris">
+ /// The Type URIs of supported services advertised on a relying party's XRDS document.
+ /// </param>
+ internal RelyingPartyEndpointDescription(Uri returnTo, string[] supportedServiceTypeUris) {
+ this.ReturnToEndpoint = returnTo;
+ this.Protocol = GetProtocolFromServices(supportedServiceTypeUris);
+ }
+
+ /// <summary>
+ /// The URL to the login page on the discovered relying party web site.
+ /// </summary>
+ public Uri ReturnToEndpoint { get; private set; }
+
+ /// <summary>
+ /// The OpenId protocol that the discovered relying party supports.
+ /// </summary>
+ public Protocol Protocol { get; private set; }
+
+ private static Protocol GetProtocolFromServices(string[] supportedServiceTypeUris) {
+ Protocol protocol = Protocol.FindBestVersion(p => p.RPReturnToTypeURI, supportedServiceTypeUris);
+ if (protocol == null) {
+ throw new InvalidOperationException("Unable to determine the version of OpenID the Relying Party supports.");
+ }
+ return protocol;
+ }
}
}
diff --git a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs
index 865b895..1618c95 100644
--- a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs
+++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs
@@ -6,12 +6,15 @@
namespace DotNetOpenAuth.OpenId {
using System;
+ using System.Linq;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using System.Web.UI.HtmlControls;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Yadis;
+ using DotNetOpenAuth.Xrds;
/// <summary>
/// A URI style of OpenID Identifier.
@@ -193,7 +196,6 @@ namespace DotNetOpenAuth.OpenId {
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>
@@ -215,7 +217,7 @@ namespace DotNetOpenAuth.OpenId {
Uri providerEndpoint = null;
Protocol discoveredProtocol = null;
Identifier providerLocalIdentifier = null;
- var linkTags = new List<HtmlLink>(Yadis.HtmlParser.HeadTags<HtmlLink>(html));
+ var linkTags = new List<HtmlLink>(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,
@@ -253,14 +255,14 @@ namespace DotNetOpenAuth.OpenId {
internal override IEnumerable<ServiceEndpoint> Discover() {
List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
// Attempt YADIS discovery
- DiscoveryResult yadisResult = Yadis.Yadis.Discover(this, IsDiscoverySecureEndToEnd);
+ DiscoveryResult yadisResult = 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);
+ xrdsEndpoints = xrdsEndpoints.Where(se => se.IsSecure);
}
endpoints.AddRange(xrdsEndpoints);
}
@@ -284,7 +286,6 @@ namespace DotNetOpenAuth.OpenId {
}
return endpoints;
}
-#endif
/// <summary>
/// Returns an <see cref="Identifier"/> that has no URI fragment.
diff --git a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs
index 4e92eff..13c8cdf 100644
--- a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs
+++ b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs
@@ -11,6 +11,8 @@ namespace DotNetOpenAuth.OpenId {
using System.Xml;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Xrds;
+ using DotNetOpenAuth.Yadis;
/// <summary>
/// An XRI style of OpenID Identifier.
@@ -48,7 +50,9 @@ namespace DotNetOpenAuth.OpenId {
/// Initializes a new instance of the <see cref="XriIdentifier"/> class.
/// </summary>
/// <param name="xri">The string value of the XRI.</param>
- internal XriIdentifier(string xri) : this(xri, false) { }
+ internal XriIdentifier(string xri)
+ : this(xri, false) {
+ }
/// <summary>
/// Initializes a new instance of the <see cref="XriIdentifier"/> class.
@@ -145,28 +149,24 @@ namespace DotNetOpenAuth.OpenId {
|| xri.StartsWith(XriScheme, StringComparison.OrdinalIgnoreCase);
}
-#if DISCOVERY // TODO: Add discovery and then re-enable this code block
- ////private 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
+ private XrdsDocument downloadXrds() {
+ var xrdsResponse = Yadis.Request(this.XrdsUrl, this.IsDiscoverySecureEndToEnd);
+ XrdsDocument doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream));
+ ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.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);
+ }
/// <summary>
/// Returns an <see cref="Identifier"/> that has no URI fragment.
diff --git a/src/DotNetOpenAuth/Util.cs b/src/DotNetOpenAuth/Util.cs
index a043c7a..d35c4e8 100644
--- a/src/DotNetOpenAuth/Util.cs
+++ b/src/DotNetOpenAuth/Util.cs
@@ -9,6 +9,8 @@ namespace DotNetOpenAuth {
using System.Globalization;
using System.Linq;
using System.Reflection;
+ using System.Text;
+ using System.Net;
/// <summary>
/// A grab-bag utility class.
@@ -49,5 +51,87 @@ namespace DotNetOpenAuth {
// Neither are null. Delegate to the Equals method.
return first.Equals(second);
}
+
+ /// <summary>
+ /// Prepares a dictionary for printing as a string.
+ /// </summary>
+ /// <remarks>
+ /// The work isn't done until (and if) the
+ /// <see cref="Object.ToString"/> method is actually called, which makes it great
+ /// for logging complex objects without being in a conditional block.
+ /// </remarks>
+ internal static object ToStringDeferred<K, V>(this IEnumerable<KeyValuePair<K, V>> pairs) {
+ return new DelayedToString<IEnumerable<KeyValuePair<K, V>>>(pairs, p => {
+ var dictionary = pairs as IDictionary<K, V>;
+ StringBuilder sb = new StringBuilder(dictionary != null ? dictionary.Count * 40 : 200);
+ foreach (var pair in pairs) {
+ sb.AppendFormat("\t{0}: {1}{2}", pair.Key, pair.Value, Environment.NewLine);
+ }
+ return sb.ToString();
+ });
+ }
+ internal static object ToStringDeferred<T>(this IEnumerable<T> list) {
+ return ToStringDeferred<T>(list, false);
+ }
+ internal static object ToStringDeferred<T>(this IEnumerable<T> list, bool multiLineElements) {
+ return new DelayedToString<IEnumerable<T>>(list, l => {
+ StringBuilder sb = new StringBuilder();
+ if (multiLineElements) {
+ sb.AppendLine("[{");
+ foreach (T obj in l) {
+ // Prepare the string repersentation of the object
+ string objString = obj != null ? obj.ToString() : "<NULL>";
+
+ // Indent every line printed
+ objString = objString.Replace(Environment.NewLine, Environment.NewLine + "\t");
+ sb.Append("\t");
+ sb.Append(objString);
+
+ if (!objString.EndsWith(Environment.NewLine)) {
+ sb.AppendLine();
+ }
+ sb.AppendLine("}, {");
+ }
+ if (sb.Length > 2) { // if anything was in the enumeration
+ sb.Length -= 2 + Environment.NewLine.Length; // trim off the last ", {\r\n"
+ } else {
+ sb.Length -= 1; // trim off the opening {
+ }
+ sb.Append("]");
+ return sb.ToString();
+ } else {
+ sb.Append("{");
+ foreach (T obj in l) {
+ sb.Append(obj != null ? obj.ToString() : "<NULL>");
+ sb.AppendLine(",");
+ }
+ if (sb.Length > 1) {
+ sb.Length -= 1;
+ }
+ sb.Append("}");
+ return sb.ToString();
+ }
+ });
+ }
+
+ private class DelayedToString<T> {
+ public DelayedToString(T obj, Func<T, string> toString) {
+ this.obj = obj;
+ this.toString = toString;
+ }
+ T obj;
+ Func<T, string> toString;
+ public override string ToString() {
+ return toString(obj);
+ }
+ }
+
+ internal static HttpWebRequest CreatePostRequest(Uri requestUri, string body) {
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri);
+ request.ContentType = "application/x-www-form-urlencoded";
+ request.ContentLength = body.Length;
+ request.Method = "POST";
+ return request;
+ }
}
}
diff --git a/src/DotNetOpenAuth/Xrds/ServiceElement.cs b/src/DotNetOpenAuth/Xrds/ServiceElement.cs
new file mode 100644
index 0000000..91c671e
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/ServiceElement.cs
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------
+// <copyright file="ServiceElement.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System;
+ using System.Collections.Generic;
+ using System.Xml.XPath;
+ using DotNetOpenAuth.OpenId;
+
+ internal class ServiceElement : XrdsNode, IComparable<ServiceElement> {
+ public ServiceElement(XPathNavigator serviceElement, XrdElement parent) :
+ base(serviceElement, parent) {
+ }
+
+ public XrdElement Xrd {
+ get { return (XrdElement)ParentNode; }
+ }
+
+ public int? Priority {
+ get {
+ XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver);
+ return n != null ? n.ValueAsInt : (int?)null;
+ }
+ }
+
+ public IEnumerable<UriElement> UriElements {
+ get {
+ List<UriElement> uris = new List<UriElement>();
+ foreach (XPathNavigator node in Node.Select("xrd:URI", XmlNamespaceResolver)) {
+ uris.Add(new UriElement(node, this));
+ }
+ uris.Sort();
+ return uris;
+ }
+ }
+
+ public IEnumerable<TypeElement> TypeElements {
+ get {
+ foreach (XPathNavigator node in Node.Select("xrd:Type", XmlNamespaceResolver)) {
+ yield return new TypeElement(node, this);
+ }
+ }
+ }
+
+ public string[] TypeElementUris {
+ get {
+ XPathNodeIterator types = Node.Select("xrd:Type", XmlNamespaceResolver);
+ string[] typeUris = new string[types.Count];
+ int i = 0;
+ foreach (XPathNavigator type in types) {
+ typeUris[i++] = type.Value;
+ }
+ return typeUris;
+ }
+ }
+
+ public Identifier ProviderLocalIdentifier {
+ get {
+ var n = Node.SelectSingleNode("xrd:LocalID", XmlNamespaceResolver)
+ ?? Node.SelectSingleNode("openid10:Delegate", XmlNamespaceResolver);
+ return (n != null) ? n.Value : null;
+ }
+ }
+
+ #region IComparable<ServiceElement> Members
+
+ public int CompareTo(ServiceElement other) {
+ if (other == null) return -1;
+ if (Priority.HasValue && other.Priority.HasValue) {
+ return Priority.Value.CompareTo(other.Priority.Value);
+ } else {
+ if (Priority.HasValue) {
+ return -1;
+ } else if (other.Priority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/TypeElement.cs b/src/DotNetOpenAuth/Xrds/TypeElement.cs
new file mode 100644
index 0000000..2770108
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/TypeElement.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------
+// <copyright file="TypeElement.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System.Xml.XPath;
+
+ internal class TypeElement : XrdsNode {
+ public TypeElement(XPathNavigator typeElement, ServiceElement parent) :
+ base(typeElement, parent) {
+ }
+
+ public string Uri {
+ get { return Node.Value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/UriElement.cs b/src/DotNetOpenAuth/Xrds/UriElement.cs
new file mode 100644
index 0000000..c8f159f
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/UriElement.cs
@@ -0,0 +1,53 @@
+//-----------------------------------------------------------------------
+// <copyright file="UriElement.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System;
+ using System.Xml.XPath;
+
+ internal class UriElement : XrdsNode, IComparable<UriElement> {
+ public UriElement(XPathNavigator uriElement, ServiceElement service) :
+ base(uriElement, service) {
+ }
+
+ public int? Priority {
+ get {
+ XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver);
+ return n != null ? n.ValueAsInt : (int?)null;
+ }
+ }
+
+ public Uri Uri {
+ get { return new Uri(Node.Value); }
+ }
+
+ public ServiceElement Service {
+ get { return (ServiceElement)ParentNode; }
+ }
+
+ #region IComparable<UriElement> Members
+
+ public int CompareTo(UriElement other) {
+ if (other == null) return -1;
+ int compare = Service.CompareTo(other.Service);
+ if (compare != 0) return compare;
+
+ if (Priority.HasValue && other.Priority.HasValue) {
+ return Priority.Value.CompareTo(other.Priority.Value);
+ } else {
+ if (Priority.HasValue) {
+ return -1;
+ } else if (other.Priority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/XrdElement.cs b/src/DotNetOpenAuth/Xrds/XrdElement.cs
new file mode 100644
index 0000000..726081c
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/XrdElement.cs
@@ -0,0 +1,115 @@
+//-----------------------------------------------------------------------
+// <copyright file="XrdElement.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+ using System.Xml.XPath;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.Messaging;
+
+ class XrdElement : XrdsNode {
+ public XrdElement(XPathNavigator xrdElement, XrdsDocument parent) :
+ base(xrdElement, parent) {
+ }
+
+ public IEnumerable<ServiceElement> Services {
+ get {
+ // We should enumerate them in priority order
+ List<ServiceElement> services = new List<ServiceElement>();
+ foreach (XPathNavigator node in Node.Select("xrd:Service", XmlNamespaceResolver)) {
+ services.Add(new ServiceElement(node, this));
+ }
+ services.Sort();
+ return services;
+ }
+ }
+
+ private int XriResolutionStatusCode {
+ get {
+ var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver);
+ string codeString = null;
+ ErrorUtilities.VerifyProtocol(n != null && !string.IsNullOrEmpty(codeString = n.GetAttribute("code", string.Empty)), XrdsStrings.XriResolutionStatusMissing);
+ int code;
+ ErrorUtilities.VerifyProtocol(int.TryParse(codeString, out code) && code >= 100 && code < 400, XrdsStrings.XriResolutionStatusMissing);
+ return code;
+ }
+ }
+
+ public bool IsXriResolutionSuccessful {
+ get {
+ return XriResolutionStatusCode == 100;
+ }
+ }
+
+ public string CanonicalID {
+ get {
+ var n = Node.SelectSingleNode("xrd:CanonicalID", XmlNamespaceResolver);
+ return n != null ? n.Value : null;
+ }
+ }
+
+ public bool IsCanonicalIdVerified {
+ get {
+ var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver);
+ return n != null && string.Equals(n.GetAttribute("cid", string.Empty), "verified", StringComparison.Ordinal);
+ }
+ }
+
+ IEnumerable<ServiceElement> searchForServiceTypeUris(Func<Protocol, string> p) {
+ var xpath = new StringBuilder();
+ xpath.Append("xrd:Service[");
+ foreach (var protocol in Protocol.AllVersions) {
+ string typeUri = p(protocol);
+ if (typeUri == null) continue;
+ xpath.Append("xrd:Type/text()='");
+ xpath.Append(typeUri);
+ xpath.Append("' or ");
+ }
+ xpath.Length -= 4;
+ xpath.Append("]");
+ var services = new List<ServiceElement>();
+ foreach (XPathNavigator service in Node.Select(xpath.ToString(), XmlNamespaceResolver)) {
+ services.Add(new ServiceElement(service, this));
+ }
+ // Put the services in their own defined priority order
+ services.Sort();
+ return services;
+ }
+
+ /// <summary>
+ /// Returns services for OP Identifiers.
+ /// </summary>
+ public IEnumerable<ServiceElement> OpenIdProviderIdentifierServices {
+ get { return searchForServiceTypeUris(p => p.OPIdentifierServiceTypeURI); }
+ }
+
+ /// <summary>
+ /// Returns services for Claimed Identifiers.
+ /// </summary>
+ public IEnumerable<ServiceElement> OpenIdClaimedIdentifierServices {
+ get { return searchForServiceTypeUris(p => p.ClaimedIdentifierServiceTypeURI); }
+ }
+
+ public IEnumerable<ServiceElement> OpenIdRelyingPartyReturnToServices {
+ get { return searchForServiceTypeUris(p => p.RPReturnToTypeURI); }
+ }
+
+ /// <summary>
+ /// An enumeration of all Service/URI elements, sorted in priority order.
+ /// </summary>
+ public IEnumerable<UriElement> ServiceUris {
+ get {
+ foreach (ServiceElement service in Services) {
+ foreach (UriElement uri in service.UriElements) {
+ yield return uri;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/XrdsDocument.cs b/src/DotNetOpenAuth/Xrds/XrdsDocument.cs
new file mode 100644
index 0000000..68e1479
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/XrdsDocument.cs
@@ -0,0 +1,157 @@
+//-----------------------------------------------------------------------
+// <copyright file="XrdsDocument.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Xml;
+ using System.Xml.XPath;
+ using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.Messaging;
+
+ internal class XrdsDocument : XrdsNode {
+ public XrdsDocument(XPathNavigator xrdsNavigator)
+ : base(xrdsNavigator) {
+ XmlNamespaceResolver.AddNamespace("xrd", XrdsNode.XrdNamespace);
+ XmlNamespaceResolver.AddNamespace("xrds", XrdsNode.XrdsNamespace);
+ XmlNamespaceResolver.AddNamespace("openid10", Protocol.V10.XmlNamespace);
+ }
+ public XrdsDocument(XmlReader reader)
+ : this(new XPathDocument(reader).CreateNavigator()) { }
+ public XrdsDocument(string xml)
+ : this(new XPathDocument(new StringReader(xml)).CreateNavigator()) { }
+
+ public IEnumerable<XrdElement> XrdElements {
+ get {
+ // We may be looking at a full XRDS document (in the case of YADIS discovery)
+ // or we may be looking at just an individual XRD element from a larger document
+ // if we asked xri.net for just one.
+ if (Node.SelectSingleNode("/xrds:XRDS", XmlNamespaceResolver) != null) {
+ foreach (XPathNavigator node in Node.Select("/xrds:XRDS/xrd:XRD", XmlNamespaceResolver)) {
+ yield return new XrdElement(node, this);
+ }
+ } else {
+ XPathNavigator node = Node.SelectSingleNode("/xrd:XRD", XmlNamespaceResolver);
+ yield return new XrdElement(node, this);
+ }
+ }
+ }
+
+ internal IEnumerable<ServiceEndpoint> CreateServiceEndpoints(UriIdentifier claimedIdentifier) {
+ var endpoints = new List<ServiceEndpoint>();
+ endpoints.AddRange(this.generateOPIdentifierServiceEndpoints(claimedIdentifier));
+ // If any OP Identifier service elements were found, we must not proceed
+ // to return any Claimed Identifier services.
+ if (endpoints.Count == 0) {
+ endpoints.AddRange(this.generateClaimedIdentifierServiceEndpoints(claimedIdentifier));
+ }
+ Logger.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Debug(endpoints.ToStringDeferred(true));
+ return endpoints;
+ }
+
+ internal IEnumerable<ServiceEndpoint> CreateServiceEndpoints(XriIdentifier userSuppliedIdentifier) {
+ var endpoints = new List<ServiceEndpoint>();
+ endpoints.AddRange(this.generateOPIdentifierServiceEndpoints(userSuppliedIdentifier));
+ // If any OP Identifier service elements were found, we must not proceed
+ // to return any Claimed Identifier services.
+ if (endpoints.Count == 0) {
+ endpoints.AddRange(generateClaimedIdentifierServiceEndpoints(userSuppliedIdentifier));
+ }
+ Logger.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Debug(endpoints.ToStringDeferred(true));
+ return endpoints;
+ }
+
+ IEnumerable<ServiceEndpoint> generateOPIdentifierServiceEndpoints(Identifier opIdentifier) {
+ foreach (var service in findOPIdentifierServices()) {
+ foreach (var uri in service.UriElements) {
+ var protocol = Protocol.FindBestVersion(p => p.OPIdentifierServiceTypeURI, service.TypeElementUris);
+ yield return ServiceEndpoint.CreateForProviderIdentifier(
+ opIdentifier, uri.Uri, service.TypeElementUris,
+ service.Priority, uri.Priority);
+ }
+ }
+ }
+
+ IEnumerable<ServiceEndpoint> generateClaimedIdentifierServiceEndpoints(UriIdentifier claimedIdentifier) {
+ foreach (var service in findClaimedIdentifierServices()) {
+ foreach (var uri in service.UriElements) {
+ yield return ServiceEndpoint.CreateForClaimedIdentifier(
+ claimedIdentifier, service.ProviderLocalIdentifier,
+ uri.Uri, service.TypeElementUris, service.Priority, uri.Priority);
+ }
+ }
+ }
+
+ IEnumerable<ServiceEndpoint> generateClaimedIdentifierServiceEndpoints(XriIdentifier userSuppliedIdentifier) {
+ foreach (var service in findClaimedIdentifierServices()) {
+ foreach (var uri in service.UriElements) {
+ // spec section 7.3.2.3 on Claimed Id -> CanonicalID substitution
+ if (service.Xrd.CanonicalID == null) {
+ Logger.WarnFormat(XrdsStrings.MissingCanonicalIDElement, userSuppliedIdentifier);
+ break; // skip on to next service
+ }
+ ErrorUtilities.VerifyProtocol(service.Xrd.IsCanonicalIdVerified, XrdsStrings.CIDVerificationFailed, userSuppliedIdentifier);
+ // In the case of XRI names, the ClaimedId is actually the CanonicalID.
+ var claimedIdentifier = new XriIdentifier(service.Xrd.CanonicalID);
+ yield return ServiceEndpoint.CreateForClaimedIdentifier(
+ claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier,
+ uri.Uri, service.TypeElementUris, service.Priority, uri.Priority);
+ }
+ }
+ }
+
+ internal IEnumerable<RelyingPartyEndpointDescription> FindRelyingPartyReceivingEndpoints() {
+ foreach (var service in findReturnToServices()) {
+ foreach (var uri in service.UriElements) {
+ yield return new RelyingPartyEndpointDescription(uri.Uri, service.TypeElementUris);
+ }
+ }
+ }
+
+ IEnumerable<ServiceElement> findOPIdentifierServices() {
+ foreach (var xrd in this.XrdElements) {
+ foreach (var service in xrd.OpenIdProviderIdentifierServices) {
+ yield return service;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the OpenID-compatible services described by a given XRDS document,
+ /// in priority order.
+ /// </summary>
+ IEnumerable<ServiceElement> findClaimedIdentifierServices() {
+ foreach (var xrd in this.XrdElements) {
+ foreach (var service in xrd.OpenIdClaimedIdentifierServices) {
+ yield return service;
+ }
+ }
+ }
+
+ IEnumerable<ServiceElement> findReturnToServices() {
+ foreach (var xrd in this.XrdElements) {
+ foreach (var service in xrd.OpenIdRelyingPartyReturnToServices) {
+ yield return service;
+ }
+ }
+ }
+
+ internal bool IsXrdResolutionSuccessful {
+ get {
+ foreach (var xrd in this.XrdElements) {
+ if (!xrd.IsXriResolutionSuccessful) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/XrdsNode.cs b/src/DotNetOpenAuth/Xrds/XrdsNode.cs
new file mode 100644
index 0000000..a1da430
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/XrdsNode.cs
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------
+// <copyright file="XrdsNode.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System.Xml;
+ using System.Xml.XPath;
+
+ internal class XrdsNode {
+ /// <summary>
+ /// The XRD namespace xri://$xrd*($v*2.0)
+ /// </summary>
+ internal const string XrdNamespace = "xri://$xrd*($v*2.0)";
+
+ /// <summary>
+ /// The XRDS namespace xri://$xrds
+ /// </summary>
+ internal const string XrdsNamespace = "xri://$xrds";
+
+ protected XrdsNode(XPathNavigator node, XrdsNode parentNode) {
+ this.Node = node;
+ this.ParentNode = parentNode;
+ this.XmlNamespaceResolver = ParentNode.XmlNamespaceResolver;
+ }
+ protected XrdsNode(XPathNavigator document) {
+ this.Node = document;
+ this.XmlNamespaceResolver = new XmlNamespaceManager(document.NameTable);
+ }
+
+ protected XPathNavigator Node { get; private set; }
+
+ protected XrdsNode ParentNode { get; private set; }
+
+ protected XmlNamespaceManager XmlNamespaceResolver { get; private set; }
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs b/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs
new file mode 100644
index 0000000..465c990
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:2.0.50727.3053
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Xrds {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class XrdsStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal XrdsStrings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.Xrds.XrdsStrings", typeof(XrdsStrings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to XRI CanonicalID verification failed..
+ /// </summary>
+ internal static string CIDVerificationFailed {
+ get {
+ return ResourceManager.GetString("CIDVerificationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failure parsing XRDS document..
+ /// </summary>
+ internal static string InvalidXRDSDocument {
+ get {
+ return ResourceManager.GetString("InvalidXRDSDocument", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The XRDS document for XRI {0} is missing the required CanonicalID element..
+ /// </summary>
+ internal static string MissingCanonicalIDElement {
+ get {
+ return ResourceManager.GetString("MissingCanonicalIDElement", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not find XRI resolution Status tag or code attribute was invalid..
+ /// </summary>
+ internal static string XriResolutionStatusMissing {
+ get {
+ return ResourceManager.GetString("XriResolutionStatusMissing", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Xrds/XrdsStrings.resx b/src/DotNetOpenAuth/Xrds/XrdsStrings.resx
new file mode 100644
index 0000000..acb43f2
--- /dev/null
+++ b/src/DotNetOpenAuth/Xrds/XrdsStrings.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CIDVerificationFailed" xml:space="preserve">
+ <value>XRI CanonicalID verification failed.</value>
+ </data>
+ <data name="InvalidXRDSDocument" xml:space="preserve">
+ <value>Failure parsing XRDS document.</value>
+ </data>
+ <data name="MissingCanonicalIDElement" xml:space="preserve">
+ <value>The XRDS document for XRI {0} is missing the required CanonicalID element.</value>
+ </data>
+ <data name="XriResolutionStatusMissing" xml:space="preserve">
+ <value>Could not find XRI resolution Status tag or code attribute was invalid.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/Yadis/ContentTypes.cs b/src/DotNetOpenAuth/Yadis/ContentTypes.cs
new file mode 100644
index 0000000..30745ee
--- /dev/null
+++ b/src/DotNetOpenAuth/Yadis/ContentTypes.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------
+// <copyright file="ContentTypes.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Yadis {
+ /// <summary>
+ /// String constants for various content-type header values used in YADIS discovery.
+ /// </summary>
+ internal static class ContentTypes {
+ /// <summary>
+ /// The text/html content-type
+ /// </summary>
+ public const string Html = "text/html";
+
+ /// <summary>
+ /// The application/xhtml+xml content-type
+ /// </summary>
+ public const string XHtml = "application/xhtml+xml";
+
+ /// <summary>
+ /// The application/xrds+xml content-type
+ /// </summary>
+ public const string Xrds = "application/xrds+xml";
+
+ /// <summary>
+ /// The text/xml content type
+ /// </summary>
+ public const string Xml = "text/xml";
+ }
+}
diff --git a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs
new file mode 100644
index 0000000..f0d58e7
--- /dev/null
+++ b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs
@@ -0,0 +1,88 @@
+//-----------------------------------------------------------------------
+// <copyright file="DiscoveryResult.cs" company="Scott Hanselman, Andrew Arnott">
+// Copyright (c) Scott Hanselman, Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Yadis {
+ using System;
+ using System.IO;
+ using System.Net.Mime;
+ using System.Web.UI.HtmlControls;
+ using System.Xml;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Contains the result of YADIS discovery.
+ /// </summary>
+ internal class DiscoveryResult {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DiscoveryResult"/> class.
+ /// </summary>
+ /// <param name="requestUri">The user-supplied identifier.</param>
+ /// <param name="initialResponse">The initial response.</param>
+ /// <param name="finalResponse">The final response.</param>
+ public DiscoveryResult(Uri requestUri, DirectWebResponse initialResponse, DirectWebResponse finalResponse) {
+ RequestUri = requestUri;
+ NormalizedUri = initialResponse.FinalUri;
+ if (finalResponse == null) {
+ ContentType = initialResponse.ContentType;
+ ResponseText = initialResponse.Body;
+ IsXrds = ContentType.MediaType == ContentTypes.Xrds;
+ } else {
+ ContentType = finalResponse.ContentType;
+ ResponseText = finalResponse.Body;
+ IsXrds = true;
+ if (initialResponse != finalResponse) {
+ YadisLocation = finalResponse.RequestUri;
+ }
+ }
+ }
+
+ /// <summary>
+ /// The URI of the original YADIS discovery request.
+ /// This is the user supplied Identifier as given in the original
+ /// YADIS discovery request.
+ /// </summary>
+ public Uri RequestUri { get; private set; }
+
+ /// <summary>
+ /// Gets the fully resolved (after redirects) URL of the user supplied Identifier.
+ /// This becomes the ClaimedIdentifier.
+ /// </summary>
+ public Uri NormalizedUri { get; private set; }
+
+ /// <summary>
+ /// Gets the location the XRDS document was downloaded from, if different
+ /// from the user supplied Identifier.
+ /// </summary>
+ public Uri YadisLocation { get; private set; }
+
+ /// <summary>
+ /// The Content-Type associated with the <see cref="ResponseText"/>.
+ /// </summary>
+ public ContentType ContentType { get; private set; }
+
+ /// <summary>
+ /// Gets the text in the final response.
+ /// This may be an XRDS document or it may be an HTML document,
+ /// as determined by the <see cref="IsXrds"/> property.
+ /// </summary>
+ public string ResponseText { get; private set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="ResponseText"/>
+ /// represents an XRDS document. False if the response is an HTML document.
+ /// </summary>
+ public bool IsXrds { get; private set; }
+
+ /// <summary>
+ /// Gets whether discovery resulted in an XRDS document at a referred location.
+ /// </summary>
+ /// <value><c>true</c> if the response to the userSuppliedIdentifier pointed to a different URL
+ /// for the XRDS document.</value>
+ public bool UsedYadisLocation {
+ get { return YadisLocation != null; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Yadis/HtmlParser.cs b/src/DotNetOpenAuth/Yadis/HtmlParser.cs
new file mode 100644
index 0000000..7d1283e
--- /dev/null
+++ b/src/DotNetOpenAuth/Yadis/HtmlParser.cs
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------
+// <copyright file="HtmlParser.cs" company="Andrew Arnott, Scott Hanselman, Jason Alexander">
+// Copyright (c) Andrew Arnott, Scott Hanselman, Jason Alexander. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Yadis {
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Web;
+ using System.Web.UI.HtmlControls;
+
+ internal static class HtmlParser {
+ private static readonly Regex attrRe = new Regex("\n# Must start with a sequence of word-characters, followed by an equals sign\n(?<attrname>(\\w|-)+)=\n\n# Then either a quoted or unquoted attribute\n(?:\n\n # Match everything that's between matching quote marks\n (?<qopen>[\"\\'])(?<attrval>.*?)\\k<qopen>\n|\n\n # If the value is not quoted, match up to whitespace\n (?<attrval>(?:[^\\s<>/]|/(?!>))+)\n)\n\n|\n\n(?<endtag>[<>])\n ", flags);
+ private const RegexOptions flags = (RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private const string tagExpr = "\n# Starts with the tag name at a word boundary, where the tag name is\n# not a namespace\n<{0}\\b(?!:)\n \n# All of the stuff up to a \">\", hopefully attributes.\n(?<attrs>[^>]*?)\n \n(?: # Match a short tag\n />\n \n| # Match a full tag\n >\n \n (?<contents>.*?)\n \n # Closed by\n (?: # One of the specified close tags\n </?{1}\\s*>\n \n # End of the string\n | \\Z\n \n )\n \n)\n ";
+ private const string startTagExpr = "\n# Starts with the tag name at a word boundary, where the tag name is\n# not a namespace\n<{0}\\b(?!:)\n \n# All of the stuff up to a \">\", hopefully attributes.\n(?<attrs>[^>]*?)\n \n(?: # Match a short tag\n />\n \n| # Match a full tag\n >\n )\n ";
+
+ private static readonly Regex headRe = tagMatcher("head", new[] { "body" });
+ private static readonly Regex htmlRe = tagMatcher("html", new string[0]);
+ private static readonly Regex removedRe = new Regex(@"<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b[^>]*>.*?</script>", flags);
+
+ public static IEnumerable<T> HeadTags<T>(string html) where T : HtmlControl, new() {
+ html = removedRe.Replace(html, string.Empty);
+ Match match = htmlRe.Match(html);
+ string tagName = (new T()).TagName;
+ if (match.Success) {
+ Match match2 = headRe.Match(html, match.Index, match.Length);
+ if (match2.Success) {
+ string text = null;
+ string text2 = null;
+ Regex regex = startTagMatcher(tagName);
+ for (Match match3 = regex.Match(html, match2.Index, match2.Length); match3.Success; match3 = match3.NextMatch()) {
+ int beginning = (match3.Index + tagName.Length) + 1;
+ int length = (match3.Index + match3.Length) - beginning;
+ Match match4 = attrRe.Match(html, beginning, length);
+ var headTag = new T();
+ while (match4.Success) {
+ if (match4.Groups["endtag"].Success) {
+ break;
+ }
+ text = match4.Groups["attrname"].Value;
+ text2 = HttpUtility.HtmlDecode(match4.Groups["attrval"].Value);
+ headTag.Attributes.Add(text, text2);
+ match4 = match4.NextMatch();
+ }
+ yield return headTag;
+ }
+ }
+ }
+ }
+
+ static Regex tagMatcher(string tagName, params string[] closeTags) {
+ string text2;
+ if (closeTags.Length > 0) {
+ StringBuilder builder = new StringBuilder();
+ builder.AppendFormat("(?:{0}", tagName);
+ int index = 0;
+ string[] textArray = closeTags;
+ int length = textArray.Length;
+ while (index < length) {
+ string text = textArray[index];
+ index++;
+ builder.AppendFormat("|{0}", text);
+ }
+ builder.Append(")");
+ text2 = builder.ToString();
+ } else {
+ text2 = tagName;
+ }
+ return new Regex(string.Format(CultureInfo.InvariantCulture,
+ tagExpr, tagName, text2), flags);
+ }
+
+ static Regex startTagMatcher(string tag_name) {
+ return new Regex(string.Format(CultureInfo.InvariantCulture, startTagExpr, tag_name), flags);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs
new file mode 100644
index 0000000..7765575
--- /dev/null
+++ b/src/DotNetOpenAuth/Yadis/Yadis.cs
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------
+// <copyright file="Yadis.cs" company="Andrew Arnott, Scott Hanselman">
+// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Yadis {
+ using System;
+ using System.IO;
+ using System.Net.Mime;
+ using System.Web.UI.HtmlControls;
+ using System.Xml;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Xrds;
+ using System.Net.Cache;
+ using System.Net;
+
+ internal class Yadis {
+ internal const string HeaderName = "X-XRDS-Location";
+
+ private static readonly IDirectWebRequestHandler discoveryRequestHandlerSsl = new UntrustedWebRequestHandler(true);
+ private static readonly IDirectWebRequestHandler discoveryRequestHandler = new UntrustedWebRequestHandler(false);
+
+ /// <summary>
+ /// Gets or sets the cache that can be used for HTTP requests made during identifier discovery.
+ /// </summary>
+ internal readonly static RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.CacheIfAvailable);
+
+ internal static DirectWebResponse Request(Uri uri, bool requireSsl, params string[] acceptTypes) {
+ ErrorUtilities.VerifyArgumentNotNull(uri, "uri");
+
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
+ request.CachePolicy = IdentifierDiscoveryCachePolicy;
+ if (acceptTypes != null) {
+ request.Accept = string.Join(",", acceptTypes);
+ }
+
+ IDirectWebRequestHandler handler = requireSsl ? discoveryRequestHandlerSsl : discoveryRequestHandler;
+ return handler.GetResponse(request);
+ }
+
+ /// <summary>
+ /// Performs YADIS discovery on some identifier.
+ /// </summary>
+ /// <param name="uri">The URI to perform discovery on.</param>
+ /// <param name="requireSsl">Whether discovery should fail if any step of it is not encrypted.</param>
+ /// <returns>
+ /// The result of discovery on the given URL.
+ /// Null may be returned if an error occurs,
+ /// or if <paramref name="requireSsl"/> is true but part of discovery
+ /// is not protected by SSL.
+ /// </returns>
+ public static DiscoveryResult Discover(UriIdentifier uri, bool requireSsl) {
+ DirectWebResponse response;
+ try {
+ if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ Logger.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri);
+ return null;
+ }
+ response = Request(uri, requireSsl, ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds);
+ if (response.Status != System.Net.HttpStatusCode.OK) {
+ return null;
+ }
+ } catch (ArgumentException ex) {
+ // Unsafe URLs generate this
+ Logger.WarnFormat("Unsafe OpenId URL detected ({0}). Request aborted. {1}", uri, ex);
+ return null;
+ }
+ DirectWebResponse response2 = null;
+ if (isXrdsDocument(response)) {
+ Logger.Debug("An XRDS response was received from GET at user-supplied identifier.");
+ response2 = response;
+ } else {
+ string uriString = response.Headers.Get(HeaderName);
+ Uri url = null;
+ if (uriString != null) {
+ if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) {
+ Logger.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url);
+ }
+ }
+ if (url == null && response.ContentType.MediaType == ContentTypes.Html) {
+ url = FindYadisDocumentLocationInHtmlMetaTags(response.Body);
+ if (url != null) {
+ Logger.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url);
+ }
+ }
+ if (url != null) {
+ if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ response2 = Request(url, requireSsl);
+ if (response2.Status != System.Net.HttpStatusCode.OK) {
+ return null;
+ }
+ } else {
+ Logger.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url);
+ }
+ }
+ }
+ return new DiscoveryResult(uri, response, response2);
+ }
+
+ private static bool isXrdsDocument(DirectWebResponse response) {
+ if (response.ContentType.MediaType == ContentTypes.Xrds) {
+ return true;
+ }
+
+ if (response.ContentType.MediaType == ContentTypes.Xml) {
+ // This COULD be an XRDS document with an imprecise content-type.
+ XmlReader reader = XmlReader.Create(new StringReader(response.Body));
+ while (reader.Read() && reader.NodeType != XmlNodeType.Element) {
+ // intentionally blank
+ }
+ if (reader.NamespaceURI == XrdsNode.XrdsNamespace && reader.Name == "XRDS") {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Searches an HTML document for a
+ /// &lt;meta http-equiv="X-XRDS-Location" content="{YadisURL}"&gt;
+ /// tag and returns the content of YadisURL.
+ /// </summary>
+ public static Uri FindYadisDocumentLocationInHtmlMetaTags(string html) {
+ foreach (var metaTag in HtmlParser.HeadTags<HtmlMeta>(html)) {
+ if (HeaderName.Equals(metaTag.HttpEquiv, StringComparison.OrdinalIgnoreCase)) {
+ if (metaTag.Content != null) {
+ Uri uri;
+ if (Uri.TryCreate(metaTag.Content, UriKind.Absolute, out uri))
+ return uri;
+ }
+ }
+ }
+ return null;
+ }
+ }
+}