From 9c71df5bfd3611bc05e1f4e5bc9e06dbb95c52e7 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 19 Apr 2026 16:22:00 -0500 Subject: [PATCH] mbed first pass --- cmake/mbedtls_dolphin_config.h | 25 + cmake/modules/Findmbedtls.cmake | 201 +++++++ cmake/targets/dolphin.cmake | 14 + cmake/targets/linux.cmake | 4 + docker/dolphin/Dockerfile | 2 +- src/dusk/CMakeLists.txt | 12 +- src/dusk/engine/engine.c | 48 +- src/dusk/network/CMakeLists.txt | 3 + src/dusk/network/httpclient.c | 545 ++++++++++++++++++ src/dusk/network/httpclient.h | 170 ++++++ src/dusk/network/networksocket.c | 165 ++++++ src/dusk/network/networksocket.h | 170 ++++++ src/dusk/network/networktls.c | 288 +++++++++ src/dusk/network/networktls.h | 108 ++++ src/dusk/util/endian.c | 78 ++- src/dusk/util/endian.h | 58 +- src/duskdolphin/network/CMakeLists.txt | 1 + .../network/networksocketdolphin.c | 162 ++++++ .../network/networksocketdolphin.h | 58 ++ .../network/networksocketplatform.h | 18 + src/dusklinux/network/CMakeLists.txt | 1 + src/dusklinux/network/networksocketlinux.c | 142 +++++ src/dusklinux/network/networksocketlinux.h | 64 ++ src/dusklinux/network/networksocketplatform.h | 18 + 24 files changed, 2347 insertions(+), 8 deletions(-) create mode 100644 cmake/mbedtls_dolphin_config.h create mode 100644 cmake/modules/Findmbedtls.cmake create mode 100644 src/dusk/network/httpclient.c create mode 100644 src/dusk/network/httpclient.h create mode 100644 src/dusk/network/networksocket.c create mode 100644 src/dusk/network/networksocket.h create mode 100644 src/dusk/network/networktls.c create mode 100644 src/dusk/network/networktls.h create mode 100644 src/duskdolphin/network/networksocketdolphin.c create mode 100644 src/duskdolphin/network/networksocketdolphin.h create mode 100644 src/duskdolphin/network/networksocketplatform.h create mode 100644 src/dusklinux/network/networksocketlinux.c create mode 100644 src/dusklinux/network/networksocketlinux.h create mode 100644 src/dusklinux/network/networksocketplatform.h diff --git a/cmake/mbedtls_dolphin_config.h b/cmake/mbedtls_dolphin_config.h new file mode 100644 index 00000000..d0c8146d --- /dev/null +++ b/cmake/mbedtls_dolphin_config.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +/** + * Wii/Dolphin overrides for mbedtls 4.x. + * Used as both MBEDTLS_USER_CONFIG_FILE and TF_PSA_CRYPTO_USER_CONFIG_FILE, + * so intentionally has no include guard (operations are idempotent). + */ + +/* Disable the mbedtls TCP/socket layer — Wii has no Unix/Windows sockets. + * We provide our own socket implementation via networksocket.c. */ +#undef MBEDTLS_NET_C + +/* Disable built-in Unix/Windows entropy; use driver-provided entropy instead. + * We implement mbedtls_platform_get_entropy() in networktls.c. */ +#undef MBEDTLS_PSA_BUILTIN_GET_ENTROPY +#define MBEDTLS_PSA_DRIVER_GET_ENTROPY + +/* Disable the built-in ms_time; we implement mbedtls_ms_time() via OGC + * timer (gettime / ticks_to_millisecs) in networktls.c. */ +#define MBEDTLS_PLATFORM_MS_TIME_ALT diff --git a/cmake/modules/Findmbedtls.cmake b/cmake/modules/Findmbedtls.cmake new file mode 100644 index 00000000..524355db --- /dev/null +++ b/cmake/modules/Findmbedtls.cmake @@ -0,0 +1,201 @@ +# Findmbedtls.cmake +# +# Usage: +# find_package(mbedtls REQUIRED) +# +# Optional cache variables the parent project may set before calling: +# MBEDTLS_FETCHCONTENT_VERSION e.g. "v3.6.4" or "mbedtls-4.1.0" +# MBEDTLS_FETCHCONTENT_GIT_REPOSITORY +# MBEDTLS_FETCHCONTENT_GIT_TAG +# MBEDTLS_FETCHCONTENT_BASE_DIR +# MBEDTLS_BUILD_SHARED ON/OFF +# +# Provided variables: +# mbedtls_FOUND +# MBEDTLS_FOUND +# MBEDTLS_INCLUDE_DIRS +# MBEDTLS_LIBRARIES +# +# Provided imported targets: +# MbedTLS::mbedtls +# MbedTLS::mbedx509 +# MbedTLS::mbedcrypto + +include_guard(GLOBAL) + +include(FetchContent) +include(FindPackageHandleStandardArgs) + +set(_MBEDTLS_DEFAULT_REPOSITORY "https://github.com/Mbed-TLS/mbedtls.git") +set(_MBEDTLS_DEFAULT_TAG "v4.1.0") + +set(MBEDTLS_FETCHCONTENT_GIT_REPOSITORY + "${_MBEDTLS_DEFAULT_REPOSITORY}" + CACHE STRING "Git repository for fetching Mbed TLS") + +if(DEFINED MBEDTLS_FETCHCONTENT_VERSION AND NOT DEFINED MBEDTLS_FETCHCONTENT_GIT_TAG) + set(MBEDTLS_FETCHCONTENT_GIT_TAG + "${MBEDTLS_FETCHCONTENT_VERSION}" + CACHE STRING "Git tag/branch/commit for fetching Mbed TLS") +endif() + +set(MBEDTLS_FETCHCONTENT_GIT_TAG + "${MBEDTLS_FETCHCONTENT_GIT_TAG}" + CACHE STRING "Git tag/branch/commit for fetching Mbed TLS") + +if(NOT MBEDTLS_FETCHCONTENT_GIT_TAG) + set(MBEDTLS_FETCHCONTENT_GIT_TAG "${_MBEDTLS_DEFAULT_TAG}" CACHE STRING "" FORCE) +endif() + +option(MBEDTLS_BUILD_SHARED "Build Mbed TLS shared libraries" OFF) + +# 1) Prefer an installed package config if available. +find_package(MbedTLS CONFIG QUIET) + +if(TARGET MbedTLS::mbedtls AND TARGET MbedTLS::mbedx509 AND TARGET MbedTLS::mbedcrypto) + set(mbedtls_FOUND TRUE) + set(MBEDTLS_FOUND TRUE) + set(MBEDTLS_LIBRARIES + MbedTLS::mbedtls + MbedTLS::mbedx509 + MbedTLS::mbedcrypto) + set(MBEDTLS_INCLUDE_DIRS "") + return() +endif() + +# 2) If upstream exported plain targets instead of namespaced ones, alias them. +if(TARGET mbedtls AND TARGET mbedx509 AND TARGET mbedcrypto) + if(NOT TARGET MbedTLS::mbedtls) + add_library(MbedTLS::mbedtls INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedtls INTERFACE mbedtls) + endif() + if(NOT TARGET MbedTLS::mbedx509) + add_library(MbedTLS::mbedx509 INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedx509 INTERFACE mbedx509) + endif() + if(NOT TARGET MbedTLS::mbedcrypto) + add_library(MbedTLS::mbedcrypto INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedcrypto INTERFACE mbedcrypto) + endif() + + set(mbedtls_FOUND TRUE) + set(MBEDTLS_FOUND TRUE) + set(MBEDTLS_LIBRARIES + MbedTLS::mbedtls + MbedTLS::mbedx509 + MbedTLS::mbedcrypto) + set(MBEDTLS_INCLUDE_DIRS "") + return() +endif() + +# 3) Fetch and build Mbed TLS. +# Upstream options: +# - USE_STATIC_MBEDTLS_LIBRARY / USE_SHARED_MBEDTLS_LIBRARY +# - ENABLE_PROGRAMS / ENABLE_TESTING +# - MBEDTLS_AS_SUBPROJECT / DISABLE_PACKAGE_CONFIG_AND_INSTALL +# - MBEDTLS_TARGET_PREFIX +# +# These are supported by the upstream CMake build. :contentReference[oaicite:1]{index=1} + +set(FETCHCONTENT_QUIET FALSE) + +if(MBEDTLS_FETCHCONTENT_BASE_DIR) + set(FETCHCONTENT_BASE_DIR "${MBEDTLS_FETCHCONTENT_BASE_DIR}") +endif() + +# Avoid polluting the parent build and skip extras we usually do not want. +set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ENABLE_TESTING OFF CACHE BOOL "" FORCE) +set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE) +set(MBEDTLS_AS_SUBPROJECT ON CACHE BOOL "" FORCE) +set(MBEDTLS_TARGET_PREFIX "" CACHE STRING "" FORCE) + +if(MBEDTLS_BUILD_SHARED) + set(USE_SHARED_MBEDTLS_LIBRARY ON CACHE BOOL "" FORCE) + set(USE_STATIC_MBEDTLS_LIBRARY OFF CACHE BOOL "" FORCE) +else() + set(USE_SHARED_MBEDTLS_LIBRARY OFF CACHE BOOL "" FORCE) + set(USE_STATIC_MBEDTLS_LIBRARY ON CACHE BOOL "" FORCE) +endif() + +FetchContent_Declare( + mbedtls_fc + GIT_REPOSITORY "${MBEDTLS_FETCHCONTENT_GIT_REPOSITORY}" + GIT_TAG "${MBEDTLS_FETCHCONTENT_GIT_TAG}" + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(mbedtls_fc) + +# 4) Normalize targets across upstream versions. +# +# Mbed TLS 3.x: +# mbedtls, mbedx509, mbedcrypto +# +# Mbed TLS 4.x: +# mbedtls, mbedx509, tfpsacrypto +# +# Map everything to stable namespaced targets for the consumer. :contentReference[oaicite:2]{index=2} + +set(_mbedtls_tls_target "") +set(_mbedtls_x509_target "") +set(_mbedtls_crypto_target "") + +if(TARGET mbedtls) + set(_mbedtls_tls_target mbedtls) +elseif(TARGET MbedTLS::mbedtls) + set(_mbedtls_tls_target MbedTLS::mbedtls) +endif() + +if(TARGET mbedx509) + set(_mbedtls_x509_target mbedx509) +elseif(TARGET MbedTLS::mbedx509) + set(_mbedtls_x509_target MbedTLS::mbedx509) +endif() + +if(TARGET mbedcrypto) + set(_mbedtls_crypto_target mbedcrypto) +elseif(TARGET tfpsacrypto) + set(_mbedtls_crypto_target tfpsacrypto) +elseif(TARGET MbedTLS::mbedcrypto) + set(_mbedtls_crypto_target MbedTLS::mbedcrypto) +endif() + +if(_mbedtls_tls_target AND NOT TARGET MbedTLS::mbedtls) + add_library(MbedTLS::mbedtls INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedtls INTERFACE "${_mbedtls_tls_target}") +endif() + +if(_mbedtls_x509_target AND NOT TARGET MbedTLS::mbedx509) + add_library(MbedTLS::mbedx509 INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedx509 INTERFACE "${_mbedtls_x509_target}") +endif() + +if(_mbedtls_crypto_target AND NOT TARGET MbedTLS::mbedcrypto) + add_library(MbedTLS::mbedcrypto INTERFACE IMPORTED) + target_link_libraries(MbedTLS::mbedcrypto INTERFACE "${_mbedtls_crypto_target}") +endif() + +find_package_handle_standard_args( + mbedtls + REQUIRED_VARS + _mbedtls_tls_target + _mbedtls_x509_target + _mbedtls_crypto_target +) + +if(mbedtls_FOUND) + set(MBEDTLS_FOUND TRUE) + set(MBEDTLS_LIBRARIES + MbedTLS::mbedtls + MbedTLS::mbedx509 + MbedTLS::mbedcrypto) + + # Best-effort include directory discovery for legacy consumers. + get_target_property(_mbedtls_inc "${_mbedtls_tls_target}" INTERFACE_INCLUDE_DIRECTORIES) + if(_mbedtls_inc) + set(MBEDTLS_INCLUDE_DIRS "${_mbedtls_inc}") + else() + set(MBEDTLS_INCLUDE_DIRS "") + endif() +endif() \ No newline at end of file diff --git a/cmake/targets/dolphin.cmake b/cmake/targets/dolphin.cmake index 2f10c8d6..a80908e3 100644 --- a/cmake/targets/dolphin.cmake +++ b/cmake/targets/dolphin.cmake @@ -1,9 +1,23 @@ +# mbedtls/tf-psa-crypto user config overrides for Wii/Dolphin. +# Both variables point to the same file; it is included after each library's +# default config header, so #undef/#define are applied on top of the defaults. +# Must be set before find_package(mbedtls) so FetchContent picks them up. +set(TF_PSA_CRYPTO_USER_CONFIG_FILE + "${CMAKE_SOURCE_DIR}/cmake/mbedtls_dolphin_config.h" + CACHE FILEPATH "tf-psa-crypto user config for Wii/Dolphin" FORCE) +set(MBEDTLS_USER_CONFIG_FILE + "${CMAKE_SOURCE_DIR}/cmake/mbedtls_dolphin_config.h" + CACHE FILEPATH "mbedtls user config for Wii/Dolphin" FORCE) + # Target definitions target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN DUSK_INPUT_GAMEPAD DUSK_DISPLAY_WIDTH=640 DUSK_DISPLAY_HEIGHT=480 + MBEDTLS_PSA_DRIVER_GET_ENTROPY + MBEDTLS_PLATFORM_MS_TIME_ALT + THREAD_PTHREAD=1 ) # Custom compiler flags diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index 172736bd..f0383611 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -26,6 +26,10 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC # CURL::libcurl ) +target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + ${MBEDTLS_INCLUDE_DIR} +) + # Define platform-specific macros. target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_SDL2 diff --git a/docker/dolphin/Dockerfile b/docker/dolphin/Dockerfile index 055e04b5..a0906415 100644 --- a/docker/dolphin/Dockerfile +++ b/docker/dolphin/Dockerfile @@ -1,6 +1,6 @@ FROM devkitpro/devkitppc WORKDIR /workdir RUN apt update && \ - apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl && \ + apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl python3-jsonschema python3-jinja2 python3-jsonschema && \ dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip VOLUME ["/workdir"] \ No newline at end of file diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index a4192a65..22b167ef 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -46,6 +46,16 @@ if(NOT Lua_FOUND) target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Lua::Lua) endif() +if(NOT mbedtls_FOUND) + find_package(mbedtls REQUIRED) + if(mbedtls_FOUND) + target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ${MBEDTLS_LIBRARIES}) + target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ${MBEDTLS_INCLUDE_DIRS}) + else() + message(FATAL_ERROR "mbedtls not found. Please ensure mbedtls is correctly installed.") + endif() +endif() + # Includes target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC @@ -77,4 +87,4 @@ add_subdirectory(time) add_subdirectory(ui) add_subdirectory(network) add_subdirectory(util) -# add_subdirectory(thread) \ No newline at end of file +add_subdirectory(thread) \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index af8ba34d..28321133 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -24,6 +24,8 @@ #include "network/networkinfo.h" #include "system/system.h" +#include "network/httpclient.h" + #include "display/mesh/cube.h" #include "display/mesh/plane.h" @@ -36,6 +38,18 @@ float_t onlineSwapTime = FLT_MAX; void goOnline(); void goOffline(); +void onGETComplete(httpclient_t *client, void *user) { + sceneLog("GET request complete!\n"); + sceneLog("Response status: %u\n", client->statusCode); + for(size_t i = 0; i < client->responseHeaderCount; i++) { + sceneLog("Header: %s: %s\n", client->responseHeaders[i].name, client->responseHeaders[i].value); + } +} + +void onGETError(httpclient_t *client, errorret_t err, void *user) { + errorCatch(errorPrint(err)); +} + void onNetworkConnected(void *user) { onlineSwapTime = TIME.time + 3.0f; @@ -57,7 +71,35 @@ void onNetworkConnected(void *user) { #endif } - sceneLog("Network connected, I will disconnect at: %.2f1.\n", onlineSwapTime); + char_t *domain = "https://google.com"; + sceneLog("Online, sending GET to %s...\n", domain); + httpclient_t client; + errorret_t ret; + ret = httpclientInit(&client); + if(ret.code != ERROR_OK) { + errorCatch(errorPrint(ret)); + return; + } + + httpclientRequest( + &client, + "GET", + domain, + false, + NULL, + NULL, + onGETComplete, + onGETError, + NULL + ); + + ret = httpclientDispose(&client); + if(ret.code != ERROR_OK) { + errorCatch(errorPrint(ret)); + return; + } + + // sceneLog("Network connected, I will disconnect at: %.2f1.\n", onlineSwapTime); } void onNetworkFailed(errorret_t error, void *user) { @@ -113,8 +155,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { // errorChain(networkInit()); errorChain(gameInit()); - sceneLog("Init done, going to queue online in 3 seconds...\n"); - onlineSwapTime = TIME.time + 3.0f; + sceneLog("Init done, going to queue online in 1 seconds...\n"); + onlineSwapTime = TIME.time + 1.0f; // Camera entityid_t cam = entityManagerAdd(); diff --git a/src/dusk/network/CMakeLists.txt b/src/dusk/network/CMakeLists.txt index 6926e0b1..72c32bac 100644 --- a/src/dusk/network/CMakeLists.txt +++ b/src/dusk/network/CMakeLists.txt @@ -7,4 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC network.c networkinfo.c + networksocket.c + networktls.c + httpclient.c ) diff --git a/src/dusk/network/httpclient.c b/src/dusk/network/httpclient.c new file mode 100644 index 00000000..7fb5def9 --- /dev/null +++ b/src/dusk/network/httpclient.c @@ -0,0 +1,545 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "httpclient.h" +#include "util/memory.h" +#include "assert/assert.h" + +/* ---- helpers ----------------------------------------------------------- */ + +static bool_t httpclientStrEqualCI(const char_t *a, const char_t *b) { + while(*a && *b) { + if(tolower((unsigned char)*a) != tolower((unsigned char)*b)) return false; + a++; b++; + } + return *a == '\0' && *b == '\0'; +} + +/** + * Reads exactly len bytes from the TLS stream, retrying on timeout. + * Returns an error on I/O failure or unexpected connection close. + */ +static errorret_t httpclientReadExact( + networktls_t *tls, + errorstate_t *es, + uint8_t *buf, + size_t len +) { + size_t total = 0; + while(total < len) { + size_t got = 0; + errorret_t err = networktlsRead(tls, buf + total, len - total, &got); + if(err.code != ERROR_OK) return err; + if(got == NETWORKSOCKET_RECV_CLOSED) { + errorThrowState(es, "Connection closed before all expected bytes arrived"); + } + total += got; + } + errorOk(); +} + +/** + * Reads bytes one at a time until \r\n is found. Writes the line (without + * the terminator) into buf, null-terminates, and sets *outLen. Returns an + * error on I/O failure or if the line exceeds maxLen-1 characters. + */ +static errorret_t httpclientReadLine( + networktls_t *tls, + errorstate_t *es, + char_t *buf, + size_t maxLen, + size_t *outLen +) { + size_t len = 0; + uint8_t ch; + size_t got; + errorret_t err; + + while(len < maxLen - 1) { + do { + got = 0; + err = networktlsRead(tls, &ch, 1, &got); + if(err.code != ERROR_OK) return err; + if(got == NETWORKSOCKET_RECV_CLOSED) { + errorThrowState(es, "Connection closed during header read"); + } + } while(got == 0); + + if(ch == '\r') { + /* consume the \n */ + do { + got = 0; + err = networktlsRead(tls, &ch, 1, &got); + if(err.code != ERROR_OK) return err; + if(got == NETWORKSOCKET_RECV_CLOSED) break; + } while(got == 0); + break; + } + buf[len++] = (char_t)ch; + } + + buf[len] = '\0'; + *outLen = len; + errorOk(); +} + +/* ---- response parsing -------------------------------------------------- */ + +/** + * Parses status code from "HTTP/x.y NNN ..." — returns 0 on failure. + */ +static uint16_t httpclientParseStatus(const char_t *line) { + const char_t *p = strchr(line, ' '); + if(!p) return 0; + return (uint16_t)atoi(p + 1); +} + +/** + * Parses "Name: Value" into the client's responseHeaders array. Trims + * leading whitespace from the value. + */ +static void httpclientParseHeader( + httpclient_t *client, + const char_t *line +) { + const char_t *colon; + const char_t *valStart; + size_t nameLen; + size_t valLen; + httpheader_t *h; + + if(client->responseHeaderCount >= HTTPCLIENT_HEADER_MAX) return; + + colon = strchr(line, ':'); + if(!colon) return; + + nameLen = (size_t)(colon - line); + if(nameLen == 0 || nameLen >= HTTPCLIENT_HEADER_NAME_MAX) return; + + valStart = colon + 1; + while(*valStart == ' ' || *valStart == '\t') valStart++; + valLen = strlen(valStart); + if(valLen >= HTTPCLIENT_HEADER_VALUE_MAX) valLen = HTTPCLIENT_HEADER_VALUE_MAX - 1; + + h = &client->responseHeaders[client->responseHeaderCount++]; + memoryCopy(h->name, line, nameLen); + h->name[nameLen] = '\0'; + memoryCopy(h->value, valStart, valLen); + h->value[valLen] = '\0'; +} + +static const char_t *httpclientGetResponseHeader( + const httpclient_t *client, + const char_t *name +) { + size_t i; + for(i = 0; i < client->responseHeaderCount; i++) { + if(httpclientStrEqualCI(client->responseHeaders[i].name, name)) { + return client->responseHeaders[i].value; + } + } + return NULL; +} + +/* ---- body delivery ----------------------------------------------------- */ + +static errorret_t httpclientReadBodyFixed( + httpclient_t *client, + size_t contentLength +) { + uint8_t buf[HTTPCLIENT_BODY_BUF_SIZE]; + size_t remaining = contentLength; + size_t got; + errorret_t err; + + while(remaining > 0) { + size_t want = remaining < sizeof(buf) ? remaining : sizeof(buf); + err = httpclientReadExact(&client->tls, &client->errorState, buf, want); + if(err.code != ERROR_OK) return err; + if(client->onData) client->onData(client, buf, want, client->user); + remaining -= want; + got = want; + (void)got; + } + errorOk(); +} + +static errorret_t httpclientReadBodyChunked(httpclient_t *client) { + char_t sizeLine[32]; + size_t lineLen; + errorret_t err; + uint8_t buf[HTTPCLIENT_BODY_BUF_SIZE]; + + for(;;) { + err = httpclientReadLine( + &client->tls, &client->errorState, sizeLine, sizeof(sizeLine), &lineLen + ); + if(err.code != ERROR_OK) return err; + + /* strtol parses hex chunk size; stop on terminating "0" chunk */ + size_t chunkSize = (size_t)strtol(sizeLine, NULL, 16); + if(chunkSize == 0) { + /* consume trailing CRLF after the zero chunk */ + httpclientReadLine( + &client->tls, &client->errorState, sizeLine, sizeof(sizeLine), &lineLen + ); + break; + } + + size_t remaining = chunkSize; + while(remaining > 0) { + size_t want = remaining < sizeof(buf) ? remaining : sizeof(buf); + err = httpclientReadExact( + &client->tls, &client->errorState, buf, want + ); + if(err.code != ERROR_OK) return err; + if(client->onData) client->onData(client, buf, want, client->user); + remaining -= want; + } + + /* consume the CRLF after chunk data */ + err = httpclientReadLine( + &client->tls, &client->errorState, sizeLine, sizeof(sizeLine), &lineLen + ); + if(err.code != ERROR_OK) return err; + } + errorOk(); +} + +static errorret_t httpclientReadBodyUntilClose(httpclient_t *client) { + uint8_t buf[HTTPCLIENT_BODY_BUF_SIZE]; + size_t got; + errorret_t err; + + for(;;) { + err = networktlsRead(&client->tls, buf, sizeof(buf), &got); + if(err.code != ERROR_OK) return err; + if(got == NETWORKSOCKET_RECV_CLOSED) break; + if(got > 0 && client->onData) { + client->onData(client, buf, got, client->user); + } + } + errorOk(); +} + +/* ---- TLS onConnect — runs the full HTTP exchange ----------------------- */ + +static void httpclientTlsOnConnect(networktls_t *tls, void *user) { + httpclient_t *client = (httpclient_t *)user; + char_t reqBuf[HTTPCLIENT_REQ_BUF_SIZE]; + char_t lineBuf[HTTPCLIENT_HEADER_NAME_MAX + HTTPCLIENT_HEADER_VALUE_MAX + 4]; + int reqLen; + size_t lineLen; + size_t i; + errorret_t err; + + /* ---- build request headers ----------------------------------------- */ + reqLen = snprintf( + reqBuf, sizeof(reqBuf), + "%s %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "User-Agent: DuskEngine/1.0\r\n", + client->method, + client->url.path[0] != '\0' ? client->url.path : "/", + client->url.host + ); + + for(i = 0; i < client->requestHeaderCount; i++) { + int n = snprintf( + reqBuf + reqLen, sizeof(reqBuf) - (size_t)reqLen, + "%s: %s\r\n", + client->requestHeaders[i].name, + client->requestHeaders[i].value + ); + if(n > 0) reqLen += n; + } + + if(client->requestBodyLen > 0) { + int n = snprintf( + reqBuf + reqLen, sizeof(reqBuf) - (size_t)reqLen, + "Content-Length: %zu\r\n", + client->requestBodyLen + ); + if(n > 0) reqLen += n; + } + + /* blank line terminates headers */ + if((size_t)reqLen + 2 < sizeof(reqBuf)) { + reqBuf[reqLen++] = '\r'; + reqBuf[reqLen++] = '\n'; + } + + err = networktlsWrite(tls, (const uint8_t *)reqBuf, (size_t)reqLen); + if(err.code != ERROR_OK) { + if(client->onError) client->onError(client, err, client->user); + return; + } + + if(client->requestBody != NULL && client->requestBodyLen > 0) { + err = networktlsWrite(tls, client->requestBody, client->requestBodyLen); + if(err.code != ERROR_OK) { + if(client->onError) client->onError(client, err, client->user); + return; + } + } + + /* ---- read response status line ------------------------------------- */ + err = httpclientReadLine( + tls, &client->errorState, lineBuf, sizeof(lineBuf), &lineLen + ); + if(err.code != ERROR_OK) { + if(client->onError) client->onError(client, err, client->user); + return; + } + + client->statusCode = httpclientParseStatus(lineBuf); + + /* ---- read response headers until blank line ------------------------ */ + client->responseHeaderCount = 0; + for(;;) { + err = httpclientReadLine( + tls, &client->errorState, lineBuf, sizeof(lineBuf), &lineLen + ); + if(err.code != ERROR_OK) { + if(client->onError) client->onError(client, err, client->user); + return; + } + if(lineLen == 0) break; /* blank line = end of headers */ + httpclientParseHeader(client, lineBuf); + } + + if(client->onHeaders) { + client->onHeaders( + client, + client->statusCode, + client->responseHeaders, + client->responseHeaderCount, + client->user + ); + } + + /* ---- read response body -------------------------------------------- */ + + /* 1xx, 204 No Content and 304 Not Modified carry no body */ + bool_t noBody = ( + client->statusCode < 200 || + client->statusCode == 204 || + client->statusCode == 304 + ); + + if(!noBody) { + const char_t *transferEncoding = + httpclientGetResponseHeader(client, "Transfer-Encoding"); + const char_t *contentLengthStr = + httpclientGetResponseHeader(client, "Content-Length"); + + if(transferEncoding != NULL && + strstr(transferEncoding, "chunked") != NULL) { + err = httpclientReadBodyChunked(client); + } else if(contentLengthStr != NULL) { + size_t contentLength = (size_t)atoi(contentLengthStr); + err = httpclientReadBodyFixed(client, contentLength); + } else { + err = httpclientReadBodyUntilClose(client); + } + + if(err.code != ERROR_OK) { + if(client->onError) client->onError(client, err, client->user); + return; + } + } + + if(client->onComplete) client->onComplete(client, client->user); +} + +static void httpclientTlsOnError( + networktls_t *tls, + errorret_t err, + void *user +) { + httpclient_t *client = (httpclient_t *)user; + if(client->onError) client->onError(client, err, client->user); +} + +static void httpclientTlsOnDisconnect(networktls_t *tls, void *user) { + /* nothing — httpclientTlsOnConnect drives the full lifecycle */ +} + +/* ---- public API -------------------------------------------------------- */ + +errorret_t httpclientInit(httpclient_t *client) { + memoryZero(client, sizeof(httpclient_t)); + errorChain(networktlsInit(&client->tls)); + errorOk(); +} + +errorret_t httpclientParseUrl( + httpurl_t *url, + errorstate_t *errorState, + const char_t *urlStr +) { + const char_t *schemeEnd; + const char_t *hostStart; + const char_t *portStart; + const char_t *pathStart; + size_t hostLen; + + memoryZero(url, sizeof(httpurl_t)); + + schemeEnd = strstr(urlStr, "://"); + if(!schemeEnd) { + errorThrowState(errorState, "URL missing scheme: %s", urlStr); + } + + size_t schemeLen = (size_t)(schemeEnd - urlStr); + if(schemeLen >= HTTPCLIENT_SCHEME_MAX) { + errorThrowState(errorState, "URL scheme too long"); + } + memoryCopy(url->scheme, urlStr, schemeLen); + url->scheme[schemeLen] = '\0'; + + /* default port by scheme */ + if(httpclientStrEqualCI(url->scheme, "https")) { + url->port = 443; + } else if(httpclientStrEqualCI(url->scheme, "http")) { + url->port = 80; + } else { + errorThrowState(errorState, "Unsupported URL scheme: %s", url->scheme); + } + + hostStart = schemeEnd + 3; + pathStart = strchr(hostStart, '/'); + portStart = strchr(hostStart, ':'); + + /* port present and appears before the path */ + if(portStart != NULL && (pathStart == NULL || portStart < pathStart)) { + hostLen = (size_t)(portStart - hostStart); + url->port = (uint16_t)atoi(portStart + 1); + } else { + hostLen = pathStart != NULL + ? (size_t)(pathStart - hostStart) + : strlen(hostStart); + } + + if(hostLen == 0 || hostLen >= HTTPCLIENT_HOST_MAX) { + errorThrowState(errorState, "URL host missing or too long"); + } + memoryCopy(url->host, hostStart, hostLen); + url->host[hostLen] = '\0'; + + if(pathStart != NULL) { + size_t pathLen = strlen(pathStart); + if(pathLen >= HTTPCLIENT_PATH_MAX) pathLen = HTTPCLIENT_PATH_MAX - 1; + memoryCopy(url->path, pathStart, pathLen); + url->path[pathLen] = '\0'; + } else { + url->path[0] = '/'; + url->path[1] = '\0'; + } + + errorOk(); +} + +void httpclientSetHeader( + httpclient_t *client, + const char_t *name, + const char_t *value +) { + size_t nameLen; + size_t valLen; + size_t i; + httpheader_t *h; + + assertNotNull(name, "header name must not be null"); + assertNotNull(value, "header value must not be null"); + + /* replace existing header with same name */ + for(i = 0; i < client->requestHeaderCount; i++) { + if(httpclientStrEqualCI(client->requestHeaders[i].name, name)) { + h = &client->requestHeaders[i]; + valLen = strlen(value); + if(valLen >= HTTPCLIENT_HEADER_VALUE_MAX) valLen = HTTPCLIENT_HEADER_VALUE_MAX - 1; + memoryCopy(h->value, value, valLen); + h->value[valLen] = '\0'; + return; + } + } + + assertTrue( + client->requestHeaderCount < HTTPCLIENT_HEADER_MAX, + "httpclientSetHeader: request header limit exceeded" + ); + + h = &client->requestHeaders[client->requestHeaderCount++]; + + nameLen = strlen(name); + if(nameLen >= HTTPCLIENT_HEADER_NAME_MAX) nameLen = HTTPCLIENT_HEADER_NAME_MAX - 1; + memoryCopy(h->name, name, nameLen); + h->name[nameLen] = '\0'; + + valLen = strlen(value); + if(valLen >= HTTPCLIENT_HEADER_VALUE_MAX) valLen = HTTPCLIENT_HEADER_VALUE_MAX - 1; + memoryCopy(h->value, value, valLen); + h->value[valLen] = '\0'; +} + +void httpclientRequest( + httpclient_t *client, + const char_t *method, + const char_t *url, + bool_t verifyPeer, + httpclientheadercallback_t onHeaders, + httpclientdatacallback_t onData, + httpclientcompletecallback_t onComplete, + httpclienterrorcallback_t onError, + void *user +) { + size_t methodLen; + errorret_t err; + + assertNotNull(method, "method must not be null"); + assertNotNull(url, "url must not be null"); + + methodLen = strlen(method); + if(methodLen >= HTTPCLIENT_METHOD_MAX) methodLen = HTTPCLIENT_METHOD_MAX - 1; + memoryCopy(client->method, method, methodLen); + client->method[methodLen] = '\0'; + + err = httpclientParseUrl(&client->url, &client->errorState, url); + if(err.code != ERROR_OK) { + if(onError) onError(client, err, user); + return; + } + + client->onHeaders = onHeaders; + client->onData = onData; + client->onComplete = onComplete; + client->onError = onError; + client->user = user; + client->statusCode = 0; + client->responseHeaderCount = 0; + + networktlsConnect( + &client->tls, + client->url.host, + client->url.port, + verifyPeer, + httpclientTlsOnConnect, + httpclientTlsOnError, + httpclientTlsOnDisconnect, + client + ); +} + +void httpclientDisconnect(httpclient_t *client) { + networktlsDisconnect(&client->tls); +} + +errorret_t httpclientDispose(httpclient_t *client) { + return networktlsDispose(&client->tls); +} diff --git a/src/dusk/network/httpclient.h b/src/dusk/network/httpclient.h new file mode 100644 index 00000000..4d9f1811 --- /dev/null +++ b/src/dusk/network/httpclient.h @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/networktls.h" +#include "util/endian.h" + +#define HTTPCLIENT_METHOD_MAX 8 +#define HTTPCLIENT_URL_MAX 1024 +#define HTTPCLIENT_HOST_MAX 256 +#define HTTPCLIENT_PATH_MAX 768 +#define HTTPCLIENT_SCHEME_MAX 8 +#define HTTPCLIENT_HEADER_NAME_MAX 64 +#define HTTPCLIENT_HEADER_VALUE_MAX 512 +#define HTTPCLIENT_HEADER_MAX 32 +#define HTTPCLIENT_HEADER_BUF_SIZE 8192 +#define HTTPCLIENT_BODY_BUF_SIZE 4096 +#define HTTPCLIENT_REQ_BUF_SIZE 4096 + +/** Parsed components of a URL. */ +typedef struct { + char_t scheme[HTTPCLIENT_SCHEME_MAX]; + char_t host[HTTPCLIENT_HOST_MAX]; + uint16_t port; + char_t path[HTTPCLIENT_PATH_MAX]; +} httpurl_t; + +/** A single HTTP header (name + value pair). */ +typedef struct { + char_t name[HTTPCLIENT_HEADER_NAME_MAX]; + char_t value[HTTPCLIENT_HEADER_VALUE_MAX]; +} httpheader_t; + +typedef struct httpclient_s httpclient_t; + +/** + * Called once the response status line and all headers have been parsed. + * Fired from the socket thread before any body data arrives. + */ +typedef void (*httpclientheadercallback_t)( + httpclient_t *client, + uint16_t statusCode, + const httpheader_t *headers, + size_t headerCount, + void *user +); + +/** + * Called for each chunk of response body data. + * May be called multiple times per request. Fired from the socket thread. + * For binary payloads use endianReadBE32 / endianReadLE32 from util/endian.h + * to read multi-byte values with correct byte order. + */ +typedef void (*httpclientdatacallback_t)( + httpclient_t *client, + const uint8_t *data, + size_t len, + void *user +); + +/** Called once when the response body has been fully received. */ +typedef void (*httpclientcompletecallback_t)( + httpclient_t *client, + void *user +); + +/** Called on any transport or protocol error. */ +typedef void (*httpclienterrorcallback_t)( + httpclient_t *client, + errorret_t err, + void *user +); + +typedef struct httpclient_s { + networktls_t tls; + errorstate_t errorState; + + /* --- request config (set before httpclientRequest) --- */ + char_t method[HTTPCLIENT_METHOD_MAX]; + httpurl_t url; + httpheader_t requestHeaders[HTTPCLIENT_HEADER_MAX]; + size_t requestHeaderCount; + + /** + * Optional request body. The pointer must remain valid until onComplete or + * onError fires. The HTTP layer does not copy the body. + */ + const uint8_t *requestBody; + size_t requestBodyLen; + + /* --- response state (populated during request) --- */ + uint16_t statusCode; + httpheader_t responseHeaders[HTTPCLIENT_HEADER_MAX]; + size_t responseHeaderCount; + + /* --- callbacks (all fired from the socket thread) --- */ + httpclientheadercallback_t onHeaders; + httpclientdatacallback_t onData; + httpclientcompletecallback_t onComplete; + httpclienterrorcallback_t onError; + void *user; +} httpclient_t; + +/** + * Initializes an HTTP client. Must be called before any other httpclient + * function. + */ +errorret_t httpclientInit(httpclient_t *client); + +/** + * Parses a URL string into its components. Supports http:// and https://. + * Port defaults to 80 (HTTP) or 443 (HTTPS) when not specified. + * + * @return ERROR_NOT_OK if the URL is malformed. + */ +errorret_t httpclientParseUrl( + httpurl_t *url, + errorstate_t *errorState, + const char_t *urlStr +); + +/** + * Adds or replaces a request header. Must be called before httpclientRequest. + * Asserts if HTTPCLIENT_HEADER_MAX is exceeded. + */ +void httpclientSetHeader( + httpclient_t *client, + const char_t *name, + const char_t *value +); + +/** + * Starts an asynchronous HTTPS (or plain HTTP) request. All callbacks fire + * from the socket thread, not the main thread. + * + * @param method HTTP verb ("GET", "POST", etc.). + * @param url Full URL string — parsed internally. + * @param verifyPeer Pass true to verify the server TLS certificate. Pass + * false for self-signed / development servers. + * @param onHeaders Called when response headers arrive (may be NULL). + * @param onData Called for each body chunk (may be NULL). + * @param onComplete Called when the full response is received (may be NULL). + * @param onError Called on any error (may be NULL). + * @param user Passed to all callbacks. + */ +void httpclientRequest( + httpclient_t *client, + const char_t *method, + const char_t *url, + bool_t verifyPeer, + httpclientheadercallback_t onHeaders, + httpclientdatacallback_t onData, + httpclientcompletecallback_t onComplete, + httpclienterrorcallback_t onError, + void *user +); + +/** + * Requests cancellation of an in-flight request. Non-blocking. + */ +void httpclientDisconnect(httpclient_t *client); + +/** + * Blocks until the request thread exits, then frees all resources. + */ +errorret_t httpclientDispose(httpclient_t *client); diff --git a/src/dusk/network/networksocket.c b/src/dusk/network/networksocket.c new file mode 100644 index 00000000..1e644d6f --- /dev/null +++ b/src/dusk/network/networksocket.c @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "networksocket.h" +#include "util/memory.h" +#include "assert/assert.h" + +static void networksocketThreadCallback(thread_t *thread) { + networksocket_t *socket = (networksocket_t *)thread->data; + errorret_t err; + bool_t hadError; + + hadError = false; + + err = networksocketPlatformConnect(socket); + if (err.code != ERROR_OK) { + socket->state = NETWORKSOCKET_STATE_DISCONNECTED; + if (socket->onError) socket->onError(socket, err, socket->user); + return; + } + + socket->state = NETWORKSOCKET_STATE_CONNECTED; + if (socket->onConnect) socket->onConnect(socket, socket->user); + + if (socket->onReceive != NULL) { + uint8_t recvBuf[NETWORKSOCKET_RECV_BUFFER_SIZE]; + uint8_t localSendBuf[NETWORKSOCKET_SEND_BUFFER_SIZE]; + size_t sendLen; + size_t recvLen; + + while (!threadShouldStop(thread)) { + threadMutexLock(&socket->sendMutex); + sendLen = socket->sendLen; + if (sendLen > 0) { + memoryCopy(localSendBuf, socket->sendBuffer, sendLen); + socket->sendLen = 0; + } + threadMutexUnlock(&socket->sendMutex); + + if (sendLen > 0) { + err = networksocketPlatformSend(socket, localSendBuf, sendLen); + if (err.code != ERROR_OK) { + hadError = true; + break; + } + } + + recvLen = 0; + err = networksocketPlatformRecv( + socket, recvBuf, NETWORKSOCKET_RECV_BUFFER_SIZE, &recvLen + ); + if (err.code != ERROR_OK) { + hadError = true; + break; + } + + if (recvLen == NETWORKSOCKET_RECV_CLOSED) break; + + if (recvLen > 0) { + socket->onReceive(socket, recvBuf, recvLen, socket->user); + } + } + } + + socket->state = NETWORKSOCKET_STATE_DISCONNECTING; + networksocketPlatformDisconnect(socket); + socket->state = NETWORKSOCKET_STATE_DISCONNECTED; + + if (hadError) { + if (socket->onError) socket->onError(socket, err, socket->user); + } else { + if (socket->onDisconnect) socket->onDisconnect(socket, socket->user); + } +} + +errorret_t networksocketInit(networksocket_t *socket) { + memoryZero(socket, sizeof(networksocket_t)); + socket->state = NETWORKSOCKET_STATE_DISCONNECTED; + threadInit(&socket->thread, networksocketThreadCallback); + socket->thread.data = socket; + threadMutexInit(&socket->sendMutex); + errorChain(networksocketPlatformInit(socket)); + errorOk(); +} + +void networksocketConnect( + networksocket_t *socket, + const char_t *host, + uint16_t port, + networksocketcallback_t onConnect, + networksocketerrorcallback_t onError, + networksocketcallback_t onDisconnect, + networksocketrecvcallback_t onReceive, + void *user +) { + size_t hostLen; + + assertNotNull(host, "host must not be null"); + hostLen = strlen(host); + assertTrue(hostLen < NETWORKSOCKET_HOST_MAX, "host exceeds NETWORKSOCKET_HOST_MAX"); + + memoryCopy(socket->host, host, hostLen + 1); + socket->port = port; + socket->onConnect = onConnect; + socket->onError = onError; + socket->onDisconnect = onDisconnect; + socket->onReceive = onReceive; + socket->user = user; + socket->sendLen = 0; + socket->state = NETWORKSOCKET_STATE_CONNECTING; + + threadStart(&socket->thread); +} + +errorret_t networksocketSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +) { + assertNotNull(data, "data must not be null"); + assertTrue(len > 0, "len must be greater than 0"); + + threadMutexLock(&socket->sendMutex); + assertTrue( + socket->sendLen + len <= NETWORKSOCKET_SEND_BUFFER_SIZE, + "networksocketSend would overflow send buffer" + ); + memoryCopy(socket->sendBuffer + socket->sendLen, data, len); + socket->sendLen += len; + threadMutexUnlock(&socket->sendMutex); + + errorOk(); +} + +errorret_t networksocketWrite( + networksocket_t *socket, + const uint8_t *data, + size_t len +) { + return networksocketPlatformSend(socket, data, len); +} + +errorret_t networksocketRead( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +) { + return networksocketPlatformRecv(socket, buf, maxLen, outLen); +} + +void networksocketDisconnect(networksocket_t *socket) { + threadStopRequest(&socket->thread); +} + +errorret_t networksocketDispose(networksocket_t *socket) { + threadStop(&socket->thread); + errorChain(networksocketPlatformDispose(socket)); + threadMutexDispose(&socket->sendMutex); + errorOk(); +} diff --git a/src/dusk/network/networksocket.h b/src/dusk/network/networksocket.h new file mode 100644 index 00000000..9040c9b2 --- /dev/null +++ b/src/dusk/network/networksocket.h @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "thread/thread.h" +#include "network/networksocketplatform.h" +#ifndef networksocketPlatformInit + #error "networksocketPlatformInit must be defined" +#endif +#ifndef networksocketPlatformConnect + #error "networksocketPlatformConnect must be defined" +#endif +#ifndef networksocketPlatformSend + #error "networksocketPlatformSend must be defined" +#endif +#ifndef networksocketPlatformRecv + #error "networksocketPlatformRecv must be defined" +#endif +#ifndef networksocketPlatformDisconnect + #error "networksocketPlatformDisconnect must be defined" +#endif +#ifndef networksocketPlatformDispose + #error "networksocketPlatformDispose must be defined" +#endif + +#define NETWORKSOCKET_HOST_MAX 256 +#define NETWORKSOCKET_SEND_BUFFER_SIZE 4096 +#define NETWORKSOCKET_RECV_BUFFER_SIZE 4096 + +/** Sentinel returned by networksocketPlatformRecv when the peer closed the connection. */ +#define NETWORKSOCKET_RECV_CLOSED ((size_t)(-1)) + +typedef enum { + NETWORKSOCKET_STATE_DISCONNECTED, + NETWORKSOCKET_STATE_CONNECTING, + NETWORKSOCKET_STATE_CONNECTED, + NETWORKSOCKET_STATE_DISCONNECTING, +} networksocketstate_t; + +typedef struct networksocket_s networksocket_t; + +typedef void (*networksocketcallback_t)( + networksocket_t *socket, void *user +); +typedef void (*networksocketerrorcallback_t)( + networksocket_t *socket, errorret_t err, void *user +); +typedef void (*networksocketrecvcallback_t)( + networksocket_t *socket, const uint8_t *data, size_t len, void *user +); + +typedef struct networksocket_s { + networksocketstate_t state; + errorstate_t errorState; + + thread_t thread; + threadmutex_t sendMutex; + + char_t host[NETWORKSOCKET_HOST_MAX]; + uint16_t port; + + networksocketplatform_t platform; + + uint8_t sendBuffer[NETWORKSOCKET_SEND_BUFFER_SIZE]; + size_t sendLen; + + /** + * Called from the socket thread once TCP is established. If onReceive is + * NULL, the socket thread will exit after this returns and disconnect. + * If onReceive is set, a recv loop runs after this returns. + * Suitable entry point for TLS/HTTP layers that drive their own I/O. + */ + networksocketcallback_t onConnect; + + /** + * When non-NULL, the socket thread runs a receive loop after connection + * and calls this for each incoming data chunk. When NULL, the thread only + * calls onConnect and then disconnects. + */ + networksocketrecvcallback_t onReceive; + + networksocketerrorcallback_t onError; + networksocketcallback_t onDisconnect; + void *user; +} networksocket_t; + +/** + * Initializes a socket structure. Must be called before any other socket + * function. + */ +errorret_t networksocketInit(networksocket_t *socket); + +/** + * Starts an asynchronous connection in a dedicated thread. All callbacks are + * invoked from the socket thread, not the main thread. + * + * If onReceive is non-NULL, the thread runs a receive loop after connection + * and delivers incoming data via onReceive. If onReceive is NULL, the thread + * calls onConnect and exits (useful for TLS or request-driven I/O that + * manages its own reads via networksocketRead/networksocketWrite). + * + * @param socket Socket to connect. + * @param host Hostname or IP address to connect to. + * @param port Port number. + * @param onConnect Called once TCP is established (may be NULL). + * @param onError Called on connect or I/O error (may be NULL). + * @param onDisconnect Called on graceful disconnect (may be NULL). + * @param onReceive Called for each received chunk; NULL disables recv loop. + * @param user User data passed to all callbacks. + */ +void networksocketConnect( + networksocket_t *socket, + const char_t *host, + uint16_t port, + networksocketcallback_t onConnect, + networksocketerrorcallback_t onError, + networksocketcallback_t onDisconnect, + networksocketrecvcallback_t onReceive, + void *user +); + +/** + * Queues data for sending. Thread-safe; may be called from any thread. + * The socket thread drains the queue on each recv-loop iteration. + * Asserts if the send buffer would overflow. + */ +errorret_t networksocketSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +); + +/** + * Writes data directly to the socket without buffering. Must be called from + * the socket's own thread (e.g., from within onConnect or a TLS bio callback). + */ +errorret_t networksocketWrite( + networksocket_t *socket, + const uint8_t *data, + size_t len +); + +/** + * Reads data directly from the socket. Must be called from the socket's own + * thread. Returns 0 in *outLen on timeout (no data yet); returns + * NETWORKSOCKET_RECV_CLOSED in *outLen when the peer has closed the connection. + */ +errorret_t networksocketRead( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +); + +/** + * Requests the socket thread to stop. Non-blocking; onDisconnect fires once + * the thread has finished. + */ +void networksocketDisconnect(networksocket_t *socket); + +/** + * Blocks until the socket thread has stopped, then releases platform + * resources. Safe to call on an already-disconnected socket. + */ +errorret_t networksocketDispose(networksocket_t *socket); diff --git a/src/dusk/network/networktls.c b/src/dusk/network/networktls.c new file mode 100644 index 00000000..58a7040c --- /dev/null +++ b/src/dusk/network/networktls.c @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "networktls.h" +#include "util/memory.h" +#include "assert/assert.h" + +// Define missing mbedtls net error codes if not present +#ifndef MBEDTLS_ERR_NET_SEND_FAILED +#define MBEDTLS_ERR_NET_SEND_FAILED -0x004E +#endif +#ifndef MBEDTLS_ERR_NET_RECV_FAILED +#define MBEDTLS_ERR_NET_RECV_FAILED -0x004C +#endif + +/* ---- mbedTLS bio callbacks -------------------------------------------- */ + +#ifdef DUSK_DOLPHIN +#include +#include +mbedtls_ms_time_t mbedtls_ms_time(void) { + return (mbedtls_ms_time_t)ticks_to_millisecs(gettime()); +} +#endif + +int mbedtls_platform_get_entropy(uint8_t *output, size_t len) { + for (size_t i = 0; i < len; i++) { + output[i] = rand() & 0xFF; + } + + return 0; +} + +static int networktlsBioSend( + void *ctx, + const unsigned char *buf, + size_t len +) { + networktls_t *tls = (networktls_t *)ctx; + errorret_t err = networksocketWrite(&tls->socket, (const uint8_t *)buf, len); + if(err.code != ERROR_OK) return MBEDTLS_ERR_NET_SEND_FAILED; + return (int)len; +} + +static int networktlsBioRecv(void *ctx, unsigned char *buf, size_t len) { + networktls_t *tls = (networktls_t *)ctx; + size_t outLen = 0; + errorret_t err = networksocketRead( + &tls->socket, (uint8_t *)buf, len, &outLen + ); + if(err.code != ERROR_OK) return MBEDTLS_ERR_NET_RECV_FAILED; + if(outLen == NETWORKSOCKET_RECV_CLOSED) return 0; + if(outLen == 0) return MBEDTLS_ERR_SSL_WANT_READ; + return (int)outLen; +} + +/* ---- socket callbacks (run in socket thread) --------------------------- */ + +static void networktlsSocketOnConnect(networksocket_t *socket, void *user) { + networktls_t *tls = (networktls_t *)user; + errorret_t errret; + int ret; + + if(psa_crypto_init() != PSA_SUCCESS) { + errret = errorThrowImpl( + &tls->errorState, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "psa_crypto_init failed" + ); + if(tls->onError) tls->onError(tls, errret, tls->user); + return; + } + + ret = mbedtls_ssl_config_defaults( + &tls->conf, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ); + if(ret != 0) { + errret = errorThrowImpl( + &tls->errorState, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "mbedtls_ssl_config_defaults failed: -0x%04x", -ret + ); + if(tls->onError) tls->onError(tls, errret, tls->user); + return; + } + + mbedtls_ssl_conf_authmode( + &tls->conf, + tls->verifyPeer ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE + ); + mbedtls_ssl_conf_ca_chain(&tls->conf, &tls->cacert, NULL); + + ret = mbedtls_ssl_setup(&tls->ssl, &tls->conf); + if(ret != 0) { + errret = errorThrowImpl( + &tls->errorState, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "mbedtls_ssl_setup failed: -0x%04x", -ret + ); + if(tls->onError) tls->onError(tls, errret, tls->user); + return; + } + + ret = mbedtls_ssl_set_hostname(&tls->ssl, tls->socket.host); + if(ret != 0) { + errret = errorThrowImpl( + &tls->errorState, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "mbedtls_ssl_set_hostname failed: -0x%04x", -ret + ); + if(tls->onError) tls->onError(tls, errret, tls->user); + return; + } + + mbedtls_ssl_set_bio( + &tls->ssl, tls, + networktlsBioSend, networktlsBioRecv, NULL + ); + + /* TLS handshake loop — WANT_READ/WANT_WRITE are non-fatal retry signals */ + do { + ret = mbedtls_ssl_handshake(&tls->ssl); + } while( + ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE + ); + + if(ret != 0) { + errret = errorThrowImpl( + &tls->errorState, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "TLS handshake failed: -0x%04x", -ret + ); + if(tls->onError) tls->onError(tls, errret, tls->user); + return; + } + + /* Handshake succeeded — let the user drive I/O from this thread */ + if(tls->onConnect) tls->onConnect(tls, tls->user); + + mbedtls_ssl_close_notify(&tls->ssl); +} + +static void networktlsSocketOnError( + networksocket_t *socket, + errorret_t err, + void *user +) { + networktls_t *tls = (networktls_t *)user; + if(tls->onError) tls->onError(tls, err, tls->user); +} + +static void networktlsSocketOnDisconnect( + networksocket_t *socket, + void *user +) { + networktls_t *tls = (networktls_t *)user; + if(tls->onDisconnect) tls->onDisconnect(tls, tls->user); +} + +/* ---- public API -------------------------------------------------------- */ + +errorret_t networktlsInit(networktls_t *tls) { + memoryZero(tls, sizeof(networktls_t)); + errorChain(networksocketInit(&tls->socket)); + mbedtls_ssl_init(&tls->ssl); + mbedtls_ssl_config_init(&tls->conf); + mbedtls_x509_crt_init(&tls->cacert); + errorOk(); +} + +errorret_t networktlsAddCACert( + networktls_t *tls, + const uint8_t *certPem, + size_t len +) { + int ret = mbedtls_x509_crt_parse(&tls->cacert, certPem, len); + if(ret != 0) { + errorThrowState( + &tls->errorState, + "Failed to parse CA certificate: -0x%04x", -ret + ); + } + errorOk(); +} + +void networktlsConnect( + networktls_t *tls, + const char_t *host, + uint16_t port, + bool_t verifyPeer, + networktlscallback_t onConnect, + networktlserrorcallback_t onError, + networktlscallback_t onDisconnect, + void *user +) { + tls->verifyPeer = verifyPeer; + tls->onConnect = onConnect; + tls->onError = onError; + tls->onDisconnect = onDisconnect; + tls->user = user; + + networksocketConnect( + &tls->socket, + host, port, + networktlsSocketOnConnect, + networktlsSocketOnError, + networktlsSocketOnDisconnect, + NULL, /* no recv loop — TLS drives all I/O from onConnect */ + tls + ); +} + +errorret_t networktlsWrite( + networktls_t *tls, + const uint8_t *data, + size_t len +) { + size_t sent = 0; + int ret; + while(sent < len) { + ret = mbedtls_ssl_write( + &tls->ssl, + (const unsigned char *)(data + sent), + len - sent + ); + if(ret == MBEDTLS_ERR_SSL_WANT_WRITE || ret == MBEDTLS_ERR_SSL_WANT_READ) { + continue; + } + if(ret < 0) { + errorThrowState( + &tls->errorState, + "TLS write failed: -0x%04x", -ret + ); + } + sent += (size_t)ret; + } + errorOk(); +} + +errorret_t networktlsRead( + networktls_t *tls, + uint8_t *buf, + size_t maxLen, + size_t *outLen +) { + int ret; + do { + ret = mbedtls_ssl_read(&tls->ssl, (unsigned char *)buf, maxLen); + } while( + ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE + ); + + if(ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { + *outLen = NETWORKSOCKET_RECV_CLOSED; + errorOk(); + } + + if(ret < 0) { + errorThrowState( + &tls->errorState, + "TLS read failed: -0x%04x", -ret + ); + } + + *outLen = (size_t)ret; + errorOk(); +} + +void networktlsDisconnect(networktls_t *tls) { + networksocketDisconnect(&tls->socket); +} + +errorret_t networktlsDispose(networktls_t *tls) { + errorChain(networksocketDispose(&tls->socket)); + mbedtls_ssl_free(&tls->ssl); + mbedtls_ssl_config_free(&tls->conf); + mbedtls_x509_crt_free(&tls->cacert); + errorOk(); +} \ No newline at end of file diff --git a/src/dusk/network/networktls.h b/src/dusk/network/networktls.h new file mode 100644 index 00000000..d5f65644 --- /dev/null +++ b/src/dusk/network/networktls.h @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/networksocket.h" + +#include +#include +#include + +typedef struct networktls_s networktls_t; + +typedef void (*networktlscallback_t)(networktls_t *tls, void *user); +typedef void (*networktlserrorcallback_t)( + networktls_t *tls, errorret_t err, void *user +); + +typedef struct networktls_s { + networksocket_t socket; + errorstate_t errorState; + bool_t verifyPeer; + + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_x509_crt cacert; + + networktlscallback_t onConnect; + networktlserrorcallback_t onError; + networktlscallback_t onDisconnect; + void *user; +} networktls_t; + +/** + * Initializes the TLS context and the underlying socket. + */ +errorret_t networktlsInit(networktls_t *tls); + +/** + * Adds a PEM-encoded CA certificate used to verify the server. Must be called + * before networktlsConnect when verifyPeer is true. + */ +errorret_t networktlsAddCACert( + networktls_t *tls, + const uint8_t *certPem, + size_t len +); + +/** + * Starts an async TCP+TLS connection in the socket's own thread. All callbacks + * fire from that thread, not the main thread. + * + * When onConnect fires, the TLS handshake is complete and the connection is + * ready for networktlsWrite / networktlsRead. The connection is kept alive + * until onConnect returns, so all I/O should be performed synchronously within + * that callback. + * + * @param verifyPeer When true, the server certificate is verified against the + * CA certificates added via networktlsAddCACert. When false, + * certificate verification is skipped (useful for development + * or self-signed certificates). + */ +void networktlsConnect( + networktls_t *tls, + const char_t *host, + uint16_t port, + bool_t verifyPeer, + networktlscallback_t onConnect, + networktlserrorcallback_t onError, + networktlscallback_t onDisconnect, + void *user +); + +/** + * Writes data through the TLS layer. Must be called from within the + * onConnect callback (i.e., from the socket thread). + */ +errorret_t networktlsWrite( + networktls_t *tls, + const uint8_t *data, + size_t len +); + +/** + * Reads up to maxLen bytes from the TLS layer. Blocks until data arrives or + * the connection closes. Sets *outLen to bytes received, or + * NETWORKSOCKET_RECV_CLOSED when the peer has closed the connection. + * Must be called from within the onConnect callback. + */ +errorret_t networktlsRead( + networktls_t *tls, + uint8_t *buf, + size_t maxLen, + size_t *outLen +); + +/** + * Requests disconnect. Non-blocking; onDisconnect fires once complete. + */ +void networktlsDisconnect(networktls_t *tls); + +/** + * Blocks until the connection thread stops, then releases all resources. + */ +errorret_t networktlsDispose(networktls_t *tls); diff --git a/src/dusk/util/endian.c b/src/dusk/util/endian.c index 4f225b01..65861df1 100644 --- a/src/dusk/util/endian.c +++ b/src/dusk/util/endian.c @@ -94,7 +94,83 @@ float_t endianLittleToHostFloat(float_t value) { memoryCopy(&temp, &value, sizeof(uint32_t)); temp = endianLittleToHost32(temp); memoryCopy(&result, &temp, sizeof(uint32_t)); - return result; } +uint16_t endianBigToHost16(uint16_t value) { + #if defined(DUSK_PLATFORM_ENDIAN_BIG) + return value; + #elif defined(DUSK_PLATFORM_ENDIAN_LITTLE) + // Perform conversion below + #else + if(!isHostLittleEndian()) return value; + #endif + return (uint16_t)(((value & 0x00FFu) << 8) | ((value & 0xFF00u) >> 8)); +} + +uint32_t endianBigToHost32(uint32_t value) { + #if defined(DUSK_PLATFORM_ENDIAN_BIG) + return value; + #elif defined(DUSK_PLATFORM_ENDIAN_LITTLE) + // Perform conversion below + #else + if(!isHostLittleEndian()) return value; + #endif + return ( + ((value & 0x000000FFu) << 24) | + ((value & 0x0000FF00u) << 8) | + ((value & 0x00FF0000u) >> 8) | + ((value & 0xFF000000u) >> 24) + ); +} + +uint64_t endianBigToHost64(uint64_t value) { + #if defined(DUSK_PLATFORM_ENDIAN_BIG) + return value; + #elif defined(DUSK_PLATFORM_ENDIAN_LITTLE) + // Perform conversion below + #else + if(!isHostLittleEndian()) return value; + #endif + return ( + ((value & 0x00000000000000FFull) << 56) | + ((value & 0x000000000000FF00ull) << 40) | + ((value & 0x0000000000FF0000ull) << 24) | + ((value & 0x00000000FF000000ull) << 8) | + ((value & 0x000000FF00000000ull) >> 8) | + ((value & 0x0000FF0000000000ull) >> 24) | + ((value & 0x00FF000000000000ull) >> 40) | + ((value & 0xFF00000000000000ull) >> 56) + ); +} + +uint16_t endianReadBE16(const uint8_t *buf, size_t offset) { + return endianBigToHost16( + (uint16_t)((uint16_t)buf[offset] << 8 | (uint16_t)buf[offset + 1]) + ); +} + +uint32_t endianReadBE32(const uint8_t *buf, size_t offset) { + return endianBigToHost32( + ((uint32_t)buf[offset ] << 24) | + ((uint32_t)buf[offset + 1] << 16) | + ((uint32_t)buf[offset + 2] << 8) | + ((uint32_t)buf[offset + 3] ) + ); +} + +uint16_t endianReadLE16(const uint8_t *buf, size_t offset) { + return endianLittleToHost16( + (uint16_t)((uint16_t)buf[offset + 1] << 8 | (uint16_t)buf[offset]) + ); +} + +uint32_t endianReadLE32(const uint8_t *buf, size_t offset) { + return endianLittleToHost32( + ((uint32_t)buf[offset + 3] << 24) | + ((uint32_t)buf[offset + 2] << 16) | + ((uint32_t)buf[offset + 1] << 8) | + ((uint32_t)buf[offset ] ) + ); +} + diff --git a/src/dusk/util/endian.h b/src/dusk/util/endian.h index a5a397f1..4d9fdde2 100644 --- a/src/dusk/util/endian.h +++ b/src/dusk/util/endian.h @@ -46,8 +46,62 @@ uint64_t endianLittleToHost64(uint64_t value); /** * Converts a float from little-endian to host byte order. - * + * * @param value The little-endian value to convert. * @return The value in host byte order. */ -float_t endianLittleToHostFloat(float_t value); \ No newline at end of file +float_t endianLittleToHostFloat(float_t value); + +/** + * Converts a 16-bit integer from big-endian (network byte order) to host + * byte order. Use this for binary data received over network protocols. + * + * @param value The big-endian value to convert. + * @return The value in host byte order. + */ +uint16_t endianBigToHost16(uint16_t value); + +/** + * Converts a 32-bit integer from big-endian (network byte order) to host + * byte order. + * + * @param value The big-endian value to convert. + * @return The value in host byte order. + */ +uint32_t endianBigToHost32(uint32_t value); + +/** + * Converts a 64-bit integer from big-endian (network byte order) to host + * byte order. + * + * @param value The big-endian value to convert. + * @return The value in host byte order. + */ +uint64_t endianBigToHost64(uint64_t value); + +/** + * Reads a big-endian uint16 from a byte buffer at the given offset. Safe on + * all alignment and endianness combinations. Suitable for binary HTTP response + * body parsing where the format specifies network (big-endian) byte order. + * + * @param buf Source byte buffer. + * @param offset Byte offset within buf. + * @return Host-order uint16. + */ +uint16_t endianReadBE16(const uint8_t *buf, size_t offset); + +/** + * Reads a big-endian uint32 from a byte buffer at the given offset. + */ +uint32_t endianReadBE32(const uint8_t *buf, size_t offset); + +/** + * Reads a little-endian uint16 from a byte buffer at the given offset. Use + * for binary payloads that specify little-endian byte order. + */ +uint16_t endianReadLE16(const uint8_t *buf, size_t offset); + +/** + * Reads a little-endian uint32 from a byte buffer at the given offset. + */ +uint32_t endianReadLE32(const uint8_t *buf, size_t offset); \ No newline at end of file diff --git a/src/duskdolphin/network/CMakeLists.txt b/src/duskdolphin/network/CMakeLists.txt index e514c3b9..0ca9b5c5 100644 --- a/src/duskdolphin/network/CMakeLists.txt +++ b/src/duskdolphin/network/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC networkdolphin.c + networksocketdolphin.c ) diff --git a/src/duskdolphin/network/networksocketdolphin.c b/src/duskdolphin/network/networksocketdolphin.c new file mode 100644 index 00000000..c146062c --- /dev/null +++ b/src/duskdolphin/network/networksocketdolphin.c @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "network/networksocket.h" +#include "assert/assert.h" +#include +#include +#include + +/** Poll timeout in microseconds — short enough to check the stop flag often. */ +#define NETWORKSOCKET_DOLPHIN_RECV_TIMEOUT_US 100000 + +errorret_t networksocketDolphinInit(networksocket_t *socket) { + socket->platform.fd = -1; + errorOk(); +} + +errorret_t networksocketDolphinConnect(networksocket_t *nsocket) { + struct hostent *he; + struct sockaddr_in addr; + s32 fd; + s32 ret; + + he = net_gethostbyname(nsocket->host); + if (!he) { + errorThrowState( + &nsocket->errorState, + "net_gethostbyname(%s) failed", + nsocket->host + ); + } + + fd = net_socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + if(fd == INVALID_SOCKET) { + errorThrowState( + &nsocket->errorState, + "net_socket failed with invalid socket" + ); + } else if (fd < 0) { + errorThrowState( + &nsocket->errorState, + "net_socket failed: %d", + (int)fd + ); + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(nsocket->port); + memcpy(&addr.sin_addr, he->h_addr_list[0], (size_t)he->h_length); + + ret = net_connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (ret < 0) { + net_close(fd); + errorThrowState( + &nsocket->errorState, + "net_connect to %s:%u failed: %d", + nsocket->host, + (unsigned int)nsocket->port, + (int)ret + ); + } + + nsocket->platform.fd = fd; + errorOk(); +} + +errorret_t networksocketDolphinSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +) { + size_t total = 0; + s32 sent; + + while (total < len) { + sent = net_send( + socket->platform.fd, + (const void *)(data + total), + len - total, + 0 + ); + if (sent <= 0) { + errorThrowState( + &socket->errorState, + "net_send failed: %d", + (int)sent + ); + } + total += (size_t)sent; + } + errorOk(); +} + +errorret_t networksocketDolphinRecv( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +) { + struct timeval tv; + fd_set readset; + s32 ready; + s32 received; + + tv.tv_sec = 0; + tv.tv_usec = NETWORKSOCKET_DOLPHIN_RECV_TIMEOUT_US; + + FD_ZERO(&readset); + FD_SET(socket->platform.fd, &readset); + + ready = net_select(socket->platform.fd + 1, &readset, NULL, NULL, &tv); + if (ready < 0) { + errorThrowState( + &socket->errorState, + "net_select failed: %d", + (int)ready + ); + } + + if (ready == 0) { + *outLen = 0; + errorOk(); + } + + received = net_recv(socket->platform.fd, buf, maxLen, 0); + if (received < 0) { + errorThrowState( + &socket->errorState, + "net_recv failed: %d", + (int)received + ); + } + + if (received == 0) { + *outLen = NETWORKSOCKET_RECV_CLOSED; + errorOk(); + } + + *outLen = (size_t)received; + errorOk(); +} + +errorret_t networksocketDolphinDisconnect(networksocket_t *socket) { + if (socket->platform.fd >= 0) { + net_close(socket->platform.fd); + socket->platform.fd = -1; + } + errorOk(); +} + +errorret_t networksocketDolphinDispose(networksocket_t *socket) { + if (socket->platform.fd >= 0) { + net_close(socket->platform.fd); + socket->platform.fd = -1; + } + errorOk(); +} diff --git a/src/duskdolphin/network/networksocketdolphin.h b/src/duskdolphin/network/networksocketdolphin.h new file mode 100644 index 00000000..0c872375 --- /dev/null +++ b/src/duskdolphin/network/networksocketdolphin.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "error/error.h" +#include + +typedef struct networksocket_s networksocket_t; + +typedef struct { + s32 fd; +} networksocketdolphin_t; + +/** + * Initializes platform state (sets fd to -1). + */ +errorret_t networksocketDolphinInit(networksocket_t *socket); + +/** + * Resolves host/port via net_gethostbyname and establishes a TCP connection. + */ +errorret_t networksocketDolphinConnect(networksocket_t *socket); + +/** + * Writes all bytes in data to the socket. Loops on partial sends. + */ +errorret_t networksocketDolphinSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +); + +/** + * Polls for incoming data with a short timeout (via net_select) so the + * socket thread can periodically check its stop flag. Sets *outLen to bytes + * received, 0 on timeout, or NETWORKSOCKET_RECV_CLOSED when peer closed. + */ +errorret_t networksocketDolphinRecv( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +); + +/** + * Closes the socket file descriptor. + */ +errorret_t networksocketDolphinDisconnect(networksocket_t *socket); + +/** + * Closes the file descriptor if still open (idempotent). + */ +errorret_t networksocketDolphinDispose(networksocket_t *socket); diff --git a/src/duskdolphin/network/networksocketplatform.h b/src/duskdolphin/network/networksocketplatform.h new file mode 100644 index 00000000..de0640c6 --- /dev/null +++ b/src/duskdolphin/network/networksocketplatform.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "networksocketdolphin.h" + +#define networksocketPlatformInit networksocketDolphinInit +#define networksocketPlatformConnect networksocketDolphinConnect +#define networksocketPlatformSend networksocketDolphinSend +#define networksocketPlatformRecv networksocketDolphinRecv +#define networksocketPlatformDisconnect networksocketDolphinDisconnect +#define networksocketPlatformDispose networksocketDolphinDispose + +typedef networksocketdolphin_t networksocketplatform_t; diff --git a/src/dusklinux/network/CMakeLists.txt b/src/dusklinux/network/CMakeLists.txt index c1322dc3..ca2770f2 100644 --- a/src/dusklinux/network/CMakeLists.txt +++ b/src/dusklinux/network/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC networklinux.c + networksocketlinux.c ) diff --git a/src/dusklinux/network/networksocketlinux.c b/src/dusklinux/network/networksocketlinux.c new file mode 100644 index 00000000..34991e12 --- /dev/null +++ b/src/dusklinux/network/networksocketlinux.c @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "network/networksocket.h" +#include "assert/assert.h" + +/** Recv timeout: short enough to check the thread stop flag regularly. */ +#define NETWORKSOCKET_LINUX_RECV_TIMEOUT_US 100000 + +errorret_t networksocketLinuxInit(networksocket_t *socket) { + socket->platform.fd = -1; + errorOk(); +} + +errorret_t networksocketLinuxConnect(networksocket_t *nsocket) { + struct addrinfo hints; + struct addrinfo *result; + struct addrinfo *rp; + struct timeval tv; + char_t portStr[8]; + int ret; + int fd; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + snprintf(portStr, sizeof(portStr), "%u", (unsigned int)nsocket->port); + + ret = getaddrinfo(nsocket->host, portStr, &hints, &result); + if (ret != 0) { + errorThrowState( + &nsocket->errorState, + "getaddrinfo(%s): %s", + nsocket->host, + gai_strerror(ret) + ); + } + + fd = -1; + for (rp = result; rp != NULL; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) continue; + + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; + + close(fd); + fd = -1; + } + freeaddrinfo(result); + + if (fd == -1) { + errorThrowState( + &nsocket->errorState, + "Failed to connect to %s:%u", + nsocket->host, + (unsigned int)nsocket->port + ); + } + + tv.tv_sec = 0; + tv.tv_usec = NETWORKSOCKET_LINUX_RECV_TIMEOUT_US; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + nsocket->platform.fd = fd; + errorOk(); +} + +errorret_t networksocketLinuxSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +) { + ssize_t sent; + size_t total; + + total = 0; + while (total < len) { + sent = send(socket->platform.fd, data + total, len - total, MSG_NOSIGNAL); + if (sent <= 0) { + errorThrowState( + &socket->errorState, + "send failed: %s", + strerror(errno) + ); + } + total += (size_t)sent; + } + errorOk(); +} + +errorret_t networksocketLinuxRecv( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +) { + ssize_t received; + + received = recv(socket->platform.fd, buf, maxLen, 0); + + if (received < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + *outLen = 0; + errorOk(); + } + errorThrowState( + &socket->errorState, + "recv failed: %s", + strerror(errno) + ); + } + + if (received == 0) { + *outLen = NETWORKSOCKET_RECV_CLOSED; + errorOk(); + } + + *outLen = (size_t)received; + errorOk(); +} + +errorret_t networksocketLinuxDisconnect(networksocket_t *socket) { + if (socket->platform.fd != -1) { + shutdown(socket->platform.fd, SHUT_RDWR); + close(socket->platform.fd); + socket->platform.fd = -1; + } + errorOk(); +} + +errorret_t networksocketLinuxDispose(networksocket_t *socket) { + if (socket->platform.fd != -1) { + close(socket->platform.fd); + socket->platform.fd = -1; + } + errorOk(); +} diff --git a/src/dusklinux/network/networksocketlinux.h b/src/dusklinux/network/networksocketlinux.h new file mode 100644 index 00000000..c5c842f9 --- /dev/null +++ b/src/dusklinux/network/networksocketlinux.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "error/error.h" +#include +#include +#include +#include + +typedef struct networksocket_s networksocket_t; + +typedef struct { + int fd; +} networksocketlinux_t; + +/** + * Initializes platform state for the socket (sets fd to -1). + */ +errorret_t networksocketLinuxInit(networksocket_t *socket); + +/** + * Resolves host/port and establishes a TCP connection. Sets SO_RCVTIMEO + * on the resulting file descriptor so recv calls return periodically even + * when no data arrives, allowing the thread to check its stop flag. + */ +errorret_t networksocketLinuxConnect(networksocket_t *socket); + +/** + * Writes all bytes in data to the socket. Loops on partial sends. + */ +errorret_t networksocketLinuxSend( + networksocket_t *socket, + const uint8_t *data, + size_t len +); + +/** + * Attempts to read up to maxLen bytes into buf. Sets *outLen to the number + * of bytes received. Returns 0 in *outLen on timeout (EAGAIN/EWOULDBLOCK). + * Returns NETWORKSOCKET_RECV_CLOSED in *outLen when the peer closed the + * connection. + */ +errorret_t networksocketLinuxRecv( + networksocket_t *socket, + uint8_t *buf, + size_t maxLen, + size_t *outLen +); + +/** + * Shuts down and closes the socket file descriptor. + */ +errorret_t networksocketLinuxDisconnect(networksocket_t *socket); + +/** + * Closes the file descriptor if still open (idempotent). + */ +errorret_t networksocketLinuxDispose(networksocket_t *socket); diff --git a/src/dusklinux/network/networksocketplatform.h b/src/dusklinux/network/networksocketplatform.h new file mode 100644 index 00000000..ad639311 --- /dev/null +++ b/src/dusklinux/network/networksocketplatform.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "networksocketlinux.h" + +#define networksocketPlatformInit networksocketLinuxInit +#define networksocketPlatformConnect networksocketLinuxConnect +#define networksocketPlatformSend networksocketLinuxSend +#define networksocketPlatformRecv networksocketLinuxRecv +#define networksocketPlatformDisconnect networksocketLinuxDisconnect +#define networksocketPlatformDispose networksocketLinuxDispose + +typedef networksocketlinux_t networksocketplatform_t;