naett.c

Tiny cross-platform HTTP / HTTPS client library in C.
git clone git://git.finwo.net/lib/naett.c
Log | Files | Refs | README | LICENSE

commit fb7cb8e69ad82109925fa7479583ffc427f89c82
parent 185c071d51ccb4635a9348e29adc6be4a7511da0
Author: Erik Agsjö <erik.agsjo@gmail.com>
Date:   Mon, 15 Nov 2021 22:52:37 +0100

iOS, consistency fixes

Diffstat:
Mexample/Makefile | 2+-
Mnaett.c | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mnaett.h | 9++++-----
Msrc/amalgam.h | 1+
Msrc/naett_android.c | 1-
Msrc/naett_core.c | 7+++++++
Msrc/naett_osx.c | 7++++---
Msrc/naett_win.c | 3+++
8 files changed, 259 insertions(+), 14 deletions(-)

diff --git a/example/Makefile b/example/Makefile @@ -8,7 +8,7 @@ else ifeq ($(UNAME_S),Darwin) LDFLAGS = -framework Cocoa else ifeq ($(UNAME_S),Linux) - #LDFLAGS = -s -lGLU -lGL -lX11 + LDFLAGS = -lcurl endif endif diff --git a/naett.c b/naett.c @@ -13,6 +13,7 @@ #if __linux__ && !__ANDROID__ #define __LINUX__ 1 +#include <curl/curl.h> #endif #if __ANDROID__ @@ -80,6 +81,9 @@ typedef struct { pthread_t workerThread; int closeRequested; #endif +#if __LINUX__ + struct curl_slist* headerList; +#endif } InternalResponse; void naettPlatformInit(naettInitData initData); @@ -95,6 +99,7 @@ void naettPlatformCloseResponse(InternalResponse* res); #include <stdlib.h> #include <stddef.h> #include <string.h> +#include <assert.h> typedef struct InternalParam* InternalParamPtr; typedef void (*ParamSetter)(InternalParamPtr param, InternalRequest* req); @@ -185,7 +190,10 @@ static int defaultBodyWriter(const void* source, int bytes, void* userData) { return bytes; } +static int initialized = 0; + static void initRequest(InternalRequest* req, const char* url) { + assert(initialized); req->options.method = strdup("GET"); req->options.timeoutMS = 5000; req->url = strdup(url); @@ -201,7 +209,9 @@ static void applyOptionParams(InternalRequest* req, InternalOption* option) { // Public API void naettInit(naettInitData initData) { + assert(!initialized); naettPlatformInit(initData); + initialized = 1; } naettOption* naettMethod(const char* method) { @@ -336,6 +346,7 @@ naettReq* naettRequestWithOptions(const char* url, int numOptions, const naettOp } naettRes* naettMake(naettReq* request) { + assert(initialized); InternalRequest* req = (InternalRequest*)request; naettAlloc(InternalResponse, res); res->request = req; @@ -423,7 +434,7 @@ void naettClose(naettRes* response) { // Inlined naett_osx.c: // //#include "naett_internal.h" -#ifdef __MACOS__ +#ifdef __APPLE__ // Inlined naett_objc.h: // #ifndef NAETT_OBJC_H @@ -522,7 +533,8 @@ int naettPlatformInitRequest(InternalRequest* req) { int bytesRead = 0; if (req->options.bodyReader != NULL) { - id bodyData = objc_msgSend_t(id, NSUInteger)(class("NSMutableData"), sel("dataWithCapacity:"), sizeof(byteBuffer)); + id bodyData = + objc_msgSend_t(id, NSUInteger)(class("NSMutableData"), sel("dataWithCapacity:"), sizeof(byteBuffer)); do { bytesRead = req->options.bodyReader(byteBuffer, sizeof(byteBuffer), req->options.bodyReaderData); @@ -615,9 +627,230 @@ void naettPlatformCloseResponse(InternalResponse* res) { release(res->session); } -#endif // __MACOS__ +#endif // __APPLE__ // End of inlined naett_osx.c // +// Inlined naett_linux.c: // +//#include "naett_internal.h" + +#if __linux__ && !__ANDROID__ + +#include <curl/curl.h> +#include <assert.h> +#include <pthread.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> + +static pthread_t workerThread; +static int handleReadFD = 0; +static int handleWriteFD = 0; + +static void panic(const char* message) { + fprintf(stderr, "%sn", message); + exit(1); +} + +static void* curlWorker(void* data) { + CURLM* mc = (CURLM*)data; + int activeHandles = 0; + int messagesLeft = 1; + + struct curl_waitfd readFd = { handleReadFD, CURL_WAIT_POLLIN }; + + union { + CURL* handle; + char buf[sizeof(CURL*)]; + } newHandle; + + int newHandlePos = 0; + + while (1) { + int status = curl_multi_perform(mc, &activeHandles); + if (status != CURLM_OK) { + panic("CURL processing failure"); + } + + int readyFDs = 0; + curl_multi_wait(mc, &readFd, 1, 1000, &readyFDs); + + if (readyFDs == 0) { + usleep(100 * 1000); + } + + int bytesRead = read(handleReadFD, newHandle.buf, sizeof(newHandle.buf) - newHandlePos); + if (bytesRead > 0) { + newHandlePos += bytesRead; + } + if (newHandlePos == sizeof(newHandle.buf)) { + curl_multi_add_handle(mc, newHandle.handle); + newHandlePos = 0; + } + + struct CURLMsg* message = curl_multi_info_read(mc, &messagesLeft); + if (message && message->msg == CURLMSG_DONE) { + CURL* handle = message->easy_handle; + InternalResponse* res = NULL; + curl_easy_getinfo(handle, CURLINFO_PRIVATE, (char**)&res); + curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res->code); + res->complete = 1; + curl_easy_cleanup(handle); + } + } +} + +void naettPlatformInit(naettInitData initData) { + curl_global_init(CURL_GLOBAL_ALL); + CURLM* mc = curl_multi_init(); + int fds[2]; + if (pipe(fds) != 0) { + panic("Failed to open pipe"); + } + handleReadFD = fds[0]; + handleWriteFD = fds[1]; + + int flags = fcntl(handleReadFD, F_GETFL, 0); + fcntl(handleReadFD, F_SETFL, flags | O_NONBLOCK); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&workerThread, &attr, curlWorker, mc); +} + +int naettPlatformInitRequest(InternalRequest* req) { + return 1; +} + +static size_t readCallback(char* buffer, size_t size, size_t numItems, void* userData) { + InternalResponse* res = (InternalResponse*)userData; + InternalRequest* req = res->request; + return req->options.bodyReader(buffer, size, req->options.bodyReaderData); +} + +static size_t writeCallback(char* ptr, size_t size, size_t numItems, void* userData) { + InternalResponse* res = (InternalResponse*)userData; + InternalRequest* req = res->request; + return req->options.bodyWriter(ptr, size * numItems, req->options.bodyWriterData); +} + +#define METHOD(A, B, C) (((A) << 16) | ((B) << 8) | (C)) + +static void setupMethod(CURL* curl, const char* method) { + if (strlen(method) < 3) { + return; + } + + int methodCode = (method[0] << 16) | (method[1] << 8) | method[2]; + + switch (methodCode) { + case METHOD('G', 'E', 'T'): + case METHOD('C', 'O', 'N'): + case METHOD('O', 'P', 'T'): + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1); + break; + case METHOD('P', 'O', 'S'): + case METHOD('P', 'A', 'T'): + case METHOD('D', 'E', 'L'): + curl_easy_setopt(curl, CURLOPT_POST, 1); + break; + case METHOD('P', 'U', 'T'): + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); + break; + case METHOD('H', 'E', 'A'): + case METHOD('T', 'R', 'A'): + curl_easy_setopt(curl, CURLOPT_NOBODY, 1); + break; + } + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); +} + +static size_t headerCallback(char* buffer, size_t size, size_t nitems, void* userData) { + InternalResponse* res = (InternalResponse*) userData; + size_t headerSize = size * nitems; + + char* headerName = strndup(buffer, headerSize); + char* split = strchr(headerName, ':'); + if (split) { + *split = 0; + split++; + while (*split == ' ') { + split++; + } + char* headerValue = strdup(split); + + char* cr = strchr(headerValue, 13); + if (cr) { + *cr = 0; + } + + char* lf = strchr(headerValue, 10); + if (lf) { + *lf = 0; + } + + naettAlloc(KVLink, node); + node->next = res->headers; + node->key = headerName; + node->value = headerValue; + res->headers = node; + } + + return headerSize; +} + +void naettPlatformMakeRequest(InternalResponse* res) { + InternalRequest* req = res->request; + + CURL* c = curl_easy_init(); + curl_easy_setopt(c, CURLOPT_URL, req->url); + curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, req->options.timeoutMS); + + curl_easy_setopt(c, CURLOPT_READFUNCTION, readCallback); + curl_easy_setopt(c, CURLOPT_READDATA, res); + + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(c, CURLOPT_WRITEDATA, res); + + curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, headerCallback); + curl_easy_setopt(c, CURLOPT_HEADERDATA, res); + + setupMethod(c, req->options.method); + + struct curl_slist* headerList = NULL; + KVLink* header = req->options.headers; + size_t bufferSize = 0; + char* buffer = NULL; + while (header) { + size_t headerLength = strlen(header->key) + strlen(header->value) + 1 + 1; // colon + null + if (headerLength > bufferSize) { + bufferSize = headerLength; + buffer = (char*)realloc(buffer, bufferSize); + } + snprintf(buffer, bufferSize, "%s:%s", header->key, header->value); + headerList = curl_slist_append(headerList, buffer); + header = header->next; + } + curl_easy_setopt(c, CURLOPT_HTTPHEADER, headerList); + free(buffer); + res->headerList = headerList; + + curl_easy_setopt(c, CURLOPT_PRIVATE, res); + + write(handleWriteFD, &c, sizeof(c)); +} + +void naettPlatformFreeRequest(InternalRequest* req) { +} + +void naettPlatformCloseResponse(InternalResponse* res) { + curl_slist_free_all(res->headerList); +} + +#endif +// End of inlined naett_linux.c // + // Inlined naett_win.c: // //#include "naett_internal.h" @@ -626,6 +859,9 @@ void naettPlatformCloseResponse(InternalResponse* res) { #include <stdlib.h> #include <string.h> +void naettPlatformInit(naettInitData initData) { +} + int naettPlatformInitRequest(InternalRequest* req) { } @@ -776,7 +1012,6 @@ static void* processRequest(void* data) { jobject methodString = (*env)->NewStringUTF(env, req->options.method); voidCall(env, connection, "setRequestMethod", "(Ljava/lang/String;)V", methodString); voidCall(env, connection, "setConnectTimeout", "(I)V", req->options.timeoutMS); - voidCall(env, connection, "setReadTimeout", "(I)V", req->options.timeoutMS); voidCall(env, connection, "setInstanceFollowRedirects", "(Z)V", 1); voidCall(env, connection, "connect", "()V"); diff --git a/naett.h b/naett.h @@ -7,9 +7,6 @@ extern "C" { #if __ANDROID__ #include <jni.h> -#endif - -#if __ANDROID__ typedef JavaVM* naettInitData; #else typedef void* naettInitData; @@ -23,10 +20,12 @@ void naettInit(naettInitData initThing); typedef struct naettReq naettReq; typedef struct naettRes naettRes; -typedef struct naettOption naettOption; typedef int (*naettReadFunc)(void* dest, int bufferSize, void* userData); typedef int (*naettWriteFunc)(const void* source, int bytes, void* userData); +// Option to `naettRequest` +typedef struct naettOption naettOption; + // Sets request method. Defaults to "GET". naettOption* naettMethod(const char* method); // Adds a request header. @@ -37,7 +36,7 @@ naettOption* naettBody(const char* body, int size); naettOption* naettBodyReader(naettReadFunc reader, void* userData); // Sets a response body writer. naettOption* naettBodyWriter(naettWriteFunc writer, void* userData); -// Sets timeout, used both for connection and read. +// Sets connection timeout in milliseconds. naettOption* naettTimeout(int milliSeconds); /** diff --git a/src/amalgam.h b/src/amalgam.h @@ -1,5 +1,6 @@ #include "naett.h" #include "naett_core.c" #include "naett_osx.c" +#include "naett_linux.c" #include "naett_win.c" #include "naett_android.c" diff --git a/src/naett_android.c b/src/naett_android.c @@ -132,7 +132,6 @@ static void* processRequest(void* data) { jobject methodString = (*env)->NewStringUTF(env, req->options.method); voidCall(env, connection, "setRequestMethod", "(Ljava/lang/String;)V", methodString); voidCall(env, connection, "setConnectTimeout", "(I)V", req->options.timeoutMS); - voidCall(env, connection, "setReadTimeout", "(I)V", req->options.timeoutMS); voidCall(env, connection, "setInstanceFollowRedirects", "(Z)V", 1); voidCall(env, connection, "connect", "()V"); diff --git a/src/naett_core.c b/src/naett_core.c @@ -3,6 +3,7 @@ #include <stdlib.h> #include <stddef.h> #include <string.h> +#include <assert.h> typedef struct InternalParam* InternalParamPtr; typedef void (*ParamSetter)(InternalParamPtr param, InternalRequest* req); @@ -93,7 +94,10 @@ static int defaultBodyWriter(const void* source, int bytes, void* userData) { return bytes; } +static int initialized = 0; + static void initRequest(InternalRequest* req, const char* url) { + assert(initialized); req->options.method = strdup("GET"); req->options.timeoutMS = 5000; req->url = strdup(url); @@ -109,7 +113,9 @@ static void applyOptionParams(InternalRequest* req, InternalOption* option) { // Public API void naettInit(naettInitData initData) { + assert(!initialized); naettPlatformInit(initData); + initialized = 1; } naettOption* naettMethod(const char* method) { @@ -244,6 +250,7 @@ naettReq* naettRequestWithOptions(const char* url, int numOptions, const naettOp } naettRes* naettMake(naettReq* request) { + assert(initialized); InternalRequest* req = (InternalRequest*)request; naettAlloc(InternalResponse, res); res->request = req; diff --git a/src/naett_osx.c b/src/naett_osx.c @@ -1,6 +1,6 @@ #include "naett_internal.h" -#ifdef __MACOS__ +#ifdef __APPLE__ #include "naett_objc.h" #include <stdlib.h> @@ -37,7 +37,8 @@ int naettPlatformInitRequest(InternalRequest* req) { int bytesRead = 0; if (req->options.bodyReader != NULL) { - id bodyData = objc_msgSend_t(id, NSUInteger)(class("NSMutableData"), sel("dataWithCapacity:"), sizeof(byteBuffer)); + id bodyData = + objc_msgSend_t(id, NSUInteger)(class("NSMutableData"), sel("dataWithCapacity:"), sizeof(byteBuffer)); do { bytesRead = req->options.bodyReader(byteBuffer, sizeof(byteBuffer), req->options.bodyReaderData); @@ -130,4 +131,4 @@ void naettPlatformCloseResponse(InternalResponse* res) { release(res->session); } -#endif // __MACOS__ +#endif // __APPLE__ diff --git a/src/naett_win.c b/src/naett_win.c @@ -5,6 +5,9 @@ #include <stdlib.h> #include <string.h> +void naettPlatformInit(naettInitData initData) { +} + int naettPlatformInitRequest(InternalRequest* req) { }