Skip to content

Commit ff47f61

Browse files
committed
A cookie bug caused a request error (net::ERR_CONNECTION_REFUSED).
1. `cookie.Name` cannot be assigned an empty string. 2. `cookie.Value` containing comma-separated JSON strings will be truncated into multiple key-value pairs. cookie string like this: " "; Location={"country":"","city":" "}
1 parent 375dd50 commit ff47f61

File tree

2 files changed

+188
-18
lines changed

2 files changed

+188
-18
lines changed

src/EmbedIO/Net/Internal/HttpListenerRequest.cs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ internal void AddHeader(string header)
290290

291291
break;
292292
case "cookie":
293-
ParseCookies(val);
293+
_cookies = ParseCookies(val);
294294

295295
break;
296296
}
@@ -405,17 +405,18 @@ private static bool IsKnownHttpMethod(string method, out HttpVerbs verb)
405405
}
406406
}
407407

408-
private void ParseCookies(string val)
408+
private static CookieList ParseCookies(string val)
409409
{
410-
_cookies ??= new CookieList();
410+
var cookies = new CookieList();
411411

412-
var cookieStrings = val.SplitByAny(';', ',')
412+
var cookieStrings = val.SplitByAny(';')
413413
.Where(x => !string.IsNullOrEmpty(x));
414414
Cookie? current = null;
415415
var version = 0;
416416

417-
foreach (var str in cookieStrings)
417+
foreach (var cookieString in cookieStrings)
418418
{
419+
var str = cookieString.Trim();
419420
if (str.StartsWith("$Version", StringComparison.Ordinal))
420421
{
421422
version = int.Parse(str.Substring(str.IndexOf('=') + 1).Unquote(), CultureInfo.InvariantCulture);
@@ -430,36 +431,47 @@ private void ParseCookies(string val)
430431
}
431432
else if (str.StartsWith("$Port", StringComparison.Ordinal) && current != null)
432433
{
433-
current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
434+
current.Port = $"\"{str.Substring(str.IndexOf('=') + 1).Trim()}\"";
434435
}
435436
else
436437
{
437438
if (current != null)
438439
{
439-
_cookies.Add(current);
440+
cookies.Add(current);
440441
}
441442

442-
current = new Cookie();
443-
var idx = str.IndexOf('=');
444-
if (idx > 0)
443+
try
445444
{
446-
current.Name = str.Substring(0, idx).Trim();
447-
current.Value = str.Substring(idx + 1).Trim();
445+
var ck = new Cookie();
446+
var idx = str.IndexOf('=');
447+
if (idx > 0)
448+
{
449+
ck.Name = str.Substring(0, idx).Trim();
450+
ck.Value = str.Substring(idx + 1).Trim();
451+
}
452+
else
453+
{
454+
ck.Name = str.Trim();
455+
ck.Value = string.Empty;
456+
}
457+
458+
ck.Version = version;
459+
460+
current = ck;
448461
}
449-
else
462+
catch (Exception e)
450463
{
451-
current.Name = str.Trim();
452-
current.Value = string.Empty;
464+
current = null;
453465
}
454-
455-
current.Version = version;
456466
}
457467
}
458468

459469
if (current != null)
460470
{
461-
_cookies.Add(current);
471+
cookies.Add(current);
462472
}
473+
474+
return cookies;
463475
}
464476

465477
private void InitializeQueryString(string query)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using EmbedIO.Net;
2+
using NUnit.Framework;
3+
using System;
4+
using System.Reflection;
5+
6+
namespace EmbedIO.Tests.Utilities
7+
{
8+
public static class HttpListenerRequestCookieExtensions
9+
{
10+
public static CookieList ParseCookies(this string cookieHeader)
11+
{
12+
var type = Type.GetType("EmbedIO.Net.Internal.HttpListenerRequest, EmbedIO");
13+
Assert.NotNull(type, "Could not find type EmbedIO.Net.Internal.HttpListenerRequest in assembly EmbedIO");
14+
15+
var method = type.GetMethod("ParseCookies", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
16+
Assert.NotNull(method, "Could not find static method ParseCookies");
17+
18+
var result = method.Invoke(null, new object[] { cookieHeader });
19+
return (CookieList)result;
20+
}
21+
}
22+
23+
[TestFixture]
24+
public class HttpListenerRequestCookieTest
25+
{
26+
[Test]
27+
public void ParseCookies_SimpleHeader_CreatesCookies()
28+
{
29+
var cookies = "a=b; c=d".ParseCookies();
30+
Assert.AreEqual(2, cookies.Count);
31+
Assert.AreEqual("a", cookies[0].Name);
32+
Assert.AreEqual("b", cookies[0].Value);
33+
Assert.AreEqual("c", cookies[1].Name);
34+
Assert.AreEqual("d", cookies[1].Value);
35+
}
36+
37+
[Test]
38+
public void ParseCookies_VersionAndAttributes_AppliesToFollowingCookies()
39+
{
40+
var cookies = "$Version=1; a=b; $Path=/; $Domain=example.com; c=d".ParseCookies();
41+
Assert.AreEqual(2, cookies.Count);
42+
43+
Assert.AreEqual(1, cookies[0].Version);
44+
Assert.AreEqual("/", cookies[0].Path);
45+
Assert.AreEqual("example.com", cookies[0].Domain);
46+
47+
Assert.AreEqual(1, cookies[1].Version);
48+
// $Path/$Domain apply only when encountered after a cookie; in this header they follow cookie a, so they apply to a only.
49+
// Depending on ParseCookies implementation, attributes after cookie may apply to the previous cookie. Ensure at least cookie names/values parsed.
50+
Assert.AreEqual("c", cookies[1].Name);
51+
Assert.AreEqual("d", cookies[1].Value);
52+
}
53+
54+
[Test]
55+
public void ParseCookies_NameOnlyCookie_CreatesEmptyValue()
56+
{
57+
var cookies = "flag".ParseCookies();
58+
Assert.AreEqual(1, cookies.Count);
59+
Assert.AreEqual("flag", cookies[0].Name);
60+
Assert.AreEqual(string.Empty, cookies[0].Value);
61+
}
62+
63+
[Test]
64+
public void ParseCookies_QuotedValue_PreservesQuotes()
65+
{
66+
var cookies = "a=\"b,c\"; d=unquoted".ParseCookies();
67+
Assert.AreEqual(2, cookies.Count);
68+
Assert.AreEqual("\"b,c\"", cookies[0].Value);
69+
Assert.AreEqual("unquoted", cookies[1].Value);
70+
}
71+
72+
[Test]
73+
public void ParseCookies_VersionBeforeCookies_AppliesToAllFollowing()
74+
{
75+
var cookies = "$Version=2; x=1; y=2".ParseCookies();
76+
Assert.AreEqual(2, cookies.Count);
77+
Assert.AreEqual(2, cookies[0].Version);
78+
Assert.AreEqual("x", cookies[0].Name);
79+
Assert.AreEqual("1", cookies[0].Value);
80+
Assert.AreEqual(2, cookies[1].Version);
81+
Assert.AreEqual("y", cookies[1].Name);
82+
Assert.AreEqual("2", cookies[1].Value);
83+
}
84+
85+
[Test]
86+
public void ParseCookies_PathAppliesToCurrentCookieOnly()
87+
{
88+
var cookies = "a=b; $Path=/x; c=d; $Path=/y; e=f".ParseCookies();
89+
Assert.AreEqual(3, cookies.Count);
90+
Assert.AreEqual("a", cookies[0].Name);
91+
Assert.AreEqual("/x", cookies[0].Path);
92+
93+
Assert.AreEqual("c", cookies[1].Name);
94+
Assert.AreEqual("/y", cookies[1].Path);
95+
96+
Assert.AreEqual("e", cookies[2].Name);
97+
Assert.IsTrue(string.IsNullOrEmpty(cookies[2].Path));
98+
}
99+
100+
[Test]
101+
public void ParseCookies_PortAttribute_SetsPortOnCurrent()
102+
{
103+
var cookies = "a=b; $Port=8080; c=d".ParseCookies();
104+
Assert.AreEqual(2, cookies.Count);
105+
Assert.AreEqual("\"8080\"", cookies[0].Port);
106+
Assert.AreEqual("c", cookies[1].Name);
107+
Assert.AreEqual("d", cookies[1].Value);
108+
}
109+
110+
[Test]
111+
public void ParseCookies_HandlesExtraSpacesAndTrimming()
112+
{
113+
var cookies = " a = b ; c= d ".ParseCookies();
114+
Assert.AreEqual(2, cookies.Count);
115+
Assert.AreEqual("a", cookies[0].Name);
116+
Assert.AreEqual("b", cookies[0].Value);
117+
Assert.AreEqual("c", cookies[1].Name);
118+
Assert.AreEqual("d", cookies[1].Value);
119+
}
120+
121+
[Test]
122+
public void ParseCookies_NoNameCookie_IsIgnored()
123+
{
124+
var cookies = "=value; a=b".ParseCookies();
125+
Assert.AreEqual(1, cookies.Count, "Cookie with no name should be ignored.");
126+
Assert.AreEqual("a", cookies[0].Name);
127+
Assert.AreEqual("b", cookies[0].Value);
128+
}
129+
130+
[Test]
131+
public void ParseCookies_JsonValue_IsPreserved()
132+
{
133+
var jsonValue = "{ \"key\": \"some value\" }";
134+
var cookieString = $"data={jsonValue}";
135+
136+
var cookies = cookieString.ParseCookies();
137+
Assert.AreEqual(1, cookies.Count);
138+
Assert.AreEqual("data", cookies[0].Name);
139+
Assert.AreEqual(jsonValue, cookies[0].Value);
140+
}
141+
142+
[Test]
143+
public void ParseCookies()
144+
{
145+
var cookies = "\" \"; Location={\"country\":\"\",\"city\":\" \"}; id=5584".ParseCookies();
146+
Assert.AreEqual(3, cookies.Count);
147+
148+
Assert.AreEqual("\" \"", cookies[0].Name);
149+
Assert.AreEqual("", cookies[0].Value);
150+
151+
Assert.AreEqual("Location", cookies[1].Name);
152+
Assert.AreEqual("{\"country\":\"\",\"city\":\" \"}", cookies[1].Value);
153+
154+
Assert.AreEqual("id", cookies[2].Name);
155+
Assert.AreEqual("5584", cookies[2].Value);
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)