diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-12-29 21:20:40 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-12-29 21:20:40 -0800 |
commit | 187d3c24b6a76ec0898399f738b3a4f82031ceb0 (patch) | |
tree | 5ee920acbfbf1bca76e3a7b4edfcb04e930e2367 /src/DotNetOpenAuth.Core/Messaging | |
parent | 5e9014f36b2d53b8e419918675df636540ea24e2 (diff) | |
download | DotNetOpenAuth-187d3c24b6a76ec0898399f738b3a4f82031ceb0.zip DotNetOpenAuth-187d3c24b6a76ec0898399f738b3a4f82031ceb0.tar.gz DotNetOpenAuth-187d3c24b6a76ec0898399f738b3a4f82031ceb0.tar.bz2 |
Replaces IDirectWebRequestHandler with HttpClient in DNOA.Core.
Build breaks are everywhere outside of just this one project as a result.
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging')
13 files changed, 87 insertions, 2257 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs deleted file mode 100644 index 20b1831..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs +++ /dev/null @@ -1,182 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="CachedDirectWebResponse.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Net; - using System.Text; - using Validation; - - /// <summary> - /// Cached details on the response from a direct web request to a remote party. - /// </summary> - [DebuggerDisplay("{Status} {ContentType.MediaType}, length: {ResponseStream.Length}")] - internal class CachedDirectWebResponse : IncomingWebResponse { - /// <summary> - /// A seekable, repeatable response stream. - /// </summary> - private MemoryStream responseStream; - - /// <summary> - /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. - /// </summary> - internal CachedDirectWebResponse() { - } - - /// <summary> - /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. - /// </summary> - /// <param name="requestUri">The request URI.</param> - /// <param name="response">The response.</param> - /// <param name="maximumBytesToRead">The maximum bytes to read.</param> - internal CachedDirectWebResponse(Uri requestUri, HttpWebResponse response, int maximumBytesToRead) - : base(requestUri, response) { - Requires.NotNull(requestUri, "requestUri"); - Requires.NotNull(response, "response"); - this.responseStream = CacheNetworkStreamAndClose(response, maximumBytesToRead); - - // BUGBUG: if the response was exactly maximumBytesToRead, we'll incorrectly believe it was truncated. - this.ResponseTruncated = this.responseStream.Length == maximumBytesToRead; - } - - /// <summary> - /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class. - /// </summary> - /// <param name="requestUri">The request URI.</param> - /// <param name="responseUri">The final URI to respond to the request.</param> - /// <param name="headers">The headers.</param> - /// <param name="statusCode">The status code.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="contentEncoding">The content encoding.</param> - /// <param name="responseStream">The response stream.</param> - internal CachedDirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding, MemoryStream responseStream) - : base(requestUri, responseUri, headers, statusCode, contentType, contentEncoding) { - Requires.NotNull(requestUri, "requestUri"); - Requires.NotNull(responseStream, "responseStream"); - this.responseStream = responseStream; - } - - /// <summary> - /// Gets a value indicating whether the cached response stream was - /// truncated to a maximum allowable length. - /// </summary> - public bool ResponseTruncated { get; private set; } - - /// <summary> - /// Gets the body of the HTTP response. - /// </summary> - public override Stream ResponseStream { - get { return this.responseStream; } - } - - /// <summary> - /// Gets or sets the cached response stream. - /// </summary> - internal MemoryStream CachedResponseStream { - get { return this.responseStream; } - set { this.responseStream = value; } - } - - /// <summary> - /// Creates a text reader for the response stream. - /// </summary> - /// <returns>The text reader, initialized for the proper encoding.</returns> - public override StreamReader GetResponseReader() { - this.ResponseStream.Seek(0, SeekOrigin.Begin); - string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - Encoding encoding = null; - if (!string.IsNullOrEmpty(contentEncoding)) { - try { - encoding = Encoding.GetEncoding(contentEncoding); - } catch (ArgumentException ex) { - Logger.Messaging.ErrorFormat("Encoding.GetEncoding(\"{0}\") threw ArgumentException: {1}", contentEncoding, ex); - } - } - - return encoding != null ? new StreamReader(this.ResponseStream, encoding) : new StreamReader(this.ResponseStream); - } - - /// <summary> - /// Gets the body of the response as a string. - /// </summary> - /// <returns>The entire body of the response.</returns> - internal string GetResponseString() { - if (this.ResponseStream != null) { - string value = this.GetResponseReader().ReadToEnd(); - this.ResponseStream.Seek(0, SeekOrigin.Begin); - return value; - } else { - return null; - } - } - - /// <summary> - /// Gets an offline snapshot version of this instance. - /// </summary> - /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> - /// <returns>A snapshot version of this instance.</returns> - /// <remarks> - /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot - /// will automatically close and dispose of the underlying response stream. - /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will - /// be the self same instance. - /// </remarks> - internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) { - return this; - } - - /// <summary> - /// Sets the response to some string, encoded as UTF-8. - /// </summary> - /// <param name="body">The string to set the response to.</param> - internal void SetResponse(string body) { - if (body == null) { - this.responseStream = null; - return; - } - - Encoding encoding = Encoding.UTF8; - this.Headers[HttpResponseHeader.ContentEncoding] = encoding.HeaderName; - this.responseStream = new MemoryStream(); - StreamWriter writer = new StreamWriter(this.ResponseStream, encoding); - writer.Write(body); - writer.Flush(); - this.ResponseStream.Seek(0, SeekOrigin.Begin); - } - - /// <summary> - /// Caches the network stream and closes it if it is open. - /// </summary> - /// <param name="response">The response whose stream is to be cloned.</param> - /// <param name="maximumBytesToRead">The maximum bytes to cache.</param> - /// <returns>The seekable Stream instance that contains a copy of what was returned in the HTTP response.</returns> - [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Assume(System.Boolean,System.String,System.String)", Justification = "No localization required.")] - private static MemoryStream CacheNetworkStreamAndClose(HttpWebResponse response, int maximumBytesToRead) { - Requires.NotNull(response, "response"); - - // Now read and cache the network stream - Stream networkStream = response.GetResponseStream(); - MemoryStream cachedStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : Math.Min((int)response.ContentLength, maximumBytesToRead)); - try { - Assumes.True(networkStream.CanRead, "HttpWebResponse.GetResponseStream() always returns a readable stream."); // CC missing - Assumes.True(cachedStream.CanWrite, "This is a MemoryStream -- it's always writable."); // CC missing - networkStream.CopyTo(cachedStream); - cachedStream.Seek(0, SeekOrigin.Begin); - - networkStream.Dispose(); - response.Close(); - - return cachedStream; - } catch { - cachedStream.Dispose(); - throw; - } - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs index 9c2ba8c..ad094e0 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -17,10 +17,13 @@ namespace DotNetOpenAuth.Messaging { using System.Linq; using System.Net; using System.Net.Cache; + using System.Net.Http; + using System.Net.Http.Headers; using System.Net.Mime; using System.Runtime.Serialization.Json; using System.Text; using System.Threading; + using System.Threading.Tasks; using System.Web; using System.Xml; using DotNetOpenAuth.Messaging.Reflection; @@ -156,11 +159,20 @@ namespace DotNetOpenAuth.Messaging { /// The binding elements to use in sending and receiving messages. /// The order they are provided is used for outgoing messgaes, and reversed for incoming messages. /// </param> - protected Channel(IMessageFactory messageTypeProvider, params IChannelBindingElement[] bindingElements) { + /// <param name="messageHandler">The HTTP handler to use for outgoing HTTP requests.</param> + protected Channel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements, HttpMessageHandler messageHandler = null) { Requires.NotNull(messageTypeProvider, "messageTypeProvider"); + messageHandler = messageHandler ?? new WebRequestHandler(); + var httpHandler = messageHandler as WebRequestHandler; + if (httpHandler != null) { + // TODO: provide this as a recommendation to derived Channel types, but without tampering with + // the setting once it has been provided to this constructor. + httpHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + } + this.messageTypeProvider = messageTypeProvider; - this.WebRequestHandler = new StandardWebRequestHandler(); + this.WebRequestHandler = new HttpClient(messageHandler); this.XmlDictionaryReaderQuotas = DefaultUntrustedXmlDictionaryReaderQuotas; this.outgoingBindingElements = new List<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements)); @@ -178,14 +190,14 @@ namespace DotNetOpenAuth.Messaging { internal event EventHandler<ChannelEventArgs> Sending; /// <summary> - /// Gets or sets an instance to a <see cref="IDirectWebRequestHandler"/> that will be used when + /// Gets or sets an instance to a <see cref="HttpClient"/> that will be used when /// submitting HTTP requests and waiting for responses. /// </summary> /// <remarks> /// This defaults to a straightforward implementation, but can be set /// to a mock object for testing purposes. /// </remarks> - public IDirectWebRequestHandler WebRequestHandler { get; set; } + public HttpClient WebRequestHandler { get; set; } /// <summary> /// Gets or sets the maximum allowable size for a 301 Redirect response before we send @@ -272,75 +284,24 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Gets or sets the cache policy to use for direct message requests. - /// </summary> - /// <value>Default is <see cref="HttpRequestCacheLevel.NoCacheNoStore"/>.</value> - protected RequestCachePolicy CachePolicy { - get { - return this.cachePolicy; - } - - set { - Requires.NotNull(value, "value"); - this.cachePolicy = value; - } - } - - /// <summary> /// Gets or sets the XML dictionary reader quotas. /// </summary> /// <value>The XML dictionary reader quotas.</value> protected virtual XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas { get; set; } /// <summary> - /// Sends an indirect message (either a request or response) - /// or direct message response for transmission to a remote party - /// and ends execution on the current page or handler. - /// </summary> - /// <param name="message">The one-way message to send</param> - /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> - /// <remarks> - /// Requires an HttpContext.Current context. - /// </remarks> - [EditorBrowsable(EditorBrowsableState.Never)] - public void Send(IProtocolMessage message) { - RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); - Requires.NotNull(message, "message"); - this.PrepareResponse(message).Respond(HttpContext.Current, true); - } - - /// <summary> - /// Sends an indirect message (either a request or response) - /// or direct message response for transmission to a remote party - /// and skips most of the remaining ASP.NET request handling pipeline. - /// Not safe to call from ASP.NET web forms. - /// </summary> - /// <param name="message">The one-way message to send</param> - /// <remarks> - /// Requires an HttpContext.Current context. - /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because - /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response. - /// Use the <see cref="Send"/> method instead for web forms. - /// </remarks> - public void Respond(IProtocolMessage message) { - RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); - Requires.NotNull(message, "message"); - this.PrepareResponse(message).Respond(); - } - - /// <summary> /// Prepares an indirect message (either a request or response) /// or direct message response for transmission to a remote party. /// </summary> /// <param name="message">The one-way message to send</param> /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> - public OutgoingWebResponse PrepareResponse(IProtocolMessage message) { + public HttpResponseMessage PrepareResponse(IProtocolMessage message) { Requires.NotNull(message, "message"); this.ProcessOutgoingMessage(message); Logger.Channel.DebugFormat("Sending message: {0}", message.GetType().Name); - OutgoingWebResponse result; + HttpResponseMessage result; switch (message.Transport) { case MessageTransport.Direct: // This is a response to a direct message. @@ -369,8 +330,13 @@ namespace DotNetOpenAuth.Messaging { // Apply caching policy to any response. We want to disable all caching because in auth* protocols, // caching can be utilized in identity spoofing attacks. - result.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, max-age=0, must-revalidate"; - result.Headers[HttpResponseHeader.Pragma] = "no-cache"; + result.Headers.CacheControl = new CacheControlHeaderValue { + NoCache = true, + NoStore = true, + MaxAge = TimeSpan.Zero, + MustRevalidate = true, + }; + result.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); return result; } @@ -502,11 +468,11 @@ namespace DotNetOpenAuth.Messaging { /// or an unexpected type of message is received. /// </exception> [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")] - public TResponse Request<TResponse>(IDirectedProtocolMessage requestMessage) + public async Task<TResponse> RequestAsync<TResponse>(IDirectedProtocolMessage requestMessage) where TResponse : class, IProtocolMessage { Requires.NotNull(requestMessage, "requestMessage"); - IProtocolMessage response = this.Request(requestMessage); + IProtocolMessage response = await this.RequestAsync(requestMessage); ErrorUtilities.VerifyProtocol(response != null, MessagingStrings.ExpectedMessageNotReceived, typeof(TResponse)); var expectedResponse = response as TResponse; @@ -521,12 +487,12 @@ namespace DotNetOpenAuth.Messaging { /// <param name="requestMessage">The message to send.</param> /// <returns>The remote party's response. Guaranteed to never be null.</returns> /// <exception cref="ProtocolException">Thrown if the response does not include a protocol message.</exception> - public IProtocolMessage Request(IDirectedProtocolMessage requestMessage) { + public async Task<IProtocolMessage> RequestAsync(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); this.ProcessOutgoingMessage(requestMessage); Logger.Channel.DebugFormat("Sending {0} request.", requestMessage.GetType().Name); - var responseMessage = this.RequestCore(requestMessage); + var responseMessage = await this.RequestCoreAsync(requestMessage); ErrorUtilities.VerifyProtocol(responseMessage != null, MessagingStrings.ExpectedMessageNotReceived, typeof(IProtocolMessage).Name); Logger.Channel.DebugFormat("Received {0} response.", responseMessage.GetType().Name); @@ -565,10 +531,10 @@ namespace DotNetOpenAuth.Messaging { /// <param name="request">The message to send.</param> /// <returns>The <see cref="HttpWebRequest"/> prepared to send the request.</returns> /// <remarks> - /// This method must be overridden by a derived class, unless the <see cref="RequestCore"/> method + /// This method must be overridden by a derived class, unless the <see cref="RequestCoreAsync"/> method /// is overridden and does not require this method. /// </remarks> - internal HttpWebRequest CreateHttpRequestTestHook(IDirectedProtocolMessage request) { + internal HttpRequestMessage CreateHttpRequestTestHook(IDirectedProtocolMessage request) { return this.CreateHttpRequest(request); } @@ -581,7 +547,7 @@ namespace DotNetOpenAuth.Messaging { /// <remarks> /// This method implements spec OAuth V1.0 section 5.3. /// </remarks> - internal OutgoingWebResponse PrepareDirectResponseTestHook(IProtocolMessage response) { + internal HttpResponseMessage PrepareDirectResponseTestHook(IProtocolMessage response) { return this.PrepareDirectResponse(response); } @@ -591,7 +557,7 @@ namespace DotNetOpenAuth.Messaging { /// <param name="response">The response that is anticipated to contain an protocol message.</param> /// <returns>The deserialized message parts, if found. Null otherwise.</returns> /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> - internal IDictionary<string, string> ReadFromResponseCoreTestHook(IncomingWebResponse response) { + internal IDictionary<string, string> ReadFromResponseCoreTestHook(HttpResponseMessage response) { return this.ReadFromResponseCore(response); } @@ -659,11 +625,11 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="message">The message.</param> /// <param name="response">The HTTP response.</param> - protected static void ApplyMessageTemplate(IMessage message, OutgoingWebResponse response) { + protected static void ApplyMessageTemplate(IMessage message, HttpResponseMessage response) { Requires.NotNull(message, "message"); var httpMessage = message as IHttpDirectResponse; if (httpMessage != null) { - response.Status = httpMessage.HttpStatusCode; + response.StatusCode = httpMessage.HttpStatusCode; foreach (string headerName in httpMessage.Headers) { response.Headers.Add(headerName, httpMessage.Headers[headerName]); } @@ -699,17 +665,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Gets the direct response of a direct HTTP request. - /// </summary> - /// <param name="webRequest">The web request.</param> - /// <returns>The response to the web request.</returns> - /// <exception cref="ProtocolException">Thrown on network or protocol errors.</exception> - protected virtual IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) { - Requires.NotNull(webRequest, "webRequest"); - return this.WebRequestHandler.GetResponse(webRequest); - } - - /// <summary> /// Submits a direct request message to some remote party and blocks waiting for an immediately reply. /// </summary> /// <param name="request">The request message.</param> @@ -719,23 +674,23 @@ namespace DotNetOpenAuth.Messaging { /// behavior. However in non-HTTP frameworks, such as unit test mocks, it may be appropriate to override /// this method to eliminate all use of an HTTP transport. /// </remarks> - protected virtual IProtocolMessage RequestCore(IDirectedProtocolMessage request) { + protected virtual async Task<IProtocolMessage> RequestCoreAsync(IDirectedProtocolMessage request) { Requires.NotNull(request, "request"); Requires.That(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient); - HttpWebRequest webRequest = this.CreateHttpRequest(request); + var webRequest = this.CreateHttpRequest(request); var directRequest = request as IHttpDirectRequest; if (directRequest != null) { foreach (string header in directRequest.Headers) { - webRequest.Headers[header] = directRequest.Headers[header]; + webRequest.Headers.Add(header, directRequest.Headers[header]); } } IDictionary<string, string> responseFields; IDirectResponseProtocolMessage responseMessage; - using (IncomingWebResponse response = this.GetDirectResponse(webRequest)) { - if (response.ResponseStream == null) { + using (HttpResponseMessage response = await this.WebRequestHandler.SendAsync(webRequest)) { + if (response.Content == null) { return null; } @@ -763,7 +718,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="response">The HTTP direct response.</param> /// <param name="message">The newly instantiated message, prior to deserialization.</param> - protected virtual void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) { + protected virtual void OnReceivingDirectResponse(HttpResponseMessage response, IDirectResponseProtocolMessage message) { } /// <summary> @@ -827,7 +782,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="message">The message to send.</param> /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> - protected virtual OutgoingWebResponse PrepareIndirectResponse(IDirectedProtocolMessage message) { + protected virtual HttpResponseMessage PrepareIndirectResponse(IDirectedProtocolMessage message) { Requires.NotNull(message, "message"); Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient); Requires.That((message.HttpMethods & (HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest)) != 0, "message", "GET or POST expected."); @@ -837,7 +792,7 @@ namespace DotNetOpenAuth.Messaging { Assumes.True(message != null && message.Recipient != null); var fields = messageAccessor.Serialize(); - OutgoingWebResponse response = null; + HttpResponseMessage response = null; bool tooLargeForGet = false; if ((message.HttpMethods & HttpDeliveryMethods.GetRequest) == HttpDeliveryMethods.GetRequest) { bool payloadInFragment = false; @@ -849,7 +804,7 @@ namespace DotNetOpenAuth.Messaging { // First try creating a 301 redirect, and fallback to a form POST // if the message is too big. response = this.Create301RedirectResponse(message, fields, payloadInFragment); - tooLargeForGet = response.Headers[HttpResponseHeader.Location].Length > this.MaximumIndirectMessageUrlLength; + tooLargeForGet = response.Headers.Location.PathAndQuery.Length > this.MaximumIndirectMessageUrlLength; } // Make sure that if the message is too large for GET that POST is allowed. @@ -876,14 +831,13 @@ namespace DotNetOpenAuth.Messaging { /// <param name="payloadInFragment">if set to <c>true</c> the redirect will contain the message payload in the #fragment portion of the URL rather than the ?querystring.</param> /// <returns>The encoded HTTP response.</returns> [Pure] - protected virtual OutgoingWebResponse Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields, bool payloadInFragment = false) { + protected virtual HttpResponseMessage Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields, bool payloadInFragment = false) { Requires.NotNull(message, "message"); Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient); Requires.NotNull(fields, "fields"); // As part of this redirect, we include an HTML body in order to get passed some proxy filters // such as WebSense. - WebHeaderCollection headers = new WebHeaderCollection(); UriBuilder builder = new UriBuilder(message.Recipient); if (payloadInFragment) { builder.AppendFragmentArgs(fields); @@ -891,16 +845,14 @@ namespace DotNetOpenAuth.Messaging { builder.AppendQueryArgs(fields); } - headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri); - headers.Add(HttpResponseHeader.ContentType, "text/html; charset=utf-8"); Logger.Http.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri); - OutgoingWebResponse response = new OutgoingWebResponse { - Status = HttpStatusCode.Redirect, - Headers = headers, - Body = string.Format(CultureInfo.InvariantCulture, RedirectResponseBodyFormat, builder.Uri.AbsoluteUri), - OriginalMessage = message + HttpResponseMessage response = new HttpResponseMessage { + StatusCode = HttpStatusCode.Redirect, + Content = new StringContent(string.Format(CultureInfo.InvariantCulture, RedirectResponseBodyFormat, builder.Uri.AbsoluteUri)), }; + response.Headers.Location = builder.Uri; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html; charset=utf-8"); return response; } @@ -912,13 +864,11 @@ namespace DotNetOpenAuth.Messaging { /// <param name="fields">The pre-serialized fields from the message.</param> /// <returns>The encoded HTTP response.</returns> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - protected virtual OutgoingWebResponse CreateFormPostResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields) { + protected virtual HttpResponseMessage CreateFormPostResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields) { Requires.NotNull(message, "message"); Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient); Requires.NotNull(fields, "fields"); - WebHeaderCollection headers = new WebHeaderCollection(); - headers.Add(HttpResponseHeader.ContentType, "text/html"); using (StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture)) { StringBuilder hiddenFields = new StringBuilder(); foreach (var field in fields) { @@ -932,12 +882,11 @@ namespace DotNetOpenAuth.Messaging { HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), hiddenFields); bodyWriter.Flush(); - OutgoingWebResponse response = new OutgoingWebResponse { - Status = HttpStatusCode.OK, - Headers = headers, - Body = bodyWriter.ToString(), - OriginalMessage = message + HttpResponseMessage response = new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(bodyWriter.ToString()), }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html"); return response; } @@ -949,7 +898,7 @@ namespace DotNetOpenAuth.Messaging { /// <param name="response">The response that is anticipated to contain an protocol message.</param> /// <returns>The deserialized message parts, if found. Null otherwise.</returns> /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> - protected abstract IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response); + protected abstract IDictionary<string, string> ReadFromResponseCore(HttpResponseMessage response); /// <summary> /// Prepares an HTTP request that carries a given message. @@ -957,10 +906,10 @@ namespace DotNetOpenAuth.Messaging { /// <param name="request">The message to send.</param> /// <returns>The <see cref="HttpWebRequest"/> prepared to send the request.</returns> /// <remarks> - /// This method must be overridden by a derived class, unless the <see cref="Channel.RequestCore"/> method + /// This method must be overridden by a derived class, unless the <see cref="Channel.RequestCoreAsync"/> method /// is overridden and does not require this method. /// </remarks> - protected virtual HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + protected virtual HttpRequestMessage CreateHttpRequest(IDirectedProtocolMessage request) { Requires.NotNull(request, "request"); Requires.That(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient); throw new NotImplementedException(); @@ -975,7 +924,7 @@ namespace DotNetOpenAuth.Messaging { /// <remarks> /// This method implements spec OAuth V1.0 section 5.3. /// </remarks> - protected abstract OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response); + protected abstract HttpResponseMessage PrepareDirectResponse(IProtocolMessage response); /// <summary> /// Serializes the given message as a JSON string. @@ -1072,7 +1021,7 @@ namespace DotNetOpenAuth.Messaging { /// This method is simply a standard HTTP Get request with the message parts serialized to the query string. /// This method satisfies OAuth 1.0 section 5.2, item #3. /// </remarks> - protected virtual HttpWebRequest InitializeRequestAsGet(IDirectedProtocolMessage requestMessage) { + protected virtual HttpRequestMessage InitializeRequestAsGet(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); Requires.That(requestMessage.Recipient != null, "requestMessage", MessagingStrings.DirectedMessageMissingRecipient); @@ -1081,7 +1030,7 @@ namespace DotNetOpenAuth.Messaging { UriBuilder builder = new UriBuilder(requestMessage.Recipient); MessagingUtilities.AppendQueryArgs(builder, fields); - HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); + var httpRequest = new HttpRequestMessage(HttpMethod.Get, builder.Uri); this.PrepareHttpWebRequest(httpRequest); return httpRequest; @@ -1096,12 +1045,12 @@ namespace DotNetOpenAuth.Messaging { /// This method is simply a standard HTTP HEAD request with the message parts serialized to the query string. /// This method satisfies OAuth 1.0 section 5.2, item #3. /// </remarks> - protected virtual HttpWebRequest InitializeRequestAsHead(IDirectedProtocolMessage requestMessage) { + protected virtual HttpRequestMessage InitializeRequestAsHead(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); Requires.That(requestMessage.Recipient != null, "requestMessage", MessagingStrings.DirectedMessageMissingRecipient); - HttpWebRequest request = this.InitializeRequestAsGet(requestMessage); - request.Method = "HEAD"; + var request = this.InitializeRequestAsGet(requestMessage); + request.Method = HttpMethod.Head; return request; } @@ -1115,27 +1064,31 @@ namespace DotNetOpenAuth.Messaging { /// with the application/x-www-form-urlencoded content type /// This method satisfies OAuth 1.0 section 5.2, item #2 and OpenID 2.0 section 4.1.2. /// </remarks> - protected virtual HttpWebRequest InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) { + protected virtual HttpRequestMessage InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); var messageAccessor = this.MessageDescriptions.GetAccessor(requestMessage); var fields = messageAccessor.Serialize(); - var httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestMessage.Recipient); this.PrepareHttpWebRequest(httpRequest); - httpRequest.CachePolicy = this.CachePolicy; - httpRequest.Method = "POST"; var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData; if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) { - var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + var content = new MultipartFormDataContent(); + foreach (var part in requestMessageWithBinaryData.BinaryData) { + content.Add(part); + } // When sending multi-part, all data gets send as multi-part -- even the non-binary data. - multiPartFields.AddRange(fields.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); - this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); + foreach (var field in fields) { + content.Add(new StringContent(field.Value), field.Key); + } + + httpRequest.Content = content; } else { ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); - this.SendParametersInEntity(httpRequest, fields); + httpRequest.Content = new FormUrlEncodedContent(fields); } return httpRequest; @@ -1149,11 +1102,11 @@ namespace DotNetOpenAuth.Messaging { /// <remarks> /// This method is simply a standard HTTP PUT request with the message parts serialized to the query string. /// </remarks> - protected virtual HttpWebRequest InitializeRequestAsPut(IDirectedProtocolMessage requestMessage) { + protected virtual HttpRequestMessage InitializeRequestAsPut(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); - HttpWebRequest request = this.InitializeRequestAsGet(requestMessage); - request.Method = "PUT"; + var request = this.InitializeRequestAsGet(requestMessage); + request.Method = HttpMethod.Put; return request; } @@ -1165,58 +1118,15 @@ namespace DotNetOpenAuth.Messaging { /// <remarks> /// This method is simply a standard HTTP DELETE request with the message parts serialized to the query string. /// </remarks> - protected virtual HttpWebRequest InitializeRequestAsDelete(IDirectedProtocolMessage requestMessage) { + protected virtual HttpRequestMessage InitializeRequestAsDelete(IDirectedProtocolMessage requestMessage) { Requires.NotNull(requestMessage, "requestMessage"); - HttpWebRequest request = this.InitializeRequestAsGet(requestMessage); - request.Method = "DELETE"; + var request = this.InitializeRequestAsGet(requestMessage); + request.Method = HttpMethod.Delete; return request; } /// <summary> - /// Sends the given parameters in the entity stream of an HTTP request. - /// </summary> - /// <param name="httpRequest">The HTTP request.</param> - /// <param name="fields">The parameters to send.</param> - /// <remarks> - /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes - /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>. - /// </remarks> - protected void SendParametersInEntity(HttpWebRequest httpRequest, IDictionary<string, string> fields) { - Requires.NotNull(httpRequest, "httpRequest"); - Requires.NotNull(fields, "fields"); - - string requestBody = MessagingUtilities.CreateQueryString(fields); - byte[] requestBytes = PostEntityEncoding.GetBytes(requestBody); - httpRequest.ContentType = HttpFormUrlEncodedContentType.ToString(); - httpRequest.ContentLength = requestBytes.Length; - Stream requestStream = this.WebRequestHandler.GetRequestStream(httpRequest); - try { - requestStream.Write(requestBytes, 0, requestBytes.Length); - } finally { - // We need to be sure to close the request stream... - // unless it is a MemoryStream, which is a clue that we're in - // a mock stream situation and closing it would preclude reading it later. - if (!(requestStream is MemoryStream)) { - requestStream.Dispose(); - } - } - } - - /// <summary> - /// Sends the given parameters in the entity stream of an HTTP request in multi-part format. - /// </summary> - /// <param name="httpRequest">The HTTP request.</param> - /// <param name="fields">The parameters to send.</param> - /// <remarks> - /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes - /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>. - /// </remarks> - protected void SendParametersInEntityAsMultipart(HttpWebRequest httpRequest, IEnumerable<MultipartPostPart> fields) { - httpRequest.PostMultipartNoGetResponse(this.WebRequestHandler, fields); - } - - /// <summary> /// Verifies the integrity and applicability of an incoming message. /// </summary> /// <param name="message">The message just received.</param> @@ -1301,7 +1211,7 @@ namespace DotNetOpenAuth.Messaging { /// Performs additional processing on an outgoing web request before it is sent to the remote server. /// </summary> /// <param name="request">The request.</param> - protected virtual void PrepareHttpWebRequest(HttpWebRequest request) { + protected virtual void PrepareHttpWebRequest(HttpRequestMessage request) { Requires.NotNull(request, "request"); } diff --git a/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs deleted file mode 100644 index f3975b3..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs +++ /dev/null @@ -1,105 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="IDirectWebRequestHandler.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.IO; - using System.Net; - using DotNetOpenAuth.Messaging; - using Validation; - - /// <summary> - /// A contract for <see cref="HttpWebRequest"/> handling. - /// </summary> - /// <remarks> - /// Implementations of this interface must be thread safe. - /// </remarks> - public interface IDirectWebRequestHandler { - /// <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] - bool CanSupport(DirectWebRequestOptions options); - - /// <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 stream 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. - /// Callers <i>must</i> close and dispose of the request stream when they are done - /// writing to it to avoid taking up the connection too long and causing long waits on - /// subsequent requests.</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> - Stream GetRequestStream(HttpWebRequest request); - - /// <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 stream 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. - /// Callers <i>must</i> close and dispose of the request stream when they are done - /// writing to it to avoid taking up the connection too long and causing long waits on - /// subsequent requests.</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> - Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options); - - /// <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> - IncomingWebResponse GetResponse(HttpWebRequest 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> - /// <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> - IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options); - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs index 2992678..074894c 100644 --- a/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs +++ b/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Messaging { using System; using System.Collections.Generic; using System.Linq; + using System.Net.Http; using System.Text; /// <summary> @@ -19,7 +20,7 @@ namespace DotNetOpenAuth.Messaging { /// Gets the parts of the message that carry binary data. /// </summary> /// <value>A list of parts. Never null.</value> - IList<MultipartPostPart> BinaryData { get; } + IList<HttpContent> BinaryData { get; } /// <summary> /// Gets a value indicating whether this message should be sent as multi-part POST. diff --git a/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs deleted file mode 100644 index abb01a1..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs +++ /dev/null @@ -1,189 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="IncomingWebResponse.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.IO; - using System.Net; - using System.Net.Mime; - using System.Text; - using Validation; - - /// <summary> - /// Details on the incoming response from a direct web request to a remote party. - /// </summary> - public abstract class IncomingWebResponse : IDisposable { - /// <summary> - /// The encoding to use in reading a response that does not declare its own content encoding. - /// </summary> - private const string DefaultContentEncoding = "ISO-8859-1"; - - /// <summary> - /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class. - /// </summary> - protected internal IncomingWebResponse() { - this.Status = HttpStatusCode.OK; - this.Headers = new WebHeaderCollection(); - } - - /// <summary> - /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class. - /// </summary> - /// <param name="requestUri">The original request URI.</param> - /// <param name="response">The response to initialize from. The network stream is used by this class directly.</param> - protected IncomingWebResponse(Uri requestUri, HttpWebResponse response) { - Requires.NotNull(requestUri, "requestUri"); - Requires.NotNull(response, "response"); - - this.RequestUri = requestUri; - if (!string.IsNullOrEmpty(response.ContentType)) { - try { - this.ContentType = new ContentType(response.ContentType); - } catch (FormatException) { - Logger.Messaging.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", response.ResponseUri.AbsoluteUri, response.ContentType); - } - } - this.ContentEncoding = string.IsNullOrEmpty(response.ContentEncoding) ? DefaultContentEncoding : response.ContentEncoding; - this.FinalUri = response.ResponseUri; - this.Status = response.StatusCode; - this.Headers = response.Headers; - } - - /// <summary> - /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class. - /// </summary> - /// <param name="requestUri">The request URI.</param> - /// <param name="responseUri">The final URI to respond to the request.</param> - /// <param name="headers">The headers.</param> - /// <param name="statusCode">The status code.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="contentEncoding">The content encoding.</param> - protected IncomingWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding) { - Requires.NotNull(requestUri, "requestUri"); - - this.RequestUri = requestUri; - this.Status = statusCode; - if (!string.IsNullOrEmpty(contentType)) { - try { - this.ContentType = new ContentType(contentType); - } catch (FormatException) { - Logger.Messaging.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", responseUri.AbsoluteUri, contentType); - } - } - this.ContentEncoding = string.IsNullOrEmpty(contentEncoding) ? DefaultContentEncoding : contentEncoding; - this.Headers = headers; - this.FinalUri = responseUri; - } - - /// <summary> - /// Gets the type of the content. - /// </summary> - public ContentType ContentType { get; private set; } - - /// <summary> - /// Gets the content encoding. - /// </summary> - public string ContentEncoding { get; private set; } - - /// <summary> - /// Gets the URI of the initial request. - /// </summary> - public Uri RequestUri { get; private set; } - - /// <summary> - /// Gets the URI that finally responded to the request. - /// </summary> - /// <remarks> - /// This can be different from the <see cref="RequestUri"/> in cases of - /// redirection during the request. - /// </remarks> - public Uri FinalUri { get; internal set; } - - /// <summary> - /// Gets the headers that must be included in the response to the user agent. - /// </summary> - /// <remarks> - /// The headers in this collection are not meant to be a comprehensive list - /// of exactly what should be sent, but are meant to augment whatever headers - /// are generally included in a typical response. - /// </remarks> - public WebHeaderCollection Headers { get; internal set; } - - /// <summary> - /// Gets the HTTP status code to use in the HTTP response. - /// </summary> - public HttpStatusCode Status { get; internal set; } - - /// <summary> - /// Gets the body of the HTTP response. - /// </summary> - public abstract Stream ResponseStream { get; } - - /// <summary> - /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. - /// </summary> - /// <returns> - /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. - /// </returns> - public override string ToString() { - StringBuilder sb = new StringBuilder(); - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "RequestUri = {0}", this.RequestUri)); - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ResponseUri = {0}", this.FinalUri)); - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "StatusCode = {0}", this.Status)); - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentType = {0}", this.ContentType)); - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentEncoding = {0}", this.ContentEncoding)); - sb.AppendLine("Headers:"); - foreach (string header in this.Headers) { - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "\t{0}: {1}", header, this.Headers[header])); - } - - return sb.ToString(); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Creates a text reader for the response stream. - /// </summary> - /// <returns>The text reader, initialized for the proper encoding.</returns> - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")] - public abstract StreamReader GetResponseReader(); - - /// <summary> - /// Gets an offline snapshot version of this instance. - /// </summary> - /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> - /// <returns>A snapshot version of this instance.</returns> - /// <remarks> - /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot - /// will automatically close and dispose of the underlying response stream. - /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will - /// be the self same instance. - /// </remarks> - internal abstract CachedDirectWebResponse GetSnapshot(int maximumBytesToCache); - - /// <summary> - /// Releases unmanaged and - optionally - managed resources - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) { - if (disposing) { - Stream responseStream = this.ResponseStream; - if (responseStream != null) { - responseStream.Dispose(); - } - } - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index 221a29c..3da62e9 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -156,37 +156,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. - /// </summary> - /// <param name="response">The response to send to the user agent.</param> - /// <returns>The <see cref="ActionResult"/> instance to be returned by the Controller's action method.</returns> - public static ActionResult AsActionResult(this OutgoingWebResponse response) { - Requires.NotNull(response, "response"); - return new OutgoingWebResponseActionResult(response); - } - - /// <summary> - /// Transforms an OutgoingWebResponse to a Web API-friendly HttpResponseMessage. - /// </summary> - /// <param name="outgoingResponse">The response to send to the user agent.</param> - /// <returns>The <see cref="HttpResponseMessage"/> instance to be returned by the Web API method.</returns> - public static HttpResponseMessage AsHttpResponseMessage(this OutgoingWebResponse outgoingResponse) { - HttpResponseMessage response = new HttpResponseMessage(outgoingResponse.Status); - if (outgoingResponse.ResponseStream != null) { - response.Content = new StreamContent(outgoingResponse.ResponseStream); - } - - var responseHeaders = outgoingResponse.Headers; - foreach (var header in responseHeaders.AllKeys) { - if (!response.Headers.TryAddWithoutValidation(header, responseHeaders[header])) { - response.Content.Headers.TryAddWithoutValidation(header, responseHeaders[header]); - } - } - - return response; - } - - /// <summary> /// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any. /// Cookieless session directory (if applicable) is also included. /// </summary> @@ -223,22 +192,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Sends a multipart HTTP POST request (useful for posting files). - /// </summary> - /// <param name="request">The HTTP request.</param> - /// <param name="requestHandler">The request handler.</param> - /// <param name="parts">The parts to include in the POST entity.</param> - /// <returns>The HTTP response.</returns> - public static IncomingWebResponse PostMultipart(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) { - Requires.NotNull(request, "request"); - Requires.NotNull(requestHandler, "requestHandler"); - Requires.NotNull(parts, "parts"); - - PostMultipartNoGetResponse(request, requestHandler, parts); - return requestHandler.GetResponse(request); - } - - /// <summary> /// Assembles a message comprised of the message on a given exception and all inner exceptions. /// </summary> /// <param name="exception">The exception.</param> @@ -496,59 +449,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it. - /// </summary> - /// <param name="request">The HTTP request.</param> - /// <param name="requestHandler">The request handler.</param> - /// <param name="parts">The parts to include in the POST entity.</param> - internal static void PostMultipartNoGetResponse(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) { - Requires.NotNull(request, "request"); - Requires.NotNull(requestHandler, "requestHandler"); - Requires.NotNull(parts, "parts"); - - Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart"); - parts = parts.CacheGeneratedResults(); - string boundary = Guid.NewGuid().ToString(); - string initialPartLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "--{0}\r\n", boundary); - string partLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}\r\n", boundary); - string finalTrailingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}--\r\n", boundary); - var contentType = new ContentType("multipart/form-data") { - Boundary = boundary, - CharSet = Channel.PostEntityEncoding.WebName, - }; - - request.Method = "POST"; - request.ContentType = contentType.ToString(); - long contentLength = parts.Sum(p => partLeadingBoundary.Length + p.Length) + finalTrailingBoundary.Length; - if (parts.Any()) { - contentLength -= 2; // the initial part leading boundary has no leading \r\n - } - request.ContentLength = contentLength; - - var requestStream = requestHandler.GetRequestStream(request); - try { - StreamWriter writer = new StreamWriter(requestStream, Channel.PostEntityEncoding); - bool firstPart = true; - foreach (var part in parts) { - writer.Write(firstPart ? initialPartLeadingBoundary : partLeadingBoundary); - firstPart = false; - part.Serialize(writer); - part.Dispose(); - } - - writer.Write(finalTrailingBoundary); - writer.Flush(); - } finally { - // We need to be sure to close the request stream... - // unless it is a MemoryStream, which is a clue that we're in - // a mock stream situation and closing it would preclude reading it later. - if (!(requestStream is MemoryStream)) { - requestStream.Dispose(); - } - } - } - - /// <summary> /// Assembles the content of the HTTP Authorization or WWW-Authenticate header. /// </summary> /// <param name="scheme">The scheme.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs b/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs deleted file mode 100644 index b4a0968..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs +++ /dev/null @@ -1,223 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="MultipartPostPart.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Net; - using System.Text; - using Validation; - - /// <summary> - /// Represents a single part in a HTTP multipart POST request. - /// </summary> - public class MultipartPostPart : IDisposable { - /// <summary> - /// The "Content-Disposition" string. - /// </summary> - private const string ContentDispositionHeader = "Content-Disposition"; - - /// <summary> - /// The two-character \r\n newline character sequence to use. - /// </summary> - private const string NewLine = "\r\n"; - - /// <summary> - /// Initializes a new instance of the <see cref="MultipartPostPart"/> class. - /// </summary> - /// <param name="contentDisposition">The content disposition of the part.</param> - public MultipartPostPart(string contentDisposition) { - Requires.NotNullOrEmpty(contentDisposition, "contentDisposition"); - - this.ContentDisposition = contentDisposition; - this.ContentAttributes = new Dictionary<string, string>(); - this.PartHeaders = new WebHeaderCollection(); - } - - /// <summary> - /// Gets or sets the content disposition. - /// </summary> - /// <value>The content disposition.</value> - public string ContentDisposition { get; set; } - - /// <summary> - /// Gets the key=value attributes that appear on the same line as the Content-Disposition. - /// </summary> - /// <value>The content attributes.</value> - public IDictionary<string, string> ContentAttributes { get; private set; } - - /// <summary> - /// Gets the headers that appear on subsequent lines after the Content-Disposition. - /// </summary> - public WebHeaderCollection PartHeaders { get; private set; } - - /// <summary> - /// Gets or sets the content of the part. - /// </summary> - public Stream Content { get; set; } - - /// <summary> - /// Gets the length of this entire part. - /// </summary> - /// <remarks>Useful for calculating the ContentLength HTTP header to send before actually serializing the content.</remarks> - public long Length { - get { - ErrorUtilities.VerifyOperation(this.Content != null && this.Content.Length >= 0, MessagingStrings.StreamMustHaveKnownLength); - - long length = 0; - length += ContentDispositionHeader.Length; - length += ": ".Length; - length += this.ContentDisposition.Length; - foreach (var pair in this.ContentAttributes) { - length += "; ".Length + pair.Key.Length + "=\"".Length + pair.Value.Length + "\"".Length; - } - - length += NewLine.Length; - foreach (string headerName in this.PartHeaders) { - length += headerName.Length; - length += ": ".Length; - length += this.PartHeaders[headerName].Length; - length += NewLine.Length; - } - - length += NewLine.Length; - length += this.Content.Length; - - return length; - } - } - - /// <summary> - /// Creates a part that represents a simple form field. - /// </summary> - /// <param name="name">The name of the form field.</param> - /// <param name="value">The value.</param> - /// <returns>The constructed part.</returns> - public static MultipartPostPart CreateFormPart(string name, string value) { - Requires.NotNullOrEmpty(name, "name"); - Requires.NotNull(value, "value"); - - var part = new MultipartPostPart("form-data"); - try { - part.ContentAttributes["name"] = name; - part.Content = new MemoryStream(Encoding.UTF8.GetBytes(value)); - return part; - } catch { - part.Dispose(); - throw; - } - } - - /// <summary> - /// Creates a part that represents a file attachment. - /// </summary> - /// <param name="name">The name of the form field.</param> - /// <param name="filePath">The path to the file to send.</param> - /// <param name="contentType">Type of the content in HTTP Content-Type format.</param> - /// <returns>The constructed part.</returns> - public static MultipartPostPart CreateFormFilePart(string name, string filePath, string contentType) { - Requires.NotNullOrEmpty(name, "name"); - Requires.NotNullOrEmpty(filePath, "filePath"); - Requires.NotNullOrEmpty(contentType, "contentType"); - - string fileName = Path.GetFileName(filePath); - var fileStream = File.OpenRead(filePath); - try { - return CreateFormFilePart(name, fileName, contentType, fileStream); - } catch { - fileStream.Dispose(); - throw; - } - } - - /// <summary> - /// Creates a part that represents a file attachment. - /// </summary> - /// <param name="name">The name of the form field.</param> - /// <param name="fileName">Name of the file as the server should see it.</param> - /// <param name="contentType">Type of the content in HTTP Content-Type format.</param> - /// <param name="content">The content of the file.</param> - /// <returns>The constructed part.</returns> - public static MultipartPostPart CreateFormFilePart(string name, string fileName, string contentType, Stream content) { - Requires.NotNullOrEmpty(name, "name"); - Requires.NotNullOrEmpty(fileName, "fileName"); - Requires.NotNullOrEmpty(contentType, "contentType"); - Requires.NotNull(content, "content"); - - var part = new MultipartPostPart("file"); - try { - part.ContentAttributes["name"] = name; - part.ContentAttributes["filename"] = fileName; - part.PartHeaders[HttpRequestHeader.ContentType] = contentType; - if (!contentType.StartsWith("text/", StringComparison.Ordinal)) { - part.PartHeaders["Content-Transfer-Encoding"] = "binary"; - } - - part.Content = content; - return part; - } catch { - part.Dispose(); - throw; - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Serializes the part to a stream. - /// </summary> - /// <param name="streamWriter">The stream writer.</param> - internal void Serialize(StreamWriter streamWriter) { - // VERY IMPORTANT: any changes at all made to this must be kept in sync with the - // Length property which calculates exactly how many bytes this method will write. - streamWriter.NewLine = NewLine; - streamWriter.Write("{0}: {1}", ContentDispositionHeader, this.ContentDisposition); - foreach (var pair in this.ContentAttributes) { - streamWriter.Write("; {0}=\"{1}\"", pair.Key, pair.Value); - } - - streamWriter.WriteLine(); - foreach (string headerName in this.PartHeaders) { - streamWriter.WriteLine("{0}: {1}", headerName, this.PartHeaders[headerName]); - } - - streamWriter.WriteLine(); - streamWriter.Flush(); - this.Content.CopyTo(streamWriter.BaseStream); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) { - if (disposing) { - this.Content.Dispose(); - } - } - -#if CONTRACTS_FULL - /// <summary> - /// Verifies conditions that should be true for any valid state of this object. - /// </summary> - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] - [ContractInvariantMethod] - private void Invariant() { - Contract.Invariant(!string.IsNullOrEmpty(this.ContentDisposition)); - Contract.Invariant(this.PartHeaders != null); - Contract.Invariant(this.ContentAttributes != null); - } -#endif - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs deleted file mode 100644 index 754d71d..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs +++ /dev/null @@ -1,115 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="NetworkDirectWebResponse.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Diagnostics; - using System.IO; - using System.Net; - using System.Text; - using Validation; - - /// <summary> - /// A live network HTTP response - /// </summary> - [DebuggerDisplay("{Status} {ContentType.MediaType}")] - internal class NetworkDirectWebResponse : IncomingWebResponse, IDisposable { - /// <summary> - /// The network response object, used to initialize this instance, that still needs - /// to be closed if applicable. - /// </summary> - private HttpWebResponse httpWebResponse; - - /// <summary> - /// The incoming network response stream. - /// </summary> - private Stream responseStream; - - /// <summary> - /// A value indicating whether a stream reader has already been - /// created on this instance. - /// </summary> - private bool streamReadBegun; - - /// <summary> - /// Initializes a new instance of the <see cref="NetworkDirectWebResponse"/> class. - /// </summary> - /// <param name="requestUri">The request URI.</param> - /// <param name="response">The response.</param> - internal NetworkDirectWebResponse(Uri requestUri, HttpWebResponse response) - : base(requestUri, response) { - Requires.NotNull(requestUri, "requestUri"); - Requires.NotNull(response, "response"); - this.httpWebResponse = response; - this.responseStream = response.GetResponseStream(); - } - - /// <summary> - /// Gets the body of the HTTP response. - /// </summary> - public override Stream ResponseStream { - get { return this.responseStream; } - } - - /// <summary> - /// Creates a text reader for the response stream. - /// </summary> - /// <returns>The text reader, initialized for the proper encoding.</returns> - public override StreamReader GetResponseReader() { - this.streamReadBegun = true; - if (this.responseStream == null) { - throw new ObjectDisposedException(GetType().Name); - } - - string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); - } - } - - /// <summary> - /// Gets an offline snapshot version of this instance. - /// </summary> - /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param> - /// <returns>A snapshot version of this instance.</returns> - /// <remarks> - /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot - /// will automatically close and dispose of the underlying response stream. - /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will - /// be the self same instance. - /// </remarks> - internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) { - ErrorUtilities.VerifyOperation(!this.streamReadBegun, "Network stream reading has already begun."); - ErrorUtilities.VerifyOperation(this.httpWebResponse != null, "httpWebResponse != null"); - - this.streamReadBegun = true; - var result = new CachedDirectWebResponse(this.RequestUri, this.httpWebResponse, maximumBytesToCache); - this.Dispose(); - return result; - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected override void Dispose(bool disposing) { - if (disposing) { - if (this.responseStream != null) { - this.responseStream.Dispose(); - this.responseStream = null; - } - if (this.httpWebResponse != null) { - this.httpWebResponse.Close(); - this.httpWebResponse = null; - } - } - - base.Dispose(disposing); - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs deleted file mode 100644 index be7774f..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs +++ /dev/null @@ -1,385 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="OutgoingWebResponse.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.ComponentModel; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Net; - using System.Net.Mime; - using System.ServiceModel.Web; - using System.Text; - using System.Threading; - using System.Web; - using Validation; - - /// <summary> - /// A protocol message (request or response) that passes from this - /// to a remote party via the user agent using a redirect or form - /// POST submission, OR a direct message response. - /// </summary> - /// <remarks> - /// <para>An instance of this type describes the HTTP response that must be sent - /// in response to the current HTTP request.</para> - /// <para>It is important that this response make up the entire HTTP response. - /// A hosting ASPX page should not be allowed to render its normal HTML output - /// after this response is sent. The normal rendered output of an ASPX page - /// can be canceled by calling <see cref="HttpResponse.End"/> after this message - /// is sent on the response stream.</para> - /// </remarks> - public class OutgoingWebResponse { - /// <summary> - /// The encoder to use for serializing the response body. - /// </summary> - private static Encoding bodyStringEncoder = new UTF8Encoding(false); - - /// <summary> - /// Initializes a new instance of the <see cref="OutgoingWebResponse"/> class. - /// </summary> - internal OutgoingWebResponse() { - this.Status = HttpStatusCode.OK; - this.Headers = new WebHeaderCollection(); - this.Cookies = new HttpCookieCollection(); - } - - /// <summary> - /// Initializes a new instance of the <see cref="OutgoingWebResponse"/> class - /// based on the contents of an <see cref="HttpWebResponse"/>. - /// </summary> - /// <param name="response">The <see cref="HttpWebResponse"/> to clone.</param> - /// <param name="maximumBytesToRead">The maximum bytes to read from the response stream.</param> - protected internal OutgoingWebResponse(HttpWebResponse response, int maximumBytesToRead) { - Requires.NotNull(response, "response"); - - this.Status = response.StatusCode; - this.Headers = response.Headers; - this.Cookies = new HttpCookieCollection(); - this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength); - using (Stream responseStream = response.GetResponseStream()) { - // BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here. - this.IsResponseTruncated = responseStream.CopyUpTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead; - this.ResponseStream.Seek(0, SeekOrigin.Begin); - } - } - - /// <summary> - /// Gets the headers that must be included in the response to the user agent. - /// </summary> - /// <remarks> - /// The headers in this collection are not meant to be a comprehensive list - /// of exactly what should be sent, but are meant to augment whatever headers - /// are generally included in a typical response. - /// </remarks> - public WebHeaderCollection Headers { get; internal set; } - - /// <summary> - /// Gets the body of the HTTP response. - /// </summary> - public Stream ResponseStream { get; internal set; } - - /// <summary> - /// Gets a value indicating whether the response stream is incomplete due - /// to a length limitation imposed by the HttpWebRequest or calling method. - /// </summary> - public bool IsResponseTruncated { get; internal set; } - - /// <summary> - /// Gets the cookies collection to add as headers to the HTTP response. - /// </summary> - public HttpCookieCollection Cookies { get; internal set; } - - /// <summary> - /// Gets or sets the body of the response as a string. - /// </summary> - public string Body { - get { return this.ResponseStream != null ? this.GetResponseReader().ReadToEnd() : null; } - set { this.SetResponse(value, null); } - } - - /// <summary> - /// Gets the HTTP status code to use in the HTTP response. - /// </summary> - public HttpStatusCode Status { get; internal set; } - - /// <summary> - /// Gets or sets a reference to the actual protocol message that - /// is being sent via the user agent. - /// </summary> - internal IProtocolMessage OriginalMessage { get; set; } - - /// <summary> - /// Creates a text reader for the response stream. - /// </summary> - /// <returns>The text reader, initialized for the proper encoding.</returns> - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")] - public StreamReader GetResponseReader() { - this.ResponseStream.Seek(0, SeekOrigin.Begin); - string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); - } - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and ends execution on the current page or handler. - /// </summary> - /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> - /// <remarks> - /// Requires a current HttpContext. - /// </remarks> - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual void Send() { - RequiresEx.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); - - this.Send(HttpContext.Current); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and ends execution on the current page or handler. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual void Send(HttpContext context) { - this.Respond(new HttpContextWrapper(context), true); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and ends execution on the current page or handler. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual void Send(HttpContextBase context) { - this.Respond(context, true); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and signals ASP.NET to short-circuit the page execution pipeline - /// now that the response has been completed. - /// Not safe to call from ASP.NET web forms. - /// </summary> - /// <remarks> - /// Requires a current HttpContext. - /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because - /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response. - /// Use the <see cref="Send()"/> method instead for web forms. - /// </remarks> - public virtual void Respond() { - RequiresEx.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); - - this.Respond(HttpContext.Current); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and signals ASP.NET to short-circuit the page execution pipeline - /// now that the response has been completed. - /// Not safe to call from ASP.NET web forms. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <remarks> - /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because - /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response. - /// Use the <see cref="Send()"/> method instead for web forms. - /// </remarks> - public void Respond(HttpContext context) { - Requires.NotNull(context, "context"); - this.Respond(new HttpContextWrapper(context)); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and signals ASP.NET to short-circuit the page execution pipeline - /// now that the response has been completed. - /// Not safe to call from ASP.NET web forms. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <remarks> - /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because - /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response. - /// Use the <see cref="Send()"/> method instead for web forms. - /// </remarks> - public virtual void Respond(HttpContextBase context) { - Requires.NotNull(context, "context"); - - this.Respond(context, false); - } - - /// <summary> - /// Submits this response to a WCF response context. Only available when no response body is included. - /// </summary> - /// <param name="responseContext">The response context to apply the response to.</param> - public virtual void Respond(OutgoingWebResponseContext responseContext) { - Requires.NotNull(responseContext, "responseContext"); - if (this.ResponseStream != null) { - throw new NotSupportedException(Strings.ResponseBodyNotSupported); - } - - responseContext.StatusCode = this.Status; - responseContext.SuppressEntityBody = true; - foreach (string header in this.Headers) { - responseContext.Headers[header] = this.Headers[header]; - } - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent. - /// </summary> - /// <param name="response">The response to set to this message.</param> - public virtual void Send(HttpListenerResponse response) { - Requires.NotNull(response, "response"); - - response.StatusCode = (int)this.Status; - MessagingUtilities.ApplyHeadersToResponse(this.Headers, response); - foreach (HttpCookie httpCookie in this.Cookies) { - var cookie = new Cookie(httpCookie.Name, httpCookie.Value) { - Expires = httpCookie.Expires, - Path = httpCookie.Path, - HttpOnly = httpCookie.HttpOnly, - Secure = httpCookie.Secure, - Domain = httpCookie.Domain, - }; - response.AppendCookie(cookie); - } - - if (this.ResponseStream != null) { - response.ContentLength64 = this.ResponseStream.Length; - this.ResponseStream.CopyTo(response.OutputStream); - } - - response.OutputStream.Close(); - } - - /// <summary> - /// Gets the URI that, when requested with an HTTP GET request, - /// would transmit the message that normally would be transmitted via a user agent redirect. - /// </summary> - /// <param name="channel">The channel to use for encoding.</param> - /// <returns> - /// The URL that would transmit the original message. This URL may exceed the normal 2K limit, - /// and should therefore be broken up manually and POSTed as form fields when it exceeds this length. - /// </returns> - /// <remarks> - /// This is useful for desktop applications that will spawn a user agent to transmit the message - /// rather than cause a redirect. - /// </remarks> - internal Uri GetDirectUriRequest(Channel channel) { - Requires.NotNull(channel, "channel"); - - var message = this.OriginalMessage as IDirectedProtocolMessage; - if (message == null) { - throw new InvalidOperationException(); // this only makes sense for directed messages (indirect responses) - } - - var fields = channel.MessageDescriptions.GetAccessor(message).Serialize(); - UriBuilder builder = new UriBuilder(message.Recipient); - MessagingUtilities.AppendQueryArgs(builder, fields); - return builder.Uri; - } - - /// <summary> - /// Sets the response to some string, encoded as UTF-8. - /// </summary> - /// <param name="body">The string to set the response to.</param> - /// <param name="contentType">Type of the content. May be null.</param> - internal void SetResponse(string body, ContentType contentType) { - if (body == null) { - this.ResponseStream = null; - return; - } - - if (contentType == null) { - contentType = new ContentType("text/html"); - contentType.CharSet = bodyStringEncoder.WebName; - } else if (contentType.CharSet != bodyStringEncoder.WebName) { - // clone the original so we're not tampering with our inputs if it came as a parameter. - contentType = new ContentType(contentType.ToString()); - contentType.CharSet = bodyStringEncoder.WebName; - } - - this.Headers[HttpResponseHeader.ContentType] = contentType.ToString(); - this.ResponseStream = new MemoryStream(); - StreamWriter writer = new StreamWriter(this.ResponseStream, bodyStringEncoder); - writer.Write(body); - writer.Flush(); - this.ResponseStream.Seek(0, SeekOrigin.Begin); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and signals ASP.NET to short-circuit the page execution pipeline - /// now that the response has been completed. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <param name="endRequest">If set to <c>false</c>, this method calls - /// <see cref="HttpApplication.CompleteRequest"/> rather than <see cref="HttpResponse.End"/> - /// to avoid a <see cref="ThreadAbortException"/>.</param> - protected internal void Respond(HttpContext context, bool endRequest) { - this.Respond(new HttpContextWrapper(context), endRequest); - } - - /// <summary> - /// Automatically sends the appropriate response to the user agent - /// and signals ASP.NET to short-circuit the page execution pipeline - /// now that the response has been completed. - /// </summary> - /// <param name="context">The context of the HTTP request whose response should be set. - /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <param name="endRequest">If set to <c>false</c>, this method calls - /// <see cref="HttpApplication.CompleteRequest"/> rather than <see cref="HttpResponse.End"/> - /// to avoid a <see cref="ThreadAbortException"/>.</param> - protected internal virtual void Respond(HttpContextBase context, bool endRequest) { - Requires.NotNull(context, "context"); - - context.Response.Clear(); - context.Response.StatusCode = (int)this.Status; - MessagingUtilities.ApplyHeadersToResponse(this.Headers, context.Response); - if (this.ResponseStream != null) { - try { - this.ResponseStream.CopyTo(context.Response.OutputStream); - } catch (HttpException ex) { - if (ex.ErrorCode == -2147467259 && context.Response.Output != null) { - // Test scenarios can generate this, since the stream is being spoofed: - // System.Web.HttpException: OutputStream is not available when a custom TextWriter is used. - context.Response.Output.Write(this.Body); - } else { - throw; - } - } - } - - foreach (string cookieName in this.Cookies) { - var cookie = this.Cookies[cookieName]; - context.Response.AppendCookie(cookie); - } - - if (endRequest) { - // This approach throws an exception in order that - // no more code is executed in the calling page. - // Microsoft no longer recommends this approach. - context.Response.End(); - } else if (context.ApplicationInstance != null) { - // This approach doesn't throw an exception, but - // still tells ASP.NET to short-circuit most of the - // request handling pipeline to speed things up. - context.ApplicationInstance.CompleteRequest(); - } - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs deleted file mode 100644 index bc2f985..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs +++ /dev/null @@ -1,45 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="OutgoingWebResponseActionResult.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Web.Mvc; - using DotNetOpenAuth.Messaging; - using Validation; - - /// <summary> - /// An ASP.NET MVC structure to represent the response to send - /// to the user agent when the controller has finished its work. - /// </summary> - internal class OutgoingWebResponseActionResult : ActionResult { - /// <summary> - /// The outgoing web response to send when the ActionResult is executed. - /// </summary> - private readonly OutgoingWebResponse response; - - /// <summary> - /// Initializes a new instance of the <see cref="OutgoingWebResponseActionResult"/> class. - /// </summary> - /// <param name="response">The response.</param> - internal OutgoingWebResponseActionResult(OutgoingWebResponse response) { - Requires.NotNull(response, "response"); - this.response = response; - } - - /// <summary> - /// Enables processing of the result of an action method by a custom type that inherits from <see cref="T:System.Web.Mvc.ActionResult"/>. - /// </summary> - /// <param name="context">The context in which to set the response.</param> - public override void ExecuteResult(ControllerContext context) { - this.response.Respond(context.HttpContext); - - // MVC likes to muck with our response. For example, when returning contrived 401 Unauthorized responses - // MVC will rewrite our response and turn it into a redirect, which breaks OAuth 2 authorization server token endpoints. - // It turns out we can prevent this unwanted behavior by flushing the response before returning from this method. - context.HttpContext.Response.Flush(); - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs index 4d5c418..3c8839e 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Messaging { using System; using System.Collections.Generic; using System.Linq; + using System.Net.Http; using System.Text; using Validation; @@ -62,7 +63,7 @@ namespace DotNetOpenAuth.Messaging { /// Creates the HTTP response to forward to the client to report the error. /// </summary> /// <returns>The HTTP response.</returns> - public OutgoingWebResponse CreateErrorResponse() { + public HttpResponseMessage CreateErrorResponse() { var response = this.channel.PrepareResponse(this.ErrorResponseMessage); return response; } diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs deleted file mode 100644 index 2383a5b..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs +++ /dev/null @@ -1,261 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="StandardWebRequestHandler.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. 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; - using Validation; - - /// <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 + "/" + Util.AssemblyFileVersion; - - #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 (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 - - /// <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; - } - } - } - } -} diff --git a/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs deleted file mode 100644 index be0182d..0000000 --- a/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs +++ /dev/null @@ -1,477 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="UntrustedWebRequestHandler.cs" company="Outercurve Foundation"> -// Copyright (c) Outercurve Foundation. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Messaging { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Diagnostics.Contracts; - using System.Globalization; - using System.IO; - using System.Net; - using System.Net.Cache; - using System.Text.RegularExpressions; - using DotNetOpenAuth.Configuration; - using DotNetOpenAuth.Messaging; - using Validation; - - /// <summary> - /// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote - /// server leaving dangling connections, sending too much data, causing requests against - /// internal servers, etc. - /// </summary> - /// <remarks> - /// Protections include: - /// * Conservative maximum time to receive the complete response. - /// * Only HTTP and HTTPS schemes are permitted. - /// * Internal IP address ranges are not permitted: 127.*.*.*, 1::* - /// * Internal host names are not permitted (periods must be found in the host name) - /// If a particular host would be permitted but is in the blacklist, it is not allowed. - /// If a particular host would not be permitted but is in the whitelist, it is allowed. - /// </remarks> - public class UntrustedWebRequestHandler : IDirectWebRequestHandler { - /// <summary> - /// The set of URI schemes allowed in untrusted web requests. - /// </summary> - private ICollection<string> allowableSchemes = new List<string> { "http", "https" }; - - /// <summary> - /// The collection of blacklisted hosts. - /// </summary> - private ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings); - - /// <summary> - /// The collection of regular expressions used to identify additional blacklisted hosts. - /// </summary> - private ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs); - - /// <summary> - /// The collection of whitelisted hosts. - /// </summary> - private ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings); - - /// <summary> - /// The collection of regular expressions used to identify additional whitelisted hosts. - /// </summary> - private ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs); - - /// <summary> - /// The maximum redirections to follow in the course of a single request. - /// </summary> - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int maximumRedirections = Configuration.MaximumRedirections; - - /// <summary> - /// The maximum number of bytes to read from the response of an untrusted server. - /// </summary> - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private int maximumBytesToRead = Configuration.MaximumBytesToRead; - - /// <summary> - /// The handler that will actually send the HTTP request and collect - /// the response once the untrusted server gates have been satisfied. - /// </summary> - private IDirectWebRequestHandler chainedWebRequestHandler; - - /// <summary> - /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class. - /// </summary> - public UntrustedWebRequestHandler() - : this(new StandardWebRequestHandler()) { - } - - /// <summary> - /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class. - /// </summary> - /// <param name="chainedWebRequestHandler">The chained web request handler.</param> - public UntrustedWebRequestHandler(IDirectWebRequestHandler chainedWebRequestHandler) { - Requires.NotNull(chainedWebRequestHandler, "chainedWebRequestHandler"); - - this.chainedWebRequestHandler = chainedWebRequestHandler; - if (Debugger.IsAttached) { - // Since a debugger is attached, requests may be MUCH slower, - // so give ourselves huge timeouts. - this.ReadWriteTimeout = TimeSpan.FromHours(1); - this.Timeout = TimeSpan.FromHours(1); - } else { - this.ReadWriteTimeout = Configuration.ReadWriteTimeout; - this.Timeout = Configuration.Timeout; - } - } - - /// <summary> - /// Gets or sets the default maximum bytes to read in any given HTTP request. - /// </summary> - /// <value>Default is 1MB. Cannot be less than 2KB.</value> - public int MaximumBytesToRead { - get { - return this.maximumBytesToRead; - } - - set { - Requires.Range(value >= 2048, "value"); - this.maximumBytesToRead = value; - } - } - - /// <summary> - /// Gets or sets the total number of redirections to allow on any one request. - /// Default is 10. - /// </summary> - public int MaximumRedirections { - get { - return this.maximumRedirections; - } - - set { - Requires.Range(value >= 0, "value"); - this.maximumRedirections = value; - } - } - - /// <summary> - /// Gets or sets the time allowed to wait for single read or write operation to complete. - /// Default is 500 milliseconds. - /// </summary> - public TimeSpan ReadWriteTimeout { get; set; } - - /// <summary> - /// Gets or sets the time allowed for an entire HTTP request. - /// Default is 5 seconds. - /// </summary> - public TimeSpan Timeout { get; set; } - - /// <summary> - /// Gets a collection of host name literals that should be allowed even if they don't - /// pass standard security checks. - /// </summary> - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")] - public ICollection<string> WhitelistHosts { get { return this.whitelistHosts; } } - - /// <summary> - /// Gets a collection of host name regular expressions that indicate hosts that should - /// be allowed even though they don't pass standard security checks. - /// </summary> - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")] - public ICollection<Regex> WhitelistHostsRegex { get { return this.whitelistHostsRegex; } } - - /// <summary> - /// Gets a collection of host name literals that should be rejected even if they - /// pass standard security checks. - /// </summary> - public ICollection<string> BlacklistHosts { get { return this.blacklistHosts; } } - - /// <summary> - /// Gets a collection of host name regular expressions that indicate hosts that should - /// be rejected even if they pass standard security checks. - /// </summary> - public ICollection<Regex> BlacklistHostsRegex { get { return this.blacklistHostsRegex; } } - - /// <summary> - /// Gets the configuration for this class that is specified in the host's .config file. - /// </summary> - private static UntrustedWebRequestElement Configuration { - get { return DotNetOpenAuthSection.Messaging.UntrustedWebRequest; } - } - - #region IDirectWebRequestHandler 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) { - // We support whatever our chained handler supports, plus RequireSsl. - return this.chainedWebRequestHandler.CanSupport(options & ~DirectWebRequestOptions.RequireSsl); - } - - /// <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) { - this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0); - - this.PrepareRequest(request, true); - - // Submit the request and get the request stream back. - return this.chainedWebRequestHandler.GetRequestStream(request, options & ~DirectWebRequestOptions.RequireSsl); - } - - /// <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="CachedDirectWebResponse"/> 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> - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] - 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. - this.PrepareRequest(request, false); - - // Since we may require SSL for every redirect, we handle each redirect manually - // in order to detect and fail if any redirect sends us to an HTTP url. - // We COULD allow automatic redirect in the cases where HTTPS is not required, - // but our mock request infrastructure can't do redirects on its own either. - Uri originalRequestUri = request.RequestUri; - int i; - for (i = 0; i < this.MaximumRedirections; i++) { - this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0); - CachedDirectWebResponse response = this.chainedWebRequestHandler.GetResponse(request, options & ~DirectWebRequestOptions.RequireSsl).GetSnapshot(this.MaximumBytesToRead); - if (response.Status == HttpStatusCode.MovedPermanently || - response.Status == HttpStatusCode.Redirect || - response.Status == HttpStatusCode.RedirectMethod || - response.Status == HttpStatusCode.RedirectKeepVerb) { - // We have no copy of the post entity stream to repeat on our manually - // cloned HttpWebRequest, so we have to bail. - ErrorUtilities.VerifyProtocol(request.Method != "POST", MessagingStrings.UntrustedRedirectsOnPOSTNotSupported); - Uri redirectUri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]); - request = request.Clone(redirectUri); - } else { - if (response.FinalUri != request.RequestUri) { - // Since we don't automatically follow redirects, there's only one scenario where this - // can happen: when the server sends a (non-redirecting) Content-Location header in the response. - // It's imperative that we do not trust that header though, so coerce the FinalUri to be - // what we just requested. - Logger.Http.WarnFormat("The response from {0} included an HTTP header indicating it's the same as {1}, but it's not a redirect so we won't trust that.", request.RequestUri, response.FinalUri); - response.FinalUri = request.RequestUri; - } - - return response; - } - } - - throw ErrorUtilities.ThrowProtocol(MessagingStrings.TooManyRedirects, originalRequestUri); - } - - /// <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> - Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request) { - return this.GetRequestStream(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> - /// <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> - IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) { - return this.GetResponse(request, DirectWebRequestOptions.None); - } - - #endregion - - /// <summary> - /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1". - /// </summary> - /// <param name="ip">The ip address to check.</param> - /// <returns> - /// <c>true</c> if this is a loopback IP address; <c>false</c> otherwise. - /// </returns> - private static bool IsIPv6Loopback(IPAddress ip) { - Requires.NotNull(ip, "ip"); - byte[] addressBytes = ip.GetAddressBytes(); - for (int i = 0; i < addressBytes.Length - 1; i++) { - if (addressBytes[i] != 0) { - return false; - } - } - if (addressBytes[addressBytes.Length - 1] != 1) { - return false; - } - return true; - } - - /// <summary> - /// Determines whether the given host name is in a host list or host name regex list. - /// </summary> - /// <param name="host">The host name.</param> - /// <param name="stringList">The list of host names.</param> - /// <param name="regexList">The list of regex patterns of host names.</param> - /// <returns> - /// <c>true</c> if the specified host falls within at least one of the given lists; otherwise, <c>false</c>. - /// </returns> - private static bool IsHostInList(string host, ICollection<string> stringList, ICollection<Regex> regexList) { - Requires.NotNullOrEmpty(host, "host"); - Requires.NotNull(stringList, "stringList"); - Requires.NotNull(regexList, "regexList"); - foreach (string testHost in stringList) { - if (string.Equals(host, testHost, StringComparison.OrdinalIgnoreCase)) { - return true; - } - } - foreach (Regex regex in regexList) { - if (regex.IsMatch(host)) { - return true; - } - } - return false; - } - - /// <summary> - /// Determines whether a given host is whitelisted. - /// </summary> - /// <param name="host">The host name to test.</param> - /// <returns> - /// <c>true</c> if the host is whitelisted; otherwise, <c>false</c>. - /// </returns> - private bool IsHostWhitelisted(string host) { - return IsHostInList(host, this.WhitelistHosts, this.WhitelistHostsRegex); - } - - /// <summary> - /// Determines whether a given host is blacklisted. - /// </summary> - /// <param name="host">The host name to test.</param> - /// <returns> - /// <c>true</c> if the host is blacklisted; otherwise, <c>false</c>. - /// </returns> - private bool IsHostBlacklisted(string host) { - return IsHostInList(host, this.BlacklistHosts, this.BlacklistHostsRegex); - } - - /// <summary> - /// Verify that the request qualifies under our security policies - /// </summary> - /// <param name="requestUri">The request URI.</param> - /// <param name="requireSsl">If set to <c>true</c>, only web requests that can be made entirely over SSL will succeed.</param> - /// <exception cref="ProtocolException">Thrown when the URI is disallowed for security reasons.</exception> - private void EnsureAllowableRequestUri(Uri requestUri, bool requireSsl) { - ErrorUtilities.VerifyProtocol(this.IsUriAllowable(requestUri), MessagingStrings.UnsafeWebRequestDetected, requestUri); - ErrorUtilities.VerifyProtocol(!requireSsl || string.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase), MessagingStrings.InsecureWebRequestWithSslRequired, requestUri); - } - - /// <summary> - /// Determines whether a URI is allowed based on scheme and host name. - /// No requireSSL check is done here - /// </summary> - /// <param name="uri">The URI to test for whether it should be allowed.</param> - /// <returns> - /// <c>true</c> if [is URI allowable] [the specified URI]; otherwise, <c>false</c>. - /// </returns> - private bool IsUriAllowable(Uri uri) { - Requires.NotNull(uri, "uri"); - if (!this.allowableSchemes.Contains(uri.Scheme)) { - Logger.Http.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri); - return false; - } - - // Allow for whitelist or blacklist to override our detection. - Func<string, bool> failsUnlessWhitelisted = (string reason) => { - if (IsHostWhitelisted(uri.DnsSafeHost)) { - return true; - } - Logger.Http.WarnFormat("Rejecting URL {0} because {1}.", uri, reason); - return false; - }; - - // Try to interpret the hostname as an IP address so we can test for internal - // IP address ranges. Note that IP addresses can appear in many forms - // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1 - // So we convert them to a canonical IPAddress instance, and test for all - // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1 - // Note that Uri.IsLoopback is very unreliable, not catching many of these variants. - IPAddress hostIPAddress; - if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress)) { - byte[] addressBytes = hostIPAddress.GetAddressBytes(); - - // The host is actually an IP address. - switch (hostIPAddress.AddressFamily) { - case System.Net.Sockets.AddressFamily.InterNetwork: - if (addressBytes[0] == 127 || addressBytes[0] == 10) { - return failsUnlessWhitelisted("it is a loopback address."); - } - break; - case System.Net.Sockets.AddressFamily.InterNetworkV6: - if (IsIPv6Loopback(hostIPAddress)) { - return failsUnlessWhitelisted("it is a loopback address."); - } - break; - default: - return failsUnlessWhitelisted("it does not use an IPv4 or IPv6 address."); - } - } else { - // The host is given by name. We require names to contain periods to - // help make sure it's not an internal address. - if (!uri.Host.Contains(".")) { - return failsUnlessWhitelisted("it does not contain a period in the host name."); - } - } - if (this.IsHostBlacklisted(uri.DnsSafeHost)) { - Logger.Http.WarnFormat("Rejected URL {0} because it is blacklisted.", uri); - return false; - } - return true; - } - - /// <summary> - /// Prepares the request by setting timeout and redirect policies. - /// </summary> - /// <param name="request">The request to prepare.</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 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 untrusted requests. - request.ReadWriteTimeout = (int)this.ReadWriteTimeout.TotalMilliseconds; - request.Timeout = (int)this.Timeout.TotalMilliseconds; - request.KeepAlive = false; - } - - // If SSL is required throughout, we cannot allow auto redirects because - // it may include a pass through an unprotected HTTP request. - // We have to follow redirects manually. - // It also allows us to ignore HttpWebResponse.FinalUri since that can be affected by - // the Content-Location header and open security holes. - request.AllowAutoRedirect = false; - } - } -} |