diff --git a/tests/main.c b/tests/main.c index d42ab61..7ceef15 100644 --- a/tests/main.c +++ b/tests/main.c @@ -23,28 +23,161 @@ typedef struct { int body_len; } Response; +// Memory function used to initialize TinyHTTPStream static void *memfunc(TinyHTTPMemoryFuncTag tag, void *ptr, int len, void *data); + +// Moves the request "str" into the stream, checks that the stream +// became ready and that it parsed the request correctly. When this +// functions returns the stream is ready for a response. static void send_request(TinyHTTPStream *stream, const char *str); + +// Copies into the "dst" buffer the output bytes from the stream +// (up to "cap" bytes) and parses them as an HTTP response into +// "res". static void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap); +static int header_exists(Response *res, TinyHTTPString name); +static int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value); + #define TEST(X) {if (!(X)) { printf("Test failed at %s:%d\n", __FILE__, __LINE__); fflush(stdout); __builtin_trap(); }} +#define TEST_START printf("Test %s:%d\n", __FILE__, __LINE__); +#define TEST_END + ////////////////////////////////////////////////////////////////////////////////////// // TEST CASES ////////////////////////////////////////////////////////////////////////////////////// -static void basic_request(void) +// Plain HTTP 1.1 request string with no Connection header +#define BASIC_REQUEST_STRING \ + "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" + +static void test_init(void) { TinyHTTPStream stream; + + TEST_START tinyhttp_stream_init(&stream, memfunc, NULL); + int state = tinyhttp_stream_state(&stream); + + // These flags must be set on init + TEST(state & TINYHTTP_STREAM_RECV); + + // These must be unset + TEST(!(state & TINYHTTP_STREAM_DIED)); + TEST(!(state & TINYHTTP_STREAM_READY)); + TEST(!(state & TINYHTTP_STREAM_REUSE)); + TEST(!(state & TINYHTTP_STREAM_RECV_STARTED)); + TEST(!(state & TINYHTTP_STREAM_SEND_STARTED)); + + tinyhttp_stream_free(&stream); + TEST_END +} + +static void test_setreuse(void) +{ + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); + + tinyhttp_stream_setreuse(&stream, 1); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); + + tinyhttp_stream_setreuse(&stream, 0); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE)); + + tinyhttp_stream_setreuse(&stream, 5847295); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_REUSE); + + tinyhttp_stream_free(&stream); + TEST_END +} + +static void +test_recv_started_flag(void) +{ + ptrdiff_t cap; + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_RECV_STARTED)); + + tinyhttp_stream_recv_buf(&stream, &cap); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_RECV_STARTED); + + tinyhttp_stream_recv_ack(&stream, 0); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_RECV_STARTED)); + + tinyhttp_stream_free(&stream); + TEST_END +} + +static void test_kill(void) +{ + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED)); + + tinyhttp_stream_kill(&stream); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_DIED); + + tinyhttp_stream_free(&stream); + TEST_END +} + +static void +test_send_started_flag(void) +{ + ptrdiff_t cap; + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_SEND_STARTED)); + + tinyhttp_stream_send_buf(&stream, &cap); + + TEST(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_SEND_STARTED); + + tinyhttp_stream_send_ack(&stream, 0); + + TEST(!(tinyhttp_stream_state(&stream) & TINYHTTP_STREAM_SEND_STARTED)); + + tinyhttp_stream_free(&stream); + TEST_END +} + +static void test_exchange(int reuse) +{ + TinyHTTPStream stream; + + TEST_START + tinyhttp_stream_init(&stream, memfunc, NULL); + + tinyhttp_stream_setreuse(&stream, reuse); + // 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"); + send_request(&stream, BASIC_REQUEST_STRING); // Build response tinyhttp_stream_response_status(&stream, 200); @@ -55,9 +188,27 @@ static void basic_request(void) Response res; recv_response(&stream, &res, buf, sizeof(buf)); - // TODO + // We expect the status line: + // HTTP/1.1 200 OK + TEST(res.minor == 1); + TEST(res.status_code == 200); + TEST(tinyhttp_streq(res.status_text, TINYHTTP_STRING("OK"))); + + if (reuse) { + // If we allowed connection reuse on this stream, we expect + // the connection to be kept alive. The response must therefore + // contain the "Connection: Keep-Alive" header or no "Connection" + // header at all since "Keep-Alive" is the default. + TEST(!header_exists(&res, TINYHTTP_STRING("Connection")) || + header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Keep-Alive"))); + } else { + // If we didn't allow connection reuse, then the response must + // contain the "Connection: Close" header + TEST(header_exists_with_value(&res, TINYHTTP_STRING("Connection"), TINYHTTP_STRING("Close"))); + } tinyhttp_stream_free(&stream); + TEST_END } ////////////////////////////////////////////////////////////////////////////////////// @@ -66,7 +217,14 @@ static void basic_request(void) int main(void) { - basic_request(); + test_init(); + test_setreuse(); + test_kill(); + test_recv_started_flag(); + test_send_started_flag(); + test_exchange(0); + test_exchange(1); + printf("OK\n"); return 0; } @@ -109,11 +267,14 @@ buffer_into_stream(TinyHTTPStream *stream, const char *src, int len) memcpy(dst, src + copied, cpy); + tinyhttp_printbytes(" >> ", src + copied, cpy); + copied += cpy; tinyhttp_stream_recv_ack(stream, cpy); state = tinyhttp_stream_state(stream); } + printf("\n"); return copied; } @@ -136,11 +297,14 @@ stream_into_buffer(TinyHTTPStream *stream, char *dst, int cap) memcpy(dst + copied, src, cpy); + tinyhttp_printbytes(" << ", src, cpy); + copied += cpy; tinyhttp_stream_send_ack(stream, cpy); state = tinyhttp_stream_state(stream); } + printf("\n"); return copied; } @@ -285,6 +449,24 @@ recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap) parse_response((TinyHTTPString) { dst, len }, res); } +static int +header_exists(Response *res, TinyHTTPString name) +{ + for (int i = 0; i < res->num_headers; i++) + if (tinyhttp_streqcase(res->headers[i].name, name)) + return 1; + return 0; +} + +static int +header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value) +{ + for (int i = 0; i < res->num_headers; i++) + if (tinyhttp_streqcase(res->headers[i].name, name)) + return tinyhttp_streqcase(res->headers[i].value, value); + return 0; +} + ////////////////////////////////////////////////////////////////////////////////////// // THE END ////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/tinyhttp.c b/tinyhttp.c index c23e842..5be1f32 100644 --- a/tinyhttp.c +++ b/tinyhttp.c @@ -70,16 +70,46 @@ #endif //////////////////////////////////////////////////////////////////////////////////// -// DEBUG UTILITIES // +// UTILITIES // //////////////////////////////////////////////////////////////////////////////////// -#if DUMP_IO -#include -static void -print_bytes(char *prefix, char *src, int len) +// TODO: Only allow this when not compiled in stand-alone mode +static int print(const char *src, int len) +{ + if (len < 0) + len = strlen(src); + +#if defined(_WIN32) + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + int num = 0; + while (num < len) { + DWORD cpy; + if (!WriteFile(handle, src + num, len - num, &cpy, NULL)) + return -1; + num += cpy; + } + return 0; +#elif defined(__linux__) + int fd = STDOUT_FILENO; + int num = 0; + while (num < len) { + int ret = write(fd, src + num, len - num); + if (ret < 0) { + if (ret == EINTR) + continue; + return -1; + } + num += ret; + } + return 0; +#endif +} + +void tinyhttp_printbytes(char *prefix, const char *src, int len) { if (src == NULL) { - printf("%s (null)\n", prefix); + print(prefix, -1); + print(" (null)\n", -1); return; } @@ -90,64 +120,57 @@ print_bytes(char *prefix, char *src, int len) while (cur < len && src[cur] != '\n' && src[cur] != '\r') cur++; if (newline) { - printf("%s", prefix); + print(prefix, -1); newline = 0; } - printf("%.*s", cur - start, src + start); + print(src + start, cur - start); if (cur < len) { if (src[cur] == '\r') - printf("\\r"); + print("\\r", -1); else { - printf("\\n\n"); + print("\\n\n", -1); newline = 1; } cur++; } } if (cur > 0 && src[cur-1] != '\n') - printf("\n"); + print("\n", -1); } -#endif -#if DUMP_IO -static void -dump_state(int state, const char *file, int line) +void tinyhttp_printstate_(int state, const char *file, const char *line) { - printf("state = "); + print("state = ", -1); if (state & TINYHTTP_STREAM_DIED) - printf("DIED "); + print("DIED ", -1); if (state & TINYHTTP_STREAM_SEND) - printf("SEND "); + print("SEND ", -1); if (state & TINYHTTP_STREAM_RECV) - printf("RECV "); + print("RECV ", -1); if (state & TINYHTTP_STREAM_READY) - printf("READY "); + print("READY ", -1); if (state & TINYHTTP_STREAM_CLOSE) - printf("CLOSE "); + print("CLOSE ", -1); if (state & TINYHTTP_STREAM_REUSE) - printf("REUSE "); + print("REUSE ", -1); if (state & TINYHTTP_STREAM_SEND_STARTED) - printf("SEND_STARTED "); + print("SEND_STARTED ", -1); if (state & TINYHTTP_STREAM_RECV_STARTED) - printf("RECV_STARTED "); + print("RECV_STARTED ", -1); - printf(" (in %s:%d)\n", file, line); + print(" (in ", -1); + print(file, -1); + print(":", -1); + print(line, -1); + print(")\n", -1); } -#define DUMP_STATE(state) dump_state(state, __FILE__, __LINE__); -#else -#define DUMP_STATE(...) -#endif - -//////////////////////////////////////////////////////////////////////////////////// -// UTILITIES // -//////////////////////////////////////////////////////////////////////////////////// static char to_lower(char c) @@ -177,6 +200,25 @@ int tinyhttp_streqcase(TinyHTTPString s1, TinyHTTPString s2) return 1; } +static TinyHTTPString trim(TinyHTTPString s) +{ + int i = 0; + while (i < s.len && (s.ptr[i] == ' ' || s.ptr[i] == '\t')) + i++; + + if (i == s.len) { + s.ptr = NULL; + s.len = 0; + } else { + s.ptr += i; + s.len -= i; + while (s.ptr[s.len-1] == ' ' || s.ptr[s.len-1] == '\n') + s.len--; + } + + return s; +} + //////////////////////////////////////////////////////////////////////////////////// // HTTP REQUEST PARSER // //////////////////////////////////////////////////////////////////////////////////// @@ -266,6 +308,7 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) while (cur < len && src[cur] != ':') // TODO: robust cur++; TinyHTTPString name = { src + name_off, cur - name_off }; + name = trim(name); if (cur == len) return -400; @@ -275,6 +318,7 @@ parse_request_head(char *src, ptrdiff_t len, TinyHTTPRequest *req) while (cur < len && src[cur] != '\r') cur++; TinyHTTPString value = { src + value_off, cur - value_off }; + value = trim(value); if (cur == len) return -400; @@ -557,7 +601,7 @@ byte_queue_peek(TinyHTTPByteQueue *queue, ptrdiff_t *len) static char* byte_queue_read_buf(TinyHTTPByteQueue *queue, ptrdiff_t *len) { - if ((queue->flags & (BYTE_QUEUE_ERROR)) || queue->data == NULL) { + if (queue->flags & BYTE_QUEUE_ERROR) { *len = 0; return NULL; } @@ -568,6 +612,8 @@ byte_queue_read_buf(TinyHTTPByteQueue *queue, ptrdiff_t *len) queue->read_target_size = queue->size; *len = queue->used; + if (queue->data == NULL) + return NULL; return queue->data + queue->head; } @@ -1399,8 +1445,6 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) stream->output_state = TINYHTTP_OUTPUT_STATE_BODY; } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (stream->output_state == TINYHTTP_OUTPUT_STATE_BODY) { if (stream->chunked) byte_queue_write(&stream->out, "0\r\n\r\n"); @@ -1427,15 +1471,13 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) tmp[9] = '0' + content_length; int i = 0; - while (i < 8 && tmp[i] == '0') + while (i < 9 && tmp[i] == '0') i++; byte_queue_patch(&stream->out, stream->content_length_value_offset, tmp + i, 10 - i); } } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (stream->output_state == TINYHTTP_OUTPUT_STATE_ERROR) { byte_queue_remove_after_lock(&stream->out); byte_queue_write(&stream->out, @@ -1446,15 +1488,11 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) ); } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (byte_queue_error(&stream->out)) { stream->state |= TINYHTTP_STREAM_DIED; return; } - DUMP_STATE(tinyhttp_stream_state(stream)); - #if DUMP_IO ptrdiff_t ressize = (byte_queue_offset(&stream->out) - stream->out.lock); print_bytes("R << ", stream->out.data + stream->out.head, ressize); @@ -1464,11 +1502,7 @@ void tinyhttp_stream_response_send(TinyHTTPStream *stream) byte_queue_read_unlock(&stream->out); stream->reqsize = 0; - DUMP_STATE(tinyhttp_stream_state(stream)); - process_next_request(stream); - - DUMP_STATE(tinyhttp_stream_state(stream)); } // See tinyhttp.h @@ -2000,8 +2034,6 @@ start_stream_operations(TinyHTTPStream *stream, SOCKET sock, { int state = tinyhttp_stream_state(stream); - DUMP_STATE(tinyhttp_stream_state(stream)); - if ((state & TINYHTTP_STREAM_RECV) && !(state & TINYHTTP_STREAM_RECV_STARTED)) { ptrdiff_t cap; char *dst = tinyhttp_stream_recv_buf(stream, &cap); @@ -2014,8 +2046,6 @@ start_stream_operations(TinyHTTPStream *stream, SOCKET sock, #endif } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (state & TINYHTTP_STREAM_SEND && !(state & TINYHTTP_STREAM_SEND_STARTED)) { ptrdiff_t len; char *src = tinyhttp_stream_send_buf(stream, &len); @@ -2028,8 +2058,6 @@ start_stream_operations(TinyHTTPStream *stream, SOCKET sock, #endif } - DUMP_STATE(tinyhttp_stream_state(stream)); - return 0; } @@ -2064,8 +2092,6 @@ intern_accepted_socket(TinyHTTPServer *server, SOCKET accepted_socket, int secur if (server->num_conns < TINYHTTP_SERVER_CONN_LIMIT * 0.7) tinyhttp_stream_setreuse(stream, 1); - DUMP_STATE(tinyhttp_stream_state(stream)); - if (CreateIoCompletionPort((HANDLE) accepted_socket, server->iocp, 0, 0) == NULL) { tinyhttp_stream_free(stream); server->stream_sockets[idx] = INVALID_SOCKET; @@ -2082,8 +2108,6 @@ intern_accepted_socket(TinyHTTPServer *server, SOCKET accepted_socket, int secur return; } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (secure) { // TODO } @@ -2186,8 +2210,6 @@ process_network_events(TinyHTTPServer *server, int timeout) int old_state = tinyhttp_stream_state(stream); - DUMP_STATE(tinyhttp_stream_state(stream)); - if (!result) { // A read or write operation failed tinyhttp_stream_kill(stream); @@ -2214,8 +2236,6 @@ process_network_events(TinyHTTPServer *server, int timeout) tinyhttp_stream_kill(stream); } - DUMP_STATE(tinyhttp_stream_state(stream)); - int state = tinyhttp_stream_state(stream); if ((state & TINYHTTP_STREAM_READY) && (old_state & TINYHTTP_STREAM_READY) == 0) { @@ -2224,8 +2244,6 @@ process_network_events(TinyHTTPServer *server, int timeout) server->ready_count++; } - DUMP_STATE(tinyhttp_stream_state(stream)); - if (state & TINYHTTP_STREAM_DIED) { tinyhttp_stream_free(stream); remove_from_ready_queue(server, idx); @@ -2235,8 +2253,6 @@ process_network_events(TinyHTTPServer *server, int timeout) server->num_conns--; } - DUMP_STATE(tinyhttp_stream_state(stream)); - return 0; } #endif diff --git a/tinyhttp.h b/tinyhttp.h index 566875d..42e8641 100644 --- a/tinyhttp.h +++ b/tinyhttp.h @@ -39,6 +39,10 @@ #define TINYHTTP_SERVER_EPOLL_BATCH_SIZE (1<<10) #endif +#define TINYHTTP_LINESTR_HELPER1(X) #X +#define TINYHTTP_LINESTR_HELPER2(X) TINYHTTP_LINESTR_HELPER1(X) +#define TINYHTTP_LINESTR TINYHTTP_LINESTR_HELPER2(__LINE__) + // Opaque types typedef struct TinyHTTPServer TinyHTTPServer; typedef struct TinyHTTPRouter TinyHTTPRouter; @@ -171,9 +175,21 @@ typedef struct { TinyHTTPRequest req; } TinyHTTPStream; +// TODO: Comment int tinyhttp_streq(TinyHTTPString s1, TinyHTTPString s2); + +// TODO: Comment int tinyhttp_streqcase(TinyHTTPString s1, TinyHTTPString s2); +// TODO: Comment +void tinyhttp_printbytes(char *prefix, const char *src, int len); + +// TODO: Comment +void tinyhttp_printstate_(int state, const char *file, const char *line); + +// TODO: Comment +#define tinyhttp_printstate(state) tinyhttp_printstate_(state, __FILE__, TINYHTTP_LINESTR) + // Initializes an HTTP stream // // TODO: Comment on memfunc @@ -245,6 +261,8 @@ void tinyhttp_stream_send_ack(TinyHTTPStream *stream, ptrdiff_t num); // // On the other hand, if this option is set the next // request will definitely be the last. +// +// NOTE: This is turned off by default void tinyhttp_stream_setreuse(TinyHTTPStream *stream, int value); // TODO: Comment