Linux HTTP implementation
This commit is contained in:
@@ -12,4 +12,5 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
|
||||
# Subdirs
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(log)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(network)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
networklinux.c
|
||||
)
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networklinux.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include <curl/curl.h>
|
||||
|
||||
static networklinux_t NETWORK_LINUX;
|
||||
|
||||
static size_t networkLinuxCurlWrite(
|
||||
char *ptr,
|
||||
size_t size,
|
||||
size_t nmemb,
|
||||
void *userdata
|
||||
) {
|
||||
assertNotNull(ptr, "curl write ptr must not be NULL");
|
||||
assertNotNull(userdata, "curl write userdata must not be NULL");
|
||||
networkhttppendingitem_t *item = (networkhttppendingitem_t *)userdata;
|
||||
size_t incoming = size * nmemb;
|
||||
size_t written = strlen(item->responseBody);
|
||||
size_t space = NETWORK_HTTP_RESPONSE_MAX - 1 - written;
|
||||
|
||||
if(incoming > space) incoming = space;
|
||||
if(incoming > 0) {
|
||||
memcpy(item->responseBody + written, ptr, incoming);
|
||||
item->responseBody[written + incoming] = '\0';
|
||||
}
|
||||
|
||||
/* Return the original byte count — curl treats anything else as an error. */
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
static void networkLinuxHTTPThread(thread_t *thread) {
|
||||
assertNotNull(thread, "Thread must not be NULL");
|
||||
assertNotNull(thread->data, "Thread data must not be NULL");
|
||||
networkhttppendingitem_t *item = (networkhttppendingitem_t *)thread->data;
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if(!curl) {
|
||||
stringCopy(
|
||||
item->errorMessage, "curl_easy_init failed", NETWORK_ERROR_MESSAGE_MAX - 1
|
||||
);
|
||||
item->isError = true;
|
||||
goto done;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, item->url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, networkLinuxCurlWrite);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, item);
|
||||
|
||||
switch(item->method) {
|
||||
case NETWORK_HTTP_REQUEST_METHOD_POST:
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(
|
||||
curl, CURLOPT_POSTFIELDS, item->hasBody ? item->body : ""
|
||||
);
|
||||
curl_easy_setopt(
|
||||
curl, CURLOPT_POSTFIELDSIZE,
|
||||
(long)(item->hasBody ? strlen(item->body) : 0)
|
||||
);
|
||||
break;
|
||||
|
||||
case NETWORK_HTTP_REQUEST_METHOD_PUT:
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
if(item->hasBody) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, item->body);
|
||||
curl_easy_setopt(
|
||||
curl, CURLOPT_POSTFIELDSIZE, (long)strlen(item->body)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case NETWORK_HTTP_REQUEST_METHOD_DELETE:
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
||||
break;
|
||||
|
||||
case NETWORK_HTTP_REQUEST_METHOD_HEAD:
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
break;
|
||||
|
||||
case NETWORK_HTTP_REQUEST_METHOD_OPTIONS:
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
|
||||
break;
|
||||
|
||||
case NETWORK_HTTP_REQUEST_METHOD_GET:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/* Build header list */
|
||||
struct curl_slist *curlHeaders = NULL;
|
||||
if(item->headerCount > 0) {
|
||||
char_t line[NETWORK_HTTP_HEADER_KEY_MAX + NETWORK_HTTP_HEADER_VAL_MAX + 3];
|
||||
for(uint32_t i = 0; i < item->headerCount; i++) {
|
||||
snprintf(
|
||||
line, sizeof(line), "%s: %s", item->headerKeys[i], item->headerVals[i]
|
||||
);
|
||||
curlHeaders = curl_slist_append(curlHeaders, line);
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlHeaders);
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
if(res != CURLE_OK) {
|
||||
stringCopy(
|
||||
item->errorMessage, curl_easy_strerror(res), NETWORK_ERROR_MESSAGE_MAX - 1
|
||||
);
|
||||
item->isError = true;
|
||||
} else {
|
||||
long code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
|
||||
item->status = (uint16_t)code;
|
||||
item->isError = false;
|
||||
}
|
||||
|
||||
if(curlHeaders) curl_slist_free_all(curlHeaders);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
done:
|
||||
threadMutexLock(&NETWORK_LINUX.resultsMutex);
|
||||
item->resultReady = true;
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
}
|
||||
|
||||
errorret_t networkLinuxInit() {
|
||||
memoryZero(&NETWORK_LINUX, sizeof(networklinux_t));
|
||||
threadMutexInit(&NETWORK_LINUX.resultsMutex);
|
||||
|
||||
CURLcode res = curl_global_init(CURL_GLOBAL_ALL);
|
||||
if(res != CURLE_OK) {
|
||||
errorThrow("curl_global_init failed: %s", curl_easy_strerror(res));
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkLinuxUpdate() {
|
||||
for(int32_t i = 0; i < NETWORK_HTTP_PENDING_MAX; i++) {
|
||||
/* Check under lock whether this slot has a result waiting. */
|
||||
threadMutexLock(&NETWORK_LINUX.resultsMutex);
|
||||
|
||||
networkhttppendingitem_t *item = &NETWORK_LINUX.requests[i];
|
||||
if(!item->used || !item->resultReady) {
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Snapshot the small values we need after we drop the lock. */
|
||||
bool_t isError = item->isError;
|
||||
uint16_t status = item->status;
|
||||
networkhttpcallback_t cb = item->callback;
|
||||
networkhttperrorcallback_t errCb = item->errorCallback;
|
||||
void *user = item->user;
|
||||
|
||||
/*
|
||||
* Keep pointers into item's string buffers — valid because item->used is
|
||||
* still true here and only this (main) thread clears it.
|
||||
*/
|
||||
const char_t *responseBody = item->responseBody;
|
||||
const char_t *errorMessage = item->errorMessage;
|
||||
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
|
||||
/* Fire the callback without holding the lock. */
|
||||
if(isError) {
|
||||
errorstate_t errState;
|
||||
errState.code = ERROR_NOT_OK;
|
||||
errState.message = (char_t *)errorMessage;
|
||||
errState.lines = (char_t *)"";
|
||||
errorret_t err;
|
||||
err.code = ERROR_NOT_OK;
|
||||
err.state = &errState;
|
||||
errCb(err, user);
|
||||
} else {
|
||||
cb(status, responseBody, NULL, 0, user);
|
||||
}
|
||||
|
||||
/* Release the slot now that the callback has consumed it. */
|
||||
threadMutexLock(&NETWORK_LINUX.resultsMutex);
|
||||
item->used = false;
|
||||
item->resultReady = false;
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkLinuxDispose() {
|
||||
curl_global_cleanup();
|
||||
threadMutexDispose(&NETWORK_LINUX.resultsMutex);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkLinuxHTTPRequest(
|
||||
const char_t *url,
|
||||
const networkhttprequestmethod_t method,
|
||||
const char_t *bodyOrNull,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
void *user,
|
||||
networkhttpcallback_t callback,
|
||||
networkhttperrorcallback_t errorCallback
|
||||
) {
|
||||
assertStrLenMin(url, 1, "URL must be non-empty");
|
||||
assertStrLenMax(url, NETWORK_HTTP_URL_MAX, "URL exceeds maximum length");
|
||||
assertNotNull(callback, "Callback must be non-NULL");
|
||||
assertNotNull(errorCallback, "Error callback must be non-NULL");
|
||||
assertTrue(headerCount == 0 || headers != NULL, "Headers must not be NULL when headerCount > 0");
|
||||
|
||||
/* Allocate a slot. */
|
||||
threadMutexLock(&NETWORK_LINUX.resultsMutex);
|
||||
|
||||
networkhttppendingitem_t *item = NULL;
|
||||
for(int32_t i = 0; i < NETWORK_HTTP_PENDING_MAX; i++) {
|
||||
if(!NETWORK_LINUX.requests[i].used) {
|
||||
item = &NETWORK_LINUX.requests[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
errorstate_t errState;
|
||||
errState.code = ERROR_NOT_OK;
|
||||
errState.message = (char_t *)"No free HTTP request slots";
|
||||
errState.lines = (char_t *)"";
|
||||
errorret_t err;
|
||||
err.code = ERROR_NOT_OK;
|
||||
err.state = &errState;
|
||||
errorCallback(err, user);
|
||||
return;
|
||||
}
|
||||
|
||||
memoryZero(item, sizeof(networkhttppendingitem_t));
|
||||
item->used = true;
|
||||
|
||||
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
|
||||
|
||||
/* Fill the slot (safe: only we have access while used=true, resultReady=false). */
|
||||
stringCopy(item->url, url, NETWORK_HTTP_URL_MAX - 1);
|
||||
item->method = method;
|
||||
|
||||
if(bodyOrNull != NULL) {
|
||||
assertStrLenMax(bodyOrNull, NETWORK_HTTP_BODY_MAX, "Body exceeds maximum length");
|
||||
stringCopy(item->body, bodyOrNull, NETWORK_HTTP_BODY_MAX - 1);
|
||||
item->hasBody = true;
|
||||
}
|
||||
|
||||
uint32_t hdrCount = headerCount;
|
||||
if(hdrCount > NETWORK_HTTP_HEADER_MAX) hdrCount = NETWORK_HTTP_HEADER_MAX;
|
||||
item->headerCount = hdrCount;
|
||||
for(uint32_t i = 0; i < hdrCount; i++) {
|
||||
assertStrLenMax(headers[i].key, NETWORK_HTTP_HEADER_KEY_MAX, "Header key exceeds maximum length");
|
||||
assertStrLenMax(headers[i].value, NETWORK_HTTP_HEADER_VAL_MAX, "Header value exceeds maximum length");
|
||||
stringCopy(item->headerKeys[i], headers[i].key, NETWORK_HTTP_HEADER_KEY_MAX - 1);
|
||||
stringCopy(item->headerVals[i], headers[i].value, NETWORK_HTTP_HEADER_VAL_MAX - 1);
|
||||
}
|
||||
|
||||
item->callback = callback;
|
||||
item->errorCallback = errorCallback;
|
||||
item->user = user;
|
||||
|
||||
/* Kick off the background thread. */
|
||||
threadInit(&item->thread, networkLinuxHTTPThread);
|
||||
item->thread.data = item;
|
||||
threadStartRequest(&item->thread);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "network/networkhttprequest.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
#define NETWORK_HTTP_PENDING_MAX 16
|
||||
#define NETWORK_HTTP_URL_MAX 512
|
||||
#define NETWORK_HTTP_BODY_MAX 8192
|
||||
#define NETWORK_HTTP_RESPONSE_MAX 65536
|
||||
#define NETWORK_HTTP_HEADER_MAX 16
|
||||
#define NETWORK_HTTP_HEADER_KEY_MAX 64
|
||||
#define NETWORK_HTTP_HEADER_VAL_MAX 256
|
||||
#define NETWORK_ERROR_MESSAGE_MAX 256
|
||||
|
||||
typedef struct {
|
||||
bool_t used;
|
||||
volatile bool_t resultReady;
|
||||
bool_t isError;
|
||||
|
||||
char_t url[NETWORK_HTTP_URL_MAX];
|
||||
networkhttprequestmethod_t method;
|
||||
char_t body[NETWORK_HTTP_BODY_MAX];
|
||||
bool_t hasBody;
|
||||
char_t headerKeys[NETWORK_HTTP_HEADER_MAX][NETWORK_HTTP_HEADER_KEY_MAX];
|
||||
char_t headerVals[NETWORK_HTTP_HEADER_MAX][NETWORK_HTTP_HEADER_VAL_MAX];
|
||||
uint32_t headerCount;
|
||||
|
||||
networkhttpcallback_t callback;
|
||||
networkhttperrorcallback_t errorCallback;
|
||||
void *user;
|
||||
|
||||
uint16_t status;
|
||||
char_t responseBody[NETWORK_HTTP_RESPONSE_MAX];
|
||||
char_t errorMessage[NETWORK_ERROR_MESSAGE_MAX];
|
||||
|
||||
thread_t thread;
|
||||
} networkhttppendingitem_t;
|
||||
|
||||
typedef struct {
|
||||
networkhttppendingitem_t requests[NETWORK_HTTP_PENDING_MAX];
|
||||
threadmutex_t resultsMutex;
|
||||
} networklinux_t;
|
||||
|
||||
/**
|
||||
* Initializes the network manager. Must be called before any other network
|
||||
* functions.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkLinuxInit();
|
||||
|
||||
/**
|
||||
* Updates the network manager, called once per frame to handle completed
|
||||
* HTTP requests.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkLinuxUpdate();
|
||||
|
||||
/**
|
||||
* Disposes the network manager.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkLinuxDispose();
|
||||
|
||||
/**
|
||||
* Submits an asynchronous HTTP request. The callback will be invoked on the
|
||||
* main thread when next available.
|
||||
*
|
||||
* @param url URL to request.
|
||||
* @param method Method to use for the request.
|
||||
* @param bodyOrNull If POST or PUT, custom body string, can be NULL.
|
||||
* @param headers Array of key-value headers.
|
||||
* @param headerCount Count of headers, can be anything if headers is NULL.
|
||||
* @param user Callback pointer received to the callback.
|
||||
* @param callback The callback to invoke when the request completes.
|
||||
* @param errorCallback The callback to invoke if the request fails.
|
||||
* Note, this doesn't count Non-200 status codes, just
|
||||
* network errors.
|
||||
*/
|
||||
void networkLinuxHTTPRequest(
|
||||
const char_t *url,
|
||||
const networkhttprequestmethod_t method,
|
||||
const char_t *bodyOrNull,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
void *user,
|
||||
networkhttpcallback_t callback,
|
||||
networkhttperrorcallback_t errorCallback
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "networklinux.h"
|
||||
|
||||
#define networkPlatformInit networkLinuxInit
|
||||
#define networkPlatformUpdate networkLinuxUpdate
|
||||
#define networkPlatformDispose networkLinuxDispose
|
||||
#define networkPlatformHTTPRequest networkLinuxHTTPRequest
|
||||
Reference in New Issue
Block a user