summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs')
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs249
1 files changed, 249 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
new file mode 100644
index 0000000..6c6a7bb
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
@@ -0,0 +1,249 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+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;
+
+ /// <summary>
+ /// The default handler for transmitting <see cref="HttpWebRequest"/> instances
+ /// and returning the responses.
+ /// </summary>
+ public class StandardWebRequestHandler : IDirectWebRequestHandler {
+ /// <summary>
+ /// The set of options this web request handler supports.
+ /// </summary>
+ private const DirectWebRequestOptions SupportedOptions = DirectWebRequestOptions.AcceptAllHttpResponses;
+
+ /// <summary>
+ /// The value to use for the User-Agent HTTP header.
+ /// </summary>
+ private static string userAgentValue = Assembly.GetExecutingAssembly().GetName().Name + "/" + Assembly.GetExecutingAssembly().GetName().Version;
+
+ #region IWebRequestHandler Members
+
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ public bool CanSupport(DirectWebRequestOptions options) {
+ return (options & ~SupportedOptions) == 0;
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ public Stream GetRequestStream(HttpWebRequest request) {
+ return this.GetRequestStream(request, DirectWebRequestOptions.None);
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
+ return GetRequestStreamCore(request);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ public IncomingWebResponse GetResponse(HttpWebRequest request) {
+ return this.GetResponse(request, DirectWebRequestOptions.None);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ 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
+
+ /// <summary>
+ /// Determines whether an exception was thrown because of the remote HTTP server returning HTTP 417 Expectation Failed.
+ /// </summary>
+ /// <param name="ex">The caught exception.</param>
+ /// <returns>
+ /// <c>true</c> if the failure was originally caused by a 417 Exceptation Failed error; otherwise, <c>false</c>.
+ /// </returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Initiates a POST request and prepares for sending data.
+ /// </summary>
+ /// <param name="request">The HTTP request with information about the remote party to contact.</param>
+ /// <returns>
+ /// The stream where the POST entity can be written.
+ /// </returns>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Prepares an HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="preparingPost"><c>true</c> if this is a POST request whose headers have not yet been sent out; <c>false</c> otherwise.</param>
+ 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;
+ }
+ }
+ }
+ }
+}