#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()
|
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
|
||||||
{
|
{
|
||||||
IReadableStringCollection cookies = Request.Cookies;
|
string cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
|
||||||
string cookie = cookies[Options.CookieName];
|
|
||||||
if (string.IsNullOrWhiteSpace(cookie))
|
if (string.IsNullOrWhiteSpace(cookie))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -148,7 +147,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
||||||
var model = new AuthenticationTicket(context.Identity, context.Properties);
|
var model = new AuthenticationTicket(context.Identity, context.Properties);
|
||||||
string cookieValue = Options.TicketDataFormat.Protect(model);
|
string cookieValue = Options.TicketDataFormat.Protect(model);
|
||||||
|
|
||||||
Response.Cookies.Append(
|
Options.CookieManager.AppendResponseCookie(
|
||||||
|
Context,
|
||||||
Options.CookieName,
|
Options.CookieName,
|
||||||
cookieValue,
|
cookieValue,
|
||||||
cookieOptions);
|
cookieOptions);
|
||||||
|
|
@ -162,7 +162,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
||||||
|
|
||||||
Options.Notifications.ResponseSignOut(context);
|
Options.Notifications.ResponseSignOut(context);
|
||||||
|
|
||||||
Response.Cookies.Delete(
|
Options.CookieManager.DeleteCookie(
|
||||||
|
Context,
|
||||||
Options.CookieName,
|
Options.CookieName,
|
||||||
cookieOptions);
|
cookieOptions);
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +181,8 @@ namespace Microsoft.AspNet.Security.Cookies
|
||||||
cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime;
|
cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response.Cookies.Append(
|
Options.CookieManager.AppendResponseCookie(
|
||||||
|
Context,
|
||||||
Options.CookieName,
|
Options.CookieName,
|
||||||
cookieValue,
|
cookieValue,
|
||||||
cookieOptions);
|
cookieOptions);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNet.Builder;
|
using Microsoft.AspNet.Builder;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Security.Cookies.Infrastructure;
|
||||||
using Microsoft.AspNet.Security.DataHandler;
|
using Microsoft.AspNet.Security.DataHandler;
|
||||||
using Microsoft.AspNet.Security.DataProtection;
|
using Microsoft.AspNet.Security.DataProtection;
|
||||||
using Microsoft.AspNet.Security.Infrastructure;
|
using Microsoft.AspNet.Security.Infrastructure;
|
||||||
|
|
@ -33,6 +34,10 @@ namespace Microsoft.AspNet.Security.Cookies
|
||||||
typeof(CookieAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
|
typeof(CookieAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
|
||||||
options.TicketDataFormat = new TicketDataFormat(dataProtector);
|
options.TicketDataFormat = new TicketDataFormat(dataProtector);
|
||||||
}
|
}
|
||||||
|
if (Options.CookieManager == null)
|
||||||
|
{
|
||||||
|
Options.CookieManager = new ChunkingCookieManager();
|
||||||
|
}
|
||||||
|
|
||||||
_logger = loggerFactory.Create(typeof(CookieAuthenticationMiddleware).FullName);
|
_logger = loggerFactory.Create(typeof(CookieAuthenticationMiddleware).FullName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Security.Cookies.Infrastructure;
|
||||||
using Microsoft.AspNet.Security.Infrastructure;
|
using Microsoft.AspNet.Security.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Security.Cookies
|
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.
|
/// used which calls DateTimeOffset.UtcNow. This is typically not replaced except for unit testing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ISystemClock SystemClock { get; set; }
|
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