diff options
Diffstat (limited to 'src')
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 '{0}' 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 '{0}'.. + /// </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 '{0}' 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 '{0}' 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 '{0}'.. + /// </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 '{0}' not under realm '{1}'.. /// </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 + /// <meta http-equiv="X-XRDS-Location" content="{YadisURL}"> + /// 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; + } + } +} |