From 8a3241d5c46e8b8fac382f606147285e314f1f6b Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Mon, 2 Mar 2015 08:49:21 +0100 Subject: Initial commit Not even the demo is able to compile yet. I'm just tracking my progress. --- .gitignore | 9 + .gitmodules | 3 + .travis.yml | 21 + CMakeLists.txt | 83 ++++ LICENSE | 14 + README | 57 +++ cmake/FindLibEV.cmake | 18 + config.h.in | 8 + demo-json-rpc-server.c | 1065 ++++++++++++++++++++++++++++++++++++++++++++++++ liberty | 1 + 10 files changed, 1279 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .travis.yml create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README create mode 100644 cmake/FindLibEV.cmake create mode 100644 config.h.in create mode 100644 demo-json-rpc-server.c create mode 160000 liberty diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..465d9dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Build files +/build + +# Qt Creator files +/CMakeLists.txt.user* +/acid.config +/acid.files +/acid.creator* +/acid.includes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5abb60c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "liberty"] + path = liberty + url = git://github.com/pjanouch/liberty.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d8e73d0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: c +notifications: + irc: + channels: "anathema.us.nu#anathema" + use_notice: true + skip_join: true +compiler: + - clang + - gcc +before_install: + - sudo add-apt-repository ppa:ukplc-team/ppa -y + - sudo apt-get update -qq +install: + - sudo apt-get install -y help2man libjansson-dev libev-dev +before_script: + - mkdir build + - cd build +script: + - cmake .. -DCMAKE_INSTALL_PREFIX=/usr + - make + - cpack -G DEB diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ca9c0ea --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,83 @@ +project (acid C) +cmake_minimum_required (VERSION 2.8.5) + +# Moar warnings +if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + # -Wunused-function is pretty annoying here, as everything is static + set (CMAKE_C_FLAGS "-std=c99 -Wall -Wextra -Wno-unused-function") +endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + +# Version +set (project_VERSION_MAJOR "0") +set (project_VERSION_MINOR "1") +set (project_VERSION_PATCH "0") + +set (project_VERSION "${project_VERSION_MAJOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}") + +# For custom modules +set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +# Dependencies +find_package (PkgConfig REQUIRED) +pkg_check_modules (dependencies REQUIRED jansson) +find_package (LibEV REQUIRED) + +set (project_libraries ${dependencies_LIBRARIES} ${LIBEV_LIBRARIES}) +include_directories (${dependencies_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) + +# Generate a configuration file +configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) +include_directories (${PROJECT_BINARY_DIR}) + +# Build the executables +add_executable (demo-json-rpc-server demo-json-rpc-server.c) +target_link_libraries (demo-json-rpc-server ${project_libraries}) + +# The files to be installed +include (GNUInstallDirs) +install (TARGETS DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) + +# Generate documentation from program help +find_program (HELP2MAN_EXECUTABLE help2man) +if (NOT HELP2MAN_EXECUTABLE) + message (FATAL_ERROR "help2man not found") +endif (NOT HELP2MAN_EXECUTABLE) + +foreach (page) + set (page_output "${PROJECT_BINARY_DIR}/${page}.1") + list (APPEND project_MAN_PAGES "${page_output}") + add_custom_command (OUTPUT ${page_output} + COMMAND ${HELP2MAN_EXECUTABLE} -N + "${PROJECT_BINARY_DIR}/${page}" -o ${page_output} + DEPENDS ${page} + COMMENT "Generating man page for ${page}" VERBATIM) +endforeach (page) + +add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) + +foreach (page ${project_MAN_PAGES}) + string (REGEX MATCH "\\.([0-9])" manpage_suffix "${page}") + install (FILES "${page}" + DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") +endforeach (page) + +# CPack +set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "A Continuous Integration Daemon") +set (CPACK_PACKAGE_VENDOR "Premysl Janouch") +set (CPACK_PACKAGE_CONTACT "Přemysl Janouch ") +set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") +set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR}) +set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR}) +set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH}) +set (CPACK_GENERATOR "TGZ;ZIP") +set (CPACK_PACKAGE_FILE_NAME + "${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") +set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}") +set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") +set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") +set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}") + +include (CPack) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..553e875 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ + Copyright (c) 2015, Přemysl Janouch + All rights reserved. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..311e388 --- /dev/null +++ b/README @@ -0,0 +1,57 @@ +acid +==== + +`acid' is A Continuous Integration Daemon. Currently under heavy development. + +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 be able to tell you about build results via e-mail and/or IRC. + +Builds will only be supported on the same machine as the daemon. Eventually I +might be able to add support for fully replicable builds using Docker. + +With this being my own project, of course it is written in event-looped C99 +where everything is stuffed into just a few files. At least I hope it's written +in a somewhat clean manner. Feel free to contribute. + +Building and Installing +----------------------- +Build dependencies: CMake, pkg-config, help2man, liberty (included) +Runtime dependencies: libev, Jansson + + $ git clone https://github.com/pjanouch/acid.git + $ git submodule init + $ git submodule update + $ mkdir build + $ cd build + $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug + $ make + +To install the application, you can do either the usual: + # make install + +Or you can try telling CMake to make a package for you. For Debian it is: + $ cpack -G DEB + # dpkg -i acid-*.deb + +Note that for versions of CMake before 2.8.9, you need to prefix cpack with +`fakeroot' or file ownership will end up wrong. + +Usage +----- +TODO. The main application hasn't been written yet. + +License +------- +`acid' is written by Přemysl Janouch . + +You may use the software under the terms of the ISC license, the text of which +is included within the package, or, at your option, you may relicense the work +under the MIT or the Modified BSD License, as listed at the following site: + +http://www.gnu.org/licenses/license-list.html diff --git a/cmake/FindLibEV.cmake b/cmake/FindLibEV.cmake new file mode 100644 index 0000000..73787a1 --- /dev/null +++ b/cmake/FindLibEV.cmake @@ -0,0 +1,18 @@ +# Public Domain + +# The author of libev is a dick and doesn't want to add support for pkg-config, +# forcing us to include this pointless file in the distribution. + +# Some distributions do add it, though +find_package (PkgConfig REQUIRED) +pkg_check_modules (LIBEV QUIET libev) + +if (NOT LIBEV_FOUND) + find_path (LIBEV_INCLUDE_DIRS ev.h) + find_library (LIBEV_LIBRARIES NAMES ev) + + if (LIBEV_INCLUDE_DIRS AND LIBEV_LIBRARIES) + set (LIBEV_FOUND TRUE) + endif (LIBEV_INCLUDE_DIRS AND LIBEV_LIBRARIES) +endif (NOT LIBEV_FOUND) + diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..89cd306 --- /dev/null +++ b/config.h.in @@ -0,0 +1,8 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define PROGRAM_NAME "${PROJECT_NAME}" +#define PROGRAM_VERSION "${project_VERSION}" + +#endif // ! CONFIG_H + diff --git a/demo-json-rpc-server.c b/demo-json-rpc-server.c new file mode 100644 index 0000000..cbce78e --- /dev/null +++ b/demo-json-rpc-server.c @@ -0,0 +1,1065 @@ +/* + * demo-json-rpc-server.c: JSON-RPC 2.0 demo server + * + * Copyright (c) 2015, Přemysl Janouch + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define LIBERTY_WANT_SSL + +#define print_fatal_data ((void *) LOG_ERR) +#define print_error_data ((void *) LOG_ERR) +#define print_warning_data ((void *) LOG_WARNING) +#define print_status_data ((void *) LOG_INFO) +#define print_debug_data ((void *) LOG_DEBUG) + +#include "config.h" +#include "liberty/liberty.c" + +#include +#include +#include +#include + +#include +#include + +// --- Extensions to liberty --------------------------------------------------- + +// These should be incorporated into the library ASAP + +#define UNPACKER_INT_BEGIN \ + if (self->len - self->offset < sizeof *value) \ + return false; \ + uint8_t *x = (uint8_t *) self->data + self->offset; \ + self->offset += sizeof *value; + +static bool +msg_unpacker_u16 (struct msg_unpacker *self, uint16_t *value) +{ + UNPACKER_INT_BEGIN + *value + = (uint16_t) x[0] << 24 | (uint16_t) x[1] << 16; + return true; +} + +static bool +msg_unpacker_u32 (struct msg_unpacker *self, uint32_t *value) +{ + UNPACKER_INT_BEGIN + *value + = (uint32_t) x[0] << 24 | (uint32_t) x[1] << 16 + | (uint32_t) x[2] << 8 | (uint32_t) x[3]; + return true; +} + +#undef UNPACKER_INT_BEGIN + +// --- Logging ----------------------------------------------------------------- + +static void +log_message_syslog (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + int prio = (int) (intptr_t) user_data; + + va_list va; + va_copy (va, ap); + int size = vsnprintf (NULL, 0, fmt, va); + va_end (va); + if (size < 0) + return; + + char buf[size + 1]; + if (vsnprintf (buf, sizeof buf, fmt, ap) >= 0) + syslog (prio, "%s%s", quote, buf); +} + +// --- Configuration (application-specific) ------------------------------------ + +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" }, + { NULL, NULL, NULL } +}; + +// --- FastCGI ----------------------------------------------------------------- + +// Constants from the FastCGI specification document + +#define FCGI_HEADER_LEN 8 + +#define FCGI_VERSION_1 1 +#define FCGI_NULL_REQUEST_ID 0 +#define FCGI_KEEP_CONN 1 + +enum fcgi_type +{ + FCGI_BEGIN_REQUEST = 1, + FCGI_ABORT_REQUEST = 2, + FCGI_END_REQUEST = 3, + FCGI_PARAMS = 4, + FCGI_STDIN = 5, + FCGI_STDOUT = 6, + FCGI_STDERR = 7, + FCGI_DATA = 8, + FCGI_GET_VALUES = 9, + FCGI_GET_VALUES_RESULT = 10, + FCGI_UNKNOWN_TYPE = 11, + FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE +}; + +enum fcgi_role +{ + FCGI_RESPONDER = 1, + FCGI_AUTHORIZER = 2, + FCGI_FILTER = 3 +}; + +enum fcgi_protocol_status +{ + FCGI_REQUEST_COMPLETE = 0, + FCGI_CANT_MPX_CONN = 1, + FCGI_OVERLOADED = 2, + FCGI_UNKNOWN_ROLE = 3 +}; + +#define FCGI_MAX_CONNS "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + +// - - Message stream parser - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct fcgi_parser; + +typedef void (*fcgi_message_fn) + (const struct fcgi_parser *parser, void *user_data); + +enum fcgi_parser_state +{ + FCGI_READING_HEADER, ///< Reading the fixed header portion + FCGI_READING_CONTENT, ///< Reading the message content + FCGI_READING_PADDING ///< Reading the padding +}; + +struct fcgi_parser +{ + enum fcgi_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + // The next block of fields is considered public: + + uint8_t version; ///< FastCGI protocol version + uint8_t type; ///< FastCGI record type + uint16_t request_id; ///< FastCGI request ID + struct str content; ///< Message data + + uint16_t content_length; ///< Message content length + uint8_t padding_length; ///< Message padding length + + fcgi_message_fn on_message; ///< Callback on message + void *user_data; ///< User data +}; + +static void +fcgi_parser_init (struct fcgi_parser *self) +{ + memset (self, 0, sizeof *self); + str_init (&self->input); + str_init (&self->content); +} + +static void +fcgi_parser_free (struct fcgi_parser *self) +{ + str_free (&self->input); + str_free (&self->content); +} + +static void +fcgi_parser_unpack_header (struct fcgi_parser *self) +{ + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, self->input.str, self->input.len); + + bool success = true; + uint8_t reserved; + success &= msg_unpacker_u8 (&unpacker, &self->version); + success &= msg_unpacker_u8 (&unpacker, &self->type); + success &= msg_unpacker_u16 (&unpacker, &self->request_id); + success &= msg_unpacker_u16 (&unpacker, &self->content_length); + success &= msg_unpacker_u8 (&unpacker, &self->padding_length); + success &= msg_unpacker_u8 (&unpacker, &reserved); + hard_assert (success); + + str_remove_slice (&self->input, 0, unpacker.offset); +} + +static void +fcgi_parser_push (struct fcgi_parser *self, const void *data, size_t len) +{ + // This could be made considerably faster for high-throughput applications + // if we use a circular buffer instead of constantly calling memmove() + str_append_data (&self->input, data, len); + + while (true) + switch (self->state) + { + case FCGI_READING_HEADER: + if (self->input.len < FCGI_HEADER_LEN) + return; + + fcgi_parser_unpack_header (self); + self->state = FCGI_READING_CONTENT; + break; + case FCGI_READING_CONTENT: + if (self->input.len < self->content_length) + return; + + // Move an appropriate part of the input buffer to the content buffer + str_reset (&self->content); + str_append_data (&self->content, self->input.str, self->content_length); + str_remove_slice (&self->input, 0, self->content_length); + self->state = FCGI_READING_PADDING; + break; + case FCGI_READING_PADDING: + if (self->input.len < self->padding_length) + return; + + // Call the callback to further process the message + self->on_message (self, self->user_data); + + // Remove the padding from the input buffer + str_remove_slice (&self->input, 0, self->padding_length); + self->state = FCGI_READING_HEADER; + break; + } +} + +// - - Name-value pair parser - - - - - - - - - - - - - - - - - - - - - - - - - + +enum fcgi_nv_parser_state +{ + FCGI_NV_PARSER_NAME_LEN, ///< The first name length octet + FCGI_NV_PARSER_NAME_LEN_FULL, ///< Remaining name length octets + FCGI_NV_PARSER_VALUE_LEN, ///< The first value length octet + FCGI_NV_PARSER_VALUE_LEN_FULL, ///< Remaining value length octets + FCGI_NV_PARSER_NAME, ///< Reading the name + FCGI_NV_PARSER_VALUE ///< Reading the value +}; + +struct fcgi_nv_parser +{ + struct str_map *output; ///< Where the pairs will be stored + + enum fcgi_nv_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + uint32_t name_len; ///< Length of the name + uint32_t value_len; ///< Length of the value + + char *name; ///< The current name, 0-terminated + char *value; ///< The current value, 0-terminated +}; + +static void +fcgi_nv_parser_init (struct fcgi_nv_parser *self) +{ + memset (self, 0, sizeof *self); + str_init (&self->input); +} + +static void +fcgi_nv_parser_free (struct fcgi_nv_parser *self) +{ + str_free (&self->input); + free (self->name); + free (self->value); +} + +static void +fcgi_nv_parser_push (struct fcgi_nv_parser *self, void *data, size_t len) +{ + // This could be optimized significantly; I'm not even trying + str_append_data (&self->input, data, len); + + while (true) + { + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, self->input.str, self->input.len); + + switch (self->state) + { + uint8_t len; + uint32_t len_full; + + case FCGI_NV_PARSER_NAME_LEN: + if (!msg_unpacker_u8 (&unpacker, &len)) + return; + + if (len >> 7) + self->state = FCGI_NV_PARSER_NAME_LEN_FULL; + else + { + self->name_len = len; + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_VALUE_LEN; + } + break; + case FCGI_NV_PARSER_NAME_LEN_FULL: + if (!msg_unpacker_u32 (&unpacker, &len_full)) + return; + + self->name_len = len_full & ~(1U << 31); + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_VALUE_LEN; + break; + case FCGI_NV_PARSER_VALUE_LEN: + if (!msg_unpacker_u8 (&unpacker, &len)) + return; + + if (len >> 7) + self->state = FCGI_NV_PARSER_VALUE_LEN_FULL; + else + { + self->value_len = len; + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_NAME; + } + break; + case FCGI_NV_PARSER_VALUE_LEN_FULL: + if (!msg_unpacker_u32 (&unpacker, &len_full)) + return; + + self->value_len = len_full & ~(1U << 31); + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_NAME; + break; + case FCGI_NV_PARSER_NAME: + if (self->input.len < self->name_len) + return; + + self->name = xmalloc (self->name_len + 1); + self->name[self->name_len] = '\0'; + memcpy (self->name, self->input.str, self->name_len); + str_remove_slice (&self->input, 0, self->name_len); + self->state = FCGI_NV_PARSER_VALUE; + break; + case FCGI_NV_PARSER_VALUE: + if (self->input.len < self->value_len) + return; + + self->value = xmalloc (self->value_len + 1); + self->value[self->value_len] = '\0'; + memcpy (self->value, self->input.str, self->value_len); + str_remove_slice (&self->input, 0, self->value_len); + self->state = FCGI_NV_PARSER_NAME_LEN; + + // The map takes ownership of the value + str_map_set (self->output, self->name, self->value); + free (self->name); + + self->name = NULL; + self->value = NULL; + break; + } + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// TODO +struct fcgi_request +{ + struct fcgi_muxer *muxer; ///< The parent muxer + + uint16_t request_id; ///< The ID of this request +}; + +// TODO +struct fcgi_muxer +{ + struct fcgi_parser parser; ///< FastCGI message parser + + /// Requests assigned to request IDs + // TODO: allocate this dynamically + struct fcgi_request *requests[1 << 16]; +}; + +static void +fcgi_muxer_on_message (const struct fcgi_parser *parser, void *user_data) +{ + struct fcgi_muxer *self = user_data; + + // TODO +} + +static void +fcgi_muxer_init (struct fcgi_muxer *self) +{ + fcgi_parser_init (&self->parser); + self->parser.on_message = fcgi_muxer_on_message; + self->parser.user_data = self; +} + +static void +fcgi_muxer_free (struct fcgi_muxer *self) +{ + fcgi_parser_free (&self->parser); +} + +static void +fcgi_muxer_push (struct fcgi_muxer *self, const void *data, size_t len) +{ + fcgi_parser_push (&self->parser, data, len); +} + +// --- SCGI -------------------------------------------------------------------- + +enum scgi_parser_state +{ + SCGI_READING_NETSTRING_LENGTH, ///< The length of the header netstring + SCGI_READING_NAME, ///< Header name + SCGI_READING_VALUE, ///< Header value + SCGI_READING_CONTENT ///< Incoming data +}; + +struct scgi_parser +{ + enum scgi_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + struct str_map headers; ///< Headers parsed + + size_t headers_len; ///< Length of the netstring contents + struct str name; ///< Header name so far + struct str value; ///< Header value so far +}; + +static void +scgi_parser_init (struct scgi_parser *self) +{ + str_init (&self->input); + str_map_init (&self->headers); + self->headers.free = free; + str_init (&self->name); + str_init (&self->value); +} + +static void +scgi_parser_free (struct scgi_parser *self) +{ + str_free (&self->input); + str_map_free (&self->headers); + str_free (&self->name); + str_free (&self->value); +} + +static bool +scgi_parser_push (struct scgi_parser *self, + void *data, size_t len, struct error **e) +{ + // This retarded netstring madness is even more complicated than FastCGI; + // this procedure could also be optimized significantly + str_append_data (&self->input, data, len); + + while (true) + switch (self->state) + { + case SCGI_READING_NETSTRING_LENGTH: + { + if (self->input.len < 1) + return true; + + char digit = *self->input.str; + // XXX: this allows for omitting the netstring length altogether + if (digit == ':') + { + self->state = SCGI_READING_NAME; + break; + } + + if (digit < '0' || digit >= '9') + { + error_set (e, "invalid header netstring"); + return false; + } + + size_t new_len = self->headers_len * 10 + (digit - '0'); + if (new_len < self->headers_len) + { + error_set (e, "header netstring is too long"); + return false; + } + self->headers_len = new_len; + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_NAME: + { + if (self->input.len < 1) + return true; + + char c = *self->input.str; + if (!self->headers_len) + { + // The netstring is ending but we haven't finished parsing it, + // or the netstring doesn't end with a comma + if (self->name.len || c != ',') + { + error_set (e, "invalid header netstring"); + return false; + } + self->state = SCGI_READING_CONTENT; + } + else if (c != '\0') + str_append_c (&self->name, c); + else + self->state = SCGI_READING_VALUE; + + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_VALUE: + { + if (self->input.len < 1) + return true; + + char c = *self->input.str; + if (!self->headers_len) + { + // The netstring is ending but we haven't finished parsing it + error_set (e, "invalid header netstring"); + return false; + } + else if (c != '\0') + str_append_c (&self->value, c); + else + { + // We've got a name-value pair, let's put it in the map + str_map_set (&self->headers, + self->name.str, str_steal (&self->value)); + + str_reset (&self->name); + str_init (&self->value); + + self->state = SCGI_READING_NAME; + } + + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_CONTENT: + // TODO: I have no idea what to do with the contents + return true; + + break; + } +} + +// --- ? ----------------------------------------------------------------------- + +// TODO +struct client +{ + LIST_HEADER (struct client) + + struct server_context *ctx; ///< Server context + + int socket_fd; ///< The TCP socket + struct str read_buffer; ///< Unprocessed input + write_queue_t write_queue; ///< Write queue + + ev_io read_watcher; ///< The socket can be read from + ev_io write_watcher; ///< The socket can be written to +}; + +static void +client_init (struct client *self) +{ + memset (self, 0, sizeof *self); + str_init (&self->read_buffer); + write_queue_init (&self->write_queue); +} + +static void +client_free (struct client *self) +{ + str_free (&self->read_buffer); + write_queue_free (&self->write_queue); +} + +// --- ? ----------------------------------------------------------------------- + +enum listener_type +{ + LISTENER_FCGI, ///< FastCGI + LISTENER_SCGI ///< SCGI +}; + +struct listener +{ + int fd; ///< Listening socket FD + ev_io watcher; ///< New connection available + enum listener_type type; ///< The protocol +}; + +struct server_context +{ + ev_signal sigterm_watcher; ///< Got SIGTERM + ev_signal sigint_watcher; ///< Got SIGINT + bool quitting; ///< User requested quitting + + struct listener *listeners; ///< Listeners + size_t n_listeners; ///< Number of listening sockets + + struct client *clients; ///< Clients + unsigned n_clients; ///< Current number of connections + + struct str_map config; ///< Server configuration +}; + +static void +server_context_init (struct server_context *self) +{ + memset (self, 0, sizeof *self); + + str_map_init (&self->config); + load_config_defaults (&self->config, g_config_table); +} + +static void +server_context_free (struct server_context *self) +{ + // TODO: free the clients (?) + // TODO: close the listeners (?) + + str_map_free (&self->config); +} + +// --- ? ----------------------------------------------------------------------- + +static void +remove_client (struct server_context *ctx, struct client *client) +{ + ev_io_stop (EV_DEFAULT_ &client->read_watcher); + ev_io_stop (EV_DEFAULT_ &client->write_watcher); + xclose (client->socket_fd); + LIST_UNLINK (ctx->clients, client); + client_free (client); + free (client); +} + +static bool +read_loop (EV_P_ ev_io *watcher, + bool (*cb) (EV_P_ ev_io *, const void *, ssize_t)) +{ + char buf[8192]; + while (true) + { + ssize_t n_read = recv (watcher->fd, buf, sizeof buf, 0); + if (n_read < 0) + { + if (errno == EAGAIN) + break; + if (errno == EINTR) + continue; + } + if (n_read <= 0 || !cb (EV_A_ watcher, buf, n_read)) + return false; + } + return true; +} + +static bool +flush_queue (write_queue_t *queue, ev_io *watcher) +{ + struct iovec vec[queue->len], *vec_iter = vec; + for (write_req_t *iter = queue->head; iter; iter = iter->next) + *vec_iter++ = iter->data; + + ssize_t written; +again: + written = writev (watcher->fd, vec, N_ELEMENTS (vec)); + if (written < 0) + { + if (errno == EAGAIN) + goto skip; + if (errno == EINTR) + goto again; + return false; + } + + write_queue_processed (queue, written); + +skip: + if (write_queue_is_empty (queue)) + ev_io_stop (EV_DEFAULT_ watcher); + else + ev_io_start (EV_DEFAULT_ watcher); + return true; +} + +static void +on_client_ready (EV_P_ ev_io *watcher, int revents) +{ + struct server_context *ctx = ev_userdata (loop); + struct client *client = watcher->data; + + if (revents & EV_READ) + if (!read_loop (EV_A_ watcher, on_client_data)) + goto error; + if (revents & EV_WRITE) + if (!flush_queue (&client->write_queue, watcher)) + goto error; + return; + +error: + remove_client (ctx, client); +} + +static void +on_fcgi_client_available (EV_P_ ev_io *watcher, int revents) +{ + struct server_context *ctx = ev_userdata (loop); + (void) revents; + + // TODO + while (true) + { + int sock_fd = accept (watcher->fd, NULL, NULL); + if (sock_fd == -1) + { + if (errno == EAGAIN) + break; + if (errno == EINTR + || errno == ECONNABORTED) + continue; + + // Stop accepting connections to prevent busy looping + ev_io_stop (EV_A_ watcher); + + print_fatal ("%s: %s", "accept", strerror (errno)); + // TODO: initiate_quit (ctx); + break; + } + + struct client *client = xmalloc (sizeof *client); + client_init (client); + client->socket_fd = sock_fd; + + set_blocking (sock_fd, false); + ev_io_init (&client->read_watcher, on_client_ready, sock_fd, EV_READ); + ev_io_init (&client->write_watcher, on_client_ready, sock_fd, EV_WRITE); + client->read_watcher.data = client; + client->write_watcher.data = client; + + // We're only interested in reading as the write queue is empty now + ev_io_start (EV_A_ &client->read_watcher); + + LIST_PREPEND (ctx->clients, client); + ctx->n_clients++; + } +} + +// --- Application setup ------------------------------------------------------- + +/// This function handles values that require validation before their first use, +/// or some kind of a transformation (such as conversion to an integer) needs +/// to be done before they can be used directly. +static bool +parse_config (struct server_context *ctx, struct error **e) +{ + (void) ctx; + (void) e; + + return true; +} + +static int +listen_finish (struct addrinfo *gai_iter) +{ + int fd = socket (gai_iter->ai_family, + gai_iter->ai_socktype, gai_iter->ai_protocol); + if (fd == -1) + return -1; + set_cloexec (fd); + + int yes = 1; + soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE, + &yes, sizeof yes) != -1); + soft_assert (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof yes) != -1); + + char host[NI_MAXHOST], port[NI_MAXSERV]; + host[0] = port[0] = '\0'; + int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, + host, sizeof host, port, sizeof port, + NI_NUMERICHOST | NI_NUMERICSERV); + if (err) + print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); + + char *address = format_host_port_pair (host, port); + if (bind (fd, gai_iter->ai_addr, gai_iter->ai_addrlen)) + print_error ("bind to %s failed: %s", address, strerror (errno)); + else if (listen (fd, 16 /* arbitrary number */)) + print_error ("listen on %s failed: %s", address, strerror (errno)); + else + { + print_status ("listening on %s", address); + free (address); + return fd; + } + + free (address); + xclose (fd); + return -1; +} + +static void +listen_resolve (struct server_context *ctx, const char *host, const char *port, + struct addrinfo *gai_hints, enum listener_type type) +{ + struct addrinfo *gai_result, *gai_iter; + int err = getaddrinfo (host, port, gai_hints, &gai_result); + if (err) + { + char *address = format_host_port_pair (host, port); + print_error ("bind to %s failed: %s: %s", + address, "getaddrinfo", gai_strerror (err)); + free (address); + return; + } + + int fd; + for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) + { + if ((fd = listen_finish (gai_iter)) == -1) + continue; + set_blocking (fd, false); + + struct listener *listener = &ctx->listeners[ctx->n_listeners++]; + switch ((listener->type = type)) + { + case LISTENER_FCGI: + ev_io_init (&listener->watcher, + on_fcgi_client_available, fd, EV_READ); + break; + case LISTENER_SCGI: + ev_io_init (&listener->watcher, + on_scgi_client_available, fd, EV_READ); + break; + } + + ev_io_start (EV_DEFAULT_ &listener->watcher); + break; + } + freeaddrinfo (gai_result); +} + +static bool +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"); + + struct addrinfo gai_hints; + memset (&gai_hints, 0, sizeof gai_hints); + + gai_hints.ai_socktype = SOCK_STREAM; + gai_hints.ai_flags = AI_PASSIVE; + + struct str_vector ports_fcgi; + struct str_vector ports_scgi; + str_vector_init (&ports_fcgi); + str_vector_init (&ports_scgi); + + if (port_fcgi) + split_str_ignore_empty (port_fcgi, ',', &ports_fcgi); + if (port_scgi) + split_str_ignore_empty (port_scgi, ',', &ports_scgi); + + size_t n_ports = ports_fcgi.len + ports_scgi.len; + ctx->listeners = xcalloc (n_ports, sizeof *ctx->listeners); + + for (size_t i = 0; i < ports_fcgi.len; i++) + listen_resolve (ctx, bind_host, ports_fcgi.vector[i], + &gai_hints, LISTENER_FCGI); + for (size_t i = 0; i < ports_scgi.len; i++) + listen_resolve (ctx, bind_host, ports_scgi.vector[i], + &gai_hints, LISTENER_SCGI); + + str_vector_free (&ports_fcgi); + str_vector_free (&ports_scgi); + + if (!ctx->n_listeners) + { + error_set (e, "%s: %s", + "network setup failed", "no ports to listen on"); + return false; + } + return true; +} + +// --- Main program ------------------------------------------------------------ + +static void +on_termination_signal (EV_P_ ev_signal *handle, int revents) +{ + struct server_context *ctx = ev_userdata (loop); + (void) handle; + (void) revents; + + // TODO: initiate_quit (ctx); +} + +static void +daemonize (void) +{ + // TODO: create and lock a PID file? + print_status ("daemonizing..."); + + if (chdir ("/")) + exit_fatal ("%s: %s", "chdir", strerror (errno)); + + pid_t pid; + if ((pid = fork ()) < 0) + exit_fatal ("%s: %s", "fork", strerror (errno)); + else if (pid) + exit (EXIT_SUCCESS); + + setsid (); + signal (SIGHUP, SIG_IGN); + + if ((pid = fork ()) < 0) + exit_fatal ("%s: %s", "fork", strerror (errno)); + else if (pid) + exit (EXIT_SUCCESS); + + openlog (PROGRAM_NAME, LOG_NDELAY | LOG_NOWAIT | LOG_PID, 0); + g_log_message_real = log_message_syslog; + + // XXX: we may close our own descriptors this way, crippling ourselves + for (int i = 0; i < 3; i++) + xclose (i); + + int tty = open ("/dev/null", O_RDWR); + if (tty != 0 || dup (0) != 1 || dup (0) != 2) + exit_fatal ("failed to reopen FD's: %s", strerror (errno)); +} + +static void +parse_program_arguments (int argc, char **argv) +{ + static const struct opt opts[] = + { + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + { 'w', "write-default-cfg", "FILENAME", + OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + "write a default configuration file and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, NULL, "JSON-RPC 2.0 demo server."); + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'w': + call_write_default_config (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + if (argc) + { + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + opt_handler_free (&oh); +} + +int +main (int argc, char *argv[]) +{ + parse_program_arguments (argc, argv); + + print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); + + struct server_context ctx; + server_context_init (&ctx); + + struct error *e = NULL; + if (!read_config_file (&ctx.config, &e)) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + struct ev_loop *loop; + if (!(loop = EV_DEFAULT)) + exit_fatal ("libev initialization failed"); + + ev_set_userdata (loop, &ctx); + + ev_signal_init (&ctx.sigterm_watcher, on_termination_signal, SIGTERM); + ev_signal_start (EV_DEFAULT_ &ctx.sigterm_watcher); + + ev_signal_init (&ctx.sigint_watcher, on_termination_signal, SIGINT); + ev_signal_start (EV_DEFAULT_ &ctx.sigint_watcher); + + (void) signal (SIGPIPE, SIG_IGN); + + if (!parse_config (&ctx, &e) + || !setup_listen_fds (&ctx, &e)) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + if (!g_debug_mode) + daemonize (); + + ev_run (loop, 0); + ev_loop_destroy (loop); + + server_context_free (&ctx); + return EXIT_SUCCESS; +} diff --git a/liberty b/liberty new file mode 160000 index 0000000..0876458 --- /dev/null +++ b/liberty @@ -0,0 +1 @@ +Subproject commit 087645848baec5e59e4296817850bd5dd240cbb2 -- cgit v1.2.3-70-g09d2