From ddae5e721f0ab77229ac1007bcf746cbfb57a27a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Nov 2025 09:04:10 +0000 Subject: [PATCH] Enhance cookie jar implementation with comprehensive RFC 6265 support This commit adds missing cookie handling cases to improve RFC 6265 compliance: Cookie Expiration: - Add expiration checking for both Expires and Max-Age attributes - Max-Age takes precedence over Expires per RFC 6265 - Automatic removal of expired cookies when processing responses - Store creation_time for accurate Max-Age calculation Cookie Management: - Update/replace existing cookies with same name, domain, and path - Prevent duplicate cookie entries in the jar - Consolidate multiple cookies into single Cookie header when sending Domain Handling: - Add domain validation to prevent servers from setting cookies for unrelated domains - Case-insensitive domain matching per RFC 6265 - Handle leading dots in Domain attribute (strip per RFC 6265 Section 5.2.3) - Subdomain matching for cookies without exact_domain flag Path Handling: - Implement RFC 6265 Section 5.1.4 default path computation - Use request path minus last component when Path attribute not specified - Fallback to "/" for edge cases Parser Improvements: - Add SameSite attribute support (None, Lax, Strict) - Gracefully ignore unknown/extension cookie attributes instead of failing - Forward compatibility with future cookie attributes Other: - Add time.h include for time() and time_t - Add http_date_to_time() helper for expiration comparison - Add is_cookie_expired() helper for expiration checks - Add compute_default_cookie_path() per RFC 6265 --- chttp.c | 227 +++++++++++++++++++++++++++++++++++++++++++------ chttp.h | 17 ++++ src/client.c | 193 +++++++++++++++++++++++++++++++++++------ src/client.h | 7 ++ src/includes.h | 1 + src/parse.c | 34 +++++++- src/parse.h | 9 ++ 7 files changed, 436 insertions(+), 52 deletions(-) diff --git a/chttp.c b/chttp.c index 6365b76..d4de694 100644 --- a/chttp.c +++ b/chttp.c @@ -1601,13 +1601,14 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // // cookie-av = expires-av / max-age-av / domain-av / // path-av / secure-av / httponly-av / - // extension-av + // samesite-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; + out->have_samesite = false; while (consume_str(&s, HTTP_STR("; "))) { if (consume_str(&s, HTTP_STR("Expires="))) { @@ -1652,6 +1653,13 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // 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. + // + // RFC 6265 Section 5.2.3: If the first character of the attribute-value + // string is ".", then ignore the leading "." + + // Skip leading dot if present + if (s.cur < s.len && s.src[s.cur] == '.') + s.cur++; int off = s.cur; if (s.cur == s.len || !is_alpha(s.src[s.cur])) @@ -1707,8 +1715,30 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // httponly-av = "HttpOnly" out->http_only = true; + } else if (consume_str(&s, HTTP_STR("SameSite="))) { + + // samesite-av = "SameSite=" samesite-value + // samesite-value = "Strict" / "Lax" / "None" + if (consume_str(&s, HTTP_STR("Strict"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_STRICT; + } else if (consume_str(&s, HTTP_STR("Lax"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_LAX; + } else if (consume_str(&s, HTTP_STR("None"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_NONE; + } else { + // Invalid SameSite value, skip to next attribute + while (s.cur < s.len && s.src[s.cur] != ';') + s.cur++; + } + } else { - return -1; // Invalid attribute + // extension-av: Unknown attribute, skip to next semicolon + // This allows forward compatibility with new cookie attributes + while (s.cur < s.len && s.src[s.cur] != ';') + s.cur++; } } @@ -3722,10 +3752,65 @@ HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client) return (HTTP_RequestBuilder) { client, i, client->conns[i].gen }; } +// Convert HTTP_Date to Unix timestamp (seconds since 1970-01-01 00:00:00 UTC) +// This is a simplified implementation that doesn't account for all edge cases +static time_t http_date_to_time(HTTP_Date date) +{ + // Days in each month (non-leap year) + static const int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + // Calculate days since epoch (1970-01-01) + int days = 0; + + // Add days for complete years + for (int y = 1970; y < date.year; y++) { + bool is_leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); + days += is_leap ? 366 : 365; + } + + // Add days for complete months in current year + for (int m = 0; m < date.month; m++) { + days += days_in_month[m]; + // Add leap day if February and current year is leap year + if (m == 1) { // February + bool is_leap = (date.year % 4 == 0 && date.year % 100 != 0) || (date.year % 400 == 0); + if (is_leap) + days++; + } + } + + // Add remaining days + days += date.day - 1; + + // Convert to seconds and add time components + time_t timestamp = (time_t)days * 86400 + date.hour * 3600 + date.minute * 60 + date.second; + + return timestamp; +} + +// Check if a cookie has expired +static bool is_cookie_expired(HTTP_CookieJarEntry entry, time_t current_time) +{ + if (entry.have_max_age) { + // Max-Age takes precedence over Expires + time_t expires_at = entry.creation_time + entry.max_age; + return current_time >= expires_at; + } + + if (entry.have_expires) { + time_t expires_at = http_date_to_time(entry.expires); + return current_time >= expires_at; + } + + // No expiration set, cookie is a session cookie (never expires in this implementation) + return false; +} + // TODO: test this function +// Domain matching is case-insensitive per RFC 6265 static bool is_subdomain(HTTP_String domain, HTTP_String subdomain) { - if (http_streq(domain, subdomain)) + if (http_streqcase(domain, subdomain)) return true; // Exact match if (domain.len > subdomain.len) @@ -3735,7 +3820,7 @@ static bool is_subdomain(HTTP_String domain, HTTP_String subdomain) subdomain.ptr + subdomain.len - domain.len, domain.len }; - if (subdomain_suffix.ptr[-1] != '.' || !http_streq(domain, subdomain_suffix)) + if (subdomain_suffix.ptr[-1] != '.' || !http_streqcase(domain, subdomain_suffix)) return false; return true; @@ -3754,13 +3839,15 @@ static bool is_subpath(HTTP_String path, HTTP_String subpath) return http_streq(path, subpath); } -static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url) +static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url, time_t current_time) { - // TODO: If the cookie is expired, ignore it regardless + // Check if cookie has expired + if (is_cookie_expired(entry, current_time)) + return false; if (entry.exact_domain) { - // Cookie domain and URL domain must match exactly - if (!http_streq(entry.domain, url.authority.host.text)) + // Cookie domain and URL domain must match exactly (case-insensitive) + if (!http_streqcase(entry.domain, url.authority.host.text)) return false; } else { // The URL's domain must match or be a subdomain of the cookie's domain @@ -3910,22 +3997,29 @@ void http_request_builder_target(HTTP_RequestBuilder builder, byte_queue_write(&conn->output, "\r\n", 2); // Find all entries from the cookie jar that should - // be sent to this server and append headers for them + // be sent to this server and consolidate them into a single Cookie header HTTP_Client *client = builder.client; HTTP_CookieJar *cookie_jar = &client->cookie_jar; + time_t current_time = time(NULL); + + bool first_cookie = true; for (int i = 0; i < cookie_jar->count; i++) { HTTP_CookieJarEntry entry = cookie_jar->items[i]; - if (should_send_cookie(entry, conn->url)) { - // TODO: Adding one header per cookie may cause the number of - // headers to increase significantly. Should probably group - // 3-4 cookies in the same headers. - byte_queue_write(&conn->output, "Cookie: ", 8); + if (should_send_cookie(entry, conn->url, current_time)) { + if (first_cookie) { + byte_queue_write(&conn->output, "Cookie: ", 8); + first_cookie = false; + } else { + byte_queue_write(&conn->output, "; ", 2); + } byte_queue_write(&conn->output, entry.name.ptr, entry.name.len); byte_queue_write(&conn->output, "=", 1); byte_queue_write(&conn->output, entry.value.ptr, entry.value.len); - byte_queue_write(&conn->output, "\r\n", 2); } } + if (!first_cookie) { + byte_queue_write(&conn->output, "\r\n", 2); + } conn->state = HTTP_CLIENT_CONN_WAIT_HEADER; } @@ -4051,12 +4145,36 @@ error: return conn->result; } +// Compute default path from request path per RFC 6265 Section 5.1.4 +static HTTP_String compute_default_cookie_path(HTTP_String request_path) +{ + // If the uri-path is empty or does not begin with "/", return "/" + if (request_path.len == 0 || request_path.ptr[0] != '/') + return HTTP_STR("/"); + + // If the uri-path contains only a single "/" character, return "/" + if (request_path.len == 1) + return HTTP_STR("/"); + + // Find the last "/" before the last character + int last_slash = -1; + for (int i = request_path.len - 1; i > 0; i--) { + if (request_path.ptr[i] == '/') { + last_slash = i; + break; + } + } + + // Return path up to (but not including) the right-most "/" + if (last_slash > 0) + return (HTTP_String) { request_path.ptr, last_slash }; + + return HTTP_STR("/"); +} + static void save_one_cookie(HTTP_CookieJar *cookie_jar, HTTP_Header set_cookie, HTTP_String domain, HTTP_String path) { - if (cookie_jar->count == HTTP_COOKIE_JAR_CAPACITY) - return; // Cookie jar capacity reached - HTTP_SetCookie parsed; if (http_parse_set_cookie(set_cookie.value, &parsed) < 0) return; // Ignore invalid Set-Cookie headers @@ -4065,9 +4183,15 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.name = parsed.name; entry.value = parsed.value; + entry.creation_time = time(NULL); if (parsed.have_domain) { - // TODO: Check that the server can set a cookie for this domain + // Validate that the server can set a cookie for this domain + // Per RFC 6265 Section 5.3: The Domain attribute must domain-match the request host + if (!is_subdomain(parsed.domain, domain)) { + // Server tried to set a cookie for a domain it doesn't control + return; + } entry.exact_domain = false; entry.domain = parsed.domain; } else { @@ -4079,14 +4203,37 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.exact_path = false; entry.path = parsed.path; } else { - // TODO: Set the path to the current endpoint minus one level - entry.exact_path = true; - entry.path = path; + // Use default path computation per RFC 6265 + HTTP_String default_path = compute_default_cookie_path(path); + entry.exact_path = false; + entry.path = default_path; } entry.secure = parsed.secure; - // Now copy all fields + // Store expiration information + entry.have_expires = parsed.have_date; + if (parsed.have_date) + entry.expires = parsed.date; + + entry.have_max_age = parsed.have_max_age; + if (parsed.have_max_age) + entry.max_age = parsed.max_age; + + // Check if this cookie replaces an existing one + // Per RFC 6265: A cookie is identified by name, domain, and path + int existing_index = -1; + for (int i = 0; i < cookie_jar->count; i++) { + HTTP_CookieJarEntry *existing = &cookie_jar->items[i]; + if (http_streq(existing->name, entry.name) && + http_streqcase(existing->domain, entry.domain) && + http_streq(existing->path, entry.path)) { + existing_index = i; + break; + } + } + + // Allocate memory for all string fields char *p = malloc(entry.name.len + entry.value.len + entry.domain.len + entry.path.len); if (p == NULL) return; @@ -4107,17 +4254,45 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.path.ptr = p; p += entry.path.len; - cookie_jar->items[cookie_jar->count++] = entry; + if (existing_index >= 0) { + // Replace existing cookie + free(cookie_jar->items[existing_index].name.ptr); // This frees the entire allocated block + cookie_jar->items[existing_index] = entry; + } else { + // Add new cookie if there's space + if (cookie_jar->count < HTTP_COOKIE_JAR_CAPACITY) { + cookie_jar->items[cookie_jar->count++] = entry; + } else { + // No space, free the allocated memory + free(entry.name.ptr); + } + } } static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int num_headers, HTTP_String domain, HTTP_String path) { - // TODO: remove expired cookies + // Remove expired cookies from the jar + time_t current_time = time(NULL); + int write_idx = 0; + for (int read_idx = 0; read_idx < cookie_jar->count; read_idx++) { + if (!is_cookie_expired(cookie_jar->items[read_idx], current_time)) { + // Keep this cookie + if (write_idx != read_idx) { + cookie_jar->items[write_idx] = cookie_jar->items[read_idx]; + } + write_idx++; + } else { + // Free expired cookie + free(cookie_jar->items[read_idx].name.ptr); + } + } + cookie_jar->count = write_idx; + // Process Set-Cookie headers for (int i = 0; i < num_headers; i++) - if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) // TODO: headers are case-insensitive, right? + if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) save_one_cookie(cookie_jar, headers[i], domain, path); } diff --git a/chttp.h b/chttp.h index 0691528..25d9a4b 100644 --- a/chttp.h +++ b/chttp.h @@ -13,6 +13,7 @@ #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -257,6 +258,12 @@ typedef struct { int second; } HTTP_Date; +typedef enum { + HTTP_SAMESITE_NONE, + HTTP_SAMESITE_LAX, + HTTP_SAMESITE_STRICT, +} HTTP_SameSite; + typedef struct { HTTP_String name; HTTP_String value; @@ -275,6 +282,9 @@ typedef struct { bool have_path; HTTP_String path; + + bool have_samesite; + HTTP_SameSite samesite; } HTTP_SetCookie; // Parses a Set-Cookie header value @@ -875,6 +885,13 @@ typedef struct { // This cookie can only be sent over HTTPS bool secure; + // Expiration information + bool have_expires; + HTTP_Date expires; + bool have_max_age; + uint32_t max_age; + time_t creation_time; // Unix timestamp when cookie was created + } HTTP_CookieJarEntry; typedef struct { diff --git a/src/client.c b/src/client.c index fb307ff..6d14245 100644 --- a/src/client.c +++ b/src/client.c @@ -100,10 +100,65 @@ HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client) return (HTTP_RequestBuilder) { client, i, client->conns[i].gen }; } +// Convert HTTP_Date to Unix timestamp (seconds since 1970-01-01 00:00:00 UTC) +// This is a simplified implementation that doesn't account for all edge cases +static time_t http_date_to_time(HTTP_Date date) +{ + // Days in each month (non-leap year) + static const int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + // Calculate days since epoch (1970-01-01) + int days = 0; + + // Add days for complete years + for (int y = 1970; y < date.year; y++) { + bool is_leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); + days += is_leap ? 366 : 365; + } + + // Add days for complete months in current year + for (int m = 0; m < date.month; m++) { + days += days_in_month[m]; + // Add leap day if February and current year is leap year + if (m == 1) { // February + bool is_leap = (date.year % 4 == 0 && date.year % 100 != 0) || (date.year % 400 == 0); + if (is_leap) + days++; + } + } + + // Add remaining days + days += date.day - 1; + + // Convert to seconds and add time components + time_t timestamp = (time_t)days * 86400 + date.hour * 3600 + date.minute * 60 + date.second; + + return timestamp; +} + +// Check if a cookie has expired +static bool is_cookie_expired(HTTP_CookieJarEntry entry, time_t current_time) +{ + if (entry.have_max_age) { + // Max-Age takes precedence over Expires + time_t expires_at = entry.creation_time + entry.max_age; + return current_time >= expires_at; + } + + if (entry.have_expires) { + time_t expires_at = http_date_to_time(entry.expires); + return current_time >= expires_at; + } + + // No expiration set, cookie is a session cookie (never expires in this implementation) + return false; +} + // TODO: test this function +// Domain matching is case-insensitive per RFC 6265 static bool is_subdomain(HTTP_String domain, HTTP_String subdomain) { - if (http_streq(domain, subdomain)) + if (http_streqcase(domain, subdomain)) return true; // Exact match if (domain.len > subdomain.len) @@ -113,7 +168,7 @@ static bool is_subdomain(HTTP_String domain, HTTP_String subdomain) subdomain.ptr + subdomain.len - domain.len, domain.len }; - if (subdomain_suffix.ptr[-1] != '.' || !http_streq(domain, subdomain_suffix)) + if (subdomain_suffix.ptr[-1] != '.' || !http_streqcase(domain, subdomain_suffix)) return false; return true; @@ -132,13 +187,15 @@ static bool is_subpath(HTTP_String path, HTTP_String subpath) return http_streq(path, subpath); } -static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url) +static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url, time_t current_time) { - // TODO: If the cookie is expired, ignore it regardless + // Check if cookie has expired + if (is_cookie_expired(entry, current_time)) + return false; if (entry.exact_domain) { - // Cookie domain and URL domain must match exactly - if (!http_streq(entry.domain, url.authority.host.text)) + // Cookie domain and URL domain must match exactly (case-insensitive) + if (!http_streqcase(entry.domain, url.authority.host.text)) return false; } else { // The URL's domain must match or be a subdomain of the cookie's domain @@ -288,22 +345,29 @@ void http_request_builder_target(HTTP_RequestBuilder builder, byte_queue_write(&conn->output, "\r\n", 2); // Find all entries from the cookie jar that should - // be sent to this server and append headers for them + // be sent to this server and consolidate them into a single Cookie header HTTP_Client *client = builder.client; HTTP_CookieJar *cookie_jar = &client->cookie_jar; + time_t current_time = time(NULL); + + bool first_cookie = true; for (int i = 0; i < cookie_jar->count; i++) { HTTP_CookieJarEntry entry = cookie_jar->items[i]; - if (should_send_cookie(entry, conn->url)) { - // TODO: Adding one header per cookie may cause the number of - // headers to increase significantly. Should probably group - // 3-4 cookies in the same headers. - byte_queue_write(&conn->output, "Cookie: ", 8); + if (should_send_cookie(entry, conn->url, current_time)) { + if (first_cookie) { + byte_queue_write(&conn->output, "Cookie: ", 8); + first_cookie = false; + } else { + byte_queue_write(&conn->output, "; ", 2); + } byte_queue_write(&conn->output, entry.name.ptr, entry.name.len); byte_queue_write(&conn->output, "=", 1); byte_queue_write(&conn->output, entry.value.ptr, entry.value.len); - byte_queue_write(&conn->output, "\r\n", 2); } } + if (!first_cookie) { + byte_queue_write(&conn->output, "\r\n", 2); + } conn->state = HTTP_CLIENT_CONN_WAIT_HEADER; } @@ -429,12 +493,36 @@ error: return conn->result; } +// Compute default path from request path per RFC 6265 Section 5.1.4 +static HTTP_String compute_default_cookie_path(HTTP_String request_path) +{ + // If the uri-path is empty or does not begin with "/", return "/" + if (request_path.len == 0 || request_path.ptr[0] != '/') + return HTTP_STR("/"); + + // If the uri-path contains only a single "/" character, return "/" + if (request_path.len == 1) + return HTTP_STR("/"); + + // Find the last "/" before the last character + int last_slash = -1; + for (int i = request_path.len - 1; i > 0; i--) { + if (request_path.ptr[i] == '/') { + last_slash = i; + break; + } + } + + // Return path up to (but not including) the right-most "/" + if (last_slash > 0) + return (HTTP_String) { request_path.ptr, last_slash }; + + return HTTP_STR("/"); +} + static void save_one_cookie(HTTP_CookieJar *cookie_jar, HTTP_Header set_cookie, HTTP_String domain, HTTP_String path) { - if (cookie_jar->count == HTTP_COOKIE_JAR_CAPACITY) - return; // Cookie jar capacity reached - HTTP_SetCookie parsed; if (http_parse_set_cookie(set_cookie.value, &parsed) < 0) return; // Ignore invalid Set-Cookie headers @@ -443,9 +531,15 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.name = parsed.name; entry.value = parsed.value; + entry.creation_time = time(NULL); if (parsed.have_domain) { - // TODO: Check that the server can set a cookie for this domain + // Validate that the server can set a cookie for this domain + // Per RFC 6265 Section 5.3: The Domain attribute must domain-match the request host + if (!is_subdomain(parsed.domain, domain)) { + // Server tried to set a cookie for a domain it doesn't control + return; + } entry.exact_domain = false; entry.domain = parsed.domain; } else { @@ -457,14 +551,37 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.exact_path = false; entry.path = parsed.path; } else { - // TODO: Set the path to the current endpoint minus one level - entry.exact_path = true; - entry.path = path; + // Use default path computation per RFC 6265 + HTTP_String default_path = compute_default_cookie_path(path); + entry.exact_path = false; + entry.path = default_path; } entry.secure = parsed.secure; - // Now copy all fields + // Store expiration information + entry.have_expires = parsed.have_date; + if (parsed.have_date) + entry.expires = parsed.date; + + entry.have_max_age = parsed.have_max_age; + if (parsed.have_max_age) + entry.max_age = parsed.max_age; + + // Check if this cookie replaces an existing one + // Per RFC 6265: A cookie is identified by name, domain, and path + int existing_index = -1; + for (int i = 0; i < cookie_jar->count; i++) { + HTTP_CookieJarEntry *existing = &cookie_jar->items[i]; + if (http_streq(existing->name, entry.name) && + http_streqcase(existing->domain, entry.domain) && + http_streq(existing->path, entry.path)) { + existing_index = i; + break; + } + } + + // Allocate memory for all string fields char *p = malloc(entry.name.len + entry.value.len + entry.domain.len + entry.path.len); if (p == NULL) return; @@ -485,17 +602,45 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar, entry.path.ptr = p; p += entry.path.len; - cookie_jar->items[cookie_jar->count++] = entry; + if (existing_index >= 0) { + // Replace existing cookie + free(cookie_jar->items[existing_index].name.ptr); // This frees the entire allocated block + cookie_jar->items[existing_index] = entry; + } else { + // Add new cookie if there's space + if (cookie_jar->count < HTTP_COOKIE_JAR_CAPACITY) { + cookie_jar->items[cookie_jar->count++] = entry; + } else { + // No space, free the allocated memory + free(entry.name.ptr); + } + } } static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int num_headers, HTTP_String domain, HTTP_String path) { - // TODO: remove expired cookies + // Remove expired cookies from the jar + time_t current_time = time(NULL); + int write_idx = 0; + for (int read_idx = 0; read_idx < cookie_jar->count; read_idx++) { + if (!is_cookie_expired(cookie_jar->items[read_idx], current_time)) { + // Keep this cookie + if (write_idx != read_idx) { + cookie_jar->items[write_idx] = cookie_jar->items[read_idx]; + } + write_idx++; + } else { + // Free expired cookie + free(cookie_jar->items[read_idx].name.ptr); + } + } + cookie_jar->count = write_idx; + // Process Set-Cookie headers for (int i = 0; i < num_headers; i++) - if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) // TODO: headers are case-insensitive, right? + if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) save_one_cookie(cookie_jar, headers[i], domain, path); } diff --git a/src/client.h b/src/client.h index ef5c4df..516fc4e 100644 --- a/src/client.h +++ b/src/client.h @@ -41,6 +41,13 @@ typedef struct { // This cookie can only be sent over HTTPS bool secure; + // Expiration information + bool have_expires; + HTTP_Date expires; + bool have_max_age; + uint32_t max_age; + time_t creation_time; // Unix timestamp when cookie was created + } HTTP_CookieJarEntry; typedef struct { diff --git a/src/includes.h b/src/includes.h index e555527..9ba5df6 100644 --- a/src/includes.h +++ b/src/includes.h @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN diff --git a/src/parse.c b/src/parse.c index a9624c1..74b9c1c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -1475,13 +1475,14 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // // cookie-av = expires-av / max-age-av / domain-av / // path-av / secure-av / httponly-av / - // extension-av + // samesite-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; + out->have_samesite = false; while (consume_str(&s, HTTP_STR("; "))) { if (consume_str(&s, HTTP_STR("Expires="))) { @@ -1526,6 +1527,13 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // 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. + // + // RFC 6265 Section 5.2.3: If the first character of the attribute-value + // string is ".", then ignore the leading "." + + // Skip leading dot if present + if (s.cur < s.len && s.src[s.cur] == '.') + s.cur++; int off = s.cur; if (s.cur == s.len || !is_alpha(s.src[s.cur])) @@ -1581,8 +1589,30 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out) // httponly-av = "HttpOnly" out->http_only = true; + } else if (consume_str(&s, HTTP_STR("SameSite="))) { + + // samesite-av = "SameSite=" samesite-value + // samesite-value = "Strict" / "Lax" / "None" + if (consume_str(&s, HTTP_STR("Strict"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_STRICT; + } else if (consume_str(&s, HTTP_STR("Lax"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_LAX; + } else if (consume_str(&s, HTTP_STR("None"))) { + out->have_samesite = true; + out->samesite = HTTP_SAMESITE_NONE; + } else { + // Invalid SameSite value, skip to next attribute + while (s.cur < s.len && s.src[s.cur] != ';') + s.cur++; + } + } else { - return -1; // Invalid attribute + // extension-av: Unknown attribute, skip to next semicolon + // This allows forward compatibility with new cookie attributes + while (s.cur < s.len && s.src[s.cur] != ';') + s.cur++; } } diff --git a/src/parse.h b/src/parse.h index 0f3a0ec..bdbfc62 100644 --- a/src/parse.h +++ b/src/parse.h @@ -130,6 +130,12 @@ typedef struct { int second; } HTTP_Date; +typedef enum { + HTTP_SAMESITE_NONE, + HTTP_SAMESITE_LAX, + HTTP_SAMESITE_STRICT, +} HTTP_SameSite; + typedef struct { HTTP_String name; HTTP_String value; @@ -148,6 +154,9 @@ typedef struct { bool have_path; HTTP_String path; + + bool have_samesite; + HTTP_SameSite samesite; } HTTP_SetCookie; // Parses a Set-Cookie header value