From 931fc441f6b89e8fed9c912ddb99d193f99f8b56 Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sun, 8 Mar 2015 09:41:10 +0100 Subject: Steady progress Added static content serving with sane content type detection. Started working on WebSockets (meanwhile neither SCGI or FastCGI is finished and almost nothing has been tested). --- .gitmodules | 3 + CMakeLists.txt | 7 +- README | 9 ++- cmake/FindLibMagic.cmake | 6 ++ demo-json-rpc-server.c | 192 +++++++++++++++++++++++++++++++++++++++++++++-- http-parser | 1 + 6 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 cmake/FindLibMagic.cmake create mode 160000 http-parser diff --git a/.gitmodules b/.gitmodules index 5abb60c..c8d5acf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "liberty"] path = liberty url = git://github.com/pjanouch/liberty.git +[submodule "http-parser"] + path = http-parser + url = https://github.com/joyent/http-parser.git diff --git a/CMakeLists.txt b/CMakeLists.txt index ca9c0ea..6540894 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,9 +23,12 @@ set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) find_package (PkgConfig REQUIRED) pkg_check_modules (dependencies REQUIRED jansson) find_package (LibEV REQUIRED) +find_package (LibMagic REQUIRED) -set (project_libraries ${dependencies_LIBRARIES} ${LIBEV_LIBRARIES}) -include_directories (${dependencies_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) +set (project_libraries ${dependencies_LIBRARIES} + ${LIBEV_LIBRARIES} ${LIBMAGIC_LIBRARIES}) +include_directories (${dependencies_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} ${LIBMAGIC_INCLUDE_DIRS}) # Generate a configuration file configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) diff --git a/README b/README index 311e388..f845a5d 100644 --- a/README +++ b/README @@ -2,13 +2,15 @@ acid ==== `acid' is A Continuous Integration Daemon. Currently under heavy development. +Right now I'm working on a demo JSON-RPC server that will serve as the basis for +the final daemon. The aim of this project is to provide a dumbed-down alternative to Travis CI. I find it way too complex to set up and run in a local setting, while the basic gist of it is actually very simple -- run some stuff on new git commits. -`acid' will provide a JSON-RPC 2.0 service for frontends over FastCGI or SCGI, -as well as a webhook endpoint for notifications about new commits. +`acid' will provide a JSON-RPC 2.0 service for frontends over FastCGI, SCGI, or +WebSockets, as well as a webhook endpoint for notifications about new commits. `acid' will be able to tell you about build results via e-mail and/or IRC. @@ -21,7 +23,8 @@ in a somewhat clean manner. Feel free to contribute. Building and Installing ----------------------- -Build dependencies: CMake, pkg-config, help2man, liberty (included) +Build dependencies: CMake, pkg-config, help2man, libmagic, + liberty (included), http-parser (included) Runtime dependencies: libev, Jansson $ git clone https://github.com/pjanouch/acid.git diff --git a/cmake/FindLibMagic.cmake b/cmake/FindLibMagic.cmake new file mode 100644 index 0000000..edae3bf --- /dev/null +++ b/cmake/FindLibMagic.cmake @@ -0,0 +1,6 @@ +# Public Domain + +find_library (LIBMAGIC_LIBRARIES magic) +find_path (LIBMAGIC_INCLUDE_DIRS magic.h) +find_package_handle_standard_args (LibMagic DEFAULT_MSG + LIBMAGIC_LIBRARIES LIBMAGIC_INCLUDE_DIRS) diff --git a/demo-json-rpc-server.c b/demo-json-rpc-server.c index 6be6b3e..25d6eaa 100644 --- a/demo-json-rpc-server.c +++ b/demo-json-rpc-server.c @@ -34,6 +34,7 @@ #include #include +#include // --- Extensions to liberty --------------------------------------------------- @@ -733,7 +734,7 @@ fcgi_muxer_on_abort_request return; } - // TODO: abort the request + // TODO: abort the request: let it somehow produce FCGI_END_REQUEST } static void @@ -995,6 +996,8 @@ static struct config_item g_config_table[] = { "bind_host", NULL, "Address of the server" }, { "port_fastcgi", "9000", "Port to bind for FastCGI" }, { "port_scgi", NULL, "Port to bind for SCGI" }, + { "port_ws", NULL, "Port to bind for WebSockets" }, + { "static_root", NULL, "The root for static content" }, { NULL, NULL, NULL } }; @@ -1146,9 +1149,6 @@ json_rpc_ping (struct server_context *ctx, json_t *params) static json_t * process_json_rpc_request (struct server_context *ctx, json_t *request) { - // TODO: takes a parsed JSON request and returns back a JSON reply. - // This function may get called multiple times for batch requests. - if (!json_is_object (request)) return json_rpc_response (NULL, NULL, json_rpc_error (JSON_RPC_ERROR_INVALID_REQUEST, NULL)); @@ -1183,7 +1183,6 @@ process_json_rpc_request (struct server_context *ctx, json_t *request) return response; // Notifications don't get responses - // TODO: separate notifications from non-notifications? json_decref (response); return NULL; } @@ -1320,7 +1319,8 @@ request_start (struct request *self, struct str_map *headers) // Unable to serve the request struct str response; str_init (&response); - str_append (&response, "404 Not Found\r\n\r\n"); + str_append (&response, "Status: 404 Not Found\n"); + str_append (&response, "Content-Type: text/plain\n\n"); self->write_cb (self->user_data, response.str, response.len); str_free (&response); return false; @@ -1393,7 +1393,147 @@ struct request_handler g_request_handler_json_rpc = // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// TODO: another request handler to respond to all GETs with a message +// TODO: refactor this spaghetti-tier code +static bool +request_handler_static_try_handle + (struct request *request, struct str_map *headers) +{ + struct server_context *ctx = request->ctx; + const char *root = str_map_find (&ctx->config, "static_root"); + if (!root) + { + print_debug ("static document root not configured"); + return false; + } + + const char *method = str_map_find (headers, "REQUEST_METHOD"); + if (!method || strcmp (method, "GET")) + return false; + + // TODO: look at , REQUEST_URI in the headers + const char *path_info = str_map_find (headers, "PATH_INFO"); + if (!path_info) + { + print_debug ("PATH_INFO not defined"); + return false; + } + + struct str_vector v; + str_vector_init (&v); + split_str_ignore_empty (path_info, '/', &v); + + struct str_vector resolved; + str_vector_init (&resolved); + + // So that the joined path begins with a slash + str_vector_add (&resolved, ""); + + // We need to filter the path to stay in our root + // Being able to read /etc/passwd would be rather embarrasing + for (size_t i = 0; i < v.len; i++) + { + const char *dir = v.vector[i]; + if (!strcmp (dir, ".")) + continue; + + if (strcmp (dir, "..")) + str_vector_add (&resolved, dir); + else if (resolved.len) + str_vector_remove (&resolved, resolved.len - 1); + } + str_vector_free (&v); + + char *suffix = join_str_vector (&resolved, '/'); + str_vector_free (&resolved); + + char *path = xstrdup_printf ("%s%s", root, suffix); + + FILE *fp = fopen (path, "rb"); + if (!fp) + { + struct str response; + str_init (&response); + str_append (&response, "Status: 404 Not Found\n"); + str_append (&response, "Content-Type: text/plain\n\n"); + str_append_printf (&response, + "File %s was not found on this server\n", suffix); + request->write_cb (request->user_data, response.str, response.len); + str_free (&response); + + free (suffix); + free (path); + return false; + } + + free (suffix); + free (path); + + uint8_t buf[8192]; + size_t len; + + // Try to detect the Content-Type from the actual contents + char *mime_type = NULL; + if ((len = fread (buf, 1, sizeof buf, fp))) + { + magic_t cookie; + const char *magic = NULL; + if ((cookie = magic_open (MAGIC_MIME))) + { + if (!magic_load (cookie, NULL) + && (magic = magic_buffer (cookie, buf, len))) + mime_type = xstrdup (magic); + magic_close (cookie); + } + } + if (!mime_type) + { + print_debug ("MIME type detection failed"); + mime_type = xstrdup ("application/octet_stream"); + } + + struct str response; + str_init (&response); + str_append (&response, "Status: 200 OK\n"); + str_append_printf (&response, "Content-Type: %s\n\n", mime_type); + request->write_cb (request->user_data, response.str, response.len); + str_free (&response); + free (mime_type); + + // Write the chunk we've used to help us with magic detection; + // obviously we have to do it after we've written the headers + if (len) + request->write_cb (request->user_data, buf, len); + + while ((len = fread (buf, 1, sizeof buf, fp))) + request->write_cb (request->user_data, buf, len); + fclose (fp); + return true; +} + +static bool +request_handler_static_push + (struct request *request, const void *data, size_t len) +{ + (void) request; + (void) data; + (void) len; + + // Ignoring all content; we shouldn't receive any (GET) + return false; +} + +static void +request_handler_static_destroy (struct request *request) +{ + (void) request; +} + +struct request_handler g_request_handler_static = +{ + .try_handle = request_handler_static_try_handle, + .push_cb = request_handler_static_push, + .destroy_cb = request_handler_static_destroy, +}; // --- Client communication handlers ------------------------------------------- @@ -1673,6 +1813,35 @@ static struct client_impl g_client_scgi = .push = client_scgi_push, }; +// - - WebSockets - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +client_ws_init (struct client *client) +{ + // TODO +} + +static void +client_ws_destroy (struct client *client) +{ + // TODO +} + +static bool +client_ws_push (struct client *client, const void *data, size_t len) +{ + // TODO: first push everything into a http_parser, then after a protocol + // upgrade start parsing the WebSocket frames themselves +} + +static struct client_impl g_client_ws = +{ + .init = client_ws_init, + .destroy = client_ws_destroy, + .push = client_ws_push, +}; + + // --- Basic server stuff ------------------------------------------------------ struct listener @@ -1874,6 +2043,7 @@ setup_listen_fds (struct server_context *ctx, struct error **e) const char *bind_host = str_map_find (&ctx->config, "bind_host"); const char *port_fcgi = str_map_find (&ctx->config, "port_fastcgi"); const char *port_scgi = str_map_find (&ctx->config, "port_scgi"); + const char *port_ws = str_map_find (&ctx->config, "port_ws"); struct addrinfo gai_hints; memset (&gai_hints, 0, sizeof gai_hints); @@ -1883,11 +2053,14 @@ setup_listen_fds (struct server_context *ctx, struct error **e) struct str_vector ports_fcgi; str_vector_init (&ports_fcgi); struct str_vector ports_scgi; str_vector_init (&ports_scgi); + struct str_vector ports_ws; str_vector_init (&ports_ws); if (port_fcgi) split_str_ignore_empty (port_fcgi, ',', &ports_fcgi); if (port_scgi) split_str_ignore_empty (port_scgi, ',', &ports_scgi); + if (port_ws) + split_str_ignore_empty (port_ws, ',', &ports_ws); size_t n_ports = ports_fcgi.len + ports_scgi.len; ctx->listeners = xcalloc (n_ports, sizeof *ctx->listeners); @@ -1898,9 +2071,13 @@ setup_listen_fds (struct server_context *ctx, struct error **e) for (size_t i = 0; i < ports_scgi.len; i++) listener_add (ctx, bind_host, ports_scgi.vector[i], &gai_hints, &g_client_scgi); + for (size_t i = 0; i < ports_ws.len; i++) + listener_add (ctx, bind_host, ports_ws.vector[i], + &gai_hints, &g_client_ws); str_vector_free (&ports_fcgi); str_vector_free (&ports_scgi); + str_vector_free (&ports_ws); if (!ctx->n_listeners) { @@ -2040,6 +2217,7 @@ main (int argc, char *argv[]) (void) signal (SIGPIPE, SIG_IGN); + LIST_PREPEND (ctx.handlers, &g_request_handler_static); LIST_PREPEND (ctx.handlers, &g_request_handler_json_rpc); if (!parse_config (&ctx, &e) diff --git a/http-parser b/http-parser new file mode 160000 index 0000000..d547f3b --- /dev/null +++ b/http-parser @@ -0,0 +1 @@ +Subproject commit d547f3b1a98ed07fdcdaf401a8cbc5fffe9bfa6c -- cgit v1.2.3-70-g09d2