Net
This commit is contained in:
@@ -37,7 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(saveInit());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
@@ -88,7 +88,7 @@ errorret_t engineDispose(void) {
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
network.c
|
||||
networkinfo.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(http)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
networkhttpheader.c
|
||||
networkhttpurl.c
|
||||
networkhttprequest.c
|
||||
networkhttpprocess.c
|
||||
networkhttpthread.c
|
||||
networkhttp.c
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttp.h"
|
||||
#include "networkhttprequest.h"
|
||||
#include "networkhttpthread.h"
|
||||
|
||||
networkhttp_t NETWORK_HTTP;
|
||||
|
||||
errorret_t networkHttpInit(void) {
|
||||
networkHttpRequestPoolInit();
|
||||
|
||||
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||
threadStart(&NETWORK_HTTP.thread);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUpdate(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
const networkhttprequeststate_t state = request->state;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||
eventInvoke(&request->onComplete, request);
|
||||
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||
eventInvoke(&request->onError, request);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
networkHttpRequestReset(request);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpDispose(void) {
|
||||
threadStop(&NETWORK_HTTP.thread);
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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"
|
||||
|
||||
typedef struct {
|
||||
thread_t thread;
|
||||
} networkhttp_t;
|
||||
|
||||
extern networkhttp_t NETWORK_HTTP;
|
||||
|
||||
/**
|
||||
* Initializes the HTTP client: prepares the request pool and starts
|
||||
* the background worker thread. Called once during engine startup.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpInit(void);
|
||||
|
||||
/**
|
||||
* Dispatches any requests that finished on the background thread since
|
||||
* the last call: fires onComplete/onError on the main thread, frees
|
||||
* the response, and returns the slot to the pool. Call once per frame.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpUpdate(void);
|
||||
|
||||
/**
|
||||
* Stops the background worker thread and disposes of the HTTP client.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpDispose(void);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpheader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||
}
|
||||
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||
return
|
||||
stringCompareInsensitive(name, "Host") == 0 ||
|
||||
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||
}
|
||||
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
) {
|
||||
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||
|
||||
networkhttpheader_t *header = &list->headers[list->count];
|
||||
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||
list->count++;
|
||||
}
|
||||
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
) {
|
||||
for(uint32_t i = 0; i < list->count; i++) {
|
||||
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||
return &list->headers[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
} networkhttpheader_t;
|
||||
|
||||
typedef struct {
|
||||
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||
uint32_t count;
|
||||
} networkhttpheaderlist_t;
|
||||
|
||||
/**
|
||||
* Clears a header list, removing all entries.
|
||||
*
|
||||
* @param list The header list to clear.
|
||||
*/
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||
|
||||
/**
|
||||
* Determines whether a header name is reserved, meaning it is computed
|
||||
* internally by the request writer and must not be set by the caller.
|
||||
* This is Host, Connection and Content-Length.
|
||||
*
|
||||
* @param name The header name to check, compared case-insensitively.
|
||||
* @return true if the header name is reserved.
|
||||
*/
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||
|
||||
/**
|
||||
* Appends a header to the list, silently doing nothing if the list is
|
||||
* already full or if skipReserved is true and the name is reserved (see
|
||||
* networkHttpHeaderIsReserved).
|
||||
*
|
||||
* @param list The header list to append to.
|
||||
* @param name The header name.
|
||||
* @param value The header value.
|
||||
* @param skipReserved If true, reserved header names are ignored.
|
||||
*/
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first header in the list matching the given name, compared
|
||||
* case-insensitively.
|
||||
*
|
||||
* @param list The header list to search.
|
||||
* @param name The header name to search for.
|
||||
* @return Pointer to the matching header, or NULL if not found.
|
||||
*/
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
);
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
) {
|
||||
switch(method) {
|
||||
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||
case NETWORK_HTTP_METHOD_GET:
|
||||
default:
|
||||
return "GET";
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
) {
|
||||
size_t cursor = 0;
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Connection: close\r\n"
|
||||
));
|
||||
|
||||
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
char_t lengthStr[24];
|
||||
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Content-Length: "
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
*outLength = cursor;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
) {
|
||||
size_t sentTotal = 0;
|
||||
while(sentTotal < length) {
|
||||
size_t sent = 0;
|
||||
errorChain(networkSocketPlatformSend(
|
||||
sock, data + sentTotal, length - sentTotal, &sent
|
||||
));
|
||||
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||
sentTotal += sent;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
) {
|
||||
size_t headerLength = 0;
|
||||
headerBuf[0] = '\0';
|
||||
|
||||
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
|
||||
for(;;) {
|
||||
size_t received = 0;
|
||||
errorChain(networkSocketPlatformReceive(
|
||||
sock, chunk, sizeof(chunk), &received
|
||||
));
|
||||
if(received == 0) {
|
||||
errorThrow("Connection closed before response headers completed");
|
||||
}
|
||||
|
||||
if(headerLength + received >= headerBufSize) {
|
||||
errorThrow("Response headers too large");
|
||||
}
|
||||
|
||||
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||
headerLength += received;
|
||||
headerBuf[headerLength] = '\0';
|
||||
|
||||
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||
if(terminator == NULL) continue;
|
||||
|
||||
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||
const size_t bodyStart = terminatorOffset + 4;
|
||||
const size_t extra = headerLength - bodyStart;
|
||||
|
||||
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||
*outLeftoverLength = extra;
|
||||
|
||||
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||
// line so callers can split on "\r\n" without a special case.
|
||||
headerBuf[terminatorOffset + 2] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
) {
|
||||
const char_t *space = strchr(line, ' ');
|
||||
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||
|
||||
char_t statusStr[4];
|
||||
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||
memoryCopy(statusStr, space + 1, 3);
|
||||
statusStr[3] = '\0';
|
||||
|
||||
if(!stringToU16(statusStr, outStatus)) {
|
||||
errorThrow("Malformed status code: %s", line);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
const char_t *colon = strchr(line, ':');
|
||||
if(colon == NULL) return;
|
||||
|
||||
const size_t nameLength = (size_t)(colon - line);
|
||||
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
memoryCopy(name, line, nameLength);
|
||||
name[nameLength] = '\0';
|
||||
|
||||
const char_t *valueStart = colon + 1;
|
||||
while(*valueStart == ' ') valueStart++;
|
||||
|
||||
const size_t valueLength = strlen(valueStart);
|
||||
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
memoryCopy(value, valueStart, valueLength + 1);
|
||||
|
||||
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
char_t *cursor = headerBuf;
|
||||
bool_t first = true;
|
||||
|
||||
for(;;) {
|
||||
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||
const bool_t isLast = lineEnd == NULL;
|
||||
if(lineEnd != NULL) *lineEnd = '\0';
|
||||
|
||||
if(first) {
|
||||
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||
first = false;
|
||||
} else if(cursor[0] != '\0') {
|
||||
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||
}
|
||||
|
||||
if(isLast) break;
|
||||
cursor = lineEnd + 2;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
) {
|
||||
const networkhttpheader_t *contentLength =
|
||||
networkHttpHeaderListFind(headers, "Content-Length");
|
||||
|
||||
if(contentLength != NULL) {
|
||||
int64_t expected = 0;
|
||||
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||
}
|
||||
|
||||
if(expected == 0) {
|
||||
*outBody = NULL;
|
||||
*outBodyLength = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *body = memoryAllocate((size_t)expected);
|
||||
const size_t initialCopy =
|
||||
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||
size_t received = initialCopy;
|
||||
|
||||
while(received < (size_t)expected) {
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + received, (size_t)expected - received, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) {
|
||||
memoryFree(body);
|
||||
errorThrow("Connection closed before full body was received");
|
||||
}
|
||||
received += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = (size_t)expected;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// No Content-Length: read until the connection closes.
|
||||
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||
|
||||
uint8_t *body = memoryAllocate(capacity);
|
||||
size_t length = 0;
|
||||
if(leftoverLength > 0) {
|
||||
memoryCopy(body, leftover, leftoverLength);
|
||||
length = leftoverLength;
|
||||
}
|
||||
|
||||
for(;;) {
|
||||
if(length == capacity) {
|
||||
const size_t newCapacity = capacity * 2;
|
||||
memoryResize((void **)&body, capacity, newCapacity);
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + length, capacity - length, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) break;
|
||||
length += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = length;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
) {
|
||||
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||
|
||||
networksocketplatform_t sock;
|
||||
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||
|
||||
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||
size_t headLength = 0;
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, (const uint8_t *)head, headLength
|
||||
));
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, request->body, request->bodyLength
|
||||
));
|
||||
}
|
||||
|
||||
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
size_t leftoverLength = 0;
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||
headerBuf, &outResponse->status, &outResponse->headers
|
||||
));
|
||||
|
||||
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||
&outResponse->body, &outResponse->bodyLength
|
||||
);
|
||||
networkSocketPlatformClose(&sock);
|
||||
errorChain(bodyRet);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 "network/http/networkhttprequest.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
#include "network/networksocketplatform.h"
|
||||
|
||||
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||
|
||||
/**
|
||||
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||
* for every step of an attempt after the socket has connected, since
|
||||
* failing partway through must not leak the socket.
|
||||
*/
|
||||
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||
errorret_t _nhpErr = (_expr); \
|
||||
if(errorIsNotOk(_nhpErr)) { \
|
||||
networkSocketPlatformClose(sock); \
|
||||
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||
*
|
||||
* @param method The method to name.
|
||||
* @return A static string naming the method.
|
||||
*/
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the request line, headers and blank line terminator (but not
|
||||
* the body) for request, targeting target, into dest.
|
||||
*
|
||||
* @param request The request being sent.
|
||||
* @param target The resolved host/port/path for this attempt.
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param outLength The number of bytes written is written here.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends length bytes of data over sock, looping until all of it has
|
||||
* been written.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @return An error if the send failed or the connection closed early.
|
||||
*/
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||
* response headers has been seen, writing the status line and header
|
||||
* lines (without the blank line) into headerBuf as a null-terminated
|
||||
* string. Any body bytes read past the terminator in the same receive
|
||||
* are written to leftover.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headerBuf The destination buffer for the header text.
|
||||
* @param headerBufSize The size of headerBuf, including the null
|
||||
* terminator.
|
||||
* @param leftover Destination buffer for any body bytes read early.
|
||||
* @param leftoverBufSize The size of leftover.
|
||||
* @param outLeftoverLength The number of bytes written to leftover is
|
||||
* written here.
|
||||
* @return An error if the headers could not be read, were malformed,
|
||||
* or did not fit within the given buffers.
|
||||
*/
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||
*
|
||||
* @param line The status line, null-terminated.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a single "Name: Value" response header line and appends it to
|
||||
* outHeaders. Silently does nothing if the line has no colon or the
|
||||
* name/value are too long to fit a networkhttpheader_t.
|
||||
*
|
||||
* @param line The header line, null-terminated.
|
||||
* @param outHeaders The header list to append to.
|
||||
*/
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses the status line and every header line out of headerBuf (as
|
||||
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||
* place to split it into lines.
|
||||
*
|
||||
* @param headerBuf The header text to parse.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @param outHeaders The header list to populate.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the response body from sock. If headers contains a
|
||||
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||
* the connection closes. Either way, leftover/leftoverLength (body
|
||||
* bytes already read while looking for the header terminator) are
|
||||
* included at the start of the result.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headers The parsed response headers.
|
||||
* @param leftover Body bytes already read past the header terminator.
|
||||
* @param leftoverLength The number of bytes in leftover.
|
||||
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||
* here, or NULL if the body is empty. Owned by the caller.
|
||||
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||
* @return An error if the body could not be fully read.
|
||||
*/
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||
* the request head + body, and reads back one parsed response. Does
|
||||
* not follow redirects -- that is the caller's responsibility.
|
||||
*
|
||||
* @param request The request to send (method, headers, body).
|
||||
* @param target The resolved host/port/path to connect to for this
|
||||
* attempt (may differ from the request's own URL if this is a
|
||||
* redirected attempt).
|
||||
* @param outResponse The parsed response is written here on success.
|
||||
* @return An error if the exchange could not be completed.
|
||||
*/
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttprequest.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
void networkHttpRequestPoolInit(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
memoryZero(request, sizeof(networkhttprequest_t));
|
||||
threadMutexInit(&request->mutex);
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
threadMutexUnlock(&request->mutex);
|
||||
return request;
|
||||
}
|
||||
|
||||
assertUnreachable("No available HTTP request slots.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||
assertNotNull(request, "request must not be NULL");
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
|
||||
if(request->body != NULL) memoryFree(request->body);
|
||||
request->body = NULL;
|
||||
request->bodyLength = 0;
|
||||
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
|
||||
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||
request->method = method;
|
||||
|
||||
errorChain(networkHttpUrlBuild(
|
||||
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||
));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
for(uint32_t i = 0; i < headerCount; i++) {
|
||||
networkHttpHeaderListAdd(
|
||||
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||
);
|
||||
}
|
||||
|
||||
request->bodyLength = bodyLength;
|
||||
if(bodyLength > 0) {
|
||||
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||
request->body = memoryAllocate(bodyLength);
|
||||
memoryCopy(request->body, body, bodyLength);
|
||||
}
|
||||
|
||||
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 "event/event.h"
|
||||
#include "thread/threadmutex.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
|
||||
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
|
||||
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_METHOD_GET,
|
||||
NETWORK_HTTP_METHOD_POST,
|
||||
NETWORK_HTTP_METHOD_PUT
|
||||
} networkhttpmethod_t;
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_REQUEST_STATE_FREE,
|
||||
NETWORK_HTTP_REQUEST_STATE_PENDING,
|
||||
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
|
||||
NETWORK_HTTP_REQUEST_STATE_DONE,
|
||||
NETWORK_HTTP_REQUEST_STATE_ERROR
|
||||
} networkhttprequeststate_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t status;
|
||||
networkhttpheaderlist_t headers;
|
||||
|
||||
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
|
||||
// event has finished firing, at which point it is freed.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
} networkhttpresponse_t;
|
||||
|
||||
typedef struct {
|
||||
threadmutex_t mutex;
|
||||
networkhttprequeststate_t state;
|
||||
|
||||
networkhttpmethod_t method;
|
||||
|
||||
// Fully built, including any query params, by networkHttpRequest.
|
||||
char_t url[NETWORK_HTTP_URL_MAX];
|
||||
networkhttpheaderlist_t requestHeaders;
|
||||
|
||||
// Owned copy, memoryAllocate'd.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
|
||||
/** Fired on the main thread once a response has been received. */
|
||||
event_t onComplete;
|
||||
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
/** Fired on the main thread if the request could not be completed. */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
networkhttpresponse_t response;
|
||||
} networkhttprequest_t;
|
||||
|
||||
extern networkhttprequest_t
|
||||
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the request pool: prepares every slot's mutex and events
|
||||
* and marks them all FREE. Called once by networkHttpInit.
|
||||
*/
|
||||
void networkHttpRequestPoolInit(void);
|
||||
|
||||
/**
|
||||
* Finds and returns a FREE slot in the request pool. The slot is not
|
||||
* marked as in-use by this call; the caller must populate it and then
|
||||
* set its state to PENDING.
|
||||
*
|
||||
* @return A free request slot.
|
||||
*/
|
||||
networkhttprequest_t * networkHttpRequestClaim(void);
|
||||
|
||||
/**
|
||||
* Resets a request slot back to FREE, freeing any owned buffers
|
||||
* (request body, response body) and clearing its event subscribers.
|
||||
*
|
||||
* @param request The request slot to reset.
|
||||
*/
|
||||
void networkHttpRequestReset(networkhttprequest_t *request);
|
||||
|
||||
/**
|
||||
* Sends an HTTP request asynchronously. The request runs on a
|
||||
* background thread; onComplete or onError is invoked on the main
|
||||
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
|
||||
* followed automatically.
|
||||
*
|
||||
* @param method The HTTP method to use.
|
||||
* @param url The "http://host[:port]/path" URL to request. Must not
|
||||
* use any scheme other than http.
|
||||
* @param headers Request headers to send, may be NULL if headerCount
|
||||
* is 0. The Host, Connection and Content-Length headers are
|
||||
* reserved and computed internally; any of those passed here
|
||||
* are ignored.
|
||||
* @param headerCount Number of entries in headers.
|
||||
* @param queryParams Query string parameters to append to url, may be
|
||||
* NULL if queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @param body Request body to send for POST/PUT, may be NULL if
|
||||
* bodyLength is 0. Ignored for GET.
|
||||
* @param bodyLength Number of bytes in body.
|
||||
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
|
||||
* response has been received, whatever its status code. May be
|
||||
* NULL to ignore.
|
||||
* @param onError Invoked with (networkhttprequest_t *, user) if the
|
||||
* request could not be completed (DNS/connect/send/receive
|
||||
* failure, or a malformed response). May be NULL to ignore.
|
||||
* @param user Arbitrary pointer forwarded to onComplete/onError.
|
||||
* @return An error if the request could not be queued (e.g. a
|
||||
* malformed url).
|
||||
*/
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpthread.h"
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void networkHttpThreadRun(thread_t *thread) {
|
||||
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
|
||||
|
||||
while(!threadShouldStop(thread)) {
|
||||
bool_t didWork = false;
|
||||
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
didWork = true;
|
||||
networkHttpThreadProcessRequest(request);
|
||||
}
|
||||
|
||||
if(threadShouldStop(thread)) break;
|
||||
if(!didWork) usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
|
||||
networkhttpurl_t target;
|
||||
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
|
||||
|
||||
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
|
||||
|
||||
if(errorIsNotOk(parseRet)) {
|
||||
errorCatch(errorPrint(parseRet));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
} else {
|
||||
for(uint32_t redirect = 0; ; redirect++) {
|
||||
errorret_t ret = networkHttpProcessAttempt(
|
||||
request, &target, &request->response
|
||||
);
|
||||
|
||||
if(errorIsNotOk(ret)) {
|
||||
errorCatch(errorPrint(ret));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
const bool_t isRedirect = request->response.status == 301;
|
||||
const networkhttpheader_t *location = isRedirect ?
|
||||
networkHttpHeaderListFind(&request->response.headers, "Location") :
|
||||
NULL;
|
||||
|
||||
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
|
||||
break;
|
||||
}
|
||||
|
||||
networkhttpurl_t nextTarget;
|
||||
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
|
||||
if(errorIsNotOk(nextRet)) {
|
||||
// Not an absolute URL we can follow -- treat the 301 itself as
|
||||
// the final response instead of failing the whole request.
|
||||
errorCatch(errorPrint(nextRet));
|
||||
break;
|
||||
}
|
||||
|
||||
// Free the intermediate response body before following further.
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
target = nextTarget;
|
||||
}
|
||||
}
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = finalState;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "thread/thread.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
|
||||
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
|
||||
|
||||
/**
|
||||
* The background worker thread body: repeatedly scans the request
|
||||
* pool for PENDING slots, processes each to completion, and marks it
|
||||
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
|
||||
* until the thread is stopped.
|
||||
*
|
||||
* @param thread The thread runner.
|
||||
*/
|
||||
void networkHttpThreadRun(thread_t *thread);
|
||||
|
||||
/**
|
||||
* Processes a single ACTIVE request slot to completion, following up
|
||||
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
|
||||
* in the DONE or ERROR state. Only called from the background thread.
|
||||
*
|
||||
* @param request The request slot to process.
|
||||
*/
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpurl.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
assertNotNull(out, "out must not be NULL");
|
||||
|
||||
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
|
||||
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
|
||||
}
|
||||
|
||||
const char_t *rest = url + 7;
|
||||
|
||||
const size_t hostLen = strcspn(rest, ":/");
|
||||
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
|
||||
errorThrow("Invalid host in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->host, rest, hostLen);
|
||||
out->host[hostLen] = '\0';
|
||||
rest += hostLen;
|
||||
|
||||
if(*rest == ':') {
|
||||
rest++;
|
||||
|
||||
const size_t portLen = strcspn(rest, "/");
|
||||
char_t portStr[8];
|
||||
if(portLen == 0 || portLen >= sizeof(portStr)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
memoryCopy(portStr, rest, portLen);
|
||||
portStr[portLen] = '\0';
|
||||
|
||||
if(!stringToU16(portStr, &out->port)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
rest += portLen;
|
||||
} else {
|
||||
out->port = NETWORK_HTTP_PORT_DEFAULT;
|
||||
}
|
||||
|
||||
const char_t *path = *rest == '\0' ? "/" : rest;
|
||||
const size_t pathLen = strlen(path);
|
||||
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
|
||||
errorThrow("Path too long in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->path, path, pathLen + 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
) {
|
||||
assertNotNull(dest, "dest must not be NULL");
|
||||
assertNotNull(baseUrl, "baseUrl must not be NULL");
|
||||
|
||||
const size_t baseLen = strlen(baseUrl);
|
||||
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
|
||||
memoryCopy(dest, baseUrl, baseLen + 1);
|
||||
|
||||
size_t cursor = baseLen;
|
||||
bool_t hasQuery = stringIncludesString(baseUrl, "?");
|
||||
|
||||
for(uint32_t i = 0; i < queryParamCount; i++) {
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, hasQuery ? "&" : "?"
|
||||
));
|
||||
hasQuery = true;
|
||||
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].name
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].value
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
) {
|
||||
const size_t len = strlen(str);
|
||||
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
|
||||
|
||||
memoryCopy(dest + *cursor, str, len + 1);
|
||||
*cursor += len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
) {
|
||||
const char_t *hex = "0123456789ABCDEF";
|
||||
|
||||
for(const char_t *p = src; *p != '\0'; p++) {
|
||||
const uint8_t c = (uint8_t)*p;
|
||||
const bool_t unreserved =
|
||||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
|
||||
|
||||
if(unreserved) {
|
||||
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = (char_t)c;
|
||||
} else {
|
||||
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = '%';
|
||||
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
|
||||
dest[(*cursor)++] = hex[c & 0xF];
|
||||
}
|
||||
}
|
||||
|
||||
dest[*cursor] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 "network/http/networkhttpheader.h"
|
||||
|
||||
#define NETWORK_HTTP_URL_MAX 1024
|
||||
#define NETWORK_HTTP_HOST_MAX 256
|
||||
#define NETWORK_HTTP_PATH_MAX 768
|
||||
#define NETWORK_HTTP_PORT_DEFAULT 80
|
||||
|
||||
typedef struct {
|
||||
char_t host[NETWORK_HTTP_HOST_MAX];
|
||||
uint16_t port;
|
||||
|
||||
// Includes the leading slash and, if present, the query string.
|
||||
char_t path[NETWORK_HTTP_PATH_MAX];
|
||||
} networkhttpurl_t;
|
||||
|
||||
/**
|
||||
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
|
||||
* rejected, since this client does not support TLS.
|
||||
*
|
||||
* @param url The URL to parse.
|
||||
* @param out The parsed URL is written here.
|
||||
* @return An error if the URL is malformed or uses an unsupported scheme.
|
||||
*/
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
|
||||
|
||||
/**
|
||||
* Copies baseUrl into dest, then appends each queryParam as a
|
||||
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
|
||||
* first one, unless baseUrl already contains a "?").
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param baseUrl The URL to copy before appending query params.
|
||||
* @param queryParams Array of query parameters to append, may be NULL if
|
||||
* queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Appends a raw (not percent-encoded) string to dest at *cursor,
|
||||
* advancing *cursor and keeping dest null-terminated.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param str The string to append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
);
|
||||
|
||||
/**
|
||||
* Percent-encodes src and appends it to dest at *cursor, advancing
|
||||
* *cursor and keeping dest null-terminated. Unreserved characters
|
||||
* (letters, digits, "-", "_", ".", "~") are copied as-is.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param src The string to percent-encode and append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
);
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "network.h"
|
||||
#include "network/http/networkhttp.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "log/log.h"
|
||||
@@ -18,11 +19,13 @@ errorret_t networkInit() {
|
||||
NETWORK.errorState.code = ERROR_OK;
|
||||
NETWORK.onDisconnect = NULL;
|
||||
|
||||
return networkPlatformInit();
|
||||
errorChain(networkPlatformInit());
|
||||
return networkHttpInit();
|
||||
}
|
||||
|
||||
errorret_t networkUpdate() {
|
||||
errorChain(networkPlatformUpdate());
|
||||
errorChain(networkHttpUpdate());
|
||||
|
||||
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
||||
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
||||
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
|
||||
}
|
||||
|
||||
errorChain(networkPlatformDispose());
|
||||
errorOk();
|
||||
return networkHttpDispose();
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
networkdolphin.c
|
||||
networksocketdolphin.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networksocketdolphin.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
#ifdef DUSK_WII
|
||||
#include <network.h>
|
||||
#endif
|
||||
|
||||
errorret_t networkSocketDolphinConnect(
|
||||
networksocketdolphin_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(host, "host must not be NULL");
|
||||
|
||||
#ifdef DUSK_WII
|
||||
struct hostent *entry = net_gethostbyname(host);
|
||||
if(entry == NULL || entry->h_addr_list[0] == NULL) {
|
||||
errorThrow("Failed to resolve host %s", host);
|
||||
}
|
||||
|
||||
const s32 fd = net_socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0) errorThrow("Failed to create socket: %d", (int_t)fd);
|
||||
|
||||
struct sockaddr_in serverAddr;
|
||||
memoryZero(&serverAddr, sizeof(serverAddr));
|
||||
serverAddr.sin_family = AF_INET;
|
||||
serverAddr.sin_port = htons(port);
|
||||
memoryCopy(
|
||||
&serverAddr.sin_addr, entry->h_addr_list[0], sizeof(serverAddr.sin_addr)
|
||||
);
|
||||
|
||||
const s32 ret = net_connect(fd, (void *)&serverAddr, sizeof(serverAddr));
|
||||
if(ret < 0) {
|
||||
net_close(fd);
|
||||
errorThrow("Failed to connect to %s:%u: %d", host, port, (int_t)ret);
|
||||
}
|
||||
|
||||
sock->fd = fd;
|
||||
errorOk();
|
||||
#else
|
||||
errorThrow(
|
||||
"Networking is not supported on this platform (%s:%u)", host, port
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
errorret_t networkSocketDolphinSend(
|
||||
networksocketdolphin_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(data, "data must not be NULL");
|
||||
assertNotNull(outSent, "outSent must not be NULL");
|
||||
|
||||
#ifdef DUSK_WII
|
||||
const s32 sent = net_send(sock->fd, data, (s32)length, 0);
|
||||
if(sent < 0) errorThrow("Failed to send data: %d", (int_t)sent);
|
||||
|
||||
*outSent = (size_t)sent;
|
||||
errorOk();
|
||||
#else
|
||||
errorThrow("Networking is not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
errorret_t networkSocketDolphinReceive(
|
||||
networksocketdolphin_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(buffer, "buffer must not be NULL");
|
||||
assertNotNull(outReceived, "outReceived must not be NULL");
|
||||
|
||||
#ifdef DUSK_WII
|
||||
const s32 received = net_recv(sock->fd, buffer, (s32)bufferSize, 0);
|
||||
if(received < 0) errorThrow("Failed to receive data: %d", (int_t)received);
|
||||
|
||||
*outReceived = (size_t)received;
|
||||
errorOk();
|
||||
#else
|
||||
errorThrow("Networking is not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
void networkSocketDolphinClose(networksocketdolphin_t *sock) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
|
||||
#ifdef DUSK_WII
|
||||
if(sock->fd >= 0) net_close(sock->fd);
|
||||
#endif
|
||||
sock->fd = -1;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
typedef struct {
|
||||
int_t fd;
|
||||
} networksocketdolphin_t;
|
||||
|
||||
/**
|
||||
* Resolves host and opens a blocking TCP connection to host:port.
|
||||
* Always fails under plain DUSK_GAMECUBE -- retail GameCube units have
|
||||
* no network hardware.
|
||||
*
|
||||
* @param sock The socket structure to initialize.
|
||||
* @param host The hostname or IP address to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @return An error if resolution or connection failed.
|
||||
*/
|
||||
errorret_t networkSocketDolphinConnect(
|
||||
networksocketdolphin_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends data over the socket. May write fewer bytes than requested; the
|
||||
* caller is responsible for looping until all data is sent.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @param outSent The number of bytes actually sent is written here.
|
||||
* @return An error if the send failed.
|
||||
*/
|
||||
errorret_t networkSocketDolphinSend(
|
||||
networksocketdolphin_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
);
|
||||
|
||||
/**
|
||||
* Receives data from the socket.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param buffer The buffer to receive into.
|
||||
* @param bufferSize The size of buffer.
|
||||
* @param outReceived The number of bytes actually received is written
|
||||
* here; 0 means the peer closed the connection.
|
||||
* @return An error if the receive failed.
|
||||
*/
|
||||
errorret_t networkSocketDolphinReceive(
|
||||
networksocketdolphin_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
);
|
||||
|
||||
/**
|
||||
* Closes the socket.
|
||||
*
|
||||
* @param sock The socket to close.
|
||||
*/
|
||||
void networkSocketDolphinClose(networksocketdolphin_t *sock);
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "networksocketdolphin.h"
|
||||
|
||||
typedef networksocketdolphin_t networksocketplatform_t;
|
||||
|
||||
#define networkSocketPlatformConnect networkSocketDolphinConnect
|
||||
#define networkSocketPlatformSend networkSocketDolphinSend
|
||||
#define networkSocketPlatformReceive networkSocketDolphinReceive
|
||||
#define networkSocketPlatformClose networkSocketDolphinClose
|
||||
@@ -6,4 +6,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
networklinux.c
|
||||
networksocketlinux.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networksocketlinux.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
|
||||
errorret_t networkSocketLinuxConnect(
|
||||
networksocketlinux_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(host, "host must not be NULL");
|
||||
|
||||
char_t portStr[8];
|
||||
stringFormat(portStr, sizeof(portStr) - 1, "%u", port);
|
||||
|
||||
struct addrinfo hints;
|
||||
memoryZero(&hints, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo *result = NULL;
|
||||
const int_t gaiRet = getaddrinfo(host, portStr, &hints, &result);
|
||||
if(gaiRet != 0) {
|
||||
errorThrow("Failed to resolve host %s: %s", host, gai_strerror(gaiRet));
|
||||
}
|
||||
|
||||
int_t fd = -1;
|
||||
for(struct addrinfo *addr = result; addr != NULL; addr = addr->ai_next) {
|
||||
fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
|
||||
if(fd < 0) continue;
|
||||
|
||||
if(connect(fd, addr->ai_addr, addr->ai_addrlen) == 0) break;
|
||||
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
|
||||
if(fd < 0) errorThrow("Failed to connect to %s:%u", host, port);
|
||||
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = NETWORK_SOCKET_LINUX_TIMEOUT_SECONDS;
|
||||
timeout.tv_usec = 0;
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
sock->fd = fd;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkSocketLinuxSend(
|
||||
networksocketlinux_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(data, "data must not be NULL");
|
||||
assertNotNull(outSent, "outSent must not be NULL");
|
||||
|
||||
const ssize_t sent = send(sock->fd, data, length, 0);
|
||||
if(sent < 0) errorThrow("Failed to send data: %s", strerror(errno));
|
||||
|
||||
*outSent = (size_t)sent;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkSocketLinuxReceive(
|
||||
networksocketlinux_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(buffer, "buffer must not be NULL");
|
||||
assertNotNull(outReceived, "outReceived must not be NULL");
|
||||
|
||||
const ssize_t received = recv(sock->fd, buffer, bufferSize, 0);
|
||||
if(received < 0) errorThrow("Failed to receive data: %s", strerror(errno));
|
||||
|
||||
*outReceived = (size_t)received;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkSocketLinuxClose(networksocketlinux_t *sock) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
if(sock->fd >= 0) close(sock->fd);
|
||||
sock->fd = -1;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
#define NETWORK_SOCKET_LINUX_TIMEOUT_SECONDS 10
|
||||
|
||||
typedef struct {
|
||||
int_t fd;
|
||||
} networksocketlinux_t;
|
||||
|
||||
/**
|
||||
* Resolves host and opens a blocking TCP connection to host:port.
|
||||
*
|
||||
* @param sock The socket structure to initialize.
|
||||
* @param host The hostname or IP address to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @return An error if resolution or connection failed.
|
||||
*/
|
||||
errorret_t networkSocketLinuxConnect(
|
||||
networksocketlinux_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends data over the socket. May write fewer bytes than requested; the
|
||||
* caller is responsible for looping until all data is sent.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @param outSent The number of bytes actually sent is written here.
|
||||
* @return An error if the send failed.
|
||||
*/
|
||||
errorret_t networkSocketLinuxSend(
|
||||
networksocketlinux_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
);
|
||||
|
||||
/**
|
||||
* Receives data from the socket.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param buffer The buffer to receive into.
|
||||
* @param bufferSize The size of buffer.
|
||||
* @param outReceived The number of bytes actually received is written
|
||||
* here; 0 means the peer closed the connection.
|
||||
* @return An error if the receive failed.
|
||||
*/
|
||||
errorret_t networkSocketLinuxReceive(
|
||||
networksocketlinux_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
);
|
||||
|
||||
/**
|
||||
* Closes the socket.
|
||||
*
|
||||
* @param sock The socket to close.
|
||||
*/
|
||||
void networkSocketLinuxClose(networksocketlinux_t *sock);
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "networksocketlinux.h"
|
||||
|
||||
typedef networksocketlinux_t networksocketplatform_t;
|
||||
|
||||
#define networkSocketPlatformConnect networkSocketLinuxConnect
|
||||
#define networkSocketPlatformSend networkSocketLinuxSend
|
||||
#define networkSocketPlatformReceive networkSocketLinuxReceive
|
||||
#define networkSocketPlatformClose networkSocketLinuxClose
|
||||
@@ -6,4 +6,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
networkpsp.c
|
||||
networksocketpsp.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "networksocketpsp.h"
|
||||
|
||||
typedef networksocketpsp_t networksocketplatform_t;
|
||||
|
||||
#define networkSocketPlatformConnect networkSocketPSPConnect
|
||||
#define networkSocketPlatformSend networkSocketPSPSend
|
||||
#define networkSocketPlatformReceive networkSocketPSPReceive
|
||||
#define networkSocketPlatformClose networkSocketPSPClose
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networksocketpsp.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include <pspnet_inet.h>
|
||||
#include <pspnet_resolver.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
errorret_t networkSocketPSPConnect(
|
||||
networksocketpsp_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(host, "host must not be NULL");
|
||||
|
||||
int_t rid = -1;
|
||||
uint8_t resolverBuf[NETWORK_SOCKET_PSP_RESOLVER_BUF_SIZE];
|
||||
int_t ret = sceNetResolverCreate(
|
||||
&rid, resolverBuf, sizeof(resolverBuf)
|
||||
);
|
||||
if(ret < 0) errorThrow("Failed to create resolver: 0x%08X", ret);
|
||||
|
||||
struct in_addr addr;
|
||||
ret = sceNetResolverStartNtoA(
|
||||
rid, host, &addr,
|
||||
NETWORK_SOCKET_PSP_RESOLVER_TIMEOUT_SECONDS,
|
||||
NETWORK_SOCKET_PSP_RESOLVER_RETRY_COUNT
|
||||
);
|
||||
sceNetResolverDelete(rid);
|
||||
if(ret < 0) errorThrow("Failed to resolve host %s: 0x%08X", host, ret);
|
||||
|
||||
const int_t fd = sceNetInetSocket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0) errorThrow("Failed to create socket: 0x%08X", fd);
|
||||
|
||||
struct sockaddr_in serverAddr;
|
||||
memoryZero(&serverAddr, sizeof(serverAddr));
|
||||
serverAddr.sin_family = AF_INET;
|
||||
serverAddr.sin_port = htons(port);
|
||||
serverAddr.sin_addr = addr;
|
||||
|
||||
ret = sceNetInetConnect(
|
||||
fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)
|
||||
);
|
||||
if(ret < 0) {
|
||||
sceNetInetClose(fd);
|
||||
errorThrow("Failed to connect to %s:%u: 0x%08X", host, port, ret);
|
||||
}
|
||||
|
||||
struct SceNetInetTimeval timeout;
|
||||
timeout.tv_sec = NETWORK_SOCKET_PSP_TIMEOUT_SECONDS;
|
||||
timeout.tv_usec = 0;
|
||||
sceNetInetSetsockopt(
|
||||
fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)
|
||||
);
|
||||
sceNetInetSetsockopt(
|
||||
fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)
|
||||
);
|
||||
|
||||
sock->fd = fd;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkSocketPSPSend(
|
||||
networksocketpsp_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(data, "data must not be NULL");
|
||||
assertNotNull(outSent, "outSent must not be NULL");
|
||||
|
||||
const int_t sent = sceNetInetSend(sock->fd, data, length, 0);
|
||||
if(sent < 0) errorThrow("Failed to send data: 0x%08X", sceNetInetGetErrno());
|
||||
|
||||
*outSent = (size_t)sent;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkSocketPSPReceive(
|
||||
networksocketpsp_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
assertNotNull(buffer, "buffer must not be NULL");
|
||||
assertNotNull(outReceived, "outReceived must not be NULL");
|
||||
|
||||
const int_t received = sceNetInetRecv(sock->fd, buffer, bufferSize, 0);
|
||||
if(received < 0) {
|
||||
errorThrow("Failed to receive data: 0x%08X", sceNetInetGetErrno());
|
||||
}
|
||||
|
||||
*outReceived = (size_t)received;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkSocketPSPClose(networksocketpsp_t *sock) {
|
||||
assertNotNull(sock, "sock must not be NULL");
|
||||
if(sock->fd >= 0) sceNetInetClose(sock->fd);
|
||||
sock->fd = -1;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
#define NETWORK_SOCKET_PSP_TIMEOUT_SECONDS 10
|
||||
#define NETWORK_SOCKET_PSP_RESOLVER_BUF_SIZE 1024
|
||||
#define NETWORK_SOCKET_PSP_RESOLVER_TIMEOUT_SECONDS 5
|
||||
#define NETWORK_SOCKET_PSP_RESOLVER_RETRY_COUNT 3
|
||||
|
||||
typedef struct {
|
||||
int_t fd;
|
||||
} networksocketpsp_t;
|
||||
|
||||
/**
|
||||
* Resolves host and opens a blocking TCP connection to host:port.
|
||||
*
|
||||
* @param sock The socket structure to initialize.
|
||||
* @param host The hostname or IP address to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @return An error if resolution or connection failed.
|
||||
*/
|
||||
errorret_t networkSocketPSPConnect(
|
||||
networksocketpsp_t *sock,
|
||||
const char_t *host,
|
||||
const uint16_t port
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends data over the socket. May write fewer bytes than requested; the
|
||||
* caller is responsible for looping until all data is sent.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @param outSent The number of bytes actually sent is written here.
|
||||
* @return An error if the send failed.
|
||||
*/
|
||||
errorret_t networkSocketPSPSend(
|
||||
networksocketpsp_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length,
|
||||
size_t *outSent
|
||||
);
|
||||
|
||||
/**
|
||||
* Receives data from the socket.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param buffer The buffer to receive into.
|
||||
* @param bufferSize The size of buffer.
|
||||
* @param outReceived The number of bytes actually received is written
|
||||
* here; 0 means the peer closed the connection.
|
||||
* @return An error if the receive failed.
|
||||
*/
|
||||
errorret_t networkSocketPSPReceive(
|
||||
networksocketpsp_t *sock,
|
||||
uint8_t *buffer,
|
||||
const size_t bufferSize,
|
||||
size_t *outReceived
|
||||
);
|
||||
|
||||
/**
|
||||
* Closes the socket.
|
||||
*
|
||||
* @param sock The socket to close.
|
||||
*/
|
||||
void networkSocketPSPClose(networksocketpsp_t *sock);
|
||||
@@ -6,6 +6,7 @@
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(thread)
|
||||
add_subdirectory(display)
|
||||
# add_subdirectory(rpg)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
add_subdirectory(http)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
dusktest(test_networkhttp.c)
|
||||
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "network/http/networkhttp.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// ============================================================
|
||||
// Fake single-connection HTTP server, driven on a background thread_t
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
int_t listenFd;
|
||||
uint16_t port;
|
||||
thread_t thread;
|
||||
|
||||
const uint8_t *responseData;
|
||||
size_t responseLength;
|
||||
|
||||
uint8_t receivedData[8192];
|
||||
size_t receivedLength;
|
||||
} fakeserver_t;
|
||||
|
||||
static void fakeServerRun(thread_t *thread) {
|
||||
fakeserver_t *server = (fakeserver_t *)thread->data;
|
||||
|
||||
struct sockaddr_in clientAddr;
|
||||
socklen_t clientAddrLen = sizeof(clientAddr);
|
||||
const int_t clientFd = accept(
|
||||
server->listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen
|
||||
);
|
||||
if(clientFd < 0) return;
|
||||
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 300000;
|
||||
setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
server->receivedLength = 0;
|
||||
for(;;) {
|
||||
if(server->receivedLength >= sizeof(server->receivedData) - 1) break;
|
||||
|
||||
const ssize_t got = recv(
|
||||
clientFd,
|
||||
server->receivedData + server->receivedLength,
|
||||
sizeof(server->receivedData) - 1 - server->receivedLength,
|
||||
0
|
||||
);
|
||||
if(got <= 0) break;
|
||||
server->receivedLength += (size_t)got;
|
||||
}
|
||||
|
||||
send(clientFd, server->responseData, server->responseLength, 0);
|
||||
close(clientFd);
|
||||
close(server->listenFd);
|
||||
server->listenFd = -1;
|
||||
}
|
||||
|
||||
static void fakeServerStart(
|
||||
fakeserver_t *server,
|
||||
const uint8_t *responseData,
|
||||
const size_t responseLength
|
||||
) {
|
||||
memoryZero(server, sizeof(fakeserver_t));
|
||||
server->responseData = responseData;
|
||||
server->responseLength = responseLength;
|
||||
|
||||
server->listenFd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
assert_true(server->listenFd >= 0);
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memoryZero(&addr, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0;
|
||||
assert_int_equal(
|
||||
bind(server->listenFd, (struct sockaddr *)&addr, sizeof(addr)), 0
|
||||
);
|
||||
|
||||
socklen_t addrLen = sizeof(addr);
|
||||
assert_int_equal(
|
||||
getsockname(server->listenFd, (struct sockaddr *)&addr, &addrLen), 0
|
||||
);
|
||||
server->port = ntohs(addr.sin_port);
|
||||
|
||||
assert_int_equal(listen(server->listenFd, 1), 0);
|
||||
|
||||
threadInit(&server->thread, fakeServerRun);
|
||||
server->thread.data = server;
|
||||
threadStart(&server->thread);
|
||||
}
|
||||
|
||||
static void fakeServerStop(fakeserver_t *server) {
|
||||
threadStop(&server->thread);
|
||||
if(server->listenFd >= 0) close(server->listenFd);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// networkHttpRequest completion capture
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
bool_t completed;
|
||||
bool_t errored;
|
||||
uint16_t status;
|
||||
uint8_t body[4096];
|
||||
size_t bodyLength;
|
||||
} capturedresponse_t;
|
||||
|
||||
static void captureOnComplete(void *params, void *user) {
|
||||
const networkhttprequest_t *request = (const networkhttprequest_t *)params;
|
||||
capturedresponse_t *captured = (capturedresponse_t *)user;
|
||||
|
||||
captured->status = request->response.status;
|
||||
captured->bodyLength = request->response.bodyLength;
|
||||
if(request->response.bodyLength > 0) {
|
||||
memoryCopy(
|
||||
captured->body, request->response.body, request->response.bodyLength
|
||||
);
|
||||
}
|
||||
captured->completed = true;
|
||||
}
|
||||
|
||||
static void captureOnError(void *params, void *user) {
|
||||
capturedresponse_t *captured = (capturedresponse_t *)user;
|
||||
captured->errored = true;
|
||||
captured->completed = true;
|
||||
}
|
||||
|
||||
static void waitForCompletion(capturedresponse_t *captured) {
|
||||
for(int32_t i = 0; i < 5000 && !captured->completed; i++) {
|
||||
networkHttpUpdate();
|
||||
usleep(1000);
|
||||
}
|
||||
assert_true(captured->completed);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Per-test setup / teardown
|
||||
// ============================================================
|
||||
|
||||
static int http_setup(void **state) {
|
||||
networkHttpInit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int http_teardown(void **state) {
|
||||
networkHttpDispose();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tests
|
||||
// ============================================================
|
||||
|
||||
static void test_get_request_headers_and_body(void **state) {
|
||||
const char_t *responseText =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
fakeserver_t server;
|
||||
fakeServerStart(
|
||||
&server, (const uint8_t *)responseText, strlen(responseText)
|
||||
);
|
||||
|
||||
char_t url[128];
|
||||
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/foo", server.port);
|
||||
|
||||
const networkhttpheader_t headers[1] = {
|
||||
{ .name = "X-Test", .value = "abc" }
|
||||
};
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_GET, url,
|
||||
headers, 1,
|
||||
NULL, 0,
|
||||
NULL, 0,
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
fakeServerStop(&server);
|
||||
|
||||
assert_false(captured.errored);
|
||||
assert_int_equal(captured.status, 200);
|
||||
assert_int_equal(captured.bodyLength, 5);
|
||||
assert_memory_equal(captured.body, "hello", 5);
|
||||
|
||||
server.receivedData[server.receivedLength] = '\0';
|
||||
const char_t *received = (const char_t *)server.receivedData;
|
||||
assert_non_null(strstr(received, "GET /foo HTTP/1.1"));
|
||||
assert_non_null(strstr(received, "X-Test: abc"));
|
||||
assert_non_null(strstr(received, "Connection: close"));
|
||||
assert_non_null(strstr(received, "Host: 127.0.0.1"));
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_query_params_are_encoded_and_appended(void **state) {
|
||||
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
|
||||
|
||||
fakeserver_t server;
|
||||
fakeServerStart(
|
||||
&server, (const uint8_t *)responseText, strlen(responseText)
|
||||
);
|
||||
|
||||
char_t url[128];
|
||||
stringFormat(
|
||||
url, sizeof(url) - 1, "http://127.0.0.1:%u/search", server.port
|
||||
);
|
||||
|
||||
const networkhttpheader_t query[2] = {
|
||||
{ .name = "q", .value = "hello world" },
|
||||
{ .name = "page", .value = "2" }
|
||||
};
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_GET, url,
|
||||
NULL, 0,
|
||||
query, 2,
|
||||
NULL, 0,
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
fakeServerStop(&server);
|
||||
|
||||
assert_false(captured.errored);
|
||||
|
||||
server.receivedData[server.receivedLength] = '\0';
|
||||
const char_t *received = (const char_t *)server.receivedData;
|
||||
assert_non_null(
|
||||
strstr(received, "GET /search?q=hello%20world&page=2 HTTP/1.1")
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_post_sends_body_with_content_length(void **state) {
|
||||
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
|
||||
|
||||
fakeserver_t server;
|
||||
fakeServerStart(
|
||||
&server, (const uint8_t *)responseText, strlen(responseText)
|
||||
);
|
||||
|
||||
char_t url[128];
|
||||
stringFormat(
|
||||
url, sizeof(url) - 1, "http://127.0.0.1:%u/submit", server.port
|
||||
);
|
||||
|
||||
const char_t *body = "{\"a\":1}";
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_POST, url,
|
||||
NULL, 0,
|
||||
NULL, 0,
|
||||
(const uint8_t *)body, strlen(body),
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
fakeServerStop(&server);
|
||||
|
||||
assert_false(captured.errored);
|
||||
assert_int_equal(captured.status, 200);
|
||||
assert_memory_equal(captured.body, "OK", 2);
|
||||
|
||||
server.receivedData[server.receivedLength] = '\0';
|
||||
const char_t *received = (const char_t *)server.receivedData;
|
||||
assert_non_null(strstr(received, "POST /submit HTTP/1.1"));
|
||||
assert_non_null(strstr(received, "Content-Length: 7"));
|
||||
assert_non_null(strstr(received, "{\"a\":1}"));
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_put_method_is_used(void **state) {
|
||||
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
|
||||
|
||||
fakeserver_t server;
|
||||
fakeServerStart(
|
||||
&server, (const uint8_t *)responseText, strlen(responseText)
|
||||
);
|
||||
|
||||
char_t url[128];
|
||||
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/item/1", server.port);
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_PUT, url,
|
||||
NULL, 0, NULL, 0, NULL, 0,
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
fakeServerStop(&server);
|
||||
|
||||
assert_false(captured.errored);
|
||||
|
||||
server.receivedData[server.receivedLength] = '\0';
|
||||
assert_non_null(
|
||||
strstr((const char_t *)server.receivedData, "PUT /item/1 HTTP/1.1")
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_301_redirect_is_followed_automatically(void **state) {
|
||||
const char_t *finalResponseText =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
|
||||
|
||||
fakeserver_t target;
|
||||
fakeServerStart(
|
||||
&target, (const uint8_t *)finalResponseText, strlen(finalResponseText)
|
||||
);
|
||||
|
||||
char_t targetUrl[128];
|
||||
stringFormat(
|
||||
targetUrl, sizeof(targetUrl) - 1,
|
||||
"http://127.0.0.1:%u/target", target.port
|
||||
);
|
||||
|
||||
char_t redirectResponseText[256];
|
||||
stringFormat(
|
||||
redirectResponseText, sizeof(redirectResponseText) - 1,
|
||||
"HTTP/1.1 301 Moved Permanently\r\n"
|
||||
"Location: %s\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"\r\n",
|
||||
targetUrl
|
||||
);
|
||||
|
||||
fakeserver_t initial;
|
||||
fakeServerStart(
|
||||
&initial,
|
||||
(const uint8_t *)redirectResponseText,
|
||||
strlen(redirectResponseText)
|
||||
);
|
||||
|
||||
char_t url[128];
|
||||
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/old", initial.port);
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_GET, url,
|
||||
NULL, 0, NULL, 0, NULL, 0,
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
fakeServerStop(&initial);
|
||||
fakeServerStop(&target);
|
||||
|
||||
assert_false(captured.errored);
|
||||
assert_int_equal(captured.status, 200);
|
||||
assert_memory_equal(captured.body, "OK", 2);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_connection_refused_triggers_onError(void **state) {
|
||||
const int_t fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
assert_true(fd >= 0);
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memoryZero(&addr, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0;
|
||||
assert_int_equal(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0);
|
||||
|
||||
socklen_t addrLen = sizeof(addr);
|
||||
assert_int_equal(getsockname(fd, (struct sockaddr *)&addr, &addrLen), 0);
|
||||
const uint16_t port = ntohs(addr.sin_port);
|
||||
close(fd); // Nothing is listening on this port now.
|
||||
|
||||
char_t url[64];
|
||||
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/", port);
|
||||
|
||||
capturedresponse_t captured;
|
||||
memoryZero(&captured, sizeof(captured));
|
||||
|
||||
const errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_GET, url,
|
||||
NULL, 0, NULL, 0, NULL, 0,
|
||||
captureOnComplete, captureOnError, &captured
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
waitForCompletion(&captured);
|
||||
|
||||
assert_true(captured.errored);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_get_request_headers_and_body, http_setup, http_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_query_params_are_encoded_and_appended, http_setup, http_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_post_sends_body_with_content_length, http_setup, http_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_put_method_is_used, http_setup, http_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_301_redirect_is_followed_automatically, http_setup, http_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_connection_refused_triggers_onError, http_setup, http_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user