Merge branch 'cookie_jar' into refactoring

# Conflicts:
#	chttp.h
#	src/client.h
This commit is contained in:
2025-11-23 22:36:01 +01:00
7 changed files with 1613 additions and 426 deletions
+673 -193
View File
File diff suppressed because it is too large Load Diff
+132 -18
View File
@@ -193,6 +193,66 @@ int http_get_param_i (HTTP_String body, HTTP_String str);
// domain an port. If port is -1, the default value of 80 is assumed.
bool http_match_host(HTTP_Request *req, HTTP_String domain, int port);
// Date and cookie types for Set-Cookie header parsing
typedef enum {
HTTP_WEEKDAY_MON,
HTTP_WEEKDAY_TUE,
HTTP_WEEKDAY_WED,
HTTP_WEEKDAY_THU,
HTTP_WEEKDAY_FRI,
HTTP_WEEKDAY_SAT,
HTTP_WEEKDAY_SUN,
} HTTP_WeekDay;
typedef enum {
HTTP_MONTH_JAN,
HTTP_MONTH_FEB,
HTTP_MONTH_MAR,
HTTP_MONTH_APR,
HTTP_MONTH_MAY,
HTTP_MONTH_JUN,
HTTP_MONTH_JUL,
HTTP_MONTH_AUG,
HTTP_MONTH_SEP,
HTTP_MONTH_OCT,
HTTP_MONTH_NOV,
HTTP_MONTH_DEC,
} HTTP_Month;
typedef struct {
HTTP_WeekDay week_day;
int day;
HTTP_Month month;
int year;
int hour;
int minute;
int second;
} HTTP_Date;
typedef struct {
HTTP_String name;
HTTP_String value;
bool secure;
bool http_only;
bool have_date;
HTTP_Date date;
bool have_max_age;
uint32_t max_age;
bool have_domain;
HTTP_String domain;
bool have_path;
HTTP_String path;
} HTTP_SetCookie;
// Parses a Set-Cookie header value
// Returns 0 on success, -1 on error
int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out);
////////////////////////////////////////////////////////////////////////////////////////
// src/thread.h
////////////////////////////////////////////////////////////////////////////////////////
@@ -764,6 +824,44 @@ int http_create_test_certificate(HTTP_String C, HTTP_String O, HTTP_String CN,
#define HTTP_CLIENT_CAPACITY (1<<7)
#endif
#ifndef HTTP_COOKIE_JAR_CAPACITY
// Maximum number of cookies that can be associated to a
// single client.
#define HTTP_COOKIE_JAR_CAPACITY 128
#endif
typedef struct {
// Cookie name and value
HTTP_String name;
HTTP_String value;
// If the "exact_domain" is true, the cookie
// can only be sent to the exact domain referred
// to by "domain" (which is never empty). If
// "exact_domain" is false, then the cookie is
// compatible with subdomains.
bool exact_domain;
HTTP_String domain;
// If "exact_path" is set, the cookie is only
// compatible with requests to paths that match
// "path" exactly. If "exact_path" is not set,
// then any path that starts with "path" is
// compatible with the cookie.
bool exact_path;
HTTP_String path;
// This cookie can only be sent over HTTPS
bool secure;
} HTTP_CookieJarEntry;
typedef struct {
int count;
HTTP_CookieJarEntry items[HTTP_COOKIE_JAR_CAPACITY];
} HTTP_CookieJar;
// Maximum number of descriptors the client will want
// to wait on. It's one per connection plus the wakeup
// self-pipe.
@@ -794,17 +892,31 @@ typedef struct {
// Generation counter for request builder validation
uint16_t gen;
// Opaque pointer set by the user while building
// the request. It's returned alongside the result.
void *user;
// TODO: comment
bool trace_bytes;
// Allocated copy of the URL string
HTTP_String url_buffer;
// Parsed URL for connection establishment
// All url.* pointers reference into url_buffer
HTTP_URL url;
// Data received from the server
ByteQueue input;
// Data being sent to the server
ByteQueue output;
// HTTP method for the request
HTTP_Method method;
// Parsed URL for connection establishment
HTTP_URL url;
// If the request is COMPLETE, indicates
// whether it completed with an error (-1)
// or a success (0). If it was a success,
// the response field is valid.
int result;
// Parsed response once complete
HTTP_Response response;
@@ -818,6 +930,9 @@ struct HTTP_Client {
uint32_t input_buffer_limit;
uint32_t output_buffer_limit;
// List of cookies created during this session
HTTP_CookieJar cookie_jar;
// Array of connections. The counter contains the
// number of structs such that state!=FREE.
int num_conns;
@@ -863,15 +978,14 @@ typedef struct {
uint16_t gen;
} HTTP_RequestBuilder;
// Create a new request builder object. If the response
// pointer is NULL, a brand new builder is created. If
// response isn't NULL (and http_free_response wasn't
// called on it yet), the connection associated to that
// previous exchange is reused. Note that it's up to the
// user to make sure the requests are targeting the same
// host. Returns 0 on success, -1 on error.
int http_client_get_builder(HTTP_Client *client,
HTTP_Response *response, HTTP_RequestBuilder *builder);
// Create a new request builder object.
HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client);
// TODO: comment
void http_request_builder_set_user(HTTP_RequestBuilder builder, void *user);
// TODO: comment
void http_request_builder_set_trace_bytes(HTTP_RequestBuilder builder, bool trace_bytes);
// Set the method and URL of the current request. This is the first
// function of the request builder that the user must call.
@@ -907,11 +1021,11 @@ int http_client_process_events(HTTP_Client *client,
// may be availabe. This function returns one of the
// buffered responses. If a request was available, true
// is returned. If no more are avaiable, false is returned.
// The returned response must either be freed using the
// http_free_response function or reused by passing it
// to http_client_get_builder.
// The returned response must be freed using the
// http_free_response function.
// TODO: Better comment talking about output arguments
bool http_client_next_response(HTTP_Client *client,
HTTP_Response **response);
HTTP_Response **response, void **user);
// Free a response object. You can't access its fields
// again after this.
+3 -4
View File
@@ -12,9 +12,7 @@ int main(void)
if (http_client_init(&client) < 0)
return -1;
HTTP_RequestBuilder builder;
if (http_client_get_builder(&client, NULL, &builder) < 0)
return -1;
HTTP_RequestBuilder builder = http_client_get_builder(&client);
http_request_builder_url(builder,
HTTP_METHOD_GET,
@@ -49,7 +47,8 @@ int main(void)
return -1;
HTTP_Response *response;
while (http_client_next_response(&client, &response)) {
void *user;
while (http_client_next_response(&client, &response, &user)) {
// TODO
http_free_response(response);
}
+405 -193
View File
@@ -1,15 +1,3 @@
static void http_client_conn_init(HTTP_ClientConn *conn,
SocketHandle handle, uint32_t input_buffer_limit,
uint32_t output_buffer_limit)
{
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
conn->handle = handle;
conn->gen = 0;
byte_queue_init(&conn->input, input_buffer_limit);
byte_queue_init(&conn->output, output_buffer_limit);
}
static void http_client_conn_free(HTTP_ClientConn *conn)
{
byte_queue_free(&conn->output);
@@ -21,6 +9,8 @@ int http_client_init(HTTP_Client *client)
client->input_buffer_limit = 1<<20;
client->output_buffer_limit = 1<<20;
client->cookie_jar.count = 0;
client->num_conns = 0;
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++) {
client->conns[i].state = HTTP_CLIENT_CONN_FREE;
@@ -40,6 +30,9 @@ void http_client_free(HTTP_Client *client)
{
socket_manager_free(&client->sockets);
for (int i = 0; i < client->cookie_jar.count; i++)
free(client->cookie_jar.items[i].name.ptr);
for (int i = 0, j = 0; j < client->num_conns; i++) {
HTTP_ClientConn *conn = &client->conns[i];
if (conn->state == HTTP_CLIENT_CONN_FREE)
@@ -94,48 +87,125 @@ request_builder_to_conn(HTTP_RequestBuilder builder)
return conn;
}
int http_client_get_builder(HTTP_Client *client,
HTTP_Response *response, HTTP_RequestBuilder *builder)
HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client)
{
HTTP_ClientConn *conn = NULL;
// Find a free connection slot
if (client->num_conns == HTTP_CLIENT_CAPACITY)
return (HTTP_RequestBuilder) { NULL, -1, -1 };
if (response != NULL && response->context != NULL) {
// Reuse the connection from the previous response
conn = (HTTP_ClientConn*) response->context;
int i = 0;
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE) {
i++;
assert(i < HTTP_CLIENT_CAPACITY);
}
client->num_conns++;
// Mark the response as freed
response->context = NULL;
client->conns[i].state = HTTP_CLIENT_CONN_WAIT_LINE;
client->conns[i].handle = SOCKET_HANDLE_INVALID;
client->conns[i].client = client;
client->conns[i].user = NULL;
client->conns[i].trace_bytes = false;
byte_queue_init(&client->conns[i].input, client->input_buffer_limit);
byte_queue_init(&client->conns[i].output, client->output_buffer_limit);
// Reset the connection for a new request
byte_queue_read_ack(&conn->input, byte_queue_read_buf(&conn->input).len);
byte_queue_read_ack(&conn->output, byte_queue_read_buf(&conn->output).len);
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
return (HTTP_RequestBuilder) { client, i, client->conns[i].gen };
}
// TODO: test this function
static bool is_subdomain(HTTP_String domain, HTTP_String subdomain)
{
if (http_streq(domain, subdomain))
return true; // Exact match
if (domain.len > subdomain.len)
return false;
HTTP_String subdomain_suffix = {
subdomain.ptr + subdomain.len - domain.len,
domain.len
};
if (subdomain_suffix.ptr[-1] != '.' || !http_streq(domain, subdomain_suffix))
return false;
return true;
}
// TODO: test this function
static bool is_subpath(HTTP_String path, HTTP_String subpath)
{
if (path.len > subpath.len)
return false;
if (subpath.len != path.len && subpath.ptr[path.len] != '/')
return false;
subpath.len = path.len;
return http_streq(path, subpath);
}
static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url)
{
// TODO: If the cookie is expired, ignore it regardless
if (entry.exact_domain) {
// Cookie domain and URL domain must match exactly
if (!http_streq(entry.domain, url.authority.host.text))
return false;
} else {
// Find a free connection slot
if (client->num_conns == HTTP_CLIENT_CAPACITY)
return -1;
int i = 0;
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE)
i++;
conn = &client->conns[i];
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
conn->handle = SOCKET_HANDLE_INVALID;
conn->client = client;
byte_queue_init(&conn->input, client->input_buffer_limit);
byte_queue_init(&conn->output, client->output_buffer_limit);
client->num_conns++;
// The URL's domain must match or be a subdomain of the cookie's domain
if (!is_subdomain(entry.domain, url.authority.host.text))
return false;
}
*builder = (HTTP_RequestBuilder) {
client,
conn - client->conns,
conn->gen
};
if (entry.exact_path) {
// Cookie path and URL path must match exactly
if (!http_streq(entry.path, url.path))
return false;
} else {
if (!is_subpath(entry.path, url.path))
return false;
}
return 0;
if (entry.secure) {
if (!http_streq(url.scheme, HTTP_STR("https")))
return false; // Cookie was marked as secure but the target URL is not HTTPS
}
return true;
}
static HTTP_String get_method_string(HTTP_Method method)
{
switch (method) {
case HTTP_METHOD_GET : return HTTP_STR("GET");
case HTTP_METHOD_HEAD : return HTTP_STR("HEAD");
case HTTP_METHOD_POST : return HTTP_STR("POST");
case HTTP_METHOD_PUT : return HTTP_STR("PUT");
case HTTP_METHOD_DELETE : return HTTP_STR("DELETE");
case HTTP_METHOD_CONNECT: return HTTP_STR("CONNECT");
case HTTP_METHOD_OPTIONS: return HTTP_STR("OPTIONS");
case HTTP_METHOD_TRACE : return HTTP_STR("TRACE");
case HTTP_METHOD_PATCH : return HTTP_STR("PATCH");
}
return HTTP_STR("???");
}
void http_request_builder_set_user(HTTP_RequestBuilder builder, void *user)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return; // Invalid builder
conn->user = user;
}
void http_request_builder_set_trace_bytes(HTTP_RequestBuilder builder, bool trace_bytes)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return; // Invalid builder
conn->trace_bytes = trace_bytes;
}
void http_request_builder_url(HTTP_RequestBuilder builder,
@@ -143,50 +213,70 @@ void http_request_builder_url(HTTP_RequestBuilder builder,
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return;
return; // Invalid builder
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
return;
return; // Request line already written
// Parse the URL to extract components
HTTP_URL parsed_url;
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
return;
// Allocate a copy of the URL string so the parsed URL pointers remain valid
char *url_copy = malloc(url.len);
if (url_copy == NULL)
return; // TODO: set error
memcpy(url_copy, url.ptr, url.len);
// Store method and parsed URL for connection establishment
conn->method = method;
conn->url = parsed_url;
conn->url_buffer.ptr = url_copy;
conn->url_buffer.len = url.len;
// Convert method enum to string
const char *method_str;
switch (method) {
case HTTP_METHOD_GET: method_str = "GET"; break;
case HTTP_METHOD_HEAD: method_str = "HEAD"; break;
case HTTP_METHOD_POST: method_str = "POST"; break;
case HTTP_METHOD_PUT: method_str = "PUT"; break;
case HTTP_METHOD_DELETE: method_str = "DELETE"; break;
case HTTP_METHOD_CONNECT: method_str = "CONNECT"; break;
case HTTP_METHOD_OPTIONS: method_str = "OPTIONS"; break;
case HTTP_METHOD_TRACE: method_str = "TRACE"; break;
case HTTP_METHOD_PATCH: method_str = "PATCH"; break;
default: return;
// Parse the copied URL (all url.* pointers will reference url_buffer)
if (http_parse_url(conn->url_buffer.ptr, conn->url_buffer.len, &conn->url) != 1) {
free(conn->url_buffer.ptr);
return; // TODO: set error
}
// Build request line: METHOD path HTTP/1.1\r\n
byte_queue_write_fmt(&conn->output, "%s %.*s HTTP/1.1\r\n",
method_str,
parsed_url.path.len, parsed_url.path.ptr);
// Write method
HTTP_String method_str = get_method_string(method);
byte_queue_write(&conn->output, method_str.ptr, method_str.len);
byte_queue_write(&conn->output, conn->url.path.ptr, conn->url.path.len);
HTTP_String query = conn->url.query;
if (query.len > 0) {
byte_queue_write(&conn->output, "?", 1);
byte_queue_write(&conn->output, query.ptr, query.len);
}
HTTP_String version = HTTP_STR("HTTP/1.1");
byte_queue_write(&conn->output, version.ptr, version.len);
byte_queue_write(&conn->output, "\r\n", 2);
// Add Host header automatically
byte_queue_write_fmt(&conn->output, "Host: %.*s",
parsed_url.authority.host.text.len,
parsed_url.authority.host.text.ptr);
conn->url.authority.host.text.len,
conn->url.authority.host.text.ptr);
if (conn->url.authority.port > 0)
byte_queue_write_fmt(&conn->output, ":%d", conn->url.authority.port);
if (parsed_url.authority.port > 0) {
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
}
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
HTTP_Client *client = builder.client;
HTTP_CookieJar *cookie_jar = &client->cookie_jar;
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);
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);
}
}
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
}
@@ -227,6 +317,7 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
// Transition from WAIT_HEADER to WAIT_BODY if needed
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
// End headers section
// TODO: add Content-Length header
byte_queue_write(&conn->output, "\r\n", 2);
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
}
@@ -237,105 +328,149 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
byte_queue_write(&conn->output, str.ptr, str.len);
}
int http_request_builder_send(HTTP_RequestBuilder builder)
static int
url_to_connect_target(HTTP_URL url,
ConnectTarget *target)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
HTTP_Authority authority = url.authority;
if (authority.port < 1) {
if (http_streq(url.scheme, HTTP_STR("https")))
target->port = 443;
else
target->port = 80;
} else {
target->port = authority.port;
}
// Set up target based on host type
if (authority.host.mode == HTTP_HOST_MODE_NAME) {
target->type = CONNECT_TARGET_NAME;
target->name = authority.host.name;
} else if (authority.host.mode == HTTP_HOST_MODE_IPV4) {
target->type = CONNECT_TARGET_IPV4;
target->ipv4 = authority.host.ipv4;
} else if (authority.host.mode == HTTP_HOST_MODE_IPV6) {
target->type = CONNECT_TARGET_IPV6;
target->ipv6 = authority.host.ipv6;
} else {
return -1;
// Finalize the request
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
// No body, just end headers
byte_queue_write(&conn->output, "\r\n", 2);
}
// Establish connection if not already connected
if (conn->handle == SOCKET_HANDLE_INVALID) {
// Determine if connection should be secure
bool secure = false;
if (conn->url.scheme.len == 5 &&
strncmp(conn->url.scheme.ptr, "https", 5) == 0) {
secure = true;
}
// Prepare connection target
ConnectTarget target;
target.port = conn->url.authority.port;
if (target.port <= 0)
target.port = secure ? 443 : 80;
// Set up target based on host type
if (conn->url.authority.host.mode == HTTP_HOST_MODE_NAME) {
target.type = CONNECT_TARGET_NAME;
target.name = conn->url.authority.host.name;
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV4) {
target.type = CONNECT_TARGET_IPV4;
target.ipv4 = conn->url.authority.host.ipv4;
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV6) {
target.type = CONNECT_TARGET_IPV6;
target.ipv6 = conn->url.authority.host.ipv6;
} else {
// Invalid host mode - clean up connection
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
conn->client->num_conns--;
return -1;
}
if (socket_connect(&conn->client->sockets, 1, &target, secure, conn) < 0) {
// Connection failed - clean up
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
conn->client->num_conns--;
return -1;
}
}
conn->state = HTTP_CLIENT_CONN_FLUSHING;
conn->gen++;
return 0;
}
// Look at the input buffer to see if a complete response
// was buffered. If it was, change the connection's status
// to COMPLETE and push it to the ready queue.
static void
check_response_buffer(HTTP_Client *client, HTTP_ClientConn *conn)
int http_request_builder_send(HTTP_RequestBuilder builder)
{
assert(conn->state == HTTP_CLIENT_CONN_BUFFERING);
HTTP_Client *client = builder.client;
if (client == NULL)
return -1;
ByteView src = byte_queue_read_buf(&conn->input);
int ret = http_parse_response(src.ptr, src.len, &conn->response);
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return -1;
if (ret < 0) {
// Invalid response
byte_queue_read_ack(&conn->input, 0);
socket_close(&client->sockets, conn->handle);
} else if (ret == 0) {
// Still waiting
byte_queue_read_ack(&conn->input, 0);
// If the queue reached its limit and we still didn't receive
// a complete response, abort the exchange.
if (byte_queue_full(&conn->input))
socket_close(&client->sockets, conn->handle);
} else {
// Ready
assert(ret == 1);
conn->state = HTTP_CLIENT_CONN_COMPLETE;
conn->response.context = conn;
// Push to the ready queue
assert(client->num_ready < HTTP_CLIENT_CAPACITY);
int tail = (client->ready_head + client->num_ready) % HTTP_CLIENT_CAPACITY;
client->ready[tail] = conn - client->conns;
client->num_ready++;
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
// No body, just end headers
byte_queue_write(&conn->output, "\r\n", 2);
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
}
if (conn->state != HTTP_CLIENT_CONN_WAIT_BODY)
goto error;
if (byte_queue_error(&conn->output))
goto error;
ConnectTarget target;
if (url_to_connect_target(conn->url, &target) < 0)
goto error;
bool secure = http_streq(conn->url.scheme, HTTP_STR("https"));
if (socket_connect(&client->sockets, 1, &target, secure, conn) < 0)
goto error;
conn->state = HTTP_CLIENT_CONN_FLUSHING;
conn->gen++;
return 0;
error:
conn->state = HTTP_CLIENT_CONN_FREE;
free(conn->url_buffer.ptr);
byte_queue_free(&conn->input);
byte_queue_free(&conn->output);
client->num_conns--;
return -1;
}
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
HTTP_CookieJarEntry entry;
entry.name = parsed.name;
entry.value = parsed.value;
if (parsed.have_domain) {
// TODO: Check that the server can set a cookie for this domain
entry.exact_domain = false;
entry.domain = parsed.domain;
} else {
entry.exact_domain = true;
entry.domain = domain;
}
if (parsed.have_path) {
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;
}
entry.secure = parsed.secure;
// Now copy all fields
char *p = malloc(entry.name.len + entry.value.len + entry.domain.len + entry.path.len);
if (p == NULL)
return;
memcpy(p, entry.name.ptr, entry.name.len);
entry.name.ptr = p;
p += entry.name.len;
memcpy(p, entry.value.ptr, entry.value.len);
entry.value.ptr = p;
p += entry.value.len;
memcpy(p, entry.domain.ptr, entry.domain.len);
entry.domain.ptr = p;
p += entry.domain.len;
memcpy(p, entry.path.ptr, entry.path.len);
entry.path.ptr = p;
p += entry.path.len;
cookie_jar->items[cookie_jar->count++] = entry;
}
static void save_cookies(HTTP_CookieJar *cookie_jar,
HTTP_Header *headers, int num_headers,
HTTP_String domain, HTTP_String path)
{
// TODO: remove expired cookies
for (int i = 0; i < num_headers; i++)
if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) // TODO: headers are case-insensitive, right?
save_one_cookie(cookie_jar, headers[i], domain, path);
}
int http_client_process_events(HTTP_Client *client,
@@ -350,65 +485,125 @@ int http_client_process_events(HTTP_Client *client,
for (int i = 0; i < num_events; i++) {
HTTP_ClientConn *conn = events[i].user;
if (conn == NULL)
continue; // If a socket is not couple to a connection,
// it means the response was already returned
// to the user.
if (events[i].type == SOCKET_EVENT_DISCONNECT) {
if (conn != NULL) {
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
client->num_conns--;
}
conn->state = HTTP_CLIENT_CONN_COMPLETE;
conn->result = -1;
} else if (events[i].type == SOCKET_EVENT_READY) {
if (conn == NULL)
continue;
// Store the handle if this is a new connection
if (conn->handle == SOCKET_HANDLE_INVALID)
conn->handle = events[i].handle;
if (conn->state == HTTP_CLIENT_CONN_FLUSHING) {
// Send request data
int num = 0;
ByteView src = byte_queue_read_buf(&conn->output);
int num = 0;
if (src.len)
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
if (conn->trace_bytes)
print_bytes(HTTP_STR("<< "), (HTTP_String){src.ptr, num});
byte_queue_read_ack(&conn->output, num);
if (byte_queue_error(&conn->output)) {
socket_close(&client->sockets, conn->handle);
} else if (byte_queue_empty(&conn->output)) {
// Request fully sent, now wait for response
conn->state = HTTP_CLIENT_CONN_BUFFERING;
continue;
}
} else if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
// Request fully sent, now wait for response
if (byte_queue_empty(&conn->output))
conn->state = HTTP_CLIENT_CONN_BUFFERING;
}
if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
// Receive response data
int min_recv = 1<<10;
byte_queue_write_setmincap(&conn->input, min_recv);
int num = 0;
ByteView dst = byte_queue_write_buf(&conn->input);
int num = 0;
if (dst.len)
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
if (conn->trace_bytes)
print_bytes(HTTP_STR(">> "), (HTTP_String){dst.ptr, num});
byte_queue_write_ack(&conn->input, num);
if (byte_queue_error(&conn->input))
if (byte_queue_error(&conn->input)) {
socket_close(&client->sockets, conn->handle);
else
check_response_buffer(client, conn);
continue;
}
ByteView src = byte_queue_read_buf(&conn->input);
int ret = http_parse_response(src.ptr, src.len, &conn->response);
if (ret == 0) {
// Still waiting
byte_queue_read_ack(&conn->input, 0);
// If the queue reached its limit and we still didn't receive
// a complete response, abort the exchange.
if (byte_queue_full(&conn->input))
socket_close(&client->sockets, conn->handle);
continue;
}
if (ret < 0) {
// Invalid response
byte_queue_read_ack(&conn->input, 0);
socket_close(&client->sockets, conn->handle);
continue;
}
// Ready
assert(ret == 1);
conn->state = HTTP_CLIENT_CONN_COMPLETE;
conn->result = 0;
conn->response.context = client;
// Store received cookies in the cookie jar
save_cookies(&client->cookie_jar,
conn->response.headers,
conn->response.num_headers,
conn->url.authority.host.text,
conn->url.path);
// TODO: Handle redirects here
}
}
if (conn->state == HTTP_CLIENT_CONN_COMPLETE) {
// Decouple from the socket
socket_set_user(&client->sockets, events[i].handle, NULL);
// Push to the ready queue
assert(client->num_ready < HTTP_CLIENT_CAPACITY);
int tail = (client->ready_head + client->num_ready) % HTTP_CLIENT_CAPACITY;
client->ready[tail] = conn - client->conns;
client->num_ready++;
}
}
return 0;
}
bool http_client_next_response(HTTP_Client *client,
HTTP_Response **response)
HTTP_Response **response, void **user)
{
if (client->num_ready == 0)
return false;
@@ -418,7 +613,15 @@ bool http_client_next_response(HTTP_Client *client,
client->num_ready--;
assert(conn->state == HTTP_CLIENT_CONN_COMPLETE);
*response = &conn->response;
if (conn->result == 0) {
*response = &conn->response;
} else {
assert(conn->result == -1);
*response = NULL;
}
*user = conn->user;
return true;
}
@@ -426,14 +629,23 @@ void http_free_response(HTTP_Response *response)
{
if (response == NULL || response->context == NULL)
return;
HTTP_ClientConn *conn = (HTTP_ClientConn*) response->context;
// Free the connection resources
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
conn->client->num_conns--;
// Mark response as freed
HTTP_Client *client = response->context;
response->context = NULL;
// TODO: I'm positive there is a better way to do this.
// It should just be a bouds check + subtraction.
HTTP_ClientConn *conn = NULL;
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++)
if (&client->conns[i].response == response) {
conn = &client->conns[i];
break;
}
if (conn == NULL)
return;
conn->state = HTTP_CLIENT_CONN_FREE;
free(conn->url_buffer.ptr);
byte_queue_free(&conn->input);
byte_queue_free(&conn->output);
client->num_conns--;
}
+72 -18
View File
@@ -10,6 +10,44 @@
// self-pipe.
#define HTTP_CLIENT_POLL_CAPACITY (HTTP_CLIENT_CAPACITY+1)
#ifndef HTTP_COOKIE_JAR_CAPACITY
// Maximum number of cookies that can be associated to a
// single client.
#define HTTP_COOKIE_JAR_CAPACITY 128
#endif
typedef struct {
// Cookie name and value
HTTP_String name;
HTTP_String value;
// If the "exact_domain" is true, the cookie
// can only be sent to the exact domain referred
// to by "domain" (which is never empty). If
// "exact_domain" is false, then the cookie is
// compatible with subdomains.
bool exact_domain;
HTTP_String domain;
// If "exact_path" is set, the cookie is only
// compatible with requests to paths that match
// "path" exactly. If "exact_path" is not set,
// then any path that starts with "path" is
// compatible with the cookie.
bool exact_path;
HTTP_String path;
// This cookie can only be sent over HTTPS
bool secure;
} HTTP_CookieJarEntry;
typedef struct {
int count;
HTTP_CookieJarEntry items[HTTP_COOKIE_JAR_CAPACITY];
} HTTP_CookieJar;
typedef enum {
HTTP_CLIENT_CONN_FREE,
HTTP_CLIENT_CONN_WAIT_LINE,
@@ -35,17 +73,31 @@ typedef struct {
// Generation counter for request builder validation
uint16_t gen;
// Opaque pointer set by the user while building
// the request. It's returned alongside the result.
void *user;
// TODO: comment
bool trace_bytes;
// Allocated copy of the URL string
HTTP_String url_buffer;
// Parsed URL for connection establishment
// All url.* pointers reference into url_buffer
HTTP_URL url;
// Data received from the server
ByteQueue input;
// Data being sent to the server
ByteQueue output;
// HTTP method for the request
HTTP_Method method;
// Parsed URL for connection establishment
HTTP_URL url;
// If the request is COMPLETE, indicates
// whether it completed with an error (-1)
// or a success (0). If it was a success,
// the response field is valid.
int result;
// Parsed response once complete
HTTP_Response response;
@@ -59,6 +111,9 @@ struct HTTP_Client {
uint32_t input_buffer_limit;
uint32_t output_buffer_limit;
// List of cookies created during this session
HTTP_CookieJar cookie_jar;
// Array of connections. The counter contains the
// number of structs such that state!=FREE.
int num_conns;
@@ -104,15 +159,14 @@ typedef struct {
uint16_t gen;
} HTTP_RequestBuilder;
// Create a new request builder object. If the response
// pointer is NULL, a brand new builder is created. If
// response isn't NULL (and http_free_response wasn't
// called on it yet), the connection associated to that
// previous exchange is reused. Note that it's up to the
// user to make sure the requests are targeting the same
// host. Returns 0 on success, -1 on error.
int http_client_get_builder(HTTP_Client *client,
HTTP_Response *response, HTTP_RequestBuilder *builder);
// Create a new request builder object.
HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client);
// TODO: comment
void http_request_builder_set_user(HTTP_RequestBuilder builder, void *user);
// TODO: comment
void http_request_builder_set_trace_bytes(HTTP_RequestBuilder builder, bool trace_bytes);
// Set the method and URL of the current request. This is the first
// function of the request builder that the user must call.
@@ -148,11 +202,11 @@ int http_client_process_events(HTTP_Client *client,
// may be availabe. This function returns one of the
// buffered responses. If a request was available, true
// is returned. If no more are avaiable, false is returned.
// The returned response must either be freed using the
// http_free_response function or reused by passing it
// to http_client_get_builder.
// The returned response must be freed using the
// http_free_response function.
// TODO: Better comment talking about output arguments
bool http_client_next_response(HTTP_Client *client,
HTTP_Response **response);
HTTP_Response **response, void **user);
// Free a response object. You can't access its fields
// again after this.
+268
View File
@@ -1322,3 +1322,271 @@ bool http_match_host(HTTP_Request *req, HTTP_String domain, int port)
HTTP_String host = req->headers[idx].value;
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;
}
+60
View File
@@ -93,3 +93,63 @@ int http_get_param_i (HTTP_String body, HTTP_String str);
// Checks whether the request was meant for the host with the given
// domain an port. If port is -1, the default value of 80 is assumed.
bool http_match_host(HTTP_Request *req, HTTP_String domain, int port);
// Date and cookie types for Set-Cookie header parsing
typedef enum {
HTTP_WEEKDAY_MON,
HTTP_WEEKDAY_TUE,
HTTP_WEEKDAY_WED,
HTTP_WEEKDAY_THU,
HTTP_WEEKDAY_FRI,
HTTP_WEEKDAY_SAT,
HTTP_WEEKDAY_SUN,
} HTTP_WeekDay;
typedef enum {
HTTP_MONTH_JAN,
HTTP_MONTH_FEB,
HTTP_MONTH_MAR,
HTTP_MONTH_APR,
HTTP_MONTH_MAY,
HTTP_MONTH_JUN,
HTTP_MONTH_JUL,
HTTP_MONTH_AUG,
HTTP_MONTH_SEP,
HTTP_MONTH_OCT,
HTTP_MONTH_NOV,
HTTP_MONTH_DEC,
} HTTP_Month;
typedef struct {
HTTP_WeekDay week_day;
int day;
HTTP_Month month;
int year;
int hour;
int minute;
int second;
} HTTP_Date;
typedef struct {
HTTP_String name;
HTTP_String value;
bool secure;
bool http_only;
bool have_date;
HTTP_Date date;
bool have_max_age;
uint32_t max_age;
bool have_domain;
HTTP_String domain;
bool have_path;
HTTP_String path;
} HTTP_SetCookie;
// Parses a Set-Cookie header value
// Returns 0 on success, -1 on error
int http_parse_set_cookie(HTTP_String str, HTTP_SetCookie *out);