//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using DotNetOpenAuth.Messaging;
///
/// The default handler for transmitting instances
/// and returning the responses.
///
public class StandardWebRequestHandler : IDirectWebRequestHandler {
///
/// The set of options this web request handler supports.
///
private const DirectWebRequestOptions SupportedOptions = DirectWebRequestOptions.AcceptAllHttpResponses;
///
/// The value to use for the User-Agent HTTP header.
///
private static string userAgentValue = Assembly.GetExecutingAssembly().GetName().Name + "/" + Assembly.GetExecutingAssembly().GetName().Version;
#region IWebRequestHandler Members
///
/// Determines whether this instance can support the specified options.
///
/// The set of options that might be given in a subsequent web request.
///
/// true if this instance can support the specified options; otherwise, false.
///
[Pure]
public bool CanSupport(DirectWebRequestOptions options) {
return (options & ~SupportedOptions) == 0;
}
///
/// Prepares an that contains an POST entity for sending the entity.
///
/// The that should contain the entity.
///
/// The writer the caller should write out the entity data to.
///
/// Thrown for any network error.
///
/// The caller should have set the
/// and any other appropriate properties before calling this method.
/// Implementations should catch and wrap it in a
/// to abstract away the transport and provide
/// a single exception type for hosts to catch.
///
public Stream GetRequestStream(HttpWebRequest request) {
return this.GetRequestStream(request, DirectWebRequestOptions.None);
}
///
/// Prepares an that contains an POST entity for sending the entity.
///
/// The that should contain the entity.
/// The options to apply to this web request.
///
/// The writer the caller should write out the entity data to.
///
/// Thrown for any network error.
///
/// The caller should have set the
/// and any other appropriate properties before calling this method.
/// Implementations should catch and wrap it in a
/// to abstract away the transport and provide
/// a single exception type for hosts to catch.
///
public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
return GetRequestStreamCore(request);
}
///
/// Processes an and converts the
/// to a instance.
///
/// The to handle.
///
/// An instance of describing the response.
///
/// Thrown for any network error.
///
/// Implementations should catch and wrap it in a
/// to abstract away the transport and provide
/// a single exception type for hosts to catch. The
/// value, if set, should be Closed before throwing.
///
public IncomingWebResponse GetResponse(HttpWebRequest request) {
return this.GetResponse(request, DirectWebRequestOptions.None);
}
///
/// Processes an and converts the
/// to a instance.
///
/// The to handle.
/// The options to apply to this web request.
///
/// An instance of describing the response.
///
/// Thrown for any network error.
///
/// Implementations should catch and wrap it in a
/// to abstract away the transport and provide
/// a single exception type for hosts to catch. The
/// value, if set, should be Closed before throwing.
///
public IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
// This request MAY have already been prepared by GetRequestStream, but
// we have no guarantee, so do it just to be safe.
PrepareRequest(request, false);
try {
Logger.Http.DebugFormat("HTTP {0} {1}", request.Method, request.RequestUri);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
return new NetworkDirectWebResponse(request.RequestUri, response);
} catch (WebException ex) {
HttpWebResponse response = (HttpWebResponse)ex.Response;
if (response != null && response.StatusCode == HttpStatusCode.ExpectationFailed &&
request.ServicePoint.Expect100Continue) {
// Some OpenID servers doesn't understand the Expect header and send 417 error back.
// If this server just failed from that, alter the ServicePoint for this server
// so that we don't send that header again next time (whenever that is).
// "Expect: 100-Continue" HTTP header. (see Google Code Issue 72)
// 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.
Logger.Http.InfoFormat("HTTP POST to {0} resulted in 417 Expectation Failed. Changing ServicePoint to not use Expect: Continue next time.", request.RequestUri);
request.ServicePoint.Expect100Continue = false; // TODO: investigate that CAS may throw here
// An alternative to ServicePoint if we don't have permission to set that,
// but we'd have to set it BEFORE each request.
////request.Expect = "";
}
if ((options & DirectWebRequestOptions.AcceptAllHttpResponses) != 0 && response != null &&
response.StatusCode != HttpStatusCode.ExpectationFailed) {
Logger.Http.InfoFormat("The HTTP error code {0} {1} is being accepted because the {2} flag is set.", (int)response.StatusCode, response.StatusCode, DirectWebRequestOptions.AcceptAllHttpResponses);
return new NetworkDirectWebResponse(request.RequestUri, response);
}
if (Logger.Http.IsErrorEnabled) {
if (response != null) {
using (var reader = new StreamReader(ex.Response.GetResponseStream())) {
Logger.Http.ErrorFormat("WebException from {0}: {1}{2}", ex.Response.ResponseUri, Environment.NewLine, reader.ReadToEnd());
}
} else {
Logger.Http.ErrorFormat("WebException {1} from {0}, no response available.", request.RequestUri, ex.Status);
}
}
// Be sure to close the response stream to conserve resources and avoid
// filling up all our incoming pipes and denying future requests.
// If in the future, some callers actually want to read this response
// we'll need to figure out how to reliably call Close on exception
// responses at all callers.
if (response != null) {
response.Close();
}
throw ErrorUtilities.Wrap(ex, MessagingStrings.ErrorInRequestReplyMessage);
}
}
#endregion
///
/// Determines whether an exception was thrown because of the remote HTTP server returning HTTP 417 Expectation Failed.
///
/// The caught exception.
///
/// true if the failure was originally caused by a 417 Exceptation Failed error; otherwise, false.
///
internal static bool IsExceptionFrom417ExpectationFailed(Exception ex) {
while (ex != null) {
WebException webEx = ex as WebException;
if (webEx != null) {
HttpWebResponse response = webEx.Response as HttpWebResponse;
if (response != null) {
if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
return true;
}
}
}
ex = ex.InnerException;
}
return false;
}
///
/// Initiates a POST request and prepares for sending data.
///
/// The HTTP request with information about the remote party to contact.
///
/// The stream where the POST entity can be written.
///
private static Stream GetRequestStreamCore(HttpWebRequest request) {
PrepareRequest(request, true);
try {
return request.GetRequestStream();
} catch (SocketException ex) {
throw ErrorUtilities.Wrap(ex, MessagingStrings.WebRequestFailed, request.RequestUri);
} catch (WebException ex) {
throw ErrorUtilities.Wrap(ex, MessagingStrings.WebRequestFailed, request.RequestUri);
}
}
///
/// Prepares an HTTP request.
///
/// The request.
/// true if this is a POST request whose headers have not yet been sent out; false otherwise.
private static void PrepareRequest(HttpWebRequest request, bool preparingPost) {
Requires.NotNull(request, "request");
// Be careful to not try to change the HTTP headers that have already gone out.
if (preparingPost || request.Method == "GET") {
// Set/override a few properties of the request to apply our policies for requests.
if (Debugger.IsAttached) {
// Since a debugger is attached, requests may be MUCH slower,
// so give ourselves huge timeouts.
request.ReadWriteTimeout = (int)TimeSpan.FromHours(1).TotalMilliseconds;
request.Timeout = (int)TimeSpan.FromHours(1).TotalMilliseconds;
}
// Some sites, such as Technorati, return 403 Forbidden on identity
// pages unless a User-Agent header is included.
if (string.IsNullOrEmpty(request.UserAgent)) {
request.UserAgent = userAgentValue;
}
}
}
}
}