From 3755f0e40e22c92d9285d2ddc551bc2f7a04c50c Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Sat, 3 Jan 2026 10:20:13 +1100 Subject: [PATCH] simple request library --- libs/request/request.c | 418 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 libs/request/request.c diff --git a/libs/request/request.c b/libs/request/request.c new file mode 100644 index 0000000..7a50e38 --- /dev/null +++ b/libs/request/request.c @@ -0,0 +1,418 @@ +#include +#include +#include +#include +#include +#include + +bool curl_init = false; + +typedef struct { + char* data; + size_t size; +} ResponseBuffer; + +typedef struct { + CURL* handle; + bool connected; + char* url; +} WebSocketConnection; + +// Global storage for WebSocket connections (simple implementation) +#define MAX_WS_CONNECTIONS 10 +WebSocketConnection ws_connections[MAX_WS_CONNECTIONS] = {0}; + +void curl_setup() { + if (!curl_init) { + curl_global_init(CURL_GLOBAL_ALL); + curl_init = true; + } +} + +// Proper write callback that accumulates data +size_t write_callback(void* ptr, size_t size, size_t nmemb, void* userdata) { + size_t total_size = size * nmemb; + ResponseBuffer* buffer = (ResponseBuffer*)userdata; + + char* new_data = realloc(buffer->data, buffer->size + total_size + 1); + if (new_data == NULL) { + printf("Failed to allocate memory\n"); + return 0; + } + + buffer->data = new_data; + memcpy(buffer->data + buffer->size, ptr, total_size); + buffer->size += total_size; + buffer->data[buffer->size] = '\0'; + + return total_size; +} + +// GET request +GroundValue get_request(GroundScope* scope, List args) { + if (args.size < 1) { + printf("Requires a URL argument\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != STRING) { + printf("Arg 1 needs to be a string (URL)\n"); + return groundCreateValue(NONE); + } + + curl_setup(); + CURL* handle = curl_easy_init(); + + if (!handle) { + printf("Curl failed to init\n"); + return groundCreateValue(NONE); + } + + ResponseBuffer buffer = {0}; + buffer.data = malloc(1); + buffer.data[0] = '\0'; + buffer.size = 0; + + curl_easy_setopt(handle, CURLOPT_URL, args.values[0].data.stringVal); + curl_easy_setopt(handle, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); + + CURLcode result = curl_easy_perform(handle); + + if (result != CURLE_OK) { + printf("Curl request failed: %s\n", curl_easy_strerror(result)); + free(buffer.data); + curl_easy_cleanup(handle); + return groundCreateValue(NONE); + } + + curl_easy_cleanup(handle); + + GroundValue ret = groundCreateValue(STRING, buffer.data); + return ret; +} + +// POST request +GroundValue post_request(GroundScope* scope, List args) { + if (args.size < 2) { + printf("Requires URL and POST data arguments\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != STRING || args.values[1].type != STRING) { + printf("Both arguments need to be strings (URL, POST data)\n"); + return groundCreateValue(NONE); + } + + curl_setup(); + CURL* handle = curl_easy_init(); + + if (!handle) { + printf("Curl failed to init\n"); + return groundCreateValue(NONE); + } + + ResponseBuffer buffer = {0}; + buffer.data = malloc(1); + buffer.data[0] = '\0'; + buffer.size = 0; + + curl_easy_setopt(handle, CURLOPT_URL, args.values[0].data.stringVal); + curl_easy_setopt(handle, CURLOPT_POST, 1L); + curl_easy_setopt(handle, CURLOPT_POSTFIELDS, args.values[1].data.stringVal); + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); + + CURLcode result = curl_easy_perform(handle); + + if (result != CURLE_OK) { + printf("Curl POST request failed: %s\n", curl_easy_strerror(result)); + free(buffer.data); + curl_easy_cleanup(handle); + return groundCreateValue(NONE); + } + + curl_easy_cleanup(handle); + + GroundValue ret = groundCreateValue(STRING, buffer.data); + return ret; +} + +// ============== WebSocket Functions ============== + +// Find an empty slot for a new WebSocket connection +int find_ws_slot() { + for (int i = 0; i < MAX_WS_CONNECTIONS; i++) { + if (!ws_connections[i].connected) { + return i; + } + } + return -1; +} + +// WebSocket Connect - returns connection ID as INT +GroundValue ws_connect(GroundScope* scope, List args) { + if (args.size < 1) { + printf("Requires a WebSocket URL argument (ws:// or wss://)\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != STRING) { + printf("Arg 1 needs to be a string (WebSocket URL)\n"); + return groundCreateValue(NONE); + } + + curl_setup(); + + int slot = find_ws_slot(); + if (slot == -1) { + printf("Maximum WebSocket connections reached\n"); + return groundCreateValue(NONE); + } + + CURL* handle = curl_easy_init(); + if (!handle) { + printf("Curl failed to init\n"); + return groundCreateValue(NONE); + } + + curl_easy_setopt(handle, CURLOPT_URL, args.values[0].data.stringVal); + curl_easy_setopt(handle, CURLOPT_CONNECT_ONLY, 2L); // WebSocket mode + curl_easy_setopt(handle, CURLOPT_TIMEOUT, 10L); + + CURLcode result = curl_easy_perform(handle); + + if (result != CURLE_OK) { + printf("WebSocket connection failed: %s\n", curl_easy_strerror(result)); + curl_easy_cleanup(handle); + return groundCreateValue(NONE); + } + + ws_connections[slot].handle = handle; + ws_connections[slot].connected = true; + ws_connections[slot].url = strdup(args.values[0].data.stringVal); + + return groundCreateValue(INT, (int64_t)slot); +} + +// WebSocket Send - sends a text message, returns bytes sent as INT +GroundValue ws_send(GroundScope* scope, List args) { + if (args.size < 2) { + printf("Requires connection ID and message\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != INT || args.values[1].type != STRING) { + printf("Args need to be (INT connection ID, STRING message)\n"); + return groundCreateValue(NONE); + } + + int conn_id = (int)args.values[0].data.intVal; + if (conn_id < 0 || conn_id >= MAX_WS_CONNECTIONS || !ws_connections[conn_id].connected) { + printf("Invalid or closed WebSocket connection\n"); + return groundCreateValue(NONE); + } + + CURL* handle = ws_connections[conn_id].handle; + const char* message = args.values[1].data.stringVal; + size_t sent; + + CURLcode result = curl_ws_send(handle, message, strlen(message), &sent, 0, CURLWS_TEXT); + + if (result != CURLE_OK) { + printf("WebSocket send failed: %s\n", curl_easy_strerror(result)); + return groundCreateValue(NONE); + } + + return groundCreateValue(INT, (int64_t)sent); +} + +// WebSocket Receive - receives a message (blocking) +GroundValue ws_receive(GroundScope* scope, List args) { + if (args.size < 1) { + printf("Requires connection ID\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != INT) { + printf("Arg 1 needs to be an INT (connection ID)\n"); + return groundCreateValue(NONE); + } + + int conn_id = (int)args.values[0].data.intVal; + if (conn_id < 0 || conn_id >= MAX_WS_CONNECTIONS || !ws_connections[conn_id].connected) { + printf("Invalid or closed WebSocket connection\n"); + return groundCreateValue(NONE); + } + + CURL* handle = ws_connections[conn_id].handle; + char buffer[4096]; + size_t received; + const struct curl_ws_frame* meta; + + CURLcode result = curl_ws_recv(handle, buffer, sizeof(buffer) - 1, &received, &meta); + + if (result != CURLE_OK) { + if (result == CURLE_AGAIN) { + // No data available right now + return groundCreateValue(STRING, ""); + } + printf("WebSocket receive failed: %s\n", curl_easy_strerror(result)); + return groundCreateValue(NONE); + } + + buffer[received] = '\0'; + + // Handle different frame types + if (meta->flags & CURLWS_CLOSE) { + printf("WebSocket close frame received\n"); + ws_connections[conn_id].connected = false; + return groundCreateValue(STRING, "[CLOSED]"); + } + + if (meta->flags & CURLWS_PING) { + // Automatically respond to ping with pong + curl_ws_send(handle, "", 0, &received, 0, CURLWS_PONG); + return groundCreateValue(STRING, "[PING]"); + } + + char* data = malloc(received + 1); + memcpy(data, buffer, received); + data[received] = '\0'; + + return groundCreateValue(STRING, data); +} + +// WebSocket Receive with timeout (non-blocking version) +GroundValue ws_receive_timeout(GroundScope* scope, List args) { + if (args.size < 2) { + printf("Requires connection ID and timeout in milliseconds\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != INT || args.values[1].type != INT) { + printf("Both arguments need to be INT (connection ID, timeout_ms)\n"); + return groundCreateValue(NONE); + } + + int conn_id = (int)args.values[0].data.intVal; + long timeout_ms = (long)args.values[1].data.intVal; + + if (conn_id < 0 || conn_id >= MAX_WS_CONNECTIONS || !ws_connections[conn_id].connected) { + printf("Invalid or closed WebSocket connection\n"); + return groundCreateValue(NONE); + } + + CURL* handle = ws_connections[conn_id].handle; + + // Set socket to non-blocking mode + curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, timeout_ms); + + char buffer[4096]; + size_t received; + const struct curl_ws_frame* meta; + + CURLcode result = curl_ws_recv(handle, buffer, sizeof(buffer) - 1, &received, &meta); + + if (result == CURLE_AGAIN || result == CURLE_OPERATION_TIMEDOUT) { + return groundCreateValue(STRING, ""); + } + + if (result != CURLE_OK) { + printf("WebSocket receive failed: %s\n", curl_easy_strerror(result)); + return groundCreateValue(NONE); + } + + buffer[received] = '\0'; + + if (meta->flags & CURLWS_CLOSE) { + ws_connections[conn_id].connected = false; + return groundCreateValue(STRING, "[CLOSED]"); + } + + if (meta->flags & CURLWS_PING) { + curl_ws_send(handle, "", 0, &received, 0, CURLWS_PONG); + return groundCreateValue(STRING, "[PING]"); + } + + char* data = malloc(received + 1); + memcpy(data, buffer, received); + data[received] = '\0'; + + return groundCreateValue(STRING, data); +} + +// WebSocket Close - properly close a connection, returns BOOL success +GroundValue ws_close(GroundScope* scope, List args) { + if (args.size < 1) { + printf("Requires connection ID\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != INT) { + printf("Arg 1 needs to be an INT (connection ID)\n"); + return groundCreateValue(NONE); + } + + int conn_id = (int)args.values[0].data.intVal; + if (conn_id < 0 || conn_id >= MAX_WS_CONNECTIONS || !ws_connections[conn_id].connected) { + printf("Invalid or already closed WebSocket connection\n"); + return groundCreateValue(BOOL, false); + } + + CURL* handle = ws_connections[conn_id].handle; + size_t sent; + + // Send close frame + curl_ws_send(handle, "", 0, &sent, 0, CURLWS_CLOSE); + + // Cleanup + curl_easy_cleanup(handle); + free(ws_connections[conn_id].url); + ws_connections[conn_id].handle = NULL; + ws_connections[conn_id].connected = false; + ws_connections[conn_id].url = NULL; + + return groundCreateValue(BOOL, true); +} + +// WebSocket Send Binary data, returns bytes sent as INT +GroundValue ws_send_binary(GroundScope* scope, List args) { + if (args.size < 2) { + printf("Requires connection ID and binary data\n"); + return groundCreateValue(NONE); + } + if (args.values[0].type != INT || args.values[1].type != STRING) { + printf("Args need to be (INT connection ID, STRING binary data)\n"); + return groundCreateValue(NONE); + } + + int conn_id = (int)args.values[0].data.intVal; + if (conn_id < 0 || conn_id >= MAX_WS_CONNECTIONS || !ws_connections[conn_id].connected) { + printf("Invalid or closed WebSocket connection\n"); + return groundCreateValue(NONE); + } + + CURL* handle = ws_connections[conn_id].handle; + const char* data = args.values[1].data.stringVal; + size_t sent; + + CURLcode result = curl_ws_send(handle, data, strlen(data), &sent, 0, CURLWS_BINARY); + + if (result != CURLE_OK) { + printf("WebSocket binary send failed: %s\n", curl_easy_strerror(result)); + return groundCreateValue(NONE); + } + + return groundCreateValue(INT, (int64_t)sent); +} + +void ground_init(GroundScope* scope) { + // HTTP Functions + groundAddNativeFunction(scope, "request_Get", get_request, STRING, 1, STRING, "url"); + groundAddNativeFunction(scope, "request_Post", post_request, STRING, 2, STRING, "url", STRING, "data"); + + // WebSocket Functions + groundAddNativeFunction(scope, "ws_Connect", ws_connect, INT, 1, STRING, "url"); + groundAddNativeFunction(scope, "ws_Send", ws_send, INT, 2, INT, "conn_id", STRING, "message"); + groundAddNativeFunction(scope, "ws_Receive", ws_receive, STRING, 1, INT, "conn_id"); + groundAddNativeFunction(scope, "ws_ReceiveTimeout", ws_receive_timeout, STRING, 2, INT, "conn_id", INT, "timeout_ms"); + groundAddNativeFunction(scope, "ws_Close", ws_close, BOOL, 1, INT, "conn_id"); + groundAddNativeFunction(scope, "ws_SendBinary", ws_send_binary, INT, 2, INT, "conn_id", STRING, "data"); +}