#32 - Port Cookie chunking from Katana.

This commit is contained in:
Chris Ross 2014-09-09 10:38:39 -07:00
parent 51c3dc98dc
commit f6d6f31414
9 changed files with 761 additions and 5 deletions

View File

@ -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);

View File

@ -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);
}

View File

@ -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; }
}
}

View File

@ -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 + '"';
}
}
}

View File

@ -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";
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}