/// <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(IOwinContext context, string key, string value, CookieOptions options) { if (context == null) { throw new ArgumentNullException("context"); } if (options == null) { throw new ArgumentNullException("options"); } var webContext = context.Get <HttpContextBase>(typeof(HttpContextBase).FullName); if (webContext == null) { Fallback.AppendResponseCookie(context, key, value, options); return; } bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); bool expiresHasValue = options.Expires.HasValue; bool sameSiteHasValue = options.SameSite.HasValue && SystemWebCookieManager.IsSameSiteAvailable; 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 \\G\\M\\T", CultureInfo.InvariantCulture), !options.Secure ? null : "; secure", !options.HttpOnly ? null : "; HttpOnly", !sameSiteHasValue ? null : "; SameSite=", !sameSiteHasValue ? null : GetStringRepresentationOfSameSite(options.SameSite.Value) ); value = value ?? string.Empty; bool quoted = false; if (IsQuoted(value)) { quoted = true; value = RemoveQuotes(value); } string escapedValue = Uri.EscapeDataString(value); // Normal cookie if (!ChunkSize.HasValue || ChunkSize.Value > prefix.Length + escapedValue.Length + suffix.Length + (quoted ? 2 : 0)) { var cookie = new HttpCookie(escapedKey, escapedValue); SetOptions(cookie, options, domainHasValue, pathHasValue, expiresHasValue); webContext.Response.AppendCookie(cookie); } 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); var cookie = new HttpCookie(escapedKey, "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture)); SetOptions(cookie, options, domainHasValue, pathHasValue, expiresHasValue); webContext.Response.AppendCookie(cookie); 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; cookie = new HttpCookie(escapedKey + "C" + chunkId.ToString(CultureInfo.InvariantCulture), quoted ? Quote(segment) : segment); SetOptions(cookie, options, domainHasValue, pathHasValue, expiresHasValue); webContext.Response.AppendCookie(cookie); } } }