From 1287f8015dd7306dedd9a818a17afff03d179f90 Mon Sep 17 00:00:00 2001 From: Mitya Selivanov Date: Fri, 29 Dec 2023 03:58:11 +0100 Subject: HTTP 1.1 impl, untested --- LICENSE | 5 +- README | 4 + TODO | 4 +- source/kit/_lib.c | 1 - source/kit/http1.h | 466 +++++++++++++++++++++++++++++++++----------- source/kit/math.h | 5 +- source/kit/sockets.h | 1 + source/kit/string_builder.c | 1 - source/kit/string_builder.h | 50 +++++ source/tests/_exe.c | 1 + source/tests/http1.test.c | 6 + 11 files changed, 425 insertions(+), 119 deletions(-) delete mode 100644 source/kit/string_builder.c create mode 100644 source/tests/http1.test.c diff --git a/LICENSE b/LICENSE index 6281517..bacfeb0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ -Copyright (c) 2012 yohhoy -Copyright (c) 2022 Yonggang Luo Copyright (c) 2022-2023 Mitya Selivanov +Copyright (c) 2022 Yonggang Luo +Copyright (c) 2016 Christian C. Sachs +Copyright (c) 2012 yohhoy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README b/README index afa93cf..3dfc507 100644 --- a/README +++ b/README @@ -28,6 +28,7 @@ Features - Async functions (coroutines) - Vector and color math - XML + - HTTP 1 client Preprocessor options - KIT_DISABLE_SHORT_NAMES - Disable short names for functions and types. @@ -40,5 +41,8 @@ Preprocessor options C11 threads implementation was forked from the Mesa source code. * https://gitlab.freedesktop.org/mesa/mesa +HTTP1 implementation was forked from the picohttpclient +* https://github.com/csachs/picohttpclient + Folder "/include" contains single header-only version of this library. Use "#define KIT_IMPLEMENTATION" to enable implementation code. diff --git a/TODO b/TODO index 87f872a..97823a2 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,8 @@ To-Do - Cryptography - hmac_sha256 https://github.com/h5p9sl/hmac_sha256 - - ecdh https://github.com/kokke/tiny-ECDH-c + - ecdh https://github.com/kokke/tiny-ECDH-c + - TLS - Allocators - Memory pool - Memory stack @@ -21,4 +22,3 @@ To-Do - Utils - parse, print - String builder - - HTTP 1 client diff --git a/source/kit/_lib.c b/source/kit/_lib.c index 53bd818..3b5d97a 100644 --- a/source/kit/_lib.c +++ b/source/kit/_lib.c @@ -1,7 +1,6 @@ #include "allocator.c" #include "array_ref.c" #include "dynamic_array.c" -#include "string_builder.c" #include "file.c" #include "input_buffer.c" #include "input_stream.c" diff --git a/source/kit/http1.h b/source/kit/http1.h index 0493d46..f4e5fc6 100644 --- a/source/kit/http1.h +++ b/source/kit/http1.h @@ -1,3 +1,7 @@ +// TODO +// - Error handling +// - HTTPS support + #ifndef KIT_HTTP1_H #define KIT_HTTP1_H @@ -5,24 +9,19 @@ #include "string_builder.h" #include "sockets.h" +#include +#include + #ifdef __cplusplus extern "C" { #endif -// TODO -// - Add source to README -// https://github.com/csachs/picohttpclient/tree/master -// - -// FIXME -// Rewrite in C. -// - #define KIT_HTTP1_CONTENT_TYPE \ "Content-Type: text/plain; version=0.0.4; charset=utf-8"; #define KIT_HTTP1_NEWLINE "\r\n" #define KIT_HTTP1_SPACE " " #define KIT_HTTP1_HEADER_SEPARATOR ": " +#define KIT_HTTP1_BUFFER_SIZE 8192 enum { KIT_HTTP1_OPTIONS, @@ -36,24 +35,24 @@ enum { }; typedef struct { - kit_str_builder_t key; - kit_str_builder_t value; + kit_str_t key; + kit_str_t value; } kit_http1_str_pair_t; typedef KIT_DA(kit_http1_str_pair_t) kit_http1_str_map_t; typedef struct { - kit_str_builder_t str; - i64 position; + kit_str_t str; + i64 position; } kit_http1_tok_t; typedef struct { - kit_str_builder_t protocol; - kit_str_builder_t host; - kit_str_builder_t port; - kit_str_builder_t address; - kit_str_builder_t query_string; - kit_str_builder_t hash; + kit_str_t protocol; + kit_str_t host; + kit_str_t port; + kit_str_t address; + kit_str_t query_string; + kit_str_t hash; kit_http1_str_map_t parameters; } kit_http1_uri_t; @@ -66,93 +65,188 @@ typedef struct { kit_str_builder_t body; } kit_http1_response_t; -static kit_str_builder_t kit_http1_tok_next(kit_http1_tok_t *tok, - kit_str_t search, - i8 return_tail) { - i64 hit = str.find(search, position); - if (hit == string::npos) { - if (return_tail) { - return tail(); - } else { - return ""; - } - } +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunknown-pragmas" +#endif - i64 previous_position = position; - position = hit + search.length(); +static kit_str_t kit_http1_tok_tail(kit_http1_tok_t *tok) { + assert(tok != NULL); - return str.substr(previous_position, hit - previous_position); -} + if (tok == NULL) + return (kit_str_t) { .size = 0, .values = NULL }; -static kit_str_builder_t kit_http1_tok_tail(kit_http1_tok_t *tok) { - i64 previous_position = position; - position = str.length(); - return str.substr(previous_position); + i64 previous_position = tok->position; + tok->position = tok->str.size; + + return (kit_str_t) { .size = tok->position - previous_position, + .values = tok->str.values + + previous_position }; } -static void kit_http1_uri_parse_params(kit_http1_uri_t *uri) { - kit_http1_tok_t qt(query_string); - for (;;) { - kit_str_builder_t key = qt.next("=", 0); - if (key == "") +static kit_str_t kit_http1_tok_next(kit_http1_tok_t *tok, + kit_str_t search, + i8 return_tail) { + assert(tok != NULL); + if (tok == NULL) + return (kit_str_t) { .size = 0, .values = NULL }; + + i64 hit = tok->position; + + while (hit + search.size <= tok->str.size) { + kit_str_t s = { .size = search.size, + .values = tok->str.values + hit }; + if (KIT_AR_EQUAL(s, search)) break; - parameters[key] = qt.next("&", 1); } + + if (hit + search.size > tok->str.size) { + if (return_tail) + return kit_http1_tok_tail(tok); + else + return (kit_str_t) { .size = 0, .values = NULL }; + } + + i64 previous_position = tok->position; + tok->position = hit + search.size; + + return (kit_str_t) { .size = hit - previous_position, + .values = tok->str.values + + previous_position }; } -static void kit_http1_uri_init(kit_http1_uri_t *uri, - kit_str_builder_t input, - i8 should_use_params) { - kit_http1_tok_t t = kit_http1_tok_t(input); - protocol = t.next("://", 0); - kit_str_builder_t host_port_str = t.next("/", 0); +// TODO +// - Return error status +static void kit_http1_uri_init(kit_http1_uri_t *uri, kit_str_t input, + kit_allocator_t *alloc) { + static const char port_80[] = "80"; + + assert(uri != NULL); + if (uri == NULL) + return; - kit_http1_tok_t host_port(host_port_str); + memset(uri, 0, sizeof *uri); - host = host_port.next(host_port_str[0] == '[' ? "]:" : ":", 1); + kit_http1_tok_t input_tok = { .str = input, .position = 0 }; - if (host[0] == '[') - host = host.substr(1, host.size() - 1); + uri->protocol = kit_http1_tok_next(&input_tok, SZ("://"), 0); + kit_str_t host_port_str = kit_http1_tok_next(&input_tok, SZ("/"), + 0); - port = host_port.tail(); - if (port.empty()) - port = "80"; + // FIXME + // Handle invalid host port error + assert(host_port_str.size > 0); + if (host_port_str.size == 0) + return; - address = t.next("?", 1); - query_string = t.next("#", 1); + kit_http1_tok_t host_port_tok = { .str = host_port_str, + .position = 0 }; - hash = t.tail(); + uri->host = kit_http1_tok_next( + &host_port_tok, + host_port_str.values[0] == '[' ? SZ("]:") : SZ(":"), 1); - if (should_use_params) { - kit_http1_uri_parse_params(); + if (uri->host.values[0] == '[') + uri->host = (kit_str_t) { .size = uri->host.size - 1, + .values = uri->host.values + 1 }; + + // TODO + // - HTTPS default port 443 + uri->port = kit_http1_tok_tail(&host_port_tok); + if (uri->port.size == 0) + uri->port = SZ(port_80); + + uri->address = kit_http1_tok_next(&input_tok, SZ("?"), 1); + uri->query_string = kit_http1_tok_next(&input_tok, SZ("#"), 1); + + uri->hash = kit_http1_tok_tail(&input_tok); + + KIT_DA_INIT(uri->parameters, 0, alloc); + + kit_http1_tok_t query_tok = { + .str = { .size = uri->query_string.size, + .values = uri->query_string.values }, + .position = 0 + }; + + for (;;) { + kit_str_t key = kit_http1_tok_next(&query_tok, SZ("="), 0); + if (key.size == 0) + break; + kit_str_t value = kit_http1_tok_next(&query_tok, SZ("&"), 1); + + i64 index = uri->parameters.size; + + KIT_DA_RESIZE(uri->parameters, index + 1); + + assert(uri->parameters.size == index + 1); + if (uri->parameters.size != index + 1) + // FIXME + // Handle bad alloc error + break; + + uri->parameters.values[index] = (kit_http1_str_pair_t) { + .key = key, .value = value + }; } } -static str_t kit_http1_method_to_str(i32 method) { - static str_t methods[] = { SZ("OPTIONS"), SZ("GET"), - SZ("HEAD"), SZ("POST"), - SZ("PUT"), SZ("DELETE"), - SZ("TRACE"), SZ("CONNECT") }; +static void kit_http1_uri_destroy(kit_http1_uri_t *uri) { + // TODO +} + +static kit_str_t kit_http1_method_to_str(i32 method) { + static kit_str_t methods[] = { { .size = 7, .values = "OPTIONS" }, + { .size = 3, .values = "GET" }, + { .size = 4, .values = "HEAD" }, + { .size = 4, .values = "POST" }, + { .size = 3, .values = "PUT" }, + { .size = 6, .values = "DELETE" }, + { .size = 5, .values = "TRACE" }, + { .size = 7, .values = "CONNECT" } }; + + assert(method >= 0 && method < sizeof methods / sizeof *methods); + if (method < 0 || method >= sizeof methods / sizeof *methods) + return (kit_str_t) { .size = 0, .values = NULL }; + return methods[method]; } +// TODO +// - Return error status static socket_t kit_http1_connect_to_uri(kit_http1_uri_t *uri) { + assert(uri != NULL); + if (uri == NULL) + return INVALID_SOCKET; + struct addrinfo hints, *result, *rp; - memset(&hints, 0, sizeof(addrinfo)); + memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - i32 getaddrinfo_result = getaddrinfo( - uri.host.c_str(), uri.port.c_str(), &hints, &result); + char host_str[128]; + char port_str[128]; + + assert(uri->host.size < sizeof host_str); + memcpy(host_str, uri->host.values, uri->host.size); + host_str[uri->host.size] = '\0'; + + assert(uri->port.size < sizeof port_str); + memcpy(port_str, uri->port.values, uri->port.size); + port_str[uri->port.size] = '\0'; + + i32 getaddrinfo_result = getaddrinfo(host_str, port_str, &hints, + &result); if (getaddrinfo_result != 0) return INVALID_SOCKET; socket_t fd = INVALID_SOCKET; - for (rp = result; rp != nullptr; rp = rp->ai_next) { + for (rp = result; rp != NULL; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd == INVALID_SOCKET) @@ -175,74 +269,226 @@ static socket_t kit_http1_connect_to_uri(kit_http1_uri_t *uri) { return fd; } -static kit_str_builder_t kit_http1_buffered_read(socket_t fd) { - i64 initial_factor = 4, buffer_increment_size = 8192, - buffer_size = 0, bytes_read = 0; - kit_str_builder_t buffer; +// TODO +// - Return error status +static kit_str_builder_t kit_http1_buffered_read( + socket_t fd, kit_allocator_t *alloc) { + kit_str_builder_t buf; + KIT_DA_INIT(buf, KIT_HTTP1_BUFFER_SIZE, alloc); + + // FIXME + // Handle bad alloc error + assert(buf.size == KIT_HTTP1_BUFFER_SIZE); + if (buf.size != KIT_HTTP1_BUFFER_SIZE) + return buf; - buffer.resize(initial_factor * buffer_increment_size); + i64 offset = 0; - bytes_read = recv( - fd, ((char *) buffer.c_str()) + buffer_size, - static_cast(buffer.size() - buffer_size), 0); + for (;;) { + i64 chunk = buf.size - offset; + i64 n = recv(fd, buf.values + offset, (socklen_t) chunk, 0); + if (n < chunk) + break; + offset += n; + KIT_DA_RESIZE(buf, offset + KIT_HTTP1_BUFFER_SIZE); - buffer_size += bytes_read; + // FIXME + // Handle bad alloc error + assert(buf.size == offset + KIT_HTTP1_BUFFER_SIZE); + if (buf.size != offset + KIT_HTTP1_BUFFER_SIZE) + break; + } - buffer.resize(buffer_size); - return buffer; + return buf; } +// TODO +// - Return error status static kit_http1_response_t kit_http1_request( - i32 method, kit_http1_uri_t *uri, kit_str_builder_t *body) { + i32 method, kit_http1_uri_t *uri, kit_str_t body, + kit_allocator_t *alloc) { socket_t fd = kit_http1_connect_to_uri(uri); + assert(fd != INVALID_SOCKET); if (fd == INVALID_SOCKET) { - return { .success = 0 }; + kit_http1_response_t res; + memset(&res, 0, sizeof res); + res.success = 0; + return res; + } + + kit_str_builder_t req; + + // reserve + KIT_DA_INIT(req, 512, alloc); + + assert(req.size == 512); + if (req.size != 512) { + // bad alloc + // + + closesocket(fd); + + kit_http1_response_t res; + memset(&res, 0, sizeof res); + res.success = 0; + return res; + } + + req.size = 0; + + // FIXME + // Handle errors + // + + kit_str_append(&req, kit_http1_method_to_str(method)); + kit_str_append(&req, SZ(" /")); + kit_str_append(&req, uri->address); + kit_str_append(&req, + uri->query_string.size == 0 ? SZ("") : SZ("?")); + kit_str_append(&req, uri->query_string); + kit_str_append(&req, SZ(" HTTP/1.1")); + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + + kit_str_append(&req, SZ("Host: ")); + kit_str_append(&req, uri->host); + kit_str_append(&req, SZ(":")); + kit_str_append(&req, uri->port); + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + + kit_str_append(&req, SZ("Accept: */*")); + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + + kit_str_append( + &req, + SZ("Content-Type: text/plain; version=0.0.4; charset=utf-8")); + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + + // FIXME + // Use our own print instead + char content_length_str[11]; + sprintf(content_length_str, "%lld", body.size); + + kit_str_append(&req, SZ("Content-Length: ")); + kit_str_append(&req, + (kit_str_t) { .size = strlen(content_length_str), + .values = content_length_str }); + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + + kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE)); + kit_str_append(&req, body); + + // Append '\0' at the end + // + { + i64 n = req.size; + KIT_DA_RESIZE(req, n + 1); + + assert(req.size == n + 1); + if (req.size != n + 1) { + KIT_DA_DESTROY(req); + closesocket(fd); + + kit_http1_response_t res; + memset(&res, 0, sizeof res); + res.success = 0; + return res; + } + + req.size = n; + req.values[n] = '\0'; } - kit_str_builder_t request = - string(kit_http1_method_to_str(method)) + string(" /") + - uri.address + ((uri.query_string == "") ? "" : "?") + - uri.query_string + " HTTP/1.1" + KIT_HTTP1_NEWLINE + - "Host: " + uri.host + ":" + uri.port + KIT_HTTP1_NEWLINE + - "Accept: */*" + KIT_HTTP1_NEWLINE + content_type + - KIT_HTTP1_NEWLINE + - "Content-Length: " + to_string(body.size()) + - KIT_HTTP1_NEWLINE + KIT_HTTP1_NEWLINE + body; + i64 req_size = req.size; + i64 n = send(fd, req.values, (socklen_t) req.size, 0); + + KIT_DA_DESTROY(req); + + if (n != req_size) { + // send failed + // - send(fd, request.c_str(), static_cast(request.size()), - 0); + closesocket(fd); - kit_str_builder_t buffer = kit_http1_buffered_read(fd); + kit_http1_response_t res; + memset(&res, 0, sizeof res); + res.success = 0; + return res; + } + + kit_str_builder_t buffer = kit_http1_buffered_read(fd, alloc); closesocket(fd); - kit_http1_response_t result = { .success = 1 }; + kit_http1_response_t res; + memset(&res, 0, sizeof res); + res.success = 1; + + kit_http1_tok_t buf_tok = { .str = { .size = buffer.size, + .values = buffer.values }, + .position = 0 }; - kit_http1_tok_t bt(buffer); + res.protocol = kit_str_build( + kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc); + res.response = kit_str_build( + kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc); + res.response_str = kit_str_build( + kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_NEWLINE), 0), alloc); - result.protocol = bt.next(KIT_HTTP1_SPACE, 0); - result.response = bt.next(KIT_HTTP1_SPACE, 0); - result.response_str = bt.next(KIT_HTTP1_NEWLINE, 0); + kit_str_builder_t header = kit_str_build( + kit_http1_tok_next(&buf_tok, + SZ(KIT_HTTP1_NEWLINE KIT_HTTP1_NEWLINE), 0), + alloc); - kit_str_builder_t header = bt.next( - KIT_HTTP1_NEWLINE KIT_HTTP1_NEWLINE, 0); + res.body = kit_str_build(kit_http1_tok_tail(&buf_tok), alloc); - result.body = bt.tail(); + kit_http1_tok_t header_tok = { + .str = (kit_str_t) { .size = header.size, + .values = header.values }, + .position = 0 + }; - kit_http1_tok_t ht(header); + KIT_DA_INIT(res.header, 0, alloc); - do { - kit_str_builder_t key = ht.next(KIT_HTTP1_HEADER_SEPARATOR, 0); + for (;;) { + kit_str_t key = kit_http1_tok_next( + &header_tok, SZ(KIT_HTTP1_HEADER_SEPARATOR), 0); if (key.size == 0) break; - result.header[key] = ht.next(KIT_HTTP1_NEWLINE, 1); - } while (1); + kit_str_t value = kit_http1_tok_next(&header_tok, + SZ(KIT_HTTP1_NEWLINE), 1); + + i64 index = res.header.size; + + KIT_DA_RESIZE(res.header, index + 1); + + assert(res.header.size == index + 1); + if (res.header.size != index + 1) { + // FIXME + // Handle bad alloc error + res.success = 0; + break; + } + + res.header.values[index] = (kit_http1_str_pair_t) { + .key = key, .value = value + }; + } + + return res; +} - return result; +static void kit_http1_response_destroy( + kit_http1_response_t *response) { + // TODO + // } +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + #ifdef __cplusplus } #endif diff --git a/source/kit/math.h b/source/kit/math.h index 45508b7..f4ab33a 100644 --- a/source/kit/math.h +++ b/source/kit/math.h @@ -63,9 +63,8 @@ typedef struct { # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" # pragma GCC diagnostic ignored "-Wunknown-pragmas" - -# pragma GCC push_options -# pragma GCC optimize("O3") +# pragma GCC push_options +# pragma GCC optimize("O3") #endif static vec2_t vec2(vec_t x, vec_t y) { diff --git a/source/kit/sockets.h b/source/kit/sockets.h index 31253fc..a9e2754 100644 --- a/source/kit/sockets.h +++ b/source/kit/sockets.h @@ -63,6 +63,7 @@ static i32 kit_socket_set_nonblocking(socket_t s) { # include # include # include +# include # define socket_t i32 # define closesocket close diff --git a/source/kit/string_builder.c b/source/kit/string_builder.c deleted file mode 100644 index f76c3a4..0000000 --- a/source/kit/string_builder.c +++ /dev/null @@ -1 +0,0 @@ -#include "string_builder.h" diff --git a/source/kit/string_builder.h b/source/kit/string_builder.h index c6d89ce..322f6e2 100644 --- a/source/kit/string_builder.h +++ b/source/kit/string_builder.h @@ -1,13 +1,63 @@ #ifndef KIT_STRING_BUILDER_H #define KIT_STRING_BUILDER_H +#include "status.h" #include "string_ref.h" #include "dynamic_array.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + typedef KIT_DA(char) kit_str_builder_t; +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-function" +# pragma GCC diagnostic ignored "-Wunknown-pragmas" +# pragma GCC push_options +# pragma GCC optimize("O3") +#endif + +static kit_str_builder_t kit_str_build(kit_str_t s, + kit_allocator_t *alloc) { + kit_str_builder_t builder; + KIT_DA_INIT(builder, s.size, alloc); + assert(builder.size == s.size); + if (builder.size == s.size) + memcpy(builder.values, s.values, s.size); + return builder; +} + +static kit_status_t kit_str_append(kit_str_builder_t *a, + kit_str_t b) { + assert(a != NULL); + if (a == NULL) + return KIT_ERROR_INVALID_ARGUMENT; + if (b.size <= 0) + return KIT_OK; + i64 n = a->size; + KIT_DA_RESIZE(*a, n + b.size); + if (a->size != n + b.size) + return KIT_ERROR_BAD_ALLOC; + memcpy(a->values + n, b.values, b.size); + return KIT_OK; +} + +#ifdef __GNUC__ +# pragma GCC pop_options +# pragma GCC diagnostic pop +#endif + +#ifdef __cplusplus +} +#endif + #ifndef KIT_DISABLE_SHORT_NAMES # define str_builder_t kit_str_builder_t +# define str_append kit_str_append #endif #endif diff --git a/source/tests/_exe.c b/source/tests/_exe.c index b406f48..f14e620 100644 --- a/source/tests/_exe.c +++ b/source/tests/_exe.c @@ -19,4 +19,5 @@ #include "duration.test.c" #include "thread.test.c" #include "xml.test.c" +#include "http1.test.c" #include "bench.test.c" diff --git a/source/tests/http1.test.c b/source/tests/http1.test.c new file mode 100644 index 0000000..d7befcb --- /dev/null +++ b/source/tests/http1.test.c @@ -0,0 +1,6 @@ +#include "../kit/http1.h" + +#define KIT_TEST_FILE http1 +#include "../kit/kit_test.h" + +TEST("http1") { } -- cgit v1.2.3