Skip to content

Commit e1a57f9

Browse files
committed
Continued with cookie parsing
1 parent b4962a9 commit e1a57f9

File tree

5 files changed

+691
-9
lines changed

5 files changed

+691
-9
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using System;
4+
using System.Text.RegularExpressions;
5+
6+
internal static class CookieDateParser
7+
{
8+
private static readonly Regex DateDeliminator = new Regex("[\\x09\\x20-\\x2F\\x3B-\\x40\\x5B-\\x60\\x7B-\\x7E]");
9+
10+
public static DateTime? Parse(String str)
11+
{
12+
if (String.IsNullOrEmpty(str))
13+
{
14+
return null;
15+
}
16+
17+
// RFC6265 Section 5.1.1:
18+
var tokens = DateDeliminator.Split(str);
19+
var hour = default(Int32?);
20+
var minute = default(Int32?);
21+
var second = default(Int32?);
22+
var dayOfMonth = default(Int32?);
23+
var month = default(Int32?);
24+
var year = default(Int32?);
25+
26+
for (var i = 0; i < tokens.Length; i++)
27+
{
28+
var token = tokens[i].Trim();
29+
30+
if (token.Length == 0)
31+
{
32+
continue;
33+
}
34+
35+
// See section 2.1
36+
if (second == null)
37+
{
38+
var result = ParseTime(token);
39+
40+
if (result != null)
41+
{
42+
hour = result[0];
43+
minute = result[1];
44+
second = result[2];
45+
continue;
46+
}
47+
}
48+
49+
// See section 2.2
50+
if (dayOfMonth == null)
51+
{
52+
var result = ParseDigits(token, 1, 2, true);
53+
54+
if (result.HasValue)
55+
{
56+
dayOfMonth = result;
57+
continue;
58+
}
59+
}
60+
61+
// See section 2.3
62+
if (month == null)
63+
{
64+
var result = ParseMonth(token);
65+
66+
if (result != null)
67+
{
68+
month = result;
69+
continue;
70+
}
71+
}
72+
73+
// See section 2.4
74+
if (year == null)
75+
{
76+
var result = ParseDigits(token, 2, 4, true);
77+
78+
if (result.HasValue)
79+
{
80+
year = result;
81+
82+
// See Section 5.1.1
83+
if (year >= 70 && year <= 99)
84+
{
85+
year += 1900;
86+
}
87+
else if (year >= 0 && year <= 69)
88+
{
89+
year += 2000;
90+
}
91+
}
92+
}
93+
}
94+
95+
// See RFC 6265 Section 5.1.1
96+
if (!dayOfMonth.HasValue || !month.HasValue || !year.HasValue || !second.HasValue ||
97+
dayOfMonth < 1 || dayOfMonth > 31 || year < 1601 || hour > 23 || minute > 59 || second > 59)
98+
{
99+
return null;
100+
}
101+
102+
return new DateTime(year.Value, month.Value, dayOfMonth.Value, hour.Value, minute.Value, second.Value, DateTimeKind.Utc);
103+
}
104+
105+
private static Int32? ParseMonth(string token)
106+
{
107+
token = token.Substring(0, 3).ToLowerInvariant();
108+
109+
switch (token)
110+
{
111+
case "jan":
112+
return 1;
113+
case "feb":
114+
return 2;
115+
case "mar":
116+
return 3;
117+
case "apr":
118+
return 4;
119+
case "may":
120+
return 5;
121+
case "jun":
122+
return 6;
123+
case "jul":
124+
return 7;
125+
case "aug":
126+
return 8;
127+
case "sep":
128+
return 9;
129+
case "oct":
130+
return 10;
131+
case "nov":
132+
return 11;
133+
case "dec":
134+
return 12;
135+
default:
136+
return null;
137+
}
138+
}
139+
140+
private static Int32? ParseDigits(string token, int minDigits, int maxDigits, bool trailing)
141+
{
142+
var count = 0;
143+
144+
while (count < token.Length)
145+
{
146+
var c = token[count];
147+
148+
// "non-digit = %x00-2F / %x3A-FF"
149+
if (c <= 0x2F || c >= 0x3A)
150+
{
151+
break;
152+
}
153+
154+
count++;
155+
}
156+
157+
// constrain to a minimum and maximum number of digits.
158+
if (count < minDigits || count > maxDigits)
159+
{
160+
return null;
161+
}
162+
else if (!trailing && count != token.Length)
163+
{
164+
return null;
165+
}
166+
167+
return Int32.Parse(token.Substring(0, count));
168+
}
169+
170+
private static Int32[] ParseTime(string token)
171+
{
172+
var parts = token.Split(':');
173+
var result = new[] { 0, 0, 0 };
174+
175+
// See RF6256 Section 5.1.1
176+
if (parts.Length != 3)
177+
{
178+
return null;
179+
}
180+
181+
for (var i = 0; i < 3; i++)
182+
{
183+
var num = ParseDigits(parts[i], 1, 2, i == 2);
184+
185+
if (!num.HasValue)
186+
{
187+
return null;
188+
}
189+
190+
result[i] = num.Value;
191+
}
192+
193+
return result;
194+
}
195+
}
196+
}
Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,199 @@
11
namespace AngleSharp.Io.Cookie
22
{
3-
internal class CookieParser
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text.RegularExpressions;
6+
7+
internal static class CookieParser
48
{
9+
private static readonly Regex InvalidChars = new Regex("[\\x00-\\x1F]");
10+
private static readonly Char[] Terminators = new[] { '\n', '\r', '\0' };
11+
12+
public static WebCookie Parse(String str, Boolean loose = false)
13+
{
14+
str = str.Trim();
15+
16+
// See section 5.2
17+
var firstSemi = str.IndexOf(';');
18+
var cookiePair = (firstSemi == -1) ? str : str.Substring(0, firstSemi);
19+
var c = ParseCookiePair(cookiePair, loose);
20+
21+
if (c == null)
22+
{
23+
return null;
24+
}
25+
26+
if (firstSemi == -1)
27+
{
28+
return c;
29+
}
30+
31+
// Section 5.2.3
32+
var unparsed = str.Substring(firstSemi + 1).Trim();
33+
34+
if (unparsed.Length > 0)
35+
{
36+
/*
37+
* 5.2 says that when looping over the items: "[p]rocess the attribute-name
38+
* and attribute-value according to the requirements in the following
39+
* subsections" for every item. Plus, for many of the individual attributes
40+
* in S5.3 it says to use the "attribute-value of the last attribute in the
41+
* cookie-attribute-list".
42+
* Therefore, in this implementation, we overwrite the previous value.
43+
*/
44+
var cookie_avs = new Queue<String>(unparsed.Split(';'));
45+
46+
while (cookie_avs.Count > 0)
47+
{
48+
var av = cookie_avs.Dequeue().Trim();
49+
50+
// happens if ";;" appears
51+
if (av.Length == 0)
52+
{
53+
continue;
54+
}
55+
56+
var av_sep = av.IndexOf('=');
57+
var av_key = String.Empty;
58+
var av_value = String.Empty;
59+
60+
if (av_sep == -1)
61+
{
62+
av_key = av;
63+
av_value = null;
64+
}
65+
else
66+
{
67+
av_key = av.Substring(0, av_sep);
68+
av_value = av.Substring(av_sep + 1);
69+
}
70+
71+
av_key = av_key.Trim().ToLowerInvariant();
72+
73+
if (!String.IsNullOrEmpty(av_value))
74+
{
75+
av_value = av_value.Trim();
76+
}
77+
78+
switch (av_key)
79+
{
80+
// Section 5.2.1
81+
case "expires":
82+
if (!String.IsNullOrEmpty(av_value))
83+
{
84+
var exp = CookieDateParser.Parse(av_value);
85+
86+
if (exp.HasValue)
87+
{
88+
c.Expires = exp;
89+
}
90+
}
91+
break;
92+
// Section 5.2.2
93+
case "max-age":
94+
if (!String.IsNullOrEmpty(av_value))
95+
{
96+
if (Int32.TryParse(av_value, out var delta))
97+
{
98+
c.MaxAge = delta;
99+
}
100+
}
101+
break;
102+
// Section 5.2.3
103+
case "domain":
104+
if (!String.IsNullOrEmpty(av_value))
105+
{
106+
var domain = av_value.Trim().TrimStart('.');
107+
108+
if (!String.IsNullOrEmpty(domain))
109+
{
110+
c.Domain = domain.ToLowerInvariant();
111+
}
112+
}
113+
break;
114+
// Section 5.2.4
115+
case "path":
116+
c.Path = !String.IsNullOrEmpty(av_value) && av_value[0] == '/' ? av_value : null;
117+
break;
118+
// Section 5.2.5
119+
case "secure":
120+
c.IsSecure = true;
121+
break;
122+
// Section 5.2.6
123+
case "httponly":
124+
c.IsHttpOnly = true;
125+
break;
126+
default:
127+
c.WithExtension(av);
128+
break;
129+
}
130+
}
131+
}
132+
133+
return c;
134+
}
135+
136+
private static WebCookie ParseCookiePair(String cookiePair, Boolean loose)
137+
{
138+
cookiePair = TrimTerminator(cookiePair);
139+
140+
var firstEq = cookiePair.IndexOf('=');
141+
142+
if (loose)
143+
{
144+
if (firstEq == 0)
145+
{
146+
// '=' is immediately at start
147+
cookiePair = cookiePair.Substring(1);
148+
// might still need to split on '='
149+
firstEq = cookiePair.IndexOf('=');
150+
}
151+
}
152+
else if (firstEq <= 0)
153+
{
154+
// no '=' or is at start
155+
// needs to have non-empty "cookie-name"
156+
return null;
157+
}
158+
159+
var cookieName = String.Empty;
160+
var cookieValue = String.Empty;
161+
162+
if (firstEq <= 0)
163+
{
164+
cookieValue = cookiePair.Trim();
165+
}
166+
else
167+
{
168+
cookieName = cookiePair.Substring(0, firstEq).Trim();
169+
cookieValue = cookiePair.Substring(firstEq + 1).Trim();
170+
}
171+
172+
if (InvalidChars.IsMatch(cookieName) || InvalidChars.IsMatch(cookieValue))
173+
{
174+
return null;
175+
}
176+
177+
return new WebCookie
178+
{
179+
Key = cookieName,
180+
Value = cookieValue,
181+
};
182+
}
183+
184+
private static String TrimTerminator(String str)
185+
{
186+
foreach (var terminator in Terminators)
187+
{
188+
var terminatorIdx = str.IndexOf(terminator);
189+
190+
if (terminatorIdx != -1)
191+
{
192+
str = str.Substring(0, terminatorIdx);
193+
}
194+
}
195+
196+
return str;
197+
}
5198
}
6199
}

0 commit comments

Comments
 (0)