|
// retoor <retoor@molodetz.nl>
|
|
#include "http_client.h"
|
|
#include <curl/curl.h>
|
|
#include <pthread.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#define HTTP_MAX_RETRIES 3
|
|
#define HTTP_RETRY_DELAY_MS 2000
|
|
struct http_client_t {
|
|
char *bearer_token;
|
|
long timeout_seconds;
|
|
long connect_timeout_seconds;
|
|
bool show_spinner;
|
|
};
|
|
struct response_buffer_t {
|
|
char *data;
|
|
size_t size;
|
|
};
|
|
static struct timespec spinner_start_time = {0, 0};
|
|
static volatile int spinner_running = 0;
|
|
static double get_elapsed_seconds(void) {
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return (now.tv_sec - spinner_start_time.tv_sec) +
|
|
(now.tv_nsec - spinner_start_time.tv_nsec) / 1e9;
|
|
}
|
|
static void *spinner_thread(void *arg) {
|
|
(void)arg;
|
|
const char *frames[] = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
|
|
int frame = 0;
|
|
while (spinner_running) {
|
|
double elapsed = get_elapsed_seconds();
|
|
fprintf(stderr, "\r%s Querying AI... (%.1fs) ", frames[frame % 10], elapsed);
|
|
fflush(stderr);
|
|
frame++;
|
|
usleep(80000);
|
|
}
|
|
return NULL;
|
|
}
|
|
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
|
|
size_t total_size = size * nmemb;
|
|
struct response_buffer_t *response = (struct response_buffer_t *)userp;
|
|
if (total_size > SIZE_MAX - response->size - 1) {
|
|
return 0;
|
|
}
|
|
char *ptr = realloc(response->data, response->size + total_size + 1);
|
|
if (!ptr) {
|
|
return 0;
|
|
}
|
|
response->data = ptr;
|
|
memcpy(&(response->data[response->size]), contents, total_size);
|
|
response->size += total_size;
|
|
response->data[response->size] = '\0';
|
|
return total_size;
|
|
}
|
|
http_client_handle http_client_create(const char *bearer_token) {
|
|
struct http_client_t *client = calloc(1, sizeof(struct http_client_t));
|
|
if (!client) return NULL;
|
|
if (bearer_token) {
|
|
client->bearer_token = strdup(bearer_token);
|
|
if (!client->bearer_token) {
|
|
free(client);
|
|
return NULL;
|
|
}
|
|
}
|
|
client->timeout_seconds = 300;
|
|
client->connect_timeout_seconds = 10;
|
|
client->show_spinner = true;
|
|
return client;
|
|
}
|
|
void http_client_destroy(http_client_handle client) {
|
|
if (!client) return;
|
|
free(client->bearer_token);
|
|
free(client);
|
|
}
|
|
void http_client_set_show_spinner(http_client_handle client, bool show) {
|
|
if (client) client->show_spinner = show;
|
|
}
|
|
void http_client_set_timeout(http_client_handle client, long timeout_seconds) {
|
|
if (client) client->timeout_seconds = timeout_seconds;
|
|
}
|
|
void http_client_set_connect_timeout(http_client_handle client, long timeout_seconds) {
|
|
if (client) client->connect_timeout_seconds = timeout_seconds;
|
|
}
|
|
r_status_t http_post(http_client_handle client, const char *url,
|
|
const char *data, char **response) {
|
|
if (!client || !url || !response) return R_ERROR_INVALID_ARG;
|
|
CURL *curl = NULL;
|
|
struct curl_slist *headers = NULL;
|
|
struct response_buffer_t resp = {NULL, 0};
|
|
int retry_count = 0;
|
|
pthread_t spinner_tid = 0;
|
|
r_status_t status = R_SUCCESS;
|
|
*response = NULL;
|
|
bool actually_show_spinner = client->show_spinner && isatty(STDERR_FILENO);
|
|
if (actually_show_spinner) {
|
|
clock_gettime(CLOCK_MONOTONIC, &spinner_start_time);
|
|
spinner_running = 1;
|
|
pthread_create(&spinner_tid, NULL, spinner_thread, NULL);
|
|
}
|
|
while (retry_count < HTTP_MAX_RETRIES) {
|
|
free(resp.data);
|
|
resp.data = malloc(1);
|
|
resp.size = 0;
|
|
if (!resp.data) {
|
|
status = R_ERROR_OUT_OF_MEMORY;
|
|
goto cleanup;
|
|
}
|
|
resp.data[0] = '\0';
|
|
curl = curl_easy_init();
|
|
if (!curl) {
|
|
status = R_ERROR_HTTP_CONNECTION;
|
|
goto cleanup;
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, client->connect_timeout_seconds);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, client->timeout_seconds);
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
if (client->bearer_token) {
|
|
char bearer_header[2048];
|
|
snprintf(bearer_header, sizeof(bearer_header), "Authorization: Bearer %s",
|
|
client->bearer_token);
|
|
headers = curl_slist_append(headers, bearer_header);
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&resp);
|
|
CURLcode res = curl_easy_perform(curl);
|
|
curl_slist_free_all(headers);
|
|
headers = NULL;
|
|
curl_easy_cleanup(curl);
|
|
curl = NULL;
|
|
if (res == CURLE_OK) {
|
|
*response = resp.data;
|
|
resp.data = NULL;
|
|
status = R_SUCCESS;
|
|
goto cleanup;
|
|
}
|
|
retry_count++;
|
|
if (actually_show_spinner) {
|
|
spinner_running = 0;
|
|
pthread_join(spinner_tid, NULL);
|
|
spinner_tid = 0;
|
|
fprintf(stderr, "\r \r");
|
|
}
|
|
fprintf(stderr, "Network error: %s (attempt %d/%d)\n",
|
|
curl_easy_strerror(res), retry_count, HTTP_MAX_RETRIES);
|
|
if (retry_count < HTTP_MAX_RETRIES) {
|
|
fprintf(stderr, "Retrying in %d seconds...\n", HTTP_RETRY_DELAY_MS / 1000);
|
|
usleep(HTTP_RETRY_DELAY_MS * 1000);
|
|
if (actually_show_spinner) {
|
|
clock_gettime(CLOCK_MONOTONIC, &spinner_start_time);
|
|
spinner_running = 1;
|
|
pthread_create(&spinner_tid, NULL, spinner_thread, NULL);
|
|
}
|
|
}
|
|
}
|
|
status = R_ERROR_HTTP_TIMEOUT;
|
|
cleanup:
|
|
if (actually_show_spinner && spinner_tid) {
|
|
spinner_running = 0;
|
|
pthread_join(spinner_tid, NULL);
|
|
fprintf(stderr, "\r \r");
|
|
fflush(stderr);
|
|
}
|
|
if (headers) curl_slist_free_all(headers);
|
|
if (curl) curl_easy_cleanup(curl);
|
|
free(resp.data);
|
|
return status;
|
|
}
|
|
r_status_t http_get(http_client_handle client, const char *url, char **response) {
|
|
if (!client || !url || !response) return R_ERROR_INVALID_ARG;
|
|
CURL *curl = NULL;
|
|
struct curl_slist *headers = NULL;
|
|
struct response_buffer_t resp = {NULL, 0};
|
|
int retry_count = 0;
|
|
r_status_t status = R_SUCCESS;
|
|
*response = NULL;
|
|
while (retry_count < HTTP_MAX_RETRIES) {
|
|
free(resp.data);
|
|
resp.data = malloc(1);
|
|
resp.size = 0;
|
|
if (!resp.data) {
|
|
status = R_ERROR_OUT_OF_MEMORY;
|
|
goto cleanup;
|
|
}
|
|
resp.data[0] = '\0';
|
|
curl = curl_easy_init();
|
|
if (!curl) {
|
|
status = R_ERROR_HTTP_CONNECTION;
|
|
goto cleanup;
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, client->connect_timeout_seconds);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
|
if (client->bearer_token) {
|
|
char bearer_header[2048];
|
|
snprintf(bearer_header, sizeof(bearer_header), "Authorization: Bearer %s",
|
|
client->bearer_token);
|
|
headers = curl_slist_append(headers, bearer_header);
|
|
}
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&resp);
|
|
CURLcode res = curl_easy_perform(curl);
|
|
curl_slist_free_all(headers);
|
|
headers = NULL;
|
|
curl_easy_cleanup(curl);
|
|
curl = NULL;
|
|
if (res == CURLE_OK) {
|
|
*response = resp.data;
|
|
resp.data = NULL;
|
|
status = R_SUCCESS;
|
|
goto cleanup;
|
|
}
|
|
retry_count++;
|
|
fprintf(stderr, "Network error: %s (attempt %d/%d)\n",
|
|
curl_easy_strerror(res), retry_count, HTTP_MAX_RETRIES);
|
|
if (retry_count < HTTP_MAX_RETRIES) {
|
|
fprintf(stderr, "Retrying in %d seconds...\n", HTTP_RETRY_DELAY_MS / 1000);
|
|
usleep(HTTP_RETRY_DELAY_MS * 1000);
|
|
}
|
|
}
|
|
status = R_ERROR_HTTP_TIMEOUT;
|
|
cleanup:
|
|
if (headers) curl_slist_free_all(headers);
|
|
if (curl) curl_easy_cleanup(curl);
|
|
free(resp.data);
|
|
return status;
|
|
}
|
|
r_status_t http_post_simple(const char *url, const char *bearer_token,
|
|
const char *data, char **response) {
|
|
http_client_handle client = http_client_create(bearer_token);
|
|
if (!client) return R_ERROR_OUT_OF_MEMORY;
|
|
r_status_t status = http_post(client, url, data, response);
|
|
http_client_destroy(client);
|
|
return status;
|
|
}
|
|
r_status_t http_get_simple(const char *url, const char *bearer_token, char **response) {
|
|
http_client_handle client = http_client_create(bearer_token);
|
|
if (!client) return R_ERROR_OUT_OF_MEMORY;
|
|
http_client_set_show_spinner(client, false);
|
|
r_status_t status = http_get(client, url, response);
|
|
http_client_destroy(client);
|
|
return status;
|
|
}
|