//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation, Scott Hanselman. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Yadis { using System; using System.Diagnostics.Contracts; using System.IO; using System.Net; using System.Net.Cache; using System.Web.UI.HtmlControls; using System.Xml; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.Xrds; /// /// YADIS discovery manager. /// internal class Yadis { /// /// The HTTP header to look for in responses to declare where the XRDS document should be found. /// internal const string HeaderName = "X-XRDS-Location"; /// /// Gets or sets the cache that can be used for HTTP requests made during identifier discovery. /// #if DEBUG internal static readonly RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.BypassCache); #else internal static readonly RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(OpenIdElement.Configuration.CacheDiscovery ? HttpRequestCacheLevel.CacheIfAvailable : HttpRequestCacheLevel.BypassCache); #endif /// /// The maximum number of bytes to read from an HTTP response /// in searching for a link to a YADIS document. /// internal const int MaximumResultToScan = 1024 * 1024; /// /// Performs YADIS discovery on some identifier. /// /// The mechanism to use for sending HTTP requests. /// The URI to perform discovery on. /// Whether discovery should fail if any step of it is not encrypted. /// /// The result of discovery on the given URL. /// Null may be returned if an error occurs, /// or if is true but part of discovery /// is not protected by SSL. /// public static DiscoveryResult Discover(IDirectWebRequestHandler requestHandler, UriIdentifier uri, bool requireSsl) { CachedDirectWebResponse response; try { if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { Logger.Yadis.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri); return null; } response = Request(requestHandler, uri, requireSsl, ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); if (response.Status != System.Net.HttpStatusCode.OK) { Logger.Yadis.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response.Status, response.Status, uri); return null; } } catch (ArgumentException ex) { // Unsafe URLs generate this Logger.Yadis.WarnFormat("Unsafe OpenId URL detected ({0}). Request aborted. {1}", uri, ex); return null; } CachedDirectWebResponse response2 = null; if (IsXrdsDocument(response)) { Logger.Yadis.Debug("An XRDS response was received from GET at user-supplied identifier."); Reporting.RecordEventOccurrence("Yadis", "XRDS in initial response"); response2 = response; } else { string uriString = response.Headers.Get(HeaderName); Uri url = null; if (uriString != null) { if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) { Logger.Yadis.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url); Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTTP header"); } } if (url == null && response.ContentType != null && (response.ContentType.MediaType == ContentTypes.Html || response.ContentType.MediaType == ContentTypes.XHtml)) { url = FindYadisDocumentLocationInHtmlMetaTags(response.GetResponseString()); if (url != null) { Logger.Yadis.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url); Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTML"); } } if (url != null) { if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { response2 = Request(requestHandler, url, requireSsl, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); if (response2.Status != HttpStatusCode.OK) { Logger.Yadis.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response2.Status, response2.Status, uri); } } else { Logger.Yadis.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url); } } } return new DiscoveryResult(uri, response, response2); } /// /// Searches an HTML document for a /// <meta http-equiv="X-XRDS-Location" content="{YadisURL}"> /// tag and returns the content of YadisURL. /// /// The HTML to search. /// The URI of the XRDS document if found; otherwise null. public static Uri FindYadisDocumentLocationInHtmlMetaTags(string html) { foreach (var metaTag in HtmlParser.HeadTags(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; } /// /// Sends a YADIS HTTP request as part of identifier discovery. /// /// The request handler to use to actually submit the request. /// The URI to GET. /// Whether only HTTPS URLs should ever be retrieved. /// The value of the Accept HTTP header to include in the request. /// The HTTP response retrieved from the request. internal static IncomingWebResponse Request(IDirectWebRequestHandler requestHandler, Uri uri, bool requireSsl, params string[] acceptTypes) { Requires.NotNull(requestHandler, "requestHandler"); Requires.NotNull(uri, "uri"); Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().ResponseStream != null); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); request.CachePolicy = IdentifierDiscoveryCachePolicy; if (acceptTypes != null) { request.Accept = string.Join(",", acceptTypes); } DirectWebRequestOptions options = DirectWebRequestOptions.None; if (requireSsl) { options |= DirectWebRequestOptions.RequireSsl; } try { return requestHandler.GetResponse(request, options); } catch (ProtocolException ex) { var webException = ex.InnerException as WebException; if (webException != null) { var response = webException.Response as HttpWebResponse; if (response != null && response.IsFromCache) { // We don't want to report error responses from the cache, since the server may have fixed // whatever was causing the problem. So try again with cache disabled. Logger.Messaging.Error("An HTTP error response was obtained from the cache. Retrying with cache disabled.", ex); var nonCachingRequest = request.Clone(); nonCachingRequest.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.Reload); return requestHandler.GetResponse(nonCachingRequest, options); } } throw; } } /// /// Determines whether a given HTTP response constitutes an XRDS document. /// /// The response to test. /// /// true if the response constains an XRDS document; otherwise, false. /// private static bool IsXrdsDocument(CachedDirectWebResponse response) { if (response.ContentType == null) { return false; } 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. response.ResponseStream.Seek(0, SeekOrigin.Begin); var readerSettings = MessagingUtilities.CreateUntrustedXmlReaderSettings(); XmlReader reader = XmlReader.Create(response.ResponseStream, readerSettings); while (reader.Read() && reader.NodeType != XmlNodeType.Element) { // intentionally blank } if (reader.NamespaceURI == XrdsNode.XrdsNamespace && reader.Name == "XRDS") { return true; } } return false; } } }