/// <summary>
		/// Downloads the XRDS document for this XRI.
		/// </summary>
		/// <param name="identifier">The identifier.</param>
		/// <param name="requestHandler">The request handler.</param>
		/// <returns>The XRDS document.</returns>
		private static XrdsDocument DownloadXrds(XriIdentifier identifier, IDirectWebRequestHandler requestHandler) {
			Requires.NotNull(identifier, "identifier");
			Requires.NotNull(requestHandler, "requestHandler");
			XrdsDocument doc;
			using (var xrdsResponse = Yadis.Request(requestHandler, GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd)) {
				var readerSettings = MessagingUtilities.CreateUntrustedXmlReaderSettings();
				doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream, readerSettings));
			}
			ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed);
			return doc;
		}
		/// <summary>
		/// Downloads the XRDS document for this XRI.
		/// </summary>
		/// <param name="identifier">The identifier.</param>
		/// <param name="requestHandler">The request handler.</param>
		/// <returns>The XRDS document.</returns>
		private static XrdsDocument DownloadXrds(XriIdentifier identifier, IDirectWebRequestHandler requestHandler) {
			Requires.NotNull(identifier, "identifier");
			Requires.NotNull(requestHandler, "requestHandler");
			Contract.Ensures(Contract.Result<XrdsDocument>() != null);
			XrdsDocument doc;
			using (var xrdsResponse = Yadis.Request(requestHandler, GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd)) {
				doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream));
			}
			ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed);
			return doc;
		}
		/// <summary>
		/// Performs discovery on the specified identifier.
		/// </summary>
		/// <param name="identifier">The identifier to perform discovery on.</param>
		/// <param name="cancellationToken">The cancellation token.</param>
		/// <returns>
		/// A sequence of service endpoints yielded by discovery.  Must not be null, but may be empty.
		/// </returns>
		public async Task<IdentifierDiscoveryServiceResult> DiscoverAsync(Identifier identifier, CancellationToken cancellationToken) {
			Requires.NotNull(identifier, "identifier");
			Verify.Operation(this.HostFactories != null, Strings.HostFactoriesRequired);
			cancellationToken.ThrowIfCancellationRequested();

			var uriIdentifier = identifier as UriIdentifier;
			if (uriIdentifier == null) {
				return new IdentifierDiscoveryServiceResult(Enumerable.Empty<IdentifierDiscoveryResult>());
			}

			var endpoints = new List<IdentifierDiscoveryResult>();

			// Attempt YADIS discovery
			DiscoveryResult yadisResult = await Yadis.DiscoverAsync(this.HostFactories, uriIdentifier, identifier.IsDiscoverySecureEndToEnd, cancellationToken);
			cancellationToken.ThrowIfCancellationRequested();
			if (yadisResult != null) {
				if (yadisResult.IsXrds) {
					try {
						XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
						var xrdsEndpoints = xrds.XrdElements.CreateServiceEndpoints(yadisResult.NormalizedUri, uriIdentifier);

						// Filter out insecure endpoints if high security is required.
						if (uriIdentifier.IsDiscoverySecureEndToEnd) {
							xrdsEndpoints = xrdsEndpoints.Where(se => se.ProviderEndpoint.IsTransportSecure());
						}
						endpoints.AddRange(xrdsEndpoints);
					} catch (XmlException ex) {
						Logger.Yadis.Error("Error while parsing the XRDS document.  Falling back to HTML discovery.", ex);
					}
				}

				// Failing YADIS discovery of an XRDS document, we try HTML discovery.
				if (endpoints.Count == 0) {
					await yadisResult.TryRevertToHtmlResponseAsync();
					var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText));
					if (htmlEndpoints.Any()) {
						Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count);
						Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true));
						endpoints.AddRange(htmlEndpoints.Where(ep => !uriIdentifier.IsDiscoverySecureEndToEnd || ep.ProviderEndpoint.IsTransportSecure()));
						if (endpoints.Count == 0) {
							Logger.Yadis.Info("No HTML discovered endpoints met the security requirements.");
						}
					} else {
						Logger.Yadis.Debug("HTML discovery failed to find any endpoints.");
					}
				} else {
					Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
				}
			}

			return new IdentifierDiscoveryServiceResult(endpoints);
		}
Esempio n. 4
0
		/// <summary>
		/// Performs discovery on the specified identifier.
		/// </summary>
		/// <param name="identifier">The identifier to perform discovery on.</param>
		/// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
		/// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
		/// <returns>
		/// A sequence of service endpoints yielded by discovery.  Must not be null, but may be empty.
		/// </returns>
		public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
			abortDiscoveryChain = false;
			var uriIdentifier = identifier as UriIdentifier;
			if (uriIdentifier == null) {
				return Enumerable.Empty<IdentifierDiscoveryResult>();
			}

			var endpoints = new List<IdentifierDiscoveryResult>();

			// Attempt YADIS discovery
			DiscoveryResult yadisResult = Yadis.Discover(requestHandler, uriIdentifier, identifier.IsDiscoverySecureEndToEnd);
			if (yadisResult != null) {
				if (yadisResult.IsXrds) {
					try {
						XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
						var xrdsEndpoints = xrds.XrdElements.CreateServiceEndpoints(yadisResult.NormalizedUri, uriIdentifier);

						// Filter out insecure endpoints if high security is required.
						if (uriIdentifier.IsDiscoverySecureEndToEnd) {
							xrdsEndpoints = xrdsEndpoints.Where(se => se.ProviderEndpoint.IsTransportSecure());
						}
						endpoints.AddRange(xrdsEndpoints);
					} catch (XmlException ex) {
						Logger.Yadis.Error("Error while parsing the XRDS document.  Falling back to HTML discovery.", ex);
					}
				}

				// Failing YADIS discovery of an XRDS document, we try HTML discovery.
				if (endpoints.Count == 0) {
					yadisResult.TryRevertToHtmlResponse();
					var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText));
					if (htmlEndpoints.Any()) {
						Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count);
						Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true));
						endpoints.AddRange(htmlEndpoints.Where(ep => !uriIdentifier.IsDiscoverySecureEndToEnd || ep.ProviderEndpoint.IsTransportSecure()));
						if (endpoints.Count == 0) {
							Logger.Yadis.Info("No HTML discovered endpoints met the security requirements.");
						}
					} else {
						Logger.Yadis.Debug("HTML discovery failed to find any endpoints.");
					}
				} else {
					Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
				}
			}
			return endpoints;
		}
		/// <summary>
		/// Validates the XML digital signature on an XRDS document.
		/// </summary>
		/// <param name="document">The XRDS document whose signature should be validated.</param>
		/// <param name="identifier">The identifier under discovery.</param>
		/// <param name="response">The response.</param>
		/// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param>
		/// <exception cref="ProtocolException">Thrown if the XRDS document has an invalid or a missing signature.</exception>
		private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) {
			Contract.Requires<ArgumentNullException>(document != null);
			Contract.Requires<ArgumentNullException>(identifier != null);
			Contract.Requires<ArgumentNullException>(response != null);

			var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature");
			var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo");
			ErrorUtilities.VerifyProtocol(
				signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null,
				"Unrecognized or missing canonicalization method.");
			ErrorUtilities.VerifyProtocol(
				signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null,
				"Unrecognized or missing signature method.");
			var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate");
			var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList();

			// Verify that we trust the signer of the certificates.
			// Start by trying to validate just the certificate used to sign the XRDS document,
			// since we can do that with partial trust.
			Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document.");
			if (!certs[0].Verify()) {
				// We couldn't verify just the signing certificate, so try to verify the whole certificate chain.
				try {
					Logger.OpenId.Debug("Verifying the whole certificate chain.");
					VerifyCertChain(certs);
					Logger.OpenId.Debug("Certificate chain verified.");
				} catch (SecurityException) {
					Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation.");
					ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted);
				}
			}

			// Verify that the certificate is issued to the host on whom we are performing discovery.
			string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false);
			ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), "X.509 signing certificate issued to {0}, but a certificate for {1} was expected.", hostName, signingHost);

			// Verify the signature itself
			byte[] signature = Convert.FromBase64String(response.Headers["Signature"]);
			var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key;
			byte[] data = new byte[response.ResponseStream.Length];
			response.ResponseStream.Seek(0, SeekOrigin.Begin);
			response.ResponseStream.Read(data, 0, data.Length);
			ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), "Invalid XmlDSig signature on XRDS document.");
		}
		/// <summary>
		/// Gets the services for an identifier that are described by an external XRDS document.
		/// </summary>
		/// <param name="xrds">The XRD elements to search for described-by services.</param>
		/// <param name="identifier">The identifier under discovery.</param>
		/// <param name="requestHandler">The request handler.</param>
		/// <returns>The discovered services.</returns>
		private static IEnumerable<IdentifierDiscoveryResult> GetExternalServices(IEnumerable<XrdElement> xrds, UriIdentifier identifier, IDirectWebRequestHandler requestHandler) {
			Contract.Requires<ArgumentNullException>(xrds != null);
			Contract.Requires<ArgumentNullException>(identifier != null);
			Contract.Requires<ArgumentNullException>(requestHandler != null);
			Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);

			var results = new List<IdentifierDiscoveryResult>();
			foreach (var serviceElement in GetDescribedByServices(xrds)) {
				var templateNode = serviceElement.Node.SelectSingleNode("google:URITemplate", serviceElement.XmlNamespaceResolver);
				var nextAuthorityNode = serviceElement.Node.SelectSingleNode("google:NextAuthority", serviceElement.XmlNamespaceResolver);
				if (templateNode != null) {
					Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri)));
					string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host;
					try {
						using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) {
							XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream));
							ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority);
							results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier));
						}
					} catch (ProtocolException ex) {
						Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex);
					} catch (XmlException ex) {
						Logger.Yadis.ErrorFormat("Error while parsing described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex);
					}
				}
			}

			return results;
		}
		/// <summary>
		/// Gets the XRD elements that have a given CanonicalID.
		/// </summary>
		/// <param name="document">The XRDS document.</param>
		/// <param name="canonicalId">The CanonicalID to match on.</param>
		/// <returns>A sequence of XRD elements.</returns>
		private static IEnumerable<XrdElement> GetXrdElements(XrdsDocument document, string canonicalId) {
			// filter to include only those XRD elements describing the host whose host-meta pointed us to this document.
			return document.XrdElements.Where(xrd => string.Equals(xrd.CanonicalID, canonicalId, StringComparison.Ordinal));
		}
		/// <summary>
		/// Performs discovery on the specified identifier.
		/// </summary>
		/// <param name="identifier">The identifier to perform discovery on.</param>
		/// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
		/// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
		/// <returns>
		/// A sequence of service endpoints yielded by discovery.  Must not be null, but may be empty.
		/// </returns>
		public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
			abortDiscoveryChain = false;

			// Google Apps are always URIs -- not XRIs.
			var uriIdentifier = identifier as UriIdentifier;
			if (uriIdentifier == null) {
				return Enumerable.Empty<IdentifierDiscoveryResult>();
			}

			var results = new List<IdentifierDiscoveryResult>();
			string signingHost;
			using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) {
				if (response != null) {
					try {
						var document = new XrdsDocument(XmlReader.Create(response.ResponseStream));
						ValidateXmlDSig(document, uriIdentifier, response, signingHost);
						var xrds = GetXrdElements(document, uriIdentifier.Uri.Host);

						// Look for claimed identifier template URIs for an additional XRDS document.
						results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler));

						// If we couldn't find any claimed identifiers, look for OP identifiers.
						// Normally this would be the opposite (OP Identifiers take precedence over
						// claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers
						// mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers,
						// which would break positive assertion checks).
						if (results.Count == 0) {
							results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier));
						}

						abortDiscoveryChain = true;
					} catch (XmlException ex) {
						Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex);
					}
				}
			}

			return results;
		}
Esempio n. 9
0
		/// <summary>
		/// Initializes a new instance of the <see cref="XrdElement"/> class.
		/// </summary>
		/// <param name="xrdElement">The XRD element.</param>
		/// <param name="parent">The parent.</param>
		public XrdElement(XPathNavigator xrdElement, XrdsDocument parent) :
			base(xrdElement, parent) {
		}
		/// <summary>
		/// Downloads the XRDS document for this XRI.
		/// </summary>
		/// <param name="identifier">The identifier.</param>
		/// <param name="hostFactories">The host factories.</param>
		/// <param name="cancellationToken">The cancellation token.</param>
		/// <returns>
		/// The XRDS document.
		/// </returns>
		private static async Task<XrdsDocument> DownloadXrdsAsync(XriIdentifier identifier, IHostFactories hostFactories, CancellationToken cancellationToken) {
			Requires.NotNull(identifier, "identifier");
			Requires.NotNull(hostFactories, "hostFactories");

			XrdsDocument doc;
			using (var xrdsResponse = await Yadis.RequestAsync(GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd, hostFactories, cancellationToken)) {
				xrdsResponse.EnsureSuccessStatusCode();
				var readerSettings = MessagingUtilities.CreateUntrustedXmlReaderSettings();
				ErrorUtilities.VerifyProtocol(xrdsResponse.Content != null, "XRDS request \"{0}\" returned no response.", GetXrdsUrl(identifier));
				await xrdsResponse.Content.LoadIntoBufferAsync();
				using (var xrdsStream = await xrdsResponse.Content.ReadAsStreamAsync()) {
					doc = new XrdsDocument(XmlReader.Create(xrdsStream, readerSettings));
				}
			}

			ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed);
			return doc;
		}
Esempio n. 11
0
 /// <summary>
 /// Initializes a new instance of the <see cref="XrdElement"/> class.
 /// </summary>
 /// <param name="xrdElement">The XRD element.</param>
 /// <param name="parent">The parent.</param>
 public XrdElement(XPathNavigator xrdElement, XrdsDocument parent) :
     base(xrdElement, parent)
 {
 }
Esempio n. 12
0
        /// <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="requestHandler">The mechanism to use for sending HTTP requests.</param>
        /// <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; or <c>null</c> if no service document was discovered.
        /// </returns>
        internal virtual IEnumerable<RelyingPartyEndpointDescription> Discover(IDirectWebRequestHandler requestHandler, bool allowRedirects)
        {
            // Attempt YADIS discovery
            DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this.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 null;
        }
		private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) {
			Requires.NotNull(document, "document");
			Requires.NotNull(identifier, "identifier");
			Requires.NotNull(response, "response");

			var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature");
			var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo");
			ErrorUtilities.VerifyProtocol(
				signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null,
				OpenIdStrings.UnsupportedCanonicalizationMethod);
			ErrorUtilities.VerifyProtocol(
				signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null,
				OpenIdStrings.UnsupportedSignatureMethod);
			var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver);
			ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate");
			var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList();

			VerifyCertificateChain(certs);

			// Verify that the certificate is issued to the host on whom we are performing discovery.
			string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false);
			ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), OpenIdStrings.MisdirectedSigningCertificate, hostName, signingHost);

			// Verify the signature itself
			byte[] signature = Convert.FromBase64String(response.Headers["Signature"]);
			var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key;
			byte[] data = new byte[response.ResponseStream.Length];
			response.ResponseStream.Seek(0, SeekOrigin.Begin);
			response.ResponseStream.Read(data, 0, data.Length);
			ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), OpenIdStrings.InvalidDSig);
		}
Esempio n. 14
0
        /// <summary>
        /// Performs discovery on the Identifier.
        /// </summary>
        /// <param name="requestHandler">The web request handler to use for discovery.</param>
        /// <returns>
        /// An initialized structure containing the discovered provider endpoint information.
        /// </returns>
        internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler)
        {
            List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();

            // Attempt YADIS discovery
            DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this, IsDiscoverySecureEndToEnd);
            if (yadisResult != null) {
                if (yadisResult.IsXrds) {
                    XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
                    var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri, this);

                    // Filter out insecure endpoints if high security is required.
                    if (IsDiscoverySecureEndToEnd) {
                        xrdsEndpoints = xrdsEndpoints.Where(se => se.IsSecure);
                    }
                    endpoints.AddRange(xrdsEndpoints);
                }

                // Failing YADIS discovery of an XRDS document, we try HTML discovery.
                if (endpoints.Count == 0) {
                    var htmlEndpoints = new List<ServiceEndpoint>(DiscoverFromHtml(yadisResult.NormalizedUri, this, yadisResult.ResponseText));
                    if (htmlEndpoints.Any()) {
                        Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count);
                        Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true));
                        endpoints.AddRange(htmlEndpoints.Where(ep => !IsDiscoverySecureEndToEnd || ep.IsSecure));
                        if (endpoints.Count == 0) {
                            Logger.Yadis.Info("No HTML discovered endpoints met the security requirements.");
                        }
                    } else {
                        Logger.Yadis.Debug("HTML discovery failed to find any endpoints.");
                    }
                } else {
                    Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
                }
            }
            return endpoints;
        }