aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml21
-rw-r--r--CMakeLists.txt83
-rw-r--r--LICENSE14
-rw-r--r--README57
-rw-r--r--cmake/FindLibEV.cmake18
-rw-r--r--config.h.in8
-rw-r--r--demo-json-rpc-server.c1065
m---------liberty0
10 files changed, 1278 insertions, 0 deletions
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 <p.janouch@gmail.com>")
+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 <p.janouch@gmail.com>
+ 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 <p.janouch@gmail.com>.
+
+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 <p.janouch@gmail.com>
+ * 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 <langinfo.h>
+#include <locale.h>
+#include <signal.h>
+#include <strings.h>
+
+#include <ev.h>
+#include <jansson.h>
+
+// --- 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
+Subproject 087645848baec5e59e4296817850bd5dd240cbb