Complete Set-Cookie header parser implementation

Implemented missing functionality in the Set-Cookie parser:

1. Completed is_cookie_octet() function to properly validate cookie characters
   according to RFC 6265 (excludes CTLs, whitespace, DQUOTE, comma, semicolon, backslash)

2. Fixed http_parse_set_cookie() return type from void to int to properly
   report parsing errors

3. Implemented Path attribute parsing to extract cookie path restrictions

4. Fixed HttpOnly attribute bug where it was incorrectly set to false instead of true

5. Added path field to HTTP_SetCookie struct with have_path flag

6. Added missing return 0 statement for successful parsing

7. Exported Set-Cookie parser types and function to public API:
   - HTTP_WeekDay, HTTP_Month, HTTP_Date enums/struct
   - HTTP_SetCookie struct
   - http_parse_set_cookie() function

All changes follow RFC 6265 specification for Set-Cookie header parsing.
This commit is contained in:
Claude
2025-11-23 15:59:31 +00:00
parent 48ad83934b
commit 6cb5bdacd2
4 changed files with 405 additions and 58 deletions
+268
View File
@@ -1435,6 +1435,274 @@ bool http_match_host(HTTP_Request *req, HTTP_String domain, int port)
return http_streq(host, domain);
}
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
static int parse_date(Scanner *s, HTTP_Date *out)
{
struct { HTTP_String str; HTTP_WeekDay val; } week_day_table[] = {
{ HTTP_STR("Mon, "), HTTP_WEEKDAY_MON },
{ HTTP_STR("Tue, "), HTTP_WEEKDAY_TUE },
{ HTTP_STR("Wed, "), HTTP_WEEKDAY_WED },
{ HTTP_STR("Thu, "), HTTP_WEEKDAY_THU },
{ HTTP_STR("Fri, "), HTTP_WEEKDAY_FRI },
{ HTTP_STR("Sat, "), HTTP_WEEKDAY_SAT },
{ HTTP_STR("Sun, "), HTTP_WEEKDAY_SUN },
};
bool found = false;
for (int i = 0; i < HTTP_COUNT(week_day_table); i++)
if (consume_str(s, week_day_table[i].str)) {
out->week_day = week_day_table[i].val;
found = true;
break;
}
if (!found)
return -1;
if (1 >= s->len - s->cur
|| !is_digit(s->src[s->cur+0])
|| !is_digit(s->src[s->cur+1]))
return -1;
out->day
= (s->src[s->cur+0] - '0') * 10
+ (s->src[s->cur+1] - '0') * 1;
s->cur += 2;
struct { HTTP_String str; HTTP_Month val; } month_table[] = {
{ HTTP_STR(" Jan "), HTTP_MONTH_JAN },
{ HTTP_STR(" Feb "), HTTP_MONTH_FEB },
{ HTTP_STR(" Mar "), HTTP_MONTH_MAR },
{ HTTP_STR(" Apr "), HTTP_MONTH_APR },
{ HTTP_STR(" May "), HTTP_MONTH_MAY },
{ HTTP_STR(" Jun "), HTTP_MONTH_JUN },
{ HTTP_STR(" Jul "), HTTP_MONTH_JUL },
{ HTTP_STR(" Aug "), HTTP_MONTH_AUG },
{ HTTP_STR(" Sep "), HTTP_MONTH_SEP },
{ HTTP_STR(" Oct "), HTTP_MONTH_OCT },
{ HTTP_STR(" Nov "), HTTP_MONTH_NOV },
{ HTTP_STR(" Dec "), HTTP_MONTH_DEC },
};
found = false;
for (int i = 0; i < HTTP_COUNT(month_table); i++)
if (consume_str(s, month_table[i].str)) {
out->month = month_table[i].val;
found = true;
break;
}
if (!found)
return -1;
if (3 >= s->len - s->cur
|| !is_digit(s->src[s->cur+0])
|| !is_digit(s->src[s->cur+1])
|| !is_digit(s->src[s->cur+2])
|| !is_digit(s->src[s->cur+3]))
return -1;
out->year
= (s->src[s->cur+0] - '0') * 1000
+ (s->src[s->cur+1] - '0') * 100
+ (s->src[s->cur+2] - '0') * 10
+ (s->src[s->cur+3] - '0') * 1;
s->cur += 4;
if (s->cur == s->len || s->src[s->cur] != ' ')
return -1;
s->cur++;
if (7 >= s->len - s->cur
|| !is_digit(s->src[s->cur+0])
|| !is_digit(s->src[s->cur+1])
|| s->src[s->cur+2] != ':'
|| !is_digit(s->src[s->cur+3])
|| !is_digit(s->src[s->cur+4])
|| s->src[s->cur+5] != ':'
|| !is_digit(s->src[s->cur+6])
|| !is_digit(s->src[s->cur+7])
|| s->src[s->cur+8] != ' '
|| s->src[s->cur+9] != 'G'
|| s->src[s->cur+10] != 'M'
|| s->src[s->cur+11] != 'T')
return -1;
out->hour
= (s->src[s->cur+0] - '0') * 10
+ (s->src[s->cur+1] - '0') * 1;
out->minute
= (s->src[s->cur+3] - '0') * 10
+ (s->src[s->cur+4] - '0') * 1;
out->second
= (s->src[s->cur+6] - '0') * 10
+ (s->src[s->cur+7] - '0') * 1;
s->cur += 12;
return 0;
}
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs,
// ; whitespace, DQUOTE, comma, semicolon,
// ; and backslash
static bool is_cookie_octet(char c)
{
return c == 0x21 ||
(c >= 0x23 && c <= 0x2B) ||
(c >= 0x2D && c <= 0x3A) ||
(c >= 0x3C && c <= 0x5B) ||
(c >= 0x5D && c <= 0x7E);
}
int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out)
{
Scanner s = { str.ptr, str.len, 0 };
// cookie-name = token
if (s.cur == s.len || !is_tchar(s.src[s.cur]))
return -1;
int off = s.cur;
do
s.cur++;
while (s.cur < s.len && is_tchar(s.src[s.cur]));
out->name = (HTTP_String) { s.src + off, s.cur - off };
// cookie-pair = cookie-name "=" cookie-value
if (s.cur == s.len || s.src[s.cur] != '=')
return -1;
s.cur++;
// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
if (s.cur < s.len && s.src[s.cur] == '"') {
s.cur++; // Consume opening double quote
int off = s.cur;
while (s.cur < s.len && is_cookie_octet(s.src[s.cur]))
s.cur++;
if (s.cur == s.len || s.src[s.cur] != '"')
return -1; // Missing closing double quote
out->value = (HTTP_String) { s.src + off, s.cur - off };
s.cur++; // Consume closing double quote
} else {
int off = s.cur;
while (s.cur < s.len && is_cookie_octet(s.src[s.cur]))
s.cur++;
out->value = (HTTP_String) { s.src + off, s.cur - off };
}
// *( ";" SP cookie-av )
//
// cookie-av = expires-av / max-age-av / domain-av /
// path-av / secure-av / httponly-av /
// extension-av
out->secure = false;
out->http_only = false;
out->have_date = false;
out->have_max_age = false;
out->have_domain = false;
out->have_path = false;
while (consume_str(&s, HTTP_STR("; "))) {
if (consume_str(&s, HTTP_STR("Expires="))) {
// expires-av = "Expires=" sane-cookie-date
if (parse_date(&s, &out->date) < 0)
return -1;
out->have_date = true;
} else if (consume_str(&s, HTTP_STR("Max-Age="))) {
// max-age-av = "Max-Age=" non-zero-digit *DIGIT
uint32_t value = 0;
if (s.cur == s.len || !is_digit(s.src[s.cur]))
return -1;
do {
int d = s.src[s.cur++] - '0';
if (value > (UINT32_MAX - d) / 10)
return -1;
value = value * 10 + d;
} while (s.cur < s.len && is_digit(s.src[s.cur]));
out->have_max_age = true;
out->max_age = value;
} else if (consume_str(&s, HTTP_STR("Domain="))) {
// domain-av = "Domain=" domain-value
// domain-value = <subdomain>
// ; defined in RFC 1034, Section 3.5
//
// From RFC 1034:
// <subdomain> ::= <label> | <subdomain> "." <label>
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
// <let-dig-hyp> ::= <let-dig> | "-"
// <let-dig> ::= <letter> | <digit>
// <letter> ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
// <digit> ::= any one of the ten digits 0 through 9
//
// If my understanding is correct, a domain is a list of labels
// concatenated by dots. Each label may contain letters, digits,
// hyphens, but the first character must be a letter and the last
// one can't be a hyphen.
int off = s.cur;
if (s.cur == s.len || !is_alpha(s.src[s.cur]))
return -1;
do
s.cur++;
while (s.cur < s.len && (
is_digit(s.src[s.cur]) ||
is_alpha(s.src[s.cur]) ||
s.src[s.cur] == '-'));
if (s.src[s.cur-1] == '-')
return -1;
while (s.cur < s.len && s.src[s.cur] == '.') {
s.cur++; // Consume dot
if (s.cur == s.len || !is_alpha(s.src[s.cur]))
return -1;
do
s.cur++;
while (s.cur < s.len && (
is_digit(s.src[s.cur]) ||
is_alpha(s.src[s.cur]) ||
s.src[s.cur] == '-'));
if (s.src[s.cur-1] == '-')
return -1;
}
out->have_domain = true;
out->domain = (HTTP_String) { s.src + off, s.cur - off };
} else if (consume_str(&s, HTTP_STR("Path="))) {
// path-av = "Path=" path-value
// path-value = <any CHAR except CTLs or ";">
int off = s.cur;
while (s.cur < s.len && s.src[s.cur] >= 0x20 && s.src[s.cur] != 0x7F && s.src[s.cur] != ';')
s.cur++;
out->have_path = true;
out->path = (HTTP_String) { s.src + off, s.cur - off };
} else if (consume_str(&s, HTTP_STR("Secure"))) {
// secure-av = "Secure"
out->secure = true;
} else if (consume_str(&s, HTTP_STR("HttpOnly"))) {
// httponly-av = "HttpOnly"
out->http_only = true;
} else {
return -1; // Invalid attribute
}
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////
// src/thread.c
////////////////////////////////////////////////////////////////////////////////////////