Parsing extension-av on Set Cookie header (#22181)

This commit is contained in:
Ondřej Štorc 2020-06-09 17:48:39 +02:00 committed by GitHub
parent 25e21df3a7
commit 9ce4a970a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 19 deletions

View File

@ -329,6 +329,7 @@ namespace Microsoft.Net.Http.Headers
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 { } }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Primitives;
@ -99,6 +100,8 @@ namespace Microsoft.Net.Http.Headers
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()
{
@ -155,6 +158,11 @@ namespace Microsoft.Net.Http.Headers
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;
@ -204,6 +212,11 @@ namespace Microsoft.Net.Http.Headers
{
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);
}
foreach (var extension in Extensions)
{
AppendSegment(builder, extension, null);
}
}
private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
@ -399,7 +417,8 @@ namespace Microsoft.Net.Http.Headers
{
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))
{
@ -499,13 +518,9 @@ namespace Microsoft.Net.Http.Headers
// 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));
}
}
@ -524,14 +539,32 @@ namespace Microsoft.Net.Http.Headers
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;
@ -555,12 +588,13 @@ namespace Microsoft.Net.Http.Headers
&& 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)
@ -569,6 +603,13 @@ namespace Microsoft.Net.Http.Headers
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();
foreach (var extension in Extensions)
{
hash ^= extension.GetHashCode();
}
return hash;
}
}
}

View File

@ -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
@ -24,9 +27,11 @@ namespace Microsoft.Net.Http.Headers
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=");
@ -59,6 +64,10 @@ namespace Microsoft.Net.Http.Headers
};
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;
}
@ -126,7 +135,10 @@ namespace Microsoft.Net.Http.Headers
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";
@ -170,6 +182,12 @@ namespace Microsoft.Net.Http.Headers
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 });
@ -185,6 +203,22 @@ namespace Microsoft.Net.Http.Headers
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;
}
@ -378,13 +412,18 @@ namespace Microsoft.Net.Http.Headers
}
[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]
@ -428,7 +467,7 @@ namespace Microsoft.Net.Http.Headers
[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)
{