Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ public SetCookieHeaderValue(Microsoft.Extensions.Primitives.StringSegment name)
public SetCookieHeaderValue(Microsoft.Extensions.Primitives.StringSegment name, Microsoft.Extensions.Primitives.StringSegment value) { }
public Microsoft.Extensions.Primitives.StringSegment Domain { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public System.DateTimeOffset? Expires { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public System.Collections.Generic.IList<Microsoft.Extensions.Primitives.StringSegment> Extensions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public bool HttpOnly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public System.TimeSpan? MaxAge { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public Microsoft.Extensions.Primitives.StringSegment Name { get { throw null; } set { } }
Expand Down
63 changes: 52 additions & 11 deletions src/Http/Headers/src/SetCookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -99,6 +100,8 @@ public StringSegment Value

public bool HttpOnly { get; set; }

public IList<StringSegment> Extensions { get; } = new List<StringSegment>();

// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
public override string ToString()
{
Expand Down Expand Up @@ -155,6 +158,11 @@ public override string ToString()
length += SeparatorToken.Length + HttpOnlyToken.Length;
}

foreach (var extension in Extensions)
{
length += SeparatorToken.Length + extension.Length;
}

return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
{
var (headerValue, maxAgeValue, sameSite) = tuple;
Expand Down Expand Up @@ -204,6 +212,11 @@ public override string ToString()
{
AppendSegment(ref span, HttpOnlyToken, null);
}

foreach (var extension in Extensions)
{
AppendSegment(ref span, extension, null);
}
});
}

Expand Down Expand Up @@ -281,6 +294,11 @@ public void AppendToStringBuilder(StringBuilder builder)
{
AppendSegment(builder, HttpOnlyToken, null);
}

foreach (var extension in Extensions)
{
AppendSegment(builder, extension, null);
}
}

private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
Expand Down Expand Up @@ -399,7 +417,8 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
{
return 0;
}
var dateString = ReadToSemicolonOrEnd(input, ref offset);
// We don't want to include comma, becouse date may contain it (eg. Sun, 06 Nov...)
var dateString = ReadToSemicolonOrEnd(input, ref offset, includeComma: false);
DateTimeOffset expirationDate;
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
{
Expand Down Expand Up @@ -499,13 +518,9 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
// extension-av = <any CHAR except CTLs or ";">
else
{
// TODO: skiping it for now to avoid parsing failure? Store it in a list?
// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return 0;
}
ReadToSemicolonOrEnd(input, ref offset);
var tokenStart = offset - itemLength;
ReadToSemicolonOrEnd(input, ref offset, includeComma: true);
result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart));
}
}

Expand All @@ -524,14 +539,32 @@ private static bool ReadEqualsSign(StringSegment input, ref int offset)
return true;
}

private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset)
private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset, bool includeComma = true)
{
var end = input.IndexOf(';', offset);
if (end < 0)
{
// Also valid end of cookie
if (includeComma)
{
end = input.IndexOf(',', offset);
}
}
else if (includeComma)
{
var commaPosition = input.IndexOf(',', offset);
if (commaPosition >= 0 && commaPosition < end)
{
end = commaPosition;
}
}

if (end < 0)
{
// Remainder of the string
end = input.Length;
}

var itemLength = end - offset;
var result = input.Subsegment(offset, itemLength);
offset += itemLength;
Expand All @@ -555,12 +588,13 @@ public override bool Equals(object? obj)
&& StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
&& Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly;
&& HttpOnly == other.HttpOnly
&& HeaderUtilities.AreEqualCollections(Extensions, other.Extensions, StringSegmentComparer.OrdinalIgnoreCase);
}

public override int GetHashCode()
{
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
var hash = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value)
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
Expand All @@ -569,6 +603,13 @@ public override int GetHashCode()
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();

foreach (var extension in Extensions)
{
hash ^= extension.GetHashCode();
}

return hash;
}
}
}
55 changes: 47 additions & 8 deletions src/Http/Headers/test/SetCookieHeaderValueTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

namespace Microsoft.Net.Http.Headers
Expand All @@ -24,9 +27,11 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
Secure = true
Secure = true,
};
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly");
header1.Extensions.Add("extension1");
header1.Extensions.Add("extension2=value");
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value");

var header2 = new SetCookieHeaderValue("name2", "");
dataset.Add(header2, "name2=");
Expand Down Expand Up @@ -59,6 +64,10 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
};
dataset.Add(header7, "name7=value7; samesite=none");

var header8 = new SetCookieHeaderValue("name8", "value8");
header8.Extensions.Add("extension1");
header8.Extensions.Add("extension2=value");
dataset.Add(header8, "name8=value8; extension1; extension2=value");

return dataset;
}
Expand Down Expand Up @@ -126,7 +135,10 @@ public static TheoryData<string> InvalidCookieValues
Path = "path1",
Secure = true
};
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly";
header1.Extensions.Add("extension1");
header1.Extensions.Add("extension2=value");

var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value";

var header2 = new SetCookieHeaderValue("name2", "value2");
var string2 = "name2=value2";
Expand Down Expand Up @@ -170,6 +182,12 @@ public static TheoryData<string> InvalidCookieValues
var string8a = "name8=value8; samesite";
var string8b = "name8=value8; samesite=invalid";

var header9 = new SetCookieHeaderValue("name9", "value9");
header9.Extensions.Add("extension1");
header9.Extensions.Add("extension2=value");
var string9 = "name9=value9; extension1; extension2=value";


dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
Expand All @@ -185,6 +203,22 @@ public static TheoryData<string> InvalidCookieValues
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
dataset.Add(new[] { header9 }.ToList(), new[] { string9 });

foreach (var item1 in SetCookieHeaderDataSet)
{
var pair_cookie1 = (SetCookieHeaderValue)item1[0];
var pair_string1 = item1[1].ToString();

foreach (var item2 in SetCookieHeaderDataSet)
{
var pair_cookie2 = (SetCookieHeaderValue)item2[0];
var pair_string2 = item2[1].ToString();

dataset.Add(new[] { pair_cookie1, pair_cookie2 }.ToList(), new[] { string.Join(", ", pair_string1, pair_string2) });

}
}

return dataset;
}
Expand Down Expand Up @@ -378,13 +412,18 @@ public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList<SetCookie
}

[Fact]
public void SetCookieHeaderValue_TryParse_SkipExtensionValues()
public void SetCookieHeaderValue_TryParse_ExtensionOrderDoesntMatter()
{
string cookieHeaderValue = "cookiename=value; extensionname=value;";
string cookieHeaderValue1 = "cookiename=value; extensionname1=value; extensionname2=value;";
string cookieHeaderValue2 = "cookiename=value; extensionname2=value; extensionname1=value;";

SetCookieHeaderValue setCookieHeaderValue1;
SetCookieHeaderValue setCookieHeaderValue2;

SetCookieHeaderValue.TryParse(cookieHeaderValue, out var setCookieHeaderValue);
Assert.Equal("value", setCookieHeaderValue!.Value);
SetCookieHeaderValue.TryParse(cookieHeaderValue1, out setCookieHeaderValue1);
SetCookieHeaderValue.TryParse(cookieHeaderValue2, out setCookieHeaderValue2);

Assert.Equal(setCookieHeaderValue1, setCookieHeaderValue2);
}

[Theory]
Expand Down Expand Up @@ -428,7 +467,7 @@ public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<SetCoo
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
IList<SetCookieHeaderValue> cookies,
IList<SetCookieHeaderValue> cookies,
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
string[] input)
{
Expand Down