Parsing extension-av on Set Cookie header (#22181)
This commit is contained in:
parent
25e21df3a7
commit
9ce4a970a2
|
|
@ -329,6 +329,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
public SetCookieHeaderValue(Microsoft.Extensions.Primitives.StringSegment name, Microsoft.Extensions.Primitives.StringSegment value) { }
|
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 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.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 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 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 { } }
|
public Microsoft.Extensions.Primitives.StringSegment Name { get { throw null; } set { } }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
|
@ -99,6 +100,8 @@ namespace Microsoft.Net.Http.Headers
|
||||||
|
|
||||||
public bool HttpOnly { get; set; }
|
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
|
// 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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
@ -155,6 +158,11 @@ namespace Microsoft.Net.Http.Headers
|
||||||
length += SeparatorToken.Length + HttpOnlyToken.Length;
|
length += SeparatorToken.Length + HttpOnlyToken.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
length += SeparatorToken.Length + extension.Length;
|
||||||
|
}
|
||||||
|
|
||||||
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
|
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
|
||||||
{
|
{
|
||||||
var (headerValue, maxAgeValue, sameSite) = tuple;
|
var (headerValue, maxAgeValue, sameSite) = tuple;
|
||||||
|
|
@ -204,6 +212,11 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
AppendSegment(ref span, HttpOnlyToken, null);
|
AppendSegment(ref span, HttpOnlyToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
AppendSegment(ref span, extension, null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,6 +294,11 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
AppendSegment(builder, HttpOnlyToken, null);
|
AppendSegment(builder, HttpOnlyToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
AppendSegment(builder, extension, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
|
private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
|
||||||
|
|
@ -399,7 +417,8 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
return 0;
|
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;
|
DateTimeOffset expirationDate;
|
||||||
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
|
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
|
||||||
{
|
{
|
||||||
|
|
@ -499,13 +518,9 @@ namespace Microsoft.Net.Http.Headers
|
||||||
// extension-av = <any CHAR except CTLs or ";">
|
// extension-av = <any CHAR except CTLs or ";">
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: skiping it for now to avoid parsing failure? Store it in a list?
|
var tokenStart = offset - itemLength;
|
||||||
// = (no spaces)
|
ReadToSemicolonOrEnd(input, ref offset, includeComma: true);
|
||||||
if (!ReadEqualsSign(input, ref offset))
|
result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart));
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
ReadToSemicolonOrEnd(input, ref offset);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,14 +539,32 @@ namespace Microsoft.Net.Http.Headers
|
||||||
return true;
|
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);
|
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)
|
if (end < 0)
|
||||||
{
|
{
|
||||||
// Remainder of the string
|
// Remainder of the string
|
||||||
end = input.Length;
|
end = input.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemLength = end - offset;
|
var itemLength = end - offset;
|
||||||
var result = input.Subsegment(offset, itemLength);
|
var result = input.Subsegment(offset, itemLength);
|
||||||
offset += itemLength;
|
offset += itemLength;
|
||||||
|
|
@ -555,12 +588,13 @@ namespace Microsoft.Net.Http.Headers
|
||||||
&& StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
|
&& StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
|
||||||
&& Secure == other.Secure
|
&& Secure == other.Secure
|
||||||
&& SameSite == other.SameSite
|
&& SameSite == other.SameSite
|
||||||
&& HttpOnly == other.HttpOnly;
|
&& HttpOnly == other.HttpOnly
|
||||||
|
&& HeaderUtilities.AreEqualCollections(Extensions, other.Extensions, StringSegmentComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
|
var hash = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
|
||||||
^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value)
|
^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value)
|
||||||
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
|
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
|
||||||
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
|
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
|
||||||
|
|
@ -569,6 +603,13 @@ namespace Microsoft.Net.Http.Headers
|
||||||
^ Secure.GetHashCode()
|
^ Secure.GetHashCode()
|
||||||
^ SameSite.GetHashCode()
|
^ SameSite.GetHashCode()
|
||||||
^ HttpOnly.GetHashCode();
|
^ HttpOnly.GetHashCode();
|
||||||
|
|
||||||
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
hash ^= extension.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Headers
|
namespace Microsoft.Net.Http.Headers
|
||||||
|
|
@ -24,9 +27,11 @@ namespace Microsoft.Net.Http.Headers
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
MaxAge = TimeSpan.FromDays(1),
|
MaxAge = TimeSpan.FromDays(1),
|
||||||
Path = "path1",
|
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", "");
|
var header2 = new SetCookieHeaderValue("name2", "");
|
||||||
dataset.Add(header2, "name2=");
|
dataset.Add(header2, "name2=");
|
||||||
|
|
@ -59,6 +64,10 @@ namespace Microsoft.Net.Http.Headers
|
||||||
};
|
};
|
||||||
dataset.Add(header7, "name7=value7; samesite=none");
|
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;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +135,10 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Path = "path1",
|
Path = "path1",
|
||||||
Secure = true
|
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 header2 = new SetCookieHeaderValue("name2", "value2");
|
||||||
var string2 = "name2=value2";
|
var string2 = "name2=value2";
|
||||||
|
|
@ -170,6 +182,12 @@ namespace Microsoft.Net.Http.Headers
|
||||||
var string8a = "name8=value8; samesite";
|
var string8a = "name8=value8; samesite";
|
||||||
var string8b = "name8=value8; samesite=invalid";
|
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 }.ToList(), new[] { string1 });
|
||||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
||||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
|
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
|
||||||
|
|
@ -185,6 +203,22 @@ namespace Microsoft.Net.Http.Headers
|
||||||
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
|
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
|
||||||
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
|
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
|
||||||
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
|
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;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
@ -378,13 +412,18 @@ namespace Microsoft.Net.Http.Headers
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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);
|
SetCookieHeaderValue.TryParse(cookieHeaderValue1, out setCookieHeaderValue1);
|
||||||
Assert.Equal("value", setCookieHeaderValue!.Value);
|
SetCookieHeaderValue.TryParse(cookieHeaderValue2, out setCookieHeaderValue2);
|
||||||
|
|
||||||
|
Assert.Equal(setCookieHeaderValue1, setCookieHeaderValue2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -428,7 +467,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
|
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
|
public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
|
||||||
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
|
#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
|
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
|
||||||
string[] input)
|
string[] input)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue