//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. 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 + "/" + Util.AssemblyFileVersion; #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 (response != null) { Logger.Http.ErrorFormat( "{0} returned {1} {2}: {3}", response.ResponseUri, (int)response.StatusCode, response.StatusCode, response.StatusDescription); if (Logger.Http.IsDebugEnabled) { using (var reader = new StreamReader(ex.Response.GetResponseStream())) { Logger.Http.DebugFormat( "WebException from {0}: {1}{2}", ex.Response.ResponseUri, Environment.NewLine, reader.ReadToEnd()); } } } else { Logger.Http.ErrorFormat( "{0} connecting to {0}", ex.Status, request.RequestUri); } // 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; } } } } }