From c91f0380ae898b08a30fff66d85bccd8c95e7f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Wed, 2 Oct 2013 18:43:47 +0200 Subject: [PATCH] initial commit --- .gitignore | 10 + CMakeLists.txt | 55 ++ COPYING | 22 + Makefile.am | 8 + README.rst | 50 ++ autogen.sh | 38 + cmake/AddTargetProperties.cmake | 13 + config.h.cmake | 6 + configure.ac | 49 ++ scgi-cgi.1 | 30 + scgi-cgi.c | 1263 +++++++++++++++++++++++++++++++ 11 files changed, 1544 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 COPYING create mode 100644 Makefile.am create mode 100644 README.rst create mode 100755 autogen.sh create mode 100644 cmake/AddTargetProperties.cmake create mode 100644 config.h.cmake create mode 100644 configure.ac create mode 100644 scgi-cgi.1 create mode 100644 scgi-cgi.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a95137 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +Makefile.in +aclocal.m4 +autom4te.cache +compile +config.h.in +configure +depcomp +install-sh +ltmain.sh +missing diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d032ed2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.6.0 FATAL_ERROR) + +cmake_policy(VERSION 2.6.0) + +SET(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) + +INCLUDE(CheckIncludeFiles) +INCLUDE(CheckLibraryExists) +INCLUDE(FindPkgConfig) +INCLUDE(AddTargetProperties) + + +PROJECT(scgi-cgi C) +SET(PACKAGE_VERSION 0.1.0) + +IF("${CMAKE_BUILD_TYPE}" STREQUAL "") + SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build, options are: None(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) Debug Release RelWithDebInfo MinSizeRel." FORCE) +ENDIF("${CMAKE_BUILD_TYPE}" STREQUAL "") + +OPTION(GCC_LIBEVENT_STATIC "link libevent static") + +# libevent +pkg_check_modules (LIBEVENT REQUIRED libevent>=2.0) +IF(GCC_LIBEVENT_STATIC) + SET(MYLIBEVENT_LDFLAGS -Wl,-Bstatic ${LIBEVENT_LDFLAGS} -Wl,-Bdynamic) +ELSE(GCC_LIBEVENT_STATIC) + SET(MYLIBEVENT_LDFLAGS ${LIBEVENT_LDFLAGS}) +ENDIF(GCC_LIBEVENT_STATIC) + + +SET(MAIN_SOURCE scgi-cgi.c) + +SET(PACKAGE_NAME ${CMAKE_PROJECT_NAME}) +SET(PACKAGE_VERSION ${PACKAGE_VERSION}) +CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_BINARY_DIR}/config.h ESCAPE_QUOTES) +ADD_DEFINITIONS(-DHAVE_CONFIG_H) +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +add_executable(scgi-cgi ${MAIN_SOURCE}) + +ADD_TARGET_PROPERTIES(scgi-cgi COMPILE_FLAGS "-std=gnu99 -Wall -g -Wshadow -W -pedantic -fPIC") + +# libev +TARGET_LINK_LIBRARIES(scgi-cgi ${MYLIBEVENT_LDFLAGS}) +ADD_TARGET_PROPERTIES(scgi-cgi COMPILE_FLAGS ${LIBEVENT_CFLAGS}) + +INSTALL(TARGETS scgi-cgi DESTINATION bin) + +# man page + +SET(CMAKE_MAN_DIR "share/man" CACHE STRING + "Install location for man pages (relative to prefix).") +MARK_AS_ADVANCED(CMAKE_MAN_DIR) + +INSTALL(FILES scgi-cgi.1 DESTINATION ${CMAKE_MAN_DIR}/man1) diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..902847d --- /dev/null +++ b/COPYING @@ -0,0 +1,22 @@ + +The MIT License + +Copyright (c) 2013 Stefan Bühler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..9aba7bf --- /dev/null +++ b/Makefile.am @@ -0,0 +1,8 @@ +EXTRA_DIST=autogen.sh CMakeLists.txt config.h.cmake scgi-cgi.1 README.rst +man1_MANS=scgi-cgi.1 + +AM_CFLAGS=$(LIBEVENT_CFLAGS) +scgi_cgi_LDADD=$(LIBEVENT_LIBS) + +bin_PROGRAMS=scgi-cgi +scgi_cgi_SOURCES=scgi-cgi.c diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..46a1840 --- /dev/null +++ b/README.rst @@ -0,0 +1,50 @@ +Description +----------- + +:Homepage: + http://redmine.lighttpd.net/projects/scgi-cgi/wiki + +scgi-cgi is a SCGI application to run normal cgi applications. It doesn't +make CGI applications faster, but it allows you to run them on a different +host and with different user permissions (without the need for suexec). + +scgi-cgi is released under the `MIT license `_ + +Usage +----- + +Examples for spawning a scgi-cgi instance with daemontools or runit:: + + #!/bin/sh + # run script + + exec spawn-scgi -n -s /var/run/scgi-cgi.sock -u www-default -U www-data -- /usr/bin/scgi-cgi + + +Build dependencies +------------------ + +* libevent (http://libevent.org/) +* cmake or autotools (for snapshots/releases the autotool generated files are included) + + +Build +----- + +* snapshot/release with autotools:: + + ./configure + make + +* build from git: ``git clone git://git.lighttpd.net/scgi-cgi.git`` + + * with autotools:: + + ./autogen.sh + ./configure + make + + * with cmake (should work with snapshots/releases too):: + + cmake . + make diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..bab5b24 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +if which glibtoolize >/dev/null 2>&1; then + LIBTOOLIZE=${LIBTOOLIZE:-glibtoolize} +else + LIBTOOLIZE=${LIBTOOLIZE:-libtoolize} +fi +LIBTOOLIZE_FLAGS="--copy --force" +ACLOCAL=${ACLOCAL:-aclocal} +AUTOHEADER=${AUTOHEADER:-autoheader} +AUTOMAKE=${AUTOMAKE:-automake} +AUTOMAKE_FLAGS="--add-missing --copy" +AUTOCONF=${AUTOCONF:-autoconf} + +ARGV0=$0 + +set -e + +if [ ! -f configure.ac -o ! -f COPYING ]; then + echo "Doesn't look like you're in the source directory" >&2 + exit 1 +fi + +run() { + echo "$ARGV0: running \`$@'" + $@ +} + +run rm -f aclocal.m4 ar-lib config.guess config.sub configure depcomp instal-sh ltmain.sh missing +run rm -rf m4 autom4te.cache + +run $LIBTOOLIZE $LIBTOOLIZE_FLAGS +run $ACLOCAL $ACLOCAL_FLAGS +run $AUTOHEADER +run $AUTOMAKE $AUTOMAKE_FLAGS +run $AUTOCONF +echo "Now type './configure ...' and 'make' to compile." diff --git a/cmake/AddTargetProperties.cmake b/cmake/AddTargetProperties.cmake new file mode 100644 index 0000000..17ee774 --- /dev/null +++ b/cmake/AddTargetProperties.cmake @@ -0,0 +1,13 @@ +MACRO(ADD_TARGET_PROPERTIES _target _name) + SET(_properties) + FOREACH(_prop ${ARGN}) + SET(_properties "${_properties} ${_prop}") + ENDFOREACH(_prop) + GET_TARGET_PROPERTY(_old_properties ${_target} ${_name}) + MESSAGE(STATUS "adding property to ${_target} ${_name}:" ${_properties}) + IF(NOT _old_properties) + # in case it's NOTFOUND + SET(_old_properties) + ENDIF(NOT _old_properties) + SET_TARGET_PROPERTIES(${_target} PROPERTIES ${_name} "${_old_properties} ${_properties}") +ENDMACRO(ADD_TARGET_PROPERTIES) diff --git a/config.h.cmake b/config.h.cmake new file mode 100644 index 0000000..69bb180 --- /dev/null +++ b/config.h.cmake @@ -0,0 +1,6 @@ +/* + CMake autogenerated config.h file. Do not edit! +*/ + +#define PACKAGE_NAME "${PACKAGE_NAME}" +#define PACKAGE_VERSION "${PACKAGE_VERSION}" diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..f089d42 --- /dev/null +++ b/configure.ac @@ -0,0 +1,49 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ([2.63]) +AC_INIT([scgi-cgi], [0.1.0], [lighttpd@stbuehler.de]) +AC_CONFIG_SRCDIR([scgi-cgi.c]) +AC_CONFIG_HEADERS([config.h]) + +AM_INIT_AUTOMAKE([-Wall -Werror foreign]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_MAKE_SET + +# Checks for libraries. + +# libevent +PKG_CHECK_MODULES(LIBEVENT, libevent >= 2.0, [],[AC_MSG_ERROR("libevent >= 2.0 not found")]) + +# Checks for header files. +AC_CHECK_HEADERS([arpa/inet.h fcntl.h stdlib.h string.h sys/socket.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_PID_T +AC_TYPE_SIZE_T + +# Checks for library functions. +AC_FUNC_FORK +AC_CHECK_FUNCS([dup2]) + +# check for extra compiler options (warning options) +if test "${GCC}" = "yes"; then + CFLAGS="${CFLAGS} -Wall -W -Wshadow -pedantic -std=gnu99" +fi + +AC_ARG_ENABLE(extra-warnings, + AC_HELP_STRING([--enable-extra-warnings],[enable extra warnings (gcc specific)]), + [case "${enableval}" in + yes) extrawarnings=true ;; + no) extrawarnings=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-extra-warnings) ;; + esac],[extrawarnings=false]) + +if test x$extrawarnings = xtrue; then + CFLAGS="${CFLAGS} -g -O2 -g2 -Wall -Wmissing-declarations -Wdeclaration-after-statement -Wno-pointer-sign -Wcast-align -Winline -Wsign-compare -Wnested-externs -Wpointer-arith -Wl,--as-needed -Wformat-security" +fi + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/scgi-cgi.1 b/scgi-cgi.1 new file mode 100644 index 0000000..842dcc2 --- /dev/null +++ b/scgi-cgi.1 @@ -0,0 +1,30 @@ +.TH scgi-cgi 1 "Oct 1, 2013" +. +.SH NAME +. +scgi-cgi \- a SCGI application to run cgi applications +. +.SH OPTIONS +. +.TP 8 +.B \-b +Executable to start instead of INTERPRETER or SCRIPT_FILENAME from SCGI environment. +.TP 8 +.B \-c +Maximum number of connections (default 16) +.TP 8 +.B \-v +Shows version information and exits +. +.SH DESCRIPTION +scgi-cgi is a SCGI application to run normal cgi applications. It doesn't +make CGI applications faster, but it allows you to run them on a different +host and with different user permissions (without the need for suexec). +.P +For running you probably want spawn-fcgi (http://redmine.lighttpd.net/projects/spawn-fcgi) +.SH EXAMPLE +Example usage: + +spawn-fcgi -n -s /var/run/scgi-cgi.sock -u www-default -U www-data -- /usr/bin/scgi-cgi +.SH AUTHOR +scgi-cgi was written by Stefan Bühler. diff --git a/scgi-cgi.c b/scgi-cgi.c new file mode 100644 index 0000000..584ceb5 --- /dev/null +++ b/scgi-cgi.c @@ -0,0 +1,1263 @@ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_BUFFER_SIZE (64u*1024u) +#define MAX_STRING_BUFFER_SIZE (64u*1024u) /* env keys and values */ + +#define CONST_STR_LEN(x) (x), sizeof(x) - 1 +#define GSTR_LEN(x) (x) ? (x)->str : "", (x) ? (x)->len : 0 +#define UNUSED(x) ((void)(x)) + +#ifdef __GNUC__ +#define ATTR_WARN_UNUSED_RESULT \ + __attribute__((warn_unused_result)) +#define ATTR_FORMAT(fmt, args) \ + __attribute__(( format(printf, fmt, args) )) +#else +#define ATTR_WARN_UNUSED_RESULT +#define ATTR_FORMAT(fmt, args) +#endif + +#define ERROR(...) printerr(__LINE__, __VA_ARGS__) + +#ifdef NDEBUG +# define DEBUG(...) do { } while (0) +#else +# define DEBUG(...) printerr(__LINE__, "DEBUG: " __VA_ARGS__) +#endif + +#define PACKAGE_DESC PACKAGE_NAME " v" PACKAGE_VERSION " - SCGI application to run normal cgi applications" + +/* force asserts to be enabled */ +#undef NDEBUG +#include + +/**************************************************************************** + * logging * + ***************************************************************************/ + +static void printerr(unsigned int line, const char *fmt, ...) ATTR_FORMAT(2, 3); +static void printerr(unsigned int line, const char *fmt, ...) { + va_list ap; + + fprintf(stderr, "scgi-cgi.c:%u:", line); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, "\n"); +} + +/**************************************************************************** + * STRING BUFFER * + ***************************************************************************/ + +typedef struct string_buffer string_buffer; +struct string_buffer { + /* always 0-terminated string (unless data == NULL), but don't count terminating 0 in `used' */ + unsigned char *data; + unsigned int used, size; +}; + +static void string_buffer_init(string_buffer *buf); +static void string_buffer_clear(string_buffer *buf); +static unsigned char* string_buffer_extract(string_buffer *buf) ATTR_WARN_UNUSED_RESULT; /* resets buffer, returns string. free string with free() */ +static int string_buffer_reserve(string_buffer *buf, unsigned int len) ATTR_WARN_UNUSED_RESULT; +static int string_buffer_append(string_buffer *buf, const unsigned char *data, unsigned int len) ATTR_WARN_UNUSED_RESULT; +static int string_buffer_append_char(string_buffer *buf, unsigned char c) ATTR_WARN_UNUSED_RESULT; +static int string_buffer_equal(string_buffer *buf, const char *data, unsigned int len); + +#define STRING_BUFFER_EQUAL(buf, str) string_buffer_equal(buf, str, sizeof(str) - 1) + + +static void string_buffer_init(string_buffer *buf) { + buf->data = NULL; + buf->used = buf->size = 0; +} + +static void string_buffer_clear(string_buffer *buf) { + if (NULL != buf->data) free(buf->data); + buf->data = NULL; + buf->used = buf->size = 0; +} + +static unsigned char* string_buffer_extract(string_buffer *buf) { + unsigned char *str = buf->data; + string_buffer_init(buf); + return str; +} + +static int string_buffer_reserve(string_buffer *buf, unsigned int len) { + assert(buf->used <= buf->size); + assert(buf->size <= MAX_STRING_BUFFER_SIZE); + + if (1 > UINT_MAX - len || len + 1 > MAX_STRING_BUFFER_SIZE - buf->used) { + DEBUG("string buffer: overflow"); + return 0; + } + unsigned int newlen = buf->used + len; + if (newlen + 1 > buf->size) { + unsigned int need_size = newlen + 1; + unsigned int want_size = (need_size + 127) & ~127; /* round up to next multiple of 128 */ + unsigned char *newdata; + + if (want_size < need_size) want_size = need_size; /* overflow handling */ + newdata = (unsigned char*) realloc(buf->data, want_size); + if (NULL == newdata) { + DEBUG("string buffer: realloc failed"); + return 0; + } + + buf->data = newdata; + buf->size = want_size; + } + + return 1; +} + +static int string_buffer_append(string_buffer *buf, const unsigned char *data, unsigned int len) { + if (!string_buffer_reserve(buf, len)) return 0; + + if (len > 0) memcpy(buf->data + buf->used, data, len); + buf->used += len; + buf->data[buf->used] = '\0'; + + return 1; +} + +static int string_buffer_append_char(string_buffer *buf, unsigned char c) { + if (!string_buffer_reserve(buf, 1)) return 0; + + buf->data[buf->used++] = c; + buf->data[buf->used] = '\0'; + + return 1; +} + +static int string_buffer_equal(string_buffer *buf, const char *data, unsigned int len) { + if (buf->used != len) return 0; + if (0 == len) return 1; + return 0 == memcmp(buf->data, data, len); +} + + +/**************************************************************************** + * SCGI PARSER * + ***************************************************************************/ + +typedef enum scgi_req_parser_state { + SCGI_REQ_PARSER_HEADER_LEN, + SCGI_REQ_PARSER_HEADER_ENV_KEY, + SCGI_REQ_PARSER_HEADER_ENV_VALUE, + SCGI_REQ_PARSER_HEADER_DONE, + SCGI_REQ_PARSER_HEADER_ERROR +} scgi_req_parser_state; + +typedef struct scgi_req_parser scgi_req_parser; +struct scgi_req_parser { + scgi_req_parser_state state; + unsigned int header_length, is_scgi; + unsigned long long content_length; + + unsigned char **environ; /* list terminated by NULL entry */ + unsigned int environ_used, environ_size; + + string_buffer key_value; + unsigned int key_length; +}; + +static int key_value_has_key(scgi_req_parser *parser, char *key, unsigned int keylen); +static void scgi_parser_init(scgi_req_parser *parser); +static void scgi_parser_clear(scgi_req_parser *parser); +static void scgi_parser_clear_environment(scgi_req_parser *parser); +static int scgi_parser_environ_reserve(scgi_req_parser *parser); +static int scgi_parser_environ_append(scgi_req_parser *parser, unsigned char *kvstr, int keylength); +static int scgi_parser_environ_copy(scgi_req_parser *parser, const char *key, unsigned int keylength); +static const char* scgi_parser_environ_get(scgi_req_parser *parser, const char *key, unsigned int keylength); +static int scgi_parse(scgi_req_parser *parser, unsigned char *data, int len) ATTR_WARN_UNUSED_RESULT; + + +static int key_value_has_key(scgi_req_parser *parser, char *key, unsigned int keylen) { + if (parser->key_length != keylen) return 0; + return 0 == memcmp(parser->key_value.data, key, keylen); +} + +#define KEY_VALUE_HAS_KEY(parser, key) key_value_has_key(parser, key, sizeof(key) - 1) + + +static void scgi_parser_init(scgi_req_parser *parser) { + parser->state = SCGI_REQ_PARSER_HEADER_LEN; + parser->header_length = parser->content_length = parser->is_scgi = 0; + + parser->environ = NULL; + parser->environ_used = parser->environ_size = 0; + + string_buffer_init(&parser->key_value); + parser->key_length = 0; +} + +static void scgi_parser_clear(scgi_req_parser *parser) { + unsigned int i; + + for (i = 0; i < parser->environ_used; ++i) { + free(parser->environ[i]); + } + free(parser->environ); + + string_buffer_clear(&parser->key_value); + + parser->state = SCGI_REQ_PARSER_HEADER_LEN; + parser->header_length = parser->content_length = parser->is_scgi = 0; + + parser->environ = NULL; + parser->environ_used = parser->environ_size = 0; + + parser->key_length = 0; +} + +static void scgi_parser_clear_environment(scgi_req_parser *parser) { + unsigned int i; + + for (i = 0; i < parser->environ_used; ++i) { + free(parser->environ[i]); + } + free(parser->environ); + + parser->environ = NULL; + parser->environ_used = parser->environ_size = 0; +} + +static int scgi_parser_environ_reserve(scgi_req_parser *parser) { + if (2 > UINT_MAX/sizeof(unsigned char*) - parser->environ_used) { + DEBUG("scgi environment: entries overflow"); + return 0; /* overflow: can't append */ + } + if (parser->environ_used + 2 > parser->environ_size) { + unsigned int need_size = sizeof(unsigned char*) * (parser->environ_used + 2); + unsigned int want_size = (need_size + 1024) & ~1023; /* round up to next multiple of 1023 (for 8-byte pointers: 128 entries) */ + unsigned char **newdata; + + if (want_size < need_size) want_size = need_size; /* overflow handling */ + newdata = (unsigned char**) realloc(parser->environ, want_size); + if (NULL == newdata) { + DEBUG("scgi environment: realloc failed"); + return 0; /* ENOMEM */ + } + + parser->environ = newdata; + parser->environ_size = want_size / sizeof(unsigned char*); + } + + return 1; +} + +/* kvstr: "KEY=VALUE", keylength = strlen("KEY") + * return values: 0: not appended: key already exists (not overwriting), or some other error; 1: appended + * kvstr is free()d if it wasn't appended. + */ +static int scgi_parser_environ_append(scgi_req_parser *parser, unsigned char *kvstr, int keylength) { + unsigned int i; + + for (i = 0; i < parser->environ_used; ++i) { + const char *e = (const char*) parser->environ[i]; + if (0 == strncmp(e, (const char *) kvstr, keylength + 1)) { + DEBUG("scgi req header: duplicate key for entry '%s', already have '%s'", kvstr, e); + goto fail; /* found */ + } + } + if (!scgi_parser_environ_reserve(parser)) goto fail; + + parser->environ[parser->environ_used++] = kvstr; + parser->environ[parser->environ_used] = NULL; + + return 1; + +fail: + free(kvstr); + return 0; +} + +static int scgi_parser_environ_copy(scgi_req_parser *parser, const char *key, unsigned int keylength) { + const char *val = getenv(key); + unsigned int vallen; + unsigned char *kvstr; + unsigned int i; + + if (NULL == val) return 1; + + vallen = strlen(val); + kvstr = malloc(keylength + 2 + vallen); + if (NULL == kvstr) return 0; + + memcpy(kvstr, key, keylength); + kvstr[keylength] = '='; + memcpy(kvstr + keylength + 1, val, vallen + 1); + + for (i = 0; i < parser->environ_used; ++i) { + const char *e = (const char*) parser->environ[i]; + if (0 == strncmp(e, (const char *) kvstr, keylength + 1)) { + /* found. overwrite: */ + free(parser->environ[i]); + parser->environ[i] = kvstr; + } + } + + if (!scgi_parser_environ_reserve(parser)) { + free(kvstr); + return 0; + } + + parser->environ[parser->environ_used++] = kvstr; + parser->environ[parser->environ_used] = NULL; + + return 1; +} + +static const char* scgi_parser_environ_get(scgi_req_parser *parser, const char *key, unsigned int keylength) { + unsigned int i; + + for (i = 0; i < parser->environ_used; ++i) { + const char *e = (const char*) parser->environ[i]; + if (0 == strncmp(e, (const char *) key, keylength) && '=' == e[keylength]) { + return (const char*) e + keylength + 1; + } + } + + return NULL; +} + +/* return values: + * -2: error + * -1: need more data for header + * l>=0: trailing l bytes of data are request body data; header finished. + */ +static int scgi_parse(scgi_req_parser *parser, unsigned char *data, int len) { + assert(len > 0); + + for (; len > 0; ++data, --len) { + unsigned char c = *data; + + switch (parser->state) { + case SCGI_REQ_PARSER_HEADER_LEN: + { + unsigned int digit; + + if (c == ':') { + /* require at least 'CONTENT_LENGTH=0;SCGI=1;' in header ('=' and ';' encoded as '\0') */ + if (parser->header_length < 24) { + DEBUG("scgi req header too small: %u", parser->header_length); + goto fail; + } + parser->state = SCGI_REQ_PARSER_HEADER_ENV_KEY; + break; + } + + if (c < '0' || c > '9') { + DEBUG("scgi req header: expected digit or ':' in header length, got '%c'", c); + goto fail; + } + if (0 == parser->header_length && c == '0') { + /* extra leading zeroes are prohibited; zero header length is not permitted either: + * require CONTENT_LENGTH and SCGI vars + * => starting 0 digit is never allowed + */ + DEBUG("scgi req header: header length starting with 0"); + goto fail; + } + + digit = c - '0'; + if (parser->header_length > UINT_MAX / 10 - digit) { + DEBUG("scgi req header: header length overflow"); + goto fail; /* overflow */ + } + parser->header_length = 10*parser->header_length + digit; + } + break; + case SCGI_REQ_PARSER_HEADER_ENV_KEY: + if (0 == parser->header_length) { + if (',' != c) { + DEBUG("scgi req header: require ',' before request body, got '%c'", c); + goto fail; /* after header require a ',' */ + } + if (0 != parser->key_value.used) { + DEBUG("scgi req header: headers ended with partial key '%s'", parser->key_value.data); + goto fail; /* partial header on headers end */ + } + if (!parser->is_scgi) { + DEBUG("scgi req header: missing SCGI=1"); + goto fail; /* is_scgi is only set after CONTENT_LENGTH (always first header) is parsed, and then SCGI=1 was found */ + } + parser->state = SCGI_REQ_PARSER_HEADER_DONE; + return len - 1; + } + --parser->header_length; + + if (0 != c) { + if ('=' == c) { + DEBUG("scgi req header: key must not include '='"); + goto fail; /* '=' can't be allowed in keys */ + } + if (!string_buffer_append_char(&parser->key_value, c)) goto fail; + break; + } + + /* key end */ + parser->key_length = parser->key_value.used; + if (!string_buffer_append_char(&parser->key_value, '=')) goto fail; + parser->state = SCGI_REQ_PARSER_HEADER_ENV_VALUE; + break; + case SCGI_REQ_PARSER_HEADER_ENV_VALUE: + if (0 == parser->header_length) { + DEBUG("scgi req header: headers ended with partial value '%s'", parser->key_value.data); + goto fail; /* partial header on headers end */ + } + --parser->header_length; + + if (0 != c) { + int vallen = 1; + while (vallen < len && 0 != data[vallen]) ++vallen; + if (!string_buffer_append(&parser->key_value, data, vallen)) goto fail; + data += (vallen - 1); + len -= (vallen - 1); + parser->header_length -= (vallen - 1); + break; + } + + /* value end */ + + if (0 == parser->environ_used) { + long long clen; + char *endptr = NULL, *startptr = (char*) parser->key_value.data + parser->key_length + 1; + + if (!KEY_VALUE_HAS_KEY(parser, "CONTENT_LENGTH")) { + DEBUG("scgi req header: first header isn't CONTENT_LENGTH: '%s'", parser->key_value.data); + goto fail; /* first entry must be CONTENT_LENGTH */ + } + if (parser->key_value.used == parser->key_length + 1) { + DEBUG("scgi req header: CONTENT_LENGTH is empty"); + goto fail; /* empty CONTENT_LENGTH */ + } + + errno = 0; + clen = strtoll(startptr, &endptr, 10); + if (0 != errno) { + DEBUG("scgi req header: parsing '%s' failed: %s", parser->key_value.data, strerror(errno)); + goto fail; + } + if (endptr != (char*) parser->key_value.data + parser->key_value.used) { + DEBUG("scgi req header: parsing '%s' failed: contained more than number", parser->key_value.data); + goto fail; /* number didn't cover complete value */ + } + if (clen < 0) { + DEBUG("scgi req header: parsing '%s' failed: negative length", parser->key_value.data); + goto fail; /* number didn't cover complete value */ + } + parser->content_length = (unsigned long long) clen; + } else if (KEY_VALUE_HAS_KEY(parser, "SCGI")) { + if (STRING_BUFFER_EQUAL(&parser->key_value, "SCGI=1")) { + parser->is_scgi = 1; + } else { + DEBUG("scgi req header: SCGI is not 1: '%s'", parser->key_value.data); + goto fail; /* SCGI must be 1 */ + } + } + if (!scgi_parser_environ_append(parser, string_buffer_extract(&parser->key_value), parser->key_length)) goto fail; /* duplicate key / ENOMEM */ + parser->key_length = 0; + parser->state = SCGI_REQ_PARSER_HEADER_ENV_KEY; + + break; + case SCGI_REQ_PARSER_HEADER_DONE: + return len; + case SCGI_REQ_PARSER_HEADER_ERROR: + goto fail; + } + } + return -1; + +fail: + parser->state = SCGI_REQ_PARSER_HEADER_ERROR; + return -2; +} + +/**************************************************************************** + * SCGI RING BUFFERS * + ***************************************************************************/ + +/* ring buffer */ +typedef struct scgi_cgi_buffer scgi_cgi_buffer; +struct scgi_cgi_buffer { + unsigned int pos, len; + unsigned char data[MAX_BUFFER_SIZE]; +}; + +static void scgi_buffer_input_location(scgi_cgi_buffer *buf, unsigned char **location, ssize_t *location_size); +static void scgi_buffer_fill(scgi_cgi_buffer *buf, ssize_t n); +static void scgi_buffer_output_location(scgi_cgi_buffer *buf, unsigned char **location, ssize_t *location_size); +static void scgi_buffer_drain(scgi_cgi_buffer *buf, ssize_t n); +static int scgi_buffer_is_input_open(scgi_cgi_buffer *buf); +static void scgi_buffer_set(scgi_cgi_buffer *buf, const char *data, ssize_t len); + +#define SCGI_BUFFER_SET(buf, str) scgi_buffer_set(buf, str, sizeof(str)-1) + +static void scgi_buffer_input_location(scgi_cgi_buffer *buf, unsigned char **location, ssize_t *location_size) { + if (buf->pos + buf->len >= MAX_BUFFER_SIZE) { + *location = buf->data + (buf->pos + buf->len - MAX_BUFFER_SIZE); + *location_size = MAX_BUFFER_SIZE - buf->len; + } else { + *location = buf->data + (buf->pos + buf->len); + *location_size = MAX_BUFFER_SIZE - (buf->pos + buf->len); + } +} + +static void scgi_buffer_fill(scgi_cgi_buffer *buf, ssize_t n) { + assert(n >= 0 && (unsigned int) n <= MAX_BUFFER_SIZE - buf->len); + buf->len += n; +} + +static void scgi_buffer_output_location(scgi_cgi_buffer *buf, unsigned char **location, ssize_t *location_size) { + *location = buf->data + buf->pos; + if (buf->pos + buf->len >= MAX_BUFFER_SIZE) { + *location_size = MAX_BUFFER_SIZE - buf->pos; + } else { + *location_size = buf->len; + } +} + +static void scgi_buffer_drain(scgi_cgi_buffer *buf, ssize_t n) { + assert(n >= 0 && (unsigned int) n <= buf->len); + buf->pos = (buf->pos + n) % MAX_BUFFER_SIZE; + buf->len -= n; + if (0 == buf->len) buf->pos = 0; +} + +static int scgi_buffer_is_input_open(scgi_cgi_buffer *buf) { + return buf->len < MAX_BUFFER_SIZE; +} + +static void scgi_buffer_set(scgi_cgi_buffer *buf, const char *data, ssize_t len) { + assert(len >= 0 && len <= MAX_BUFFER_SIZE); + memcpy(buf->data, data, len); + buf->pos = 0; + buf->len = len; +} + +/**************************************************************************** + * SCGI CHILD + SERVER * + ***************************************************************************/ + +typedef struct scgi_cgi_server scgi_cgi_server; +typedef struct scgi_cgi_child scgi_cgi_child; + +struct scgi_cgi_server { + struct event_base *base; + struct event *listen_watcher; + + struct event + *sig_w_CHLD, + *sig_w_INT, + *sig_w_TERM, + *sig_w_HUP; + + const char *binary; + + unsigned int children_used, children_size; + scgi_cgi_child **children; +}; + +struct scgi_cgi_child { + unsigned int ndx; + scgi_cgi_server *srv; + + struct event *sock_in_watcher, *sock_out_watcher; + scgi_req_parser req_parser; + + pid_t pid; + + int child_stdout, child_stdin; + struct event *pipe_in_watcher, *pipe_out_watcher; + + scgi_cgi_buffer request_buf; + scgi_cgi_buffer response_buf; +}; + +static void fd_init(int fd); +static void _my_event_free_with_fd(struct event **event); +static void _my_event_free(struct event **event); +#define MY_EVENT_FREE_WITH_FD(event) _my_event_free_with_fd(&(event)) +#define MY_EVENT_FREE(event) _my_event_free(&(event)) + +static scgi_cgi_child* scgi_cgi_child_create(scgi_cgi_server *srv, int fd); +static void scgi_cgi_child_free(scgi_cgi_child *cld); +static void scgi_cgi_child_check_done(scgi_cgi_child *cld); +static void scgi_cgi_child_exec(scgi_cgi_child *cld); +static void scgi_cgi_child_start(scgi_cgi_child *cld); +static void scgi_cgi_close_socket(scgi_cgi_child *cld); +static void scgi_cgi_child_sock_in_cb(evutil_socket_t fd, short what, void *arg); +static void scgi_cgi_child_sock_out_cb(evutil_socket_t fd, short what, void *arg); +static void scgi_cgi_child_pipe_in_cb(evutil_socket_t fd, short what, void *arg); +static void scgi_cgi_child_pipe_out_cb(evutil_socket_t fd, short what, void *arg); + +static scgi_cgi_server* scgi_cgi_server_create(int fd, const char *binary, unsigned int maxconns); +static void scgi_cgi_server_free(scgi_cgi_server* srv); +static void scgi_cgi_server_child_finished(scgi_cgi_server *srv, scgi_cgi_child *cld); +static void scgi_cgi_server_accept(evutil_socket_t fd, short what, void *arg); +static void sigint_cb(evutil_socket_t fd, short what, void *arg); +static void sigchld_cb(evutil_socket_t fd, short what, void *arg); + + +/**************************************************************************** + * SCGI utils implementation * + ***************************************************************************/ + +static void fd_init(int fd) { +#ifdef _WIN32 + int i = 1; +#endif +#ifdef FD_CLOEXEC + /* close fd on exec (cgi) */ + fcntl(fd, F_SETFD, FD_CLOEXEC); +#endif +#ifdef O_NONBLOCK + fcntl(fd, F_SETFL, O_NONBLOCK | O_RDWR); +#elif defined _WIN32 + ioctlsocket(fd, FIONBIO, &i); +#endif +} + +static void _my_event_free_with_fd(struct event **event) { + int fd; + if (NULL == *event) return; + fd = event_get_fd(*event); + event_free(*event); + *event = NULL; + if (-1 != fd) close(fd); +} + +static void _my_event_free(struct event **event) { + if (NULL == *event) return; + event_free(*event); + *event = NULL; +} + +/**************************************************************************** + * SCGI CHILD implementation * + ***************************************************************************/ + + +static scgi_cgi_child* scgi_cgi_child_create(scgi_cgi_server *srv, int fd) { + struct event_base *base = srv->base; + scgi_cgi_child *cld; + int pipe_stdout[2], pipe_stdin[2]; + if (-1 == pipe(pipe_stdout)) { + ERROR("couldn't create pipe: %s", strerror(errno)); + return NULL; + } + if (-1 == pipe(pipe_stdin)) { + ERROR("couldn't create pipe: %s", strerror(errno)); + close(pipe_stdout[0]); close(pipe_stdout[1]); return NULL; + } + + cld = calloc(1, sizeof(scgi_cgi_child)); + if (NULL == cld) { + ERROR("couldn't alloc: %s", strerror(errno)); + close(pipe_stdout[0]); close(pipe_stdout[1]); close(pipe_stdin[0]); close(pipe_stdin[1]); return NULL; + } + + cld->srv = srv; + cld->pid = -1; + cld->child_stdin = pipe_stdin[0]; + cld->child_stdout = pipe_stdout[1]; + + scgi_parser_init(&cld->req_parser); + + cld->sock_in_watcher = event_new(base, fd, EV_READ | EV_PERSIST, scgi_cgi_child_sock_in_cb, cld); + cld->sock_out_watcher = event_new(base, fd, EV_WRITE | EV_PERSIST, scgi_cgi_child_sock_out_cb, cld); + fd_init(pipe_stdin[1]); + cld->pipe_out_watcher = event_new(base, pipe_stdin[1], EV_WRITE | EV_PERSIST, scgi_cgi_child_pipe_out_cb, cld); + fd_init(pipe_stdout[0]); + cld->pipe_in_watcher = event_new(base, pipe_stdout[0], EV_READ | EV_PERSIST, scgi_cgi_child_pipe_in_cb, cld); + + if (NULL == cld->sock_in_watcher || NULL == cld->sock_out_watcher || NULL == cld->pipe_in_watcher || NULL == cld->pipe_out_watcher) { + ERROR("couldn't alloc: %s", strerror(errno)); + if (NULL == cld->pipe_in_watcher) close(pipe_stdin[1]); + if (NULL == cld->pipe_out_watcher) close(pipe_stdout[0]); + scgi_cgi_child_free(cld); + return NULL; + } + + /* start handling */ + event_add(cld->sock_in_watcher, NULL); + + return cld; +} + +static void scgi_cgi_child_free(scgi_cgi_child *cld) { + /* shared fd */ + if (NULL != cld->sock_in_watcher) { + MY_EVENT_FREE(cld->sock_out_watcher); + MY_EVENT_FREE_WITH_FD(cld->sock_in_watcher); + } else { + MY_EVENT_FREE_WITH_FD(cld->sock_out_watcher); + } + + MY_EVENT_FREE_WITH_FD(cld->pipe_in_watcher); + MY_EVENT_FREE_WITH_FD(cld->pipe_out_watcher); + + if (-1 != cld->child_stdin) { + close(cld->child_stdin); + cld->child_stdin = -1; + } + if (-1 != cld->child_stdout) { + close(cld->child_stdout); + cld->child_stdout = -1; + } + + scgi_parser_clear(&cld->req_parser); + + if (-1 != cld->pid) { + kill(cld->pid, SIGTERM); + cld->pid = -1; + } + + free(cld); +} + +static void scgi_cgi_child_check_done(scgi_cgi_child *cld) { + if (-1 == cld->pid && NULL == cld->sock_out_watcher) { + MY_EVENT_FREE_WITH_FD(cld->sock_in_watcher); + MY_EVENT_FREE_WITH_FD(cld->pipe_in_watcher); + MY_EVENT_FREE_WITH_FD(cld->pipe_out_watcher); + } + + if (-1 != cld->pid || NULL != cld->sock_out_watcher || NULL != cld->sock_in_watcher || NULL != cld->pipe_in_watcher || NULL != cld->pipe_out_watcher) return; + + scgi_cgi_server_child_finished(cld->srv, cld); +} + +static const char http_503_message[] = + "Status: 503 Service Unavailable\r\n" + "Content-Length: 0\r\n" + "\r\n" + ; + +static void scgi_cgi_child_exec(scgi_cgi_child *cld) { + char **newenv; + const char *path = cld->srv->binary; + char * args[] = { NULL, NULL }; + + if (cld->child_stdin != 0) { + dup2(cld->child_stdin, 0); + close(cld->child_stdin); + } + if (cld->child_stdout != 1) { + dup2(cld->child_stdout, 1); + close(cld->child_stdout); + } +#ifdef FD_CLOEXEC + /* UNDO close fd on exec (cgi) */ + fcntl(0, F_SETFD, 0); + fcntl(1, F_SETFD, 0); +#endif + + if (NULL == path) path = scgi_parser_environ_get(&cld->req_parser, CONST_STR_LEN("INTERPRETER")); + if (NULL == path) path = scgi_parser_environ_get(&cld->req_parser, CONST_STR_LEN("SCRIPT_FILENAME")); + args[0] = (char*) path; + + /* try changing the directory. don't care about memleaks, execve() coming soon :) */ + { + char *dir = strdup(path), *sep; + if (NULL == (sep = strrchr(dir, '/'))) { + chdir("/"); + } else { + *sep = '\0'; + chdir(dir); + } + } + + scgi_parser_environ_copy(&cld->req_parser, CONST_STR_LEN("PATH")); + newenv = (char**) cld->req_parser.environ; + + execve(path, args, newenv); + + fprintf(stderr, "couldn't execve '%s': %s\n", path, strerror(errno)); + + write(1, http_503_message, sizeof(http_503_message)); + exit(-1); +} + +static void scgi_cgi_child_start(scgi_cgi_child *cld) { + cld->pid = fork(); + switch (cld->pid) { + case 0: + /* child process */ + scgi_cgi_child_exec(cld); + break; + case -1: + /* error */ + fprintf(stderr, "couldn't fork: %s\n", strerror(errno)); + + SCGI_BUFFER_SET(&cld->response_buf, http_503_message); + if (NULL != cld->sock_out_watcher) event_add(cld->sock_out_watcher, NULL); + + /* don't need those anymore */ + scgi_parser_clear_environment(&cld->req_parser); + close(cld->child_stdout); cld->child_stdout = -1; + close(cld->child_stdin); cld->child_stdin = -1; + + /* close pipe stuff */ + MY_EVENT_FREE_WITH_FD(cld->pipe_in_watcher); + MY_EVENT_FREE_WITH_FD(cld->pipe_out_watcher); + + break; + default: + /* don't need those anymore */ + scgi_parser_clear_environment(&cld->req_parser); + close(cld->child_stdout); cld->child_stdout = -1; + close(cld->child_stdin); cld->child_stdin = -1; + + /* start reading */ + event_add(cld->pipe_in_watcher, NULL); + break; + } +} + +static void scgi_cgi_close_socket(scgi_cgi_child *cld) { + cld->response_buf.pos = cld->response_buf.len = 0; + /* shared fd */ + if (NULL != cld->sock_in_watcher) { + MY_EVENT_FREE(cld->sock_out_watcher); + MY_EVENT_FREE_WITH_FD(cld->sock_in_watcher); + } else { + MY_EVENT_FREE_WITH_FD(cld->sock_out_watcher); + } + if (NULL != cld->pipe_out_watcher) event_add(cld->pipe_out_watcher, NULL); + if (NULL != cld->pipe_in_watcher) event_add(cld->pipe_in_watcher, NULL); + + scgi_cgi_child_check_done(cld); +} + +static void scgi_cgi_child_sock_in_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_child *cld = (scgi_cgi_child*) arg; + UNUSED(what); + assert(NULL != cld->sock_in_watcher); + + for (;;) { + unsigned char *buf; + ssize_t r; + + scgi_buffer_input_location(&cld->request_buf, &buf, &r); + if (0 == r) { /* buffer is full */ + event_del(cld->sock_in_watcher); + break; + } + r = read(fd, buf, r); + if (0 == r) { /* eof */ + /* shared fd */ + if (NULL == cld->sock_out_watcher) { + MY_EVENT_FREE_WITH_FD(cld->sock_in_watcher); + } else { + MY_EVENT_FREE(cld->sock_in_watcher); + } + break; + } else if (0 > r) { + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + break; /* try again later */ + default: + goto close_sock; + } + break; + } else { + if (SCGI_REQ_PARSER_HEADER_DONE != cld->req_parser.state) { + ssize_t result; + assert(0 == cld->request_buf.len); + result = scgi_parse(&cld->req_parser, buf, r); + DEBUG("scgi req parse result: %i", (int) result); + if (result >= 0) { + assert(SCGI_REQ_PARSER_HEADER_DONE == cld->req_parser.state); + if (result > 0) { + cld->request_buf.pos = r - result; + cld->request_buf.len = result; + } + scgi_cgi_child_start(cld); + } else if (result < -1) { + goto close_sock; + } + } else if (NULL != cld->pipe_out_watcher) { + scgi_buffer_fill(&cld->request_buf, r); + } else { + goto close_sock; + } + } + } + + if (cld->request_buf.len > cld->req_parser.content_length) { + cld->request_buf.len = cld->req_parser.content_length; + goto close_sock; + } + + if (NULL == cld->sock_in_watcher || 0 < cld->request_buf.len || 0 == cld->req_parser.content_length) { + if (NULL != cld->pipe_out_watcher) event_add(cld->pipe_out_watcher, NULL); + } + + scgi_cgi_child_check_done(cld); + return; + +close_sock: + scgi_cgi_close_socket(cld); +} + +static void scgi_cgi_child_sock_out_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_child *cld = (scgi_cgi_child*) arg; + UNUSED(what); + assert(NULL != cld->sock_out_watcher); + + for (;;) { + unsigned char *buf; + ssize_t r; + + scgi_buffer_output_location(&cld->response_buf, &buf, &r); + if (0 == r) { /* buffer empty */ + event_del(cld->sock_out_watcher); + break; + } + r = write(fd, buf, r); + if (0 >= r) { + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + break; /* try again later */ + default: + goto close_sock; + } + break; + } + scgi_buffer_drain(&cld->response_buf, r); + } + + if (0 == cld->response_buf.len && NULL == cld->pipe_in_watcher) { + shutdown(fd, SHUT_RDWR); + goto close_sock; + } else if (NULL != cld->pipe_in_watcher && scgi_buffer_is_input_open(&cld->response_buf)) { + event_add(cld->pipe_in_watcher, NULL); + } + + scgi_cgi_child_check_done(cld); + return; + +close_sock: + scgi_cgi_close_socket(cld); +} + +static void scgi_cgi_child_pipe_in_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_child *cld = (scgi_cgi_child*) arg; + UNUSED(what); + assert(NULL != cld->pipe_in_watcher); + + for (;;) { + unsigned char *buf; + ssize_t r; + + scgi_buffer_input_location(&cld->response_buf, &buf, &r); + if (0 == r) { /* buffer full */ + event_del(cld->pipe_in_watcher); + break; + } + r = read(fd, buf, r); + if (0 == r) { /* eof */ + MY_EVENT_FREE_WITH_FD(cld->pipe_in_watcher); + break; + } else if (0 > r) { + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + break; /* try again later */ + default: + MY_EVENT_FREE_WITH_FD(cld->pipe_in_watcher); + break; + } + break; + } else { + if (NULL != cld->sock_out_watcher) { + scgi_buffer_fill(&cld->response_buf, r); + } + } + } + + if (NULL == cld->pipe_in_watcher || cld->response_buf.len > 0) { + if (NULL != cld->sock_out_watcher) event_add(cld->sock_out_watcher, NULL); + } + + scgi_cgi_child_check_done(cld); +} + +static void scgi_cgi_child_pipe_out_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_child *cld = (scgi_cgi_child*) arg; + UNUSED(what); + assert(NULL != cld->pipe_out_watcher); + + for (;;) { + unsigned char *buf; + ssize_t r; + + scgi_buffer_output_location(&cld->request_buf, &buf, &r); + assert(cld->req_parser.content_length >= (size_t) r); + + if (0 == r) { /* buffer empty */ + event_del(cld->pipe_out_watcher); + break; + } + r = write(fd, buf, r); + if (0 >= r) { + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + break; /* try again later */ + default: + goto close_pipe; + } + break; + } + cld->req_parser.content_length -= r; + scgi_buffer_drain(&cld->request_buf, r); + } + + if (0 == cld->req_parser.content_length || (0 == cld->request_buf.len && NULL == cld->sock_in_watcher)) { + goto close_pipe; + } else if (NULL != cld->sock_in_watcher && scgi_buffer_is_input_open(&cld->request_buf)) { + event_add(cld->sock_in_watcher, NULL); + } + + scgi_cgi_child_check_done(cld); + return; + +close_pipe: + MY_EVENT_FREE_WITH_FD(cld->pipe_out_watcher); + cld->request_buf.pos = cld->request_buf.len = 0; + if (NULL != cld->sock_in_watcher) event_add(cld->sock_in_watcher, NULL); + + scgi_cgi_child_check_done(cld); +} + +/**************************************************************************** + * SCGI SERVER implementation * + ***************************************************************************/ + +#define CATCH_SIGNAL(cb, n) do { \ + srv->sig_w_##n = event_new(srv->base, SIG##n, EV_SIGNAL|EV_PERSIST, cb, srv); \ + assert(NULL != srv->sig_w_##n); \ + event_add(srv->sig_w_##n, NULL); \ +} while (0) + +#define UNCATCH_SIGNAL(n) MY_EVENT_FREE(srv->sig_w_##n) + +static scgi_cgi_server* scgi_cgi_server_create(int fd, const char *binary, unsigned int maxconns) { + scgi_cgi_server* srv = calloc(1, sizeof(scgi_cgi_server)); + + srv->children = (scgi_cgi_child**) calloc(maxconns, sizeof(scgi_cgi_child*)); + assert(NULL != srv->children); + srv->children_used = 0; + srv->children_size = maxconns; + + srv->binary = binary; + + srv->base = event_base_new(); + fd_init(fd); + srv->listen_watcher = event_new(srv->base, fd, EV_READ | EV_PERSIST, scgi_cgi_server_accept, srv); + event_add(srv->listen_watcher, NULL); + + CATCH_SIGNAL(sigint_cb, INT); + CATCH_SIGNAL(sigint_cb, TERM); + CATCH_SIGNAL(sigint_cb, HUP); + CATCH_SIGNAL(sigchld_cb, CHLD); + + return srv; +} + +static void scgi_cgi_server_free(scgi_cgi_server* srv) { + while (srv->children_used > 0) { + scgi_cgi_server_child_finished(srv, srv->children[0]); + } + + MY_EVENT_FREE_WITH_FD(srv->listen_watcher); + + UNCATCH_SIGNAL(INT); + UNCATCH_SIGNAL(TERM); + UNCATCH_SIGNAL(HUP); + UNCATCH_SIGNAL(CHLD); + + free(srv->children); + srv->children = NULL; + srv->children_size = 0; + free(srv); +} + +static void scgi_cgi_server_child_finished(scgi_cgi_server *srv, scgi_cgi_child *cld) { + unsigned int ndx = cld->ndx; + assert(srv->children[ndx] == cld); + assert(ndx < srv->children_used); + + DEBUG("Child %i finished", ndx); + + --srv->children_used; + if (ndx != srv->children_used) { + srv->children[ndx] = srv->children[srv->children_used]; + srv->children[ndx]->ndx = ndx; + } + srv->children[srv->children_used] = NULL; + + scgi_cgi_child_free(cld); + + if (NULL == srv->listen_watcher && 0 == srv->children_used) UNCATCH_SIGNAL(CHLD); /* shutdown */ + if (NULL != srv->listen_watcher) event_add(srv->listen_watcher, NULL); +} + +static void scgi_cgi_server_accept(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_server *srv = (scgi_cgi_server*) arg; + int confd; + UNUSED(what); + + if (srv->children_used == srv->children_size) { + event_del(srv->listen_watcher); + return; + } + + while (srv->children_used < srv->children_size) { + if (-1 != (confd = accept(fd, NULL, NULL))) { + scgi_cgi_child *cld; + + fd_init(confd); + cld = scgi_cgi_child_create(srv, confd); + if (NULL == cld) { + if (0 == srv->children_used) { + ERROR("no children running, and child creation failed. abort."); + exit(-2); + } + ERROR("child creation failed, disable listening temporarily until next child finishes"); + close(confd); + event_del(srv->listen_watcher); + return; + } + srv->children[srv->children_used++] = cld; + } else { + break; + } + } +} + +static void sigint_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_server *srv = (scgi_cgi_server*) arg; + UNUSED(fd); + UNUSED(what); + + UNCATCH_SIGNAL(INT); + UNCATCH_SIGNAL(TERM); + UNCATCH_SIGNAL(HUP); + + MY_EVENT_FREE_WITH_FD(srv->listen_watcher); + + if (0 == srv->children_used) UNCATCH_SIGNAL(CHLD); + + fprintf(stderr, "Got signal, shutdown (%i children remaining)\n", srv->children_used); +} + +static void sigchld_cb(evutil_socket_t fd, short what, void *arg) { + scgi_cgi_server *srv = (scgi_cgi_server*) arg; + pid_t pid; + int status; + UNUSED(fd); + UNUSED(what); + + while (srv->children_used > 0) { + if (-1 != (pid = waitpid(-1, &status, WNOHANG))) { + unsigned int i; + for (i = 0; i < srv->children_used; ++i) { + scgi_cgi_child *cld = srv->children[i]; + if (cld->pid == pid) { + DEBUG("child %i terminated with status %i", i, status); + cld->pid = -1; + scgi_cgi_child_check_done(cld); + break; + } + } + } else { + break; + } + } +} + +static void show_help() { + fprintf(stderr, PACKAGE_DESC "\n"); + fprintf(stderr, + "Usage: scgi-cgi [-b binary] [-c maxconns] [-h] [-v] -- [binary]\n" + "Options:\n" + " -b binary the executable to call instead of INTERPRETER\n" + " or SCRIPT_FILENAME from SCGI environment (default: none)\n" + " -c maxconns how many connections to accept at the same time (default: 16)\n" + " -v show version\n" + " -h show this help\n" + ); +} + +int main (int argc, char **argv) { + scgi_cgi_server *srv; + const char *binary = NULL; + unsigned int maxconn = 16; + int o; + + while(-1 != (o = getopt(argc, argv, "b:c:hv"))) { + switch(o) { + case 'b': + binary = optarg; + break; + case 'c': + maxconn = atoi(optarg); + break; + case 'v': + fprintf(stderr, PACKAGE_DESC "\n"); + return 0; + case 'h': + show_help(); + return 0; + default: + show_help(); + return -1; + } + } + + if (optind < argc && argv[optind] && NULL == binary) binary = argv[optind]; + + srv = scgi_cgi_server_create(0, binary, maxconn); + event_base_loop(srv->base, 0); + scgi_cgi_server_free(srv); + return 0; +}