diff --git a/src/http.c b/src/http.c new file mode 100644 index 00000000..fafcf5b0 --- /dev/null +++ b/src/http.c @@ -0,0 +1,218 @@ +#include "http.h" + +#include "log.h" +#include "mem.h" + +#include "picohttpparser.h" +#include "uv.h" + +#include + +#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32) +#include +#endif + +typedef struct _tf_http_connection_t +{ + tf_http_t* http; + uv_tcp_t tcp; + + char buffer[8192]; + size_t buffer_length; + int parsed_length; + + tf_http_callback_t* callback; + void* user_data; +} tf_http_connection_t; + +typedef struct _tf_http_handler_t +{ + const char* pattern; + tf_http_callback_t* callback; + void* user_data; +} tf_http_handler_t; + +typedef struct _tf_http_t +{ + uv_tcp_t** listeners; + int listeners_count; + + tf_http_connection_t** connections; + int connections_count; + + tf_http_handler_t* handlers; + int handlers_count; + + uv_loop_t* loop; +} tf_http_t; + +tf_http_t* tf_http_create(uv_loop_t* loop) +{ + tf_http_t* http = tf_malloc(sizeof(tf_http_t)); + *http = (tf_http_t) + { + .loop = loop, + }; + return http; +} + +void _http_allocate_buffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) +{ + tf_http_connection_t* connection = handle->data; + *buf = uv_buf_init(connection->buffer + connection->buffer_length, sizeof(connection->buffer) - connection->buffer_length); +} + +bool _http_find_handler(tf_http_t* http, const char* path, tf_http_callback_t** out_callback, void** out_user_data) +{ + for (int i = 0; i < http->handlers_count; i++) + { + if (!http->handlers[i].pattern) + { + *out_callback = http->handlers[i].callback; + *out_user_data = http->handlers[i].user_data; + return true; + } + } + return false; +} + +void _http_on_read(uv_stream_t* stream, ssize_t read_size, const uv_buf_t* buffer) +{ + tf_http_connection_t* connection = stream->data; + if (read_size > 0) + { + connection->buffer_length += read_size; + + const char* method = NULL; + size_t method_length = 0; + const char* path = NULL; + size_t path_length = 0; + int minor_version = 0; + struct phr_header headers[100]; + size_t header_count = sizeof(headers) / sizeof(*headers); + + int parse_result = phr_parse_request(connection->buffer, connection->buffer_length, &method, &method_length, &path, &path_length, &minor_version, headers, &header_count, connection->parsed_length); + connection->parsed_length = connection->buffer_length; + if (parse_result > 0) + { + if (_http_find_handler(connection->http, path, &connection->callback, &connection->user_data) && connection->callback) + { + char* method_copy = alloca(method_length + 1); + memcpy(method_copy, method, method_length); + method_copy[method_length] = '\0'; + + char* path_copy = alloca(path_length + 1); + memcpy(path_copy, path, path_length); + path_copy[path_length] = '\0'; + + tf_http_request_t request = + { + .connection = connection, + .phase = k_http_callback_phase_headers_received, + .method = method_copy, + .path = path_copy, + .headers = headers, + .headers_count = (int)header_count, + .user_data = connection->user_data, + }; + connection->callback(&request); + } + } + else + { + tf_printf("phr_parse_request: %d\n", parse_result); + } + } +} + +static void _http_on_connection(uv_stream_t* stream, int status) +{ + tf_http_t* http = stream->data; + tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t)); + *connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection } }; + int r = uv_tcp_init(connection->http->loop, &connection->tcp); + if (r) + { + tf_printf("uv_tcp_init: %s\n", uv_strerror(r)); + tf_free(connection); + return; + } + + r = uv_accept(stream, (uv_stream_t*)&connection->tcp); + if (r) + { + tf_printf("uv_accept: %s\n", uv_strerror(r)); + uv_close((uv_handle_t*)&connection->tcp, NULL); + tf_free(connection); + return; + } + + r = uv_read_start((uv_stream_t*)&connection->tcp, _http_allocate_buffer, _http_on_read); + if (r) + { + tf_printf("uv_read-start: %s\n", uv_strerror(r)); + uv_close((uv_handle_t*)&connection->tcp, NULL); + tf_free(connection); + return; + } + + http->connections = tf_realloc(http->connections, http->connections_count + 1); + http->connections[http->connections_count++] = connection; +} + +void tf_http_listen(tf_http_t* http, int port) +{ + uv_tcp_t* tcp = tf_malloc(sizeof(uv_tcp_t)); + *tcp = (uv_tcp_t) { .data = http }; + int r = uv_tcp_init(http->loop, tcp); + if (r) + { + tf_printf("uv_tcp_init: %s\n", uv_strerror(r)); + } + + if (r == 0) + { + struct sockaddr_in addr = + { + .sin_family = AF_INET, + .sin_addr = { .s_addr = INADDR_ANY }, + .sin_port = ntohs(port), + }; + r = uv_tcp_bind(tcp, (struct sockaddr*)&addr, 0); + if (r) + { + tf_printf("uv_tcp_bind: %s\n", uv_strerror(r)); + } + } + + if (r == 0) + { + r = uv_listen((uv_stream_t*)tcp, 16, _http_on_connection); + if (r) + { + tf_printf("uv_listen: %s\n", uv_strerror(r)); + } + } + + if (r == 0) + { + http->listeners = tf_realloc(http->listeners, sizeof(uv_tcp_t*) * (http->listeners_count + 1)); + http->listeners[http->listeners_count++] = tcp; + } +} + +void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_t* callback, void* user_data) +{ + http->handlers = tf_realloc(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1)); + http->handlers[http->handlers_count++] = (tf_http_handler_t) + { + .pattern = tf_strdup(pattern), + .callback = callback, + .user_data = user_data, + }; +} + +void tf_http_destroy(tf_http_t* http) +{ + tf_free(http); +} diff --git a/src/http.h b/src/http.h new file mode 100644 index 00000000..2a73a9af --- /dev/null +++ b/src/http.h @@ -0,0 +1,29 @@ +#pragma once + +typedef struct uv_loop_s uv_loop_t; +typedef struct _tf_http_t tf_http_t; +typedef struct _tf_http_connection_t tf_http_connection_t; + +typedef enum _tf_http_callback_phase_t +{ + k_http_callback_phase_headers_received, + k_http_callback_phase_request_done, +} tf_http_callback_phase_t; + +typedef struct _tf_http_request_t +{ + tf_http_callback_phase_t phase; + tf_http_connection_t* connection; + const char* method; + const char* path; + struct phr_header* headers; + int headers_count; + void* user_data; +} tf_http_request_t; + +typedef void (tf_http_callback_t)(tf_http_request_t* request); + +tf_http_t* tf_http_create(uv_loop_t* loop); +void tf_http_listen(tf_http_t* http, int port); +void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_t* callback, void* user_data); +void tf_http_destroy(tf_http_t* http); diff --git a/src/tests.c b/src/tests.c index 9c9ead8b..d269bcda 100644 --- a/src/tests.c +++ b/src/tests.c @@ -1,5 +1,6 @@ #include "tests.h" +#include "http.h" #include "log.h" #include "mem.h" #include "ssb.tests.h" @@ -11,6 +12,8 @@ #include #include +#include + #if defined(_WIN32) #define WIFEXITED(x) 1 #define WEXITSTATUS(x) (x) @@ -667,6 +670,38 @@ static void _test_b64(const tf_test_options_t* options) unlink("out/test.js"); } +static void _test_http_thread(void* data) +{ +#if defined(__linux__) + int r = system("curl -v http://localhost:23456/"); + tf_printf("curl returned %d\n", WEXITSTATUS(r)); + assert(WEXITSTATUS(r) == 0); +#endif +} + +static void _test_http_handler(tf_http_request_t* request) +{ + tf_printf("HANDLER %d\n", request->phase); +} + +static void _test_http(const tf_test_options_t* options) +{ + uv_loop_t loop = { 0 }; + uv_loop_init(&loop); + tf_http_t* http = tf_http_create(&loop); + tf_http_add_handler(http, NULL, _test_http_handler, NULL); + tf_http_listen(http, 23456); + + uv_thread_t thread = { 0 }; + uv_thread_create(&thread, _test_http_thread, NULL); + uv_run(&loop, UV_RUN_DEFAULT); + + tf_http_destroy(http); + uv_loop_close(&loop); + + uv_thread_join(&thread); +} + static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options), bool opt_in) { bool specified = false; @@ -706,6 +741,7 @@ static void _tf_test_run(const tf_test_options_t* options, const char* name, voi void tf_tests(const tf_test_options_t* options) { #if !TARGET_OS_IPHONE + _tf_test_run(options, "http", _test_http, false); _tf_test_run(options, "ssb", tf_ssb_test_ssb, false); _tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false); _tf_test_run(options, "ssb_following", tf_ssb_test_following, false);