//-----------------------------------------------------------------------
//
// 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;
}
}
}