Skip to content

Commit 9ce4a97

Browse files
authored
Parsing extension-av on Set Cookie header (#22181)
1 parent 25e21df commit 9ce4a97

File tree

3 files changed

+100
-19
lines changed

3 files changed

+100
-19
lines changed

src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ public SetCookieHeaderValue(Microsoft.Extensions.Primitives.StringSegment name)
329329
public SetCookieHeaderValue(Microsoft.Extensions.Primitives.StringSegment name, Microsoft.Extensions.Primitives.StringSegment value) { }
330330
public Microsoft.Extensions.Primitives.StringSegment Domain { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
331331
public System.DateTimeOffset? Expires { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
332+
public System.Collections.Generic.IList<Microsoft.Extensions.Primitives.StringSegment> Extensions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
332333
public bool HttpOnly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
333334
public System.TimeSpan? MaxAge { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
334335
public Microsoft.Extensions.Primitives.StringSegment Name { get { throw null; } set { } }

src/Http/Headers/src/SetCookieHeaderValue.cs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Diagnostics.Contracts;
9+
using System.Linq;
910
using System.Text;
1011
using Microsoft.Extensions.Primitives;
1112

@@ -99,6 +100,8 @@ public StringSegment Value
99100

100101
public bool HttpOnly { get; set; }
101102

103+
public IList<StringSegment> Extensions { get; } = new List<StringSegment>();
104+
102105
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
103106
public override string ToString()
104107
{
@@ -155,6 +158,11 @@ public override string ToString()
155158
length += SeparatorToken.Length + HttpOnlyToken.Length;
156159
}
157160

161+
foreach (var extension in Extensions)
162+
{
163+
length += SeparatorToken.Length + extension.Length;
164+
}
165+
158166
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
159167
{
160168
var (headerValue, maxAgeValue, sameSite) = tuple;
@@ -204,6 +212,11 @@ public override string ToString()
204212
{
205213
AppendSegment(ref span, HttpOnlyToken, null);
206214
}
215+
216+
foreach (var extension in Extensions)
217+
{
218+
AppendSegment(ref span, extension, null);
219+
}
207220
});
208221
}
209222

@@ -281,6 +294,11 @@ public void AppendToStringBuilder(StringBuilder builder)
281294
{
282295
AppendSegment(builder, HttpOnlyToken, null);
283296
}
297+
298+
foreach (var extension in Extensions)
299+
{
300+
AppendSegment(builder, extension, null);
301+
}
284302
}
285303

286304
private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value)
@@ -399,7 +417,8 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
399417
{
400418
return 0;
401419
}
402-
var dateString = ReadToSemicolonOrEnd(input, ref offset);
420+
// We don't want to include comma, becouse date may contain it (eg. Sun, 06 Nov...)
421+
var dateString = ReadToSemicolonOrEnd(input, ref offset, includeComma: false);
403422
DateTimeOffset expirationDate;
404423
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
405424
{
@@ -499,13 +518,9 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
499518
// extension-av = <any CHAR except CTLs or ";">
500519
else
501520
{
502-
// TODO: skiping it for now to avoid parsing failure? Store it in a list?
503-
// = (no spaces)
504-
if (!ReadEqualsSign(input, ref offset))
505-
{
506-
return 0;
507-
}
508-
ReadToSemicolonOrEnd(input, ref offset);
521+
var tokenStart = offset - itemLength;
522+
ReadToSemicolonOrEnd(input, ref offset, includeComma: true);
523+
result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart));
509524
}
510525
}
511526

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

527-
private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset)
542+
private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset, bool includeComma = true)
528543
{
529544
var end = input.IndexOf(';', offset);
545+
if (end < 0)
546+
{
547+
// Also valid end of cookie
548+
if (includeComma)
549+
{
550+
end = input.IndexOf(',', offset);
551+
}
552+
}
553+
else if (includeComma)
554+
{
555+
var commaPosition = input.IndexOf(',', offset);
556+
if (commaPosition >= 0 && commaPosition < end)
557+
{
558+
end = commaPosition;
559+
}
560+
}
561+
530562
if (end < 0)
531563
{
532564
// Remainder of the string
533565
end = input.Length;
534566
}
567+
535568
var itemLength = end - offset;
536569
var result = input.Subsegment(offset, itemLength);
537570
offset += itemLength;
@@ -555,12 +588,13 @@ public override bool Equals(object? obj)
555588
&& StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
556589
&& Secure == other.Secure
557590
&& SameSite == other.SameSite
558-
&& HttpOnly == other.HttpOnly;
591+
&& HttpOnly == other.HttpOnly
592+
&& HeaderUtilities.AreEqualCollections(Extensions, other.Extensions, StringSegmentComparer.OrdinalIgnoreCase);
559593
}
560594

561595
public override int GetHashCode()
562596
{
563-
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
597+
var hash = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name)
564598
^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value)
565599
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
566600
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
@@ -569,6 +603,13 @@ public override int GetHashCode()
569603
^ Secure.GetHashCode()
570604
^ SameSite.GetHashCode()
571605
^ HttpOnly.GetHashCode();
606+
607+
foreach (var extension in Extensions)
608+
{
609+
hash ^= extension.GetHashCode();
610+
}
611+
612+
return hash;
572613
}
573614
}
574615
}

src/Http/Headers/test/SetCookieHeaderValueTest.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Net;
78
using System.Text;
9+
using Microsoft.Extensions.Primitives;
10+
using Moq;
811
using Xunit;
912

1013
namespace Microsoft.Net.Http.Headers
@@ -24,9 +27,11 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
2427
HttpOnly = true,
2528
MaxAge = TimeSpan.FromDays(1),
2629
Path = "path1",
27-
Secure = true
30+
Secure = true,
2831
};
29-
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");
32+
header1.Extensions.Add("extension1");
33+
header1.Extensions.Add("extension2=value");
34+
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");
3035

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

67+
var header8 = new SetCookieHeaderValue("name8", "value8");
68+
header8.Extensions.Add("extension1");
69+
header8.Extensions.Add("extension2=value");
70+
dataset.Add(header8, "name8=value8; extension1; extension2=value");
6271

6372
return dataset;
6473
}
@@ -126,7 +135,10 @@ public static TheoryData<string> InvalidCookieValues
126135
Path = "path1",
127136
Secure = true
128137
};
129-
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";
138+
header1.Extensions.Add("extension1");
139+
header1.Extensions.Add("extension2=value");
140+
141+
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";
130142

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

185+
var header9 = new SetCookieHeaderValue("name9", "value9");
186+
header9.Extensions.Add("extension1");
187+
header9.Extensions.Add("extension2=value");
188+
var string9 = "name9=value9; extension1; extension2=value";
189+
190+
173191
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
174192
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
175193
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
@@ -185,6 +203,22 @@ public static TheoryData<string> InvalidCookieValues
185203
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
186204
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
187205
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
206+
dataset.Add(new[] { header9 }.ToList(), new[] { string9 });
207+
208+
foreach (var item1 in SetCookieHeaderDataSet)
209+
{
210+
var pair_cookie1 = (SetCookieHeaderValue)item1[0];
211+
var pair_string1 = item1[1].ToString();
212+
213+
foreach (var item2 in SetCookieHeaderDataSet)
214+
{
215+
var pair_cookie2 = (SetCookieHeaderValue)item2[0];
216+
var pair_string2 = item2[1].ToString();
217+
218+
dataset.Add(new[] { pair_cookie1, pair_cookie2 }.ToList(), new[] { string.Join(", ", pair_string1, pair_string2) });
219+
220+
}
221+
}
188222

189223
return dataset;
190224
}
@@ -378,13 +412,18 @@ public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList<SetCookie
378412
}
379413

380414
[Fact]
381-
public void SetCookieHeaderValue_TryParse_SkipExtensionValues()
415+
public void SetCookieHeaderValue_TryParse_ExtensionOrderDoesntMatter()
382416
{
383-
string cookieHeaderValue = "cookiename=value; extensionname=value;";
417+
string cookieHeaderValue1 = "cookiename=value; extensionname1=value; extensionname2=value;";
418+
string cookieHeaderValue2 = "cookiename=value; extensionname2=value; extensionname1=value;";
384419

420+
SetCookieHeaderValue setCookieHeaderValue1;
421+
SetCookieHeaderValue setCookieHeaderValue2;
385422

386-
SetCookieHeaderValue.TryParse(cookieHeaderValue, out var setCookieHeaderValue);
387-
Assert.Equal("value", setCookieHeaderValue!.Value);
423+
SetCookieHeaderValue.TryParse(cookieHeaderValue1, out setCookieHeaderValue1);
424+
SetCookieHeaderValue.TryParse(cookieHeaderValue2, out setCookieHeaderValue2);
425+
426+
Assert.Equal(setCookieHeaderValue1, setCookieHeaderValue2);
388427
}
389428

390429
[Theory]
@@ -428,7 +467,7 @@ public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList<SetCoo
428467
[MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))]
429468
public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues(
430469
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
431-
IList<SetCookieHeaderValue> cookies,
470+
IList<SetCookieHeaderValue> cookies,
432471
#pragma warning restore xUnit1026 // Theory methods should use all of their parameters
433472
string[] input)
434473
{

0 commit comments

Comments
 (0)