summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs2
-rw-r--r--src/DotNetOpenAuth.Test/Mocks/TestBadChannel.cs2
-rw-r--r--src/DotNetOpenAuth.Test/Mocks/TestChannel.cs2
-rw-r--r--src/DotNetOpenAuth.Test/Mocks/TestWebRequestHandler.cs6
-rw-r--r--src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs6
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs2
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs66
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs410
-rw-r--r--src/DotNetOpenAuth.Test/OpenId/XriIdentifierTests.cs470
-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
47 files changed, 3243 insertions, 115 deletions
diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs
index 2f82c06..e10cda5 100644
--- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs
+++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs
@@ -68,7 +68,7 @@ namespace DotNetOpenAuth.Test.Mocks {
return request.Message;
}
- protected override IDictionary<string, string> ReadFromResponseInternal(Response response) {
+ protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) {
Channel_Accessor accessor = Channel_Accessor.AttachShadow(this.wrappedChannel);
return accessor.ReadFromResponseInternal(response);
}
diff --git a/src/DotNetOpenAuth.Test/Mocks/TestBadChannel.cs b/src/DotNetOpenAuth.Test/Mocks/TestBadChannel.cs
index 515766e..cc63f22 100644
--- a/src/DotNetOpenAuth.Test/Mocks/TestBadChannel.cs
+++ b/src/DotNetOpenAuth.Test/Mocks/TestBadChannel.cs
@@ -37,7 +37,7 @@ namespace DotNetOpenAuth.Test.Mocks {
return base.ReadFromRequest(request);
}
- protected override IDictionary<string, string> ReadFromResponseInternal(Response response) {
+ protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) {
throw new NotImplementedException();
}
diff --git a/src/DotNetOpenAuth.Test/Mocks/TestChannel.cs b/src/DotNetOpenAuth.Test/Mocks/TestChannel.cs
index 0f7f4b8..11dc97e 100644
--- a/src/DotNetOpenAuth.Test/Mocks/TestChannel.cs
+++ b/src/DotNetOpenAuth.Test/Mocks/TestChannel.cs
@@ -19,7 +19,7 @@ namespace DotNetOpenAuth.Test.Mocks {
: base(messageTypeProvider, bindingElements) {
}
- protected override IDictionary<string, string> ReadFromResponseInternal(Response response) {
+ protected override IDictionary<string, string> ReadFromResponseInternal(DirectWebResponse response) {
throw new NotImplementedException("ReadFromResponseInternal");
}
diff --git a/src/DotNetOpenAuth.Test/Mocks/TestWebRequestHandler.cs b/src/DotNetOpenAuth.Test/Mocks/TestWebRequestHandler.cs
index 7117bc1..af971a0 100644
--- a/src/DotNetOpenAuth.Test/Mocks/TestWebRequestHandler.cs
+++ b/src/DotNetOpenAuth.Test/Mocks/TestWebRequestHandler.cs
@@ -12,13 +12,13 @@ namespace DotNetOpenAuth.Test.Mocks {
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth.ChannelElements;
- internal class TestWebRequestHandler : IWebRequestHandler {
+ internal class TestWebRequestHandler : IDirectWebRequestHandler {
private StringBuilder postEntity;
/// <summary>
/// Gets or sets the callback used to provide the mock response for the mock request.
/// </summary>
- internal Func<HttpWebRequest, Response> Callback { get; set; }
+ internal Func<HttpWebRequest, DirectWebResponse> Callback { get; set; }
/// <summary>
/// Gets the stream that was written out as if on an HTTP request.
@@ -63,7 +63,7 @@ namespace DotNetOpenAuth.Test.Mocks {
/// <returns>
/// An instance of <see cref="Response"/> describing the response.
/// </returns>
- public Response GetResponse(HttpWebRequest request) {
+ public DirectWebResponse GetResponse(HttpWebRequest request) {
if (this.Callback == null) {
throw new InvalidOperationException("Set the Callback property first.");
}
diff --git a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs
index b1fe7c4..73542b4 100644
--- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs
+++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs
@@ -111,7 +111,7 @@ namespace DotNetOpenAuth.Test.ChannelElements {
writer.Flush();
ms.Seek(0, SeekOrigin.Begin);
Channel_Accessor channelAccessor = Channel_Accessor.AttachShadow(this.channel);
- IDictionary<string, string> deserializedFields = channelAccessor.ReadFromResponseInternal(new Response { ResponseStream = ms });
+ IDictionary<string, string> deserializedFields = channelAccessor.ReadFromResponseInternal(new DirectWebResponse { ResponseStream = ms });
Assert.AreEqual(fields.Count, deserializedFields.Count);
foreach (string key in fields.Keys) {
Assert.AreEqual(fields[key], deserializedFields[key]);
@@ -228,7 +228,7 @@ namespace DotNetOpenAuth.Test.ChannelElements {
HttpMethods = scheme,
};
- Response rawResponse = null;
+ DirectWebResponse rawResponse = null;
this.webRequestHandler.Callback = (req) => {
Assert.IsNotNull(req);
HttpRequestInfo reqInfo = ConvertToRequestInfo(req, this.webRequestHandler.RequestEntityStream);
@@ -246,7 +246,7 @@ namespace DotNetOpenAuth.Test.ChannelElements {
{ "Location", request.Location.AbsoluteUri },
{ "Timestamp", XmlConvert.ToString(request.Timestamp, XmlDateTimeSerializationMode.Utc) },
};
- rawResponse = new Response {
+ rawResponse = new DirectWebResponse {
Body = MessagingUtilities.CreateQueryString(responseFields),
};
return rawResponse;
diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs
index cd4b4b6..304473c 100644
--- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs
+++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/OpenIdChannelTests.cs
@@ -79,7 +79,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements {
{ "var1", "value1" },
{ "var2", "value2" },
};
- Response response = new Response {
+ var response = new DirectWebResponse {
ResponseStream = new MemoryStream(KeyValueFormEncoding.GetBytes(fields)),
};
Assert.IsTrue(MessagingUtilities.AreEquivalent(fields, this.accessor.ReadFromResponseInternal(response)));
diff --git a/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs
new file mode 100644
index 0000000..8c3bc3b
--- /dev/null
+++ b/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using DotNetOpenAuth.OpenId;
+
+namespace DotNetOpenAuth.Test.OpenId {
+ [TestClass]
+ public class IdentifierTests {
+ string uri = "http://www.yahoo.com/";
+ string uriNoScheme = "www.yahoo.com";
+ string uriHttps = "https://www.yahoo.com/";
+ string xri = "=arnott*andrew";
+
+ [TestMethod]
+ public void Parse() {
+ Assert.IsInstanceOfType(typeof(UriIdentifier), Identifier.Parse(uri));
+ Assert.IsInstanceOfType(typeof(XriIdentifier), Identifier.Parse(xri));
+ }
+
+ /// <summary>
+ /// Tests conformance with 2.0 spec section 7.2#2
+ /// </summary>
+ [TestMethod]
+ public void ParseEndUserSuppliedXriIdentifer() {
+ List<char> symbols = new List<char>(XriIdentifier.GlobalContextSymbols);
+ symbols.Add('(');
+ List<string> prefixes = new List<string>();
+ prefixes.AddRange(symbols.Select(s => s.ToString()));
+ prefixes.AddRange(symbols.Select(s => "xri://" + s.ToString()));
+ foreach (string prefix in prefixes) {
+ var id = Identifier.Parse(prefix + "andrew");
+ Assert.IsInstanceOfType(typeof(XriIdentifier), id);
+ }
+ }
+
+ /// <summary>
+ /// Verifies conformance with 2.0 spec section 7.2#3
+ /// </summary>
+ [TestMethod]
+ public void ParseEndUserSuppliedUriIdentifier() {
+ // verify a fully-qualified Uri
+ var id = Identifier.Parse(uri);
+ Assert.IsInstanceOfType(typeof(UriIdentifier), id);
+ Assert.AreEqual(uri, ((UriIdentifier)id).Uri.AbsoluteUri);
+ // verify an HTTPS Uri
+ id = Identifier.Parse(uriHttps);
+ Assert.IsInstanceOfType(typeof(UriIdentifier), id);
+ Assert.AreEqual(uriHttps, ((UriIdentifier)id).Uri.AbsoluteUri);
+ // verify that if the scheme is missing it is added automatically
+ id = Identifier.Parse(uriNoScheme);
+ Assert.IsInstanceOfType(typeof(UriIdentifier), id);
+ Assert.AreEqual(uri, ((UriIdentifier)id).Uri.AbsoluteUri);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void ParseNull() {
+ Identifier.Parse(null);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void ParseEmpty() {
+ Identifier.Parse(string.Empty);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs
new file mode 100644
index 0000000..e6e9149
--- /dev/null
+++ b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs
@@ -0,0 +1,410 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Web;
+using DotNetOpenAuth.OpenId.RelyingParty;
+using DotNetOpenAuth.Test.Mocks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using DotNetOpenAuth.OpenId;
+
+namespace DotNetOpenAuth.Test.OpenId {
+ [TestClass]
+ public class UriIdentifierTests : OpenIdTestBase {
+ string goodUri = "http://blog.nerdbank.net/";
+ string relativeUri = "host/path";
+ string badUri = "som%-)830w8vf/?.<>,ewackedURI";
+
+ [TestInitialize]
+ public override void SetUp() {
+ if (!UntrustedWebRequest.WhitelistHosts.Contains("localhost"))
+ UntrustedWebRequest.WhitelistHosts.Add("localhost");
+ }
+
+ [TestCleanup]
+ public override void Cleanup() {
+ Mocks.MockHttpRequest.Reset();
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorNullUri() {
+ new UriIdentifier((Uri)null);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorNullString() {
+ new UriIdentifier((string)null);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorBlank() {
+ new UriIdentifier(string.Empty);
+ }
+
+ [TestMethod, ExpectedException(typeof(UriFormatException))]
+ public void CtorBadUri() {
+ new UriIdentifier(badUri);
+ }
+
+ [TestMethod]
+ public void CtorGoodUri() {
+ var uri = new UriIdentifier(goodUri);
+ Assert.AreEqual(new Uri(goodUri), uri.Uri);
+ Assert.IsFalse(uri.SchemeImplicitlyPrepended);
+ Assert.IsFalse(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod]
+ public void CtorStringNoSchemeSecure() {
+ var uri = new UriIdentifier("host/path", true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod]
+ public void CtorStringHttpsSchemeSecure() {
+ var uri = new UriIdentifier("https://host/path", true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentException))]
+ public void CtorStringHttpSchemeSecure() {
+ new UriIdentifier("http://host/path", true);
+ }
+
+ [TestMethod]
+ public void CtorUriHttpsSchemeSecure() {
+ var uri = new UriIdentifier(new Uri("https://host/path"), true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentException))]
+ public void CtorUriHttpSchemeSecure() {
+ new UriIdentifier(new Uri("http://host/path"), true);
+ }
+
+ /// <summary>
+ /// Verifies that the fragment is not stripped from an Identifier.
+ /// </summary>
+ /// <remarks>
+ /// Although fragments should be stripped from user supplied identifiers,
+ /// they should NOT be stripped from claimed identifiers. So the UriIdentifier
+ /// class, which serves both identifier types, must not do the stripping.
+ /// </remarks>
+ [TestMethod]
+ public void DoesNotStripFragment() {
+ Uri original = new Uri("http://a/b#c");
+ UriIdentifier identifier = new UriIdentifier(original);
+ Assert.AreEqual(original.Fragment, identifier.Uri.Fragment);
+ }
+
+ [TestMethod]
+ public void IsValid() {
+ Assert.IsTrue(UriIdentifier.IsValidUri(goodUri));
+ Assert.IsFalse(UriIdentifier.IsValidUri(badUri));
+ Assert.IsTrue(UriIdentifier.IsValidUri(relativeUri), "URL lacking http:// prefix should have worked anyway.");
+ }
+
+ [TestMethod]
+ public void TrimFragment() {
+ Identifier noFragment = UriIdentifier.Parse("http://a/b");
+ Identifier fragment = UriIdentifier.Parse("http://a/b#c");
+ Assert.AreSame(noFragment, noFragment.TrimFragment());
+ Assert.AreEqual(noFragment, fragment.TrimFragment());
+ }
+
+ [TestMethod]
+ public void ToStringTest() {
+ Assert.AreEqual(goodUri, new UriIdentifier(goodUri).ToString());
+ }
+
+ [TestMethod]
+ public void EqualsTest() {
+ Assert.AreEqual(new UriIdentifier(goodUri), new UriIdentifier(goodUri));
+ // This next test is an interesting side-effect of passing off to Uri.Equals. But it's probably ok.
+ Assert.AreEqual(new UriIdentifier(goodUri), new UriIdentifier(goodUri + "#frag"));
+ Assert.AreNotEqual(new UriIdentifier(goodUri), new UriIdentifier(goodUri + "a"));
+ Assert.AreNotEqual(null, new UriIdentifier(goodUri));
+ Assert.AreNotEqual(goodUri, new UriIdentifier(goodUri));
+ }
+
+ [TestMethod]
+ public void UnicodeTest() {
+ string unicodeUrl = "http://nerdbank.org/opaffirmative/崎村.aspx";
+ Assert.IsTrue(UriIdentifier.IsValidUri(unicodeUrl));
+ Identifier id;
+ Assert.IsTrue(UriIdentifier.TryParse(unicodeUrl, out id));
+ Assert.AreEqual("/opaffirmative/%E5%B4%8E%E6%9D%91.aspx", ((UriIdentifier)id).Uri.AbsolutePath);
+ Assert.AreEqual(Uri.EscapeUriString(unicodeUrl), id.ToString());
+ }
+
+ void discover(string url, ProtocolVersion version, Identifier expectedLocalId, bool expectSreg, bool useRedirect) {
+ discover(url, version, expectedLocalId, expectSreg, useRedirect, null);
+ }
+ void discover(string url, ProtocolVersion version, Identifier expectedLocalId, bool expectSreg, bool useRedirect, WebHeaderCollection headers) {
+ Protocol protocol = Protocol.Lookup(version);
+ UriIdentifier claimedId = TestSupport.GetFullUrl(url);
+ UriIdentifier userSuppliedIdentifier = TestSupport.GetFullUrl(
+ "Discovery/htmldiscovery/redirect.aspx?target=" + url);
+ if (expectedLocalId == null) expectedLocalId = claimedId;
+ Identifier idToDiscover = useRedirect ? userSuppliedIdentifier : claimedId;
+
+ string contentType;
+ if (url.EndsWith("html")) {
+ contentType = "text/html";
+ } else if (url.EndsWith("xml")) {
+ contentType = "application/xrds+xml";
+ } else {
+ throw new InvalidOperationException();
+ }
+ MockHttpRequest.RegisterMockResponse(new Uri(idToDiscover), claimedId, contentType,
+ headers ?? new WebHeaderCollection(), TestSupport.LoadEmbeddedFile(url));
+
+ ServiceEndpoint se = idToDiscover.Discover().FirstOrDefault();
+ Assert.IsNotNull(se, url + " failed to be discovered.");
+ Assert.AreSame(protocol, se.Protocol);
+ Assert.AreEqual(claimedId, se.ClaimedIdentifier);
+ Assert.AreEqual(expectedLocalId, se.ProviderLocalIdentifier);
+ Assert.AreEqual(expectSreg ? 2 : 1, se.ProviderSupportedServiceTypeUris.Length);
+ Assert.IsTrue(Array.IndexOf(se.ProviderSupportedServiceTypeUris, protocol.ClaimedIdentifierServiceTypeURI) >= 0);
+ Assert.AreEqual(expectSreg, se.IsExtensionSupported(new ClaimsRequest()));
+ }
+ void discoverXrds(string page, ProtocolVersion version, Identifier expectedLocalId) {
+ discoverXrds(page, version, expectedLocalId, null);
+ }
+ void discoverXrds(string page, ProtocolVersion version, Identifier expectedLocalId, WebHeaderCollection headers) {
+ if (!page.Contains(".")) page += ".xml";
+ discover("/Discovery/xrdsdiscovery/" + page, version, expectedLocalId, true, false, headers);
+ discover("/Discovery/xrdsdiscovery/" + page, version, expectedLocalId, true, true, headers);
+ }
+ void discoverHtml(string page, ProtocolVersion version, Identifier expectedLocalId, bool useRedirect) {
+ discover("/Discovery/htmldiscovery/" + page, version, expectedLocalId, false, useRedirect);
+ }
+ void discoverHtml(string scenario, ProtocolVersion version, Identifier expectedLocalId) {
+ string page = scenario + ".html";
+ discoverHtml(page, version, expectedLocalId, false);
+ discoverHtml(page, version, expectedLocalId, true);
+ }
+ void failDiscover(string url) {
+ UriIdentifier userSuppliedId = TestSupport.GetFullUrl(url);
+
+ Mocks.MockHttpRequest.RegisterMockResponse(new Uri(userSuppliedId), userSuppliedId, "text/html",
+ TestSupport.LoadEmbeddedFile(url));
+
+ Assert.AreEqual(0, userSuppliedId.Discover().Count()); // ... but that no endpoint info is discoverable
+ }
+ void failDiscoverHtml(string scenario) {
+ failDiscover("/Discovery/htmldiscovery/" + scenario + ".html");
+ }
+ void failDiscoverXrds(string scenario) {
+ failDiscover("/Discovery/xrdsdiscovery/" + scenario + ".xml");
+ }
+ [TestMethod]
+ public void HtmlDiscover_11() {
+ discoverHtml("html10prov", ProtocolVersion.V11, null);
+ discoverHtml("html10both", ProtocolVersion.V11, "http://c/d");
+ failDiscoverHtml("html10del");
+ }
+ [TestMethod]
+ public void HtmlDiscover_20() {
+ discoverHtml("html20prov", ProtocolVersion.V20, null);
+ discoverHtml("html20both", ProtocolVersion.V20, "http://c/d");
+ failDiscoverHtml("html20del");
+ discoverHtml("html2010", ProtocolVersion.V20, "http://c/d");
+ discoverHtml("html1020", ProtocolVersion.V20, "http://c/d");
+ discoverHtml("html2010combinedA", ProtocolVersion.V20, "http://c/d");
+ discoverHtml("html2010combinedB", ProtocolVersion.V20, "http://c/d");
+ discoverHtml("html2010combinedC", ProtocolVersion.V20, "http://c/d");
+ failDiscoverHtml("html20relative");
+ }
+ [TestMethod]
+ public void XrdsDiscoveryFromHead() {
+ Mocks.MockHttpRequest.RegisterMockResponse(new Uri("http://localhost/xrds1020.xml"),
+ "application/xrds+xml", TestSupport.LoadEmbeddedFile("/Discovery/xrdsdiscovery/xrds1020.xml"));
+ discoverXrds("XrdsReferencedInHead.html", ProtocolVersion.V10, null);
+ }
+ [TestMethod]
+ public void XrdsDiscoveryFromHttpHeader() {
+ WebHeaderCollection headers = new WebHeaderCollection();
+ headers.Add("X-XRDS-Location", TestSupport.GetFullUrl("http://localhost/xrds1020.xml").AbsoluteUri);
+ Mocks.MockHttpRequest.RegisterMockResponse(new Uri("http://localhost/xrds1020.xml"),
+ "application/xrds+xml", TestSupport.LoadEmbeddedFile("/Discovery/xrdsdiscovery/xrds1020.xml"));
+ discoverXrds("XrdsReferencedInHttpHeader.html", ProtocolVersion.V10, null, headers);
+ }
+ [TestMethod]
+ public void XrdsDirectDiscovery_10() {
+ failDiscoverXrds("xrds-irrelevant");
+ discoverXrds("xrds10", ProtocolVersion.V10, null);
+ discoverXrds("xrds11", ProtocolVersion.V11, null);
+ discoverXrds("xrds1020", ProtocolVersion.V10, null);
+ }
+ [TestMethod]
+ public void XrdsDirectDiscovery_20() {
+ discoverXrds("xrds20", ProtocolVersion.V20, null);
+ discoverXrds("xrds2010a", ProtocolVersion.V20, null);
+ discoverXrds("xrds2010b", ProtocolVersion.V20, null);
+ }
+
+ [TestMethod]
+ public void NormalizeCase() {
+ // only the host name can be normalized in casing safely.
+ Identifier id = "http://HOST:80/PaTH?KeY=VaLUE#fRag";
+ Assert.AreEqual("http://host/PaTH?KeY=VaLUE#fRag", id.ToString());
+ // make sure https is preserved, along with port 80, which is NON-default for https
+ id = "https://HOST:80/PaTH?KeY=VaLUE#fRag";
+ Assert.AreEqual("https://host:80/PaTH?KeY=VaLUE#fRag", id.ToString());
+ }
+
+ [TestMethod]
+ public void HttpSchemePrepended() {
+ UriIdentifier id = new UriIdentifier("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.SchemeImplicitlyPrepended);
+ }
+
+ ////[TestMethod, Ignore("The spec says http:// must be prepended in this case, but that just creates an invalid URI. Our UntrustedWebRequest will stop disallowed schemes.")]
+ public void CtorDisallowedScheme() {
+ UriIdentifier id = new UriIdentifier(new Uri("ftp://host/path"));
+ Assert.AreEqual("http://ftp://host/path", id.ToString());
+ Assert.IsTrue(id.SchemeImplicitlyPrepended);
+ }
+
+ [TestMethod]
+ public void DiscoveryWithRedirects() {
+ MockHttpRequest.Reset();
+ Identifier claimedId = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20);
+
+ // Add a couple of chained redirect pages that lead to the claimedId.
+ Uri userSuppliedUri = TestSupport.GetFullUrl("/someSecurePage", null, true);
+ Uri insecureMidpointUri = TestSupport.GetFullUrl("/insecureStop");
+ MockHttpRequest.RegisterMockRedirect(userSuppliedUri, insecureMidpointUri);
+ MockHttpRequest.RegisterMockRedirect(insecureMidpointUri, new Uri(claimedId.ToString()));
+
+ // don't require secure SSL discovery for this test.
+ Identifier userSuppliedIdentifier = new UriIdentifier(userSuppliedUri, false);
+ Assert.AreEqual(1, userSuppliedIdentifier.Discover().Count());
+ }
+
+ [TestMethod]
+ public void TryRequireSslAdjustsIdentifier() {
+ Identifier secureId;
+ // Try Parse and ctor without explicit scheme
+ var id = Identifier.Parse("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.TryRequireSsl(out secureId));
+ Assert.IsTrue(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("https://www.yahoo.com/", secureId.ToString());
+
+ id = new UriIdentifier("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.TryRequireSsl(out secureId));
+ Assert.IsTrue(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("https://www.yahoo.com/", secureId.ToString());
+
+ // Try Parse and ctor with explicit http:// scheme
+ id = Identifier.Parse("http://www.yahoo.com");
+ Assert.IsFalse(id.TryRequireSsl(out secureId));
+ Assert.IsFalse(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("http://www.yahoo.com/", secureId.ToString());
+ Assert.AreEqual(0, secureId.Discover().Count());
+
+ id = new UriIdentifier("http://www.yahoo.com");
+ Assert.IsFalse(id.TryRequireSsl(out secureId));
+ Assert.IsFalse(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("http://www.yahoo.com/", secureId.ToString());
+ Assert.AreEqual(0, secureId.Discover().Count());
+ }
+
+ [TestMethod]
+ public void DiscoverRequireSslWithSecureRedirects() {
+ MockHttpRequest.Reset();
+ Identifier claimedId = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, true);
+
+ // Add a couple of chained redirect pages that lead to the claimedId.
+ // All redirects should be secure.
+ Uri userSuppliedUri = TestSupport.GetFullUrl("/someSecurePage", null, true);
+ Uri secureMidpointUri = TestSupport.GetFullUrl("/secureStop", null, true);
+ MockHttpRequest.RegisterMockRedirect(userSuppliedUri, secureMidpointUri);
+ MockHttpRequest.RegisterMockRedirect(secureMidpointUri, new Uri(claimedId.ToString()));
+
+ Identifier userSuppliedIdentifier = new UriIdentifier(userSuppliedUri, true);
+ Assert.AreEqual(1, userSuppliedIdentifier.Discover().Count());
+ }
+
+ [TestMethod, ExpectedException(typeof(OpenIdException))]
+ public void DiscoverRequireSslWithInsecureRedirect() {
+ MockHttpRequest.Reset();
+ Identifier claimedId = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, true);
+
+ // Add a couple of chained redirect pages that lead to the claimedId.
+ // Include an insecure HTTP jump in those redirects to verify that
+ // the ultimate endpoint is never found as a result of high security profile.
+ Uri userSuppliedUri = TestSupport.GetFullUrl("/someSecurePage", null, true);
+ Uri insecureMidpointUri = TestSupport.GetFullUrl("/insecureStop");
+ MockHttpRequest.RegisterMockRedirect(userSuppliedUri, insecureMidpointUri);
+ MockHttpRequest.RegisterMockRedirect(insecureMidpointUri, new Uri(claimedId.ToString()));
+
+ Identifier userSuppliedIdentifier = new UriIdentifier(userSuppliedUri, true);
+ userSuppliedIdentifier.Discover();
+ }
+
+ [TestMethod]
+ public void DiscoveryRequireSslWithInsecureXrdsInSecureHtmlHead() {
+ var insecureXrdsSource = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, false);
+ Uri secureClaimedUri = TestSupport.GetFullUrl("/secureId", null, true);
+
+ string html = string.Format("<html><head><meta http-equiv='X-XRDS-Location' content='{0}'/></head><body></body></html>",
+ insecureXrdsSource);
+ MockHttpRequest.RegisterMockResponse(secureClaimedUri, "text/html", html);
+
+ Identifier userSuppliedIdentifier = new UriIdentifier(secureClaimedUri, true);
+ Assert.AreEqual(0, userSuppliedIdentifier.Discover().Count());
+ }
+
+ [TestMethod]
+ public void DiscoveryRequireSslWithInsecureXrdsInSecureHttpHeader() {
+ var insecureXrdsSource = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, false);
+ Uri secureClaimedUri = TestSupport.GetFullUrl("/secureId", null, true);
+
+ string html = "<html><head></head><body></body></html>";
+ WebHeaderCollection headers = new WebHeaderCollection {
+ { "X-XRDS-Location", insecureXrdsSource }
+ };
+ MockHttpRequest.RegisterMockResponse(secureClaimedUri, secureClaimedUri, "text/html", headers, html);
+
+ Identifier userSuppliedIdentifier = new UriIdentifier(secureClaimedUri, true);
+ Assert.AreEqual(0, userSuppliedIdentifier.Discover().Count());
+ }
+
+ [TestMethod]
+ public void DiscoveryRequireSslWithInsecureXrdsButSecureLinkTags() {
+ var insecureXrdsSource = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, false);
+ Uri secureClaimedUri = TestSupport.GetFullUrl("/secureId", null, true);
+
+ Identifier localIdForLinkTag = TestSupport.GetDelegateUrl(TestSupport.Scenarios.AlwaysDeny, true);
+ string html = string.Format(@"
+ <html><head>
+ <meta http-equiv='X-XRDS-Location' content='{0}'/> <!-- this one will be insecure and ignored -->
+ <link rel='openid2.provider' href='{1}' />
+ <link rel='openid2.local_id' href='{2}' />
+ </head><body></body></html>",
+ HttpUtility.HtmlEncode(insecureXrdsSource),
+ HttpUtility.HtmlEncode(TestSupport.GetFullUrl("/" + TestSupport.ProviderPage, null, true).AbsoluteUri),
+ HttpUtility.HtmlEncode(localIdForLinkTag.ToString()));
+ MockHttpRequest.RegisterMockResponse(secureClaimedUri, "text/html", html);
+
+ Identifier userSuppliedIdentifier = new UriIdentifier(secureClaimedUri, true);
+ Assert.AreEqual(localIdForLinkTag, userSuppliedIdentifier.Discover().Single().ProviderLocalIdentifier);
+ }
+
+ [TestMethod]
+ public void DiscoveryRequiresSslIgnoresInsecureEndpointsInXrds() {
+ var insecureEndpoint = TestSupport.GetServiceEndpoint(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, 10, false);
+ var secureEndpoint = TestSupport.GetServiceEndpoint(TestSupport.Scenarios.ApproveOnSetup, ProtocolVersion.V20, 20, true);
+ UriIdentifier secureClaimedId = new UriIdentifier(TestSupport.GetFullUrl("/claimedId", null, true), true);
+ MockHttpRequest.RegisterMockXrdsResponse(secureClaimedId, new ServiceEndpoint[] { insecureEndpoint, secureEndpoint });
+ Assert.AreEqual(secureEndpoint.ProviderLocalIdentifier, secureClaimedId.Discover().Single().ProviderLocalIdentifier);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Test/OpenId/XriIdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/XriIdentifierTests.cs
new file mode 100644
index 0000000..1edc086
--- /dev/null
+++ b/src/DotNetOpenAuth.Test/OpenId/XriIdentifierTests.cs
@@ -0,0 +1,470 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using DotNetOpenAuth.OpenId.RelyingParty;
+using DotNetOpenAuth.Test.Mocks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using DotNetOpenAuth.OpenId;
+
+namespace DotNetOpenAuth.Test.OpenId {
+ [TestClass]
+ public class XriIdentifierTests : OpenIdTestBase {
+ string goodXri = "=Andrew*Arnott";
+ string badXri = "some\\wacky%^&*()non-XRI";
+
+ [TestCleanup]
+ public void TearDown() {
+ MockHttpRequest.Reset();
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorNull() {
+ new XriIdentifier(null);
+ }
+
+ [TestMethod, ExpectedException(typeof(ArgumentNullException))]
+ public void CtorBlank() {
+ new XriIdentifier(string.Empty);
+ }
+
+ [TestMethod, ExpectedException(typeof(FormatException))]
+ public void CtorBadXri() {
+ new XriIdentifier(badXri);
+ }
+
+ [TestMethod]
+ public void CtorGoodXri() {
+ var xri = new XriIdentifier(goodXri);
+ Assert.AreEqual(goodXri, xri.OriginalXri);
+ Assert.AreEqual(goodXri, xri.CanonicalXri); // assumes 'goodXri' is canonical already
+ Assert.IsFalse(xri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod]
+ public void CtorGoodXriSecure() {
+ var xri = new XriIdentifier(goodXri, true);
+ Assert.AreEqual(goodXri, xri.OriginalXri);
+ Assert.AreEqual(goodXri, xri.CanonicalXri); // assumes 'goodXri' is canonical already
+ Assert.IsTrue(xri.IsDiscoverySecureEndToEnd);
+ }
+
+ [TestMethod]
+ public void IsValid() {
+ Assert.IsTrue(XriIdentifier.IsValidXri(goodXri));
+ Assert.IsFalse(XriIdentifier.IsValidXri(badXri));
+ }
+
+ /// <summary>
+ /// Verifies 2.0 spec section 7.2#1
+ /// </summary>
+ [TestMethod]
+ public void StripXriScheme() {
+ var xri = new XriIdentifier("xri://" + goodXri);
+ Assert.AreEqual("xri://" + goodXri, xri.OriginalXri);
+ Assert.AreEqual(goodXri, xri.CanonicalXri);
+ }
+
+ [TestMethod]
+ public void TrimFragment() {
+ Identifier xri = new XriIdentifier(goodXri);
+ Assert.AreSame(xri, xri.TrimFragment());
+ }
+
+ [TestMethod]
+ public void ToStringTest() {
+ Assert.AreEqual(goodXri, new XriIdentifier(goodXri).ToString());
+ }
+
+ [TestMethod]
+ public void EqualsTest() {
+ Assert.AreEqual(new XriIdentifier(goodXri), new XriIdentifier(goodXri));
+ Assert.AreNotEqual(new XriIdentifier(goodXri), new XriIdentifier(goodXri + "a"));
+ Assert.AreNotEqual(null, new XriIdentifier(goodXri));
+ Assert.AreNotEqual(goodXri, new XriIdentifier(goodXri));
+ }
+
+#if DISCOVERY // TODO: Add discovery and then re-enable this code block
+ private ServiceEndpoint verifyCanonicalId(Identifier iname, string expectedClaimedIdentifier) {
+ ServiceEndpoint se = iname.Discover().FirstOrDefault();
+ if (expectedClaimedIdentifier != null) {
+ Assert.IsNotNull(se);
+ Assert.AreEqual(expectedClaimedIdentifier, se.ClaimedIdentifier.ToString(), "i-name {0} discovery resulted in unexpected CanonicalId", iname);
+ Assert.IsTrue(se.ProviderSupportedServiceTypeUris.Length > 0);
+ } else {
+ Assert.IsNull(se);
+ }
+ return se;
+ }
+#endif
+
+ [TestMethod]
+ public void Discover() {
+ string xrds = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD version='2.0' xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*Arnott</Query>
+ <Status ceid='off' cid='verified' code='100'/>
+ <Expires>2008-07-14T02:03:24.000Z</Expires>
+ <ProviderID>xri://=</ProviderID>
+ <LocalID>!9b72.7dd1.50a9.5ccd</LocalID>
+ <CanonicalID>=!9B72.7DD1.50A9.5CCD</CanonicalID>
+
+ <Service priority='10'>
+ <ProviderID>xri://!!1008</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='default' select='false'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null' select='false'/>
+ <URI append='qxri' priority='1'>http://1id.com/contact/</URI>
+
+ </Service>
+ <Service priority='10'>
+ <ProviderID>xri://!!1008</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Type match='null' select='false'/>
+ <URI append='qxri' priority='1'>http://1id.com/</URI>
+ </Service>
+
+ <Service priority='10'>
+ <ProviderID>xri://!!1008</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <URI append='none' priority='10'>http://1id.com/sso</URI>
+ </Service>
+</XRD>";
+ Dictionary<string, string> mocks = new Dictionary<string, string> {
+ { "https://xri.net/=Arnott?_xrd_r=application/xrd%2Bxml;sep=false", xrds },
+ { "https://xri.net/=!9B72.7DD1.50A9.5CCD?_xrd_r=application/xrd%2Bxml;sep=false", xrds },
+ };
+ MockHttpRequest.RegisterMockXrdsResponses(mocks);
+
+ string expectedCanonicalId = "=!9B72.7DD1.50A9.5CCD";
+ ServiceEndpoint se = verifyCanonicalId("=Arnott", expectedCanonicalId);
+ Assert.AreEqual(Protocol.V10, se.Protocol);
+ Assert.AreEqual("http://1id.com/sso", se.ProviderEndpoint.ToString());
+ Assert.AreEqual(se.ClaimedIdentifier, se.ProviderLocalIdentifier);
+ Assert.AreEqual("=Arnott", se.FriendlyIdentifierForDisplay);
+ }
+
+ [TestMethod]
+ public void DiscoverCommunityInameCanonicalIDs() {
+ string llliResponse = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD version='2.0' xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*llli</Query>
+ <Status ceid='off' cid='verified' code='100'/>
+ <Expires>2008-07-14T02:21:06.000Z</Expires>
+ <ProviderID>xri://@</ProviderID>
+ <LocalID>!72cd.a072.157e.a9c6</LocalID>
+ <CanonicalID>@!72CD.A072.157E.A9C6</CanonicalID>
+ <Service priority='10'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <URI append='none' priority='1'>https://login.llli.org/server/</URI>
+ </Service>
+ <Service priority='1'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type match='null' select='false'/>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Path match='default'/>
+ <Path>(+index)</Path>
+ <URI append='qxri' priority='1'>http://linksafe-forward.ezibroker.net/forwarding/</URI>
+ </Service>
+ <Service priority='10'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://$res*auth*($v*2.0)</Type>
+ <MediaType>application/xrds+xml;trust=none</MediaType>
+ <URI priority='10'>http://resolve.ezibroker.net/resolve/@llli/</URI>
+ </Service>
+ <Service priority='10'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <URI append='authority' priority='1'>http://linksafe-contact.ezibroker.net/contact/</URI>
+ </Service>
+</XRD>
+";
+ string llliAreaResponse = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*area</Query>
+ <Status cid='verified' code='100'>SUCCESS</Status>
+ <ServerStatus code='100'>SUCCESS</ServerStatus>
+ <Expires>2008-07-15T01:21:07.000Z</Expires>
+ <ProviderID>xri://!!1003</ProviderID>
+ <LocalID>0000.0000.3B9A.CA0C</LocalID>
+ <CanonicalID>@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C</CanonicalID>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <URI append='none' priority='1'>https://login.llli.org/server/</URI>
+ </Service>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <URI append='authority' priority='1'>http://linksafe-contact.ezibroker.net/contact/</URI>
+ </Service>
+ <Service priority='1'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Type match='null' select='false'/>
+ <Path>(+index)</Path>
+ <Path match='default'/>
+ <URI append='qxri' priority='1'>http://linksafe-forward.ezibroker.net/forwarding/</URI>
+ </Service>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://$res*auth*($v*2.0)</Type>
+ <MediaType>application/xrds+xml;trust=none</MediaType>
+ <URI>http://resolve.ezibroker.net/resolve/@llli*area/</URI>
+ </Service>
+</XRD>";
+ string llliAreaCanadaUnattachedResponse = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*canada.unattached</Query>
+ <Status cid='verified' code='100'>SUCCESS</Status>
+ <ServerStatus code='100'>SUCCESS</ServerStatus>
+ <Expires>2008-07-15T01:21:08.000Z</Expires>
+ <ProviderID>xri://!!1003</ProviderID>
+ <LocalID>0000.0000.3B9A.CA41</LocalID>
+ <CanonicalID>@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C!0000.0000.3B9A.CA41</CanonicalID>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <URI append='none' priority='1'>https://login.llli.org/server/</URI>
+ </Service>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <URI append='authority' priority='1'>http://linksafe-contact.ezibroker.net/contact/</URI>
+ </Service>
+ <Service priority='1'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Type match='null' select='false'/>
+ <Path>(+index)</Path>
+ <Path match='default'/>
+ <URI append='qxri' priority='1'>http://linksafe-forward.ezibroker.net/forwarding/</URI>
+ </Service>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://$res*auth*($v*2.0)</Type>
+ <MediaType>application/xrds+xml;trust=none</MediaType>
+ <URI>http://resolve.ezibroker.net/resolve/@llli*area*canada.unattached/</URI>
+ </Service>
+</XRD>";
+ string llliAreaCanadaUnattachedAdaResponse = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*ada</Query>
+ <Status cid='verified' code='100'>SUCCESS</Status>
+ <ServerStatus code='100'>SUCCESS</ServerStatus>
+ <Expires>2008-07-15T01:21:10.000Z</Expires>
+ <ProviderID>xri://!!1003</ProviderID>
+ <LocalID>0000.0000.3B9A.CA01</LocalID>
+ <CanonicalID>@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C!0000.0000.3B9A.CA41!0000.0000.3B9A.CA01</CanonicalID>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <URI append='none' priority='1'>https://login.llli.org/server/</URI>
+ </Service>
+ <Service>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <URI append='authority' priority='1'>http://linksafe-contact.ezibroker.net/contact/</URI>
+ </Service>
+ <Service priority='1'>
+ <ProviderID>xri://!!1003!103</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Type match='null' select='false'/>
+ <Path>(+index)</Path>
+ <Path match='default'/>
+ <URI append='qxri' priority='1'>http://linksafe-forward.ezibroker.net/forwarding/</URI>
+ </Service>
+</XRD>";
+ string webResponse = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD version='2.0' xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*Web</Query>
+ <Status ceid='off' cid='verified' code='100'/>
+ <Expires>2008-07-14T02:21:12.000Z</Expires>
+ <ProviderID>xri://=</ProviderID>
+ <LocalID>!91f2.8153.f600.ae24</LocalID>
+ <CanonicalID>=!91F2.8153.F600.AE24</CanonicalID>
+ <Service priority='10'>
+ <Type select='true'>xri://+i-service*(+locator)*($v*1.0)</Type>
+ <Path select='true'>(+locator)</Path>
+ <MediaType match='default' select='false'/>
+ <URI append='qxri'>http://locator.fullxri.com/locator/</URI>
+ </Service>
+ <Service priority='10'>
+ <ProviderID>xri://=web</ProviderID>
+ <Type select='true'>xri://$res*auth*($v*2.0)</Type>
+ <Type select='true'>xri://$res*auth*($v*2.0)</Type>
+ <MediaType select='true'>application/xrds+xml</MediaType>
+ <URI append='qxri' priority='1'>https://resolve.freexri.com/ns/=web/</URI>
+ <URI append='qxri' priority='2'>http://resolve.freexri.com/ns/=web/</URI>
+ </Service>
+ <Service priority='10'>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <Type select='true'>http://specs.openid.net/auth/2.0/signon</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default' select='false'/>
+ <MediaType match='default' select='false'/>
+ <URI append='none' priority='2'>http://authn.fullxri.com/authentication/</URI>
+ <URI append='none' priority='1'>https://authn.fullxri.com/authentication/</URI>
+ </Service>
+ <Service priority='10'>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null' select='false'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null' select='false'/>
+ <MediaType match='default' select='false'/>
+ <URI append='qxri'>http://contact.fullxri.com/contact/</URI>
+ </Service>
+ <KeyInfo xmlns='http://www.w3.org/2000/09/xmldsig#'>
+ <X509Data>
+ <X509Certificate>
+MIIExzCCA6+gAwIBAgIJAM+MlFr0Sth6MA0GCSqGSIb3DQEBBQUAMIGdMR8wHQYD
+VQQDExZTdXBlcnZpbGxhaW46IFRoZSBSb290MQswCQYDVQQGEwJVUzERMA8GA1UE
+CBMITmV3IFlvcmsxDzANBgNVBAcTBkdvdGhhbTEgMB4GA1UEChMXU3VwZXJ2aWxs
+YWluIFVuaXZlcnNpdHkxJzAlBgkqhkiG9w0BCQEWGHBlbmd1aW5Ac3VwZXJ2aWxs
+YWluLmVkdTAeFw0wNjA4MTcxOTU5NTNaFw0xMTA4MTYxOTU5NTNaMIGdMR8wHQYD
+VQQDExZTdXBlcnZpbGxhaW46IFRoZSBSb290MQswCQYDVQQGEwJVUzERMA8GA1UE
+CBMITmV3IFlvcmsxDzANBgNVBAcTBkdvdGhhbTEgMB4GA1UEChMXU3VwZXJ2aWxs
+YWluIFVuaXZlcnNpdHkxJzAlBgkqhkiG9w0BCQEWGHBlbmd1aW5Ac3VwZXJ2aWxs
+YWluLmVkdTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL6uFqas4dK6
+A2wTZL0viRQNJrPyFnFBDSZGib/2ijhgzed/vvmZIBM9sFpwahcuR5hvyKUe37/c
+/RSZXoNDi/eiNOx4qb0l9UB6bd8qvc4V1PnLE7L+ZYcmwrvTKm4x8qXMgEv1wca2
+FPsreHNPdLiTUZ8v0tDTWi3Mgi7y47VTzJaTkcfmO1nL6xAtln5sLdH0PbMM3LAp
+T1d3nwI3VdbhqqZ+6+OKEuC8gk5iH4lfrbr6C9bYS6vzIKrotHpZ3N2aIC3NMjJD
+PMw/mfCuADfRNlHXgZW+0zyUkwGTMDea8qgsoAMWJGdeTIw8I1I3RhnbgLzdsNQl
+b/1ZXx1uJRUCAwEAAaOCAQYwggECMB0GA1UdDgQWBBQe+xSjYTrlfraJARjMxscb
+j36jvDCB0gYDVR0jBIHKMIHHgBQe+xSjYTrlfraJARjMxscbj36jvKGBo6SBoDCB
+nTEfMB0GA1UEAxMWU3VwZXJ2aWxsYWluOiBUaGUgUm9vdDELMAkGA1UEBhMCVVMx
+ETAPBgNVBAgTCE5ldyBZb3JrMQ8wDQYDVQQHEwZHb3RoYW0xIDAeBgNVBAoTF1N1
+cGVydmlsbGFpbiBVbml2ZXJzaXR5MScwJQYJKoZIhvcNAQkBFhhwZW5ndWluQHN1
+cGVydmlsbGFpbi5lZHWCCQDPjJRa9ErYejAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
+DQEBBQUAA4IBAQC4SPBDGYAxfbXd8N5OvG0drM7a5hjXfcCZpiILlPSRpxp79yh7
+I5vVWxBxUfolwbei7PTBVy7CE27SUbSICeqWjcDCfjNjiZk6mLS80rm/TdLrHSyM
++Ujlw9MGcBGaLI+sdziDUMtTQDpeAyQTaGVbh1mx5874Hlo1VXqGYNo0RwR+iLfs
+x48VuO6GbWVyxtktkE2ypz1KLWiyI056YynydRvuBCBHeRqGUixPlH9CrmeSCP2S
+sfbiKnMOGXjIYbvbsTAMdW2iqg6IWa/fgxhvZoAXChM9bkhisJQc0qD0J5TJQwgr
+uEyb50RJ7DWmXctSC0b3eymZ2lSXxAWNOsNy
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+</XRD>";
+ MockHttpRequest.RegisterMockXrdsResponses(new Dictionary<string, string> {
+ { "https://xri.net/@llli?_xrd_r=application/xrd%2Bxml;sep=false", llliResponse },
+ { "https://xri.net/@llli*area?_xrd_r=application/xrd%2Bxml;sep=false", llliAreaResponse },
+ { "https://xri.net/@llli*area*canada.unattached?_xrd_r=application/xrd%2Bxml;sep=false", llliAreaCanadaUnattachedResponse },
+ { "https://xri.net/@llli*area*canada.unattached*ada?_xrd_r=application/xrd%2Bxml;sep=false", llliAreaCanadaUnattachedAdaResponse },
+ { "https://xri.net/=Web?_xrd_r=application/xrd%2Bxml;sep=false", webResponse },
+ });
+ verifyCanonicalId("@llli", "@!72CD.A072.157E.A9C6");
+ verifyCanonicalId("@llli*area", "@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C");
+ verifyCanonicalId("@llli*area*canada.unattached", "@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C!0000.0000.3B9A.CA41");
+ verifyCanonicalId("@llli*area*canada.unattached*ada", "@!72CD.A072.157E.A9C6!0000.0000.3B9A.CA0C!0000.0000.3B9A.CA41!0000.0000.3B9A.CA01");
+ verifyCanonicalId("=Web", "=!91F2.8153.F600.AE24");
+ }
+
+ [TestMethod]
+ public void DiscoveryCommunityInameDelegateWithoutCanonicalID() {
+ MockHttpRequest.RegisterMockXrdsResponses(new Dictionary<string, string> {
+ { "https://xri.net/=Web*andrew.arnott?_xrd_r=application/xrd%2Bxml;sep=false", @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*andrew.arnott</Query>
+ <Status cid='absent' code='100'>Success</Status>
+ <ServerStatus code='100'>Success</ServerStatus>
+ <Expires>2008-07-14T03:30:59.722Z</Expires>
+ <ProviderID>=!91F2.8153.F600.AE24</ProviderID>
+ <Service>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='none' priority='2'>http://www.myopenid.com/server</URI>
+ <openid:Delegate xmlns:openid='http://openid.net/xmlns/1.0'>http://blog.nerdbank.net</openid:Delegate>
+ </Service>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <MediaType match='default'/>
+ <URI append='qxri'>http://contact.freexri.com/contact/</URI>
+ </Service>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Path select='true'>(+index)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='qxri'>http://forwarding.freexri.com/forwarding/</URI>
+ </Service>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='none' priority='2'>http://authn.freexri.com/authentication/</URI>
+ <URI append='none' priority='1'>https://authn.freexri.com/authentication/</URI>
+ </Service>
+ <ServedBy>OpenXRI</ServedBy>
+</XRD>" },
+ { "https://xri.net/@id*andrewarnott?_xrd_r=application/xrd%2Bxml;sep=false", @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>*andrewarnott</Query>
+ <Status cid='absent' code='100'>Success</Status>
+ <ServerStatus code='100'>Success</ServerStatus>
+ <Expires>2008-07-14T03:31:00.466Z</Expires>
+ <ProviderID>@!B1E8.C27B.E41C.25C3</ProviderID>
+ <Service>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='none' priority='2'>http://www.myopenid.com/server</URI>
+ <openid:Delegate xmlns:openid='http://openid.net/xmlns/1.0'>http://blog.nerdbank.net</openid:Delegate>
+ </Service>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <MediaType match='default'/>
+ <URI append='qxri'>http://contact.freexri.com/contact/</URI>
+ </Service>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>xri://+i-service*(+forwarding)*($v*1.0)</Type>
+ <Path select='true'>(+index)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='qxri'>http://forwarding.freexri.com/forwarding/</URI>
+ </Service>
+ <ServedBy>OpenXRI</ServedBy>
+</XRD>" },
+ });
+ // Consistent with spec section 7.3.2.3, we do not permit
+ // delegation on XRI discovery when there is no CanonicalID present.
+ verifyCanonicalId("=Web*andrew.arnott", null);
+ verifyCanonicalId("@id*andrewarnott", null);
+ }
+
+ ////[TestMethod, Ignore("XRI parsing and normalization is not implemented (yet).")]
+ public void NormalizeCase() {
+ Identifier id = "=!9B72.7dd1.50a9.5ccd";
+ Assert.AreEqual("=!9B72.7DD1.50A9.5CCD", id.ToString());
+ }
+ }
+}
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;
+ }
+ }
+}