Permalink
Browse files

Add MaxHttpCollectionKey support

  • Loading branch information...
1 parent f2081e0 commit 22878f780e03c32d7b22d7f56501db013f4f0cb7 hongmeig committed with bradwilson Apr 12, 2012
View
58 src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs
@@ -3,6 +3,8 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
@@ -19,9 +21,15 @@ namespace System.Net.Http.Formatting
/// </summary>
public abstract class MediaTypeFormatter
{
+ private const int DefaultMinHttpCollectionKeys = 1;
+ private const int DefaultMaxHttpCollectionKeys = 1000; // same default as ASPNET
+ private const string IWellKnownComparerTypeName = "System.IWellKnownStringEqualityComparer, mscorlib, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089";
+
private static readonly ConcurrentDictionary<Type, Type> _delegatingEnumerableCache = new ConcurrentDictionary<Type, Type>();
private static ConcurrentDictionary<Type, ConstructorInfo> _delegatingEnumerableConstructorCache = new ConcurrentDictionary<Type, ConstructorInfo>();
-
+ private static Lazy<int> _defaultMaxHttpCollectionKeys = new Lazy<int>(InitializeDefaultCollectionKeySize, true); // Max number of keys is 1000
+ private static int _maxHttpCollectionKeys = -1;
+
/// <summary>
/// Initializes a new instance of the <see cref="MediaTypeFormatter"/> class.
/// </summary>
@@ -33,6 +41,31 @@ protected MediaTypeFormatter()
}
/// <summary>
+ /// Gets or sets the maximum number of keys stored in a NameValueCollection.
+ /// </summary>
+ public static int MaxHttpCollectionKeys
+ {
+ get
+ {
+ if (_maxHttpCollectionKeys < 0)
+ {
+ _maxHttpCollectionKeys = _defaultMaxHttpCollectionKeys.Value;
+ }
+
+ return _maxHttpCollectionKeys;
+ }
+ set
+ {
+ if (value < DefaultMinHttpCollectionKeys)
+ {
+ throw new ArgumentOutOfRangeException("value", value, RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, DefaultMinHttpCollectionKeys));
+ }
+
+ _maxHttpCollectionKeys = value;
+ }
+ }
+
+ /// <summary>
/// Gets the mutable collection of <see cref="MediaTypeHeaderValue"/> elements supported by
/// this <see cref="MediaTypeFormatter"/> instance.
/// </summary>
@@ -119,6 +152,29 @@ private static bool TryGetDelegatingType(Type interfaceType, ref Type type)
return false;
}
+ private static int InitializeDefaultCollectionKeySize()
+ {
+ // we first detect if we are running on 4.5, return Max value if we are.
+ Type comparerType = Type.GetType(IWellKnownComparerTypeName, throwOnError: false);
+
+ if (comparerType != null)
+ {
+ return Int32.MaxValue;
+ }
+
+ // we should try to read it from the AppSettings
+ // if we found the aspnet settings configured, we will use that. Otherwise, we used the default
+ NameValueCollection settings = ConfigurationManager.AppSettings;
+ int result;
+
+ if (settings == null || !Int32.TryParse(settings["aspnet:MaxHttpCollectionKeys"], out result) || result < 0)
+ {
+ result = DefaultMaxHttpCollectionKeys;
+ }
+
+ return result;
+ }
+
/// <summary>
/// This method converts <see cref="IEnumerable{T}"/> (and interfaces that mandate it) to a <see cref="DelegatingEnumerable{T}"/> for serialization purposes.
/// </summary>
View
15 src/System.Net.Http.Formatting/Internal/HttpValueCollection.cs
@@ -5,6 +5,7 @@
using System.Net.Http.Internal;
using System.Runtime.Serialization;
using System.Text;
+using System.Web.Http;
namespace System.Net.Http.Formatting.Internal
{
@@ -15,7 +16,7 @@ namespace System.Net.Http.Formatting.Internal
internal class HttpValueCollection : NameValueCollection
{
private HttpValueCollection()
- : base(StringComparer.Ordinal) // case-sensitive keys
+ : base(StringComparer.OrdinalIgnoreCase) // case-insensitive keys
{
}
@@ -30,9 +31,11 @@ public static NameValueCollection Create(IEnumerable<KeyValuePair<string, string
var nvc = new HttpValueCollection();
// Ordering example:
- // k=A&j=B&k=C --> k:[A,C];j=[B].
+ // k=A&j=B&k=C --> k:[A,C];j=[B].
foreach (KeyValuePair<string, string> kv in pairs)
{
+ ThrowIfMaxHttpCollectionKeysExceeded(nvc.Count);
+
string key = kv.Key;
if (key == null)
{
@@ -50,6 +53,14 @@ public static NameValueCollection Create(IEnumerable<KeyValuePair<string, string
return nvc;
}
+ private static void ThrowIfMaxHttpCollectionKeysExceeded(int count)
+ {
+ if (count >= MediaTypeFormatter.MaxHttpCollectionKeys)
+ {
+ throw Error.InvalidOperation(System.Net.Http.Properties.Resources.MaxHttpCollectionKeyLimitReached, MediaTypeFormatter.MaxHttpCollectionKeys, typeof(MediaTypeFormatter));
+ }
+ }
+
protected HttpValueCollection(SerializationInfo info, StreamingContext context)
: base(info, context)
{
View
11 src/System.Net.Http.Formatting/Properties/Resources.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.488
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -286,6 +286,15 @@ internal class Resources {
}
/// <summary>
+ /// Looks up a localized string similar to The number of keys in a NameValueCollection has exceeded the limit of &apos;{0}&apos;. You can adjust it by modifying the MaxHttpCollectionKeys property on the &apos;{1}&apos; class..
+ /// </summary>
+ internal static string MaxHttpCollectionKeyLimitReached {
+ get {
+ return ResourceManager.GetString("MaxHttpCollectionKeyLimitReached", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The media type formatter of type &apos;{0}&apos; does not support reading because it does not implement the ReadFromStreamAsync method..
/// </summary>
internal static string MediaTypeFormatterCannotRead {
View
3 src/System.Net.Http.Formatting/Properties/Resources.resx
@@ -279,4 +279,7 @@
<data name="CookieNull" xml:space="preserve">
<value>Cookie cannot be null.</value>
</data>
+ <data name="MaxHttpCollectionKeyLimitReached" xml:space="preserve">
+ <value>The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.</value>
+ </data>
</root>
View
1 src/System.Net.Http.Formatting/System.Net.Http.Formatting.csproj
@@ -51,6 +51,7 @@
<HintPath>..\..\packages\Newtonsoft.Json.4.5.1\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
+ <Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.0.20326.1\lib\net40\System.Net.Http.dll</HintPath>
View
12 src/System.Web.Http/Internal/UriQueryUtility.cs
@@ -4,8 +4,10 @@
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
+using System.Net.Http.Formatting;
using System.Runtime.Serialization;
using System.Text;
+using System.Web.Http.Properties;
namespace System.Web.Http.Internal
{
@@ -64,6 +66,8 @@ internal void FillFromString(string s, bool urlencoded)
while (i < l)
{
+ ThrowIfMaxHttpCollectionKeysExceeded();
+
// find next & while noting first = on the way (and if there are more)
int si = i;
int ti = -1;
@@ -194,6 +198,14 @@ string ToString(bool urlencoded, IDictionary excludeKeys)
return s.ToString();
}
+
+ private void ThrowIfMaxHttpCollectionKeysExceeded()
+ {
+ if (Count >= MediaTypeFormatter.MaxHttpCollectionKeys)
+ {
+ throw Error.InvalidOperation(SRResources.MaxHttpCollectionKeyLimitReached, MediaTypeFormatter.MaxHttpCollectionKeys, typeof(MediaTypeFormatter));
+ }
+ }
}
// The implementation below is ported from WebUtility for use in .Net 4
View
12 src/System.Web.Http/ModelBinding/FormDataCollectionExtensions.cs
@@ -86,16 +86,26 @@ internal static NameValueCollection GetJQueryValueNameValueCollection(this FormD
throw Error.ArgumentNull("formData");
}
- NameValueCollection nvc = new NameValueCollection();
+ NameValueCollection nvc = new NameValueCollection(StringComparer.OrdinalIgnoreCase);
foreach (var kv in formData)
{
+ ThrowIfMaxHttpCollectionKeysExceeded(nvc.Count);
+
string key = NormalizeJQueryToMvc(kv.Key);
string value = kv.Value ?? String.Empty;
nvc.Add(key, value);
}
return nvc;
}
+ private static void ThrowIfMaxHttpCollectionKeysExceeded(int count)
+ {
+ if (count >= MediaTypeFormatter.MaxHttpCollectionKeys)
+ {
+ throw Error.InvalidOperation(SRResources.MaxHttpCollectionKeyLimitReached, MediaTypeFormatter.MaxHttpCollectionKeys, typeof(MediaTypeFormatter));
+ }
+ }
+
// Create a IValueProvider for the given form, assuming a JQuery syntax.
internal static IValueProvider GetJQueryValueProvider(this FormDataCollection formData)
{
View
11 src/System.Web.Http/Properties/SRResources.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.488
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -495,6 +495,15 @@ internal class SRResources {
}
/// <summary>
+ /// Looks up a localized string similar to The number of keys in a NameValueCollection has exceeded the limit of &apos;{0}&apos;. You can adjust it by modifying the MaxHttpCollectionKeys property on the &apos;{1}&apos; class..
+ /// </summary>
+ internal static string MaxHttpCollectionKeyLimitReached {
+ get {
+ return ResourceManager.GetString("MaxHttpCollectionKeyLimitReached", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Method &apos;{0}&apos; in type &apos;{1}&apos; does not return a value.
/// </summary>
internal static string MethodIsVoid {
View
3 src/System.Web.Http/Properties/SRResources.resx
@@ -476,4 +476,7 @@ The request for '{0}' has found the following matching controllers:{2}</value>
<data name="DefaultServices_InvalidServiceType" xml:space="preserve">
<value>The service type {0} is not supported.</value>
</data>
+ <data name="MaxHttpCollectionKeyLimitReached" xml:space="preserve">
+ <value>The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.</value>
+ </data>
</root>
View
6 test/System.Net.Http.Formatting.Test.Unit/Formatting/FormDataCollectionTests.cs
@@ -97,15 +97,15 @@ public void GetValues()
}
[Fact]
- public void CaseSensitive()
+ public void CaseInSensitive()
{
- FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1&X=2"));
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1&Y=2"));
NameValueCollection nvc = form.ReadAsNameValueCollection();
Assert.Equal(2, nvc.Count);
Assert.Equal("1", nvc.Get("x"));
- Assert.Equal("2", nvc.Get("X"));
+ Assert.Equal("2", nvc.Get("y"));
}
[Fact]
View
14 test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterTests.cs
@@ -41,6 +41,20 @@ public void Constructor()
}
[Fact]
+ public void MaxCollectionKeySize_RoundTrips()
+ {
+ Assert.Reflection.IntegerProperty<MediaTypeFormatter, int>(
+ null,
+ c => MediaTypeFormatter.MaxHttpCollectionKeys,
+ expectedDefaultValue: 1000,
+ minLegalValue: 1,
+ illegalLowerValue: 0,
+ maxLegalValue: null,
+ illegalUpperValue: null,
+ roundTripTestValue: 125);
+ }
+
+ [Fact]
[Trait("Description", "SupportedMediaTypes is a mutable collection.")]
public void SupportedMediaTypesIsMutable()
{
View
244 test/System.Web.Http.SelfHost.Test/MaxHttpCollectionKeyTests.cs
@@ -0,0 +1,244 @@
+// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using Newtonsoft.Json.Linq;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.SelfHost
+{
+ public class MaxHttpCollectionKeyTests
+ {
+ private HttpServer server = null;
+ private string baseAddress = null;
+ private HttpClient httpClient = null;
+
+ public MaxHttpCollectionKeyTests()
+ {
+ this.SetupHost();
+ }
+
+ public void SetupHost()
+ {
+ baseAddress = String.Format("http://localhost/");
+
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "MaxHttpCollectionKeyType" });
+
+ server = new HttpServer(config);
+
+ httpClient = new HttpClient(server);
+ }
+
+ [Theory]
+ [InlineData("PostCustomer")]
+ [InlineData("PostFormData")]
+ public void PostManyKeysInFormUrlEncodedThrows(string actionName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(Path.Combine(baseAddress, "MaxHttpCollectionKeyType/" + actionName));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+ request.Method = HttpMethod.Post;
+ request.Content = new StringContent(GenerateHttpCollectionKeyInput(100), UTF8Encoding.UTF8, "application/x-www-form-urlencoded");
+ MediaTypeFormatter.MaxHttpCollectionKeys = 99;
+
+ // Action
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ string expectedResponseValue = @"The number of keys in a NameValueCollection has exceeded the limit of '99'. You can adjust it by modifying the MaxHttpCollectionKeys property on the 'System.Net.Http.Formatting.MediaTypeFormatter' class.";
+ Assert.Equal(expectedResponseValue, response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Theory]
+ [InlineData("PostCustomer")]
+ [InlineData("PostFormData")]
+ public void PostNotTooManyKeysInFormUrlEncodedWorks(string actionName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(Path.Combine(baseAddress, "MaxHttpCollectionKeyType/" + actionName));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+ request.Method = HttpMethod.Post;
+ request.Content = new StringContent(GenerateHttpCollectionKeyInput(100), UTF8Encoding.UTF8, "application/x-www-form-urlencoded");
+ MediaTypeFormatter.MaxHttpCollectionKeys = 1000;
+
+ // Action
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string expectedResponseValue = @"<string xmlns=""http://schemas.microsoft.com/2003/10/Serialization/"">success from " + actionName + "</string>";
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("application/xml", response.Content.Headers.ContentType.MediaType);
+ Assert.Equal(expectedResponseValue, response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Theory]
+ [InlineData("PostCustomerFromUri")]
+ [InlineData("GetWithQueryable")]
+ public void PostManyKeysInUriThrows(string actionName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(Path.Combine(baseAddress, "MaxHttpCollectionKeyType/" + actionName + "/?" + GenerateHttpCollectionKeyInput(100)));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+
+ if (actionName.StartsWith("Post"))
+ {
+ request.Method = HttpMethod.Post;
+ request.Content = new StringContent("", UTF8Encoding.UTF8, "application/x-www-form-urlencoded");
+ }
+ else
+ {
+ request.Method = HttpMethod.Get;
+ }
+
+ MediaTypeFormatter.MaxHttpCollectionKeys = 99;
+
+ // Action
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("PostCustomerFromUri")]
+ [InlineData("GetWithQueryable")]
+ public void PostNotTooManyKeysInUriWorks(string actionName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(Path.Combine(baseAddress, "MaxHttpCollectionKeyType/" + actionName + "/?" + GenerateHttpCollectionKeyInput(100)));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+
+ if (actionName.StartsWith("Post"))
+ {
+ request.Method = HttpMethod.Post;
+ request.Content = new StringContent("", UTF8Encoding.UTF8, "application/x-www-form-urlencoded");
+ }
+ else
+ {
+ request.Method = HttpMethod.Get;
+ }
+
+ MediaTypeFormatter.MaxHttpCollectionKeys = 1000;
+
+ // Action
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string expectedResponseValue = @"success from " + actionName;
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("application/xml", response.Content.Headers.ContentType.MediaType);
+ Assert.True(response.Content.ReadAsStringAsync().Result.Contains(expectedResponseValue));
+ }
+
+ private static string GenerateHttpCollectionKeyInput(int num)
+ {
+ StringBuilder sb = new StringBuilder("a=0");
+
+ for (int i = 0; i < num; i++)
+ {
+ sb.Append("&");
+ sb.Append(i.ToString());
+ sb.Append("=0");
+ }
+
+ return sb.ToString();
+ }
+ }
+
+ public class MaxHttpCollectionKeyTypeController : ApiController
+ {
+ // Post strongly typed Customer
+ public string PostCustomer(Customer a)
+ {
+ if (!ModelState.IsValid)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
+ ModelBinding.ModelState value = null;
+ ModelState.TryGetValue("a", out value);
+ response.Content = new StringContent(value.Errors[0].ErrorMessage);
+ throw new HttpResponseException(response);
+ }
+
+ return "success from PostCustomer";
+ }
+
+ // Post form data
+ public string PostFormData(FormDataCollection a)
+ {
+ if (!ModelState.IsValid)
+ {
+ throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest));
+ }
+
+ try
+ {
+ NameValueCollection collection = a.ReadAsNameValueCollection();
+ }
+ catch (InvalidOperationException ex)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
+ response.Content = new StringContent(ex.Message);
+ throw new HttpResponseException(response);
+ }
+
+ return "success from PostFormData";
+ }
+
+ public string PostCustomerFromUri([FromUri]Customer a)
+ {
+ if (!ModelState.IsValid)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
+ ModelBinding.ModelState value = null;
+ ModelState.TryGetValue("a", out value);
+ response.Content = new StringContent(value.Errors[0].ErrorMessage);
+ throw new HttpResponseException(response);
+ }
+
+ return "success from PostCustomerFromUri";
+ }
+
+ [Queryable]
+ public IQueryable<string> GetWithQueryable()
+ {
+ return new List<string>(){"success from GetWithQueryable"}.AsQueryable();
+ }
+
+ public string PostJToken(JToken token)
+ {
+ if (!ModelState.IsValid)
+ {
+ throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest));
+ }
+
+ return "success from PostJToken";
+ }
+ }
+
+ public class Customer
+ {
+ public string Name { get; set; }
+ public int Age { get; set; }
+
+ public override string ToString()
+ {
+ return "ModelBindingItem(" + Name + "," + Age + ")";
+ }
+ }
+}
View
1 test/System.Web.Http.SelfHost.Test/System.Web.Http.SelfHost.Test.csproj
@@ -71,6 +71,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Compile Include="MaxHttpCollectionKeyTests.cs" />
<Compile Include="DeeplyNestedTypeTests.cs" />
<Compile Include="HttpSelfHostConfigurationTest.cs" />
<Compile Include="HttpSelfHostServerTest.cs" />

0 comments on commit 22878f7

Please sign in to comment.