diff --git a/core/core.js b/core/core.js index 3927d606..3d595a0d 100644 --- a/core/core.js +++ b/core/core.js @@ -1006,6 +1006,20 @@ loadSettings().then(function() { } }); httpd_impl.start(tildefriends.http_port); + + if (tildefriends.args.httpdc && tildefriends.https_port) { + async function start_tls() { + const kCertificatePath = "data/httpd/certificate.pem"; + const kPrivateKeyPath = "data/httpd/privatekey.pem"; + let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath)); + let certificate = utf8Decode(await File.readFile(kCertificatePath)); + let context = new TlsContext(); + context.setPrivateKey(privateKey); + context.setCertificate(certificate); + httpd_impl.start(tildefriends.https_port, context); + } + start_tls(); + } }).catch(function(error) { print('Failed to load settings.'); printError({print: print}, error); diff --git a/src/http.c b/src/http.c index 15683660..192b1a73 100644 --- a/src/http.c +++ b/src/http.c @@ -2,6 +2,7 @@ #include "log.h" #include "mem.h" +#include "tls.h" #include "util.js.h" #include "picohttpparser.h" @@ -21,27 +22,30 @@ static const int k_timeout_ms = 60000; typedef struct _tf_http_connection_t { tf_http_t* http; + tf_tls_session_t* tls; uv_tcp_t tcp; uv_shutdown_t shutdown; uv_timer_t timeout; int ref_count; + bool is_handshaking; + bool is_receiving_headers; + bool is_response_sent; + bool is_shutting_down; const char* method; const char* path; const char* query; int minor_version; + char incoming[8192]; + char headers_buffer[8192]; size_t headers_buffer_length; int parsed_length; - char buffer[8192]; - size_t buffer_length; - struct phr_header headers[32]; int headers_length; - bool headers_done; tf_http_callback_t* callback; tf_http_request_t* request; @@ -62,9 +66,16 @@ typedef struct _tf_http_handler_t void* user_data; } tf_http_handler_t; +typedef struct _tf_http_listener_t +{ + tf_http_t* http; + tf_tls_context_t* tls; + uv_tcp_t tcp; +} tf_http_listener_t; + typedef struct _tf_http_t { - uv_tcp_t** listeners; + tf_http_listener_t** listeners; int listeners_count; tf_http_connection_t** connections; @@ -81,6 +92,7 @@ static const char* _http_connection_get_header(const tf_http_connection_t* conne static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason); static const char* _http_status_text(int status); static void _http_timer_reset(tf_http_connection_t* connection); +static void _http_tls_update(tf_http_connection_t* connection); tf_http_t* tf_http_create(uv_loop_t* loop) { @@ -92,17 +104,10 @@ tf_http_t* tf_http_create(uv_loop_t* loop) return http; } -void _http_allocate_buffer(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) +void _http_allocate_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { tf_http_connection_t* connection = handle->data; - if (!connection->headers_done) - { - *buf = uv_buf_init(connection->headers_buffer + connection->headers_buffer_length, sizeof(connection->headers_buffer) - connection->headers_buffer_length); - } - else - { - *buf = uv_buf_init(connection->buffer + connection->buffer_length, sizeof(connection->buffer) - connection->buffer_length); - } + *buf = uv_buf_init(connection->incoming, sizeof(connection->incoming)); } bool _http_find_handler(tf_http_t* http, const char* path, tf_http_callback_t** out_callback, void** out_user_data) @@ -136,6 +141,8 @@ static void _http_connection_on_close(uv_handle_t* handle) static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason) { + connection->is_shutting_down = true; + if (connection->tcp.data && !uv_is_closing((uv_handle_t*)&connection->tcp)) { uv_close((uv_handle_t*)&connection->tcp, _http_connection_on_close); @@ -144,6 +151,11 @@ static void _http_connection_destroy(tf_http_connection_t* connection, const cha { uv_close((uv_handle_t*)&connection->timeout, _http_connection_on_close); } + if (connection->tls) + { + tf_tls_session_destroy(connection->tls); + connection->tls = NULL; + } if (connection->ref_count == 0 && !connection->tcp.data && @@ -175,11 +187,14 @@ static void _http_builtin_404_handler(tf_http_request_t* request) static void _http_reset_connection(tf_http_connection_t* connection) { - connection->headers_done = false; - connection->headers_buffer_length = 0; connection->body_length = 0; connection->content_length = 0; + connection->headers_buffer_length = 0; + connection->headers_length = 0; + connection->is_receiving_headers = true; + connection->is_response_sent = false; connection->parsed_length = 0; + connection->path = NULL; } static void _http_websocket_mask_in_place(uint8_t* p, uint32_t mask, size_t size) @@ -268,6 +283,10 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d memmove(connection->body, (char*)connection->body + total_length, connection->body_length - total_length); connection->body_length -= total_length; } + else + { + break; + } } } else @@ -300,101 +319,135 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d tf_http_request_ref(request); connection->callback(request); tf_http_request_release(request); - if (!connection->is_websocket) - { - _http_reset_connection(connection); - } } } } +static size_t _http_on_read_plain_internal(tf_http_connection_t* connection, const void* data, size_t read_size) +{ + if (connection->is_receiving_headers) + { + size_t used_read_size = tf_min(read_size, sizeof(connection->headers_buffer) - connection->headers_buffer_length); + memcpy(connection->headers_buffer + connection->headers_buffer_length, data, used_read_size); + connection->headers_buffer_length += used_read_size; + + const char* method = NULL; + size_t method_length = 0; + const char* path = NULL; + size_t path_length = 0; + size_t header_count = sizeof(connection->headers) / sizeof(*connection->headers); + + int parse_result = phr_parse_request(connection->headers_buffer, connection->headers_buffer_length, &method, &method_length, &path, &path_length, &connection->minor_version, connection->headers, &header_count, connection->parsed_length); + connection->parsed_length = connection->headers_buffer_length; + if (parse_result > 0) + { + connection->is_receiving_headers = false; + connection->headers_length = header_count; + connection->method = method; + ((char*)connection->method)[method_length] = '\0'; + connection->path = path; + ((char*)connection->path)[path_length] = '\0'; + char* q = strchr(connection->path, '?'); + if (q) + { + *q = '\0'; + connection->query = q + 1; + } + + connection->connection_close = connection->minor_version == 0; + + for (int i = 0; i < (int)header_count; i++) + { + for (size_t j = 0; j < connection->headers[i].name_len; j++) + { + if (connection->headers[i].name[j] >= 'A' && + connection->headers[i].name[j] <= 'Z') + { + ((char*)connection->headers[i].name)[j] += 'a' - 'A'; + } + } + ((char*)connection->headers[i].name)[connection->headers[i].name_len] = '\0'; + ((char*)connection->headers[i].value)[connection->headers[i].value_len] = '\0'; + if (strcasecmp(connection->headers[i].name, "content-length") == 0) + { + connection->content_length = strtoull(connection->headers[i].value, NULL, 10); + } + else if (strcasecmp(connection->headers[i].name, "connection") == 0) + { + if (strcasecmp(connection->headers[i].value, "close") == 0) + { + connection->connection_close = true; + } + } + } + + if (connection->content_length) + { + connection->body = tf_malloc(connection->content_length); + } + + if (!_http_find_handler(connection->http, connection->path, &connection->callback, &connection->user_data) || !connection->callback) + { + connection->callback = _http_builtin_404_handler; + } + size_t consumed = read_size - (connection->headers_buffer_length - connection->parsed_length) - (read_size - used_read_size); + _http_add_body_bytes(connection, NULL, 0); + return consumed; + } + else if (parse_result == -2) + { + /* Incomplete. Will try again next time. */ + return used_read_size; + } + else + { + tf_printf("phr_parse_request: %d\n", parse_result); + _http_connection_destroy(connection, "failed to parse request headers"); + return used_read_size; + } + } + else + { + _http_add_body_bytes(connection, data, read_size); + return read_size; + } +} + +static void _http_on_read_plain(tf_http_connection_t* connection, const void* data, size_t read_size) +{ + size_t total_consumed = 0; + while (total_consumed < read_size) + { + size_t consumed = _http_on_read_plain_internal(connection, ((const uint8_t*)data) + total_consumed, read_size - total_consumed); + if (!consumed) + { + _http_connection_destroy(connection, "_http_on_read_plain_internal didn't consume any data"); + break; + } + total_consumed += consumed; + } +} + static void _http_on_read(uv_stream_t* stream, ssize_t read_size, const uv_buf_t* buffer) { tf_http_connection_t* connection = stream->data; _http_timer_reset(connection); if (read_size > 0) { - if (!connection->headers_done) + if (connection->tls) { - connection->headers_buffer_length += read_size; - - const char* method = NULL; - size_t method_length = 0; - const char* path = NULL; - size_t path_length = 0; - size_t header_count = sizeof(connection->headers) / sizeof(*connection->headers); - - int parse_result = phr_parse_request(connection->headers_buffer, connection->headers_buffer_length, &method, &method_length, &path, &path_length, &connection->minor_version, connection->headers, &header_count, connection->parsed_length); - connection->parsed_length = connection->headers_buffer_length; - if (parse_result > 0) + if (tf_tls_session_write_encrypted(connection->tls, buffer->base, read_size) < 0) { - connection->headers_done = true; - connection->headers_length = header_count; - connection->method = method; - ((char*)connection->method)[method_length] = '\0'; - connection->path = path; - ((char*)connection->path)[path_length] = '\0'; - char* q = strchr(connection->path, '?'); - if (q) - { - *q = '\0'; - connection->query = q + 1; - } - - connection->connection_close = connection->minor_version == 0; - - for (int i = 0; i < (int)header_count; i++) - { - for (size_t j = 0; j < connection->headers[i].name_len; j++) - { - if (connection->headers[i].name[j] >= 'A' && - connection->headers[i].name[j] <= 'Z') - { - ((char*)connection->headers[i].name)[j] += 'a' - 'A'; - } - } - ((char*)connection->headers[i].name)[connection->headers[i].name_len] = '\0'; - ((char*)connection->headers[i].value)[connection->headers[i].value_len] = '\0'; - if (strcasecmp(connection->headers[i].name, "content-length") == 0) - { - connection->content_length = strtoull(connection->headers[i].value, NULL, 10); - } - else if (strcasecmp(connection->headers[i].name, "connection") == 0) - { - if (strcasecmp(connection->headers[i].value, "close") == 0) - { - connection->connection_close = true; - } - } - } - - if (connection->content_length) - { - connection->body = tf_malloc(connection->content_length); - } - - if (!_http_find_handler(connection->http, connection->path, &connection->callback, &connection->user_data) || !connection->callback) - { - connection->callback = _http_builtin_404_handler; - } - - _http_add_body_bytes(connection, connection->headers_buffer + parse_result, connection->headers_buffer_length - parse_result); - } - else if (parse_result == -2) - { - /* Incomplete. Will try again next time. */ + _http_connection_destroy(connection, "tf_tls_session_write_encrypted"); } else { - tf_printf("phr_parse_request: %d\n", parse_result); - _http_connection_destroy(connection, "failed to parse request headers"); + _http_tls_update(connection); } } else { - connection->buffer_length += read_size; - _http_add_body_bytes(connection, connection->buffer, connection->buffer_length); - connection->buffer_length = 0; + _http_on_read_plain(connection, buffer->base, read_size); } } else if (read_size < 0) @@ -428,9 +481,21 @@ static void _http_timer_reset(tf_http_connection_t* connection) static void _http_on_connection(uv_stream_t* stream, int status) { - tf_http_t* http = stream->data; + tf_http_listener_t* listener = stream->data; + tf_http_t* http = listener->http; tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t)); - *connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection } }; + *connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection }, .is_receiving_headers = true }; + if (listener->tls) + { + connection->tls = tf_tls_context_create_session(listener->tls); + if (!connection->tls) + { + _http_connection_destroy(connection, "tf_tls_context_create_session"); + return; + } + tf_tls_session_start_accept(connection->tls); + connection->is_handshaking = true; + } int r = uv_tcp_init(connection->http->loop, &connection->tcp); if (r) { @@ -471,15 +536,20 @@ static void _http_on_connection(uv_stream_t* stream, int status) return; } + if (connection->tls) + { + _http_tls_update(connection); + } + http->connections = tf_realloc(http->connections, sizeof(tf_http_connection_t*) * (http->connections_count + 1)); http->connections[http->connections_count++] = connection; } -void tf_http_listen(tf_http_t* http, int port) +void tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls) { - uv_tcp_t* tcp = tf_malloc(sizeof(uv_tcp_t)); - *tcp = (uv_tcp_t) { .data = http }; - int r = uv_tcp_init(http->loop, tcp); + tf_http_listener_t* listener = tf_malloc(sizeof(tf_http_listener_t)); + *listener = (tf_http_listener_t) { .http = http, .tls = tls, .tcp = { .data = listener } }; + int r = uv_tcp_init(http->loop, &listener->tcp); if (r) { tf_printf("uv_tcp_init: %s\n", uv_strerror(r)); @@ -493,7 +563,7 @@ void tf_http_listen(tf_http_t* http, int port) .sin_addr = { .s_addr = INADDR_ANY }, .sin_port = ntohs(port), }; - r = uv_tcp_bind(tcp, (struct sockaddr*)&addr, 0); + r = uv_tcp_bind(&listener->tcp, (struct sockaddr*)&addr, 0); if (r) { tf_printf("uv_tcp_bind: %s\n", uv_strerror(r)); @@ -502,7 +572,7 @@ void tf_http_listen(tf_http_t* http, int port) if (r == 0) { - r = uv_listen((uv_stream_t*)tcp, 16, _http_on_connection); + r = uv_listen((uv_stream_t*)&listener->tcp, 16, _http_on_connection); if (r) { tf_printf("uv_listen: %s\n", uv_strerror(r)); @@ -511,8 +581,8 @@ void tf_http_listen(tf_http_t* http, int port) if (r == 0) { - http->listeners = tf_realloc(http->listeners, sizeof(uv_tcp_t*) * (http->listeners_count + 1)); - http->listeners[http->listeners_count++] = tcp; + http->listeners = tf_realloc(http->listeners, sizeof(tf_http_listener_t*) * (http->listeners_count + 1)); + http->listeners[http->listeners_count++] = listener; } } @@ -527,17 +597,18 @@ void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_ }; } -static void _http_free_on_close(uv_handle_t* handle) +static void _http_free_listener_on_close(uv_handle_t* handle) { + tf_http_listener_t* listener = handle->data; handle->data = NULL; - tf_free(handle); + tf_free(listener); } void tf_http_destroy(tf_http_t* http) { for (int i = 0; i < http->listeners_count; i++) { - uv_close((uv_handle_t*)http->listeners[i], _http_free_on_close); + uv_close((uv_handle_t*)&http->listeners[i]->tcp, _http_free_listener_on_close); } tf_free(http->listeners); http->listeners = NULL; @@ -582,16 +653,77 @@ static void _http_on_shutdown(uv_shutdown_t* request, int status) request->data = NULL; } +static void _http_write_internal(tf_http_connection_t* connection, const void* data, size_t size) +{ + if (size && !connection->is_shutting_down) + { + uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size); + *write = (uv_write_t) { .data = connection }; + memcpy(write + 1, data, size); + int r = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (void*)(write + 1), .len = size }, 1, _http_on_write); + if (r) + { + tf_printf("uv_write: %s\n", uv_strerror(r)); + } + } +} + +static void _http_tls_update(tf_http_connection_t* connection) +{ + bool again = true; + while (again) + { + again = false; + + if (connection->is_handshaking) + { + switch (tf_tls_session_handshake(connection->tls)) + { + case k_tls_handshake_done: + connection->is_handshaking = false; + break; + case k_tls_handshake_more: + break; + case k_tls_handshake_failed: + _http_connection_destroy(connection, "tf_tls_session_handshake"); + return; + } + } + + char buffer[8192]; + int r = tf_tls_session_read_encrypted(connection->tls, buffer, sizeof(buffer)); + if (r > 0) + { + _http_write_internal(connection, buffer, r); + again = true; + } + + r = tf_tls_session_read_plain(connection->tls, buffer, sizeof(buffer)); + if (r > 0) + { + _http_on_read_plain(connection, buffer, r); + again = true; + } + } +} + static void _http_write(tf_http_connection_t* connection, const void* data, size_t size) { _http_timer_reset(connection); - uv_write_t* write = tf_malloc(sizeof(uv_write_t) + size); - *write = (uv_write_t) { .data = connection }; - memcpy(write + 1, data, size); - int r = uv_write(write, (uv_stream_t*)&connection->tcp, &(uv_buf_t) { .base = (void*)(write + 1), .len = size }, 1, _http_on_write); - if (r) + if (connection->tls) { - tf_printf("uv_write: %s\n", uv_strerror(r)); + int r = tf_tls_session_write_plain(connection->tls, data, size); + if (r < (ssize_t)size) + { + char buffer[8192]; + tf_tls_session_get_error(connection->tls, buffer, sizeof(buffer)); + tf_printf("tf_tls_session_write_plain: %s\n", buffer); + } + _http_tls_update(connection); + } + else + { + _http_write_internal(connection, data, size); } } @@ -602,6 +734,12 @@ void tf_http_request_send(tf_http_request_t* request, const void* data, size_t s void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length) { + if (request->connection->is_response_sent) + { + return; + } + request->connection->is_response_sent = true; + const char* status_text = _http_status_text(status); /* HTTP/1.x 200 OK\r\n */ bool sent_content_length = false; @@ -629,9 +767,7 @@ void tf_http_respond(tf_http_request_t* request, int status, const char** header headers_length += content_length_buffer_length; } - uv_write_t* write = tf_malloc(sizeof(uv_write_t) + headers_length + content_length + 1); - *write = (uv_write_t) { .data = request->connection }; - char* buffer = (char*)(write + 1); + char* buffer = alloca(headers_length + 1); int offset = snprintf(buffer, headers_length + 1, "HTTP/1.%d %03d %s\r\n", request->connection->minor_version, status, status_text); if (headers) { @@ -647,16 +783,12 @@ void tf_http_respond(tf_http_request_t* request, int status, const char** header } offset += snprintf(buffer + offset, headers_length + 1 - offset, "\r\n"); assert(offset == headers_length); + _http_write(request->connection, buffer, headers_length); if (content_length) { - memcpy(buffer + offset, body, content_length); + _http_write(request->connection, body, content_length); } _http_timer_reset(request->connection); - int r = uv_write(write, (uv_stream_t*)&request->connection->tcp, &(uv_buf_t) { .base = buffer, .len = headers_length + content_length }, 1, _http_on_write); - if (r) - { - tf_printf("uv_write: %s\n", uv_strerror(r)); - } if (request->connection->connection_close && !request->connection->shutdown.data) @@ -682,7 +814,10 @@ void tf_http_request_release(tf_http_request_t* request) { if (--request->connection->ref_count == 0) { - /* Reset the connection? */ + if (!request->connection->is_websocket) + { + _http_reset_connection(request->connection); + } } if (--request->ref_count == 0) { diff --git a/src/http.h b/src/http.h index 39fb3769..3ae18989 100644 --- a/src/http.h +++ b/src/http.h @@ -2,10 +2,11 @@ #include -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 struct _tf_http_request_t tf_http_request_t; +typedef struct _tf_http_t tf_http_t; +typedef struct _tf_tls_context_t tf_tls_context_t; +typedef struct uv_loop_s uv_loop_t; typedef enum _tf_http_callback_phase_t { @@ -35,7 +36,7 @@ typedef struct _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_listen(tf_http_t* http, int port, tf_tls_context_t* tls); void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_t* callback, void* user_data); void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length); size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data); diff --git a/src/httpd.js.c b/src/httpd.js.c index 60c6dbe5..e4985aa5 100644 --- a/src/httpd.js.c +++ b/src/httpd.js.c @@ -4,6 +4,7 @@ #include "log.h" #include "mem.h" #include "task.h" +#include "tlscontext.js.h" #include "util.js.h" #include "picohttpparser.h" @@ -291,7 +292,8 @@ static JSValue _httpd_start(JSContext* context, JSValueConst this_val, int argc, tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id); int port = 0; JS_ToInt32(context, &port, argv[0]); - tf_http_listen(http, port); + tf_tls_context_t* tls = tf_tls_context_get(JS_DupValue(context, argv[1])); + tf_http_listen(http, port, tls); return JS_UNDEFINED; } @@ -340,7 +342,7 @@ void tf_httpd_register(JSContext* context) JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context)); JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_all, "all", 2)); JS_SetPropertyStr(context, httpd, "registerSocketHandler", JS_NewCFunction(context, _httpd_register_socket_handler, "register_socket_handler", 2)); - JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_start, "start", 1)); + JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_start, "start", 2)); JS_SetPropertyStr(context, global, "httpdc", httpd); JS_FreeValue(context, global); } diff --git a/src/main.c b/src/main.c index b0d6857d..cca68e17 100644 --- a/src/main.c +++ b/src/main.c @@ -574,7 +574,7 @@ static int _tf_command_usage(const char* file, int argc, char* argv[]) { tf_printf(" %s - %s\n", k_commands[i].name, k_commands[i].description); } - return 0; + return -1; } #endif #endif diff --git a/src/tests.c b/src/tests.c index 0763c6fc..86235ce4 100644 --- a/src/tests.c +++ b/src/tests.c @@ -739,7 +739,7 @@ static void _test_http(const tf_test_options_t* options) tf_http_t* http = tf_http_create(&loop); tf_http_add_handler(http, "/hello", _test_http_handler, NULL); tf_http_add_handler(http, "/post", _test_http_handler_post, NULL); - tf_http_listen(http, 23456); + tf_http_listen(http, 23456, NULL); test_http_t test = { .loop = &loop }; uv_async_init(&loop, &test.async, _test_http_async);