commit 08956a84cf3d021536297cbf879b22a997b40e9c Author: Francesco Cozzuto Date: Thu Apr 17 02:18:33 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8096e02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +blog +blog.exe diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a11e55 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ + +all: + gcc example.c tinyhttp.c -o blog -Wall -Wextra -ggdb -lws2_32 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/blog.c b/blog.c new file mode 100644 index 0000000..30d6c9b --- /dev/null +++ b/blog.c @@ -0,0 +1,73 @@ +#include +#include "tinyhttp.h" + +sig_atomic_t should_exit = 0; + +static void +signal_handler(int sig) +{ + if (sig == SIGINT) + should_exit = 1; +} + +static void *memfunc(TinyHTTPMemoryFuncTag tag, + void *ptr, int len, void *data) +{ + switch (tag) { + + case TINYHTTP_MEM_MALLOC: + return malloc(len); + + case TINYHTTP_MEM_REALLOC: + return realloc(ptr, len); + + case TINYHTTP_MEM_FREE: + free(ptr); + return NULL; + } + return NULL; +} + +int main(void) +{ + signal(SIGINT, signal_handler); + + TinyHTTPServerConfig config = { + + .reuse = 1, + + .plain_addr = "127.0.0.1", + .plain_port = 8080, + + .secure = 0, + .secure_addr = "127.0.0.1", + .secure_port = 8443, + + .cert_file = "cert.pem", + .private_key_file = "privkey.pem", + }; + + TinyHTTPServer *server = tinyhttp_server_init(config, memfunc, NULL); + if (server == NULL) + return -1; + + TinyHTTPRouter *router = tinyhttp_router_init(); + if (router == NULL) { + tinyhttp_server_free(server); + return -1; + } + + tinyhttp_router_dir(router, "/", "/docroot", 0); + + while (!should_exit) { + TinyHTTPRequest *req; + TinyHTTPResponse res; + if (tinyhttp_server_wait(server, &req, &res, 1000)) + continue; // Timeout or error + tinyhttp_router_resolve(router, server, req, res); + } + + tinyhttp_router_free(router); + tinyhttp_server_free(server); + return 0; +} \ No newline at end of file diff --git a/example.c b/example.c new file mode 100644 index 0000000..074517e --- /dev/null +++ b/example.c @@ -0,0 +1,69 @@ +#include +#include +#include "tinyhttp.h" + +sig_atomic_t should_exit = 0; + +static void +signal_handler(int sig) +{ + if (sig == SIGINT) + 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_REALLOC: + return realloc(ptr, len); + + case TINYHTTP_MEM_FREE: + free(ptr); + return NULL; + } + return NULL; +} + +int main(void) +{ + signal(SIGINT, signal_handler); + + TinyHTTPServerConfig config = { + + .reuse = 1, + + .plain_addr = "127.0.0.1", + .plain_port = 8080, + .plain_backlog = 32, + + .secure = 1, + .secure_addr = "127.0.0.1", + .secure_port = 8443, + .secure_backlog = 32, + + .cert_file = "cert.pem", + .private_key_file = "privkey.pem", + }; + + TinyHTTPServer *server = tinyhttp_server_init(config, memfunc, NULL); + if (server == NULL) + return -1; + + while (!should_exit) { + TinyHTTPRequest *req; + TinyHTTPResponse res; + if (tinyhttp_server_wait(server, &req, &res, 1000)) + continue; // Timeout or error + tinyhttp_response_status(res, 200); + tinyhttp_response_send(res); + } + + tinyhttp_server_free(server); + return 0; +} diff --git a/tinyhttp.c b/tinyhttp.c new file mode 100644 index 0000000..7d22cd4 --- /dev/null +++ b/tinyhttp.c @@ -0,0 +1,2354 @@ +#include "tinyhttp.h" + +#include +#include +#include +#include + +#ifdef TINYHTTP_SERVER_ENABLE +#if defined(_WIN32) +#include +#include +#define CLOSESOCKET closesocket +#elif defined(__linux__) +#include +#include +#include +#include +#include +#include +#include +#define SOCKET int +#define CLOSESOCKET close +#define INVALID_SOCKET -1 +#endif +#endif + +#define MAX_U16 ((unsigned short) -1) +#define MAX_U32 ((unsigned int) -1) +#define MAX_U64 ((unsigned long long) -1ULL) + +#define ASSERT(X) {if (!(X)) __builtin_trap();} +#define COUNTOF(X) (sizeof(X)/sizeof((X)[0])) + +//////////////////////////////////////////////////////////////////////////////////// +// HTTP REQUEST PARSER // +//////////////////////////////////////////////////////////////////////////////////// + +static ptrdiff_t +parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) +{ + ptrdiff_t off; + ptrdiff_t cur = 0; + + int found = 0; + for (ptrdiff_t cur = 0; len - cur > 3; cur++) { + if (src[cur+0] == '\r' && + src[cur+1] == '\n' && // Boyer-Moore? + src[cur+2] == '\r' && + src[cur+3] == '\n') { + found = 1; + break; + } + } + if (!found) + return 0; + + if (len - cur > 3 + && src[cur+0] == 'G' + && src[cur+1] == 'E' + && src[cur+2] == 'T' + && src[cur+3] == ' ') { + cur += 4; + req->method = TINYHTTP_METHOD_GET; + } else if (len - cur > 4 + && src[cur+0] == 'P' + && src[cur+1] == 'O' + && src[cur+2] == 'S' + && src[cur+3] == 'T' + && src[cur+4] == ' ') { + cur += 5; + req->method = TINYHTTP_METHOD_POST; + } else + return -405; + + off = cur; + while (cur < len && src[cur] != ' ') // TODO: More robust + cur++; + req->path = src + off; + req->path_len = cur - off; + + if (len - cur <= 5 + || src[cur+0] != ' ' + || src[cur+1] != 'H' + || src[cur+2] != 'T' + || src[cur+3] != 'T' + || src[cur+4] != 'P' + || src[cur+5] != '/') + return -400; + cur += 6; + + if (3 < len - cur + && src[cur+0] == '1' + && src[cur+1] == '.' + && src[cur+2] == '1' + && src[cur+3] == '\r' + && src[cur+4] == '\n') { + cur += 5; + req->minor = 1; + } else if (4 < len - cur + && src[cur+0] == '1' + && src[cur+1] == '.' + && src[cur+2] == '0' + && src[cur+3] == '\r' + && src[cur+4] == '\n') { + cur += 5; + req->minor = 0; + } else if (2 < len - cur + && src[cur+0] == '1' + && src[cur+1] == '\r' + && src[cur+2] == '\n') { + cur += 3; + req->minor = 0; + } else { + return -505; + } + + req->num_headers = 0; + while (len - cur < 2 || src[cur+0] != '\r' || src[cur+1] != '\n') { + + ptrdiff_t name_off = cur; + while (cur < len && src[cur] != ':') // TODO: robust + cur++; + ptrdiff_t name_len = cur - name_off; + + if (cur == len) + return -400; + cur++; + + ptrdiff_t value_off = cur; + while (cur < len && src[cur] != '\r') + cur++; + ptrdiff_t value_len = cur - value_off; + + if (cur == len) + return -400; + cur++; + + if (cur == len || src[cur] != '\n') + return -400; + cur++; + + // TODO: Validate name and value: + // 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; + } + } + 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) +{ + for (int i = 0; i < req->num_headers; i++) { + TinyHTTPHeader *header = &req->headers[i]; + if (eqstrnocase(header->name, header->name_len, name, strlen(name))) + return i; + } + return -1; +} + +enum { + TRANSFER_ENCODING_CHUNKED, + TRANSFER_ENCODING_COMPRESS, + TRANSFER_ENCODING_DEFLATE, + TRANSFER_ENCODING_GZIP, +}; + +static int +parse_transfer_encoding(char *src, ptrdiff_t len, int *items, int max) +{ + int num = 0; + ptrdiff_t cur = 0; + for (;;) { + + while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) + cur++; + + if (6 < len - cur + && src[cur+0] == 'c' + && src[cur+1] == 'h' + && src[cur+2] == 'u' + && src[cur+3] == 'n' + && src[cur+4] == 'k' + && src[cur+5] == 'e' + && src[cur+6] == 'd') { + if (num == max) + return -1; + items[num++] = TRANSFER_ENCODING_CHUNKED; + cur += 7; + } else if (7 < len - cur + && src[cur+0] == 'c' + && src[cur+1] == 'o' + && src[cur+2] == 'm' + && src[cur+3] == 'p' + && src[cur+4] == 'r' + && src[cur+5] == 'e' + && src[cur+6] == 's' + && src[cur+7] == 's') { + if (num == max) + return -1; + items[num++] = TRANSFER_ENCODING_COMPRESS; + cur += 8; + } else if (6 < len - cur + && src[cur+0] == 'd' + && src[cur+1] == 'e' + && src[cur+2] == 'f' + && src[cur+3] == 'l' + && src[cur+4] == 'a' + && src[cur+5] == 't' + && src[cur+6] == 'e') { + if (num == max) + return -1; + items[num++] = TRANSFER_ENCODING_DEFLATE; + cur += 7; + } else if (3 < len - cur + && src[cur+0] == 'g' + && src[cur+1] == 'z' + && src[cur+2] == 'i' + && src[cur+3] == 'p') { + if (num == max) + return -1; + items[num++] = TRANSFER_ENCODING_GZIP; + cur += 4; + } else { + return -1; + } + + while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) + cur++; + + if (cur == len) + break; + + if (src[cur] != ',') + return -1; + cur++; + } + + return num; +} + +static int is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int +parse_content_length(char *src, ptrdiff_t len, unsigned long long *out) +{ + ptrdiff_t cur = 0; + while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) + cur++; + + if (cur == len || !is_digit(src[cur])) + return -1; + + unsigned long long buf = 0; + do { + int d = src[cur++] - '0'; + if (buf > (MAX_U64 - d) / 10) + return -1; + buf = buf * 10 + d; + } while (cur < len && is_digit(src[cur])); + + *out = buf; + return 0; +} + +static int +parse_request(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPRequest *req) +{ + ptrdiff_t ret = parse_request_head(src, len, req); + if (ret <= 0) + return ret; + ptrdiff_t head_len = ret; + + int transfer_encoding_index = find_header(req, "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)); + if (num < 0) + return -400; + + for (;;) { + + return -501; // TODO: Parse chunks + } + + return 1; + } + + int content_length_index = find_header(req, "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) + return -400; + if (content_length > body_limit || content_length > MAX_U32) + return -413; + if (content_length < (unsigned long long) (len - head_len)) + return 0; + + req->body = src + head_len; + req->body_len = content_length; + return head_len + content_length; + } + + req->body = NULL; + req->body_len = 0; + return head_len; +} + +//////////////////////////////////////////////////////////////////////////////////// +// BYTE QUEUE // +//////////////////////////////////////////////////////////////////////////////////// +// +// This is the implementation of a byte queue useful +// for systems that need to process streams of bytes. +// +// It features sticky errors, a zero-copy interface, +// and a safe mechanism to patch previously written +// bytes. +// +// Only up to 4GB of data can be stored at once. + +static void* +callback_malloc(TinyHTTPByteQueue *queue, ptrdiff_t len) +{ + return queue->memfunc(TINYHTTP_MEM_MALLOC, NULL, len, queue->memfuncdata); +} + +static void* +callback_realloc(TinyHTTPByteQueue *queue, void *ptr, ptrdiff_t len) +{ + return queue->memfunc(TINYHTTP_MEM_REALLOC, ptr, len, queue->memfuncdata); +} + +static void +callback_free(TinyHTTPByteQueue *queue, void *ptr, ptrdiff_t len) +{ + queue->memfunc(TINYHTTP_MEM_FREE, ptr, len, queue->memfuncdata); +} + + +#include +static void +print_bytes(char *prefix, char *src, int len) +{ + if (src == NULL) { + printf("%s (null)\n", prefix); + return; + } + + int cur = 0; + int newline = 1; + while (cur < len) { + int start = cur; + while (cur < len && src[cur] != '\n' && src[cur] != '\r') + cur++; + if (newline) { + printf("%s", prefix); + newline = 0; + } + printf("%.*s", cur - start, src + start); + if (cur < len) { + if (src[cur] == '\r') + printf("\\r"); + else { + printf("\\n\n"); + newline = 1; + } + cur++; + } + } + if (cur > 0 && src[cur-1] != '\n') + printf("\n"); +} + +// Initialize the queue +static void +byte_queue_init(TinyHTTPByteQueue *queue, unsigned int limit, TinyHTTPMemoryFunc memfunc, void *memfuncdata) +{ + queue->flags = 0; + queue->lock = 0; + queue->head = 0; + queue->size = 0; + queue->used = 0; + queue->curs = 0; + queue->limit = limit; + queue->data = NULL; + queue->read_target = NULL; + queue->memfunc = memfunc; + queue->memfuncdata = memfuncdata; +} + +// Deinitialize the queue +static void +byte_queue_free(TinyHTTPByteQueue *queue) +{ + if (queue->read_target) { + if (queue->read_target != queue->data) + callback_free(queue, queue->read_target, queue->read_target_size); + queue->read_target = NULL; + queue->read_target_size = 0; + } + + callback_free(queue, queue->data, queue->size); + queue->data = NULL; +} + +static int +byte_queue_error(TinyHTTPByteQueue *queue) +{ + return queue->flags & BYTE_QUEUE_ERROR; +} + +static void +byte_queue_setlimit(TinyHTTPByteQueue *queue, unsigned int value) +{ + queue->limit = value; +} + +static char* +byte_queue_peek(TinyHTTPByteQueue *queue, ptrdiff_t *len) +{ + if ((queue->flags & (BYTE_QUEUE_ERROR)) || queue->data == NULL) { + *len = 0; + return NULL; + } + + *len = queue->used; + return queue->data + queue->head; +} + +// Start a read operation on the queue. +// +// This function returnes the pointer to the memory region containing the bytes +// to read. Callers can't read more than [*len] bytes from it. To complete the +// read, the [byte_queue_read_ack] function must be called with the number of +// bytes that were acknowledged by the caller. +// +// Note: +// - You can't have more than one pending read. +static char* +byte_queue_read_buf(TinyHTTPByteQueue *queue, ptrdiff_t *len) +{ + if ((queue->flags & (BYTE_QUEUE_ERROR)) || queue->data == NULL) { + *len = 0; + return NULL; + } + + ASSERT((queue->flags & BYTE_QUEUE_READ) == 0); + queue->flags |= BYTE_QUEUE_READ; + queue->read_target = queue->data; + queue->read_target_size = queue->size; + + *len = queue->used; + return queue->data + queue->head; +} + +// Complete a previously started operation on the queue. +static void +byte_queue_read_ack(TinyHTTPByteQueue *queue, ptrdiff_t num) +{ + if (queue->flags & BYTE_QUEUE_ERROR) + return; + + if ((queue->flags & BYTE_QUEUE_READ) == 0) + return; + + queue->flags &= ~BYTE_QUEUE_READ; + + ASSERT(num <= queue->used); + queue->head += num; + queue->used -= num; + queue->curs += num; + + if (queue->read_target) { + if (queue->read_target != queue->data) + callback_free(queue, queue->read_target, queue->read_target_size); + queue->read_target = NULL; + queue->read_target_size = 0; + } +} + +static int +byte_queue_read_started(TinyHTTPByteQueue *queue) +{ + return (queue->flags & BYTE_QUEUE_READ) == BYTE_QUEUE_READ; +} + +static char* +byte_queue_write_buf(TinyHTTPByteQueue *queue, ptrdiff_t *cap) +{ + if ((queue->flags & BYTE_QUEUE_ERROR) || queue->data == NULL) { + *cap = 0; + return NULL; + } + + ASSERT((queue->flags & BYTE_QUEUE_WRITE) == 0); + queue->flags |= BYTE_QUEUE_WRITE; + + *cap = queue->size - (queue->head + queue->used); + return queue->data + (queue->head + queue->used); +} + +static void +byte_queue_write_ack(TinyHTTPByteQueue *queue, ptrdiff_t num) +{ + if (queue->flags & BYTE_QUEUE_ERROR) + return; + + if ((queue->flags & BYTE_QUEUE_WRITE) == 0) + return; + + queue->flags &= ~BYTE_QUEUE_WRITE; + queue->used += num; +} + +static int +byte_queue_write_started(TinyHTTPByteQueue *queue) +{ + return (queue->flags & BYTE_QUEUE_WRITE) == BYTE_QUEUE_WRITE; +} + +// Sets the minimum capacity for the next write operation +// and returns 1 if the content of the queue was moved, else +// 0 is returned. +// +// You must not call this function while a write is pending. +// In other words, you must do this: +// +// byte_queue_write_setmincap(queue, mincap); +// dst = byte_queue_write_buf(queue, &cap); +// ... +// byte_queue_write_ack(num); +// +// And NOT this: +// +// dst = byte_queue_write_buf(queue, &cap); +// byte_queue_write_setmincap(queue, mincap); <-- BAD +// ... +// byte_queue_write_ack(num); +// +static int +byte_queue_write_setmincap(TinyHTTPByteQueue *queue, ptrdiff_t mincap) +{ + // Sticky error + if (queue->flags & BYTE_QUEUE_ERROR) + return 0; + + // In general, the queue's contents look like this: + // + // size + // v + // [___xxxxxxxxxxxx________] + // ^ ^ ^ + // 0 head head + used + // + // This function needs to make sure that at least [mincap] + // bytes are available on the right side of the content. + // + // We have 3 cases: + // + // 1) If there is enough memory already, this function doesn't + // need to do anything. + // + // 2) If there isn't enough memory on the right but there is + // enough free memory if we cound the left unused region, + // then the content is moved back to the + // start of the buffer. + // + // 3) If there isn't enough memory considering both sides, this + // function needs to allocate a new buffer. + // + // If there are pending read or write operations, the application + // is holding pointers to the buffer, so we need to make sure + // to not invalidate them. The only real problem is pending reads + // since this function can only be called before starting a write + // opearation. + // + // To avoid invalidating the read pointer when we allocate a new + // buffer, we don't free the old buffer. Instead, we store the + // pointer in the "old" field so that the read ack function can + // free it. + // + // To avoid invalidating the pointer when we are moving back the + // content since there is enough memory at the start of the buffer, + // we just avoid that. Even if there is enough memory considering + // left and right free regions, we allocate a new buffer. + + ASSERT((queue->flags & BYTE_QUEUE_WRITE) == 0); + + unsigned int total_free_space = queue->size - queue->used; + unsigned int free_space_after_data = queue->size - queue->used - queue->head; + + int moved = 0; + if (free_space_after_data < mincap) { + + if (total_free_space < mincap || (queue->read_target == queue->data)) { + // Resize required + + if (queue->used + mincap > queue->limit) { + queue->flags |= BYTE_QUEUE_ERROR; + return 0; + } + + unsigned int size; + if (queue->size > MAX_U32 / 2) + size = MAX_U32; + else + size = 2 * queue->size; + + if (size < queue->used + mincap) + size = queue->used + mincap; + + if (size > queue->limit) + size = queue->limit; + + char *data = callback_malloc(queue, size); + if (!data) { + queue->flags |= BYTE_QUEUE_ERROR; + return 0; + } + + if (queue->used > 0) + memcpy(data, queue->data + queue->head, queue->used); + + if (queue->read_target != queue->data) + callback_free(queue, queue->data, queue->size); + + queue->data = data; + queue->head = 0; + queue->size = size; + + } else { + // Move required + memmove(queue->data, queue->data + queue->head, queue->used); + queue->head = 0; + } + + moved = 1; + } + + return moved; +} + +static TinyHTTPByteQueueOffset +byte_queue_offset(TinyHTTPByteQueue *queue) +{ + if (queue->flags & BYTE_QUEUE_ERROR) + return (TinyHTTPByteQueueOffset) { 0 }; + return (TinyHTTPByteQueueOffset) { queue->curs + queue->used }; +} + +static unsigned int +byte_queue_size_from_offset(TinyHTTPByteQueue *queue, TinyHTTPByteQueueOffset off) +{ + return queue->curs + queue->used - off; +} + +static void +byte_queue_patch(TinyHTTPByteQueue *queue, TinyHTTPByteQueueOffset off, + char *src, unsigned int len) +{ + // Check that the offset is in range + ASSERT(off >= queue->curs && off - queue->curs < queue->used); + + // Check that the length is in range + ASSERT(len <= queue->used - (off - queue->curs)); + + // Perform the patch + char *dst = queue->data + queue->head + (off - queue->curs); + memcpy(dst, src, len); +} + +static void +byte_queue_remove_after_lock(TinyHTTPByteQueue *queue) +{ + ASSERT(queue->flags & BYTE_QUEUE_LOCK); + + unsigned long long num = (queue->curs + queue->head) - queue->lock; + ASSERT(num <= queue->used); + + queue->used -= num; +} + +static void +byte_queue_write(TinyHTTPByteQueue *queue, const char *str) +{ + ptrdiff_t cap; + ptrdiff_t len = strlen(str); + byte_queue_write_setmincap(queue, len); + char *dst = byte_queue_write_buf(queue, &cap); + if (dst) memcpy(dst, str, len); + byte_queue_write_ack(queue, len); +} + +static void +byte_queue_write_fmt2(TinyHTTPByteQueue *queue, const char *fmt, va_list args) +{ + if (queue->flags & BYTE_QUEUE_ERROR) + return; + + va_list args2; + va_copy(args2, args); + + ptrdiff_t cap; + char *dst = byte_queue_write_buf(queue, &cap); + + int len = vsnprintf(dst, cap, fmt, args); + if (len < 0) { + queue->flags |= BYTE_QUEUE_ERROR; + va_end(args2); + va_end(args); + return; + } + + if (len > cap) { + byte_queue_write_ack(queue, 0); + byte_queue_write_setmincap(queue, len+1); + dst = byte_queue_write_buf(queue, &cap); + vsnprintf(dst, cap, fmt, args2); + } + + byte_queue_write_ack(queue, len); + + va_end(args2); + va_end(args); +} + +static void +byte_queue_write_fmt(TinyHTTPByteQueue *queue, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + byte_queue_write_fmt2(queue, fmt, args); + va_end(args); +} + +static unsigned int +byte_queue_read_size(TinyHTTPByteQueue *queue) +{ + if (queue->lock & BYTE_QUEUE_LOCK) + return queue->curs + queue->used - queue->lock; + return queue->used; +} + +static void +byte_queue_read_lock(TinyHTTPByteQueue *queue) +{ + ASSERT((queue->flags & BYTE_QUEUE_LOCK) == 0); + queue->lock = queue->curs + queue->used; + queue->flags |= BYTE_QUEUE_LOCK; +} + +static void +byte_queue_read_unlock(TinyHTTPByteQueue *queue) +{ + ASSERT(queue->flags & BYTE_QUEUE_LOCK); + queue->flags &= ~BYTE_QUEUE_LOCK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// HTTP STREAM // +//////////////////////////////////////////////////////////////////////////////////// + +#define EIGHT_ZEROS "00000000" +#define NINE_SPACES " " + +static const char* +get_status_text(int code) +{ + switch(code) { + + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 208: return "Already Reported"; + + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 306: return "Switch Proxy"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 418: return "I'm a teapot"; + case 420: return "Enhance your calm"; + case 422: return "Unprocessable Entity"; + case 426: return "Upgrade Required"; + case 429: return "Too many requests"; + case 431: return "Request Header Fields Too Large"; + case 449: return "Retry With"; + case 451: return "Unavailable For Legal Reasons"; + + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 509: return "Bandwidth Limit Exceeded"; + } + return "???"; +} + +// See tinyhttp.h +void tinyhttp_stream_init(TinyHTTPStream *stream, TinyHTTPMemoryFunc memfunc, void *memfuncdata) +{ + // Since we are the server, we are expecting + // the client to send data first. + stream->state = TINYHTTP_STREAM_RECV; + + // We only use the output state when a request + // has been received. + stream->output_state = TINYHTTP_OUTPUT_STATE_NONE; + + // Set the maximum content length + stream->bodylimit = 1<<29; // 500MB + + byte_queue_init(&stream->in, 1<<29, memfunc, memfuncdata); + byte_queue_init(&stream->out, 1<<29, memfunc, memfuncdata); +} + +// See tinyhttp.h +void tinyhttp_stream_free(TinyHTTPStream *stream) +{ + if (stream->state == TINYHTTP_STREAM_FREE) + return; + byte_queue_free(&stream->out); + byte_queue_free(&stream->in); + stream->state = TINYHTTP_STREAM_FREE; +} + +// See tinyhttp.h +int tinyhttp_stream_state(TinyHTTPStream *stream) +{ + // The state is stored in stream->state, but the + // TINYHTTP_STREAM_SEND and TINYHTTP_STREAM_RECV are evaluated + // lazily to avoid possible invalid states. + + int state = stream->state; + + // The TINYHTTP_STREAM_FREE state is exclusive + if (state == TINYHTTP_STREAM_FREE) + return state; + + // If there is data to read in the output buffer, + // we are interested in sending data. + if (byte_queue_read_size(&stream->out)) + state |= TINYHTTP_STREAM_SEND; + + // If we don't have a buffered request and the + // connection is not closing, we are interested + // in receiving data. + if ((state & (TINYHTTP_STREAM_READY | TINYHTTP_STREAM_CLOSE)) == 0) + state |= TINYHTTP_STREAM_RECV; + + if (byte_queue_write_started(&stream->in)) + state |= TINYHTTP_STREAM_RECV_STARTED; + + if (byte_queue_read_started(&stream->out)) + state |= TINYHTTP_STREAM_SEND_STARTED; + + if (stream->reqsize > 0) + state |= TINYHTTP_STREAM_READY; + + return state; +} + +// See tinyhttp.h +char *tinyhttp_stream_net_recv_buf(TinyHTTPStream *stream, ptrdiff_t *cap) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) { + *cap = 0; + return NULL; + } + + // Make sure at least this free space is available + ptrdiff_t minrecv = 1<<9; + int resized = byte_queue_write_setmincap(&stream->in, minrecv); + + // If the input data was moved and a request was + // buffered, we need to update the pointers of the + // parsed request. We do this by parsing again. + if (resized && (stream->state & TINYHTTP_STREAM_READY)) { + + char *src; + ptrdiff_t len; + + // Get the new input data location + src = byte_queue_peek(&stream->in, &len); + + // Parse again. We assume everything will go + // well as it did the first time. + parse_request(src, len, stream->bodylimit, &stream->req); + } + + // Forward the write region from the input buffer + return byte_queue_write_buf(&stream->in, cap); +} + +static int +should_keep_alive(TinyHTTPStream *stream) +{ + ASSERT(stream->reqsize > 0); + + // If the parent system doesn't want us to reuse + // the connection, we certainly can't keep alive. + if ((stream->state & TINYHTTP_STREAM_REUSE) == 0) + return 0; + + // If the client is using HTTP/1.0, we can't + // keep alive. + if (stream->req.minor == 0) + return 0; + + return 1; +} + +static void +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); + int ret = parse_request(src, len, stream->bodylimit, &stream->req); + + // Request is incomplete + if (ret == 0) { + byte_queue_read_ack(&stream->in, 0); + return; + } + + // Invalid request + if (ret < 0) { + + byte_queue_read_ack(&stream->in, 0); + + int status = -ret; + byte_queue_write_fmt(&stream->out, "HTTP/1.1 %d %s\r\n", status, get_status_text(status)); + if (byte_queue_error(&stream->out)) { + tinyhttp_stream_free(stream); + return; + } + stream->state |= TINYHTTP_STREAM_CLOSE; + return; + } + + // Request buffered + ASSERT(ret > 0); + + stream->reqsize = ret; + + // Start up the output state machine + stream->output_state = TINYHTTP_OUTPUT_STATE_STATUS; + + // Configure chunked coding for this request + stream->chunked = 0; + + // Determine whether this connection will be + // kept alive after this request/response exchange; + stream->keepalive = should_keep_alive(stream); + + // Don't allow bytes written to the output buffer + // from this point on to be send over the network + // until the response was completely buffered. + // + // This is because if an error occurs while responding, + // we may want to reset everything and start from + // scratch. + byte_queue_read_lock(&stream->out); +} + +// See tinyhttp.h +void tinyhttp_stream_net_recv_ack(TinyHTTPStream *stream, ptrdiff_t num) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + byte_queue_write_ack(&stream->in, num); + + // Since new data was ready, the state of the + // connection may need to change. + + // If a request was already buffered, the state + // won't change until a response is generated. + if (stream->reqsize > 0) + return; + + process_next_request(stream); +} + +// See tinyhttp.h +char *tinyhttp_stream_net_send_buf(TinyHTTPStream *stream, ptrdiff_t *len) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return NULL; + + return byte_queue_read_buf(&stream->out, len); +} + +// See tinyhttp.h +void tinyhttp_stream_net_send_ack(TinyHTTPStream *stream, ptrdiff_t num) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + byte_queue_read_ack(&stream->out, num); + + if (byte_queue_read_size(&stream->out) == 0 && (stream->state & TINYHTTP_STREAM_CLOSE)) { + tinyhttp_stream_free(stream); + return; + } +} + +// See tinyhttp.h +void tinyhttp_stream_setreuse(TinyHTTPStream *stream, int value) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (value) + stream->state |= TINYHTTP_STREAM_REUSE; + else + stream->state &= ~TINYHTTP_STREAM_REUSE; +} + +// See tinyhttp.h +void tinyhttp_stream_setbodylimit(TinyHTTPStream *stream, unsigned long long value) +{ + stream->bodylimit = value; +} + +// See tinyhttp.h +void tinyhttp_stream_setinbuflimit(TinyHTTPStream *stream, unsigned int value) +{ + byte_queue_setlimit(&stream->in, value); +} + +// See tinyhttp.h +void tinyhttp_stream_setoutbuflimit(TinyHTTPStream *stream, unsigned int value) +{ + byte_queue_setlimit(&stream->out, value); +} + +// See tinyhttp.h +TinyHTTPRequest *tinyhttp_stream_request(TinyHTTPStream *stream) +{ + if (stream->reqsize > 0) + return &stream->req; + return NULL; +} + +// See tinyhttp.h +void tinyhttp_stream_response_status(TinyHTTPStream *stream, int status) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (stream->output_state != TINYHTTP_OUTPUT_STATE_STATUS) { + if (stream->output_state != TINYHTTP_OUTPUT_STATE_NONE) + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + return; + } + + byte_queue_write_fmt(&stream->out, "HTTP/1.1 %d %s\r\n", status, get_status_text(status)); + + stream->output_state = TINYHTTP_OUTPUT_STATE_HEADER; +} + +// See tinyhttp.h +void tinyhttp_stream_response_header(TinyHTTPStream *stream, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + tinyhttp_stream_response_header_fmt(stream, fmt, args); + va_end(args); +} + +// See tinyhttp.h +void tinyhttp_stream_response_header_fmt(TinyHTTPStream *stream, const char *fmt, va_list args) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (stream->output_state != TINYHTTP_OUTPUT_STATE_HEADER) { + if (stream->output_state != TINYHTTP_OUTPUT_STATE_NONE) + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + return; + } + + byte_queue_write_fmt2(&stream->out, fmt, args); +} + +static void +append_special_headers(TinyHTTPStream *stream) +{ + if (stream->keepalive) + byte_queue_write(&stream->out, "Connection: Keep-Alive\r\n"); + else { + if (stream->req.minor > 0) + byte_queue_write(&stream->out, "Connection: Close\r\n"); + } + + if (stream->chunked) + byte_queue_write(&stream->out, "Transfer-Encoding: Chunked\r\n"); + else { + byte_queue_write(&stream->out, "Content-Length: "); + stream->content_length_value_offset = byte_queue_offset(&stream->out); + byte_queue_write(&stream->out, NINE_SPACES "\r\n"); + } + + byte_queue_write(&stream->out, "\r\n"); + stream->content_length_offset = byte_queue_offset(&stream->out); +} + +// See tinyhttp.h +void tinyhttp_stream_response_body_setmincap(TinyHTTPStream *stream, ptrdiff_t mincap) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_HEADER) { + append_special_headers(stream); + stream->output_state = TINYHTTP_OUTPUT_STATE_BODY; + } + + if (stream->output_state != TINYHTTP_OUTPUT_STATE_BODY) { + if (stream->output_state != TINYHTTP_OUTPUT_STATE_NONE) + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + return; + } + + // Always add some extra padding in case we need + // to append a chunk header. + byte_queue_write_setmincap(&stream->out, mincap + 20); +} + +// See tinyhttp.h +char *tinyhttp_stream_response_body_buf(TinyHTTPStream *stream, ptrdiff_t *cap) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return NULL; + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_HEADER) { + append_special_headers(stream); + stream->output_state = TINYHTTP_OUTPUT_STATE_BODY; + } + + if (stream->output_state != TINYHTTP_OUTPUT_STATE_BODY) { + if (stream->output_state != TINYHTTP_OUTPUT_STATE_NONE) + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + *cap = 0; + return NULL; + } + + if (stream->chunked) + byte_queue_write(&stream->out, EIGHT_ZEROS "\r\n"); + + return byte_queue_write_buf(&stream->out, cap); +} + +// See tinyhttp.h +void tinyhttp_stream_response_body_ack(TinyHTTPStream *stream, ptrdiff_t num) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (stream->output_state != TINYHTTP_OUTPUT_STATE_BODY) { + if (stream->output_state != TINYHTTP_OUTPUT_STATE_NONE) + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + return; + } + + if (stream->chunked) { + + if ((num & 0xffffffff)) { + stream->output_state = TINYHTTP_OUTPUT_STATE_ERROR; + return; + } + + char tmp[8]; + tmp[7] = (num >> 28) & 0xF; + tmp[6] = (num >> 24) & 0xF; + tmp[5] = (num >> 20) & 0xF; + tmp[4] = (num >> 16) & 0xF; + tmp[3] = (num >> 12) & 0xF; + tmp[2] = (num >> 8) & 0xF; + tmp[1] = (num >> 4) & 0xF; + tmp[0] = (num >> 0) & 0xF; + + for (int i = 0; i < 8; i++) { + if (tmp[i] < 10) + tmp[i] += '0'; + else + tmp[i] += 'a'; + } + + byte_queue_patch(&stream->out, byte_queue_offset(&stream->out) - 10, tmp, 8); + } + + byte_queue_write_ack(&stream->out, num); + byte_queue_write(&stream->out, "\r\n"); +} + +// See tinyhttp.h +void tinyhttp_stream_response_send(TinyHTTPStream *stream) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_NONE) + return; + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_HEADER) { + append_special_headers(stream); + stream->output_state = TINYHTTP_OUTPUT_STATE_BODY; + } + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_BODY) { + if (stream->chunked) + byte_queue_write(&stream->out, "0\r\n\r\n"); + else { + + ptrdiff_t content_length = byte_queue_size_from_offset(&stream->out, stream->content_length_offset); + ASSERT(content_length >= 0); + + if (content_length > MAX_U32) { + // TODO + } + + char tmp[9]; + unsigned int magn = 1000000000; + for (int i = 0; i < 9; i++) { + tmp[i] = '0' + content_length / magn; + content_length %= magn; + magn /= 10; + } + + int i = 0; + while (i < 8 && tmp[i] == '0') + i++; + + byte_queue_patch(&stream->out, stream->content_length_value_offset, tmp + i, 9 - i); + } + } + + if (stream->output_state == TINYHTTP_OUTPUT_STATE_ERROR) { + byte_queue_remove_after_lock(&stream->out); + byte_queue_write(&stream->out, + "HTTP/1.1 500 Internal Server Error\r\n" + "Content-Length: 0\r\n" + "Connection: Close\r\n" + "\r\n" + ); + } + + if (byte_queue_error(&stream->out)) { + tinyhttp_stream_free(stream); + return; + } + + //ptrdiff_t ressize = (byte_queue_offset(&stream->out) - stream->out.lock); + + byte_queue_read_ack(&stream->in, stream->reqsize); + byte_queue_read_unlock(&stream->out); + stream->reqsize = 0; + + process_next_request(stream); +} + +// See tinyhttp.h +void tinyhttp_stream_response_undo(TinyHTTPStream *stream) +{ + // Sticky error + if (stream->state == TINYHTTP_STREAM_FREE) + return; + + byte_queue_remove_after_lock(&stream->out); + stream->output_state = TINYHTTP_OUTPUT_STATE_STATUS; +} + +//////////////////////////////////////////////////////////////////////////////////// +// HTTP SERVER // +//////////////////////////////////////////////////////////////////////////////////// + +struct TinyHTTPServer { +#if defined(_WIN32) + HANDLE iocp; + OVERLAPPED plain_accept_overlapped; + OVERLAPPED secure_accept_overlapped; + SOCKET plain_accept_target; + SOCKET secure_accept_target; + OVERLAPPED recv_overlapped[TINYHTTP_SERVER_CONN_LIMIT]; + OVERLAPPED send_overlapped[TINYHTTP_SERVER_CONN_LIMIT]; +#elif defined(__linux__) + int epoll_fd; +#endif + TinyHTTPMemoryFunc memfunc; + void *memfuncdata; + SOCKET plain_listen_socket; + SOCKET secure_listen_socket; + int num_conns; + int ready_head; + int ready_count; + int ready_queue[TINYHTTP_SERVER_CONN_LIMIT]; + unsigned short stream_gens[TINYHTTP_SERVER_CONN_LIMIT]; + TinyHTTPStream stream_state[TINYHTTP_SERVER_CONN_LIMIT]; + SOCKET stream_sockets[TINYHTTP_SERVER_CONN_LIMIT]; +}; + +static int +socket_set_block(SOCKET fd, int value) +{ +#if defined(__linux__) + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + + if (value) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + if (fcntl(fd, F_SETFL, flags) < 0) + return -1; + + return 0; +#elif defined(_WIN32) + u_long mode = !value; + int ret = ioctlsocket(fd, FIONBIO, &mode); + if (ret == SOCKET_ERROR) + return -1; + return 0; +#else + return -1; +#endif +} + +static SOCKET +socket_listen(const char *addr, int port, int backlog, int reuse) +{ + SOCKET fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == INVALID_SOCKET) + return INVALID_SOCKET; + + if (socket_set_block(fd, 0) < 0) { + CLOSESOCKET(fd); + return INVALID_SOCKET; + } + + if (reuse) { + int one = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*) &one, sizeof(one)) < 0) { + CLOSESOCKET(fd); + return INVALID_SOCKET; + } + } + + struct in_addr bind_buf2; + if (addr == NULL) + bind_buf2.s_addr = htonl(INADDR_ANY); + else if (inet_pton(AF_INET, addr, &bind_buf2) != 1) { + CLOSESOCKET(fd); + return INVALID_SOCKET; + } + + struct sockaddr_in bind_buf; + bind_buf.sin_family = AF_INET; + bind_buf.sin_port = htons(port); + bind_buf.sin_addr = bind_buf2; + + if (bind(fd, (struct sockaddr*) &bind_buf, sizeof(bind_buf)) < 0) { + CLOSESOCKET(fd); + return INVALID_SOCKET; + } + + if (listen(fd, backlog) < 0) { + CLOSESOCKET(fd); + return INVALID_SOCKET; + } + + return fd; +} + +static TinyHTTPStream* +response_to_stream(TinyHTTPResponse res) +{ + if (res.server == NULL) + return NULL; + if (res.idx >= TINYHTTP_SERVER_CONN_LIMIT) + return NULL; + if (res.gen != res.server->stream_gens[res.idx]) + return NULL; + return &res.server->stream_state[res.idx]; +} + +// TODO: The generation counters allow freeing streams while the user is still +// holding a response handle that references them, but the user may still +// be holding the pointer to the request structure. How to fix this? Should +// callers only be allowed to access TinyHTTPRequest before the next call +// to [tinyhttp_server_wait]? Or should connections be marked as pending +// until [tinyhttp_server_send] is called? + +TinyHTTPServer* tinyhttp_server_init(TinyHTTPServerConfig config, + TinyHTTPMemoryFunc memfunc, void *memfuncdata) +{ + TinyHTTPServer *server = memfunc(TINYHTTP_MEM_MALLOC, NULL, sizeof(TinyHTTPServer), memfuncdata); + if (server == NULL) + return NULL; + + server->memfunc = memfunc; + server->memfuncdata = memfuncdata; + + server->num_conns = 0; + server->ready_head = 0; + server->ready_count = 0; + + for (int i = 0; i < TINYHTTP_SERVER_CONN_LIMIT; i++) { + server->stream_gens[i] = 1; + server->stream_sockets[i] = INVALID_SOCKET; + } + + server->plain_listen_socket = socket_listen(config.plain_addr, + config.plain_port, config.plain_backlog, config.reuse); + if (server->plain_listen_socket == INVALID_SOCKET) { + free(server); + return NULL; + } + + if (config.secure) { + + server->secure_listen_socket = socket_listen(config.secure_addr, + config.secure_port, config.secure_backlog, config.reuse); + if (server->secure_listen_socket == INVALID_SOCKET) { + CLOSESOCKET(server->plain_listen_socket); + free(server); + return NULL; + } + + } else + server->secure_listen_socket = INVALID_SOCKET; + +#if defined(_WIN32) + server->iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); + if (server->iocp == INVALID_HANDLE_VALUE) { + CLOSESOCKET(server->plain_listen_socket); + if (server->secure_listen_socket != INVALID_SOCKET) + CLOSESOCKET(server->secure_listen_socket); + free(server); + return NULL; + } +#elif defined(__linux__) + server->epoll_fd = epoll_create1(0); + if (server->epoll_fd < 0) { + CLOSESOCKET(server->plain_listen_socket); + if (server->secure_listen_socket != INVALID_SOCKET) + CLOSESOCKET(server->secure_listen_socket); + free(server); + return NULL; + } + struct epoll_event epoll_buf; + epoll_buf.data.fd = -1; + epoll_buf.events = EPOLLIN; + if (epoll_ctl(server->epoll_fd, EPOLL_CTL_ADD, server->plain_listen_socket, &epoll_buf) < 0) { + CLOSESOCKET(server->plain_listen_socket); + if (server->secure_listen_socket != INVALID_SOCKET) + CLOSESOCKET(server->secure_listen_socket); + close(server->epoll_fd); + free(server); + return NULL; + } + if (config.secure) { + epoll_buf.data.fd = -2; + epoll_buf.events = EPOLLIN; + if (epoll_ctl(server->epoll_fd, EPOLL_CTL_ADD, server->secure_listen_socket, &epoll_buf) < 0) { + CLOSESOCKET(server->plain_listen_socket); + if (server->secure_listen_socket != INVALID_SOCKET) + CLOSESOCKET(server->secure_listen_socket); + close(server->epoll_fd); + free(server); + return NULL; + } + } +#endif + + return server; +} + +void tinyhttp_server_free(TinyHTTPServer *server) +{ + for (int i = 0; i < TINYHTTP_SERVER_CONN_LIMIT; i++) { + if (server->stream_sockets[i] != INVALID_SOCKET) { + CLOSESOCKET(server->stream_sockets[i]); + tinyhttp_stream_free(&server->stream_state[i]); + } + } + + CLOSESOCKET(server->plain_listen_socket); + + if (server->secure_listen_socket != INVALID_SOCKET) + CLOSESOCKET(server->secure_listen_socket); + +#if defined(_WIN32) + CloseHandle(server->iocp); +#elif defined(__linux__) + close(server->epoll_fd); +#endif + + TinyHTTPMemoryFunc memfunc = server->memfunc; + void *memfuncdata = server->memfuncdata; + memfunc(TINYHTTP_MEM_FREE, server, sizeof(TinyHTTPServer), memfuncdata); +} + +static void +invalidate_handles_to_stream(TinyHTTPServer *server, TinyHTTPStream *stream) +{ + int idx = stream - server->stream_state; + unsigned short *gen = &server->stream_gens[idx]; + (*gen)++; + if (*gen == MAX_U16 || *gen == 0) + *gen = 1; +} + +#if defined(__linux__) + +static unsigned long long +get_current_time_ms(void) +{ + struct timespec ts; + int result = clock_gettime(CLOCK_REALTIME, &ts); + if (result) + return MAX_U64; + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int +accept_from_listen_socket(TinyHTTPServer *server, SOCKET listen_socket, int secure) +{ + int errors = 0; + while (server->num_conns < TINYHTTP_SERVER_CONN_LIMIT) { + SOCKET accepted_socket = accept(listen_socket, NULL, NULL); + if (accepted_socket == INVALID_SOCKET) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + errors++; + if (errors == 1000) + break; + continue; + } + errors = 0; + + if (socket_set_block(accepted_socket, 0)) { + CLOSESOCKET(accepted_socket); + continue; + } + + int idx = 0; + while (server->stream_sockets[idx] != INVALID_SOCKET) + idx++; + + server->stream_sockets[idx] = accepted_socket; + TinyHTTPStream *stream = &server->stream_state[idx]; + + tinyhttp_stream_init(stream, server->memfunc, server->memfuncdata); + int state = tinyhttp_stream_state(stream); + + struct epoll_event epoll_buf; + epoll_buf.data.fd = idx; + epoll_buf.events = 0; + if (state & TINYHTTP_STREAM_RECV) epoll_buf.events |= EPOLLIN; + if (state & TINYHTTP_STREAM_SEND) epoll_buf.events |= EPOLLOUT; + if (epoll_ctl(server->epoll_fd, EPOLL_CTL_ADD, accepted_socket, &epoll_buf) < 0) { + CLOSESOCKET(accepted_socket); + tinyhttp_stream_free(stream); + continue; + } + + if (secure) { + // TODO + } + + server->num_conns++; + } + return 0; +} + +static int +process_network_events(TinyHTTPServer *server, int timeout) +{ + // TODO: timeouts + + struct epoll_event batch[TINYHTTP_SERVER_EPOLL_BATCH_SIZE]; + + int num; + do + num = epoll_wait(server->epoll_fd, batch, TINYHTTP_SERVER_EPOLL_BATCH_SIZE, timeout); + while (num < 0 && errno == EINTR); + + for (int i = 0; i < num; i++) { + + int idx = batch[i].data.fd; + int flags = batch[i].events; + + if (idx == -1) { + // New plain connections + if (accept_from_listen_socket(server, server->plain_listen_socket, 0) < 0) + return -1; + } else if (idx == -2) { + // New secure connections + if (accept_from_listen_socket(server, server->secure_listen_socket, 1) < 0) + return -1; + } else { + + SOCKET sock = server->stream_sockets[idx]; + TinyHTTPStream *stream = &server->stream_state[idx]; + + if (flags & (EPOLLERR | EPOLLHUP)) + tinyhttp_stream_free(stream); + + int state = tinyhttp_stream_state(stream); + if (flags & EPOLLIN) { + while (state & TINYHTTP_STREAM_RECV) { + ptrdiff_t cap; + char *dst = tinyhttp_stream_net_recv_buf(stream, &cap); + if (dst == NULL) + continue; + int ret = recv(sock, dst, cap, 0); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + tinyhttp_stream_free(stream); + break; + } + if (ret == 0) { + tinyhttp_stream_free(stream); + break; + } + tinyhttp_stream_net_recv_ack(stream, ret); + state = tinyhttp_stream_state(stream); + } + tinyhttp_stream_net_recv_ack(stream, 0); + } + + if (flags & EPOLLOUT) { + while (state & TINYHTTP_STREAM_SEND) { + ptrdiff_t len; + char *src = tinyhttp_stream_net_send_buf(stream, &len); + if (src == NULL) + continue; + int ret = send(sock, src, len, 0); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + tinyhttp_stream_free(stream); + break; + } + if (ret == 0) { + tinyhttp_stream_free(stream); + break; + } + tinyhttp_stream_net_send_ack(stream, ret); + state = tinyhttp_stream_state(stream); + } + tinyhttp_stream_net_send_ack(stream, 0); + } + + int new_state = tinyhttp_stream_state(&server->stream_state[idx]); + if ((state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND)) != (new_state & (TINYHTTP_STREAM_RECV | TINYHTTP_STREAM_SEND))) { + struct epoll_event tmp; + tmp.data.fd = idx; + tmp.events = 0; + if (new_state & TINYHTTP_STREAM_RECV) tmp.events |= EPOLLIN; + if (new_state & TINYHTTP_STREAM_RECV) tmp.events |= EPOLLOUT; + if (epoll_ctl(server->epoll_fd, EPOLL_CTL_MOD, sock, &tmp) < 0) { + tinyhttp_stream_free(stream); + new_state = tinyhttp_stream_state(stream); + } + } + + if (state & TINYHTTP_STREAM_READY) { + int ready_idx = (server->ready_head + server->ready_count) % TINYHTTP_SERVER_CONN_LIMIT; + server->ready_queue[ready_idx] = idx; + server->ready_count++; + } + + if (new_state == TINYHTTP_STREAM_FREE) { + // TODO: Remove from the ready list + CLOSESOCKET(sock); + invalidate_handles_to_stream(server, stream); + server->stream_sockets[idx] = INVALID_SOCKET; + server->num_conns--; + } + } + } + + return 0; +} +#elif defined(_WIN32) + +static unsigned long long +get_current_time_ms(void) +{ + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + + ULARGE_INTEGER uli; + uli.LowPart = ft.dwLowDateTime; + uli.HighPart = ft.dwHighDateTime; + + // Convert Windows file time (100ns since 1601-01-01) to + // Unix epoch time (seconds since 1970-01-01) + // 116444736000000000 = number of 100ns intervals from 1601 to 1970 + return (uli.QuadPart - 116444736000000000ULL) / 10000ULL; +} + +static int +process_network_events(TinyHTTPServer *server, int timeout) +{ + // TODO: timeouts + + DWORD transferred; + ULONG_PTR key; + OVERLAPPED *overlapped; + BOOL result = GetQueuedCompletionStatus(server->iocp, + &transferred, &key, &overlapped, INFINITE); + + if (!result) { + // TODO + } + + if (overlapped == &server->plain_accept_overlapped) { + // New plain connection + return 0; + } + + if (overlapped == &server->secure_accept_overlapped) { + // New secure connection + return 0; + } + + int idx = key; + TinyHTTPStream *stream = &server->stream_state[idx]; + + SOCKET sock = server->stream_sockets[idx]; + OVERLAPPED *recv_overlapped = &server->recv_overlapped[idx]; + OVERLAPPED *send_overlapped = &server->send_overlapped[idx]; + + if (recv_overlapped == overlapped) + tinyhttp_stream_net_recv_ack(stream, transferred); + else { + ASSERT(send_overlapped == overlapped); + tinyhttp_stream_net_send_ack(stream, transferred); + } + + int state = tinyhttp_stream_state(stream); + + if ((state & TINYHTTP_STREAM_RECV) && !(state & TINYHTTP_STREAM_RECV_STARTED)) { + ptrdiff_t cap; + char *dst = tinyhttp_stream_net_recv_buf(stream, &cap); + memset(recv_overlapped, 0, sizeof(*recv_overlapped)); + int ok = ReadFile((HANDLE) sock, dst, cap, NULL, recv_overlapped); + if (!ok && GetLastError() != ERROR_IO_PENDING) { + tinyhttp_stream_free(stream); + state = tinyhttp_stream_state(stream); + } + } + + if (state & TINYHTTP_STREAM_SEND && !(state & TINYHTTP_STREAM_SEND_STARTED)) { + ptrdiff_t len; + char *src = tinyhttp_stream_net_send_buf(stream, &len); + memset(send_overlapped, 0, sizeof(*send_overlapped)); + int ok = WriteFile((HANDLE) sock, src, len, NULL, send_overlapped); + if (!ok && GetLastError() != ERROR_IO_PENDING) { + tinyhttp_stream_free(stream); + state = tinyhttp_stream_state(stream); + } + } + + if (state == TINYHTTP_STREAM_FREE) { + // TODO: Remove from the ready list + CLOSESOCKET(sock); + invalidate_handles_to_stream(server, stream); + server->stream_sockets[idx] = INVALID_SOCKET; + server->num_conns--; + } + + return 0; +} +#endif + +int tinyhttp_server_wait(TinyHTTPServer *server, TinyHTTPRequest **req, + TinyHTTPResponse *res, int timeout) +{ + unsigned long long start_time_ms = -1ULL; + if (timeout >= 0) { + start_time_ms = get_current_time_ms(); + if (start_time_ms == -1ULL) + return -1; + } + + while (server->ready_count == 0) { + + int timeout2; + if (timeout < 0) + timeout2 = -1; + else { + unsigned long long current_time_ms = get_current_time_ms(); + if (current_time_ms == -1ULL) + return -1; + if (current_time_ms < start_time_ms) + return -1; + if (current_time_ms - start_time_ms > INT_MAX) + return -1; + int elapsed = (int) (current_time_ms - start_time_ms); + if (elapsed > timeout) + return 1; + timeout2 = timeout - elapsed; + } + + int ret = process_network_events(server, timeout2); + if (ret < 0) + return -1; + } + + ASSERT(server->ready_count > 0); + + int idx = server->ready_queue[server->ready_head]; + server->ready_head = (server->ready_head + 1) % TINYHTTP_SERVER_CONN_LIMIT; + server->ready_count--; + + unsigned short gen = server->stream_gens[idx]; + TinyHTTPStream *stream = &server->stream_state[idx]; + + *res = (TinyHTTPResponse) { .server=server, .gen=gen, .idx=idx }; + *req = tinyhttp_stream_request(stream); + + return 0; +} + +void tinyhttp_response_status(TinyHTTPResponse res, int status) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + tinyhttp_stream_response_status(stream, status); +} + +void tinyhttp_response_header(TinyHTTPResponse res, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + tinyhttp_response_header_fmt(res, fmt, args); + va_end(args); +} + +void tinyhttp_response_header_fmt(TinyHTTPResponse res, const char *fmt, va_list args) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + tinyhttp_stream_response_header_fmt(stream, fmt, args); +} + +void tinyhttp_response_body_setmincap(TinyHTTPResponse res, ptrdiff_t mincap) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + tinyhttp_stream_response_body_setmincap(stream, mincap); +} + +char* tinyhttp_response_body_buf(TinyHTTPResponse res, ptrdiff_t *cap) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) { + *cap = 0; + return NULL; // Invalid handle + } + + return tinyhttp_stream_response_body_buf(stream, cap); +} + +void tinyhttp_response_body_ack(TinyHTTPResponse res, ptrdiff_t num) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + return tinyhttp_stream_response_body_ack(stream, num); +} + +void tinyhttp_response_send(TinyHTTPResponse res) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + tinyhttp_stream_response_send(stream); + + invalidate_handles_to_stream(res.server, stream); + + TinyHTTPServer *server = res.server; + int idx = stream - server->stream_state; + + if (tinyhttp_stream_request(stream)) { + int ready_idx = (server->ready_head + server->ready_count) % TINYHTTP_SERVER_CONN_LIMIT; + server->ready_queue[ready_idx] = idx; + server->ready_count++; + } + + int state = tinyhttp_stream_state(stream); + +#if defined(__linux__) + SOCKET sock = server->stream_sockets[idx]; + struct epoll_event epoll_buf; + epoll_buf.data.fd = idx; + epoll_buf.events = 0; + if (state & TINYHTTP_STREAM_RECV) epoll_buf.events |= EPOLLIN; + if (state & TINYHTTP_STREAM_SEND) epoll_buf.events |= EPOLLOUT; + if (epoll_ctl(server->epoll_fd, EPOLL_CTL_MOD, sock, &epoll_buf) < 0) { + ASSERT(0); // TODO + } +#endif +} + +void tinyhttp_response_undo(TinyHTTPResponse res) +{ + TinyHTTPStream *stream = response_to_stream(res); + if (stream == NULL) + return; // Invalid handle + + tinyhttp_stream_response_undo(stream); +} + +//////////////////////////////////////////////////////////////////////////////////// +// HTTP ROUTER // +//////////////////////////////////////////////////////////////////////////////////// +#ifdef TINYHTTP_ROUTER_ENABLE + +#include +#include +#include +#include + +typedef struct { + char *ptr; + int len; +} string; + +#define S(X) ((string) {(X), sizeof(X)-1}) + +typedef enum { + ROUTE_STATIC_DIR, +} RouteType; + +typedef struct { + RouteType type; + string endpoint; + string path; + int dir_listing; +} Route; + +struct TinyHTTPRouter { + int num_routes; + int max_routes; + Route routes[]; +}; + +TinyHTTPRouter *tinyhttp_router_init(void) +{ + int max_routes = 32; + TinyHTTPRouter *router = malloc(max_routes * sizeof(TinyHTTPRouter)); + if (router == NULL) + return NULL; + router->max_routes = max_routes; + router->num_routes = 0; + return router; +} + +void tinyhttp_router_free(TinyHTTPRouter *router) +{ + free(router); +} + +void tinyhttp_router_dir(TinyHTTPRouter *router, string endpoint, string path, int dir_listing) +{ + if (router->num_routes == router->max_routes) + abort(); + Route *route = &router->routes[router->num_routes++]; + route->type = ROUTE_STATIC_DIR; + route->endpoint = endpoint; + route->path = path; + route->dir_listing = dir_listing; +} + +static int +valid_component_char(char c) +{ + return is_alpha(c) || is_digit(c) || c == '-' || c == '_' || c == '.'; // TODO +} + +static int +parse_path(string path, string *comps, int max_comps) +{ + // We treat relative and absolute paths the same + if (path.len > 0 && path.ptr[0] == '/') { + path.ptr++; + path.len--; + if (path.len == 0) + return 0; + } + + int num = 0; + int cur = 0; + for (;;) { + if (cur == path.len || !valid_component_char(path.ptr[cur])) + return -1; // Empty component + int start = cur; + do + cur++; + while (cur < path.len && valid_component_char(path.ptr[cur])); + string comp = { path.ptr + start, cur - start }; + + if (eqstr(comp, S(".."))) { + if (num == 0) + return -1; + num--; + } else if (!eqstr(comp, S("."))) { + if (num == max_comps) + return -1; + comps[num++] = comp; + } + + if (cur < path.len) { + if (path.ptr[cur] != '/') + return -1; + cur++; + } + + if (cur == path.len) + break; + } + + return num; +} + +static int swap_parents(string original_parent_path, string new_parent_path, string path, char *mem, int max) +{ + int num_original_parent_path_comps; + string original_parent_path_comps[TINYHTTP_ROUTER_MAX_PATH_COMPONENTS]; + + int num_new_parent_path_comps; + string new_parent_path_comps[TINYHTTP_ROUTER_MAX_PATH_COMPONENTS]; + + int num_path_comps; + string path_comps[TINYHTTP_ROUTER_MAX_PATH_COMPONENTS]; + + num_original_parent_path_comps = parse_path(original_parent_path, original_parent_path_comps, TINYHTTP_ROUTER_MAX_PATH_COMPONENTS); + num_new_parent_path_comps = parse_path(new_parent_path, new_parent_path_comps, TINYHTTP_ROUTER_MAX_PATH_COMPONENTS); + num_path_comps = parse_path(path, path_comps, TINYHTTP_ROUTER_MAX_PATH_COMPONENTS); + if (num_original_parent_path_comps < 0 || num_new_parent_path_comps < 0 || num_path_comps < 0) + return -1; + + int match = 1; + if (num_path_comps < num_original_parent_path_comps) + match = 0; + else { + for (int i = 0; i < num_original_parent_path_comps; i++) + if (!eqstr(original_parent_path_comps[i], path_comps[i])) { + match = 0; + break; + } + } + if (!match) + return 0; + + int num_result_comps = num_new_parent_path_comps + num_path_comps - num_original_parent_path_comps; + if (num_result_comps < 0 || num_result_comps > TINYHTTP_ROUTER_MAX_PATH_COMPONENTS) + return -1; + + string result_comps[TINYHTTP_ROUTER_MAX_PATH_COMPONENTS]; + for (int i = 0; i < num_new_parent_path_comps; i++) + result_comps[i] = new_parent_path_comps[i]; + + for (int i = 0; i < num_path_comps; i++) + result_comps[num_new_parent_path_comps + i] = path_comps[num_original_parent_path_comps + i]; + + int result_flat_len = 0; + for (int i = 0; i < num_result_comps; i++) + result_flat_len += result_comps[i].len + 1; + + if (result_flat_len >= max) + return -1; + int copied = 0; + for (int i = 0; i < num_result_comps; i++) { + if (i > 0) + mem[copied++] = '/'; + memcpy(mem + copied, result_comps[i].ptr, result_comps[i].len); + copied += result_comps[i].len; + } + + mem[copied] = '\0'; + return result_flat_len; +} + +static void +respond_with_regular_file(TinyHTTPServer *server, TinyHTTPResponse response, int fd, int file_size) +{ + http_response_write_status(server, response, 200); + + int cap; + void *dst = http_response_write_body_ptr(server, response, file_size, &cap); + if (dst) { + int copied = 0; + while (copied < file_size) { + int ret = read(fd, dst + copied, file_size - copied); + if (ret < 0) { + if (errno == EINTR) + continue; + http_response_undo(server, response); + http_response_write_status(server, response, 500); + http_response_send(server, response); + return; + } + if (ret == 0) + break; + copied += ret; + } + if (copied < file_size) { + http_response_undo(server, response); + http_response_write_status(server, response, 500); + http_response_send(server, response); + return; + } + } + http_response_write_body_ack(server, response, file_size); + http_response_send(server, response); +} + +static void +respond_with_dir_listing(TinyHTTPServer *server, TinyHTTPResponse response, DIR *dir) +{ + http_response_write_status(server, response, 200); + http_response_write_header(server, response, "Content-Type: text/html"); + http_response_write_body(server, response, S("
    ")); + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + string name = { entry->d_name, strlen(entry->d_name) }; // TODO: Check that d_name is always zero terminated + if (eqstr(name, S(".")) || eqstr(name, S(".."))) + continue; + int cap; + char *ptr = http_response_write_body_ptr(server, response, 128, &cap); + int len = snprintf(ptr, cap, "
  • %s
  • ", entry->d_name); // TODO: add link + if (len < 0 || len > cap) { + // TODO + } + http_response_write_body_ack(server, response, len); + } + http_response_write_body(server, response, S("
")); + http_response_send(server, response); +} + +static int +serve_static_dir(TinyHTTPServer *server, TinyHTTPResponse response, string base_endpoint, string base_path, string endpoint, int dir_listing) +{ + char mem[1<<12]; + int res = swap_parents(base_endpoint, base_path, endpoint, mem, sizeof(mem)); + if (res <= 0) + return res; + string path = {mem, res}; // Note that this is zero terminated + + int fd = open(path.ptr, O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) + return 0; + http_response_write_status(server, response, 500); + http_response_send(server, response); + return 1; + } + + struct stat info; + if (fstat(fd, &info) < 0) { + http_response_write_status(server, response, 500); + http_response_send(server, response); + close(fd); + return 1; + } + + if (S_ISDIR(info.st_mode)) { + + int fd2 = openat(fd, "index.html", O_RDONLY); + if (fd2 < 0) { + if (errno != ENOENT) { + http_response_write_status(server, response, 500); + http_response_send(server, response); + close(fd); + return 1; + } + // Allow falling through + } else { + + struct stat info2; + if (fstat(fd2, &info2) < 0) { + http_response_write_status(server, response, 500); + http_response_send(server, response); + close(fd2); + close(fd); + return 1; + } + + respond_with_regular_file(server, response, fd2, info2.st_size); + + close(fd2); + close(fd); + return 1; + } + // Allow falling through + } + + if (S_ISDIR(info.st_mode)) { + + if (!dir_listing) { + close(fd); + return 0; + } + + DIR *dir = fdopendir(fd); + if (dir == NULL) { + http_response_write_status(server, response, 500); + http_response_send(server, response); + close(fd); + return 1; + } + + respond_with_dir_listing(server, response, dir); + closedir(dir); // This also closes fd + return 1; + } + + if (!S_ISREG(info.st_mode)) { + http_response_write_status(server, response, 500); + http_response_send(server, response); + close(fd); + return 1; + } + int file_size = info.st_size; + + respond_with_regular_file(server, response, fd, file_size); + close(fd); + return 1; +} + +void http_router_resolve(TinyHTTPRouter *router, TinyHTTPServer *server, TinyHTTPRequest *request, TinyHTTPResponse response) +{ + for (int i = 0; i < router->num_routes; i++) { + Route *route = &router->routes[i]; + switch (route->type) { + case ROUTE_STATIC_DIR: + if (1 == serve_static_dir(server, response, route->endpoint, route->path, request->path, route->dir_listing)) + return; + break; + + default: + http_response_write_status(server, response, 500); + http_response_send(server, response); + return; + } + } + http_response_write_status(server, response, 404); + http_response_send(server, response); +} + +#endif // TINYHTTP_ROUTER_ENABLE \ No newline at end of file diff --git a/tinyhttp.h b/tinyhttp.h new file mode 100644 index 0000000..dc66b0c --- /dev/null +++ b/tinyhttp.h @@ -0,0 +1,539 @@ +#include +#include + +//////////////////////////////////////////////////////////////////////////// +// OVERVIEW // +//////////////////////////////////////////////////////////////////////////// +// +// TinyHTTP is the implementation of an HTTP sever. +// +// Users can use one of two interfaces: +// +// 1) Stream interface: This is the implementation of a single HTTP connection. +// It abstracts the communication between the server and one client. The +// advantage is it does not depend on specific I/O and can easily be embedded +// in the user's I/O model. +// +// 2) Server interface: This is the full server implementation. It uses the +// streaming interface by adding platform-specific I/O primitives. It uses +// epoll on Linux and I/O completion ports on Windows. This can also serve +// as reference for how to use the streaming interface to build a custom +// server abstraction. +// +// A program using the server interface looks like this: +// +// int main(void) +// { +// HTTPServerConfig config = { +// // ... set config here ... +// }; +// HTTPServer *s = http_server_init(config); +// if (s == NULL) return -1; +// +// for (;;) { +// TinyHTTPRequest *req; +// TinyHTTPResponse res; +// +// int ret = http_server_wait(s, &req, &res, 1000); +// if (ret == 1) continue; // Timeout +// if (ret < 0) continue; // Error +// +// // Respond +// +// if (req->method == TINYHTTP_METHOD_POST) { +// tinyhttp_response_status(res, 405); +// tinyhttp_response_send(res); +// continue; +// } +// +// tinyhttp_response_status(res, 200); +// tinyhttp_response_header(res, "Server: TinyHTTP version %d", 0); +// tinyhttp_response_body_setmincap(res, 1<<9); +// ptrdiff_t cap; +// char *dst = tinyhttp_response_body_buf(res, &cap); +// int len = snprintf(dst, cap, "Hello, world!"); +// if (len < 0 || len > cap) { +// tinyhttp_response_undo(res); +// tinyhttp_response_status(res, 500); +// } +// tinyhttp_response_send(res); +// } +// +// http_server_free(s); +// return 0; +// } +// +// TODO: Make sure this example is up to date +// +// Note that this example does full error checking. The public API of tinyhttp +// tries very hard to handle any errors internally, keeping your routes as simple +// as possible. +// +// Once you get a response object from the wait function, you don't need to respond +// immediately. For instance if the response requires an operation to complete, you +// can store the response handle somewhere and accept new responses in the mean time. +// +// 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. + +//////////////////////////////////////////////////////////////////////////// +// CONFIGURATION // +//////////////////////////////////////////////////////////////////////////// + +#define TINYHTTP_SERVER_ENABLE +//#define TINYHTTP_ROUTER_ENABLE + +#define TINYHTTP_ROUTER_MAX_PATH_COMPONENTS 32 + +#define TINYHTTP_HEADER_LIMIT 32 +#define TINYHTTP_SERVER_CONN_LIMIT (1<<10) +#define TINYHTTP_SERVER_EPOLL_BATCH_SIZE (1<<10) + +//////////////////////////////////////////////////////////////////////////// +// HTTP SERVER INTEFACE // +//////////////////////////////////////////////////////////////////////////// + +typedef struct TinyHTTPServer TinyHTTPServer; + +typedef enum { + TINYHTTP_METHOD_GET, + TINYHTTP_METHOD_POST, +} TinyHTTPMethod; + +typedef struct { + char *name; + ptrdiff_t name_len; + char *value; + ptrdiff_t value_len; +} TinyHTTPHeader; + +typedef struct { + TinyHTTPMethod method; + int minor; + char* path; + ptrdiff_t path_len; + int num_headers; + TinyHTTPHeader headers[TINYHTTP_HEADER_LIMIT]; + char* body; + ptrdiff_t body_len; +} TinyHTTPRequest; + +typedef struct { + TinyHTTPServer *server; + unsigned short idx; + unsigned short gen; +} TinyHTTPResponse; + +typedef struct { + + int reuse; + + // HTTP + const char *plain_addr; + int plain_port; + int plain_backlog; + + // HTTPS + int secure; + const char *secure_addr; + int secure_port; + int secure_backlog; + const char *cert_file; + const char *private_key_file; + +} TinyHTTPServerConfig; + +typedef enum { + TINYHTTP_MEM_MALLOC, + TINYHTTP_MEM_REALLOC, + TINYHTTP_MEM_FREE, +} TinyHTTPMemoryFuncTag; + +typedef void*(*TinyHTTPMemoryFunc)(TinyHTTPMemoryFuncTag tag, + void *ptr, int len, void *data); + +#ifdef TINYHTTP_SERVER_ENABLE + +// TODO: Comment +TinyHTTPServer *tinyhttp_server_init(TinyHTTPServerConfig config, + TinyHTTPMemoryFunc memfunc, void *memfuncdata); + +// TODO: Comment +void tinyhttp_server_free(TinyHTTPServer *server); + +// TODO: Comment +int tinyhttp_server_wait(TinyHTTPServer *server, TinyHTTPRequest **req, + TinyHTTPResponse *res, int timeout); + +// TODO: Comment +void tinyhttp_response_status(TinyHTTPResponse res, int status); + +// TODO: Comment +void tinyhttp_response_header(TinyHTTPResponse res, + const char *fmt, ...); + +// TODO: Comment +void tinyhttp_response_header_fmt(TinyHTTPResponse res, + const char *fmt, va_list args); + +// TODO: Comment +void tinyhttp_response_body_setmincap(TinyHTTPResponse res, + ptrdiff_t mincap); + +// TODO: Comment +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_send(TinyHTTPResponse res); + +// TODO: Comment +void tinyhttp_response_undo(TinyHTTPResponse res); + +#endif // TINYHTTP_SERVER_ENABLE + +//////////////////////////////////////////////////////////////////////////// +// HTTP STREAM INTEFACE // +//////////////////////////////////////////////////////////////////////////// +// +// The TinyHTTPStream object abstracts the HTTP request/response +// state machine in a platorm-agnostic way. +// +// The connection is first initialized with [tinyhttp_stream_init]. +// From this point on, the connection interacts with the network +// using the [tinyhttp_stream_net_{recv|send}_{buf|ack}] functions. +// +// The state of the connection can be queried using the +// [tinyhttp_stream_state] function. If the state is [TINYHTTP_STREAM_FREE], +// the connection was terminated and the caller must release +// any resources it allocated for it. If the connection is still +// active, zero or more of these flags may be set: +// +// TINYHTTP_STREAM_READY +// An HTTP request was completely buffered and its parsed +// version can be accessed through the [tinyhttp_stream_request] +// function. +// +// TINYHTTP_STREAM_RECV +// The connection expects some bytes from the network. +// +// TINYHTTP_STREAM_SEND +// The connection needs to send bytes on the network +// +// Any call to [tinyhttp_stream_*] functions may cause this state +// to change. When the HTTP-CONN_READY flag is set, the caller +// must create a response by calling these functions in +// sequence: +// +// tinyhttp_stream_response_status +// tinyhttp_stream_response_header (optional) +// tinyhttp_stream_response_body_setmincap/buf/ack (optional) +// tinyhttp_stream_response_send +// +// The header and body functions may be called any number +// of times. +// +// When [tinyhttp_stream_response_send] is called, the buffered +// request is removed and the following buffered requests +// is parsed. If no requests are left, the TINYHTTP_STREAM_READY +// state is unset. +// +// If at any point you want to change the response, you +// can undo all the calls by calling: +// +// tinyhttp_stream_response_undo +// +// To start again from [tinyhttp_stream_response_status] +// +// Note that due to pipelining, the stream may still be reading +// after sending a response. + +// See [tinyhttp_stream_state] +enum { + TINYHTTP_STREAM_FREE = 1 << 0, + TINYHTTP_STREAM_READY = 1 << 1, + TINYHTTP_STREAM_RECV = 1 << 2, + TINYHTTP_STREAM_SEND = 1 << 3, + TINYHTTP_STREAM_CLOSE = 1 << 4, + TINYHTTP_STREAM_REUSE = 1 << 5, + TINYHTTP_STREAM_SEND_STARTED = 1 << 6, + TINYHTTP_STREAM_RECV_STARTED = 1 << 7, +}; + +// Internal use +typedef enum { + TINYHTTP_OUTPUT_STATE_NONE, + TINYHTTP_OUTPUT_STATE_STATUS, + TINYHTTP_OUTPUT_STATE_HEADER, + TINYHTTP_OUTPUT_STATE_BODY, + TINYHTTP_OUTPUT_STATE_ERROR, +} TinyHTTPConnectionOutputState; + +// Internal use +enum { + BYTE_QUEUE_ERROR = 1 << 0, + BYTE_QUEUE_LOCK = 1 << 1, + BYTE_QUEUE_READ = 1 << 2, + BYTE_QUEUE_WRITE = 1 << 3, +}; + +// Internal use +typedef struct { + + TinyHTTPMemoryFunc memfunc; + void *memfuncdata; + + unsigned long long curs; + unsigned int lock; // TODO: Should this be u64? + + char* data; + unsigned int head; + unsigned int size; + unsigned int used; + unsigned int limit; + + char* read_target; + unsigned int read_target_size; + + int flags; +} TinyHTTPByteQueue; + +// Internal use +typedef unsigned long long TinyHTTPByteQueueOffset; + +typedef struct { + int state; + int chunked; + int keepalive; + ptrdiff_t reqsize; + unsigned long long bodylimit; + TinyHTTPConnectionOutputState output_state; + TinyHTTPByteQueueOffset content_length_value_offset; + TinyHTTPByteQueueOffset content_length_offset; + TinyHTTPByteQueue in; + TinyHTTPByteQueue out; + TinyHTTPRequest req; +} TinyHTTPStream; + +// Initializes an HTTP stream +// +// TODO: Comment on memfunc +void +tinyhttp_stream_init(TinyHTTPStream *stream, + TinyHTTPMemoryFunc memfunc, void *memfuncdata); + +// Deinitializes an HTTP stream +// +// This function may be called at any time on a memory +// region where [tinyhttp_stream_init] was called at +// least once. +// +// From the moment this function is called until the +// next call to [tinyhttp_stream_init], all [tinyhttp_stream_*] +// calls become no-ops. +void tinyhttp_stream_free(TinyHTTPStream *stream); + +// Returns the current state of the connection +// +// TODO: List the possible states +// +// Note: +// - Calling this function is always valid on a +// memory region where [tinyhttp_stream_init] was +// called at least once. +int tinyhttp_stream_state(TinyHTTPStream *stream); + +// Returns the pointer and capacity of a memory region +// where data should be read from the network. +// +// You can write at least [*cap] bytes at the returned +// pointer, and need to call [tinyhttp_stream_net_recv_ack] +// when the write is complete. +// +// Note: +// - Due to sticky errors, this function may return +// the null pointer and a capacity of 0. The caller +// must not write to the pointer but act as if +// everything went okay. +char *tinyhttp_stream_recv_buf(TinyHTTPStream *stream, int *cap); + +// Acknowledge the bytes written from the network +// +// The write must have been initiated by calling +// [tinyhttp_stream_net_recv_buf]. +void tinyhttp_stream_recv_ack(TinyHTTPStream *stream, int num); + +// Returns the pointer and capacity of a memory region +// where data should be written to the network. +// +// When the write is complete, [tinyhttp_stream_net_send_ack] +// should be called with the number of consumed bytes. +char *tinyhttp_stream_send_buf(TinyHTTPStream *stream, int *len); + +// See [tinyhttp_stream_net_send_buf] +void tinyhttp_stream_send_ack(TinyHTTPStream *stream, int num); + +// Enable/disable connection reuse for this HTTP +// connection. This change takes effect at the start +// of the next request. +// +// Note that even if the reuse option is set, the +// connection may still be closed due to errors or +// the client deciding otherwise. +// +// On the other hand, if this option is set the next +// request will definitely be the last. +void tinyhttp_stream_setreuse(TinyHTTPStream *stream, int value); + +// TODO: Comment +void tinyhttp_stream_setbodylimit(TinyHTTPStream *stream, unsigned long long value); + +// TODO: Comment +void tinyhttp_stream_setinbuflimit(TinyHTTPStream *stream, unsigned int value); + +// TODO: Comment +void tinyhttp_stream_setoutbuflimit(TinyHTTPStream *stream, unsigned int value); + +// TODO: Comment +TinyHTTPRequest *tinyhttp_stream_request(TinyHTTPStream *stream); + +// TODO: Comment +void tinyhttp_stream_response_status(TinyHTTPStream *stream, int status); + +// TODO: Comment +void tinyhttp_stream_response_header(TinyHTTPStream *stream, const char *fmt, ...); + +// TODO: Comment +void tinyhttp_stream_response_header_fmt(TinyHTTPStream *stream, const char *fmt, va_list args); + +// TODO: Comment +void tinyhttp_stream_response_body_setmincap(TinyHTTPStream *stream, ptrdiff_t mincap); + +// TODO: Comment +char *tinyhttp_stream_response_body_buf(TinyHTTPStream *stream, ptrdiff_t *cap); + +// TODO: Comment +void tinyhttp_stream_response_body_ack(TinyHTTPStream *stream, ptrdiff_t num); + +// TODO: Comment +void tinyhttp_stream_response_send(TinyHTTPStream *stream); + +// TODO: Comment +void tinyhttp_stream_response_undo(TinyHTTPStream *stream); + +//////////////////////////////////////////////////////////////////////////// +// HTTP ROUTER // +//////////////////////////////////////////////////////////////////////////// + +#ifdef TINYHTTP_ROUTER_ENABLE + +typedef struct TinyHTTPRouter TinyHTTPRouter; + +// TODO: Comment +TinyHTTPRouter *tinyhttp_router_init(void); + +// TODO: Comment +void tinyhttp_router_free(TinyHTTPRouter *router); + +// TODO: Comment +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); + +#endif // TINYHTTP_ROUTER_ENABLE \ No newline at end of file