Refactoring of the HTTP client
This commit is contained in:
@@ -3660,28 +3660,13 @@ int http_create_test_certificate(HTTP_String C, HTTP_String O, HTTP_String CN,
|
|||||||
// src/client.c
|
// src/client.c
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
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);
|
|
||||||
byte_queue_free(&conn->input);
|
|
||||||
}
|
|
||||||
|
|
||||||
int http_client_init(HTTP_Client *client)
|
int http_client_init(HTTP_Client *client)
|
||||||
{
|
{
|
||||||
client->input_buffer_limit = 1<<20;
|
client->input_buffer_limit = 1<<20;
|
||||||
client->output_buffer_limit = 1<<20;
|
client->output_buffer_limit = 1<<20;
|
||||||
|
|
||||||
|
client->cookie_jar.count = 0;
|
||||||
|
|
||||||
client->num_conns = 0;
|
client->num_conns = 0;
|
||||||
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++) {
|
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++) {
|
||||||
client->conns[i].state = HTTP_CLIENT_CONN_FREE;
|
client->conns[i].state = HTTP_CLIENT_CONN_FREE;
|
||||||
@@ -3701,6 +3686,9 @@ void http_client_free(HTTP_Client *client)
|
|||||||
{
|
{
|
||||||
socket_manager_free(&client->sockets);
|
socket_manager_free(&client->sockets);
|
||||||
|
|
||||||
|
for (int i = 0; i < cookie_jar->count; i++)
|
||||||
|
free(client->cookie_jar.items[i].name.ptr);
|
||||||
|
|
||||||
for (int i = 0, j = 0; j < client->num_conns; i++) {
|
for (int i = 0, j = 0; j < client->num_conns; i++) {
|
||||||
HTTP_ClientConn *conn = &client->conns[i];
|
HTTP_ClientConn *conn = &client->conns[i];
|
||||||
if (conn->state == HTTP_CLIENT_CONN_FREE)
|
if (conn->state == HTTP_CLIENT_CONN_FREE)
|
||||||
@@ -3755,48 +3743,123 @@ request_builder_to_conn(HTTP_RequestBuilder builder)
|
|||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
int http_client_get_builder(HTTP_Client *client,
|
HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client)
|
||||||
HTTP_Response *response, HTTP_RequestBuilder *builder)
|
|
||||||
{
|
{
|
||||||
HTTP_ClientConn *conn = NULL;
|
|
||||||
|
|
||||||
if (response != NULL && response->context != NULL) {
|
|
||||||
// Reuse the connection from the previous response
|
|
||||||
conn = (HTTP_ClientConn*) response->context;
|
|
||||||
|
|
||||||
// Mark the response as freed
|
|
||||||
response->context = NULL;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Find a free connection slot
|
// Find a free connection slot
|
||||||
if (client->num_conns == HTTP_CLIENT_CAPACITY)
|
if (client->num_conns == HTTP_CLIENT_CAPACITY)
|
||||||
return -1;
|
return (HTTP_RequestBuilder) { NULL, -1, -1 };
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE)
|
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE) {
|
||||||
i++;
|
i++;
|
||||||
|
assert(i < HTTP_CLIENT_CAPACITY);
|
||||||
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++;
|
client->num_conns++;
|
||||||
|
|
||||||
|
client->conns[i].state = HTTP_CLIENT_CONN_WAIT_LINE;
|
||||||
|
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);
|
||||||
|
|
||||||
|
return (HTTP_RequestBuilder) { client, conn - client->conns, conn->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,
|
||||||
|
entry.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 {
|
||||||
|
// 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) {
|
if (entry.exact_path) {
|
||||||
client,
|
// Cookie path and URL path must match exactly
|
||||||
conn - client->conns,
|
if (!http_streq(entry.path, url.path))
|
||||||
conn->gen
|
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,
|
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||||
@@ -3804,50 +3867,77 @@ void http_request_builder_url(HTTP_RequestBuilder builder,
|
|||||||
{
|
{
|
||||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||||
if (conn == NULL)
|
if (conn == NULL)
|
||||||
return;
|
return; // Invalid builder
|
||||||
|
|
||||||
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
|
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
|
||||||
return;
|
return; // Request line already written
|
||||||
|
|
||||||
// Parse the URL to extract components
|
// Parse the URL to extract components
|
||||||
HTTP_URL parsed_url;
|
HTTP_URL parsed_url;
|
||||||
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
|
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
|
||||||
return;
|
return; // TODO: set error
|
||||||
|
|
||||||
// Store method and parsed URL for connection establishment
|
HTTP_String domain = parsed_url.authority.host.text;
|
||||||
conn->method = method;
|
HTTP_String path = parsed_url.path;
|
||||||
conn->url = parsed_url;
|
char *p = malloc(domain.len + path.len);
|
||||||
|
if (p == NULL)
|
||||||
|
return; // TODO: set error
|
||||||
|
|
||||||
// Convert method enum to string
|
memcpy(p, domain.ptr, domain.len);
|
||||||
const char *method_str;
|
domain.ptr = p;
|
||||||
switch (method) {
|
p += domain.len;
|
||||||
case HTTP_METHOD_GET: method_str = "GET"; break;
|
|
||||||
case HTTP_METHOD_HEAD: method_str = "HEAD"; break;
|
memcpy(p, path.ptr, path.len);
|
||||||
case HTTP_METHOD_POST: method_str = "POST"; break;
|
path.ptr = p;
|
||||||
case HTTP_METHOD_PUT: method_str = "PUT"; break;
|
p += path.len;
|
||||||
case HTTP_METHOD_DELETE: method_str = "DELETE"; break;
|
|
||||||
case HTTP_METHOD_CONNECT: method_str = "CONNECT"; break;
|
conn->domain = domain;
|
||||||
case HTTP_METHOD_OPTIONS: method_str = "OPTIONS"; break;
|
conn->path = path;
|
||||||
case HTTP_METHOD_TRACE: method_str = "TRACE"; break;
|
|
||||||
case HTTP_METHOD_PATCH: method_str = "PATCH"; break;
|
// Write method
|
||||||
default: return;
|
HTTP_String method_str = get_method_string(method);
|
||||||
|
byte_queue_write(&conn->output, method_str.ptr, method_str.len);
|
||||||
|
|
||||||
|
HTTP_String path = parsed_url.path;
|
||||||
|
byte_queue_write(&conn->output, path.ptr, path.len);
|
||||||
|
|
||||||
|
HTTP_String query = parsed_url.query;
|
||||||
|
if (query.len > 0) {
|
||||||
|
byte_queue_write(&conn->output, "?", 1);
|
||||||
|
byte_queue_write(&conn->output, query.ptr, query.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build request line: METHOD path HTTP/1.1\r\n
|
HTTP_String version = HTTP_STR("HTTP/1.1");
|
||||||
byte_queue_write_fmt(&conn->output, "%s %.*s HTTP/1.1\r\n",
|
byte_queue_write(&conn->output, version.ptr, version.len);
|
||||||
method_str,
|
|
||||||
parsed_url.path.len, parsed_url.path.ptr);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
|
|
||||||
// Add Host header automatically
|
// Add Host header automatically
|
||||||
byte_queue_write_fmt(&conn->output, "Host: %.*s",
|
byte_queue_write_fmt(&conn->output, "Host: %.*s",
|
||||||
parsed_url.authority.host.text.len,
|
parsed_url.authority.host.text.len,
|
||||||
parsed_url.authority.host.text.ptr);
|
parsed_url.authority.host.text.ptr);
|
||||||
|
if (parsed_url.authority.port > 0)
|
||||||
if (parsed_url.authority.port > 0) {
|
|
||||||
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
|
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
|
||||||
}
|
|
||||||
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
|
||||||
|
// be sent to this server and append headers for them
|
||||||
|
HTTP_CookieJar *cookie_jar = &conn->client->cookie_jar;
|
||||||
|
for (int i = 0; i < cookie_jar->count; i++) {
|
||||||
|
HTTP_CookieJarEntry entry = cookie_jar->items[i];
|
||||||
|
if (should_send_cookie(entry, parsed_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, HTTP_STR("Cookie: "));
|
||||||
|
byte_queue_write(&conn->output, entry.name);
|
||||||
|
byte_queue_write(&conn->output, HTTP_STR("="));
|
||||||
|
byte_queue_write(&conn->output, entry.value);
|
||||||
|
byte_queue_write(&conn->output, HTTP_STR("\r\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
|
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3888,6 +3978,7 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
|
|||||||
// Transition from WAIT_HEADER to WAIT_BODY if needed
|
// Transition from WAIT_HEADER to WAIT_BODY if needed
|
||||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
||||||
// End headers section
|
// End headers section
|
||||||
|
// TODO: add Content-Length header
|
||||||
byte_queue_write(&conn->output, "\r\n", 2);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
||||||
}
|
}
|
||||||
@@ -3898,105 +3989,149 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
|
|||||||
byte_queue_write(&conn->output, str.ptr, str.len);
|
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);
|
HTTP_Authority authority = url.authority;
|
||||||
if (conn == NULL)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
// Finalize the request
|
if (authority.port < 1) {
|
||||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
if (http_streq(url.scheme, HTTP_STR("https")))
|
||||||
// No body, just end headers
|
target.port = 443;
|
||||||
byte_queue_write(&conn->output, "\r\n", 2);
|
else
|
||||||
|
target.port = 80;
|
||||||
|
} else {
|
||||||
|
target->port = authority.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Set up target based on host type
|
||||||
if (conn->url.authority.host.mode == HTTP_HOST_MODE_NAME) {
|
if (authority.host.mode == HTTP_HOST_MODE_NAME) {
|
||||||
target.type = CONNECT_TARGET_NAME;
|
target.type = CONNECT_TARGET_NAME;
|
||||||
target.name = conn->url.authority.host.name;
|
target.name = authority.host.name;
|
||||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV4) {
|
} else if (authority.host.mode == HTTP_HOST_MODE_IPV4) {
|
||||||
target.type = CONNECT_TARGET_IPV4;
|
target.type = CONNECT_TARGET_IPV4;
|
||||||
target.ipv4 = conn->url.authority.host.ipv4;
|
target.ipv4 = authority.host.ipv4;
|
||||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV6) {
|
} else if (authority.host.mode == HTTP_HOST_MODE_IPV6) {
|
||||||
target.type = CONNECT_TARGET_IPV6;
|
target.type = CONNECT_TARGET_IPV6;
|
||||||
target.ipv6 = conn->url.authority.host.ipv6;
|
target.ipv6 = authority.host.ipv6;
|
||||||
} else {
|
} else {
|
||||||
// Invalid host mode - clean up connection
|
|
||||||
http_client_conn_free(conn);
|
|
||||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
|
||||||
conn->client->num_conns--;
|
|
||||||
return -1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look at the input buffer to see if a complete response
|
int http_request_builder_send(HTTP_RequestBuilder builder)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
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);
|
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||||
int ret = http_parse_response(src.ptr, src.len, &conn->response);
|
if (conn == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
if (ret < 0) {
|
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
||||||
// Invalid response
|
// No body, just end headers
|
||||||
byte_queue_read_ack(&conn->input, 0);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
socket_close(&client->sockets, conn->handle);
|
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
||||||
|
|
||||||
} 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_BODY)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
if (byte_queue_error(&conn->output))
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
ConnectTarget target;
|
||||||
|
if (url_to_connect_target(url, &target) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
bool secure = http_streq(url.scheme, HTTP_STR("https"));
|
||||||
|
if (socket_connect(&conn->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->domain.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(headers[i].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) {
|
||||||
|
antry.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].value, domain, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
int http_client_process_events(HTTP_Client *client,
|
int http_client_process_events(HTTP_Client *client,
|
||||||
@@ -4011,57 +4146,113 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
for (int i = 0; i < num_events; i++) {
|
for (int i = 0; i < num_events; i++) {
|
||||||
|
|
||||||
HTTP_ClientConn *conn = events[i].user;
|
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 (events[i].type == SOCKET_EVENT_DISCONNECT) {
|
||||||
|
|
||||||
if (conn != NULL) {
|
conn->state = HTTP_CLIENT_CONN_COMPLETE;
|
||||||
http_client_conn_free(conn);
|
conn->result = -1;
|
||||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
|
||||||
client->num_conns--;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (events[i].type == SOCKET_EVENT_READY) {
|
} 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) {
|
if (conn->state == HTTP_CLIENT_CONN_FLUSHING) {
|
||||||
|
|
||||||
// Send request data
|
|
||||||
int num = 0;
|
|
||||||
ByteView src = byte_queue_read_buf(&conn->output);
|
ByteView src = byte_queue_read_buf(&conn->output);
|
||||||
|
|
||||||
|
int num = 0;
|
||||||
if (src.len)
|
if (src.len)
|
||||||
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
|
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
|
||||||
|
|
||||||
|
if (conn->trace_bytes)
|
||||||
|
print_bytes(HTTP_STR("<< "), src.ptr, num);
|
||||||
|
|
||||||
byte_queue_read_ack(&conn->output, num);
|
byte_queue_read_ack(&conn->output, num);
|
||||||
|
|
||||||
if (byte_queue_error(&conn->output)) {
|
if (byte_queue_error(&conn->output)) {
|
||||||
socket_close(&client->sockets, conn->handle);
|
socket_close(&client->sockets, conn->handle);
|
||||||
} else if (byte_queue_empty(&conn->output)) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Request fully sent, now wait for response
|
// Request fully sent, now wait for response
|
||||||
|
if (byte_queue_empty(&conn->output))
|
||||||
conn->state = HTTP_CLIENT_CONN_BUFFERING;
|
conn->state = HTTP_CLIENT_CONN_BUFFERING;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
|
if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
|
||||||
|
|
||||||
// Receive response data
|
// Receive response data
|
||||||
int min_recv = 1<<10;
|
int min_recv = 1<<10;
|
||||||
byte_queue_write_setmincap(&conn->input, min_recv);
|
byte_queue_write_setmincap(&conn->input, min_recv);
|
||||||
|
|
||||||
int num = 0;
|
|
||||||
ByteView dst = byte_queue_write_buf(&conn->input);
|
ByteView dst = byte_queue_write_buf(&conn->input);
|
||||||
|
|
||||||
|
int num = 0;
|
||||||
if (dst.len)
|
if (dst.len)
|
||||||
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
|
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
|
||||||
|
|
||||||
|
if (conn->trace_bytes)
|
||||||
|
print_bytes(HTTP_STR(">> "), dst.ptr, num);
|
||||||
|
|
||||||
byte_queue_write_ack(&conn->input, 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);
|
socket_close(&client->sockets, conn->handle);
|
||||||
else
|
continue;
|
||||||
check_response_buffer(client, conn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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->domain,
|
||||||
|
conn->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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4069,7 +4260,7 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool http_client_next_response(HTTP_Client *client,
|
bool http_client_next_response(HTTP_Client *client,
|
||||||
HTTP_Response **response)
|
HTTP_Response **response, void **user)
|
||||||
{
|
{
|
||||||
if (client->num_ready == 0)
|
if (client->num_ready == 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -4079,7 +4270,15 @@ bool http_client_next_response(HTTP_Client *client,
|
|||||||
client->num_ready--;
|
client->num_ready--;
|
||||||
|
|
||||||
assert(conn->state == HTTP_CLIENT_CONN_COMPLETE);
|
assert(conn->state == HTTP_CLIENT_CONN_COMPLETE);
|
||||||
|
|
||||||
|
if (conn->result == 0) {
|
||||||
*response = &conn->response;
|
*response = &conn->response;
|
||||||
|
} else {
|
||||||
|
assert(conn->result == -1);
|
||||||
|
*response = NULL;
|
||||||
|
}
|
||||||
|
*user = conn->user;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4087,16 +4286,25 @@ void http_free_response(HTTP_Response *response)
|
|||||||
{
|
{
|
||||||
if (response == NULL || response->context == NULL)
|
if (response == NULL || response->context == NULL)
|
||||||
return;
|
return;
|
||||||
|
HTTP_Client *client = response->context;
|
||||||
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
|
|
||||||
response->context = NULL;
|
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->domain.ptr);
|
||||||
|
byte_queue_free(&conn->input);
|
||||||
|
byte_queue_free(&conn->output);
|
||||||
|
client->num_conns--;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -824,6 +824,44 @@ int http_create_test_certificate(HTTP_String C, HTTP_String O, HTTP_String CN,
|
|||||||
#define HTTP_CLIENT_CAPACITY (1<<7)
|
#define HTTP_CLIENT_CAPACITY (1<<7)
|
||||||
#endif
|
#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;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
HTTP_CLIENT_CONN_FREE,
|
HTTP_CLIENT_CONN_FREE,
|
||||||
HTTP_CLIENT_CONN_WAIT_LINE,
|
HTTP_CLIENT_CONN_WAIT_LINE,
|
||||||
@@ -840,26 +878,32 @@ typedef struct HTTP_Client HTTP_Client;
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
HTTP_ClientConnState state;
|
HTTP_ClientConnState state;
|
||||||
|
|
||||||
// Handle to the socket
|
|
||||||
SocketHandle handle;
|
|
||||||
|
|
||||||
// Pointer back to the client
|
|
||||||
HTTP_Client *client;
|
|
||||||
|
|
||||||
// Generation counter for request builder validation
|
// Generation counter for request builder validation
|
||||||
uint16_t gen;
|
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;
|
||||||
|
|
||||||
|
// Endpoint domain and path. These must be saved
|
||||||
|
// in case the server wants to set some cookies.
|
||||||
|
HTTP_String domain;
|
||||||
|
HTTP_String path;
|
||||||
|
|
||||||
// Data received from the server
|
// Data received from the server
|
||||||
ByteQueue input;
|
ByteQueue input;
|
||||||
|
|
||||||
// Data being sent to the server
|
// Data being sent to the server
|
||||||
ByteQueue output;
|
ByteQueue output;
|
||||||
|
|
||||||
// HTTP method for the request
|
// If the request is COMPLETE, indicates
|
||||||
HTTP_Method method;
|
// whether it completed with an error (-1)
|
||||||
|
// or a success (0). If it was a success,
|
||||||
// Parsed URL for connection establishment
|
// the response field is valid.
|
||||||
HTTP_URL url;
|
int result;
|
||||||
|
|
||||||
// Parsed response once complete
|
// Parsed response once complete
|
||||||
HTTP_Response response;
|
HTTP_Response response;
|
||||||
@@ -873,6 +917,9 @@ struct HTTP_Client {
|
|||||||
uint32_t input_buffer_limit;
|
uint32_t input_buffer_limit;
|
||||||
uint32_t output_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
|
// Array of connections. The counter contains the
|
||||||
// number of structs such that state!=FREE.
|
// number of structs such that state!=FREE.
|
||||||
int num_conns;
|
int num_conns;
|
||||||
@@ -928,6 +975,12 @@ typedef struct {
|
|||||||
int http_client_get_builder(HTTP_Client *client,
|
int http_client_get_builder(HTTP_Client *client,
|
||||||
HTTP_Response *response, HTTP_RequestBuilder *builder);
|
HTTP_Response *response, HTTP_RequestBuilder *builder);
|
||||||
|
|
||||||
|
// 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
|
// Set the method and URL of the current request. This is the first
|
||||||
// function of the request builder that the user must call.
|
// function of the request builder that the user must call.
|
||||||
void http_request_builder_url(HTTP_RequestBuilder builder,
|
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||||
@@ -962,11 +1015,11 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
// may be availabe. This function returns one of the
|
// may be availabe. This function returns one of the
|
||||||
// buffered responses. If a request was available, true
|
// buffered responses. If a request was available, true
|
||||||
// is returned. If no more are avaiable, false is returned.
|
// is returned. If no more are avaiable, false is returned.
|
||||||
// The returned response must either be freed using the
|
// The returned response must be freed using the
|
||||||
// http_free_response function or reused by passing it
|
// http_free_response function.
|
||||||
// to http_client_get_builder.
|
// TODO: Better comment talking about output arguments
|
||||||
bool http_client_next_response(HTTP_Client *client,
|
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
|
// Free a response object. You can't access its fields
|
||||||
// again after this.
|
// again after this.
|
||||||
|
|||||||
+263
-202
@@ -1,21 +1,4 @@
|
|||||||
|
|
||||||
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);
|
|
||||||
byte_queue_free(&conn->input);
|
|
||||||
}
|
|
||||||
|
|
||||||
int http_client_init(HTTP_Client *client)
|
int http_client_init(HTTP_Client *client)
|
||||||
{
|
{
|
||||||
client->input_buffer_limit = 1<<20;
|
client->input_buffer_limit = 1<<20;
|
||||||
@@ -99,48 +82,26 @@ request_builder_to_conn(HTTP_RequestBuilder builder)
|
|||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
int http_client_get_builder(HTTP_Client *client,
|
HTTP_RequestBuilder http_client_get_builder(HTTP_Client *client)
|
||||||
HTTP_Response *response, HTTP_RequestBuilder *builder)
|
|
||||||
{
|
{
|
||||||
HTTP_ClientConn *conn = NULL;
|
|
||||||
|
|
||||||
if (response != NULL && response->context != NULL) {
|
|
||||||
// Reuse the connection from the previous response
|
|
||||||
conn = (HTTP_ClientConn*) response->context;
|
|
||||||
|
|
||||||
// Mark the response as freed
|
|
||||||
response->context = NULL;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Find a free connection slot
|
// Find a free connection slot
|
||||||
if (client->num_conns == HTTP_CLIENT_CAPACITY)
|
if (client->num_conns == HTTP_CLIENT_CAPACITY)
|
||||||
return -1;
|
return (HTTP_RequestBuilder) { NULL, -1, -1 };
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE)
|
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE) {
|
||||||
i++;
|
i++;
|
||||||
|
assert(i < HTTP_CLIENT_CAPACITY);
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
|
client->num_conns++;
|
||||||
|
|
||||||
*builder = (HTTP_RequestBuilder) {
|
client->conns[i].state = HTTP_CLIENT_CONN_WAIT_LINE;
|
||||||
client,
|
client->conns[i].user = NULL;
|
||||||
conn - client->conns,
|
client->conns[i].trace_bytes = false;
|
||||||
conn->gen
|
byte_queue_init(&client->conns[i].input, client->input_buffer_limit);
|
||||||
};
|
byte_queue_init(&client->conns[i].output, client->output_buffer_limit);
|
||||||
|
|
||||||
return 0;
|
return (HTTP_RequestBuilder) { client, conn - client->conns, conn->gen };
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test this function
|
// TODO: test this function
|
||||||
@@ -177,6 +138,8 @@ static bool is_subpath(HTTP_String path, HTTP_String subpath)
|
|||||||
|
|
||||||
static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url)
|
static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url)
|
||||||
{
|
{
|
||||||
|
// TODO: If the cookie is expired, ignore it regardless
|
||||||
|
|
||||||
if (entry.exact_domain) {
|
if (entry.exact_domain) {
|
||||||
// Cookie domain and URL domain must match exactly
|
// Cookie domain and URL domain must match exactly
|
||||||
if (!http_streq(entry.domain, url.authority.host.text))
|
if (!http_streq(entry.domain, url.authority.host.text))
|
||||||
@@ -204,52 +167,97 @@ static bool should_send_cookie(HTTP_CookieJarEntry entry, HTTP_URL url)
|
|||||||
return true;
|
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,
|
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||||
HTTP_Method method, HTTP_String url)
|
HTTP_Method method, HTTP_String url)
|
||||||
{
|
{
|
||||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||||
if (conn == NULL)
|
if (conn == NULL)
|
||||||
return;
|
return; // Invalid builder
|
||||||
|
|
||||||
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
|
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
|
||||||
return;
|
return; // Request line already written
|
||||||
|
|
||||||
// Parse the URL to extract components
|
// Parse the URL to extract components
|
||||||
HTTP_URL parsed_url;
|
HTTP_URL parsed_url;
|
||||||
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
|
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
|
||||||
return;
|
return; // TODO: set error
|
||||||
|
|
||||||
// Store method and parsed URL for connection establishment
|
HTTP_String domain = parsed_url.authority.host.text;
|
||||||
conn->method = method;
|
HTTP_String path = parsed_url.path;
|
||||||
conn->url = parsed_url;
|
char *p = malloc(domain.len + path.len);
|
||||||
|
if (p == NULL)
|
||||||
|
return; // TODO: set error
|
||||||
|
|
||||||
// Convert method enum to string
|
memcpy(p, domain.ptr, domain.len);
|
||||||
const char *method_str;
|
domain.ptr = p;
|
||||||
switch (method) {
|
p += domain.len;
|
||||||
case HTTP_METHOD_GET: method_str = "GET"; break;
|
|
||||||
case HTTP_METHOD_HEAD: method_str = "HEAD"; break;
|
memcpy(p, path.ptr, path.len);
|
||||||
case HTTP_METHOD_POST: method_str = "POST"; break;
|
path.ptr = p;
|
||||||
case HTTP_METHOD_PUT: method_str = "PUT"; break;
|
p += path.len;
|
||||||
case HTTP_METHOD_DELETE: method_str = "DELETE"; break;
|
|
||||||
case HTTP_METHOD_CONNECT: method_str = "CONNECT"; break;
|
conn->domain = domain;
|
||||||
case HTTP_METHOD_OPTIONS: method_str = "OPTIONS"; break;
|
conn->path = path;
|
||||||
case HTTP_METHOD_TRACE: method_str = "TRACE"; break;
|
|
||||||
case HTTP_METHOD_PATCH: method_str = "PATCH"; break;
|
// Write method
|
||||||
|
HTTP_String method_str = get_method_string(method);
|
||||||
|
byte_queue_write(&conn->output, method_str.ptr, method_str.len);
|
||||||
|
|
||||||
|
HTTP_String path = parsed_url.path;
|
||||||
|
byte_queue_write(&conn->output, path.ptr, path.len);
|
||||||
|
|
||||||
|
HTTP_String query = parsed_url.query;
|
||||||
|
if (query.len > 0) {
|
||||||
|
byte_queue_write(&conn->output, "?", 1);
|
||||||
|
byte_queue_write(&conn->output, query.ptr, query.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build request line: METHOD path HTTP/1.1\r\n
|
HTTP_String version = HTTP_STR("HTTP/1.1");
|
||||||
byte_queue_write_fmt(&conn->output, "%s %.*s HTTP/1.1\r\n",
|
byte_queue_write(&conn->output, version.ptr, version.len);
|
||||||
method_str,
|
|
||||||
parsed_url.path.len, parsed_url.path.ptr);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
|
|
||||||
// Add Host header automatically
|
// Add Host header automatically
|
||||||
byte_queue_write_fmt(&conn->output, "Host: %.*s",
|
byte_queue_write_fmt(&conn->output, "Host: %.*s",
|
||||||
parsed_url.authority.host.text.len,
|
parsed_url.authority.host.text.len,
|
||||||
parsed_url.authority.host.text.ptr);
|
parsed_url.authority.host.text.ptr);
|
||||||
|
if (parsed_url.authority.port > 0)
|
||||||
if (parsed_url.authority.port > 0) {
|
|
||||||
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
|
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
|
||||||
}
|
|
||||||
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
|
||||||
@@ -309,6 +317,7 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
|
|||||||
// Transition from WAIT_HEADER to WAIT_BODY if needed
|
// Transition from WAIT_HEADER to WAIT_BODY if needed
|
||||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
||||||
// End headers section
|
// End headers section
|
||||||
|
// TODO: add Content-Length header
|
||||||
byte_queue_write(&conn->output, "\r\n", 2);
|
byte_queue_write(&conn->output, "\r\n", 2);
|
||||||
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
||||||
}
|
}
|
||||||
@@ -319,80 +328,93 @@ void http_request_builder_body(HTTP_RequestBuilder builder,
|
|||||||
byte_queue_write(&conn->output, str.ptr, str.len);
|
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);
|
HTTP_Authority authority = url.authority;
|
||||||
if (conn == NULL)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
// Finalize the request
|
if (authority.port < 1) {
|
||||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
if (http_streq(url.scheme, HTTP_STR("https")))
|
||||||
// No body, just end headers
|
target.port = 443;
|
||||||
byte_queue_write(&conn->output, "\r\n", 2);
|
else
|
||||||
|
target.port = 80;
|
||||||
|
} else {
|
||||||
|
target->port = authority.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Set up target based on host type
|
||||||
if (conn->url.authority.host.mode == HTTP_HOST_MODE_NAME) {
|
if (authority.host.mode == HTTP_HOST_MODE_NAME) {
|
||||||
target.type = CONNECT_TARGET_NAME;
|
target.type = CONNECT_TARGET_NAME;
|
||||||
target.name = conn->url.authority.host.name;
|
target.name = authority.host.name;
|
||||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV4) {
|
} else if (authority.host.mode == HTTP_HOST_MODE_IPV4) {
|
||||||
target.type = CONNECT_TARGET_IPV4;
|
target.type = CONNECT_TARGET_IPV4;
|
||||||
target.ipv4 = conn->url.authority.host.ipv4;
|
target.ipv4 = authority.host.ipv4;
|
||||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV6) {
|
} else if (authority.host.mode == HTTP_HOST_MODE_IPV6) {
|
||||||
target.type = CONNECT_TARGET_IPV6;
|
target.type = CONNECT_TARGET_IPV6;
|
||||||
target.ipv6 = conn->url.authority.host.ipv6;
|
target.ipv6 = authority.host.ipv6;
|
||||||
} else {
|
} else {
|
||||||
// Invalid host mode - clean up connection
|
|
||||||
http_client_conn_free(conn);
|
|
||||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
|
||||||
conn->client->num_conns--;
|
|
||||||
return -1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int num_headers)
|
int http_request_builder_send(HTTP_RequestBuilder builder)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < num_headers; i++) {
|
HTTP_Client *client = builder.client;
|
||||||
if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) {
|
if (client == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||||
|
if (conn == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
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(url, &target) < 0)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
bool secure = http_streq(url.scheme, HTTP_STR("https"));
|
||||||
|
if (socket_connect(&conn->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->domain.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;
|
HTTP_SetCookie parsed;
|
||||||
if (http_parse_set_cookie(headers[i].value, &parsed) < 0)
|
if (http_parse_set_cookie(headers[i].value, &parsed) < 0)
|
||||||
continue; // Ignore invalid Set-Cookie headers
|
return; // Ignore invalid Set-Cookie headers
|
||||||
|
|
||||||
if (cookie_jar->count == HTTP_COOKIE_JAR_CAPACITY)
|
|
||||||
break;
|
|
||||||
|
|
||||||
HTTP_CookieJarEntry entry;
|
HTTP_CookieJarEntry entry;
|
||||||
|
|
||||||
entry.name = parsed.name;
|
entry.name = parsed.name;
|
||||||
entry.value = parsed.value;
|
entry.value = parsed.value;
|
||||||
|
|
||||||
@@ -401,9 +423,8 @@ static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int n
|
|||||||
entry.exact_domain = false;
|
entry.exact_domain = false;
|
||||||
entry.domain = parsed.domain;
|
entry.domain = parsed.domain;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Set the domain to the specific one used for this interaction
|
|
||||||
entry.exact_domain = true;
|
entry.exact_domain = true;
|
||||||
entry.domain = xxx;
|
entry.domain = domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.have_path) {
|
if (parsed.have_path) {
|
||||||
@@ -412,7 +433,7 @@ static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int n
|
|||||||
} else {
|
} else {
|
||||||
// TODO: Set the path to the current endpoint minus one level
|
// TODO: Set the path to the current endpoint minus one level
|
||||||
entry.exact_path = true;
|
entry.exact_path = true;
|
||||||
entry.path = xxx;
|
entry.path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.secure = parsed.secure;
|
entry.secure = parsed.secure;
|
||||||
@@ -420,7 +441,7 @@ static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int n
|
|||||||
// Now copy all fields
|
// Now copy all 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)
|
||||||
break;
|
return;
|
||||||
|
|
||||||
memcpy(p, entry.name.ptr, entry.name.len);
|
memcpy(p, entry.name.ptr, entry.name.len);
|
||||||
entry.name.ptr = p;
|
entry.name.ptr = p;
|
||||||
@@ -439,50 +460,17 @@ static void save_cookies(HTTP_CookieJar *cookie_jar, HTTP_Header *headers, int n
|
|||||||
p += entry.path.len;
|
p += entry.path.len;
|
||||||
|
|
||||||
cookie_jar->items[cookie_jar->count++] = entry;
|
cookie_jar->items[cookie_jar->count++] = entry;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look at the input buffer to see if a complete response
|
static void save_cookies(HTTP_CookieJar *cookie_jar,
|
||||||
// was buffered. If it was, change the connection's status
|
HTTP_Header *headers, int num_headers,
|
||||||
// to COMPLETE and push it to the ready queue.
|
HTTP_String domain, HTTP_String path)
|
||||||
static void
|
|
||||||
check_response_buffer(HTTP_Client *client, HTTP_ClientConn *conn)
|
|
||||||
{
|
{
|
||||||
assert(conn->state == HTTP_CLIENT_CONN_BUFFERING);
|
// TODO: remove expired cookies
|
||||||
|
|
||||||
ByteView src = byte_queue_read_buf(&conn->input);
|
for (int i = 0; i < num_headers; i++)
|
||||||
int ret = http_parse_response(src.ptr, src.len, &conn->response);
|
if (http_streqcase(headers[i].name, HTTP_STR("Set-Cookie"))) // TODO: headers are case-insensitive, right?
|
||||||
|
save_one_cookie(cookie_jar, headers[i].value, domain, path);
|
||||||
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;
|
|
||||||
|
|
||||||
save_cookies(&client->cookie_jar, conn->response.headers, conn->response.num_headers);
|
|
||||||
|
|
||||||
// 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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int http_client_process_events(HTTP_Client *client,
|
int http_client_process_events(HTTP_Client *client,
|
||||||
@@ -497,57 +485,113 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
for (int i = 0; i < num_events; i++) {
|
for (int i = 0; i < num_events; i++) {
|
||||||
|
|
||||||
HTTP_ClientConn *conn = events[i].user;
|
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 (events[i].type == SOCKET_EVENT_DISCONNECT) {
|
||||||
|
|
||||||
if (conn != NULL) {
|
conn->state = HTTP_CLIENT_CONN_COMPLETE;
|
||||||
http_client_conn_free(conn);
|
conn->result = -1;
|
||||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
|
||||||
client->num_conns--;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (events[i].type == SOCKET_EVENT_READY) {
|
} 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) {
|
if (conn->state == HTTP_CLIENT_CONN_FLUSHING) {
|
||||||
|
|
||||||
// Send request data
|
|
||||||
int num = 0;
|
|
||||||
ByteView src = byte_queue_read_buf(&conn->output);
|
ByteView src = byte_queue_read_buf(&conn->output);
|
||||||
|
|
||||||
|
int num = 0;
|
||||||
if (src.len)
|
if (src.len)
|
||||||
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
|
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
|
||||||
|
|
||||||
|
if (conn->trace_bytes)
|
||||||
|
print_bytes(HTTP_STR("<< "), src.ptr, num);
|
||||||
|
|
||||||
byte_queue_read_ack(&conn->output, num);
|
byte_queue_read_ack(&conn->output, num);
|
||||||
|
|
||||||
if (byte_queue_error(&conn->output)) {
|
if (byte_queue_error(&conn->output)) {
|
||||||
socket_close(&client->sockets, conn->handle);
|
socket_close(&client->sockets, conn->handle);
|
||||||
} else if (byte_queue_empty(&conn->output)) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Request fully sent, now wait for response
|
// Request fully sent, now wait for response
|
||||||
|
if (byte_queue_empty(&conn->output))
|
||||||
conn->state = HTTP_CLIENT_CONN_BUFFERING;
|
conn->state = HTTP_CLIENT_CONN_BUFFERING;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
|
if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
|
||||||
|
|
||||||
// Receive response data
|
// Receive response data
|
||||||
int min_recv = 1<<10;
|
int min_recv = 1<<10;
|
||||||
byte_queue_write_setmincap(&conn->input, min_recv);
|
byte_queue_write_setmincap(&conn->input, min_recv);
|
||||||
|
|
||||||
int num = 0;
|
|
||||||
ByteView dst = byte_queue_write_buf(&conn->input);
|
ByteView dst = byte_queue_write_buf(&conn->input);
|
||||||
|
|
||||||
|
int num = 0;
|
||||||
if (dst.len)
|
if (dst.len)
|
||||||
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
|
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
|
||||||
|
|
||||||
|
if (conn->trace_bytes)
|
||||||
|
print_bytes(HTTP_STR(">> "), dst.ptr, num);
|
||||||
|
|
||||||
byte_queue_write_ack(&conn->input, 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);
|
socket_close(&client->sockets, conn->handle);
|
||||||
else
|
continue;
|
||||||
check_response_buffer(client, conn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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->domain,
|
||||||
|
conn->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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +599,7 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool http_client_next_response(HTTP_Client *client,
|
bool http_client_next_response(HTTP_Client *client,
|
||||||
HTTP_Response **response)
|
HTTP_Response **response, void **user)
|
||||||
{
|
{
|
||||||
if (client->num_ready == 0)
|
if (client->num_ready == 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -565,7 +609,15 @@ bool http_client_next_response(HTTP_Client *client,
|
|||||||
client->num_ready--;
|
client->num_ready--;
|
||||||
|
|
||||||
assert(conn->state == HTTP_CLIENT_CONN_COMPLETE);
|
assert(conn->state == HTTP_CLIENT_CONN_COMPLETE);
|
||||||
|
|
||||||
|
if (conn->result == 0) {
|
||||||
*response = &conn->response;
|
*response = &conn->response;
|
||||||
|
} else {
|
||||||
|
assert(conn->result == -1);
|
||||||
|
*response = NULL;
|
||||||
|
}
|
||||||
|
*user = conn->user;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,14 +625,23 @@ void http_free_response(HTTP_Response *response)
|
|||||||
{
|
{
|
||||||
if (response == NULL || response->context == NULL)
|
if (response == NULL || response->context == NULL)
|
||||||
return;
|
return;
|
||||||
|
HTTP_Client *client = response->context;
|
||||||
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
|
|
||||||
response->context = NULL;
|
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->domain.ptr);
|
||||||
|
byte_queue_free(&conn->input);
|
||||||
|
byte_queue_free(&conn->output);
|
||||||
|
client->num_conns--;
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-15
@@ -59,26 +59,32 @@ typedef struct HTTP_Client HTTP_Client;
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
HTTP_ClientConnState state;
|
HTTP_ClientConnState state;
|
||||||
|
|
||||||
// Handle to the socket
|
|
||||||
SocketHandle handle;
|
|
||||||
|
|
||||||
// Pointer back to the client
|
|
||||||
HTTP_Client *client;
|
|
||||||
|
|
||||||
// Generation counter for request builder validation
|
// Generation counter for request builder validation
|
||||||
uint16_t gen;
|
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;
|
||||||
|
|
||||||
|
// Endpoint domain and path. These must be saved
|
||||||
|
// in case the server wants to set some cookies.
|
||||||
|
HTTP_String domain;
|
||||||
|
HTTP_String path;
|
||||||
|
|
||||||
// Data received from the server
|
// Data received from the server
|
||||||
ByteQueue input;
|
ByteQueue input;
|
||||||
|
|
||||||
// Data being sent to the server
|
// Data being sent to the server
|
||||||
ByteQueue output;
|
ByteQueue output;
|
||||||
|
|
||||||
// HTTP method for the request
|
// If the request is COMPLETE, indicates
|
||||||
HTTP_Method method;
|
// whether it completed with an error (-1)
|
||||||
|
// or a success (0). If it was a success,
|
||||||
// Parsed URL for connection establishment
|
// the response field is valid.
|
||||||
HTTP_URL url;
|
int result;
|
||||||
|
|
||||||
// Parsed response once complete
|
// Parsed response once complete
|
||||||
HTTP_Response response;
|
HTTP_Response response;
|
||||||
@@ -150,6 +156,12 @@ typedef struct {
|
|||||||
int http_client_get_builder(HTTP_Client *client,
|
int http_client_get_builder(HTTP_Client *client,
|
||||||
HTTP_Response *response, HTTP_RequestBuilder *builder);
|
HTTP_Response *response, HTTP_RequestBuilder *builder);
|
||||||
|
|
||||||
|
// 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
|
// Set the method and URL of the current request. This is the first
|
||||||
// function of the request builder that the user must call.
|
// function of the request builder that the user must call.
|
||||||
void http_request_builder_url(HTTP_RequestBuilder builder,
|
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||||
@@ -184,11 +196,11 @@ int http_client_process_events(HTTP_Client *client,
|
|||||||
// may be availabe. This function returns one of the
|
// may be availabe. This function returns one of the
|
||||||
// buffered responses. If a request was available, true
|
// buffered responses. If a request was available, true
|
||||||
// is returned. If no more are avaiable, false is returned.
|
// is returned. If no more are avaiable, false is returned.
|
||||||
// The returned response must either be freed using the
|
// The returned response must be freed using the
|
||||||
// http_free_response function or reused by passing it
|
// http_free_response function.
|
||||||
// to http_client_get_builder.
|
// TODO: Better comment talking about output arguments
|
||||||
bool http_client_next_response(HTTP_Client *client,
|
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
|
// Free a response object. You can't access its fields
|
||||||
// again after this.
|
// again after this.
|
||||||
|
|||||||
Reference in New Issue
Block a user