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 c9523b511fb3a3e1edc8e08a8f3577fb64dd1000
parent 5853eb1f7f6458a277f1264e2305b33cf2712aa9
Author: Erik Agsjö <erik.agsjo@gmail.com>
Date:   Mon,  6 Dec 2021 00:04:13 +0100

First win version

Diffstat:
Mexample/Makefile | 5+++--
Mnaett.c | 287+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mnaett.h | 2++
Msrc/bundle.sh | 2+-
Msrc/naett_internal.h | 12++++++++++++
Msrc/naett_win.c | 258++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 554 insertions(+), 12 deletions(-)

diff --git a/example/Makefile b/example/Makefile @@ -1,12 +1,13 @@ -CFLAGS = -I.. -g -Wall -pedantic -Wno-gnu-zero-variadic-macro-arguments +CFLAGS = -I.. -g -Wall -pedantic #CFLAGS = -O2 ifeq ($(OS),Windows_NT) - + LDFLAGS = -lwinhttp else UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) LDFLAGS = -framework Cocoa + CFLAGS += -Wno-gnu-zero-variadic-macro-arguments else ifeq ($(UNAME_S),Linux) LDFLAGS = -lcurl -lpthread endif diff --git a/naett.c b/naett.c @@ -8,6 +8,7 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include <windows.h> +#include <winhttp.h> #define __WINDOWS__ 1 #endif @@ -66,6 +67,13 @@ typedef struct { #if __ANDROID__ jobject urlObject; #endif +#if __WINDOWS__ + HINTERNET session; + HINTERNET connection; + HINTERNET request; + LPWSTR host; + LPWSTR resource; +#endif } InternalRequest; typedef struct { @@ -84,6 +92,10 @@ typedef struct { #if __LINUX__ struct curl_slist* headerList; #endif +#if __WINDOWS__ + char buffer[10240]; + size_t bytesLeft; +#endif } InternalResponse; void naettPlatformInit(naettInitData initData); @@ -503,14 +515,17 @@ void naettClose(naettRes* response) { #define sel(NAME) sel_registerName(NAME) #define class(NAME) ((id)objc_getClass(NAME)) -#define makeClass(NAME, SUPER) objc_allocateClassPair((Class)objc_getClass(SUPER), NAME, 0) +#define makeClass(NAME, SUPER) \ + objc_allocateClassPair((Class)objc_getClass(SUPER), NAME, 0) // Check here to get the signature right: // https://nshipster.com/type-encodings/ // https://ko9.org/posts/encode-types/ -#define addMethod(CLASS, NAME, IMPL, SIGNATURE) if (!class_addMethod(CLASS, sel(NAME), (IMP) (IMPL), (SIGNATURE))) assert(false) +#define addMethod(CLASS, NAME, IMPL, SIGNATURE) \ + if (!class_addMethod(CLASS, sel(NAME), (IMP) (IMPL), (SIGNATURE))) assert(false) -#define addIvar(CLASS, NAME, SIZE, SIGNATURE) if (!class_addIvar(CLASS, NAME, SIZE, rint(log2(SIZE)), SIGNATURE)) assert(false) +#define addIvar(CLASS, NAME, SIZE, SIGNATURE) \ + if (!class_addIvar(CLASS, NAME, SIZE, rint(log2(SIZE)), SIGNATURE)) assert(false) #define objc_alloc(CLASS) objc_msgSend_id(class(CLASS), sel("alloc")) #define autorelease(OBJ) objc_msgSend_void(OBJ, sel("autorelease")) @@ -535,7 +550,7 @@ void naettClose(naettRes* response) { #ifdef DEBUG static void _showPools(const char* context) { - fprintf(stderr, "NSAutoreleasePool@%s:n", context); + fprintf(stderr, "NSAutoreleasePool@%s:\n", context); objc_msgSend_void(class("NSAutoreleasePool"), sel("showPools")); } #define showPools(x) _showPools((x)) @@ -706,7 +721,7 @@ static int handleReadFD = 0; static int handleWriteFD = 0; static void panic(const char* message) { - fprintf(stderr, "%sn", message); + fprintf(stderr, "%s\n", message); exit(1); } @@ -920,18 +935,276 @@ void naettPlatformCloseResponse(InternalResponse* res) { #ifdef __WINDOWS__ #include <stdlib.h> +#include <stdio.h> #include <string.h> +#include <winhttp.h> + +void naettPlatformInit(naettInitData initData) { +} + +char* winToUTF8(LPWSTR source) { + int length = WideCharToMultiByte(CP_UTF8, 0, source, -1, NULL, 0, NULL, NULL); + char* chars = (char*)malloc(length); + int result = WideCharToMultiByte(CP_UTF8, 0, source, -1, chars, length, NULL, NULL); + if (!result) { + free(chars); + return NULL; + } + return chars; +} + +LPWSTR winFromUTF8(const char* source) { + int length = MultiByteToWideChar(CP_UTF8, 0, source, -1, NULL, 0); + LPWSTR chars = (LPWSTR)malloc(length * sizeof(WCHAR)); + int result = MultiByteToWideChar(CP_UTF8, 0, source, -1, chars, length); + if (!result) { + free(chars); + return NULL; + } + return chars; +} + +#define ASPRINTF(result, fmt, ...) \ + { \ + size_t len = snprintf(NULL, 0, fmt, __VA_ARGS__); \ + *(result) = (char*)malloc(len + 1); \ + snprintf(*(result), len + 1, fmt, __VA_ARGS__); \ + } + +LPCWSTR packHeaders(InternalRequest* req) { + char* packed = strdup(""); -void naettPlatformInit(naettInitData initData) { + KVLink* node = req->options.headers; + while (node != NULL) { + char* update; + ASPRINTF(&update, "%s%s=%s%s", packed, node->key, node->value, node->next ? "\r\n" : ""); + free(packed); + packed = update; + node = node->next; + } + + LPCWSTR winHeaders = winFromUTF8(packed); + free(packed); + return winHeaders; +} + +static void unpackHeaders(InternalResponse* res, LPWSTR packed) { + int len = 0; + while ((len = wcslen(packed)) != 0) { + char* header = winToUTF8(packed); + char* split = strchr(header, ':'); + if (split) { + *split = 0; + split++; + while (*split == ' ') { + split++; + } + naettAlloc(KVLink, node); + node->key = strdup(header); + node->value = strdup(split); + node->next = res->headers; + res->headers = node; + printf("Unpacked header: %s=%s\n", node->key, node->value); + } + free(header); + packed += len + 1; + } +} + +static void callback(HINTERNET request, + DWORD_PTR context, + DWORD status, + LPVOID statusInformation, + DWORD statusInfoLength) { + InternalResponse* res = (InternalResponse*)context; + + switch (status) { + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: { + DWORD bufSize = 0; + WinHttpQueryHeaders(request, + WINHTTP_QUERY_RAW_HEADERS, + WINHTTP_HEADER_NAME_BY_INDEX, + NULL, + &bufSize, + WINHTTP_NO_HEADER_INDEX); + LPWSTR buffer = (LPWSTR)malloc(bufSize); + WinHttpQueryHeaders(request, + WINHTTP_QUERY_RAW_HEADERS, + WINHTTP_HEADER_NAME_BY_INDEX, + buffer, + &bufSize, + WINHTTP_NO_HEADER_INDEX); + unpackHeaders(res, buffer); + free(buffer); + + DWORD statusCode = 0; + DWORD statusCodeSize = sizeof(statusCode); + + WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &statusCode, + &statusCodeSize, + WINHTTP_NO_HEADER_INDEX); + res->code = statusCode; + + if (!WinHttpQueryDataAvailable(request, NULL)) { + res->code = naettProtocolError; + res->complete = 1; + } + } break; + + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: { + DWORD* available = (DWORD*)statusInformation; + res->bytesLeft = *available; + if (res->bytesLeft == 0) { + res->complete = 1; + break; + } + + size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer)); + if (!WinHttpReadData(request, res->buffer, bytesToRead, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + }break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: { + size_t bytesRead = statusInfoLength; + + InternalRequest* req = res->request; + if (req->options.bodyWriter(res->buffer, bytesRead, req->options.bodyWriterData) != bytesRead) { + res->code = naettReadError; + res->complete = 1; + } + + res->bytesLeft -= bytesRead; + if (res->bytesLeft > 0) { + size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer)); + if (!WinHttpReadData(request, res->buffer, bytesToRead, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + } else { + if (!WinHttpQueryDataAvailable(request, NULL)) { + res->code = naettProtocolError; + res->complete = 1; + } + } + } break; + + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: { + int bytesRead = res->request->options.bodyReader( + res->buffer, sizeof(res->buffer), res->request->options.bodyReaderData); + if (bytesRead) { + WinHttpWriteData(request, res->buffer, bytesRead, NULL); + } else { + if (!WinHttpReceiveResponse(request, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + } + } break; + + // + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: { + WINHTTP_ASYNC_RESULT* result = (WINHTTP_ASYNC_RESULT*)statusInformation; + switch (result->dwResult) { + case API_RECEIVE_RESPONSE: + case API_QUERY_DATA_AVAILABLE: + case API_READ_DATA: + res->code = naettReadError; + break; + case API_WRITE_DATA: + res->code = naettWriteError; + break; + case API_SEND_REQUEST: + res->code = naettConnectionError; + break; + default: + res->code = naettGenericError; + } + + res->complete = 1; + } break; + } } int naettPlatformInitRequest(InternalRequest* req) { + LPWSTR url = winFromUTF8(req->url); + printf("Connecting to %S\n", url); + + URL_COMPONENTS components; + ZeroMemory(&components, sizeof(components)); + components.dwStructSize = sizeof(components); + components.dwSchemeLength = (DWORD)-1; + components.dwHostNameLength = (DWORD)-1; + components.dwUrlPathLength = (DWORD)-1; + components.dwExtraInfoLength = (DWORD)-1; + BOOL cracked = WinHttpCrackUrl(url, 0, 0, &components); + + if (!cracked) { + free(url); + return 0; + } + + req->host = wcsncat(wcsdup(L""), components.lpszHostName, components.dwHostNameLength); + req->resource = wcsncat(wcsdup(L""), components.lpszUrlPath, components.dwUrlPathLength); + free(url); + + req->session = WinHttpOpen( + L"Naett/1.0", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); + + if (!req->session) { + return 0; + } + + WinHttpSetStatusCallback(req->session, callback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0); + + req->connection = WinHttpConnect(req->session, req->host, components.nPort, 0); + if (!req->connection) { + WinHttpCloseHandle(req->session); + return 0; + } + + LPWSTR verb = winFromUTF8(req->options.method); + req->request = WinHttpOpenRequest(req->connection, + verb, + req->resource, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); + free(verb); + if (!req->request) { + WinHttpCloseHandle(req->session); + WinHttpCloseHandle(req->connection); + return 0; + } + + LPCWSTR headers = packHeaders(req); + WinHttpAddRequestHeaders(req->request, headers, 0, WINHTTP_ADDREQ_FLAG_ADD); + printf("Request headers: %S\n", headers); + free((LPWSTR)headers); + + return 1; } -void naettPlatformMakeRequest(InternalRequest* req, InternalResponse* res) { +void naettPlatformMakeRequest(InternalResponse* res) { + if (!WinHttpSendRequest(res->request->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, NULL, 0, 0, (DWORD_PTR)res)) { + printf("Failed to send request\n"); + res->code = naettConnectionError; + res->complete = 1; + } } void naettPlatformFreeRequest(InternalRequest* req) { + WinHttpCloseHandle(req->session); + WinHttpCloseHandle(req->connection); + WinHttpCloseHandle(req->request); + free(req->host); + free(req->resource); } void naettPlatformCloseResponse(InternalResponse* res) { diff --git a/naett.h b/naett.h @@ -79,6 +79,8 @@ enum naettStatus { naettConnectionError = -1, naettProtocolError = -2, naettReadError = -3, + naettWriteError = -4, + naettGenericError = -5, naettProcessing = 0, }; diff --git a/src/bundle.sh b/src/bundle.sh @@ -18,7 +18,7 @@ bundle() { echo "// Inlined $file: //" >> "$target" IFS="" - while read line; do + while read -r line; do if [[ $line =~ ^\#include\ \"(.+)\" && -f ${BASH_REMATCH[1]} ]]; then include=${BASH_REMATCH[1]} if bundle $include; then diff --git a/src/naett_internal.h b/src/naett_internal.h @@ -4,6 +4,7 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include <windows.h> +#include <winhttp.h> #define __WINDOWS__ 1 #endif @@ -62,6 +63,13 @@ typedef struct { #if __ANDROID__ jobject urlObject; #endif +#if __WINDOWS__ + HINTERNET session; + HINTERNET connection; + HINTERNET request; + LPWSTR host; + LPWSTR resource; +#endif } InternalRequest; typedef struct { @@ -80,6 +88,10 @@ typedef struct { #if __LINUX__ struct curl_slist* headerList; #endif +#if __WINDOWS__ + char buffer[10240]; + size_t bytesLeft; +#endif } InternalResponse; void naettPlatformInit(naettInitData initData); diff --git a/src/naett_win.c b/src/naett_win.c @@ -3,18 +3,272 @@ #ifdef __WINDOWS__ #include <stdlib.h> +#include <stdio.h> #include <string.h> +#include <winhttp.h> -void naettPlatformInit(naettInitData initData) { +void naettPlatformInit(naettInitData initData) { +} + +char* winToUTF8(LPWSTR source) { + int length = WideCharToMultiByte(CP_UTF8, 0, source, -1, NULL, 0, NULL, NULL); + char* chars = (char*)malloc(length); + int result = WideCharToMultiByte(CP_UTF8, 0, source, -1, chars, length, NULL, NULL); + if (!result) { + free(chars); + return NULL; + } + return chars; +} + +LPWSTR winFromUTF8(const char* source) { + int length = MultiByteToWideChar(CP_UTF8, 0, source, -1, NULL, 0); + LPWSTR chars = (LPWSTR)malloc(length * sizeof(WCHAR)); + int result = MultiByteToWideChar(CP_UTF8, 0, source, -1, chars, length); + if (!result) { + free(chars); + return NULL; + } + return chars; +} + +#define ASPRINTF(result, fmt, ...) \ + { \ + size_t len = snprintf(NULL, 0, fmt, __VA_ARGS__); \ + *(result) = (char*)malloc(len + 1); \ + snprintf(*(result), len + 1, fmt, __VA_ARGS__); \ + } + +LPCWSTR packHeaders(InternalRequest* req) { + char* packed = strdup(""); + + KVLink* node = req->options.headers; + while (node != NULL) { + char* update; + ASPRINTF(&update, "%s%s=%s%s", packed, node->key, node->value, node->next ? "\r\n" : ""); + free(packed); + packed = update; + node = node->next; + } + + LPCWSTR winHeaders = winFromUTF8(packed); + free(packed); + return winHeaders; +} + +static void unpackHeaders(InternalResponse* res, LPWSTR packed) { + int len = 0; + while ((len = wcslen(packed)) != 0) { + char* header = winToUTF8(packed); + char* split = strchr(header, ':'); + if (split) { + *split = 0; + split++; + while (*split == ' ') { + split++; + } + naettAlloc(KVLink, node); + node->key = strdup(header); + node->value = strdup(split); + node->next = res->headers; + res->headers = node; + } + free(header); + packed += len + 1; + } +} + +static void callback(HINTERNET request, + DWORD_PTR context, + DWORD status, + LPVOID statusInformation, + DWORD statusInfoLength) { + InternalResponse* res = (InternalResponse*)context; + + switch (status) { + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: { + DWORD bufSize = 0; + WinHttpQueryHeaders(request, + WINHTTP_QUERY_RAW_HEADERS, + WINHTTP_HEADER_NAME_BY_INDEX, + NULL, + &bufSize, + WINHTTP_NO_HEADER_INDEX); + LPWSTR buffer = (LPWSTR)malloc(bufSize); + WinHttpQueryHeaders(request, + WINHTTP_QUERY_RAW_HEADERS, + WINHTTP_HEADER_NAME_BY_INDEX, + buffer, + &bufSize, + WINHTTP_NO_HEADER_INDEX); + unpackHeaders(res, buffer); + free(buffer); + + DWORD statusCode = 0; + DWORD statusCodeSize = sizeof(statusCode); + + WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &statusCode, + &statusCodeSize, + WINHTTP_NO_HEADER_INDEX); + res->code = statusCode; + + if (!WinHttpQueryDataAvailable(request, NULL)) { + res->code = naettProtocolError; + res->complete = 1; + } + } break; + + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: { + DWORD* available = (DWORD*)statusInformation; + res->bytesLeft = *available; + if (res->bytesLeft == 0) { + res->complete = 1; + break; + } + + size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer)); + if (!WinHttpReadData(request, res->buffer, bytesToRead, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + }break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: { + size_t bytesRead = statusInfoLength; + + InternalRequest* req = res->request; + if (req->options.bodyWriter(res->buffer, bytesRead, req->options.bodyWriterData) != bytesRead) { + res->code = naettReadError; + res->complete = 1; + } + + res->bytesLeft -= bytesRead; + if (res->bytesLeft > 0) { + size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer)); + if (!WinHttpReadData(request, res->buffer, bytesToRead, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + } else { + if (!WinHttpQueryDataAvailable(request, NULL)) { + res->code = naettProtocolError; + res->complete = 1; + } + } + } break; + + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: { + int bytesRead = res->request->options.bodyReader( + res->buffer, sizeof(res->buffer), res->request->options.bodyReaderData); + if (bytesRead) { + WinHttpWriteData(request, res->buffer, bytesRead, NULL); + } else { + if (!WinHttpReceiveResponse(request, NULL)) { + res->code = naettReadError; + res->complete = 1; + } + } + } break; + + // + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: { + WINHTTP_ASYNC_RESULT* result = (WINHTTP_ASYNC_RESULT*)statusInformation; + switch (result->dwResult) { + case API_RECEIVE_RESPONSE: + case API_QUERY_DATA_AVAILABLE: + case API_READ_DATA: + res->code = naettReadError; + break; + case API_WRITE_DATA: + res->code = naettWriteError; + break; + case API_SEND_REQUEST: + res->code = naettConnectionError; + break; + default: + res->code = naettGenericError; + } + + res->complete = 1; + } break; + } } int naettPlatformInitRequest(InternalRequest* req) { + LPWSTR url = winFromUTF8(req->url); + + URL_COMPONENTS components; + ZeroMemory(&components, sizeof(components)); + components.dwStructSize = sizeof(components); + components.dwSchemeLength = (DWORD)-1; + components.dwHostNameLength = (DWORD)-1; + components.dwUrlPathLength = (DWORD)-1; + components.dwExtraInfoLength = (DWORD)-1; + BOOL cracked = WinHttpCrackUrl(url, 0, 0, &components); + + if (!cracked) { + free(url); + return 0; + } + + req->host = wcsncat(wcsdup(L""), components.lpszHostName, components.dwHostNameLength); + req->resource = wcsncat(wcsdup(L""), components.lpszUrlPath, components.dwUrlPathLength); + free(url); + + req->session = WinHttpOpen( + L"Naett/1.0", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); + + if (!req->session) { + return 0; + } + + WinHttpSetStatusCallback(req->session, callback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0); + + req->connection = WinHttpConnect(req->session, req->host, components.nPort, 0); + if (!req->connection) { + WinHttpCloseHandle(req->session); + return 0; + } + + LPWSTR verb = winFromUTF8(req->options.method); + req->request = WinHttpOpenRequest(req->connection, + verb, + req->resource, + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); + free(verb); + if (!req->request) { + WinHttpCloseHandle(req->session); + WinHttpCloseHandle(req->connection); + return 0; + } + + LPCWSTR headers = packHeaders(req); + WinHttpAddRequestHeaders(req->request, headers, 0, WINHTTP_ADDREQ_FLAG_ADD); + free((LPWSTR)headers); + + return 1; } -void naettPlatformMakeRequest(InternalRequest* req, InternalResponse* res) { +void naettPlatformMakeRequest(InternalResponse* res) { + if (!WinHttpSendRequest(res->request->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, NULL, 0, 0, (DWORD_PTR)res)) { + res->code = naettConnectionError; + res->complete = 1; + } } void naettPlatformFreeRequest(InternalRequest* req) { + WinHttpCloseHandle(req->session); + WinHttpCloseHandle(req->connection); + WinHttpCloseHandle(req->request); + free(req->host); + free(req->resource); } void naettPlatformCloseResponse(InternalResponse* res) {