From e1dca84a44d2908c164a648c5c1c2dcce7e7be8c Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sun, 20 Apr 2025 14:11:52 +0200 Subject: [PATCH] Basic test infrastructure --- .gitignore | 6 +- Makefile | 163 ++++--- examples/server_api.c | 44 +- examples/stream_api_with_select.c | 202 ++++----- tests/main.c | 290 +++++++++++++ tests/picohttpparser.c | 685 ++++++++++++++++++++++++++++++ tests/picohttpparser.h | 90 ++++ tinyhttp.c | 157 ++++--- tinyhttp.h | 30 +- 9 files changed, 1407 insertions(+), 260 deletions(-) create mode 100644 tests/main.c create mode 100644 tests/picohttpparser.c create mode 100644 tests/picohttpparser.h diff --git a/.gitignore b/.gitignore index a1ebdf4..30188f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -blog -blog.exe -example -example.exe \ No newline at end of file +*.out +*.exe \ No newline at end of file diff --git a/Makefile b/Makefile index 7a433a4..97c31f7 100644 --- a/Makefile +++ b/Makefile @@ -1,74 +1,119 @@ -# Makefile for tinyhttp with cross-platform support -# Supports Windows and Linux with various build configurations +CC = gcc +RM = rm +MKDIR = mkdir + +EXT_WINDOWS = exe +EXT_LINUX = out + +# Can be either RELEASE or DEBUG +BUILD = RELEASE + +SUFFIX_RELEASE = +SUFFIX_DEBUG = _debug + +# ------------------------------------------------------ # + +TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/main.c +TEST_HFILES = tinyhttp.h tests/picohttpparser.h + +TEST_FLAGS = -Wall -Wextra +TEST_FLAGS_DEBUG = -ggdb +TEST_FLAGS_RELEASE = -O2 -DNDEBUG + +TEST_FLAGS_WINDOWS = -lws2_32 +TEST_FLAGS_WINDOWS_DEBUG = +TEST_FLAGS_WINDOWS_RELEASE = + +TEST_FLAGS_LINUX = +TEST_FLAGS_LINUX_DEBUG = +TEST_FLAGS_LINUX_RELEASE = + +# ------------------------------------------------------ # + +DEMO0_CFILES = tinyhttp.c examples/server_api.c +DEMO0_HFILES = tinyhttp.h + +DEMO0_FLAGS = +DEMO0_FLAGS_DEBUG = -ggdb +DEMO0_FLAGS_RELEASE = -O2 -DNDEBUG + +DEMO0_FLAGS_WINDOWS = -lws2_32 +DEMO0_FLAGS_WINDOWS_DEBUG = +DEMO0_FLAGS_WINDOWS_RELEASE = + +DEMO0_FLAGS_LINUX = +DEMO0_FLAGS_LINUX_DEBUG = +DEMO0_FLAGS_LINUX_RELEASE = + +# ------------------------------------------------------ # + +DEMO1_CFILES = tinyhttp.c examples/stream_api_with_select.c +DEMO1_HFILES = tinyhttp.h +DEMO1_FLAGS = + +DEMO1_FLAGS = +DEMO1_FLAGS_DEBUG = -ggdb +DEMO1_FLAGS_RELEASE = -O2 -DNDEBUG + +DEMO1_FLAGS_WINDOWS = -lws2_32 +DEMO1_FLAGS_WINDOWS_DEBUG = +DEMO1_FLAGS_WINDOWS_RELEASE = + +DEMO1_FLAGS_LINUX = +DEMO1_FLAGS_LINUX_DEBUG = +DEMO1_FLAGS_LINUX_RELEASE = + +# ------------------------------------------------------ # +# ------------------------------------------------------ # +# ------------------------------------------------------ # -# Detect operating system ifeq ($(OS),Windows_NT) - DETECTED_OS := Windows - # On Windows, we need to link with winsock library - LIBS := -lws2_32 - RM := del /Q - # Windows executable extension - EXE := .exe + OSTAG = WINDOWS else - DETECTED_OS := $(shell uname -s) - LIBS := - RM := rm -f - EXE := + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + OSTAG = LINUX + endif + ifeq ($(UNAME_S),Darwin) + OSTAG = OSX + endif endif -# Compiler and flags -CC := gcc -CFLAGS := -Wall -Wextra -I. -LDFLAGS := +EXT = ${EXT_$(OSTAG)} -# Project files -SRC := tinyhttp.c example.c -HEADERS := tinyhttp.h -TARGET := example$(EXE) +TEST_FLAGS += ${TEST_FLAGS_$(BUILD)} +TEST_FLAGS += ${TEST_FLAGS_$(OSTAG)} +TEST_FLAGS += ${TEST_FLAGS_$(OSTAG)_$(BUILD)} -# Default target -.PHONY: all clean release debug asan coverage +DEMO0_FLAGS += ${DEMO0_FLAGS_$(BUILD)} +DEMO0_FLAGS += ${DEMO0_FLAGS_$(OSTAG)} +DEMO0_FLAGS += ${DEMO0_FLAGS_$(OSTAG)_$(BUILD)} -# Default is release build -all: release +DEMO1_FLAGS += ${DEMO1_FLAGS_$(BUILD)} +DEMO1_FLAGS += ${DEMO1_FLAGS_$(OSTAG)} +DEMO1_FLAGS += ${DEMO1_FLAGS_$(OSTAG)_$(BUILD)} -# Release build -release: CFLAGS += -O2 -DNDEBUG -release: $(TARGET) +SUFFIX = ${SUFFIX_$(BUILD)} -# Debug build -debug: CFLAGS += -ggdb -DDEBUG -debug: $(TARGET) +# ------------------------------------------------------ # +# ------------------------------------------------------ # +# ------------------------------------------------------ # -# Address Sanitizer build -asan: CFLAGS += -fsanitize=address -fno-omit-frame-pointer -O1 -asan: LDFLAGS += -fsanitize=address -asan: $(TARGET) +.PHONY: all clean -# Coverage build -coverage: CFLAGS += -fprofile-arcs -ftest-coverage -O0 -coverage: LDFLAGS += -fprofile-arcs -ftest-coverage -coverage: $(TARGET) +all: out/test$(SUFFIX).$(EXT) out/demo0$(SUFFIX).$(EXT) out/demo1$(SUFFIX).$(EXT) -# Compile and link -$(TARGET): $(SRC) $(HEADERS) - $(CC) $(CFLAGS) $(SRC) -o $@ $(LDFLAGS) $(LIBS) +out: + $(MKDIR) out + +out/test$(SUFFIX).$(EXT): out $(TEST_CFILES) $(TEST_HFILES) + $(CC) -o $@ $(TEST_CFILES) $(TEST_FLAGS) + +out/demo0$(SUFFIX).$(EXT): out $(DEMO0_CFILES) $(DEMO0_HFILES) + $(CC) -o $@ $(DEMO0_CFILES) $(DEMO0_FLAGS) + +out/demo1$(SUFFIX).$(EXT): out $(DEMO1_CFILES) $(DEMO1_HFILES) + $(CC) -o $@ $(DEMO1_CFILES) $(DEMO1_FLAGS) -# Clean build artifacts clean: -ifeq ($(DETECTED_OS),Windows) - $(RM) $(TARGET) *.gcda *.gcno *.gcov -else - $(RM) $(TARGET) *.o *.gcda *.gcno *.gcov -endif - -# Show help -help: - @echo "Available targets:" - @echo " all - Same as 'release'" - @echo " release - Optimized build (-O2, NDEBUG)" - @echo " debug - Debug build with symbols (-ggdb)" - @echo " asan - Address Sanitizer build" - @echo " coverage - Code coverage build" - @echo " clean - Remove build artifacts" - @echo " help - Show this help" \ No newline at end of file + $(RM) -fr out diff --git a/examples/server_api.c b/examples/server_api.c index a4f83cd..3df8dba 100644 --- a/examples/server_api.c +++ b/examples/server_api.c @@ -1,7 +1,7 @@ #include #include #include -#include "tinyhttp.h" +#include "../tinyhttp.h" sig_atomic_t should_exit = 0; @@ -12,22 +12,6 @@ signal_handler(int sig) should_exit = 1; } -static void *memfunc(TinyHTTPMemoryFuncTag tag, - void *ptr, int len, void *data) -{ - (void) data; - switch (tag) { - - case TINYHTTP_MEM_MALLOC: - return malloc(len); - - case TINYHTTP_MEM_FREE: - free(ptr); - return NULL; - } - return NULL; -} - int main(void) { signal(SIGINT, signal_handler); @@ -39,31 +23,29 @@ int main(void) .plain_backlog = 32, }; - TinyHTTPServer *server = tinyhttp_server_init(config, memfunc, NULL); + TinyHTTPServer *server = tinyhttp_server_init(config); if (server == NULL) return -1; while (!should_exit) { - + + int ret; TinyHTTPRequest *req; TinyHTTPResponse res; - - int ret = tinyhttp_server_wait(server, &req, &res, 1000); + + ret = tinyhttp_server_wait(server, &req, &res, 1000); if (ret < 0) return -1; // Error if (ret > 0) continue; // Timeout - + tinyhttp_response_status(res, 200); - - tinyhttp_response_body_setmincap(res, 1<<10); - ptrdiff_t cap; - char *buf = tinyhttp_response_body_buf(res, &cap); - int len = buf ? snprintf(buf, cap, "Hello, world!") : 0; - if (len < 0 || len > cap) abort(); - tinyhttp_response_body_ack(res, len); - + tinyhttp_response_body(res, "Hello, world!", -1); + + //tinyhttp_response_undo(res); + //tinyhttp_response_status(res, 500); + tinyhttp_response_send(res); } tinyhttp_server_free(server); return 0; -} \ No newline at end of file +} diff --git a/examples/stream_api_with_select.c b/examples/stream_api_with_select.c index a9f0a9d..f64551d 100644 --- a/examples/stream_api_with_select.c +++ b/examples/stream_api_with_select.c @@ -1,95 +1,107 @@ -// A program using the stream interface may look like this: -// -// void respond(TinyHTTPStream *stream) -// { -// TinyHTTPRequest *req = tinyhttp_stream_request(stream); -// if (req->method != TINYHTTP_METHOD_GET) -// tinyhttp_stream_status(stream, 405); -// else -// tinyhttp_stream_status(stream, 200); -// tinyhttp_stream_send(stream); -// } -// -// int main(void) -// { -// int listen_fd = socket(AF_INET, SOCK_STREAM, 0); -// -// struct sockaddr_in buf; -// buf.sin_family = AF_INET; -// buf.sin_port = htons(port); -// buf.sin_addr.s_addr = htonl(INADDR_ANY); -// bind(listen_fd, (struct sockaddr*) &buf, sizeof(buf)); -// -// listen(listen_fd, 32); -// -// int num_conns = 0; -// int fds[1000]; -// TinyHTTPStream streams[1000]; -// -// for (int i = 0; i < 1000; i++) -// fds[i] = -1; -// -// for (;;) { -// // TODO: timeouts -// -// fd_set readset; -// fd_set writeset; -// FD_ZERO(&readset); -// FD_ZERO(&writeset); -// -// FD_SET(&readset); -// int max_fd = listen_fd; -// for (int i = 0; i < 1000; i++) { -// if (fds[i] == -1) continue; -// int state = tinyhttp_stream_state(&streams[i]); -// if (state & TINYHTTP_STREAM_RECV) -// FD_SET(fds[i], &readset); -// if (state & TINYHTTP_STREAM_SEND) -// FD_SET(fds[i], &writeset); -// if (state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND)) -// if (max_fd < fds[i]) max_fd = fds[i]; -// } -// -// int num = select(max_fd+1, &readset, &writeset, NULL, NULL); -// -// if (FD_ISSET(liste_fd, &readset)) { -// // TODO -// } -// -// int ready_queue[1000]; -// int ready_head = 0; -// int ready_count = 0; -// for (int i = 0; i < 1000; i++) { -// // TODO -// } -// -// while (ready_count > 0) { -// -// int idx = ready_queue[ready_head]; -// TinyHTTPStream *stream = &streams[idx]; -// -// TinyHTTPRequest *req = tinyhttp_stream_request(stream); -// assert(req); -// -// respond(stream); -// -// ready_head = (ready_head + 1) % 1000; -// ready_count--; -// if (tinyhttp_stream_request(stream)) { -// ready_queue[(ready_head + ready_count) % 1000] = idx; -// ready_count++; -// } -// } -// } -// -// close(listen_fd); -// return 0; -// } -// -// Note that this example does not keep track of timeouts. -// -// The recv_buf/recv_ack and send_buf/send_ack interface is very handy as it's -// compatible both with readyness-based event loops (epoll, poll, select) and -// completion-based event loops (iocp, io_uring). Since the stream object does -// not read from the socket directly, you can easily implement HTTPS by providing -// it with TLS-encoded data instead of data directly from the socket. +#include + +#if defined(_WIN32) +#include +#define CLOSESOCKET closesocket +#elif defined(__linux__) +#include +#include +#include +#include +#define CLOSESOCKET close +#else +#error "Only Windows and Linux are supported" +#endif + +#include "../tinyhttp.h" + +#define PORT 8080 +#define ADDR "127.0.0.1" + +// TODO: Complete this + +void respond(TinyHTTPStream *stream) +{ + TinyHTTPRequest *req = tinyhttp_stream_request(stream); + if (req->method != TINYHTTP_METHOD_GET) + tinyhttp_stream_response_status(stream, 405); + else + tinyhttp_stream_response_status(stream, 200); + tinyhttp_stream_response_send(stream); +} + +int main(void) +{ + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + + struct sockaddr_in buf; + buf.sin_family = AF_INET; + buf.sin_port = htons(PORT); + buf.sin_addr.s_addr = inet_addr(ADDR); + bind(listen_fd, (struct sockaddr*) &buf, sizeof(buf)); + + listen(listen_fd, 32); + + int num_conns = 0; + int fds[1000]; + TinyHTTPStream streams[1000]; + + for (int i = 0; i < 1000; i++) + fds[i] = -1; + + for (;;) { + // TODO: timeouts + + fd_set readset; + fd_set writeset; + FD_ZERO(&readset); + FD_ZERO(&writeset); + + FD_SET(listen_fd, &readset); + int max_fd = listen_fd; + for (int i = 0; i < 1000; i++) { + if (fds[i] == -1) continue; + int state = tinyhttp_stream_state(&streams[i]); + if (state & TINYHTTP_STREAM_RECV) + FD_SET(fds[i], &readset); + if (state & TINYHTTP_STREAM_SEND) + FD_SET(fds[i], &writeset); + if (state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND)) + if (max_fd < fds[i]) max_fd = fds[i]; + } + + int num = select(max_fd+1, &readset, &writeset, NULL, NULL); + + if (FD_ISSET(listen_fd, &readset)) { + // TODO + } + + int ready_queue[1000]; + int ready_head = 0; + int ready_count = 0; + for (int i = 0; i < 1000; i++) { + // TODO + } + + while (ready_count > 0) { + + int idx = ready_queue[ready_head]; + TinyHTTPStream *stream = &streams[idx]; + + TinyHTTPRequest *req = tinyhttp_stream_request(stream); + assert(req); + + respond(stream); + + ready_head = (ready_head + 1) % 1000; + ready_count--; + if (tinyhttp_stream_request(stream)) { + ready_queue[(ready_head + ready_count) % 1000] = idx; + ready_count++; + } + } + } + + CLOSESOCKET(listen_fd); + return 0; +} \ No newline at end of file diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..d42ab61 --- /dev/null +++ b/tests/main.c @@ -0,0 +1,290 @@ +#include +#include +#include + +#include "picohttpparser.h" +#include "../tinyhttp.h" + +////////////////////////////////////////////////////////////////////////////////////// +// TYPES, MACROS, PROTOTYPES +////////////////////////////////////////////////////////////////////////////////////// + +typedef struct { + + int minor; + + int status_code; + TinyHTTPString status_text; + + int num_headers; + TinyHTTPHeader headers[TINYHTTP_HEADER_LIMIT]; + + char *body; + int body_len; +} Response; + +static void *memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data); +static void send_request(TinyHTTPStream *stream, const char *str); +static void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap); + +#define TEST(X) {if (!(X)) { printf("Test failed at %s:%d\n", __FILE__, __LINE__); fflush(stdout); __builtin_trap(); }} + +////////////////////////////////////////////////////////////////////////////////////// +// TEST CASES +////////////////////////////////////////////////////////////////////////////////////// + +static void basic_request(void) +{ + TinyHTTPStream stream; + tinyhttp_stream_init(&stream, memfunc, NULL); + + // Send request + send_request(&stream, + "GET / HTTP/1.1\r\n" + "Host: 127.0.0.1:8080\r\n" + "User-Agent: curl/7.81.0\r\n" + "Accept: */*\r\n" + "\r\n"); + + // Build response + tinyhttp_stream_response_status(&stream, 200); + tinyhttp_stream_response_send(&stream); + + // Receive response + char buf[1<<12]; + Response res; + recv_response(&stream, &res, buf, sizeof(buf)); + + // TODO + + tinyhttp_stream_free(&stream); +} + +////////////////////////////////////////////////////////////////////////////////////// +// ENTRY POINT +////////////////////////////////////////////////////////////////////////////////////// + +int main(void) +{ + basic_request(); + return 0; +} + +////////////////////////////////////////////////////////////////////////////////////// +// Helper Functions +////////////////////////////////////////////////////////////////////////////////////// + +static void* +memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data) +{ + (void) data; + switch (tag) { + + case TINYHTTP_MEM_MALLOC: + return malloc(len); + + case TINYHTTP_MEM_FREE: + free(ptr); + return NULL; + } + return NULL; +} + +static int +buffer_into_stream(TinyHTTPStream *stream, const char *src, int len) +{ + int state = tinyhttp_stream_state(stream); + + ptrdiff_t copied = 0; + while (copied < len && (state & TINYHTTP_STREAM_RECV)) { + + char *dst; + ptrdiff_t cap; + ptrdiff_t cpy; + + dst = tinyhttp_stream_recv_buf(stream, &cap); + + cpy = len - copied; + if (cpy > cap) cpy = cap; + + memcpy(dst, src + copied, cpy); + + copied += cpy; + tinyhttp_stream_recv_ack(stream, cpy); + state = tinyhttp_stream_state(stream); + } + + return copied; +} + +static int +stream_into_buffer(TinyHTTPStream *stream, char *dst, int cap) +{ + int state = tinyhttp_stream_state(stream); + + int copied = 0; + while (copied < cap && (state & TINYHTTP_STREAM_SEND)) { + + char *src; + ptrdiff_t len; + ptrdiff_t cpy; + + src = tinyhttp_stream_send_buf(stream, &len); + + cpy = cap - copied; + if (cpy > len) cpy = len; + + memcpy(dst + copied, src, cpy); + + copied += cpy; + tinyhttp_stream_send_ack(stream, cpy); + state = tinyhttp_stream_state(stream); + } + + return copied; +} + +static void +expect_request(TinyHTTPStream *stream, TinyHTTPRequest expreq) +{ + int state = tinyhttp_stream_state(stream); + TEST(state & TINYHTTP_STREAM_READY); + + TinyHTTPRequest *req = tinyhttp_stream_request(stream); + TEST(req); + + TEST(req->method == expreq.method); + TEST(req->minor == expreq.minor); + TEST(tinyhttp_streq(req->path, expreq.path)); + TEST(req->num_headers == expreq.num_headers); + for (int i = 0; i < expreq.num_headers; i++) { + TEST(tinyhttp_streq(req->headers[i].name, expreq.headers[i].name)); + TEST(tinyhttp_streq(req->headers[i].value, expreq.headers[i].value)); + } + + TEST(req->body_len == expreq.body_len); + if (expreq.body_len == 0) { + TEST(req->body == NULL); + } else { + TEST(!memcmp(req->body, expreq.body, expreq.body_len)); + } +} + +static int +parse_request(TinyHTTPString txt, TinyHTTPRequest *req) +{ + const char *method; + size_t method_len; + + const char *path; + size_t path_len; + + int minor; + + struct phr_header headers[TINYHTTP_HEADER_LIMIT]; + size_t num_headers = TINYHTTP_HEADER_LIMIT; + + int ret = phr_parse_request( + txt.ptr, txt.len, + &method, &method_len, + &path, &path_len, + &minor, + headers, &num_headers, + 0); + TEST(ret == txt.len); + + if (method_len == 3 && !memcmp("GET", method, 3)) { + req->method = TINYHTTP_METHOD_GET; + } else if (method_len == 4 && !memcmp("POST", method, 4)) { + req->method = TINYHTTP_METHOD_POST; + } else { + return -1; + } + + req->minor = minor; + req->path = (TinyHTTPString) { path, path_len }; + req->num_headers = num_headers; + + for (int i = 0; i < (int) num_headers; i++) { + req->headers[i].name = (TinyHTTPString) { + headers[i].name, + headers[i].name_len, + }; + req->headers[i].value = (TinyHTTPString) { + headers[i].value, + headers[i].value_len, + }; + } + + req->body = NULL; // TODO + req->body_len = 0; // TODO + + return 0; +} + +static void +parse_response(TinyHTTPString txt, Response *res) +{ + int minor; + + int status_code; + const char *status_text; + size_t status_text_len; + + struct phr_header headers[TINYHTTP_HEADER_LIMIT]; + size_t num_headers = TINYHTTP_HEADER_LIMIT; + + int ret = phr_parse_response( + txt.ptr, txt.len, + &minor, + &status_code, &status_text, &status_text_len, + headers, &num_headers, + 0); + TEST(ret == txt.len); + + res->minor = minor; + res->status_code = status_code; + res->status_text = (TinyHTTPString) { status_text, status_text_len }; + + res->num_headers = num_headers; + for (int i = 0; i < (int) num_headers; i++) { + res->headers[i].name = (TinyHTTPString) { + headers[i].name, + headers[i].name_len + }; + res->headers[i].value = (TinyHTTPString) { + headers[i].value, + headers[i].value_len + }; + } + + res->body = NULL; // TODO + res->body_len = 0; // TODO +} + +static void +send_request(TinyHTTPStream *stream, const char *str) +{ + int received = buffer_into_stream(stream, str, strlen(str)); + TEST(received == (int) strlen(str)); + + TinyHTTPRequest req; + TEST(parse_request((TinyHTTPString) {str, strlen(str)}, &req) == 0); + + expect_request(stream, req); +} + +static void +recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap) +{ + int len = stream_into_buffer(stream, dst, cap); + + int state = tinyhttp_stream_state(stream); + TEST((state & TINYHTTP_STREAM_SEND) == 0); + + parse_response((TinyHTTPString) { dst, len }, res); +} + +////////////////////////////////////////////////////////////////////////////////////// +// THE END +////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/tests/picohttpparser.c b/tests/picohttpparser.c new file mode 100644 index 0000000..90985fd --- /dev/null +++ b/tests/picohttpparser.c @@ -0,0 +1,685 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + + #include + #include + #include + #ifdef __SSE4_2__ + #ifdef _MSC_VER + #include + #else + #include + #endif + #endif + #include "picohttpparser.h" + + #if __GNUC__ >= 3 + #define likely(x) __builtin_expect(!!(x), 1) + #define unlikely(x) __builtin_expect(!!(x), 0) + #else + #define likely(x) (x) + #define unlikely(x) (x) + #endif + + #ifdef _MSC_VER + #define ALIGNED(n) _declspec(align(n)) + #else + #define ALIGNED(n) __attribute__((aligned(n))) + #endif + + #define IS_PRINTABLE_ASCII(c) ((unsigned char)(c)-040u < 0137u) + + #define CHECK_EOF() \ + if (buf == buf_end) { \ + *ret = -2; \ + return NULL; \ + } + + #define EXPECT_CHAR_NO_CHECK(ch) \ + if (*buf++ != ch) { \ + *ret = -1; \ + return NULL; \ + } + + #define EXPECT_CHAR(ch) \ + CHECK_EOF(); \ + EXPECT_CHAR_NO_CHECK(ch); + + #define ADVANCE_TOKEN(tok, toklen) \ + do { \ + const char *tok_start = buf; \ + static const char ALIGNED(16) ranges2[16] = "\000\040\177\177"; \ + int found2; \ + buf = findchar_fast(buf, buf_end, ranges2, 4, &found2); \ + if (!found2) { \ + CHECK_EOF(); \ + } \ + while (1) { \ + if (*buf == ' ') { \ + break; \ + } else if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { \ + if ((unsigned char)*buf < '\040' || *buf == '\177') { \ + *ret = -1; \ + return NULL; \ + } \ + } \ + ++buf; \ + CHECK_EOF(); \ + } \ + tok = tok_start; \ + toklen = buf - tok_start; \ + } while (0) + + static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\1\0\1\1\1\1\1\0\0\1\1\0\1\1\0\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0" + "\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\1\1" + "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\1\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + static const char *findchar_fast(const char *buf, const char *buf_end, const char *ranges, size_t ranges_size, int *found) + { + *found = 0; + #if __SSE4_2__ + if (likely(buf_end - buf >= 16)) { + __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); + + size_t left = (buf_end - buf) & ~15; + do { + __m128i b16 = _mm_loadu_si128((const __m128i *)buf); + int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); + if (unlikely(r != 16)) { + buf += r; + *found = 1; + break; + } + buf += 16; + left -= 16; + } while (likely(left != 0)); + } + #else + /* suppress unused parameter warning */ + (void)buf_end; + (void)ranges; + (void)ranges_size; + #endif + return buf; + } + + static const char *get_token_to_eol(const char *buf, const char *buf_end, const char **token, size_t *token_len, int *ret) + { + const char *token_start = buf; + + #ifdef __SSE4_2__ + static const char ALIGNED(16) ranges1[16] = "\0\010" /* allow HT */ + "\012\037" /* allow SP and up to but not including DEL */ + "\177\177"; /* allow chars w. MSB set */ + int found; + buf = findchar_fast(buf, buf_end, ranges1, 6, &found); + if (found) + goto FOUND_CTL; + #else + /* find non-printable char within the next 8 bytes, this is the hottest code; manually inlined */ + while (likely(buf_end - buf >= 8)) { + #define DOIT() \ + do { \ + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) \ + goto NonPrintable; \ + ++buf; \ + } while (0) + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + DOIT(); + #undef DOIT + continue; + NonPrintable: + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + ++buf; + } + #endif + for (;; ++buf) { + CHECK_EOF(); + if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { + if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { + goto FOUND_CTL; + } + } + } + FOUND_CTL: + if (likely(*buf == '\015')) { + ++buf; + EXPECT_CHAR('\012'); + *token_len = buf - 2 - token_start; + } else if (*buf == '\012') { + *token_len = buf - token_start; + ++buf; + } else { + *ret = -1; + return NULL; + } + *token = token_start; + + return buf; + } + + static const char *is_complete(const char *buf, const char *buf_end, size_t last_len, int *ret) + { + int ret_cnt = 0; + buf = last_len < 3 ? buf : buf + last_len - 3; + + while (1) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + CHECK_EOF(); + EXPECT_CHAR('\012'); + ++ret_cnt; + } else if (*buf == '\012') { + ++buf; + ++ret_cnt; + } else { + ++buf; + ret_cnt = 0; + } + if (ret_cnt == 2) { + return buf; + } + } + + *ret = -2; + return NULL; + } + + #define PARSE_INT(valp_, mul_) \ + if (*buf < '0' || '9' < *buf) { \ + buf++; \ + *ret = -1; \ + return NULL; \ + } \ + *(valp_) = (mul_) * (*buf++ - '0'); + + #define PARSE_INT_3(valp_) \ + do { \ + int res_ = 0; \ + PARSE_INT(&res_, 100) \ + *valp_ = res_; \ + PARSE_INT(&res_, 10) \ + *valp_ += res_; \ + PARSE_INT(&res_, 1) \ + *valp_ += res_; \ + } while (0) + + /* returned pointer is always within [buf, buf_end), or null */ + static const char *parse_token(const char *buf, const char *buf_end, const char **token, size_t *token_len, char next_char, + int *ret) + { + /* We use pcmpestri to detect non-token characters. This instruction can take no more than eight character ranges (8*2*8=128 + * bits that is the size of a SSE register). Due to this restriction, characters `|` and `~` are handled in the slow loop. */ + static const char ALIGNED(16) ranges[] = "\x00 " /* control chars and up to SP */ + "\"\"" /* 0x22 */ + "()" /* 0x28,0x29 */ + ",," /* 0x2c */ + "//" /* 0x2f */ + ":@" /* 0x3a-0x40 */ + "[]" /* 0x5b-0x5d */ + "{\xff"; /* 0x7b-0xff */ + const char *buf_start = buf; + int found; + buf = findchar_fast(buf, buf_end, ranges, sizeof(ranges) - 1, &found); + if (!found) { + CHECK_EOF(); + } + while (1) { + if (*buf == next_char) { + break; + } else if (!token_char_map[(unsigned char)*buf]) { + *ret = -1; + return NULL; + } + ++buf; + CHECK_EOF(); + } + *token = buf_start; + *token_len = buf - buf_start; + return buf; + } + + /* returned pointer is always within [buf, buf_end), or null */ + static const char *parse_http_version(const char *buf, const char *buf_end, int *minor_version, int *ret) + { + /* we want at least [HTTP/1.] to try to parse */ + if (buf_end - buf < 9) { + *ret = -2; + return NULL; + } + EXPECT_CHAR_NO_CHECK('H'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('T'); + EXPECT_CHAR_NO_CHECK('P'); + EXPECT_CHAR_NO_CHECK('/'); + EXPECT_CHAR_NO_CHECK('1'); + EXPECT_CHAR_NO_CHECK('.'); + PARSE_INT(minor_version, 1); + return buf; + } + + static const char *parse_headers(const char *buf, const char *buf_end, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) + { + for (;; ++*num_headers) { + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + break; + } else if (*buf == '\012') { + ++buf; + break; + } + if (*num_headers == max_headers) { + *ret = -1; + return NULL; + } + if (!(*num_headers != 0 && (*buf == ' ' || *buf == '\t'))) { + /* parsing name, but do not discard SP before colon, see + * http://www.mozilla.org/security/announce/2006/mfsa2006-33.html */ + if ((buf = parse_token(buf, buf_end, &headers[*num_headers].name, &headers[*num_headers].name_len, ':', ret)) == NULL) { + return NULL; + } + if (headers[*num_headers].name_len == 0) { + *ret = -1; + return NULL; + } + ++buf; + for (;; ++buf) { + CHECK_EOF(); + if (!(*buf == ' ' || *buf == '\t')) { + break; + } + } + } else { + headers[*num_headers].name = NULL; + headers[*num_headers].name_len = 0; + } + const char *value; + size_t value_len; + if ((buf = get_token_to_eol(buf, buf_end, &value, &value_len, ret)) == NULL) { + return NULL; + } + /* remove trailing SPs and HTABs */ + const char *value_end = value + value_len; + for (; value_end != value; --value_end) { + const char c = *(value_end - 1); + if (!(c == ' ' || c == '\t')) { + break; + } + } + headers[*num_headers].value = value; + headers[*num_headers].value_len = value_end - value; + } + return buf; + } + + static const char *parse_request(const char *buf, const char *buf_end, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, + size_t max_headers, int *ret) + { + /* skip first empty line (some clients add CRLF after POST content) */ + CHECK_EOF(); + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } + + /* parse request line */ + if ((buf = parse_token(buf, buf_end, method, method_len, ' ', ret)) == NULL) { + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + ADVANCE_TOKEN(*path, *path_len); + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + if (*method_len == 0 || *path_len == 0) { + *ret = -1; + return NULL; + } + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + if (*buf == '\015') { + ++buf; + EXPECT_CHAR('\012'); + } else if (*buf == '\012') { + ++buf; + } else { + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); + } + + int phr_parse_request(const char *buf_start, size_t len, const char **method, size_t *method_len, const char **path, + size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len) + { + const char *buf = buf_start, *buf_end = buf_start + len; + size_t max_headers = *num_headers; + int r; + + *method = NULL; + *method_len = 0; + *path = NULL; + *path_len = 0; + *minor_version = -1; + *num_headers = 0; + + /* if last_len != 0, check if the request is complete (a fast countermeasure + againt slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_request(buf, buf_end, method, method_len, path, path_len, minor_version, headers, num_headers, max_headers, + &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); + } + + static const char *parse_response(const char *buf, const char *buf_end, int *minor_version, int *status, const char **msg, + size_t *msg_len, struct phr_header *headers, size_t *num_headers, size_t max_headers, int *ret) + { + /* parse "HTTP/1.x" */ + if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { + return NULL; + } + /* skip space */ + if (*buf != ' ') { + *ret = -1; + return NULL; + } + do { + ++buf; + CHECK_EOF(); + } while (*buf == ' '); + /* parse status code, we want at least [:digit:][:digit:][:digit:] to try to parse */ + if (buf_end - buf < 4) { + *ret = -2; + return NULL; + } + PARSE_INT_3(status); + + /* get message including preceding space */ + if ((buf = get_token_to_eol(buf, buf_end, msg, msg_len, ret)) == NULL) { + return NULL; + } + if (*msg_len == 0) { + /* ok */ + } else if (**msg == ' ') { + /* Remove preceding space. Successful return from `get_token_to_eol` guarantees that we would hit something other than SP + * before running past the end of the given buffer. */ + do { + ++*msg; + --*msg_len; + } while (**msg == ' '); + } else { + /* garbage found after status code */ + *ret = -1; + return NULL; + } + + return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); + } + + int phr_parse_response(const char *buf_start, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len) + { + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *minor_version = -1; + *status = 0; + *msg = NULL; + *msg_len = 0; + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_response(buf, buf_end, minor_version, status, msg, msg_len, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); + } + + int phr_parse_headers(const char *buf_start, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len) + { + const char *buf = buf_start, *buf_end = buf + len; + size_t max_headers = *num_headers; + int r; + + *num_headers = 0; + + /* if last_len != 0, check if the response is complete (a fast countermeasure + against slowloris */ + if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { + return r; + } + + if ((buf = parse_headers(buf, buf_end, headers, num_headers, max_headers, &r)) == NULL) { + return r; + } + + return (int)(buf - buf_start); + } + + enum { + CHUNKED_IN_CHUNK_SIZE, + CHUNKED_IN_CHUNK_EXT, + CHUNKED_IN_CHUNK_DATA, + CHUNKED_IN_CHUNK_CRLF, + CHUNKED_IN_TRAILERS_LINE_HEAD, + CHUNKED_IN_TRAILERS_LINE_MIDDLE + }; + + static int decode_hex(int ch) + { + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } else if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 0xa; + } else if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 0xa; + } else { + return -1; + } + } + + ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *_bufsz) + { + size_t dst = 0, src = 0, bufsz = *_bufsz; + ssize_t ret = -2; /* incomplete */ + + decoder->_total_read += bufsz; + + while (1) { + switch (decoder->_state) { + case CHUNKED_IN_CHUNK_SIZE: + for (;; ++src) { + int v; + if (src == bufsz) + goto Exit; + if ((v = decode_hex(buf[src])) == -1) { + if (decoder->_hex_count == 0) { + ret = -1; + goto Exit; + } + /* the only characters that may appear after the chunk size are BWS, semicolon, or CRLF */ + switch (buf[src]) { + case ' ': + case '\011': + case ';': + case '\012': + case '\015': + break; + default: + ret = -1; + goto Exit; + } + break; + } + if (decoder->_hex_count == sizeof(size_t) * 2) { + ret = -1; + goto Exit; + } + decoder->bytes_left_in_chunk = decoder->bytes_left_in_chunk * 16 + v; + ++decoder->_hex_count; + } + decoder->_hex_count = 0; + decoder->_state = CHUNKED_IN_CHUNK_EXT; + /* fallthru */ + case CHUNKED_IN_CHUNK_EXT: + /* RFC 7230 A.2 "Line folding in chunk extensions is disallowed" */ + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + if (decoder->bytes_left_in_chunk == 0) { + if (decoder->consume_trailer) { + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + } else { + goto Complete; + } + } + decoder->_state = CHUNKED_IN_CHUNK_DATA; + /* fallthru */ + case CHUNKED_IN_CHUNK_DATA: { + size_t avail = bufsz - src; + if (avail < decoder->bytes_left_in_chunk) { + if (dst != src) + memmove(buf + dst, buf + src, avail); + src += avail; + dst += avail; + decoder->bytes_left_in_chunk -= avail; + goto Exit; + } + if (dst != src) + memmove(buf + dst, buf + src, decoder->bytes_left_in_chunk); + src += decoder->bytes_left_in_chunk; + dst += decoder->bytes_left_in_chunk; + decoder->bytes_left_in_chunk = 0; + decoder->_state = CHUNKED_IN_CHUNK_CRLF; + } + /* fallthru */ + case CHUNKED_IN_CHUNK_CRLF: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src] != '\012') { + ret = -1; + goto Exit; + } + ++src; + decoder->_state = CHUNKED_IN_CHUNK_SIZE; + break; + case CHUNKED_IN_TRAILERS_LINE_HEAD: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] != '\015') + break; + } + if (buf[src++] == '\012') + goto Complete; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_MIDDLE; + /* fallthru */ + case CHUNKED_IN_TRAILERS_LINE_MIDDLE: + for (;; ++src) { + if (src == bufsz) + goto Exit; + if (buf[src] == '\012') + break; + } + ++src; + decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; + break; + default: + assert(!"decoder is corrupt"); + } + } + + Complete: + ret = bufsz - src; + Exit: + if (dst != src) + memmove(buf + dst, buf + src, bufsz - src); + *_bufsz = dst; + /* if incomplete but the overhead of the chunked encoding is >=100KB and >80%, signal an error */ + if (ret == -2) { + decoder->_total_overhead += bufsz - dst; + if (decoder->_total_overhead >= 100 * 1024 && decoder->_total_read - decoder->_total_overhead < decoder->_total_read / 4) + ret = -1; + } + return ret; + } + + int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder) + { + return decoder->_state == CHUNKED_IN_CHUNK_DATA; + } + + #undef CHECK_EOF + #undef EXPECT_CHAR + #undef ADVANCE_TOKEN \ No newline at end of file diff --git a/tests/picohttpparser.h b/tests/picohttpparser.h new file mode 100644 index 0000000..f4332b9 --- /dev/null +++ b/tests/picohttpparser.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, + * Shigeo Mitsunari + * + * The software is licensed under either the MIT License (below) or the Perl + * license. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + + #ifndef picohttpparser_h + #define picohttpparser_h + + #include + #include + + #ifdef _MSC_VER + #define ssize_t intptr_t + #endif + + #ifdef __cplusplus + extern "C" { + #endif + + /* contains name and value of a header (name == NULL if is a continuing line + * of a multiline header */ + struct phr_header { + const char *name; + size_t name_len; + const char *value; + size_t value_len; + }; + + /* returns number of bytes consumed if successful, -2 if request is partial, + * -1 if failed */ + int phr_parse_request(const char *buf, size_t len, const char **method, size_t *method_len, const char **path, size_t *path_len, + int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len); + + /* ditto */ + int phr_parse_response(const char *_buf, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, + struct phr_header *headers, size_t *num_headers, size_t last_len); + + /* ditto */ + int phr_parse_headers(const char *buf, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len); + + /* should be zero-filled before start */ + struct phr_chunked_decoder { + size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ + char consume_trailer; /* if trailing headers should be consumed */ + char _hex_count; + char _state; + uint64_t _total_read; + uint64_t _total_overhead; + }; + + /* the function rewrites the buffer given as (buf, bufsz) removing the chunked- + * encoding headers. When the function returns without an error, bufsz is + * updated to the length of the decoded data available. Applications should + * repeatedly call the function while it returns -2 (incomplete) every time + * supplying newly arrived data. If the end of the chunked-encoded data is + * found, the function returns a non-negative number indicating the number of + * octets left undecoded, that starts from the offset returned by `*bufsz`. + * Returns -1 on error. + */ + ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *bufsz); + + /* returns if the chunked decoder is in middle of chunked data */ + int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder); + + #ifdef __cplusplus + } + #endif + + #endif \ No newline at end of file diff --git a/tinyhttp.c b/tinyhttp.c index fa312bb..c23e842 100644 --- a/tinyhttp.c +++ b/tinyhttp.c @@ -114,8 +114,8 @@ static void dump_state(int state, const char *file, int line) { printf("state = "); - if (state == TINYHTTP_STREAM_FREE) - printf("FREE "); + if (state & TINYHTTP_STREAM_DIED) + printf("DIED "); if (state & TINYHTTP_STREAM_SEND) printf("SEND "); @@ -145,6 +145,38 @@ dump_state(int state, const char *file, int line) #define DUMP_STATE(...) #endif +//////////////////////////////////////////////////////////////////////////////////// +// UTILITIES // +//////////////////////////////////////////////////////////////////////////////////// + +static char +to_lower(char c) +{ + if (c >= 'A' && c <= 'Z') + return c - 'A' + 'a'; + return c; +} + +int tinyhttp_streq(TinyHTTPString s1, TinyHTTPString s2) +{ + if (s1.len != s2.len) + return 0; + for (int i = 0; i < s1.len; i++) + if (s1.ptr[i] != s2.ptr[i]) + return 0; + return 1; +} + +int tinyhttp_streqcase(TinyHTTPString s1, TinyHTTPString s2) +{ + if (s1.len != s2.len) + return 0; + for (int i = 0; i < s1.len; i++) + if (to_lower(s1.ptr[i]) != to_lower(s2.ptr[i])) + return 0; + return 1; +} + //////////////////////////////////////////////////////////////////////////////////// // HTTP REQUEST PARSER // //////////////////////////////////////////////////////////////////////////////////// @@ -189,8 +221,7 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) off = cur; while (cur < len && src[cur] != ' ') // TODO: More robust cur++; - req->path = src + off; - req->path_len = cur - off; + req->path = (TinyHTTPString) { src + off, cur - off }; if (len - cur <= 5 || src[cur+0] != ' ' @@ -234,7 +265,7 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) ptrdiff_t name_off = cur; while (cur < len && src[cur] != ':') // TODO: robust cur++; - ptrdiff_t name_len = cur - name_off; + TinyHTTPString name = { src + name_off, cur - name_off }; if (cur == len) return -400; @@ -243,7 +274,7 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) ptrdiff_t value_off = cur; while (cur < len && src[cur] != '\r') cur++; - ptrdiff_t value_len = cur - value_off; + TinyHTTPString value = { src + value_off, cur - value_off }; if (cur == len) return -400; @@ -257,44 +288,20 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) // 1) No spaces are allowed after the name // 2) Spaces should be trimmed from the value - if (req->num_headers < TINYHTTP_HEADER_LIMIT) { - TinyHTTPHeader *header = &req->headers[req->num_headers++]; - header->name = src + name_off; - header->name_len = name_len; - header->value = src + value_off; - header->value_len = value_len; - } + if (req->num_headers < TINYHTTP_HEADER_LIMIT) + req->headers[req->num_headers++] = (TinyHTTPHeader) { name, value }; } cur += 2; return cur; } -static char -to_lower(char c) -{ - if (c >= 'A' && c <= 'Z') - return c - 'A' + 'a'; - return c; -} - static int -eqstrnocase(const char *s1, ptrdiff_t len1, const char *s2, ptrdiff_t len2) -{ - if (len1 != len2) - return 0; - for (int i = 0; i < len1; i++) - if (to_lower(s1[i]) != to_lower(s2[i])) - return 0; - return 1; -} - -static int -find_header(TinyHTTPRequest *req, const char *name) +find_header(TinyHTTPRequest *req, TinyHTTPString name) { for (int i = 0; i < req->num_headers; i++) { TinyHTTPHeader *header = &req->headers[i]; - if (eqstrnocase(header->name, header->name_len, name, strlen(name))) + if (tinyhttp_streqcase(header->name, name)) return i; } return -1; @@ -308,7 +315,7 @@ enum { }; static int -parse_transfer_encoding(char *src, ptrdiff_t len, int *items, int max) +parse_transfer_encoding(const char *src, ptrdiff_t len, int *items, int max) { int num = 0; ptrdiff_t cur = 0; @@ -387,7 +394,7 @@ static int is_digit(char c) } static int -parse_content_length(char *src, ptrdiff_t len, unsigned long long *out) +parse_content_length(const char *src, ptrdiff_t len, unsigned long long *out) { ptrdiff_t cur = 0; while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) @@ -416,13 +423,13 @@ parse_request(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPR return ret; ptrdiff_t head_len = ret; - int transfer_encoding_index = find_header(req, "Transfer-Encoding"); + int transfer_encoding_index = find_header(req, TINYHTTP_STRING("Transfer-Encoding")); if (transfer_encoding_index >= 0) { TinyHTTPHeader *header = &req->headers[transfer_encoding_index]; int items[8]; - int num = parse_transfer_encoding(header->value, header->value_len, items, COUNTOF(items)); + int num = parse_transfer_encoding(header->value.ptr, header->value.len, items, COUNTOF(items)); if (num < 0) return -400; @@ -434,13 +441,13 @@ parse_request(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPR return 1; } - int content_length_index = find_header(req, "Content-Length"); + int content_length_index = find_header(req, TINYHTTP_STRING("Content-Length")); if (content_length_index >= 0) { TinyHTTPHeader *header = &req->headers[content_length_index]; unsigned long long content_length; - if (parse_content_length(header->value, header->value_len, &content_length) < 0) + if (parse_content_length(header->value.ptr, header->value.len, &content_length) < 0) return -400; if (content_length > body_limit || content_length > MAX_U32) return -413; @@ -1072,7 +1079,7 @@ process_next_request(TinyHTTPStream *stream) // Try parsing the request from the buffered bytes. ptrdiff_t len; - char *src = byte_queue_read_buf(&stream->in, &len); + char *src = byte_queue_read_buf(&stream->in, &len); // TODO: What if this returns NULL? int ret = parse_request(src, len, stream->bodylimit, &stream->req); // Request is incomplete @@ -1270,6 +1277,19 @@ append_special_headers(TinyHTTPStream *stream) stream->content_length_offset = byte_queue_offset(&stream->out); } +void tinyhttp_stream_response_body(TinyHTTPStream *stream, const char *src, int len) +{ + if (len < 0) len = strlen(src); + + tinyhttp_stream_response_body_setmincap(stream, len); + + ptrdiff_t cap; + char *dst = tinyhttp_stream_response_body_buf(stream, &cap); + if (dst) + memcpy(dst, src, len); + tinyhttp_stream_response_body_ack(stream, len); +} + // See tinyhttp.h void tinyhttp_stream_response_body_setmincap(TinyHTTPStream *stream, ptrdiff_t mincap) { @@ -1482,8 +1502,6 @@ struct TinyHTTPServer { #elif defined(__linux__) int epoll_fd; #endif - TinyHTTPMemoryFunc memfunc; - void *memfuncdata; SOCKET plain_listen_socket; SOCKET secure_listen_socket; int num_conns; @@ -1618,6 +1636,22 @@ remove_from_ready_queue(TinyHTTPServer *server, int idx) server->ready_count--; } +static void* +default_memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data) +{ + (void) data; + switch (tag) { + + case TINYHTTP_MEM_MALLOC: + return malloc(len); + + case TINYHTTP_MEM_FREE: + free(ptr); + return NULL; + } + return NULL; +} + #if defined(__linux__) static void @@ -1699,7 +1733,7 @@ accept_from_listen_socket(TinyHTTPServer *server, SOCKET listen_socket, int secu server->stream_sockets[idx] = accepted_socket; TinyHTTPStream *stream = &server->stream_state[idx]; - tinyhttp_stream_init(stream, server->memfunc, server->memfuncdata); + tinyhttp_stream_init(stream, default_memfunc, NULL); if (server->num_conns < TINYHTTP_SERVER_CONN_LIMIT * 0.7) tinyhttp_stream_setreuse(stream, 1); @@ -2025,13 +2059,11 @@ intern_accepted_socket(TinyHTTPServer *server, SOCKET accepted_socket, int secur server->stream_sockets[idx] = accepted_socket; TinyHTTPStream *stream = &server->stream_state[idx]; - tinyhttp_stream_init(stream, server->memfunc, server->memfuncdata); + tinyhttp_stream_init(stream, default_memfunc, NULL); if (server->num_conns < TINYHTTP_SERVER_CONN_LIMIT * 0.7) tinyhttp_stream_setreuse(stream, 1); - int state = tinyhttp_stream_state(stream); - DUMP_STATE(tinyhttp_stream_state(stream)); if (CreateIoCompletionPort((HANDLE) accepted_socket, server->iocp, 0, 0) == NULL) { @@ -2209,21 +2241,17 @@ process_network_events(TinyHTTPServer *server, int timeout) } #endif -TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config, - TinyHTTPMemoryFunc memfunc, void *memfuncdata) +TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config) { #if !TINYHTTP_HTTPS_ENABLE if (config.secure) return NULL; #endif - TinyHTTPServer *server = memfunc(TINYHTTP_MEM_MALLOC, NULL, sizeof(TinyHTTPServer), memfuncdata); + TinyHTTPServer *server = malloc(sizeof(TinyHTTPServer)); if (server == NULL) return NULL; - server->memfunc = memfunc; - server->memfuncdata = memfuncdata; - server->num_conns = 0; server->ready_head = 0; server->ready_count = 0; @@ -2249,7 +2277,7 @@ TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config, #endif if (server->plain_listen_socket == INVALID_SOCKET) { - memfunc(TINYHTTP_MEM_FREE, server, sizeof(TinyHTTPServer), NULL); + free(server); return NULL; } @@ -2263,7 +2291,7 @@ TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config, CLOSESOCKET(server->plain_listen_socket); - memfunc(TINYHTTP_MEM_FREE, server, sizeof(TinyHTTPServer), NULL); + free(server); return NULL; } } @@ -2276,7 +2304,7 @@ TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config, if (server->secure_listen_socket != INVALID_SOCKET) CLOSESOCKET(server->secure_listen_socket); - memfunc(TINYHTTP_MEM_FREE, server, sizeof(TinyHTTPServer), NULL); + free(server); return NULL; } @@ -2298,10 +2326,7 @@ void tinyhttp_server_free(TinyHTTPServer *server) CLOSESOCKET(server->secure_listen_socket); server_free_platform(server); - - TinyHTTPMemoryFunc memfunc = server->memfunc; - void *memfuncdata = server->memfuncdata; - memfunc(TINYHTTP_MEM_FREE, server, sizeof(TinyHTTPServer), memfuncdata); + free(server); } int tinyhttp_server_wait(TinyHTTPServer *server, TinyHTTPRequest **req, @@ -2408,6 +2433,18 @@ void tinyhttp_response_body_ack(TinyHTTPResponse res, ptrdiff_t num) tinyhttp_stream_response_body_ack(stream, num); } +void tinyhttp_response_body(TinyHTTPResponse res, char *src, int len) +{ + if (len < 0) len = strlen(src); + + tinyhttp_response_body_setmincap(res, len); + ptrdiff_t cap; + char *dst = tinyhttp_response_body_buf(res, &cap); + if (dst) + memcpy(dst, src, len); + tinyhttp_response_body_ack(res, len); +} + void tinyhttp_response_send(TinyHTTPResponse res) { TinyHTTPStream *stream = response_to_stream(res); diff --git a/tinyhttp.h b/tinyhttp.h index 1f22a1d..566875d 100644 --- a/tinyhttp.h +++ b/tinyhttp.h @@ -43,23 +43,27 @@ typedef struct TinyHTTPServer TinyHTTPServer; typedef struct TinyHTTPRouter TinyHTTPRouter; +typedef struct { + const char *ptr; + ptrdiff_t len; +} TinyHTTPString; + +#define TINYHTTP_STRING(X) ((TinyHTTPString) {(X), sizeof(X)-1}) + typedef enum { TINYHTTP_METHOD_GET, TINYHTTP_METHOD_POST, } TinyHTTPMethod; typedef struct { - char *name; - ptrdiff_t name_len; - char *value; - ptrdiff_t value_len; + TinyHTTPString name; + TinyHTTPString value; } TinyHTTPHeader; typedef struct { TinyHTTPMethod method; int minor; - char* path; - ptrdiff_t path_len; + TinyHTTPString path; int num_headers; TinyHTTPHeader headers[TINYHTTP_HEADER_LIMIT]; char* body; @@ -167,6 +171,9 @@ typedef struct { TinyHTTPRequest req; } TinyHTTPStream; +int tinyhttp_streq(TinyHTTPString s1, TinyHTTPString s2); +int tinyhttp_streqcase(TinyHTTPString s1, TinyHTTPString s2); + // Initializes an HTTP stream // // TODO: Comment on memfunc @@ -262,8 +269,7 @@ void tinyhttp_stream_response_header(TinyHTTPStream *stream, const char *fmt, .. void tinyhttp_stream_response_header_fmt(TinyHTTPStream *stream, const char *fmt, va_list args); // TODO: Comment -// TODO: Implement -void tinyhttp_stream_response_body(TinyHTTPStream *stream, char *src, int len); +void tinyhttp_stream_response_body(TinyHTTPStream *stream, const char *src, int len); // TODO: Comment void tinyhttp_stream_response_body_setmincap(TinyHTTPStream *stream, ptrdiff_t mincap); @@ -281,8 +287,7 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream); void tinyhttp_stream_response_undo(TinyHTTPStream *stream); // TODO: Comment -TinyHTTPServer *tinyhttp_server_init(TinyHTTPServerConfig config, - TinyHTTPMemoryFunc memfunc, void *memfuncdata); +TinyHTTPServer *tinyhttp_server_init(TinyHTTPServerConfig config); // TODO: Comment void tinyhttp_server_free(TinyHTTPServer *server); @@ -312,6 +317,9 @@ char *tinyhttp_response_body_buf(TinyHTTPResponse res, ptrdiff_t *cap); // TODO: Comment void tinyhttp_response_body_ack(TinyHTTPResponse res, ptrdiff_t num); +// TODO: Comment +void tinyhttp_response_body(TinyHTTPResponse res, char *src, int len); + // TODO: Comment void tinyhttp_response_send(TinyHTTPResponse res); @@ -328,4 +336,4 @@ void tinyhttp_router_free(TinyHTTPRouter *router); void tinyhttp_router_resolve(TinyHTTPRouter *router, TinyHTTPServer *server, TinyHTTPRequest *request, TinyHTTPResponse response); // TODO: Comment -void tinyhttp_router_dir(TinyHTTPRouter *router, const char *endpoint, const char *path, int dir_listing); \ No newline at end of file +void tinyhttp_router_dir(TinyHTTPRouter *router, const char *endpoint, const char *path, int dir_listing);