#32 - Port Cookie chunking from Katana.
This commit is contained in:
parent
51c3dc98dc
commit
f6d6f31414
|
|
@ -37,8 +37,7 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
|
||||
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
|
||||
{
|
||||
IReadableStringCollection cookies = Request.Cookies;
|
||||
string cookie = cookies[Options.CookieName];
|
||||
string cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
|
||||
if (string.IsNullOrWhiteSpace(cookie))
|
||||
{
|
||||
return null;
|
||||
|
|
@ -148,7 +147,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
var model = new AuthenticationTicket(context.Identity, context.Properties);
|
||||
string cookieValue = Options.TicketDataFormat.Protect(model);
|
||||
|
||||
Response.Cookies.Append(
|
||||
Options.CookieManager.AppendResponseCookie(
|
||||
Context,
|
||||
Options.CookieName,
|
||||
cookieValue,
|
||||
cookieOptions);
|
||||
|
|
@ -162,7 +162,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
|
||||
Options.Notifications.ResponseSignOut(context);
|
||||
|
||||
Response.Cookies.Delete(
|
||||
Options.CookieManager.DeleteCookie(
|
||||
Context,
|
||||
Options.CookieName,
|
||||
cookieOptions);
|
||||
}
|
||||
|
|
@ -180,7 +181,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime;
|
||||
}
|
||||
|
||||
Response.Cookies.Append(
|
||||
Options.CookieManager.AppendResponseCookie(
|
||||
Context,
|
||||
Options.CookieName,
|
||||
cookieValue,
|
||||
cookieOptions);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Security.Cookies.Infrastructure;
|
||||
using Microsoft.AspNet.Security.DataHandler;
|
||||
using Microsoft.AspNet.Security.DataProtection;
|
||||
using Microsoft.AspNet.Security.Infrastructure;
|
||||
|
|
@ -33,6 +34,10 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
typeof(CookieAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
|
||||
options.TicketDataFormat = new TicketDataFormat(dataProtector);
|
||||
}
|
||||
if (Options.CookieManager == null)
|
||||
{
|
||||
Options.CookieManager = new ChunkingCookieManager();
|
||||
}
|
||||
|
||||
_logger = loggerFactory.Create(typeof(CookieAuthenticationMiddleware).FullName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Security.Cookies.Infrastructure;
|
||||
using Microsoft.AspNet.Security.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies
|
||||
|
|
@ -135,5 +136,12 @@ namespace Microsoft.AspNet.Security.Cookies
|
|||
/// used which calls DateTimeOffset.UtcNow. This is typically not replaced except for unit testing.
|
||||
/// </summary>
|
||||
public ISystemClock SystemClock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The component used to get cookies from the request or set them on the response.
|
||||
///
|
||||
/// ChunkingCookieManager will be used by default.
|
||||
/// </summary>
|
||||
public ICookieManager CookieManager { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,310 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
|
||||
/// from requests.
|
||||
/// </summary>
|
||||
public class ChunkingCookieManager : ICookieManager
|
||||
{
|
||||
public ChunkingCookieManager()
|
||||
{
|
||||
ChunkSize = 4090;
|
||||
ThrowForPartialCookies = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple
|
||||
/// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all
|
||||
/// common browsers.
|
||||
///
|
||||
/// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain.
|
||||
/// </summary>
|
||||
public int? ChunkSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Throw if not all chunks of a cookie are available on a request for re-assembly.
|
||||
/// </summary>
|
||||
public bool ThrowForPartialCookies { get; set; }
|
||||
|
||||
// Parse the "chunks:XX" to determine how many chunks there should be.
|
||||
private static int ParseChunksCount(string value)
|
||||
{
|
||||
if (value != null && value.StartsWith("chunks:", StringComparison.Ordinal))
|
||||
{
|
||||
string chunksCountString = value.Substring("chunks:".Length);
|
||||
int chunksCount;
|
||||
if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount))
|
||||
{
|
||||
return chunksCount;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the reassembled cookie. Non chunked cookies are returned normally.
|
||||
/// Cookies with missing chunks just have their "chunks:XX" header returned.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns>The reassembled cookie, if any, or null.</returns>
|
||||
public string GetRequestCookie(HttpContext context, string key)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException("context");
|
||||
}
|
||||
|
||||
IReadableStringCollection requestCookies = context.Request.Cookies;
|
||||
string value = requestCookies[key];
|
||||
int chunksCount = ParseChunksCount(value);
|
||||
if (chunksCount > 0)
|
||||
{
|
||||
bool quoted = false;
|
||||
string[] chunks = new string[chunksCount];
|
||||
for (int chunkId = 1; chunkId <= chunksCount; chunkId++)
|
||||
{
|
||||
string chunk = requestCookies[key + "C" + chunkId.ToString(CultureInfo.InvariantCulture)];
|
||||
if (chunk == null)
|
||||
{
|
||||
if (ThrowForPartialCookies)
|
||||
{
|
||||
int totalSize = 0;
|
||||
for (int i = 0; i < chunkId - 1; i++)
|
||||
{
|
||||
totalSize += chunks[i].Length;
|
||||
}
|
||||
throw new FormatException(
|
||||
string.Format(CultureInfo.CurrentCulture, Resources.Exception_ImcompleteChunkedCookie, chunkId - 1, chunksCount, totalSize));
|
||||
}
|
||||
// Missing chunk, abort by returning the original cookie value. It may have been a false positive?
|
||||
return value;
|
||||
}
|
||||
if (IsQuoted(chunk))
|
||||
{
|
||||
// Note: Since we assume these cookies were generated by our code, then we can assume that if one cookie has quotes then they all do.
|
||||
quoted = true;
|
||||
chunk = RemoveQuotes(chunk);
|
||||
}
|
||||
chunks[chunkId - 1] = chunk;
|
||||
}
|
||||
string merged = string.Join(string.Empty, chunks);
|
||||
if (quoted)
|
||||
{
|
||||
merged = Quote(merged);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit
|
||||
/// then it will be broken down into multiple cookies as follows:
|
||||
/// Set-Cookie: CookieName=chunks:3; path=/
|
||||
/// Set-Cookie: CookieNameC1=Segment1; path=/
|
||||
/// Set-Cookie: CookieNameC2=Segment2; path=/
|
||||
/// Set-Cookie: CookieNameC3=Segment3; path=/
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="options"></param>
|
||||
public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException("context");
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException("options");
|
||||
}
|
||||
|
||||
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||
bool expiresHasValue = options.Expires.HasValue;
|
||||
|
||||
string escapedKey = Uri.EscapeDataString(key);
|
||||
string prefix = escapedKey + "=";
|
||||
|
||||
string suffix = string.Concat(
|
||||
!domainHasValue ? null : "; domain=",
|
||||
!domainHasValue ? null : options.Domain,
|
||||
!pathHasValue ? null : "; path=",
|
||||
!pathHasValue ? null : options.Path,
|
||||
!expiresHasValue ? null : "; expires=",
|
||||
!expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT",
|
||||
!options.Secure ? null : "; secure",
|
||||
!options.HttpOnly ? null : "; HttpOnly");
|
||||
|
||||
value = value ?? string.Empty;
|
||||
bool quoted = false;
|
||||
if (IsQuoted(value))
|
||||
{
|
||||
quoted = true;
|
||||
value = RemoveQuotes(value);
|
||||
}
|
||||
string escapedValue = Uri.EscapeDataString(value);
|
||||
|
||||
// Normal cookie
|
||||
IHeaderDictionary responseHeaders = context.Response.Headers;
|
||||
if (!ChunkSize.HasValue || ChunkSize.Value > prefix.Length + escapedValue.Length + suffix.Length + (quoted ? 2 : 0))
|
||||
{
|
||||
string setCookieValue = string.Concat(
|
||||
prefix,
|
||||
quoted ? Quote(escapedValue) : escapedValue,
|
||||
suffix);
|
||||
responseHeaders.AppendValues(Constants.Headers.SetCookie, setCookieValue);
|
||||
}
|
||||
else if (ChunkSize.Value < prefix.Length + suffix.Length + (quoted ? 2 : 0) + 10)
|
||||
{
|
||||
// 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX".
|
||||
// No room for data, we can't chunk the options and name
|
||||
throw new InvalidOperationException(Resources.Exception_CookieLimitTooSmall);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Break the cookie down into multiple cookies.
|
||||
// Key = CookieName, value = "Segment1Segment2Segment2"
|
||||
// Set-Cookie: CookieName=chunks:3; path=/
|
||||
// Set-Cookie: CookieNameC1="Segment1"; path=/
|
||||
// Set-Cookie: CookieNameC2="Segment2"; path=/
|
||||
// Set-Cookie: CookieNameC3="Segment3"; path=/
|
||||
int dataSizePerCookie = ChunkSize.Value - prefix.Length - suffix.Length - (quoted ? 2 : 0) - 3; // Budget 3 chars for the chunkid.
|
||||
int cookieChunkCount = (int)Math.Ceiling(escapedValue.Length * 1.0 / dataSizePerCookie);
|
||||
|
||||
responseHeaders.AppendValues(Constants.Headers.SetCookie, prefix + "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture) + suffix);
|
||||
|
||||
string[] chunks = new string[cookieChunkCount];
|
||||
int offset = 0;
|
||||
for (int chunkId = 1; chunkId <= cookieChunkCount; chunkId++)
|
||||
{
|
||||
int remainingLength = escapedValue.Length - offset;
|
||||
int length = Math.Min(dataSizePerCookie, remainingLength);
|
||||
string segment = escapedValue.Substring(offset, length);
|
||||
offset += length;
|
||||
|
||||
chunks[chunkId - 1] = string.Concat(
|
||||
escapedKey,
|
||||
"C",
|
||||
chunkId.ToString(CultureInfo.InvariantCulture),
|
||||
"=",
|
||||
quoted ? "\"" : string.Empty,
|
||||
segment,
|
||||
quoted ? "\"" : string.Empty,
|
||||
suffix);
|
||||
}
|
||||
responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on
|
||||
/// the request, delete each chunk.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="options"></param>
|
||||
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException("context");
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException("options");
|
||||
}
|
||||
|
||||
string escapedKey = Uri.EscapeDataString(key);
|
||||
List<string> keys = new List<string>();
|
||||
keys.Add(escapedKey + "=");
|
||||
|
||||
string requestCookie = context.Request.Cookies[key];
|
||||
int chunks = ParseChunksCount(requestCookie);
|
||||
if (chunks > 0)
|
||||
{
|
||||
for (int i = 1; i <= chunks + 1; i++)
|
||||
{
|
||||
string subkey = escapedKey + "C" + i.ToString(CultureInfo.InvariantCulture);
|
||||
keys.Add(subkey + "=");
|
||||
}
|
||||
}
|
||||
|
||||
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
|
||||
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
|
||||
|
||||
Func<string, bool> rejectPredicate;
|
||||
Func<string, bool> predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase));
|
||||
if (domainHasValue)
|
||||
{
|
||||
rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
else if (pathHasValue)
|
||||
{
|
||||
rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
rejectPredicate = value => predicate(value);
|
||||
}
|
||||
|
||||
IHeaderDictionary responseHeaders = context.Response.Headers;
|
||||
IList<string> existingValues = responseHeaders.GetValues(Constants.Headers.SetCookie);
|
||||
if (existingValues != null)
|
||||
{
|
||||
responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray());
|
||||
}
|
||||
|
||||
AppendResponseCookie(
|
||||
context,
|
||||
key,
|
||||
string.Empty,
|
||||
new CookieOptions
|
||||
{
|
||||
Path = options.Path,
|
||||
Domain = options.Domain,
|
||||
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
});
|
||||
|
||||
for (int i = 1; i <= chunks; i++)
|
||||
{
|
||||
AppendResponseCookie(
|
||||
context,
|
||||
key + "C" + i.ToString(CultureInfo.InvariantCulture),
|
||||
string.Empty,
|
||||
new CookieOptions
|
||||
{
|
||||
Path = options.Path,
|
||||
Domain = options.Domain,
|
||||
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsQuoted(string value)
|
||||
{
|
||||
return value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"';
|
||||
}
|
||||
|
||||
private static string RemoveQuotes(string value)
|
||||
{
|
||||
return value.Substring(1, value.Length - 2);
|
||||
}
|
||||
|
||||
private static string Quote(string value)
|
||||
{
|
||||
return '"' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies.Infrastructure
|
||||
{
|
||||
internal static class Constants
|
||||
{
|
||||
internal static class Headers
|
||||
{
|
||||
internal const string SetCookie = "Set-Cookie";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used by the CookieAuthenticationMiddleware to process request and response cookies.
|
||||
/// It is abstracted from the normal cookie APIs to allow for complex operations like chunking.
|
||||
/// </summary>
|
||||
public interface ICookieManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a cookie of the given name from the request.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
string GetRequestCookie(HttpContext context, string key);
|
||||
|
||||
/// <summary>
|
||||
/// Append the given cookie to the response.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="options"></param>
|
||||
void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Append a delete cookie to the response.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="options"></param>
|
||||
void DeleteCookie(HttpContext context, string key, CookieOptions options);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.34003
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.Cookies.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The cookie key and options are larger than ChunksSize, leaving no room for data..
|
||||
/// </summary>
|
||||
internal static string Exception_CookieLimitTooSmall
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("Exception_CookieLimitTooSmall", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded..
|
||||
/// </summary>
|
||||
internal static string Exception_ImcompleteChunkedCookie
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("Exception_ImcompleteChunkedCookie", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Exception_CookieLimitTooSmall" xml:space="preserve">
|
||||
<value>The cookie key and options are larger than ChunksSize, leaving no room for data.</value>
|
||||
</data>
|
||||
<data name="Exception_ImcompleteChunkedCookie" xml:space="preserve">
|
||||
<value>The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Security.Cookies.Infrastructure
|
||||
{
|
||||
public class CookieChunkingTests
|
||||
{
|
||||
[Fact]
|
||||
public void AppendLargeCookie_Appended()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
|
||||
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
|
||||
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
|
||||
Assert.Equal(1, values.Count);
|
||||
Assert.Equal("TestCookie=" + testString + "; path=/", values[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendLargeCookieWithLimit_Chunked()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
|
||||
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
new ChunkingCookieManager() { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
|
||||
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
|
||||
Assert.Equal(9, values.Count);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"TestCookie=chunks:8; path=/",
|
||||
"TestCookieC1=abcdefgh; path=/",
|
||||
"TestCookieC2=ijklmnop; path=/",
|
||||
"TestCookieC3=qrstuvwx; path=/",
|
||||
"TestCookieC4=yz012345; path=/",
|
||||
"TestCookieC5=6789ABCD; path=/",
|
||||
"TestCookieC6=EFGHIJKL; path=/",
|
||||
"TestCookieC7=MNOPQRST; path=/",
|
||||
"TestCookieC8=UVWXYZ; path=/",
|
||||
}, values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendLargeQuotedCookieWithLimit_QuotedChunked()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
|
||||
string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"";
|
||||
new ChunkingCookieManager() { ChunkSize = 32 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
|
||||
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
|
||||
Assert.Equal(9, values.Count);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"TestCookie=chunks:8; path=/",
|
||||
"TestCookieC1=\"abcdefgh\"; path=/",
|
||||
"TestCookieC2=\"ijklmnop\"; path=/",
|
||||
"TestCookieC3=\"qrstuvwx\"; path=/",
|
||||
"TestCookieC4=\"yz012345\"; path=/",
|
||||
"TestCookieC5=\"6789ABCD\"; path=/",
|
||||
"TestCookieC6=\"EFGHIJKL\"; path=/",
|
||||
"TestCookieC7=\"MNOPQRST\"; path=/",
|
||||
"TestCookieC8=\"UVWXYZ\"; path=/",
|
||||
}, values);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLargeChunkedCookie_Reassembled()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
context.Request.Headers.AppendValues("Cookie",
|
||||
"TestCookie=chunks:7",
|
||||
"TestCookieC1=abcdefghi",
|
||||
"TestCookieC2=jklmnopqr",
|
||||
"TestCookieC3=stuvwxyz0",
|
||||
"TestCookieC4=123456789",
|
||||
"TestCookieC5=ABCDEFGHI",
|
||||
"TestCookieC6=JKLMNOPQR",
|
||||
"TestCookieC7=STUVWXYZ");
|
||||
|
||||
string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
|
||||
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
Assert.Equal(testString, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLargeChunkedCookieWithQuotes_Reassembled()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
context.Request.Headers.AppendValues("Cookie",
|
||||
"TestCookie=chunks:7",
|
||||
"TestCookieC1=\"abcdefghi\"",
|
||||
"TestCookieC2=\"jklmnopqr\"",
|
||||
"TestCookieC3=\"stuvwxyz0\"",
|
||||
"TestCookieC4=\"123456789\"",
|
||||
"TestCookieC5=\"ABCDEFGHI\"",
|
||||
"TestCookieC6=\"JKLMNOPQR\"",
|
||||
"TestCookieC7=\"STUVWXYZ\"");
|
||||
|
||||
string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
|
||||
string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"";
|
||||
Assert.Equal(testString, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
context.Request.Headers.AppendValues("Cookie",
|
||||
"TestCookie=chunks:7",
|
||||
"TestCookieC1=abcdefghi",
|
||||
// Missing chunk "TestCookieC2=jklmnopqr",
|
||||
"TestCookieC3=stuvwxyz0",
|
||||
"TestCookieC4=123456789",
|
||||
"TestCookieC5=ABCDEFGHI",
|
||||
"TestCookieC6=JKLMNOPQR",
|
||||
"TestCookieC7=STUVWXYZ");
|
||||
|
||||
Assert.Throws<FormatException>(() => new ChunkingCookieManager().GetRequestCookie(context, "TestCookie"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassembled()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
context.Request.Headers.AppendValues("Cookie",
|
||||
"TestCookie=chunks:7",
|
||||
"TestCookieC1=abcdefghi",
|
||||
// Missing chunk "TestCookieC2=jklmnopqr",
|
||||
"TestCookieC3=stuvwxyz0",
|
||||
"TestCookieC4=123456789",
|
||||
"TestCookieC5=ABCDEFGHI",
|
||||
"TestCookieC6=JKLMNOPQR",
|
||||
"TestCookieC7=STUVWXYZ");
|
||||
|
||||
string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie");
|
||||
string testString = "chunks:7";
|
||||
Assert.Equal(testString, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteChunkedCookieWithOptions_AllDeleted()
|
||||
{
|
||||
HttpContext context = new DefaultHttpContext();
|
||||
context.Request.Headers.AppendValues("Cookie", "TestCookie=chunks:7");
|
||||
|
||||
new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" });
|
||||
var cookies = context.Response.Headers.GetValues("Set-Cookie");
|
||||
Assert.Equal(8, cookies.Count);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC3=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC4=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC5=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC6=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
"TestCookieC7=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
|
||||
}, cookies);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue