diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2013-03-26 11:19:06 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2013-03-26 11:19:06 -0700 |
commit | 3d37ff45cab6838d80b22e6b782a0b9b4c2f4aeb (patch) | |
tree | c15816c3d7f6e74334553f2ff98605ce1c22c538 /src/DotNetOpenAuth.Core/Messaging | |
parent | 5e9014f36b2d53b8e419918675df636540ea24e2 (diff) | |
parent | e6f7409f4caceb7bc2a5b4ddbcb1a4097af340f2 (diff) | |
download | DotNetOpenAuth-3d37ff45cab6838d80b22e6b782a0b9b4c2f4aeb.zip DotNetOpenAuth-3d37ff45cab6838d80b22e6b782a0b9b4c2f4aeb.tar.gz DotNetOpenAuth-3d37ff45cab6838d80b22e6b782a0b9b4c2f4aeb.tar.bz2 |
Move to HttpClient throughout library.
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging')
23 files changed, 856 insertions, 2574 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/HardCodedKeyCryptoKeyStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/HardCodedKeyCryptoKeyStore.cs new file mode 100644 index 0000000..c828616 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/HardCodedKeyCryptoKeyStore.cs @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------- +// <copyright file="HardCodedKeyCryptoKeyStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Bindings { + using System; + using System.Collections.Generic; + using Validation; + + /// <summary> + /// A trivial implementation of <see cref="ICryptoKeyStore"/> that has only one fixed key. + /// This is meant for simple, low-security applications. Greater security requires an + /// implementation of <see cref="ICryptoKeyStore"/> that actually stores and retrieves + /// keys from a persistent store. + /// </summary> + public class HardCodedKeyCryptoKeyStore : ICryptoKeyStore { + /// <summary> + /// The handle to report for the hard-coded key. + /// </summary> + private const string HardCodedKeyHandle = "fxd"; + + /// <summary> + /// The one crypto key singleton instance. + /// </summary> + private readonly CryptoKey OneCryptoKey; + + /// <summary> + /// Initializes a new instance of the <see cref="HardCodedKeyCryptoKeyStore"/> class. + /// </summary> + /// <param name="secretAsBase64">The 256-bit secret as a base64 encoded string.</param> + public HardCodedKeyCryptoKeyStore(string secretAsBase64) + : this(Convert.FromBase64String(Requires.NotNull(secretAsBase64, "secretAsBase64"))) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="HardCodedKeyCryptoKeyStore"/> class. + /// </summary> + /// <param name="secret">The 256-bit secret.</param> + public HardCodedKeyCryptoKeyStore(byte[] secret) { + Requires.NotNull(secret, "secret"); + this.OneCryptoKey = new CryptoKey(secret, DateTime.MaxValue.AddDays(-2).ToUniversalTime()); + } + + #region ICryptoKeyStore Members + + /// <summary> + /// Gets the key in a given bucket and handle. + /// </summary> + /// <param name="bucket">The bucket name. Case sensitive.</param> + /// <param name="handle">The key handle. Case sensitive.</param> + /// <returns> + /// The cryptographic key, or <c>null</c> if no matching key was found. + /// </returns> + public CryptoKey GetKey(string bucket, string handle) { + if (handle == HardCodedKeyHandle) { + return OneCryptoKey; + } + + return null; + } + + /// <summary> + /// Gets a sequence of existing keys within a given bucket. + /// </summary> + /// <param name="bucket">The bucket name. Case sensitive.</param> + /// <returns> + /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc" />. + /// </returns> + public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) { + return new[] { new KeyValuePair<string, CryptoKey>(HardCodedKeyHandle, OneCryptoKey) }; + } + + /// <summary> + /// Stores a cryptographic key. + /// </summary> + /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param> + /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param> + /// <param name="key">The key to store.</param> + /// <exception cref="System.NotSupportedException"></exception> + public void StoreKey(string bucket, string handle, CryptoKey key) { + throw new NotSupportedException(); + } + + /// <summary> + /// Removes the key. + /// </summary> + /// <param name="bucket">The bucket name. Case sensitive.</param> + /// <param name="handle">The key handle. Case sensitive.</param> + /// <exception cref="System.NotSupportedException"></exception> + public void RemoveKey(string bucket, string handle) { + throw new NotSupportedException(); + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs index 7ab78db..f19d4bd 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs @@ -6,6 +6,8 @@ namespace DotNetOpenAuth.Messaging.Bindings { using System; + using System.Threading; + using System.Threading.Tasks; using DotNetOpenAuth.Configuration; /// <summary> @@ -14,6 +16,16 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// </summary> internal class StandardExpirationBindingElement : IChannelBindingElement { /// <summary> + /// A reusable pre-completed task that may be returned multiple times to reduce GC pressure. + /// </summary> + private static readonly Task<MessageProtections?> NullTask = Task.FromResult<MessageProtections?>(null); + + /// <summary> + /// A reusable pre-completed task that may be returned multiple times to reduce GC pressure. + /// </summary> + private static readonly Task<MessageProtections?> CompletedExpirationTask = Task.FromResult<MessageProtections?>(MessageProtections.Expiration); + + /// <summary> /// Initializes a new instance of the <see cref="StandardExpirationBindingElement"/> class. /// </summary> internal StandardExpirationBindingElement() { @@ -51,24 +63,30 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// Sets the timestamp on an outgoing message. /// </summary> /// <param name="message">The outgoing message.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. /// </returns> - public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection" /> properties where applicable. + /// </remarks> + public Task<MessageProtections?> ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage; if (expiringMessage != null) { expiringMessage.UtcCreationDate = DateTime.UtcNow; - return MessageProtections.Expiration; + return CompletedExpirationTask; } - return null; + return NullTask; } /// <summary> /// Reads the timestamp on a message and throws an exception if the message is too old. /// </summary> /// <param name="message">The incoming message.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. @@ -78,7 +96,7 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// Thrown when the binding element rules indicate that this message is invalid and should /// NOT be processed. /// </exception> - public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + public Task<MessageProtections?> ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage; if (expiringMessage != null) { // Yes the UtcCreationDate is supposed to always be in UTC already, @@ -96,10 +114,10 @@ namespace DotNetOpenAuth.Messaging.Bindings { MessagingStrings.MessageTimestampInFuture, creationDate); - return MessageProtections.Expiration; + return CompletedExpirationTask; } - return null; + return NullTask; } #endregion diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs index 45bccdf..65c7882 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs @@ -7,6 +7,8 @@ namespace DotNetOpenAuth.Messaging.Bindings { using System; using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; using Validation; /// <summary> @@ -14,6 +16,16 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// </summary> internal class StandardReplayProtectionBindingElement : IChannelBindingElement { /// <summary> + /// A reusable, precompleted task that can be returned many times to reduce GC pressure. + /// </summary> + private static readonly Task<MessageProtections?> NullTask = Task.FromResult<MessageProtections?>(null); + + /// <summary> + /// A reusable, precompleted task that can be returned many times to reduce GC pressure. + /// </summary> + private static readonly Task<MessageProtections?> CompletedReplayProtectionTask = Task.FromResult<MessageProtections?>(MessageProtections.ReplayProtection); + + /// <summary> /// These are the characters that may be chosen from when forming a random nonce. /// </summary> private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -96,30 +108,36 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// Applies a nonce to the message. /// </summary> /// <param name="message">The message to apply replay protection to.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. /// </returns> - public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + public Task<MessageProtections?> ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage; if (nonceMessage != null) { nonceMessage.Nonce = this.GenerateUniqueFragment(); - return MessageProtections.ReplayProtection; + return CompletedReplayProtectionTask; } - return null; + return NullTask; } /// <summary> /// Verifies that the nonce in an incoming message has not been seen before. /// </summary> /// <param name="message">The incoming message.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. /// </returns> /// <exception cref="ReplayedMessageException">Thrown when the nonce check revealed a replayed message.</exception> - public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection" /> properties where applicable. + /// </remarks> + public Task<MessageProtections?> ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage; if (nonceMessage != null && nonceMessage.Nonce != null) { ErrorUtilities.VerifyProtocol(nonceMessage.Nonce.Length > 0 || this.AllowZeroLengthNonce, MessagingStrings.InvalidNonceReceived); @@ -129,10 +147,10 @@ namespace DotNetOpenAuth.Messaging.Bindings { throw new ReplayedMessageException(message); } - return MessageProtections.ReplayProtection; + return CompletedReplayProtectionTask; } - return null; + return NullTask; } #endregion 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..9414166 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -17,10 +17,14 @@ 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.Net.Sockets; 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; @@ -136,31 +140,25 @@ namespace DotNetOpenAuth.Messaging { private IMessageFactory messageTypeProvider; /// <summary> - /// Backing store for the <see cref="CachePolicy"/> property. - /// </summary> - private RequestCachePolicy cachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore); - - /// <summary> /// Backing field for the <see cref="MaximumIndirectMessageUrlLength"/> property. /// </summary> private int maximumIndirectMessageUrlLength = Configuration.DotNetOpenAuthSection.Messaging.MaximumIndirectMessageUrlLength; /// <summary> - /// Initializes a new instance of the <see cref="Channel"/> class. + /// Initializes a new instance of the <see cref="Channel" /> class. /// </summary> - /// <param name="messageTypeProvider"> - /// A class prepared to analyze incoming messages and indicate what concrete - /// message types can deserialize from it. - /// </param> - /// <param name="bindingElements"> - /// 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="messageTypeProvider">A class prepared to analyze incoming messages and indicate what concrete + /// message types can deserialize from it.</param> + /// <param name="bindingElements">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> + /// <param name="hostFactories">The host factories.</param> + protected Channel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements, IHostFactories hostFactories) { Requires.NotNull(messageTypeProvider, "messageTypeProvider"); + Requires.NotNull(bindingElements, "bindingElements"); + Requires.NotNull(hostFactories, "hostFactories"); this.messageTypeProvider = messageTypeProvider; - this.WebRequestHandler = new StandardWebRequestHandler(); + this.HostFactories = hostFactories; this.XmlDictionaryReaderQuotas = DefaultUntrustedXmlDictionaryReaderQuotas; this.outgoingBindingElements = new List<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements)); @@ -178,14 +176,9 @@ namespace DotNetOpenAuth.Messaging { internal event EventHandler<ChannelEventArgs> Sending; /// <summary> - /// Gets or sets an instance to a <see cref="IDirectWebRequestHandler"/> that will be used when - /// submitting HTTP requests and waiting for responses. + /// Gets the host factories instance to use. /// </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 IHostFactories HostFactories { get; private set; } /// <summary> /// Gets or sets the maximum allowable size for a 301 Redirect response before we send @@ -272,75 +265,25 @@ 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> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> - public OutgoingWebResponse PrepareResponse(IProtocolMessage message) { + public async Task<HttpResponseMessage> PrepareResponseAsync(IProtocolMessage message, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(message, "message"); - this.ProcessOutgoingMessage(message); + await this.ProcessOutgoingMessageAsync(message, cancellationToken); 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 +312,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; } @@ -378,114 +326,74 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Gets the protocol message embedded in the given HTTP request, if present. /// </summary> - /// <returns>The deserialized message, if one is found. Null otherwise.</returns> - /// <remarks> - /// Requires an HttpContext.Current context. - /// </remarks> - /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception> - public IDirectedProtocolMessage ReadFromRequest() { - return this.ReadFromRequest(this.GetRequestFromContext()); - } - - /// <summary> - /// Gets the protocol message embedded in the given HTTP request, if present. - /// </summary> - /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam> - /// <param name="request">The deserialized message, if one is found. Null otherwise.</param> - /// <returns>True if the expected message was recognized and deserialized. False otherwise.</returns> - /// <remarks> - /// Requires an HttpContext.Current context. - /// </remarks> - /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception> - /// <exception cref="ProtocolException">Thrown when a request message of an unexpected type is received.</exception> - public bool TryReadFromRequest<TRequest>(out TRequest request) - where TRequest : class, IProtocolMessage { - return TryReadFromRequest<TRequest>(this.GetRequestFromContext(), out request); - } - - /// <summary> - /// Gets the protocol message embedded in the given HTTP request, if present. - /// </summary> /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam> + /// <param name="cancellationToken">The cancellation token.</param> /// <param name="httpRequest">The request to search for an embedded message.</param> - /// <param name="request">The deserialized message, if one is found. Null otherwise.</param> - /// <returns>True if the expected message was recognized and deserialized. False otherwise.</returns> - /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception> + /// <returns> + /// True if the expected message was recognized and deserialized. False otherwise. + /// </returns> + /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current" /> is null.</exception> /// <exception cref="ProtocolException">Thrown when a request message of an unexpected type is received.</exception> - public bool TryReadFromRequest<TRequest>(HttpRequestBase httpRequest, out TRequest request) + public async Task<TRequest> TryReadFromRequestAsync<TRequest>(HttpRequestMessage httpRequest, CancellationToken cancellationToken) where TRequest : class, IProtocolMessage { Requires.NotNull(httpRequest, "httpRequest"); - IProtocolMessage untypedRequest = this.ReadFromRequest(httpRequest); + IProtocolMessage untypedRequest = await this.ReadFromRequestAsync(httpRequest, cancellationToken); if (untypedRequest == null) { - request = null; - return false; + return null; } - request = untypedRequest as TRequest; + var request = untypedRequest as TRequest; ErrorUtilities.VerifyProtocol(request != null, MessagingStrings.UnexpectedMessageReceived, typeof(TRequest), untypedRequest.GetType()); - - return true; - } - - /// <summary> - /// Gets the protocol message embedded in the current HTTP request. - /// </summary> - /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam> - /// <returns>The deserialized message. Never null.</returns> - /// <remarks> - /// Requires an HttpContext.Current context. - /// </remarks> - /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception> - /// <exception cref="ProtocolException">Thrown if the expected message was not recognized in the response.</exception> - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")] - public TRequest ReadFromRequest<TRequest>() - where TRequest : class, IProtocolMessage { - return this.ReadFromRequest<TRequest>(this.GetRequestFromContext()); + return request; } /// <summary> /// Gets the protocol message embedded in the given HTTP request. /// </summary> /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam> + /// <param name="cancellationToken">The cancellation token.</param> /// <param name="httpRequest">The request to search for an embedded message.</param> - /// <returns>The deserialized message. Never null.</returns> + /// <returns> + /// The deserialized message. Never null. + /// </returns> /// <exception cref="ProtocolException">Thrown if the expected message was not recognized in the response.</exception> [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")] - public TRequest ReadFromRequest<TRequest>(HttpRequestBase httpRequest) + public async Task<TRequest> ReadFromRequestAsync<TRequest>(HttpRequestMessage httpRequest, CancellationToken cancellationToken) where TRequest : class, IProtocolMessage { Requires.NotNull(httpRequest, "httpRequest"); - TRequest request; - if (this.TryReadFromRequest<TRequest>(httpRequest, out request)) { - return request; - } else { - throw ErrorUtilities.ThrowProtocol(MessagingStrings.ExpectedMessageNotReceived, typeof(TRequest)); - } + + TRequest request = await this.TryReadFromRequestAsync<TRequest>(httpRequest, cancellationToken); + ErrorUtilities.VerifyProtocol(request != null, MessagingStrings.ExpectedMessageNotReceived, typeof(TRequest)); + return request; } /// <summary> /// Gets the protocol message that may be embedded in the given HTTP request. /// </summary> /// <param name="httpRequest">The request to search for an embedded message.</param> - /// <returns>The deserialized message, if one is found. Null otherwise.</returns> - public IDirectedProtocolMessage ReadFromRequest(HttpRequestBase httpRequest) { + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// The deserialized message, if one is found. Null otherwise. + /// </returns> + public async Task<IDirectedProtocolMessage> ReadFromRequestAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken) { Requires.NotNull(httpRequest, "httpRequest"); - if (Logger.Channel.IsInfoEnabled && httpRequest.GetPublicFacingUrl() != null) { - Logger.Channel.InfoFormat("Scanning incoming request for messages: {0}", httpRequest.GetPublicFacingUrl().AbsoluteUri); + if (Logger.Channel.IsInfoEnabled && httpRequest.RequestUri != null) { + Logger.Channel.InfoFormat("Scanning incoming request for messages: {0}", httpRequest.RequestUri.AbsoluteUri); } - IDirectedProtocolMessage requestMessage = this.ReadFromRequestCore(httpRequest); + IDirectedProtocolMessage requestMessage = await this.ReadFromRequestCoreAsync(httpRequest, cancellationToken); if (requestMessage != null) { Logger.Channel.DebugFormat("Incoming request received: {0}", requestMessage.GetType().Name); var directRequest = requestMessage as IHttpDirectRequest; if (directRequest != null) { - foreach (string header in httpRequest.Headers) { - directRequest.Headers[header] = httpRequest.Headers[header]; + foreach (var header in httpRequest.Headers) { + directRequest.Headers.Add(header.Key, header.Value); } } - this.ProcessIncomingMessage(requestMessage); + await this.ProcessIncomingMessageAsync(requestMessage, cancellationToken); } return requestMessage; @@ -496,17 +404,18 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <typeparam name="TResponse">The expected type of the message to be received.</typeparam> /// <param name="requestMessage">The message to send.</param> - /// <returns>The remote party's response.</returns> - /// <exception cref="ProtocolException"> - /// Thrown if no message is recognized in the response - /// or an unexpected type of message is received. - /// </exception> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// The remote party's response. + /// </returns> + /// <exception cref="ProtocolException">Thrown if no message is recognized in the response + /// 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, CancellationToken cancellationToken) where TResponse : class, IProtocolMessage { Requires.NotNull(requestMessage, "requestMessage"); - IProtocolMessage response = this.Request(requestMessage); + IProtocolMessage response = await this.RequestAsync(requestMessage, cancellationToken); ErrorUtilities.VerifyProtocol(response != null, MessagingStrings.ExpectedMessageNotReceived, typeof(TResponse)); var expectedResponse = response as TResponse; @@ -519,18 +428,21 @@ namespace DotNetOpenAuth.Messaging { /// Sends a direct message to a remote party and waits for the response. /// </summary> /// <param name="requestMessage">The message to send.</param> - /// <returns>The remote party's response. Guaranteed to never be null.</returns> + /// <param name="cancellationToken">The cancellation token.</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, CancellationToken cancellationToken) { Requires.NotNull(requestMessage, "requestMessage"); - this.ProcessOutgoingMessage(requestMessage); + await this.ProcessOutgoingMessageAsync(requestMessage, cancellationToken); Logger.Channel.DebugFormat("Sending {0} request.", requestMessage.GetType().Name); - var responseMessage = this.RequestCore(requestMessage); + var responseMessage = await this.RequestCoreAsync(requestMessage, cancellationToken); ErrorUtilities.VerifyProtocol(responseMessage != null, MessagingStrings.ExpectedMessageNotReceived, typeof(IProtocolMessage).Name); Logger.Channel.DebugFormat("Received {0} response.", responseMessage.GetType().Name); - this.ProcessIncomingMessage(responseMessage); + await this.ProcessIncomingMessageAsync(responseMessage, cancellationToken); return responseMessage; } @@ -551,12 +463,14 @@ namespace DotNetOpenAuth.Messaging { /// Verifies the integrity and applicability of an incoming message. /// </summary> /// <param name="message">The message just received.</param> - /// <exception cref="ProtocolException"> - /// Thrown when the message is somehow invalid. - /// This can be due to tampering, replay attack or expiration, among other things. - /// </exception> - internal void ProcessIncomingMessageTestHook(IProtocolMessage message) { - this.ProcessIncomingMessage(message); + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// A task that completes with the asynchronous operation. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the message is somehow invalid. + /// This can be due to tampering, replay attack or expiration, among other things.</exception> + internal Task ProcessIncomingMessageTestHookAsync(IProtocolMessage message, CancellationToken cancellationToken) { + return this.ProcessIncomingMessageAsync(message, cancellationToken); } /// <summary> @@ -565,10 +479,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 +495,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,20 +505,39 @@ 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) { - return this.ReadFromResponseCore(response); + internal Task<IDictionary<string, string>> ReadFromResponseCoreAsyncTestHook(HttpResponseMessage response, CancellationToken cancellationToken) { + return this.ReadFromResponseCoreAsync(response, cancellationToken); } - /// <remarks> - /// This method should NOT be called by derived types - /// except when sending ONE WAY request messages. - /// </remarks> /// <summary> /// Prepares a message for transmit by applying signatures, nonces, etc. /// </summary> /// <param name="message">The message to prepare for sending.</param> - internal void ProcessOutgoingMessageTestHook(IProtocolMessage message) { - this.ProcessOutgoingMessage(message); + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// A task that completes with the asynchronous operation. + /// </returns> + /// <remarks> + /// This method should NOT be called by derived types + /// except when sending ONE WAY request messages. + /// </remarks> + internal Task ProcessOutgoingMessageTestHookAsync(IProtocolMessage message, CancellationToken cancellationToken = default(CancellationToken)) { + return this.ProcessOutgoingMessageAsync(message, cancellationToken); + } + + /// <summary> + /// Parses the URL encoded form content. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A sequence of key=value pairs found in the request's entity; or an empty sequence if none are found.</returns> + protected internal static async Task<IEnumerable<KeyValuePair<string, string>>> ParseUrlEncodedFormContentAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + if (request.Content != null && request.Content.Headers.ContentType != null + && request.Content.Headers.ContentType.MediaType.Equals(HttpFormUrlEncoded)) { + return HttpUtility.ParseQueryString(await request.Content.ReadAsStringAsync()).AsKeyValuePairs(); + } + + return Enumerable.Empty<KeyValuePair<string, string>>(); } /// <summary> @@ -635,19 +568,43 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Adds just the binary data part of a message to a multipart form content object. + /// </summary> + /// <param name="requestMessageWithBinaryData">The request message with binary data.</param> + /// <returns>The initialized HttpContent.</returns> + protected static MultipartFormDataContent InitializeMultipartFormDataContent(IMessageWithBinaryData requestMessageWithBinaryData) { + Requires.NotNull(requestMessageWithBinaryData, "requestMessageWithBinaryData"); + + var content = new MultipartFormDataContent(); + foreach (var part in requestMessageWithBinaryData.BinaryData) { + if (string.IsNullOrEmpty(part.Name)) { + content.Add(part.Content); + } else if (string.IsNullOrEmpty(part.FileName)) { + content.Add(part.Content, part.Name); + } else { + content.Add(part.Content, part.Name, part.FileName); + } + } + + return content; + } + + /// <summary> /// Checks whether a given HTTP method is expected to include an entity body in its request. /// </summary> /// <param name="httpMethod">The HTTP method.</param> /// <returns><c>true</c> if the HTTP method is supposed to have an entity; <c>false</c> otherwise.</returns> - protected static bool HttpMethodHasEntity(string httpMethod) { - if (string.Equals(httpMethod, "GET", StringComparison.Ordinal) || - string.Equals(httpMethod, "HEAD", StringComparison.Ordinal) || - string.Equals(httpMethod, "DELETE", StringComparison.Ordinal) || - string.Equals(httpMethod, "OPTIONS", StringComparison.Ordinal)) { + protected static bool HttpMethodHasEntity(HttpMethod httpMethod) { + Requires.NotNull(httpMethod, "httpMethod"); + + if (httpMethod == HttpMethod.Get || + httpMethod == HttpMethod.Head || + httpMethod == HttpMethod.Delete || + httpMethod == HttpMethod.Options) { return false; - } else if (string.Equals(httpMethod, "POST", StringComparison.Ordinal) || - string.Equals(httpMethod, "PUT", StringComparison.Ordinal) || - string.Equals(httpMethod, "PATCH", StringComparison.Ordinal)) { + } else if (httpMethod == HttpMethod.Post || + httpMethod == HttpMethod.Put || + string.Equals(httpMethod.Method, "PATCH", StringComparison.Ordinal)) { return true; } else { throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod); @@ -659,11 +616,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,63 +656,76 @@ 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> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The response message, or null if the response did not carry a message.</returns> /// <remarks> /// Typically a deriving channel will override <see cref="CreateHttpRequest"/> to customize this method's /// 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, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); Requires.That(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient); - HttpWebRequest webRequest = this.CreateHttpRequest(request); + if (this.OutgoingMessageFilter != null) { + this.OutgoingMessageFilter(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]; + foreach (var header in directRequest.Headers) { + webRequest.Headers.Add(header.Key, header.Value); } } - IDictionary<string, string> responseFields; - IDirectResponseProtocolMessage responseMessage; - - using (IncomingWebResponse response = this.GetDirectResponse(webRequest)) { - if (response.ResponseStream == null) { - return null; - } - - responseFields = this.ReadFromResponseCore(response); - if (responseFields == null) { - return null; - } - - responseMessage = this.MessageFactory.GetNewResponseMessage(request, responseFields); - if (responseMessage == null) { - return null; + try { + using (var httpClient = this.HostFactories.CreateHttpClient()) { + using (var response = await httpClient.SendAsync(webRequest, cancellationToken)) { + if (response.Content != null) { + var responseFields = await this.ReadFromResponseCoreAsync(response, cancellationToken); + if (responseFields != null) { + var responseMessage = this.MessageFactory.GetNewResponseMessage(request, responseFields); + if (responseMessage != null) { + this.OnReceivingDirectResponse(response, responseMessage); + + var messageAccessor = this.MessageDescriptions.GetAccessor(responseMessage); + messageAccessor.Deserialize(responseFields); + + return responseMessage; + } + } + } + + if (!response.IsSuccessStatusCode) { + var errorContent = (response.Content != null) ? await response.Content.ReadAsStringAsync() : null; + Logger.Http.ErrorFormat( + "Error received in HTTP response: {0} {1}\n{2}", (int)response.StatusCode, response.ReasonPhrase, errorContent); + response.EnsureSuccessStatusCode(); // throw so we can wrap it in our catch block. + } + + return null; + } } - - this.OnReceivingDirectResponse(response, responseMessage); + } catch (HttpRequestException requestException) { + throw ErrorUtilities.Wrap(requestException, "Error sending HTTP request or receiving response."); } + } - var messageAccessor = this.MessageDescriptions.GetAccessor(responseMessage); - messageAccessor.Deserialize(responseFields); + /// <summary> + /// Provides derived-types the opportunity to wrap an <see cref="HttpMessageHandler"/> with another one. + /// </summary> + /// <param name="innerHandler">The inner handler received from <see cref="IHostFactories"/></param> + /// <returns>The handler to use in <see cref="HttpClient"/> instances.</returns> + protected virtual HttpMessageHandler WrapMessageHandler(HttpMessageHandler innerHandler) { + //TODO: make sure that everyone calls this to wrap their handlers rather than using the one directly returned + //from IHostFactories. - return responseMessage; + // No wrapping by default. + return innerHandler; } /// <summary> @@ -763,24 +733,27 @@ 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> /// Gets the protocol message that may be embedded in the given HTTP request. /// </summary> /// <param name="request">The request to search for an embedded message.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The deserialized message, if one is found. Null otherwise.</returns> - protected virtual IDirectedProtocolMessage ReadFromRequestCore(HttpRequestBase request) { + protected virtual async Task<IDirectedProtocolMessage> ReadFromRequestCoreAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); - Logger.Channel.DebugFormat("Incoming HTTP request: {0} {1}", request.HttpMethod, request.GetPublicFacingUrl().AbsoluteUri); + Logger.Channel.DebugFormat("Incoming HTTP request: {0} {1}", request.Method, request.RequestUri.AbsoluteUri); + + var fields = new Dictionary<string, string>(); // Search Form data first, and if nothing is there search the QueryString - Assumes.True(request.Form != null && request.GetQueryStringBeforeRewriting() != null); - var fields = request.Form.ToDictionary(); - if (fields.Count == 0 && request.HttpMethod != "POST") { // OpenID 2.0 section 4.1.2 - fields = request.GetQueryStringBeforeRewriting().ToDictionary(); + fields.AddRange(await ParseUrlEncodedFormContentAsync(request, cancellationToken)); + + if (fields.Count == 0 && request.Method.Method != "POST") { // OpenID 2.0 section 4.1.2 + fields.AddRange(HttpUtility.ParseQueryString(request.RequestUri.Query).AsKeyValuePairs()); } MessageReceivingEndpoint recipient; @@ -827,7 +800,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 +810,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 +822,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 +849,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 +863,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 HttpResponseMessageWithOriginal(message) { + 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 +882,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 +900,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 HttpResponseMessageWithOriginal(message) { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(bodyWriter.ToString()), }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html"); return response; } @@ -947,9 +914,12 @@ namespace DotNetOpenAuth.Messaging { /// Gets the protocol message that may be in the given HTTP response. /// </summary> /// <param name="response">The response that is anticipated to contain an protocol message.</param> - /// <returns>The deserialized message parts, if found. Null otherwise.</returns> + /// <param name="cancellationToken">The cancellation token.</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 Task<IDictionary<string, string>> ReadFromResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken); /// <summary> /// Prepares an HTTP request that carries a given message. @@ -957,10 +927,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 +945,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. @@ -1005,15 +975,24 @@ namespace DotNetOpenAuth.Messaging { return dictionary; } + internal Action<IProtocolMessage> OutgoingMessageFilter { get; set; } + + internal Action<IProtocolMessage> IncomingMessageFilter { get; set; } + /// <summary> /// Prepares a message for transmit by applying signatures, nonces, etc. /// </summary> /// <param name="message">The message to prepare for sending.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// A task that completes with the asynchronous operation. + /// </returns> + /// <exception cref="UnprotectedMessageException">Thrown if the message does not have the minimal required protections applied.</exception> /// <remarks> /// This method should NOT be called by derived types /// except when sending ONE WAY request messages. /// </remarks> - protected void ProcessOutgoingMessage(IProtocolMessage message) { + protected async Task ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { Requires.NotNull(message, "message"); Logger.Channel.DebugFormat("Preparing to send {0} ({1}) message.", message.GetType().Name, message.Version); @@ -1028,7 +1007,7 @@ namespace DotNetOpenAuth.Messaging { MessageProtections appliedProtection = MessageProtections.None; foreach (IChannelBindingElement bindingElement in this.outgoingBindingElements) { Assumes.True(bindingElement.Channel != null); - MessageProtections? elementProtection = bindingElement.ProcessOutgoingMessage(message); + MessageProtections? elementProtection = await bindingElement.ProcessOutgoingMessageAsync(message, cancellationToken); if (elementProtection.HasValue) { Logger.Bindings.DebugFormat("Binding element {0} applied to message.", bindingElement.GetType().FullName); @@ -1049,6 +1028,10 @@ namespace DotNetOpenAuth.Messaging { this.EnsureValidMessageParts(message); message.EnsureValidMessage(); + if (this.OutgoingMessageFilter != null) { + this.OutgoingMessageFilter(message); + } + if (Logger.Channel.IsInfoEnabled) { var directedMessage = message as IDirectedProtocolMessage; string recipient = (directedMessage != null && directedMessage.Recipient != null) ? directedMessage.Recipient.AbsoluteUri : "<response>"; @@ -1072,7 +1055,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 +1064,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 +1079,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 +1098,28 @@ 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 = InitializeMultipartFormDataContent(requestMessageWithBinaryData); // 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 +1133,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,66 +1149,26 @@ 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> - /// <exception cref="ProtocolException"> - /// Thrown when the message is somehow invalid. - /// This can be due to tampering, replay attack or expiration, among other things. - /// </exception> - protected virtual void ProcessIncomingMessage(IProtocolMessage message) { + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// A task that completes with the asynchronous operation. + /// </returns> + /// <exception cref="UnprotectedMessageException">Thrown if the message does not have the minimal required protections applied.</exception> + /// <exception cref="ProtocolException">Thrown when the message is somehow invalid. + /// This can be due to tampering, replay attack or expiration, among other things.</exception> + protected virtual async Task ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) { Requires.NotNull(message, "message"); if (Logger.Channel.IsInfoEnabled) { @@ -1237,10 +1181,14 @@ namespace DotNetOpenAuth.Messaging { messageAccessor.ToStringDeferred()); } + if (this.IncomingMessageFilter != null) { + this.IncomingMessageFilter(message); + } + MessageProtections appliedProtection = MessageProtections.None; foreach (IChannelBindingElement bindingElement in this.IncomingBindingElements) { Assumes.True(bindingElement.Channel != null); // CC bug: this.IncomingBindingElements ensures this... why must we assume it here? - MessageProtections? elementProtection = bindingElement.ProcessIncomingMessage(message); + MessageProtections? elementProtection = await bindingElement.ProcessIncomingMessageAsync(message, cancellationToken); if (elementProtection.HasValue) { Logger.Bindings.DebugFormat("Binding element {0} applied to message.", bindingElement.GetType().FullName); @@ -1301,7 +1249,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"); } @@ -1386,18 +1334,6 @@ namespace DotNetOpenAuth.Messaging { return -((int)protection1).CompareTo((int)protection2); // descending flag ordinal order } -#if CONTRACTS_FULL - /// <summary> - /// Verifies conditions that should be true for any valid state of this object. - /// </summary> - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")] - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] - [ContractInvariantMethod] - private void ObjectInvariant() { - Contract.Invariant(this.MessageDescriptions != null); - } -#endif - /// <summary> /// Verifies that all required message parts are initialized to values /// prior to sending the message to a remote party. diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index f3a1ba8..924a3c2 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -222,6 +222,58 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// When overridden in a derived class, gets an array of client-supported MIME accept types. + /// </summary> + /// <returns>An array of client-supported MIME accept types.</returns> + public override string[] AcceptTypes { + get { + if (this.Headers["Accept"] != null) { + return this.headers["Accept"].Split(','); + } + + return new string[0]; + } + } + + /// <summary> + /// When overridden in a derived class, gets information about the URL of the client request that linked to the current URL. + /// </summary> + /// <returns>The URL of the page that linked to the current request.</returns> + public override Uri UrlReferrer { + get { + if (this.Headers["Referer"] != null) { // misspelled word intentional, per RFC + return new Uri(this.Headers["Referer"]); + } + + return null; + } + } + + /// <summary> + /// When overridden in a derived class, gets the length, in bytes, of content that was sent by the client. + /// </summary> + /// <returns>The length, in bytes, of content that was sent by the client.</returns> + public override int ContentLength { + get { + if (this.Headers["Content-Length"] != null) { + return int.Parse(this.headers["Content-Length"]); + } + + return 0; + } + } + + /// <summary> + /// When overridden in a derived class, gets or sets the MIME content type of the request. + /// </summary> + /// <returns>The MIME content type of the request, such as "text/html".</returns> + /// <exception cref="System.NotImplementedException"></exception> + public override string ContentType { + get { return this.Headers["Content-Type"]; } + set { throw new NotImplementedException(); } + } + + /// <summary> /// Creates an <see cref="HttpRequestBase"/> instance that describes the specified HTTP request. /// </summary> /// <param name="request">The request.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpResponseMessageWithOriginal.cs b/src/DotNetOpenAuth.Core/Messaging/HttpResponseMessageWithOriginal.cs new file mode 100644 index 0000000..4db9f63 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/HttpResponseMessageWithOriginal.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="HttpResponseMessageWithOriginal.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System.Net; + using System.Net.Http; + + using Validation; + + internal class HttpResponseMessageWithOriginal : HttpResponseMessage { + /// <summary> + /// Initializes a new instance of the <see cref="HttpResponseMessageWithOriginal"/> class. + /// </summary> + /// <param name="originalMessage">The original message.</param> + /// <param name="statusCode">The status code.</param> + internal HttpResponseMessageWithOriginal(IMessage originalMessage, HttpStatusCode statusCode = HttpStatusCode.OK) + : base(statusCode) { + OriginalMessage = originalMessage; + Requires.NotNull(originalMessage, "originalMessage"); + } + + /// <summary> + /// Gets the original message. + /// </summary> + internal IMessage OriginalMessage { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs index fca46a0..fe2cf3d 100644 --- a/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs +++ b/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs @@ -6,6 +6,8 @@ namespace DotNetOpenAuth.Messaging { using System; + using System.Threading; + using System.Threading.Tasks; using Validation; /// <summary> @@ -33,6 +35,7 @@ namespace DotNetOpenAuth.Messaging { /// Prepares a message for sending based on the rules of this channel binding element. /// </summary> /// <param name="message">The message to prepare for sending.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. @@ -41,13 +44,14 @@ namespace DotNetOpenAuth.Messaging { /// Implementations that provide message protection must honor the /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. /// </remarks> - MessageProtections? ProcessOutgoingMessage(IProtocolMessage message); + Task<MessageProtections?> ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken); /// <summary> /// Performs any transformation on an incoming message that may be necessary and/or /// validates an incoming message based on the rules of this channel binding element. /// </summary> /// <param name="message">The incoming message to process.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns> /// The protections (if any) that this binding element applied to the message. /// Null if this binding element did not even apply to this binding element. @@ -60,6 +64,6 @@ namespace DotNetOpenAuth.Messaging { /// Implementations that provide message protection must honor the /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. /// </remarks> - MessageProtections? ProcessIncomingMessage(IProtocolMessage message); + Task<MessageProtections?> ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken); } } 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/IHttpDirectRequest.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs index 7b26869..226102d 100644 --- a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs +++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs @@ -15,6 +15,6 @@ namespace DotNetOpenAuth.Messaging { /// Gets the HTTP headers of the request. /// </summary> /// <value>May be an empty collection, but must not be <c>null</c>.</value> - WebHeaderCollection Headers { get; } + System.Net.Http.Headers.HttpRequestHeaders Headers { get; } } } diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs index 2992678..84a7760 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<MultipartContentMember> 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/MessageProtectionTasks.cs b/src/DotNetOpenAuth.Core/Messaging/MessageProtectionTasks.cs new file mode 100644 index 0000000..37c86ca --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/MessageProtectionTasks.cs @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------- +// <copyright file="MessageProtectionTasks.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + /// <summary> + /// Reusable pre-completed tasks that may be returned multiple times to reduce GC pressure. + /// </summary> + internal static class MessageProtectionTasks { + /// <summary> + /// A task whose result is <c>null</c> + /// </summary> + internal static readonly Task<MessageProtections?> Null = Task.FromResult<MessageProtections?>(null); + + /// <summary> + /// A task whose result is <see cref="MessageProtections.None"/> + /// </summary> + internal static readonly Task<MessageProtections?> None = + Task.FromResult<MessageProtections?>(MessageProtections.None); + + /// <summary> + /// A task whose result is <see cref="MessageProtections.TamperProtection"/> + /// </summary> + internal static readonly Task<MessageProtections?> TamperProtection = + Task.FromResult<MessageProtections?>(MessageProtections.TamperProtection); + + /// <summary> + /// A task whose result is <see cref="MessageProtections.ReplayProtection"/> + /// </summary> + internal static readonly Task<MessageProtections?> ReplayProtection = + Task.FromResult<MessageProtections?>(MessageProtections.ReplayProtection); + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index 221a29c..df11a16 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -15,12 +15,14 @@ namespace DotNetOpenAuth.Messaging { using System.Linq; using System.Net; using System.Net.Http; + using System.Net.Http.Headers; using System.Net.Mime; using System.Runtime.Serialization.Json; using System.Security; using System.Security.Cryptography; using System.Text; using System.Threading; + using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Xml; @@ -86,6 +88,11 @@ namespace DotNetOpenAuth.Messaging { private const int SymmetricSecretHandleLength = 4; /// <summary> + /// A pre-completed task. + /// </summary> + private static readonly Task CompletedTaskField = Task.FromResult<object>(null); + + /// <summary> /// The default lifetime of a private secret. /// </summary> private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Messaging.PrivateSecretMaximumAge; @@ -149,41 +156,17 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Gets a random number generator for use on the current thread only. + /// Gets a pre-completed task. /// </summary> - internal static Random NonCryptoRandomDataGenerator { - get { return ThreadSafeRandom.RandomNumberGenerator; } - } - - /// <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); + internal static Task CompletedTask { + get { return CompletedTaskField; } } /// <summary> - /// Transforms an OutgoingWebResponse to a Web API-friendly HttpResponseMessage. + /// Gets a random number generator for use on the current thread only. /// </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; + internal static Random NonCryptoRandomDataGenerator { + get { return ThreadSafeRandom.RandomNumberGenerator; } } /// <summary> @@ -223,22 +206,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> @@ -393,7 +360,15 @@ namespace DotNetOpenAuth.Messaging { // HttpRequest.Url gives us the internal URL in a cloud environment, // So we use a variable that (at least from what I can tell) gives us // the public URL: - if (serverVariables["HTTP_HOST"] != null) { + string httpHost; + try { + httpHost = serverVariables["HTTP_HOST"]; + } catch (NullReferenceException) { + // The VS dev web server can throw this. :( + httpHost = null; + } + + if (httpHost != null) { ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols."); string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme; Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); @@ -426,6 +401,115 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <returns>The URI that the outside world used to create this request.</returns> + public static Uri GetPublicFacingUrl() { + ErrorUtilities.VerifyHttpContext(); + return GetPublicFacingUrl(new HttpRequestWrapper(HttpContext.Current.Request)); + } + + /// <summary> + /// Wraps a response message as an MVC <see cref="ActionResult"/> so it can be conveniently returned from an MVC controller's action method. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns>An <see cref="ActionResult"/> instance.</returns> + public static ActionResult AsActionResult(this HttpResponseMessage response) { + Requires.NotNull(response, "response"); + return new HttpResponseMessageActionResult(response); + } + + /// <summary> + /// Wraps an instance of <see cref="HttpRequestBase"/> as an <see cref="HttpRequestMessage"/> instance. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>An instance of <see cref="HttpRequestMessage"/></returns> + public static HttpRequestMessage AsHttpRequestMessage(this HttpRequestBase request) { + Requires.NotNull(request, "request"); + + Uri publicFacingUrl = request.GetPublicFacingUrl(); + var httpRequest = new HttpRequestMessage(new HttpMethod(request.HttpMethod), publicFacingUrl); + + if (request.Form != null) { + // Avoid a request message that will try to read the request stream twice for already parsed data. + httpRequest.Content = new FormUrlEncodedContent(request.Form.AsKeyValuePairs()); + } else if (request.InputStream != null) { + httpRequest.Content = new StreamContent(request.InputStream); + } + + httpRequest.CopyHeadersFrom(request); + return httpRequest; + } + + /// <summary> + /// Sends a response message to the HTTP client. + /// </summary> + /// <param name="response">The response message.</param> + /// <param name="context">The HTTP context to send the response with.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// A task that completes with the asynchronous operation. + /// </returns> + public static async Task SendAsync(this HttpResponseMessage response, HttpContextBase context = null, CancellationToken cancellationToken = default(CancellationToken)) { + Requires.NotNull(response, "response"); + if (context == null) { + ErrorUtilities.VerifyHttpContext(); + context = new HttpContextWrapper(HttpContext.Current); + } + + var responseContext = context.Response; + responseContext.StatusCode = (int)response.StatusCode; + responseContext.StatusDescription = response.ReasonPhrase; + foreach (var header in response.Headers) { + foreach (var value in header.Value) { + responseContext.AddHeader(header.Key, value); + } + } + + if (response.Content != null) { + await response.Content.CopyToAsync(responseContext.OutputStream).ConfigureAwait(false); + } + } + + /// <summary> + /// Disposes a value if it is not null. + /// </summary> + /// <param name="disposable">The disposable value.</param> + internal static void DisposeIfNotNull(this IDisposable disposable) { + if (disposable != null) { + disposable.Dispose(); + } + } + + /// <summary> + /// Clones the specified <see cref="HttpRequestMessage"/> so it can be re-sent. + /// </summary> + /// <param name="original">The original message.</param> + /// <returns>The cloned message</returns> + /// <remarks> + /// This is useful when an HTTP request fails, and after a little tweaking should be resent. + /// Since <see cref="HttpRequestMessage"/> remembers it was already sent, it will not permit being + /// sent a second time. This method clones the message so its contents are identical but allows + /// re-sending. + /// </remarks> + internal static HttpRequestMessage Clone(this HttpRequestMessage original) { + Requires.NotNull(original, "original"); + + var clone = new HttpRequestMessage(original.Method, original.RequestUri); + clone.Content = original.Content; + foreach (var header in original.Headers) { + clone.Headers.Add(header.Key, header.Value); + } + + foreach (var property in original.Properties) { + clone.Properties[property.Key] = property.Value; + } + + clone.Version = original.Version; + return clone; + } + + /// <summary> /// Gets the URL to the root of a web site, which may include a virtual directory path. /// </summary> /// <returns>An absolute URI.</returns> @@ -496,71 +580,16 @@ 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> /// <param name="fields">The fields to include.</param> - /// <returns>A value prepared for an HTTP header.</returns> - internal static string AssembleAuthorizationHeader(string scheme, IEnumerable<KeyValuePair<string, string>> fields) { - Requires.NotNullOrEmpty(scheme, "scheme"); + /// <returns> + /// A value prepared for an HTTP header. + /// </returns> + internal static string AssembleAuthorizationHeader(IEnumerable<KeyValuePair<string, string>> fields) { Requires.NotNull(fields, "fields"); var authorization = new StringBuilder(); - authorization.Append(scheme); - authorization.Append(" "); foreach (var pair in fields) { string key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); string value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value); @@ -579,24 +608,15 @@ namespace DotNetOpenAuth.Messaging { /// <param name="scheme">The scheme. Must not be null or empty.</param> /// <param name="authorizationHeader">The authorization header. May be null or empty.</param> /// <returns>A sequence of key=value pairs discovered in the header. Never null, but may be empty.</returns> - internal static IEnumerable<KeyValuePair<string, string>> ParseAuthorizationHeader(string scheme, string authorizationHeader) { + internal static IEnumerable<KeyValuePair<string, string>> ParseAuthorizationHeader(string scheme, AuthenticationHeaderValue authorizationHeader) { Requires.NotNullOrEmpty(scheme, "scheme"); - string prefix = scheme + " "; - if (authorizationHeader != null) { - // The authorization header may have multiple sections. Look for the appropriate one. - string[] authorizationSections = new string[] { authorizationHeader }; // what is the right delimiter, if any? - foreach (string authorization in authorizationSections) { - string trimmedAuth = authorization.Trim(); - if (trimmedAuth.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { // RFC 2617 says this is case INsensitive - string data = trimmedAuth.Substring(prefix.Length); - return from element in data.Split(CommaArray) - let parts = element.Trim().Split(EqualsArray, 2) - let key = Uri.UnescapeDataString(parts[0]) - let value = Uri.UnescapeDataString(parts[1].Trim(QuoteArray)) - select new KeyValuePair<string, string>(key, value); - } - } + if (authorizationHeader != null && authorizationHeader.Scheme.Equals(scheme, StringComparison.OrdinalIgnoreCase)) { // RFC 2617 says this is case INsensitive + return from element in authorizationHeader.Parameter.Split(CommaArray) + let parts = element.Trim().Split(EqualsArray, 2) + let key = Uri.UnescapeDataString(parts[0]) + let value = Uri.UnescapeDataString(parts[1].Trim(QuoteArray)) + select new KeyValuePair<string, string>(key, value); } return Enumerable.Empty<KeyValuePair<string, string>>(); @@ -1104,31 +1124,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Adds a set of HTTP headers to an <see cref="HttpResponse"/> instance, - /// taking care to set some headers to the appropriate properties of - /// <see cref="HttpResponse" /> - /// </summary> - /// <param name="headers">The headers to add.</param> - /// <param name="response">The <see cref="HttpListenerResponse"/> instance to set the appropriate values to.</param> - internal static void ApplyHeadersToResponse(WebHeaderCollection headers, HttpListenerResponse response) { - Requires.NotNull(headers, "headers"); - Requires.NotNull(response, "response"); - - foreach (string headerName in headers) { - switch (headerName) { - case "Content-Type": - response.ContentType = headers[HttpResponseHeader.ContentType]; - break; - - // Add more special cases here as necessary. - default: - response.AddHeader(headerName, headers[headerName]); - break; - } - } - } - - /// <summary> /// Copies the contents of one stream to another. /// </summary> /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param> @@ -1179,80 +1174,20 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Clones an <see cref="HttpWebRequest"/> in order to send it again. - /// </summary> - /// <param name="request">The request to clone.</param> - /// <returns>The newly created instance.</returns> - internal static HttpWebRequest Clone(this HttpWebRequest request) { - Requires.NotNull(request, "request"); - Requires.That(request.RequestUri != null, "request", "request.RequestUri cannot be null."); - return Clone(request, request.RequestUri); - } - - /// <summary> - /// Clones an <see cref="HttpWebRequest"/> in order to send it again. + /// Clones an <see cref="HttpWebRequest" /> in order to send it again. /// </summary> - /// <param name="request">The request to clone.</param> - /// <param name="newRequestUri">The new recipient of the request.</param> - /// <returns>The newly created instance.</returns> - internal static HttpWebRequest Clone(this HttpWebRequest request, Uri newRequestUri) { + /// <param name="message">The message to set headers on.</param> + /// <param name="request">The request with headers to clone.</param> + internal static void CopyHeadersFrom(this HttpRequestMessage message, HttpRequestBase request) { Requires.NotNull(request, "request"); - Requires.NotNull(newRequestUri, "newRequestUri"); - - var newRequest = (HttpWebRequest)WebRequest.Create(newRequestUri); + Requires.NotNull(message, "message"); - // First copy headers. Only set those that are explicitly set on the original request, - // because some properties (like IfModifiedSince) activate special behavior when set, - // even when set to their "original" values. foreach (string headerName in request.Headers) { - switch (headerName) { - case "Accept": newRequest.Accept = request.Accept; break; - case "Connection": break; // Keep-Alive controls this - case "Content-Length": newRequest.ContentLength = request.ContentLength; break; - case "Content-Type": newRequest.ContentType = request.ContentType; break; - case "Expect": newRequest.Expect = request.Expect; break; - case "Host": break; // implicitly copied as part of the RequestUri - case "If-Modified-Since": newRequest.IfModifiedSince = request.IfModifiedSince; break; - case "Keep-Alive": newRequest.KeepAlive = request.KeepAlive; break; - case "Proxy-Connection": break; // no property equivalent? - case "Referer": newRequest.Referer = request.Referer; break; - case "Transfer-Encoding": newRequest.TransferEncoding = request.TransferEncoding; break; - case "User-Agent": newRequest.UserAgent = request.UserAgent; break; - default: newRequest.Headers[headerName] = request.Headers[headerName]; break; + string[] headerValues = request.Headers.GetValues(headerName); + if (!message.Headers.TryAddWithoutValidation(headerName, headerValues)) { + message.Content.Headers.TryAddWithoutValidation(headerName, headerValues); } } - - newRequest.AllowAutoRedirect = request.AllowAutoRedirect; - newRequest.AllowWriteStreamBuffering = request.AllowWriteStreamBuffering; - newRequest.AuthenticationLevel = request.AuthenticationLevel; - newRequest.AutomaticDecompression = request.AutomaticDecompression; - newRequest.CachePolicy = request.CachePolicy; - newRequest.ClientCertificates = request.ClientCertificates; - newRequest.ConnectionGroupName = request.ConnectionGroupName; - newRequest.ContinueDelegate = request.ContinueDelegate; - newRequest.CookieContainer = request.CookieContainer; - newRequest.Credentials = request.Credentials; - newRequest.ImpersonationLevel = request.ImpersonationLevel; - newRequest.MaximumAutomaticRedirections = request.MaximumAutomaticRedirections; - newRequest.MaximumResponseHeadersLength = request.MaximumResponseHeadersLength; - newRequest.MediaType = request.MediaType; - newRequest.Method = request.Method; - newRequest.Pipelined = request.Pipelined; - newRequest.PreAuthenticate = request.PreAuthenticate; - newRequest.ProtocolVersion = request.ProtocolVersion; - newRequest.ReadWriteTimeout = request.ReadWriteTimeout; - newRequest.SendChunked = request.SendChunked; - newRequest.Timeout = request.Timeout; - newRequest.UseDefaultCredentials = request.UseDefaultCredentials; - - try { - newRequest.Proxy = request.Proxy; - newRequest.UnsafeAuthenticatedConnectionSharing = request.UnsafeAuthenticatedConnectionSharing; - } catch (SecurityException) { - Logger.Messaging.Warn("Unable to clone some HttpWebRequest properties due to partial trust."); - } - - return newRequest; } /// <summary> @@ -1503,8 +1438,8 @@ namespace DotNetOpenAuth.Messaging { /// <param name="request">The request to get recipient information from.</param> /// <returns>The recipient.</returns> /// <exception cref="ArgumentException">Thrown if the HTTP request is something we can't handle.</exception> - internal static MessageReceivingEndpoint GetRecipient(this HttpRequestBase request) { - return new MessageReceivingEndpoint(request.GetPublicFacingUrl(), GetHttpDeliveryMethod(request.HttpMethod)); + internal static MessageReceivingEndpoint GetRecipient(this HttpRequestMessage request) { + return new MessageReceivingEndpoint(request.RequestUri, GetHttpDeliveryMethod(request.Method.Method)); } /// <summary> @@ -1538,23 +1473,23 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="httpMethod">The HTTP method.</param> /// <returns>An HTTP verb, such as GET, POST, PUT, DELETE, PATCH, or OPTION.</returns> - internal static string GetHttpVerb(HttpDeliveryMethods httpMethod) { + internal static HttpMethod GetHttpVerb(HttpDeliveryMethods httpMethod) { if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.GetRequest) { - return "GET"; + return HttpMethod.Get; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PostRequest) { - return "POST"; + return HttpMethod.Post; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PutRequest) { - return "PUT"; + return HttpMethod.Put; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.DeleteRequest) { - return "DELETE"; + return HttpMethod.Delete; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.HeadRequest) { - return "HEAD"; + return HttpMethod.Head; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PatchRequest) { - return "PATCH"; + return new HttpMethod("PATCH"); } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.OptionsRequest) { - return "OPTIONS"; + return HttpMethod.Options; } else if ((httpMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) { - return "GET"; // if AuthorizationHeaderRequest is specified without an explicit HTTP verb, assume GET. + return HttpMethod.Get; // if AuthorizationHeaderRequest is specified without an explicit HTTP verb, assume GET. } else { throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod); } @@ -1580,6 +1515,29 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets the URI that contains the entire payload that would be sent by the browser for the specified redirect-based request message. + /// </summary> + /// <param name="response">The redirecting response message.</param> + /// <returns>The absolute URI that could be retrieved to send the same message the browser would.</returns> + /// <exception cref="System.NotSupportedException">Thrown if the message is not a redirect message.</exception> + internal static Uri GetDirectUriRequest(this HttpResponseMessage response) { + Requires.NotNull(response, "response"); + Requires.Argument( + response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectKeepVerb + || response.StatusCode == HttpStatusCode.RedirectMethod || response.StatusCode == HttpStatusCode.TemporaryRedirect, + "response", + "Redirecting response expected."); + + if (response.Headers.Location != null) { + return response.Headers.Location; + } else { + // Some responses are so large that they're HTML/JS self-posting pages. + // We can't create long URLs for those, at present. + throw new NotSupportedException(); + } + } + + /// <summary> /// Collects a sequence of key=value pairs into a dictionary. /// </summary> /// <typeparam name="TKey">The type of the key.</typeparam> @@ -1592,6 +1550,21 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Enumerates all members of the collection as key=value pairs. + /// </summary> + /// <param name="nvc">The collection to enumerate.</param> + /// <returns>A sequence of pairs.</returns> + internal static IEnumerable<KeyValuePair<string, string>> AsKeyValuePairs(this NameValueCollection nvc) { + Requires.NotNull(nvc, "nvc"); + + foreach (string key in nvc) { + foreach (string value in nvc.GetValues(key)) { + yield return new KeyValuePair<string, string>(key, value); + } + } + } + + /// <summary> /// Converts a <see cref="NameValueCollection"/> to an IDictionary<string, string>. /// </summary> /// <param name="nvc">The NameValueCollection to convert. May be null.</param> @@ -1862,6 +1835,11 @@ namespace DotNetOpenAuth.Messaging { internal static string EscapeUriDataStringRfc3986(string value) { Requires.NotNull(value, "value"); + // fast path for empty values. + if (value.Length == 0) { + return value; + } + // Start with RFC 2396 escaping by calling the .NET method to do the work. // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation). // If it does, the escaping we do that follows it will be a no-op since the @@ -2039,5 +2017,33 @@ namespace DotNetOpenAuth.Messaging { #endregion } + + /// <summary> + /// An MVC <see cref="ActionResult"/> that wraps an <see cref="HttpResponseMessage"/> + /// </summary> + private class HttpResponseMessageActionResult : ActionResult { + /// <summary> + /// The wrapped response. + /// </summary> + private readonly HttpResponseMessage response; + + /// <summary> + /// Initializes a new instance of the <see cref="HttpResponseMessageActionResult"/> class. + /// </summary> + /// <param name="response">The response.</param> + internal HttpResponseMessageActionResult(HttpResponseMessage 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 the <see cref="T:System.Web.Mvc.ActionResult" /> class. + /// </summary> + /// <param name="context">The context in which the result is executed. The context information includes the controller, HTTP content, request context, and route data.</param> + public override void ExecuteResult(ControllerContext context) { + // TODO: fix this to be asynchronous. + this.response.SendAsync(context.HttpContext).GetAwaiter().GetResult(); + } + } } } diff --git a/src/DotNetOpenAuth.Core/Messaging/MultipartContentMember.cs b/src/DotNetOpenAuth.Core/Messaging/MultipartContentMember.cs new file mode 100644 index 0000000..fd5bfb5 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/MultipartContentMember.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// <copyright file="MultipartContentMember.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + + /// <summary> + /// Describes a part from a multi-part POST. + /// </summary> + public struct MultipartContentMember { + /// <summary> + /// Initializes a new instance of the <see cref="MultipartContentMember"/> struct. + /// </summary> + /// <param name="content">The content.</param> + /// <param name="name">The name of this part as it may come from an HTML form.</param> + /// <param name="fileName">Name of the file.</param> + public MultipartContentMember(HttpContent content, string name = null, string fileName = null) + : this() { + this.Content = content; + this.Name = name; + this.FileName = fileName; + } + + /// <summary> + /// Gets or sets the content. + /// </summary> + /// <value> + /// The content. + /// </value> + public HttpContent Content { get; set; } + + /// <summary> + /// Gets or sets the HTML form name. + /// </summary> + /// <value> + /// The name. + /// </value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the name of the file. + /// </summary> + /// <value> + /// The name of the file. + /// </value> + public string FileName { get; set; } + } +} 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..b5cab3b 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs @@ -8,7 +8,10 @@ namespace DotNetOpenAuth.Messaging { using System; using System.Collections.Generic; using System.Linq; + using System.Net.Http; using System.Text; + using System.Threading; + using System.Threading.Tasks; using Validation; /// <summary> @@ -61,10 +64,12 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Creates the HTTP response to forward to the client to report the error. /// </summary> - /// <returns>The HTTP response.</returns> - public OutgoingWebResponse CreateErrorResponse() { - var response = this.channel.PrepareResponse(this.ErrorResponseMessage); - return response; + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns> + /// The HTTP response. + /// </returns> + public Task<HttpResponseMessage> CreateErrorResponseAsync(CancellationToken cancellationToken) { + return this.channel.PrepareResponseAsync(this.ErrorResponseMessage, cancellationToken); } } } diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs index 9cb80b0..413d0ad 100644 --- a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs @@ -27,16 +27,15 @@ namespace DotNetOpenAuth.Messaging { private readonly ICollection<Version> versions; /// <summary> - /// Initializes a new instance of the <see cref="StandardMessageFactoryChannel"/> class. + /// Initializes a new instance of the <see cref="StandardMessageFactoryChannel" /> class. /// </summary> /// <param name="messageTypes">The message types that might be encountered.</param> /// <param name="versions">All the possible message versions that might be encountered.</param> - /// <param name="bindingElements"> - /// 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 StandardMessageFactoryChannel(ICollection<Type> messageTypes, ICollection<Version> versions, params IChannelBindingElement[] bindingElements) - : base(new StandardMessageFactory(), bindingElements) { + /// <param name="hostFactories">The host factories.</param> + /// <param name="bindingElements">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 StandardMessageFactoryChannel(ICollection<Type> messageTypes, ICollection<Version> versions, IHostFactories hostFactories, IChannelBindingElement[] bindingElements = null) + : base(new StandardMessageFactory(), bindingElements ?? new IChannelBindingElement[0], hostFactories) { Requires.NotNull(messageTypes, "messageTypes"); Requires.NotNull(versions, "versions"); 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; - } - } -} |