Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddae5e721f |
@@ -1601,13 +1601,14 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out)
|
|||||||
//
|
//
|
||||||
// cookie-av = expires-av / max-age-av / domain-av /
|
// cookie-av = expires-av / max-age-av / domain-av /
|
||||||
// path-av / secure-av / httponly-av /
|
// path-av / secure-av / httponly-av /
|
||||||
// extension-av
|
// samesite-av / extension-av
|
||||||
out->secure = false;
|
out->secure = false;
|
||||||
out->http_only = false;
|
out->http_only = false;
|
||||||
out->have_date = false;
|
out->have_date = false;
|
||||||
out->have_max_age = false;
|
out->have_max_age = false;
|
||||||
out->have_domain = false;
|
out->have_domain = false;
|
||||||
out->have_path = false;
|
out->have_path = false;
|
||||||
|
out->have_samesite = false;
|
||||||
while (consume_str(&s, HTTP_STR("; "))) {
|
while (consume_str(&s, HTTP_STR("; "))) {
|
||||||
if (consume_str(&s, HTTP_STR("Expires="))) {
|
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,
|
// concatenated by dots. Each label may contain letters, digits,
|
||||||
// hyphens, but the first character must be a letter and the last
|
// hyphens, but the first character must be a letter and the last
|
||||||
// one can't be a hyphen.
|
// 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;
|
int off = s.cur;
|
||||||
if (s.cur == s.len || !is_alpha(s.src[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"
|
// httponly-av = "HttpOnly"
|
||||||
out->http_only = true;
|
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 {
|
} 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 };
|
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
|
// TODO: test this function
|
||||||
|
// Domain matching is case-insensitive per RFC 6265
|
||||||
static bool is_subdomain(HTTP_String domain, HTTP_String subdomain)
|
static bool is_subdomain(HTTP_String domain, HTTP_String subdomain)
|
||||||
{
|
{
|
||||||
if (http_streq(domain, subdomain))
|
if (http_streqcase(domain, subdomain))
|
||||||
return true; // Exact match
|
return true; // Exact match
|
||||||
|
|
||||||
if (domain.len > subdomain.len)
|
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,
|
subdomain.ptr + subdomain.len - domain.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 false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -3754,13 +3839,15 @@ static bool is_subpath(HTTP_String path, HTTP_String subpath)
|
|||||||
return http_streq(path, 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) {
|
if (entry.exact_domain) {
|
||||||
// Cookie domain and URL domain must match exactly
|
// Cookie domain and URL domain must match exactly (case-insensitive)
|
||||||
if (!http_streq(entry.domain, url.authority.host.text))
|
if (!http_streqcase(entry.domain, url.authority.host.text))
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// The URL's domain must match or be a subdomain of the cookie's domain
|
// 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);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
|
|
||||||
// Find all entries from the cookie jar that should
|
// 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_Client *client = builder.client;
|
||||||
HTTP_CookieJar *cookie_jar = &client->cookie_jar;
|
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++) {
|
for (int i = 0; i < cookie_jar->count; i++) {
|
||||||
HTTP_CookieJarEntry entry = cookie_jar->items[i];
|
HTTP_CookieJarEntry entry = cookie_jar->items[i];
|
||||||
if (should_send_cookie(entry, conn->url)) {
|
if (should_send_cookie(entry, conn->url, current_time)) {
|
||||||
// TODO: Adding one header per cookie may cause the number of
|
if (first_cookie) {
|
||||||
// headers to increase significantly. Should probably group
|
byte_queue_write(&conn->output, "Cookie: ", 8);
|
||||||
// 3-4 cookies in the same headers.
|
first_cookie = false;
|
||||||
byte_queue_write(&conn->output, "Cookie: ", 8);
|
} else {
|
||||||
|
byte_queue_write(&conn->output, "; ", 2);
|
||||||
|
}
|
||||||
byte_queue_write(&conn->output, entry.name.ptr, entry.name.len);
|
byte_queue_write(&conn->output, entry.name.ptr, entry.name.len);
|
||||||
byte_queue_write(&conn->output, "=", 1);
|
byte_queue_write(&conn->output, "=", 1);
|
||||||
byte_queue_write(&conn->output, entry.value.ptr, entry.value.len);
|
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;
|
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
|
||||||
}
|
}
|
||||||
@@ -4051,12 +4145,36 @@ error:
|
|||||||
return conn->result;
|
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,
|
static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
||||||
HTTP_Header set_cookie, HTTP_String domain, HTTP_String path)
|
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;
|
HTTP_SetCookie parsed;
|
||||||
if (http_parse_set_cookie(set_cookie.value, &parsed) < 0)
|
if (http_parse_set_cookie(set_cookie.value, &parsed) < 0)
|
||||||
return; // Ignore invalid Set-Cookie headers
|
return; // Ignore invalid Set-Cookie headers
|
||||||
@@ -4065,9 +4183,15 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
|
|
||||||
entry.name = parsed.name;
|
entry.name = parsed.name;
|
||||||
entry.value = parsed.value;
|
entry.value = parsed.value;
|
||||||
|
entry.creation_time = time(NULL);
|
||||||
|
|
||||||
if (parsed.have_domain) {
|
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.exact_domain = false;
|
||||||
entry.domain = parsed.domain;
|
entry.domain = parsed.domain;
|
||||||
} else {
|
} else {
|
||||||
@@ -4079,14 +4203,37 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
entry.exact_path = false;
|
entry.exact_path = false;
|
||||||
entry.path = parsed.path;
|
entry.path = parsed.path;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Set the path to the current endpoint minus one level
|
// Use default path computation per RFC 6265
|
||||||
entry.exact_path = true;
|
HTTP_String default_path = compute_default_cookie_path(path);
|
||||||
entry.path = path;
|
entry.exact_path = false;
|
||||||
|
entry.path = default_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.secure = parsed.secure;
|
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);
|
char *p = malloc(entry.name.len + entry.value.len + entry.domain.len + entry.path.len);
|
||||||
if (p == NULL)
|
if (p == NULL)
|
||||||
return;
|
return;
|
||||||
@@ -4107,17 +4254,45 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
entry.path.ptr = p;
|
entry.path.ptr = p;
|
||||||
p += entry.path.len;
|
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,
|
static void save_cookies(HTTP_CookieJar *cookie_jar,
|
||||||
HTTP_Header *headers, int num_headers,
|
HTTP_Header *headers, int num_headers,
|
||||||
HTTP_String domain, HTTP_String path)
|
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++)
|
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);
|
save_one_cookie(cookie_jar, headers[i], domain, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
@@ -257,6 +258,12 @@ typedef struct {
|
|||||||
int second;
|
int second;
|
||||||
} HTTP_Date;
|
} HTTP_Date;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
HTTP_SAMESITE_NONE,
|
||||||
|
HTTP_SAMESITE_LAX,
|
||||||
|
HTTP_SAMESITE_STRICT,
|
||||||
|
} HTTP_SameSite;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
HTTP_String name;
|
HTTP_String name;
|
||||||
HTTP_String value;
|
HTTP_String value;
|
||||||
@@ -275,6 +282,9 @@ typedef struct {
|
|||||||
|
|
||||||
bool have_path;
|
bool have_path;
|
||||||
HTTP_String path;
|
HTTP_String path;
|
||||||
|
|
||||||
|
bool have_samesite;
|
||||||
|
HTTP_SameSite samesite;
|
||||||
} HTTP_SetCookie;
|
} HTTP_SetCookie;
|
||||||
|
|
||||||
// Parses a Set-Cookie header value
|
// Parses a Set-Cookie header value
|
||||||
@@ -875,6 +885,13 @@ typedef struct {
|
|||||||
// This cookie can only be sent over HTTPS
|
// This cookie can only be sent over HTTPS
|
||||||
bool secure;
|
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;
|
} HTTP_CookieJarEntry;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
+169
-24
@@ -100,10 +100,65 @@ HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client)
|
|||||||
return (HTTP_RequestBuilder) { client, i, client->conns[i].gen };
|
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
|
// TODO: test this function
|
||||||
|
// Domain matching is case-insensitive per RFC 6265
|
||||||
static bool is_subdomain(HTTP_String domain, HTTP_String subdomain)
|
static bool is_subdomain(HTTP_String domain, HTTP_String subdomain)
|
||||||
{
|
{
|
||||||
if (http_streq(domain, subdomain))
|
if (http_streqcase(domain, subdomain))
|
||||||
return true; // Exact match
|
return true; // Exact match
|
||||||
|
|
||||||
if (domain.len > subdomain.len)
|
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,
|
subdomain.ptr + subdomain.len - domain.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 false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -132,13 +187,15 @@ static bool is_subpath(HTTP_String path, HTTP_String subpath)
|
|||||||
return http_streq(path, 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) {
|
if (entry.exact_domain) {
|
||||||
// Cookie domain and URL domain must match exactly
|
// Cookie domain and URL domain must match exactly (case-insensitive)
|
||||||
if (!http_streq(entry.domain, url.authority.host.text))
|
if (!http_streqcase(entry.domain, url.authority.host.text))
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// The URL's domain must match or be a subdomain of the cookie's domain
|
// 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);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
|
|
||||||
// Find all entries from the cookie jar that should
|
// 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_Client *client = builder.client;
|
||||||
HTTP_CookieJar *cookie_jar = &client->cookie_jar;
|
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++) {
|
for (int i = 0; i < cookie_jar->count; i++) {
|
||||||
HTTP_CookieJarEntry entry = cookie_jar->items[i];
|
HTTP_CookieJarEntry entry = cookie_jar->items[i];
|
||||||
if (should_send_cookie(entry, conn->url)) {
|
if (should_send_cookie(entry, conn->url, current_time)) {
|
||||||
// TODO: Adding one header per cookie may cause the number of
|
if (first_cookie) {
|
||||||
// headers to increase significantly. Should probably group
|
byte_queue_write(&conn->output, "Cookie: ", 8);
|
||||||
// 3-4 cookies in the same headers.
|
first_cookie = false;
|
||||||
byte_queue_write(&conn->output, "Cookie: ", 8);
|
} else {
|
||||||
|
byte_queue_write(&conn->output, "; ", 2);
|
||||||
|
}
|
||||||
byte_queue_write(&conn->output, entry.name.ptr, entry.name.len);
|
byte_queue_write(&conn->output, entry.name.ptr, entry.name.len);
|
||||||
byte_queue_write(&conn->output, "=", 1);
|
byte_queue_write(&conn->output, "=", 1);
|
||||||
byte_queue_write(&conn->output, entry.value.ptr, entry.value.len);
|
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;
|
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
|
||||||
}
|
}
|
||||||
@@ -429,12 +493,36 @@ error:
|
|||||||
return conn->result;
|
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,
|
static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
||||||
HTTP_Header set_cookie, HTTP_String domain, HTTP_String path)
|
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;
|
HTTP_SetCookie parsed;
|
||||||
if (http_parse_set_cookie(set_cookie.value, &parsed) < 0)
|
if (http_parse_set_cookie(set_cookie.value, &parsed) < 0)
|
||||||
return; // Ignore invalid Set-Cookie headers
|
return; // Ignore invalid Set-Cookie headers
|
||||||
@@ -443,9 +531,15 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
|
|
||||||
entry.name = parsed.name;
|
entry.name = parsed.name;
|
||||||
entry.value = parsed.value;
|
entry.value = parsed.value;
|
||||||
|
entry.creation_time = time(NULL);
|
||||||
|
|
||||||
if (parsed.have_domain) {
|
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.exact_domain = false;
|
||||||
entry.domain = parsed.domain;
|
entry.domain = parsed.domain;
|
||||||
} else {
|
} else {
|
||||||
@@ -457,14 +551,37 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
entry.exact_path = false;
|
entry.exact_path = false;
|
||||||
entry.path = parsed.path;
|
entry.path = parsed.path;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Set the path to the current endpoint minus one level
|
// Use default path computation per RFC 6265
|
||||||
entry.exact_path = true;
|
HTTP_String default_path = compute_default_cookie_path(path);
|
||||||
entry.path = path;
|
entry.exact_path = false;
|
||||||
|
entry.path = default_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.secure = parsed.secure;
|
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);
|
char *p = malloc(entry.name.len + entry.value.len + entry.domain.len + entry.path.len);
|
||||||
if (p == NULL)
|
if (p == NULL)
|
||||||
return;
|
return;
|
||||||
@@ -485,17 +602,45 @@ static void save_one_cookie(HTTP_CookieJar *cookie_jar,
|
|||||||
entry.path.ptr = p;
|
entry.path.ptr = p;
|
||||||
p += entry.path.len;
|
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,
|
static void save_cookies(HTTP_CookieJar *cookie_jar,
|
||||||
HTTP_Header *headers, int num_headers,
|
HTTP_Header *headers, int num_headers,
|
||||||
HTTP_String domain, HTTP_String path)
|
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++)
|
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);
|
save_one_cookie(cookie_jar, headers[i], domain, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ typedef struct {
|
|||||||
// This cookie can only be sent over HTTPS
|
// This cookie can only be sent over HTTPS
|
||||||
bool secure;
|
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;
|
} HTTP_CookieJarEntry;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
|||||||
+32
-2
@@ -1475,13 +1475,14 @@ int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out)
|
|||||||
//
|
//
|
||||||
// cookie-av = expires-av / max-age-av / domain-av /
|
// cookie-av = expires-av / max-age-av / domain-av /
|
||||||
// path-av / secure-av / httponly-av /
|
// path-av / secure-av / httponly-av /
|
||||||
// extension-av
|
// samesite-av / extension-av
|
||||||
out->secure = false;
|
out->secure = false;
|
||||||
out->http_only = false;
|
out->http_only = false;
|
||||||
out->have_date = false;
|
out->have_date = false;
|
||||||
out->have_max_age = false;
|
out->have_max_age = false;
|
||||||
out->have_domain = false;
|
out->have_domain = false;
|
||||||
out->have_path = false;
|
out->have_path = false;
|
||||||
|
out->have_samesite = false;
|
||||||
while (consume_str(&s, HTTP_STR("; "))) {
|
while (consume_str(&s, HTTP_STR("; "))) {
|
||||||
if (consume_str(&s, HTTP_STR("Expires="))) {
|
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,
|
// concatenated by dots. Each label may contain letters, digits,
|
||||||
// hyphens, but the first character must be a letter and the last
|
// hyphens, but the first character must be a letter and the last
|
||||||
// one can't be a hyphen.
|
// 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;
|
int off = s.cur;
|
||||||
if (s.cur == s.len || !is_alpha(s.src[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"
|
// httponly-av = "HttpOnly"
|
||||||
out->http_only = true;
|
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 {
|
} 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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ typedef struct {
|
|||||||
int second;
|
int second;
|
||||||
} HTTP_Date;
|
} HTTP_Date;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
HTTP_SAMESITE_NONE,
|
||||||
|
HTTP_SAMESITE_LAX,
|
||||||
|
HTTP_SAMESITE_STRICT,
|
||||||
|
} HTTP_SameSite;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
HTTP_String name;
|
HTTP_String name;
|
||||||
HTTP_String value;
|
HTTP_String value;
|
||||||
@@ -148,6 +154,9 @@ typedef struct {
|
|||||||
|
|
||||||
bool have_path;
|
bool have_path;
|
||||||
HTTP_String path;
|
HTTP_String path;
|
||||||
|
|
||||||
|
bool have_samesite;
|
||||||
|
HTTP_SameSite samesite;
|
||||||
} HTTP_SetCookie;
|
} HTTP_SetCookie;
|
||||||
|
|
||||||
// Parses a Set-Cookie header value
|
// Parses a Set-Cookie header value
|
||||||
|
|||||||
Reference in New Issue
Block a user