diff options
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging')
23 files changed, 681 insertions, 113 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/Base64WebEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/Base64WebEncoder.cs new file mode 100644 index 0000000..135e650 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Base64WebEncoder.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------- +// <copyright file="Base64WebEncoder.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 DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// A message part encoder that translates between <c>byte[]</c> and base64web encoded strings. + /// </summary> + internal class Base64WebEncoder : IMessagePartEncoder { + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns>The <paramref name="value"/> in string form, ready for message transport.</returns> + public string Encode(object value) { + return MessagingUtilities.ConvertToBase64WebSafeString((byte[])value); + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns>The deserialized form of the given string.</returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return MessagingUtilities.FromBase64WebSafeString(value); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs index c58702c..672a942 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -38,6 +38,16 @@ namespace DotNetOpenAuth.Messaging { internal static readonly Encoding PostEntityEncoding = new UTF8Encoding(false); /// <summary> + /// A default set of XML dictionary reader quotas that are relatively safe from causing unbounded memory consumption. + /// </summary> + internal static readonly XmlDictionaryReaderQuotas DefaultUntrustedXmlDictionaryReaderQuotas = new XmlDictionaryReaderQuotas { + MaxArrayLength = 1, + MaxDepth = 2, + MaxBytesPerRead = 8 * 1024, + MaxStringContentLength = 16 * 1024, + }; + + /// <summary> /// The content-type used on HTTP POST requests where the POST entity is a /// URL-encoded series of key=value pairs. /// </summary> @@ -143,18 +153,16 @@ namespace DotNetOpenAuth.Messaging { /// 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.</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) { Requires.NotNull(messageTypeProvider, "messageTypeProvider"); this.messageTypeProvider = messageTypeProvider; this.WebRequestHandler = new StandardWebRequestHandler(); - this.XmlDictionaryReaderQuotas = new XmlDictionaryReaderQuotas { - MaxArrayLength = 1, - MaxDepth = 2, - MaxBytesPerRead = 8 * 1024, - MaxStringContentLength = 16 * 1024, - }; + this.XmlDictionaryReaderQuotas = DefaultUntrustedXmlDictionaryReaderQuotas; this.outgoingBindingElements = new List<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements)); this.incomingBindingElements = new List<IChannelBindingElement>(this.outgoingBindingElements); @@ -475,6 +483,14 @@ namespace DotNetOpenAuth.Messaging { IDirectedProtocolMessage requestMessage = this.ReadFromRequestCore(httpRequest); 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]; + } + } + this.ProcessIncomingMessage(requestMessage); } @@ -714,6 +730,13 @@ namespace DotNetOpenAuth.Messaging { Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient); HttpWebRequest webRequest = this.CreateHttpRequest(request); + var directRequest = request as IHttpDirectRequest; + if (directRequest != null) { + foreach (string header in directRequest.Headers) { + webRequest.Headers[header] = directRequest.Headers[header]; + } + } + IDictionary<string, string> responseFields; IDirectResponseProtocolMessage responseMessage; @@ -973,17 +996,7 @@ namespace DotNetOpenAuth.Messaging { [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] protected virtual string SerializeAsJson(IMessage message) { Requires.NotNull(message, "message"); - - MessageDictionary messageDictionary = this.MessageDescriptions.GetAccessor(message); - using (var memoryStream = new MemoryStream()) { - using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream, Encoding.UTF8)) { - MessageSerializer.Serialize(messageDictionary, jsonWriter); - jsonWriter.Flush(); - } - - string json = Encoding.UTF8.GetString(memoryStream.ToArray()); - return json; - } + return MessagingUtilities.SerializeAsJson(message, this.MessageDescriptions); } /// <summary> @@ -1079,6 +1092,7 @@ namespace DotNetOpenAuth.Messaging { UriBuilder builder = new UriBuilder(requestMessage.Recipient); MessagingUtilities.AppendQueryArgs(builder, fields); HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); + this.PrepareHttpWebRequest(httpRequest); return httpRequest; } @@ -1119,6 +1133,7 @@ namespace DotNetOpenAuth.Messaging { var fields = messageAccessor.Serialize(); var httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); + this.PrepareHttpWebRequest(httpRequest); httpRequest.CachePolicy = this.CachePolicy; httpRequest.Method = "POST"; @@ -1296,6 +1311,14 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// 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) { + Requires.NotNull(request, "request"); + } + + /// <summary> /// Customizes the binding element order for outgoing and incoming messages. /// </summary> /// <param name="outgoingOrder">The outgoing order.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBag.cs b/src/DotNetOpenAuth.Core/Messaging/DataBag.cs index c9c3415..0800840 100644 --- a/src/DotNetOpenAuth.Core/Messaging/DataBag.cs +++ b/src/DotNetOpenAuth.Core/Messaging/DataBag.cs @@ -14,7 +14,7 @@ namespace DotNetOpenAuth.Messaging { /// A collection of message parts that will be serialized into a single string, /// to be set into a larger message. /// </summary> - internal abstract class DataBag : IMessage { + public abstract class DataBag : IMessage { /// <summary> /// The default version for DataBags. /// </summary> @@ -105,7 +105,7 @@ namespace DotNetOpenAuth.Messaging { /// </remarks> [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Accessed by reflection")] [MessagePart("t", IsRequired = true, AllowEmpty = false)] - private Type BagType { + protected virtual Type BagType { get { return this.GetType(); } } diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs index 9d4b93e..2452502 100644 --- a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs +++ b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs @@ -22,7 +22,7 @@ namespace DotNetOpenAuth.Messaging { /// A serializer for <see cref="DataBag"/>-derived types /// </summary> /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam> - internal abstract class DataBagFormatterBase<T> : IDataBagFormatter<T> where T : DataBag, new() { + internal abstract class DataBagFormatterBase<T> : IDataBagFormatter<T> where T : DataBag { /// <summary> /// The message description cache to use for data bag types. /// </summary> @@ -146,6 +146,8 @@ namespace DotNetOpenAuth.Messaging { /// <returns>A non-null, non-empty value.</returns> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] public string Serialize(T message) { + Requires.NotNull(message, "message"); + message.UtcCreationDate = DateTime.UtcNow; if (this.decodeOnceOnly != null) { @@ -190,14 +192,13 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Deserializes a <see cref="DataBag"/>, including decompression, decryption, signature and nonce validation where applicable. /// </summary> + /// <param name="message">The instance to initialize with deserialized data.</param> /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be null.</param> /// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation.</param> - /// <returns> - /// The deserialized value. Never null. - /// </returns> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - public T Deserialize(IProtocolMessage containingMessage, string value, string messagePartName) { + public void Deserialize(T message, IProtocolMessage containingMessage, string value, string messagePartName) { + Requires.NotNull(message, "message"); Requires.NotNull(containingMessage, "containingMessage"); Requires.NotNullOrEmpty(value, "value"); Requires.NotNullOrEmpty(messagePartName, "messagePartName"); @@ -209,15 +210,15 @@ namespace DotNetOpenAuth.Messaging { value = valueWithoutHandle; } - var message = new T { ContainingMessage = containingMessage }; + message.ContainingMessage = containingMessage; byte[] data = MessagingUtilities.FromBase64WebSafeString(value); byte[] signature = null; if (this.signed) { using (var dataStream = new MemoryStream(data)) { var dataReader = new BinaryReader(dataStream); - signature = dataReader.ReadBuffer(); - data = dataReader.ReadBuffer(); + signature = dataReader.ReadBuffer(1024); + data = dataReader.ReadBuffer(8 * 1024); } // Verify that the verification code was issued by message authorization server. @@ -254,8 +255,6 @@ namespace DotNetOpenAuth.Messaging { } ((IMessage)message).EnsureValidMessage(); - - return message; } /// <summary> @@ -287,7 +286,7 @@ namespace DotNetOpenAuth.Messaging { Requires.NotNull(signature, "signature"); if (this.asymmetricSigning != null) { - using (var hasher = new SHA1CryptoServiceProvider()) { + using (var hasher = SHA1.Create()) { return this.asymmetricSigning.VerifyData(signedData, hasher, signature); } } else { @@ -310,13 +309,13 @@ namespace DotNetOpenAuth.Messaging { Contract.Ensures(Contract.Result<byte[]>() != null); if (this.asymmetricSigning != null) { - using (var hasher = new SHA1CryptoServiceProvider()) { + using (var hasher = SHA1.Create()) { return this.asymmetricSigning.SignData(bytesToSign, hasher); } } else { var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle); ErrorUtilities.VerifyProtocol(key != null, MessagingStrings.MissingDecryptionKeyForHandle, this.cryptoKeyBucket, symmetricSecretHandle); - using (var symmetricHasher = new HMACSHA256(key.Key)) { + using (var symmetricHasher = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, key.Key)) { return symmetricHasher.ComputeHash(bytesToSign); } } diff --git a/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs index f499d67..2237cc7 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs @@ -193,17 +193,17 @@ namespace DotNetOpenAuth.Messaging { /// Throws a <see cref="ProtocolException"/> if some <paramref name="condition"/> evaluates to false. /// </summary> /// <param name="condition">True to do nothing; false to throw the exception.</param> - /// <param name="message">The error message for the exception.</param> + /// <param name="unformattedMessage">The error message for the exception.</param> /// <param name="args">The string formatting arguments, if any.</param> /// <exception cref="ProtocolException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception> [Pure] - internal static void VerifyProtocol(bool condition, string message, params object[] args) { + internal static void VerifyProtocol(bool condition, string unformattedMessage, params object[] args) { Requires.NotNull(args, "args"); Contract.Ensures(condition); Contract.EnsuresOnThrow<ProtocolException>(!condition); - Contract.Assume(message != null); + Contract.Assume(unformattedMessage != null); if (!condition) { - var exception = new ProtocolException(string.Format(CultureInfo.CurrentCulture, message, args)); + var exception = new ProtocolException(string.Format(CultureInfo.CurrentCulture, unformattedMessage, args)); if (Logger.Messaging.IsErrorEnabled) { Logger.Messaging.Error( string.Format( @@ -220,7 +220,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Throws a <see cref="ProtocolException"/>. /// </summary> - /// <param name="message">The message to set in the exception.</param> + /// <param name="unformattedMessage">The message to set in the exception.</param> /// <param name="args">The formatting arguments of the message.</param> /// <returns> /// An InternalErrorException, which may be "thrown" by the caller in order @@ -229,10 +229,10 @@ namespace DotNetOpenAuth.Messaging { /// </returns> /// <exception cref="ProtocolException">Always thrown.</exception> [Pure] - internal static Exception ThrowProtocol(string message, params object[] args) { + internal static Exception ThrowProtocol(string unformattedMessage, params object[] args) { Requires.NotNull(args, "args"); - Contract.Assume(message != null); - VerifyProtocol(false, message, args); + Contract.Assume(unformattedMessage != null); + VerifyProtocol(false, unformattedMessage, args); // we never reach here, but this allows callers to "throw" this method. return new InternalErrorException(); diff --git a/src/DotNetOpenAuth.Core/Messaging/HmacAlgorithms.cs b/src/DotNetOpenAuth.Core/Messaging/HmacAlgorithms.cs new file mode 100644 index 0000000..872b4ac --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/HmacAlgorithms.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="HmacAlgorithms.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + + /// <summary> + /// HMAC-SHA algorithm names that can be passed to the <see cref="HMAC.Create(string)"/> method. + /// </summary> + internal static class HmacAlgorithms { + /// <summary> + /// The name of the HMAC-SHA1 algorithm. + /// </summary> + internal const string HmacSha1 = "HMACSHA1"; + + /// <summary> + /// The name of the HMAC-SHA256 algorithm. + /// </summary> + internal const string HmacSha256 = "HMACSHA256"; + + /// <summary> + /// The name of the HMAC-SHA384 algorithm. + /// </summary> + internal const string HmacSha384 = "HMACSHA384"; + + /// <summary> + /// The name of the HMAC-SHA512 algorithm. + /// </summary> + internal const string HmacSha512 = "HMACSHA512"; + + /// <summary> + /// Creates an HMAC-SHA algorithm with the specified name and key. + /// </summary> + /// <param name="algorithmName">A name from the available choices in the static const members of this class.</param> + /// <param name="key">The secret key used as the HMAC.</param> + /// <returns>The HMAC algorithm instance.</returns> + internal static HMAC Create(string algorithmName, byte[] key) { + Requires.NotNullOrEmpty(algorithmName, "algorithmName"); + Requires.NotNull(key, "key"); + + HMAC hmac = HMAC.Create(algorithmName); + try { + hmac.Key = key; + return hmac; + } catch { +#if CLR4 + hmac.Dispose(); +#endif + throw; + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs index 9579a81..dad6bf6 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs @@ -20,6 +20,11 @@ namespace DotNetOpenAuth.Messaging { internal const string Authorization = "Authorization"; /// <summary> + /// The WWW-Authenticate header, which is included in HTTP 401 Unauthorized responses to help the client know which authorization schemes are supported. + /// </summary> + internal const string WwwAuthenticate = "WWW-Authenticate"; + + /// <summary> /// The Content-Type header, which specifies the MIME type of the accompanying body data. /// </summary> internal const string ContentType = "Content-Type"; diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index 0f60e04..4b4a3fe 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -13,6 +13,10 @@ namespace DotNetOpenAuth.Messaging { using System.Globalization; using System.IO; using System.Net; +#if CLR4 + using System.Net.Http; + using System.Net.Http.Headers; +#endif using System.Net.Mime; using System.ServiceModel.Channels; using System.Web; @@ -90,7 +94,7 @@ namespace DotNetOpenAuth.Messaging { this.requestUri = requestUri; this.form = form ?? new NameValueCollection(); this.queryString = HttpUtility.ParseQueryString(requestUri.Query); - this.headers = headers ?? new NameValueCollection(); + this.headers = headers ?? new WebHeaderCollection(); this.serverVariables = new NameValueCollection(); } @@ -105,12 +109,33 @@ namespace DotNetOpenAuth.Messaging { this.requestUri = listenerRequest.Url; this.queryString = listenerRequest.QueryString; this.headers = listenerRequest.Headers; - this.form = ParseFormData(listenerRequest.HttpMethod, listenerRequest.Headers, listenerRequest.InputStream); + this.form = ParseFormData(listenerRequest.HttpMethod, listenerRequest.Headers, () => listenerRequest.InputStream); this.serverVariables = new NameValueCollection(); Reporting.RecordRequestStatistics(this); } +#if CLR4 + /// <summary> + /// Initializes a new instance of the <see cref="HttpRequestInfo" /> class. + /// </summary> + /// <param name="request">The request.</param> + internal HttpRequestInfo(HttpRequestMessage request) { + Requires.NotNull(request, "request"); + + this.httpMethod = request.Method.ToString(); + this.requestUri = request.RequestUri; + this.queryString = HttpUtility.ParseQueryString(request.RequestUri.Query); + this.headers = new NameValueCollection(); + AddHeaders(this.headers, request.Headers); + AddHeaders(this.headers, request.Content.Headers); + this.form = ParseFormData(this.httpMethod, this.headers, () => request.Content.ReadAsStreamAsync().Result); + this.serverVariables = new NameValueCollection(); + + Reporting.RecordRequestStatistics(this); + } +#endif + /// <summary> /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. /// </summary> @@ -126,7 +151,7 @@ namespace DotNetOpenAuth.Messaging { this.requestUri = requestUri; this.headers = headers; this.queryString = HttpUtility.ParseQueryString(requestUri.Query); - this.form = ParseFormData(httpMethod, headers, inputStream); + this.form = ParseFormData(httpMethod, headers, () => inputStream); this.serverVariables = new NameValueCollection(); Reporting.RecordRequestStatistics(this); @@ -200,6 +225,17 @@ namespace DotNetOpenAuth.Messaging { return new HttpRequestInfo(listenerRequest); } +#if CLR4 + /// <summary> + /// Creates an <see cref="HttpRequestBase"/> instance that describes the specified HTTP request. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <returns>An instance of <see cref="HttpRequestBase"/>.</returns> + public static HttpRequestBase Create(HttpRequestMessage request) { + return new HttpRequestInfo(request); + } +#endif + /// <summary> /// Creates an <see cref="HttpRequestBase"/> instance that describes the specified HTTP request. /// </summary> @@ -229,14 +265,15 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="httpMethod">The HTTP method.</param> /// <param name="headers">The headers.</param> - /// <param name="inputStream">The input stream.</param> + /// <param name="inputStreamFunc">A function that returns the input stream.</param> /// <returns>The non-null collection of form variables.</returns> - private static NameValueCollection ParseFormData(string httpMethod, NameValueCollection headers, Stream inputStream) { + private static NameValueCollection ParseFormData(string httpMethod, NameValueCollection headers, Func<Stream> inputStreamFunc) { Requires.NotNullOrEmpty(httpMethod, "httpMethod"); Requires.NotNull(headers, "headers"); ContentType contentType = string.IsNullOrEmpty(headers[HttpRequestHeaders.ContentType]) ? null : new ContentType(headers[HttpRequestHeaders.ContentType]); - if (inputStream != null && httpMethod == "POST" && contentType != null && string.Equals(contentType.MediaType, Channel.HttpFormUrlEncoded, StringComparison.Ordinal)) { + if (httpMethod == "POST" && contentType != null && string.Equals(contentType.MediaType, Channel.HttpFormUrlEncoded, StringComparison.Ordinal) && inputStreamFunc != null) { + var inputStream = inputStreamFunc(); var reader = new StreamReader(inputStream); long originalPosition = 0; if (inputStream.CanSeek) { @@ -252,5 +289,23 @@ namespace DotNetOpenAuth.Messaging { return new NameValueCollection(); } + +#if CLR4 + /// <summary> + /// Adds HTTP headers to a <see cref="NameValueCollection"/>. + /// </summary> + /// <param name="collectionToFill">The collection to be modified with added entries.</param> + /// <param name="headers">The collection to read from.</param> + private static void AddHeaders(NameValueCollection collectionToFill, HttpHeaders headers) { + Requires.NotNull(collectionToFill, "collectionToFill"); + Requires.NotNull(headers, "headers"); + + foreach (var header in headers) { + foreach (var value in header.Value) { + collectionToFill.Add(header.Key, value); + } + } + } +#endif } } diff --git a/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs index 9086ee9..923773e 100644 --- a/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs +++ b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs @@ -13,7 +13,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam> [ContractClass(typeof(IDataBagFormatterContract<>))] - internal interface IDataBagFormatter<T> where T : DataBag, new() { + internal interface IDataBagFormatter<in T> where T : DataBag { /// <summary> /// Serializes the specified message. /// </summary> @@ -24,13 +24,11 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Deserializes a <see cref="DataBag"/>. /// </summary> + /// <param name="message">The instance to deserialize into</param> /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be null.</param> /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation.</param> - /// <returns> - /// The deserialized value. Never null. - /// </returns> - T Deserialize(IProtocolMessage containingMessage, string data, string messagePartName); + void Deserialize(T message, IProtocolMessage containingMessage, string data, string messagePartName); } /// <summary> @@ -62,13 +60,12 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Deserializes a <see cref="DataBag"/>. /// </summary> + /// <param name="message">The instance to deserialize into</param> /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param> /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> /// <param name="messagePartName">Name of the message part whose value is to be deserialized. Used for exception messages.</param> - /// <returns> - /// The deserialized value. Never null. - /// </returns> - T IDataBagFormatter<T>.Deserialize(IProtocolMessage containingMessage, string data, string messagePartName) { + void IDataBagFormatter<T>.Deserialize(T message, IProtocolMessage containingMessage, string data, string messagePartName) { + Requires.NotNull(message, "message"); Requires.NotNull(containingMessage, "containingMessage"); Requires.NotNullOrEmpty(data, "data"); Requires.NotNullOrEmpty(messagePartName, "messagePartName"); diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs new file mode 100644 index 0000000..7153334 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// <copyright file="IHttpDirectRequest.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System.Diagnostics.Contracts; + using System.Net; + + /// <summary> + /// An interface that allows direct request messages to capture the details of the HTTP request they arrived on. + /// </summary> + [ContractClass(typeof(IHttpDirectRequestContract))] + public interface IHttpDirectRequest : IMessage { + /// <summary> + /// 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; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs new file mode 100644 index 0000000..cfde6cf --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// <copyright file="IHttpDirectRequestContract.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.Linq; + using System.Net; + using System.Text; + + /// <summary> + /// Contract class for the <see cref="IHttpDirectRequest"/> interface. + /// </summary> + [ContractClassFor(typeof(IHttpDirectRequest))] + public abstract class IHttpDirectRequestContract : IHttpDirectRequest { + #region IHttpDirectRequest Members + + /// <summary> + /// Gets the HTTP headers of the request. + /// </summary> + /// <value>May be an empty collection, but must not be <c>null</c>.</value> + WebHeaderCollection IHttpDirectRequest.Headers { + get { + Contract.Ensures(Contract.Result<WebHeaderCollection>() != null); + throw new NotImplementedException(); + } + } + + #endregion + + #region IMessage Members + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + Version IMessage.Version { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + IDictionary<string, string> IMessage.ExtraData { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + void IMessage.EnsureValidMessage() { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs b/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs index 6fd95ee..8ef9b7e 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs @@ -101,6 +101,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets or sets a value indicating whether the value contained by this property contains + /// sensitive information that should generally not be logged. + /// </summary> + /// <value> + /// <c>true</c> if this instance is security sensitive; otherwise, <c>false</c>. + /// </value> + public bool IsSecuritySensitive { get; set; } + + /// <summary> /// Gets or sets the minimum version of the protocol this attribute applies to /// and overrides any attributes with lower values for this property. /// </summary> diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs index bdca190..15df48a 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs @@ -74,7 +74,7 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Reads the data from a message instance and writes a XML/JSON encoding of it. + /// Reads the data from a message instance and writes an XML/JSON encoding of it. /// </summary> /// <param name="messageDictionary">The message to be serialized.</param> /// <param name="writer">The writer to use for the serialized form.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs index 2fe273f..4f89589 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.239 +// Runtime Version:4.0.30319.18010 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -106,6 +106,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to Decoding failed due to data corruption.. + /// </summary> + internal static string DataCorruptionDetected { + get { + return ResourceManager.GetString("DataCorruptionDetected", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to An instance of type {0} was expected, but received unexpected derived type {1}.. /// </summary> internal static string DerivedTypeNotExpected { diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx index fbdb63d..15ca046 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx @@ -112,10 +112,10 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="ArgumentPropertyMissing" xml:space="preserve"> <value>Argument's {0}.{1} property is required but is empty or null.</value> @@ -333,4 +333,7 @@ <data name="UnexpectedBufferLength" xml:space="preserve"> <value>Unexpected buffer length.</value> </data> + <data name="DataCorruptionDetected" xml:space="preserve"> + <value>Decoding failed due to data corruption.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index 084403a..7aa4469 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -15,7 +15,11 @@ namespace DotNetOpenAuth.Messaging { using System.IO.Compression; using System.Linq; using System.Net; +#if CLR4 + using System.Net.Http; +#endif using System.Net.Mime; + using System.Runtime.Serialization.Json; using System.Security; using System.Security.Cryptography; using System.Text; @@ -136,6 +140,21 @@ namespace DotNetOpenAuth.Messaging { }; /// <summary> + /// The available compression algorithms. + /// </summary> + internal enum CompressionMethod { + /// <summary> + /// The Deflate algorithm. + /// </summary> + Deflate, + + /// <summary> + /// The GZip algorithm. + /// </summary> + Gzip, + } + + /// <summary> /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. /// </summary> /// <param name="response">The response to send to the user agent.</param> @@ -145,6 +164,29 @@ namespace DotNetOpenAuth.Messaging { return new OutgoingWebResponseActionResult(response); } +#if CLR4 + /// <summary> + /// Transforms an OutgoingWebResponse to a Web API-friendly HttpResponseMessage. + /// </summary> + /// <param name="outgoingResponse">The response to send to the user agent.</param> + /// <returns>The <see cref="HttpResponseMessage"/> instance to be returned by the Web API method.</returns> + public static HttpResponseMessage AsHttpResponseMessage(this OutgoingWebResponse outgoingResponse) { + HttpResponseMessage response = new HttpResponseMessage(outgoingResponse.Status); + if (outgoingResponse.ResponseStream != null) { + response.Content = new StreamContent(outgoingResponse.ResponseStream); + } + + var responseHeaders = outgoingResponse.Headers; + foreach (var header in responseHeaders.AllKeys) { + if (!response.Headers.TryAddWithoutValidation(header, responseHeaders[header])) { + response.Content.Headers.TryAddWithoutValidation(header, responseHeaders[header]); + } + } + + return response; + } +#endif + /// <summary> /// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any. /// Cookieless session directory (if applicable) is also included. @@ -291,6 +333,56 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Compares to string values for ordinal equality in such a way that its execution time does not depend on how much of the value matches. + /// </summary> + /// <param name="value1">The first value.</param> + /// <param name="value2">The second value.</param> + /// <returns>A value indicating whether the two strings share ordinal equality.</returns> + /// <remarks> + /// In signature equality checks, a difference in execution time based on how many initial characters match MAY + /// be used as an attack to figure out the expected signature. It is therefore important to make a signature + /// equality check's execution time independent of how many characters match the expected value. + /// See http://codahale.com/a-lesson-in-timing-attacks/ for more information. + /// </remarks> + public static bool EqualsConstantTime(string value1, string value2) { + // If exactly one value is null, they don't match. + if (value1 == null ^ value2 == null) { + return false; + } + + // If both values are null (since if one is at this point then they both are), it's a match. + if (value1 == null) { + return true; + } + + if (value1.Length != value2.Length) { + return false; + } + + // This looks like a pretty crazy way to compare values, but it provides a constant time equality check, + // and is more resistant to compiler optimizations than simply setting a boolean flag and returning the boolean after the loop. + int result = 0; + for (int i = 0; i < value1.Length; i++) { + result |= value1[i] ^ value2[i]; + } + + return result == 0; + } + + /// <summary> + /// Gets the URL to the root of a web site, which may include a virtual directory path. + /// </summary> + /// <returns>An absolute URI.</returns> + internal static Uri GetWebRoot() { + HttpRequestBase requestInfo = new HttpRequestWrapper(HttpContext.Current.Request); + UriBuilder realmUrl = new UriBuilder(requestInfo.GetPublicFacingUrl()); + realmUrl.Path = HttpContext.Current.Request.ApplicationPath; + realmUrl.Query = null; + realmUrl.Fragment = null; + return realmUrl.Uri; + } + + /// <summary> /// Creates the XML reader settings to use for reading XML from untrusted sources. /// </summary> /// <returns> @@ -731,7 +823,7 @@ namespace DotNetOpenAuth.Messaging { using (var encryptedStream = new MemoryStream(buffer)) { var encryptedStreamReader = new BinaryReader(encryptedStream); - byte[] encryptedPrequel = encryptedStreamReader.ReadBytes(encryptedStreamReader.ReadInt32()); + byte[] encryptedPrequel = encryptedStreamReader.ReadBuffer(4096); byte[] prequel = crypto.Decrypt(encryptedPrequel, false); using (var symmetricCrypto = new RijndaelManaged()) { @@ -779,6 +871,12 @@ namespace DotNetOpenAuth.Messaging { var cryptoKeyPair = cryptoKeyStore.GetKeys(bucket).FirstOrDefault(pair => pair.Value.Key.Length == keySize / 8); if (cryptoKeyPair.Value == null || cryptoKeyPair.Value.ExpiresUtc < DateTime.UtcNow + minimumRemainingLife) { // No key exists with enough remaining life for the required purpose. Create a new key. + if (cryptoKeyPair.Value == null) { + Logger.Messaging.InfoFormat("{0}.GetKeys returned no keys for bucket \"{1}\" with the required key length of {2} bits. A new key will be created", typeof(ICryptoKeyStore), bucket, keySize); + } else { + Logger.Messaging.InfoFormat("The first key returned by {0}.GetKeys for bucket \"{1}\" with the required key length of {2} bits was too near expiry to use. A new key will be created", typeof(ICryptoKeyStore), bucket, keySize); + } + ErrorUtilities.VerifyHost(minimumRemainingLife <= SymmetricSecretKeyLifespan, "Unable to create a new symmetric key with the required lifespan of {0} because it is beyond the limit of {1}.", minimumRemainingLife, SymmetricSecretKeyLifespan); byte[] secret = GetCryptoRandomData(keySize / 8); DateTime expires = DateTime.UtcNow + SymmetricSecretKeyLifespan; @@ -793,6 +891,7 @@ namespace DotNetOpenAuth.Messaging { cryptoKeyStore.StoreKey(bucket, handle, cryptoKey); } catch (CryptoKeyCollisionException) { ErrorUtilities.VerifyInternal(++failedAttempts < 3, "Unable to derive a unique handle to a private symmetric key."); + Logger.Messaging.Warn("A randomly generated crypto key handle collided with an existing handle. Another randomly generated handle will be attempted till the retry count is met."); goto tryAgain; } } @@ -804,19 +903,36 @@ namespace DotNetOpenAuth.Messaging { /// Compresses a given buffer. /// </summary> /// <param name="buffer">The buffer to compress.</param> + /// <param name="method">The compression algorithm to use.</param> /// <returns>The compressed data.</returns> [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")] [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - internal static byte[] Compress(byte[] buffer) { + internal static byte[] Compress(byte[] buffer, CompressionMethod method = CompressionMethod.Deflate) { Requires.NotNull(buffer, "buffer"); Contract.Ensures(Contract.Result<byte[]>() != null); using (var ms = new MemoryStream()) { - using (var compressingStream = new DeflateStream(ms, CompressionMode.Compress, true)) { + Stream compressingStream = null; + try { + switch (method) { + case CompressionMethod.Deflate: + compressingStream = new DeflateStream(ms, CompressionMode.Compress, true); + break; + case CompressionMethod.Gzip: + compressingStream = new GZipStream(ms, CompressionMode.Compress, true); + break; + default: + Requires.InRange(false, "method"); + break; + } + compressingStream.Write(buffer, 0, buffer.Length); + return ms.ToArray(); + } finally { + if (compressingStream != null) { + compressingStream.Dispose(); + } } - - return ms.ToArray(); } } @@ -824,17 +940,35 @@ namespace DotNetOpenAuth.Messaging { /// Decompresses a given buffer. /// </summary> /// <param name="buffer">The buffer to decompress.</param> + /// <param name="method">The compression algorithm used.</param> /// <returns>The decompressed data.</returns> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")] - internal static byte[] Decompress(byte[] buffer) { + internal static byte[] Decompress(byte[] buffer, CompressionMethod method = CompressionMethod.Deflate) { Requires.NotNull(buffer, "buffer"); Contract.Ensures(Contract.Result<byte[]>() != null); using (var compressedDataStream = new MemoryStream(buffer)) { using (var decompressedDataStream = new MemoryStream()) { - using (var decompressingStream = new DeflateStream(compressedDataStream, CompressionMode.Decompress, true)) { + Stream decompressingStream = null; + try { + switch (method) { + case CompressionMethod.Deflate: + decompressingStream = new DeflateStream(compressedDataStream, CompressionMode.Decompress, true); + break; + case CompressionMethod.Gzip: + decompressingStream = new GZipStream(compressedDataStream, CompressionMode.Decompress, true); + break; + default: + Requires.InRange(false, "method"); + break; + } + decompressingStream.CopyTo(decompressedDataStream); + } finally { + if (decompressingStream != null) { + decompressingStream.Dispose(); + } } return decompressedDataStream.ToArray(); @@ -881,7 +1015,7 @@ namespace DotNetOpenAuth.Messaging { missingPaddingCharacters = 0; break; default: - throw ErrorUtilities.ThrowInternal("No more than two padding characters should be present for base64."); + throw new ProtocolException(MessagingStrings.DataCorruptionDetected, new ArgumentException("No more than two padding characters should be present for base64.")); } var builder = new StringBuilder(base64WebSafe, base64WebSafe.Length + missingPaddingCharacters); builder.Replace('-', '+').Replace('_', '/'); @@ -891,43 +1025,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Compares to string values for ordinal equality in such a way that its execution time does not depend on how much of the value matches. - /// </summary> - /// <param name="value1">The first value.</param> - /// <param name="value2">The second value.</param> - /// <returns>A value indicating whether the two strings share ordinal equality.</returns> - /// <remarks> - /// In signature equality checks, a difference in execution time based on how many initial characters match MAY - /// be used as an attack to figure out the expected signature. It is therefore important to make a signature - /// equality check's execution time independent of how many characters match the expected value. - /// See http://codahale.com/a-lesson-in-timing-attacks/ for more information. - /// </remarks> - internal static bool EqualsConstantTime(string value1, string value2) { - // If exactly one value is null, they don't match. - if (value1 == null ^ value2 == null) { - return false; - } - - // If both values are null (since if one is at this point then they both are), it's a match. - if (value1 == null) { - return true; - } - - if (value1.Length != value2.Length) { - return false; - } - - // This looks like a pretty crazy way to compare values, but it provides a constant time equality check, - // and is more resistant to compiler optimizations than simply setting a boolean flag and returning the boolean after the loop. - int result = 0; - for (int i = 0; i < value1.Length; i++) { - result |= value1[i] ^ value2[i]; - } - - return result == 0; - } - - /// <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" /> @@ -1588,10 +1685,17 @@ namespace DotNetOpenAuth.Messaging { /// Reads a buffer that is prefixed with its own length. /// </summary> /// <param name="reader">The binary reader positioned at the buffer length.</param> + /// <param name="maxBufferSize"> + /// The maximum size of the buffer that should be permitted. + /// Although the stream will indicate the size of the buffer, this mitigates data corruption + /// or DoS attacks causing the web server to allocate too much memory for a small data packet. + /// </param> /// <returns>The read buffer.</returns> - internal static byte[] ReadBuffer(this BinaryReader reader) { + internal static byte[] ReadBuffer(this BinaryReader reader, int maxBufferSize) { Requires.NotNull(reader, "reader"); + Requires.InRange(maxBufferSize > 0 && maxBufferSize < 1024 * 1024, "maxBufferSize"); int length = reader.ReadInt32(); + ErrorUtilities.VerifyProtocol(length <= maxBufferSize, MessagingStrings.DataCorruptionDetected); byte[] buffer = new byte[length]; ErrorUtilities.VerifyProtocol(reader.Read(buffer, 0, length) == length, MessagingStrings.UnexpectedBufferLength); return buffer; @@ -1623,6 +1727,68 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Serializes the given message as a JSON string. + /// </summary> + /// <param name="message">The message to serialize.</param> + /// <param name="messageDescriptions">The cached message descriptions to use for reflection.</param> + /// <returns>A JSON string.</returns> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] + internal static string SerializeAsJson(IMessage message, MessageDescriptionCollection messageDescriptions) { + Requires.NotNull(message, "message"); + Requires.NotNull(messageDescriptions, "messageDescriptions"); + + var encoding = Encoding.UTF8; + var bytes = SerializeAsJsonBytes(message, messageDescriptions, encoding); + string json = encoding.GetString(bytes); + return json; + } + + /// <summary> + /// Serializes the given message as a JSON string. + /// </summary> + /// <param name="message">The message to serialize.</param> + /// <param name="messageDescriptions">The cached message descriptions to use for reflection.</param> + /// <param name="encoding">The encoding to use. Defaults to <see cref="Encoding.UTF8"/></param> + /// <returns>A JSON string.</returns> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] + internal static byte[] SerializeAsJsonBytes(IMessage message, MessageDescriptionCollection messageDescriptions, Encoding encoding = null) { + Requires.NotNull(message, "message"); + Requires.NotNull(messageDescriptions, "messageDescriptions"); + + encoding = encoding ?? Encoding.UTF8; + MessageDictionary messageDictionary = messageDescriptions.GetAccessor(message); + using (var memoryStream = new MemoryStream()) { + using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream, encoding)) { + MessageSerializer.Serialize(messageDictionary, jsonWriter); + jsonWriter.Flush(); + } + + return memoryStream.ToArray(); + } + } + + /// <summary> + /// Deserializes a JSON object into a message. + /// </summary> + /// <param name="jsonBytes">The buffer containing the JSON string.</param> + /// <param name="receivingMessage">The message to deserialize the object into.</param> + /// <param name="messageDescriptions">The cache of message descriptions.</param> + /// <param name="encoding">The encoding that the JSON bytes are in.</param> + internal static void DeserializeFromJson(byte[] jsonBytes, IMessage receivingMessage, MessageDescriptionCollection messageDescriptions, Encoding encoding = null) { + Requires.NotNull(jsonBytes, "jsonBytes"); + Requires.NotNull(receivingMessage, "receivingMessage"); + Requires.NotNull(messageDescriptions, "messageDescriptions"); + + encoding = encoding ?? Encoding.UTF8; + MessageDictionary messageDictionary = messageDescriptions.GetAccessor(receivingMessage); + using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(jsonBytes, 0, jsonBytes.Length, encoding, Channel.DefaultUntrustedXmlDictionaryReaderQuotas, null)) { + MessageSerializer.Deserialize(messageDictionary, jsonReader); + } + } + + /// <summary> /// Prepares what SHOULD be simply a string value for safe injection into Javascript /// by using appropriate character escaping. /// </summary> @@ -1761,7 +1927,8 @@ namespace DotNetOpenAuth.Messaging { // the public URL: if (serverVariables["HTTP_HOST"] != 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; + string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? + (string.Equals(serverVariables["HTTP_FRONT_END_HTTPS"], "on", StringComparison.OrdinalIgnoreCase) ? Uri.UriSchemeHttps : request.Url.Scheme); Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); UriBuilder publicRequestUri = new UriBuilder(request.Url); publicRequestUri.Scheme = scheme; diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs index 67eccce..edfbc7d 100644 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs @@ -9,9 +9,11 @@ namespace DotNetOpenAuth.Messaging { using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Globalization; using System.IO; using System.Net; using System.Net.Mime; + using System.ServiceModel.Web; using System.Text; using System.Threading; using System.Web; @@ -213,6 +215,23 @@ namespace DotNetOpenAuth.Messaging { } /// <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> @@ -282,6 +301,7 @@ namespace DotNetOpenAuth.Messaging { writer.Write(body); writer.Flush(); this.ResponseStream.Seek(0, SeekOrigin.Begin); + this.Headers[HttpResponseHeader.ContentLength] = this.ResponseStream.Length.ToString(CultureInfo.InvariantCulture); } /// <summary> diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs index a5fe782..7691cc4 100644 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs @@ -35,6 +35,11 @@ namespace DotNetOpenAuth.Messaging { /// <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/ProtocolException.cs b/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs index e26d15e..982e1c0 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs @@ -42,10 +42,10 @@ namespace DotNetOpenAuth.Messaging { /// such that it can be sent as a protocol message response to a remote caller. /// </summary> /// <param name="message">The human-readable exception message.</param> - /// <param name="faultedMessage">The message that was the cause of the exception. Must not be null.</param> - protected internal ProtocolException(string message, IProtocolMessage faultedMessage) - : base(message) { - Requires.NotNull(faultedMessage, "faultedMessage"); + /// <param name="faultedMessage">The message that was the cause of the exception. May be null.</param> + /// <param name="innerException">The inner exception to include.</param> + protected internal ProtocolException(string message, IProtocolMessage faultedMessage, Exception innerException = null) + : base(message, innerException) { this.FaultedMessage = faultedMessage; } diff --git a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs new file mode 100644 index 0000000..c2dc34e --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="ProtocolFaultResponseException.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; + + /// <summary> + /// An exception to represent errors in the local or remote implementation of the protocol + /// that includes the response message that should be returned to the HTTP client to comply + /// with the protocol specification. + /// </summary> + public class ProtocolFaultResponseException : ProtocolException { + /// <summary> + /// The channel that produced the error response message, to be used in constructing the actual HTTP response. + /// </summary> + private readonly Channel channel; + + /// <summary> + /// Initializes a new instance of the <see cref="ProtocolFaultResponseException"/> class + /// such that it can be sent as a protocol message response to a remote caller. + /// </summary> + /// <param name="channel">The channel to use when encoding the response message.</param> + /// <param name="errorResponse">The message to send back to the HTTP client.</param> + /// <param name="faultedMessage">The message that was the cause of the exception. May be null.</param> + /// <param name="innerException">The inner exception.</param> + /// <param name="message">The message for the exception.</param> + protected internal ProtocolFaultResponseException(Channel channel, IDirectResponseProtocolMessage errorResponse, IProtocolMessage faultedMessage = null, Exception innerException = null, string message = null) + : base(message ?? (innerException != null ? innerException.Message : null), faultedMessage, innerException) { + Requires.NotNull(channel, "channel"); + Requires.NotNull(errorResponse, "errorResponse"); + this.channel = channel; + this.ErrorResponseMessage = errorResponse; + } + + /// <summary> + /// Initializes a new instance of the <see cref="ProtocolFaultResponseException"/> class. + /// </summary> + /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/> + /// that holds the serialized object data about the exception being thrown.</param> + /// <param name="context">The System.Runtime.Serialization.StreamingContext + /// that contains contextual information about the source or destination.</param> + protected ProtocolFaultResponseException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { + throw new NotImplementedException(); + } + + /// <summary> + /// Gets the protocol message to send back to the client to report the error. + /// </summary> + public IDirectResponseProtocolMessage ErrorResponseMessage { get; private set; } + + /// <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; + } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs index b2c4664..0f140d6 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs @@ -115,6 +115,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { this.RequiredProtection = attribute.RequiredProtection; this.IsRequired = attribute.IsRequired; this.AllowEmpty = attribute.AllowEmpty; + this.IsSecuritySensitive = attribute.IsSecuritySensitive; this.memberDeclaredType = (this.field != null) ? this.field.FieldType : this.property.PropertyType; this.defaultMemberValue = DeriveDefaultValue(this.memberDeclaredType); @@ -189,6 +190,15 @@ namespace DotNetOpenAuth.Messaging.Reflection { internal bool IsConstantValueAvailableStatically { get; set; } /// <summary> + /// Gets or sets a value indicating whether the value contained by this property contains + /// sensitive information that should generally not be logged. + /// </summary> + /// <value> + /// <c>true</c> if this instance is security sensitive; otherwise, <c>false</c>. + /// </value> + internal bool IsSecuritySensitive { get; set; } + + /// <summary> /// Gets the static constant value for this message part without a message instance. /// </summary> internal string StaticConstantValue { diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs index 7a1d194..7ca5d45 100644 --- a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs @@ -31,7 +31,10 @@ namespace DotNetOpenAuth.Messaging { /// </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 apply to the channel.</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) { Requires.NotNull(messageTypes, "messageTypes"); diff --git a/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs index 92b1928..242175e 100644 --- a/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs +++ b/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs @@ -20,7 +20,7 @@ namespace DotNetOpenAuth.Messaging { /// A serializer for <see cref="DataBag"/>-derived types /// </summary> /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam> - internal class UriStyleMessageFormatter<T> : DataBagFormatterBase<T> where T : DataBag, new() { + internal class UriStyleMessageFormatter<T> : DataBagFormatterBase<T> where T : DataBag { /// <summary> /// Initializes a new instance of the <see cref="UriStyleMessageFormatter<T>"/> class. /// </summary> |