diff options
-rw-r--r-- | .gitignore | 13 | ||||
-rw-r--r-- | COPYING | 22 | ||||
-rw-r--r-- | Makefile.am | 17 | ||||
-rw-r--r-- | README | 2 | ||||
-rwxr-xr-x | autogen.sh | 27 | ||||
-rw-r--r-- | configure.ac | 61 | ||||
-rw-r--r-- | libafcgi.c | 874 | ||||
-rw-r--r-- | libafcgi.h | 167 | ||||
-rw-r--r-- | libafcgi.pc.in | 11 |
9 files changed, 1194 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d46e92d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +Makefile.in +aclocal.m4 +autom4te.cache +config.guess +config.sub +configure +depcomp +install-sh +libafcgi-config.h.in +ltmain.sh +m4/ +missing +*~ @@ -0,0 +1,22 @@ + +The MIT License + +Copyright (c) 2010 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..842a9b6 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,17 @@ +EXTRA_DIST=autogen.sh libafcgi.pc.in + +ACLOCAL_AMFLAGS=-I m4 + +AM_CFLAGS=$(GLIB_CFLAGS) + +lib_LTLIBRARIES=libafcgi.la +libafcgi_la_SOURCES=libafcgi.c +libafcgi_la_LIBADD=$(GLIB_LIBS) +libafcgi_la_LDFLAGS= -version-info 0:0:0 + +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = libafcgi.pc + +$(pkgconfig_DATA): config.status + +include_HEADERS = libafcgi.h libafcgi-config.h @@ -0,0 +1,2 @@ + +libafcgi is a libev based asynchronous FastCGI library. diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..ae2d948 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +LIBTOOLIZE=${LIBTOOLIZE:-libtoolize} +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 + + +run() { + echo "$ARGV0: running \`$@'" + $@ +} + +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/configure.ac b/configure.ac new file mode 100644 index 0000000..9061066 --- /dev/null +++ b/configure.ac @@ -0,0 +1,61 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ([2.63]) +AC_INIT([libafcgi], [0.1.0], [lighttpd@stbuehler.de]) +AC_CONFIG_SRCDIR([libafcgi.c]) +AC_CONFIG_HEADERS([libafcgi-config.h]) + +AC_CONFIG_MACRO_DIR([m4]) + +AM_INIT_AUTOMAKE([-Wall -Werror foreign]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_LIBTOOL +AC_PROG_MAKE_SET + +# Checks for libraries. + +# glib-2.0 +PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.16.0, [ + AC_DEFINE([HAVE_GLIB_H], [1], [glib.h]) +],[AC_MSG_ERROR("glib-2.0 >= 2.16.0 not found")]) + +# lib ev +AC_CHECK_HEADERS([ev.h], [], [AC_MSG_ERROR("ev.h not found")]) +AC_CHECK_LIB([ev], [ev_loop], [ + LIBS="-lev ${LIBS}" + AC_DEFINE([HAVE_LIBEV], [1], [ev_loop in -lev]) + ], [AC_MSG_ERROR("libev 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 libafcgi.pc]) +AC_OUTPUT diff --git a/libafcgi.c b/libafcgi.c new file mode 100644 index 0000000..7be3b46 --- /dev/null +++ b/libafcgi.c @@ -0,0 +1,874 @@ + +#include "libafcgi.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <sys/socket.h> +#include <fcntl.h> +#include <unistd.h> +#include <string.h> + +typedef struct fastcgi_queue_link { + GList queue_link; + enum { FASTCGI_QUEUE_STRING, FASTCGI_QUEUE_BYTEARRAY } elem_type; +} fastcgi_queue_link; + +/* some util functions */ +#define GSTR_LEN(x) ((x) ? (x)->str : ""), ((x) ? (x)->len : 0) +#define GBARR_LEN(x) ((x)->data), ((x)->len) +#define UNUSED(x) ((void)(x)) +#define ERROR(...) g_printerr("libafcgi.c:" G_STRINGIFY(__LINE__) ": " __VA_ARGS__) + +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 fastcgi_queue_link* fastcgi_queue_link_new_string(GString *s) { + fastcgi_queue_link *l = g_slice_new0(fastcgi_queue_link); + l->queue_link.data = s; + l->elem_type = FASTCGI_QUEUE_STRING; + return l; +} + +static fastcgi_queue_link* fastcgi_queue_link_new_bytearray(GByteArray *a) { + fastcgi_queue_link *l = g_slice_new0(fastcgi_queue_link); + l->queue_link.data = a; + l->elem_type = FASTCGI_QUEUE_BYTEARRAY ; + return l; +} + +static void fastcgi_queue_link_free(fastcgi_queue *queue, fastcgi_queue_link *l) { + switch (l->elem_type) { + case FASTCGI_QUEUE_STRING: + if (queue) queue->length -= ((GString*)l->queue_link.data)->len; + g_string_free(l->queue_link.data, TRUE); + break; + case FASTCGI_QUEUE_BYTEARRAY: + if (queue) queue->length -= ((GByteArray*)l->queue_link.data)->len; + g_byte_array_free(l->queue_link.data, TRUE); + break; + } + g_slice_free(fastcgi_queue_link, l); +} + +static fastcgi_queue_link *fastcgi_queue_peek_head(fastcgi_queue *queue) { + return (fastcgi_queue_link*) g_queue_peek_head_link(&queue->queue); +} + +static fastcgi_queue_link *fastcgi_queue_pop_head(fastcgi_queue *queue) { + return (fastcgi_queue_link*) g_queue_pop_head_link(&queue->queue); +} + +void fastcgi_queue_clear(fastcgi_queue *queue) { + fastcgi_queue_link *l; + queue->offset = 0; + while (NULL != (l = fastcgi_queue_pop_head(queue))) { + fastcgi_queue_link_free(queue, l); + } + g_assert(0 == queue->length); +} + +void fastcgi_queue_append_string(fastcgi_queue *queue, GString *buf) { + fastcgi_queue_link *l; + if (!buf) return; + if (!buf->len) { g_string_free(buf, TRUE); return; } + l = fastcgi_queue_link_new_string(buf); + g_queue_push_tail_link(&queue->queue, (GList*) l); + queue->length += buf->len; +} + +void fastcgi_queue_append_bytearray(fastcgi_queue *queue, GByteArray *buf) { + fastcgi_queue_link *l; + if (!buf) return; + if (!buf->len) { g_byte_array_free(buf, TRUE); return; } + l = fastcgi_queue_link_new_bytearray(buf); + g_queue_push_tail_link(&queue->queue, (GList*) l); + queue->length += buf->len; +} + +/* return values: 0 ok, -1 error, -2 con closed */ +gint fastcgi_queue_write(int fd, fastcgi_queue *queue, gsize max_write) { + gsize rem_write = max_write; + g_assert(rem_write <= G_MAXSSIZE); +#ifdef TCP_CORK + int corked = 0; +#endif + +#ifdef TCP_CORK + /* Linux: put a cork into the socket as we want to combine the write() calls + * but only if we really have multiple chunks + */ + if (queue->queue.length > 1) { + corked = 1; + setsockopt(fd, IPPROTO_TCP, TCP_CORK, &corked, sizeof(corked)); + } +#endif + + while (rem_write > 0 && queue->length > 0) { + fastcgi_queue_link *l = fastcgi_queue_peek_head(queue); + gsize towrite, datalen; + gssize res; + gchar *data; + switch (l->elem_type) { + case FASTCGI_QUEUE_STRING: + data = ((GString*) l->queue_link.data)->str; + datalen = towrite = ((GString*) l->queue_link.data)->len; + break; + case FASTCGI_QUEUE_BYTEARRAY: + data = (gchar*) ((GByteArray*) l->queue_link.data)->data; + datalen = towrite = ((GByteArray*) l->queue_link.data)->len; + break; + default: + g_error("invalid fastcgi_queue_link type\n"); + } + towrite -= queue->offset; data += queue->offset; + if (towrite > rem_write) towrite = rem_write; + res = write(fd, data, towrite); + if (-1 == res) { +#ifdef TCP_CORK + if (corked) { + corked = 0; + setsockopt(fd, IPPROTO_TCP, TCP_CORK, &corked, sizeof(corked)); + } +#endif + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + return 0; /* try again later */ + case ECONNRESET: + case EPIPE: + return -2; + default: + ERROR("write to fd=%d failed, %s\n", fd, g_strerror(errno) ); + return -1; + } + } else { + queue->offset += res; + rem_write -= res; + if (queue->offset == datalen) { + queue->offset = 0; + fastcgi_queue_link_free(queue, fastcgi_queue_pop_head(queue)); + } + } + } + +#ifdef TCP_CORK + if (corked) { + corked = 0; + setsockopt(fd, IPPROTO_TCP, TCP_CORK, &corked, sizeof(corked)); + } +#endif + + return 0; +} + + +static void ev_io_add_events(struct ev_loop *loop, ev_io *watcher, int events) { + if ((watcher->events & events) == events) return; + ev_io_stop(loop, watcher); + ev_io_set(watcher, watcher->fd, watcher->events | events); + ev_io_start(loop, watcher); +} + +static void ev_io_rem_events(struct ev_loop *loop, ev_io *watcher, int events) { + if (0 == (watcher->events & events)) return; + ev_io_stop(loop, watcher); + ev_io_set(watcher, watcher->fd, watcher->events & ~events); + ev_io_start(loop, watcher); +} +/* end: some util functions */ + +static const guint8 __padding[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + +static void append_padding_str(GString *s, guint8 padlen) { + g_string_append_len(s, (const gchar*) __padding, padlen); +} + +static void append_padding_bytearray(GByteArray *a, guint8 padlen) { + g_byte_array_append(a, __padding, padlen); +} + +/* returns padding length */ +static guint8 stream_build_fcgi_record(GByteArray *buf, guint8 type, guint16 requestid, guint16 datalen) { + guint8 padlen = (8 - (datalen & 0x7)) % 8; /* padding must be < 8 */ + + /* alloc enough space */ + g_byte_array_set_size(buf, FCGI_HEADER_LEN); + buf->len = 0; + + buf->data[buf->len++] = FCGI_VERSION_1; + buf->data[buf->len++] = type; + buf->data[buf->len++] = (guint8) (requestid >> 8); + buf->data[buf->len++] = (guint8) (requestid); + buf->data[buf->len++] = (guint8) (datalen >> 8); + buf->data[buf->len++] = (guint8) (datalen); + buf->data[buf->len++] = padlen; + buf->data[buf->len++] = 0; + return padlen; +} + +/* returns padding length */ +static guint8 stream_send_fcgi_record(fastcgi_queue *out, guint8 type, guint16 requestid, guint16 datalen) { + GByteArray *record = g_byte_array_sized_new(FCGI_HEADER_LEN); + guint8 padlen = stream_build_fcgi_record(record, type, requestid, datalen); + fastcgi_queue_append_bytearray(out, record); + return padlen; +} + +static void stream_send_data(fastcgi_queue *out, guint8 type, guint16 requestid, const guint8 *data, size_t datalen) { + while (datalen > 0) { + guint16 tosend = (datalen > G_MAXUINT16) ? G_MAXUINT16 : datalen; + guint8 padlen = stream_send_fcgi_record(out, type, requestid, tosend); + GByteArray *buf = g_byte_array_sized_new(tosend + padlen); + g_byte_array_append(buf, data, tosend); + append_padding_bytearray(buf, padlen); + fastcgi_queue_append_bytearray(out, buf); + data += tosend; + datalen -= tosend; + } +} + +/* kills string */ +static void stream_send_string(fastcgi_queue *out, guint8 type, guint16 requestid, GString *data) { + if (data->len > G_MAXUINT16) { + stream_send_data(out, type, requestid, (const guint8*) GSTR_LEN(data)); + g_string_free(data, TRUE); + } else { + guint8 padlen = stream_send_fcgi_record(out, type, requestid, data->len); + append_padding_str(data, padlen); + fastcgi_queue_append_string(out, data); + } +} + +/* kills bytearray */ +static void stream_send_bytearray(fastcgi_queue *out, guint8 type, guint16 requestid, GByteArray *data) { + if (data->len > G_MAXUINT16) { + stream_send_data(out, type, requestid, GBARR_LEN(data)); + g_byte_array_free(data, TRUE); + } else { + guint8 padlen = stream_send_fcgi_record(out, type, requestid, data->len); + append_padding_bytearray(data, padlen); + fastcgi_queue_append_bytearray(out, data); + } +} + +static void stream_send_end_request(fastcgi_queue *out, guint16 requestID, gint32 appStatus, enum FCGI_ProtocolStatus status) { + GByteArray *record; + + record = g_byte_array_sized_new(16); + stream_build_fcgi_record(record, FCGI_END_REQUEST, requestID, 8); + + /* alloc enough space */ + g_byte_array_set_size(record, 16); + record->len = 8; + + appStatus = htonl(appStatus); + g_byte_array_append(record, (const guchar*) &appStatus, sizeof(appStatus)); + record->data[record->len++] = status; + g_byte_array_append(record, __padding, 3); + fastcgi_queue_append_bytearray(out, record); +} + + +static void write_queue(fastcgi_connection *fcon) { + if (fcon->closing) return; + + if (fastcgi_queue_write(fcon->fd, &fcon->write_queue, 256*1024) < 0) { + fastcgi_connection_close(fcon); + return; + } + + if (fcon->fsrv->callbacks->cb_wrote_data) { + fcon->fsrv->callbacks->cb_wrote_data(fcon); + } + + if (!fcon->closing) { + if (fcon->write_queue.length > 0) { + ev_io_add_events(fcon->fsrv->loop, &fcon->fd_watcher, EV_WRITE); + } else { + ev_io_rem_events(fcon->fsrv->loop, &fcon->fd_watcher, EV_WRITE); + if (0 == fcon->requestID) { + if (!(fcon->flags & FCGI_KEEP_CONN)) { + fastcgi_connection_close(fcon); + } + } + } + } +} + +static GByteArray* read_chunk(fastcgi_connection *fcon, guint maxlen) { + gssize res; + GByteArray *buf; + int tmp_errno; + + buf = g_byte_array_sized_new(maxlen); + g_byte_array_set_size(buf, maxlen); + if (0 == maxlen) return buf; + + res = read(fcon->fd, buf->data, maxlen); + if (res == -1) { + tmp_errno = errno; + g_byte_array_free(buf, TRUE); + errno = tmp_errno; + return NULL; + } else if (res == 0) { + g_byte_array_free(buf, TRUE); + errno = ECONNRESET; + return NULL; + } else { + g_byte_array_set_size(buf, res); + return buf; + } +} + +/* read content + padding, but only returns content data. decrements counters */ +static GByteArray *read_content(fastcgi_connection *fcon) { + GByteArray *buf; + + buf = read_chunk(fcon, fcon->content_remaining + fcon->padding_remaining); + if (!buf) return NULL; + if (buf->len > fcon->content_remaining) { + fcon->padding_remaining -= (buf->len - fcon->content_remaining); + g_byte_array_set_size(buf, fcon->content_remaining); + fcon->content_remaining = 0; + } else { + fcon->content_remaining -= buf->len; + } + return buf; +} + +static gboolean read_append_chunk(fastcgi_connection *fcon, GByteArray *buf) { + gssize res; + int tmp_errno; + guint curlen = buf->len; + const guint maxlen = fcon->content_remaining + fcon->padding_remaining; + if (0 == maxlen) return TRUE; + + g_byte_array_set_size(buf, curlen + maxlen); + res = read(fcon->fd, buf->data + curlen, maxlen); + if (res == -1) { + tmp_errno = errno; + g_byte_array_set_size(buf, curlen); + errno = tmp_errno; + return FALSE; + } else if (res == 0) { + g_byte_array_set_size(buf, curlen); + errno = ECONNRESET; + return FALSE; + } else { + /* remove padding data */ + if (res > fcon->content_remaining) { + fcon->padding_remaining -= res - fcon->content_remaining; + res = fcon->content_remaining; + } + g_byte_array_set_size(buf, curlen + res); + fcon->content_remaining -= res; + return TRUE; + } +} + +static gboolean read_key_value(fastcgi_connection *fcon, GByteArray *buf, guint *pos, gchar **key, guint *keylen, gchar **value, guint *valuelen) { + const unsigned char *data = (const unsigned char*) buf->data; + guint32 klen, vlen; + guint p = *pos, len = buf->len; + + if (len - p < 2) return FALSE; + + klen = data[p++]; + if (klen & 0x80) { + if (len - p < 100) return FALSE; + klen = ((klen & 0x7f) << 24) | (data[p] << 16) | (data[p+1] << 8) | data[p+2]; + p += 3; + } + vlen = data[p++]; + if (vlen & 0x80) { + if (len - p < 100) return FALSE; + vlen = ((vlen & 0x7f) << 24) | (data[p] << 16) | (data[p+1] << 8) | data[p+2]; + p += 3; + } + if (klen > FASTCGI_MAX_KEYLEN || vlen > FASTCGI_MAX_VALUELEN) { + fastcgi_connection_close(fcon); + return FALSE; + } + if (len - p < klen + vlen) return FALSE; + *key = (gchar*) &buf->data[p]; + *keylen = klen; + p += klen; + *value = (gchar*) &buf->data[p]; + *valuelen = vlen; + p += vlen; + *pos = p; + return TRUE; +} + +static void parse_params(const fastcgi_callbacks *fcbs, fastcgi_connection *fcon) { + if (!fcon->current_header.contentLength) { + fcbs->cb_new_request(fcon); + g_byte_array_set_size(fcon->parambuf, 0); + } else { + guint pos = 0, keylen = 0, valuelen = 0; + gchar *key = NULL, *value = NULL; + while (read_key_value(fcon, fcon->parambuf, &pos, &key, &keylen, &value, &valuelen)) { + GString *gkey = g_string_new_len(key, keylen); + GString *gvalue = g_string_new_len(value, valuelen); + g_hash_table_insert(fcon->environ, gkey, gvalue); + } + if (!fcon->closing) + g_byte_array_remove_range(fcon->parambuf, 0, pos); + } +} + +static void parse_get_values(fastcgi_connection *fcon) { + /* just send the request back and don't insert results */ + GByteArray *tmp = g_byte_array_sized_new(0); + stream_send_bytearray(&fcon->write_queue, FCGI_GET_VALUES_RESULT, 0, fcon->buffer); + *fcon->buffer = *tmp; + /* TODO: provide get-values result */ +} + +static void read_queue(fastcgi_connection *fcon) { + gssize res; + GByteArray *buf; + const fastcgi_callbacks *fcbs = fcon->fsrv->callbacks; + + for (;;) { + if (fcon->closing || fcon->read_suspended) return; + + if (fcon->headerbuf_used < 8) { + const unsigned char *data = fcon->headerbuf; + res = read(fcon->fd, fcon->headerbuf + fcon->headerbuf_used, 8 - fcon->headerbuf_used); + if (0 == res) { errno = ECONNRESET; goto handle_error; } + if (-1 == res) goto handle_error; + fcon->headerbuf_used += res; + if (fcon->headerbuf_used < 8) return; /* need more data */ + + fcon->current_header.version = data[0]; + fcon->current_header.type = data[1]; + fcon->current_header.requestID = (data[2] << 8) | (data[3]); + fcon->current_header.contentLength = (data[4] << 8) | (data[5]); + fcon->current_header.paddingLength = data[6]; + fcon->content_remaining = fcon->current_header.contentLength; + fcon->padding_remaining = fcon->current_header.paddingLength; + fcon->first = TRUE; + g_byte_array_set_size(fcon->buffer, 0); + + if (fcon->current_header.version != FCGI_VERSION_1) { + fastcgi_connection_close(fcon); + return; + } + } + + if (fcon->current_header.type != FCGI_BEGIN_REQUEST && + (0 != fcon->current_header.requestID) && fcon->current_header.requestID != fcon->requestID) { + /* ignore packet data */ + if (0 != fcon->content_remaining + fcon->padding_remaining) { + if (NULL == (buf = read_content(fcon))) goto handle_error; + g_byte_array_free(buf, TRUE); + } + if (0 == fcon->content_remaining + fcon->padding_remaining) { + fcon->headerbuf_used = 0; + } + continue; + } + + if (fcon->first || fcon->content_remaining) { + fcon->first = FALSE; + switch (fcon->current_header.type) { + case FCGI_BEGIN_REQUEST: + if (8 != fcon->current_header.contentLength || 0 == fcon->current_header.requestID) goto error; + if (!read_append_chunk(fcon, fcon->buffer)) goto handle_error; + if (0 == fcon->content_remaining) { + if (fcon->requestID) { + stream_send_end_request(&fcon->write_queue, fcon->current_header.requestID, 0, FCGI_CANT_MPX_CONN); + } else { + unsigned char *data = (unsigned char*) fcon->buffer->data; + fcon->requestID = fcon->current_header.requestID; + fcon->role = (data[0] << 8) | (data[1]); + fcon->flags = data[2]; + g_byte_array_set_size(fcon->parambuf, 0); + } + } + break; + case FCGI_ABORT_REQUEST: + if (0 != fcon->current_header.contentLength || 0 == fcon->current_header.requestID) goto error; + fcbs->cb_request_aborted(fcon); + break; + case FCGI_END_REQUEST: + goto error; /* invalid type */ + case FCGI_PARAMS: + if (0 == fcon->current_header.requestID) goto error; + if (!read_append_chunk(fcon, fcon->parambuf)) goto handle_error; + parse_params(fcbs, fcon); + break; + case FCGI_STDIN: + if (0 == fcon->current_header.requestID) goto error; + buf = NULL; + if (0 != fcon->content_remaining && + NULL == (buf = read_content(fcon))) goto handle_error; + if (fcbs->cb_received_stdin) { + fcbs->cb_received_stdin(fcon, buf); + } else { + g_byte_array_free(buf, TRUE); + } + break; + case FCGI_STDOUT: + goto error; /* invalid type */ + case FCGI_STDERR: + goto error; /* invalid type */ + case FCGI_DATA: + if (0 == fcon->current_header.requestID) goto error; + buf = NULL; + if (0 != fcon->content_remaining && + NULL == (buf = read_content(fcon))) goto handle_error; + if (fcbs->cb_received_data) { + fcbs->cb_received_data(fcon, buf); + } else { + g_byte_array_free(buf, TRUE); + } + break; + case FCGI_GET_VALUES: + if (0 != fcon->current_header.requestID) goto error; + if (!read_append_chunk(fcon, fcon->buffer)) goto handle_error; + if (0 == fcon->content_remaining) + parse_get_values(fcon); + break; + case FCGI_GET_VALUES_RESULT: + goto error; /* invalid type */ + break; + case FCGI_UNKNOWN_TYPE: + /* we didn't send anything fancy, so this is not expected */ + goto error; /* invalid type */ + default: + break; + } + } + + if (0 == fcon->content_remaining) { + if (0 == fcon->padding_remaining) { + fcon->headerbuf_used = 0; + } else { + if (NULL == (buf = read_chunk(fcon, fcon->padding_remaining))) goto handle_error; + fcon->padding_remaining -= buf->len; + if (0 == fcon->padding_remaining) { + fcon->headerbuf_used = 0; + } + g_byte_array_free(buf, TRUE); + } + } + + } + + return; + +handle_error: + switch (errno) { + case EINTR: + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + return; /* try again later */ + case ECONNRESET: + break; + default: + ERROR("read from fd=%d failed, %s\n", fcon->fd, g_strerror(errno) ); + break; + } + +error: + if (0 != fcon->requestID) + fcbs->cb_request_aborted(fcon); + fastcgi_connection_close(fcon); +} + +static void fastcgi_connection_fd_cb(struct ev_loop *loop, ev_io *w, int revents) { + fastcgi_connection *fcon = (fastcgi_connection*) w->data; + UNUSED(loop); + + if (revents & EV_READ) { + read_queue(fcon); + } + + if (revents & EV_WRITE) { + write_queue(fcon); + } +} + +static void _g_string_destroy(gpointer data) { + g_string_free(data, TRUE); +} + +static fastcgi_connection *fastcgi_connecion_create(fastcgi_server *fsrv, gint fd, guint id) { + fastcgi_connection *fcon = g_slice_new0(fastcgi_connection); + + fcon->fsrv = fsrv; + fcon->fcon_id = id; + + fcon->buffer = g_byte_array_sized_new(0); + fcon->parambuf = g_byte_array_sized_new(0); + fcon->environ = g_hash_table_new_full((GHashFunc) g_string_hash, (GEqualFunc) g_string_equal, _g_string_destroy, _g_string_destroy); + + fcon->fd = fd; + fd_init(fcon->fd); + ev_io_init(&fcon->fd_watcher, fastcgi_connection_fd_cb, fcon->fd, EV_READ); + fcon->fd_watcher.data = fcon; + ev_io_start(fcon->fsrv->loop, &fcon->fd_watcher); + + return fcon; +} + +static void fastcgi_connection_free(fastcgi_connection *fcon) { + fcon->fsrv->callbacks->cb_reset_connection(fcon); + + if (fcon->fd != -1) { + ev_io_stop(fcon->fsrv->loop, &fcon->fd_watcher); + close(fcon->fd); + fcon->fd = -1; + } + + fastcgi_queue_clear(&fcon->write_queue); + g_hash_table_destroy(fcon->environ); + g_byte_array_free(fcon->buffer, TRUE); + g_byte_array_free(fcon->parambuf, TRUE); + + g_slice_free(fastcgi_connection, fcon); +} + +void fastcgi_connection_close(fastcgi_connection *fcon) { + fcon->closing = TRUE; + if (fcon->fd != -1) { + ev_io_stop(fcon->fsrv->loop, &fcon->fd_watcher); + close(fcon->fd); + fcon->fd = -1; + } + + fastcgi_queue_clear(&fcon->write_queue); + + g_byte_array_set_size(fcon->buffer, 0); + g_byte_array_set_size(fcon->parambuf, 0); + g_hash_table_remove_all(fcon->environ); + + ev_prepare_start(fcon->fsrv->loop, &fcon->fsrv->closing_watcher); +} + +static void fastcgi_server_fd_cb(struct ev_loop *loop, ev_io *w, int revents) { + fastcgi_server *fsrv = (fastcgi_server*) w->data; + fastcgi_connection *fcon; + void (*cb_new_connection)(fastcgi_connection *fcon) = fsrv->callbacks->cb_new_connection; + + g_assert(revents & EV_READ); + + for (;;) { + gint fd = accept(fsrv->fd, NULL, NULL); + if (-1 == fd) { + switch (errno) { + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + case EINTR: + /* we were stopped _before_ we had a connection */ + case ECONNABORTED: /* this is a FreeBSD thingy */ + /* we were stopped _after_ we had a connection */ + return; + case EMFILE: + if (0 == fsrv->max_connections) { + fsrv->max_connections = fsrv->connections->len / 2; + } else { + fsrv->max_connections = fsrv->max_connections / 2; + } + ERROR("dropped connection limit to %u as we got EMFILE\n", fsrv->max_connections); + ev_io_rem_events(loop, w, EV_READ); + return; + default: + ERROR("accept failed on fd=%d with error: %s\nshutting down\n", fsrv->fd, g_strerror(errno)); + fastcgi_server_stop(fsrv); + return; + } + } + + fcon = fastcgi_connecion_create(fsrv, fd, fsrv->connections->len); + g_ptr_array_add(fsrv->connections, fcon); + if (cb_new_connection) { + cb_new_connection(fcon); + } + + if (fsrv->connections->len >= fsrv->max_connections) { + ev_io_rem_events(loop, w, EV_READ); + return; + } + + if (fsrv->do_shutdown) return; + } +} + +static void fastcgi_cleanup_connections(fastcgi_server *fsrv) { + guint i; + + for (i = 0; i < fsrv->connections->len; ) { + fastcgi_connection *fcon = g_ptr_array_index(fsrv->connections, i); + if (fcon->closing) { + fastcgi_connection *t_fcon; + guint l = fsrv->connections->len-1; + t_fcon = g_ptr_array_index(fsrv->connections, i) = g_ptr_array_index(fsrv->connections, l); + g_ptr_array_set_size(fsrv->connections, l); + t_fcon->fcon_id = i; + fastcgi_connection_free(fcon); + } else { + i++; + } + } +} + +static void fastcgi_closing_cb(struct ev_loop *loop, ev_prepare *w, int revents) { + UNUSED(revents); + ev_prepare_stop(loop, w); + fastcgi_cleanup_connections((fastcgi_server*) w->data); +} + +fastcgi_server *fastcgi_server_create(struct ev_loop *loop, gint socketfd, const fastcgi_callbacks *callbacks, guint max_connections) { + fastcgi_server *fsrv = g_slice_new0(fastcgi_server); + + fsrv->callbacks = callbacks; + + fsrv->max_connections = max_connections; + + fsrv->connections = g_ptr_array_sized_new(fsrv->max_connections); + + fsrv->loop = loop; + fsrv->fd = socketfd; + fd_init(fsrv->fd); + ev_io_init(&fsrv->fd_watcher, fastcgi_server_fd_cb, fsrv->fd, EV_READ); + fsrv->fd_watcher.data = fsrv; + ev_io_start(fsrv->loop, &fsrv->fd_watcher); + + ev_prepare_init(&fsrv->closing_watcher, fastcgi_closing_cb); + fsrv->closing_watcher.data = fsrv; + + return fsrv; +} + +void fastcgi_server_stop(fastcgi_server *fsrv) { + if (fsrv->do_shutdown) return; + fsrv->do_shutdown = TRUE; + + ev_io_stop(fsrv->loop, &fsrv->fd_watcher); + close(fsrv->fd); + fsrv->fd = -1; +} + +void fastcgi_server_free(fastcgi_server *fsrv) { + guint i; + void (*cb_request_aborted)(fastcgi_connection *fcon) = fsrv->callbacks->cb_request_aborted; + if (!fsrv->do_shutdown) fastcgi_server_stop(fsrv); + ev_prepare_stop(fsrv->loop, &fsrv->closing_watcher); + + for (i = 0; i < fsrv->connections->len; i++) { + fastcgi_connection *fcon = g_ptr_array_index(fsrv->connections, i); + cb_request_aborted(fcon); + fcon->closing = TRUE; + } + fastcgi_cleanup_connections(fsrv); + g_ptr_array_free(fsrv->connections, TRUE); + + g_slice_free(fastcgi_server, fsrv); +} + +void fastcgi_end_request(fastcgi_connection *fcon, gint32 appStatus, enum FCGI_ProtocolStatus status) { + gboolean had_data = (fcon->write_queue.length > 0); + + if (0 == fcon->requestID) return; + stream_send_end_request(&fcon->write_queue, fcon->requestID, appStatus, status); + fcon->requestID = 0; + if (!had_data) write_queue(fcon); +} + +void fastcgi_suspend_read(fastcgi_connection *fcon) { + fcon->read_suspended = TRUE; + ev_io_rem_events(fcon->fsrv->loop, &fcon->fd_watcher, EV_READ); +} + +void fastcgi_resume_read(fastcgi_connection *fcon) { + fcon->read_suspended = FALSE; + ev_io_add_events(fcon->fsrv->loop, &fcon->fd_watcher, EV_READ); +} + +void fastcgi_send_out(fastcgi_connection *fcon, GString *data) { + gboolean had_data = (fcon->write_queue.length > 0); + if (!data) { + stream_send_fcgi_record(&fcon->write_queue, FCGI_STDOUT, fcon->requestID, 0); + } else { + stream_send_string(&fcon->write_queue, FCGI_STDOUT, fcon->requestID, data); + } + if (!had_data) write_queue(fcon); +} + +void fastcgi_send_err(fastcgi_connection *fcon, GString *data) { + gboolean had_data = (fcon->write_queue.length > 0); + if (!data) { + stream_send_fcgi_record(&fcon->write_queue, FCGI_STDERR, fcon->requestID, 0); + } else { + stream_send_string(&fcon->write_queue, FCGI_STDERR, fcon->requestID, data); + } + if (!had_data) write_queue(fcon); +} + +void fastcgi_send_out_bytearray(fastcgi_connection *fcon, GByteArray *data) { + gboolean had_data = (fcon->write_queue.length > 0); + if (!data) { + stream_send_fcgi_record(&fcon->write_queue, FCGI_STDOUT, fcon->requestID, 0); + } else { + stream_send_bytearray(&fcon->write_queue, FCGI_STDOUT, fcon->requestID, data); + } + if (!had_data) write_queue(fcon); +} + +void fastcgi_send_err_bytearray(fastcgi_connection *fcon, GByteArray *data) { + gboolean had_data = (fcon->write_queue.length > 0); + if (!data) { + stream_send_fcgi_record(&fcon->write_queue, FCGI_STDERR, fcon->requestID, 0); + } else { + stream_send_bytearray(&fcon->write_queue, FCGI_STDERR, fcon->requestID, data); + } + if (!had_data) write_queue(fcon); +} + +char** fastcgi_build_env(fastcgi_connection *con) { + GPtrArray *env = g_ptr_array_new(); + GHashTableIter iter; + gpointer pkey, pvalue; + + g_hash_table_iter_init(&iter, con->environ); + while (g_hash_table_iter_next(&iter, &pkey, &pvalue)) { + GString *key = pkey, *value = pvalue; + char *s = g_malloc(key->len + value->len + 2); + memcpy(s, key->str, key->len); + memcpy(s + key->len + 1, value->str, value->len); + s[key->len] = '='; + s[key->len + value->len + 1] = '\0'; + g_ptr_array_add(env, s); + } + g_ptr_array_add(env, NULL); + + return (char**) g_ptr_array_free(env, FALSE); +} + +const gchar* fastcgi_connection_environ_lookup(fastcgi_connection *fcon, const gchar* key, gsize keylen) { + GString s = { (gchar*) key, keylen, 0 }; + GString *value = g_hash_table_lookup(fcon->environ, &s); + return (NULL != value) ? value->str : NULL; +} diff --git a/libafcgi.h b/libafcgi.h new file mode 100644 index 0000000..e067bb1 --- /dev/null +++ b/libafcgi.h @@ -0,0 +1,167 @@ +#ifndef _FCGI_CGI_FASTCGI_H +#define _FCGI_CGI_FASTCGI_H + +#include "libafcgi-config.h" + +/* NO multiplexing support */ + +#include <glib.h> +#include <ev.h> + +/* FastCGI constants */ +# define FCGI_VERSION_1 1 +# define FCGI_HEADER_LEN 8 + + 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 + }; +# define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + + enum FCGI_Flags { + FCGI_KEEP_CONN = 1 + }; + + enum FCGI_Role { + FCGI_RESPONDER = 1, + FCGI_AUTHORIZER = 2, + FCGI_FILTER = 3 + }; + + enum FCGI_ProtocolStatus { + FCGI_REQUEST_COMPLETE = 0, + FCGI_CANT_MPX_CONN = 1, + FCGI_OVERLOADED = 2, + FCGI_UNKNOWN_ROLE = 3 + }; + +#define FASTCGI_MAX_KEYLEN 1024 +#define FASTCGI_MAX_VALUELEN 64*1024 +/* end FastCGI constants */ + +struct fastcgi_server; +typedef struct fastcgi_server fastcgi_server; + +struct fastcgi_callbacks; +typedef struct fastcgi_callbacks fastcgi_callbacks; + +struct fastcgi_connection; +typedef struct fastcgi_connection fastcgi_connection; + +struct fastcgi_queue; +typedef struct fastcgi_queue fastcgi_queue; + +struct fastcgi_server { +/* custom user data */ + gpointer data; + +/* private data */ + gboolean do_shutdown; + + const fastcgi_callbacks *callbacks; + + guint max_connections; + GPtrArray *connections; + guint cur_requests; + + gint fd; + struct ev_loop *loop; + ev_io fd_watcher; + ev_prepare closing_watcher; +}; + +struct fastcgi_callbacks { + void (*cb_new_connection)(fastcgi_connection *fcon); /* new connection accepted */ + void (*cb_new_request)(fastcgi_connection *fcon); /* new request on connection, env/params are ready */ + void (*cb_wrote_data)(fastcgi_connection *fcon); + void (*cb_received_stdin)(fastcgi_connection *fcon, GByteArray *data); /* data == NULL => eof */ + void (*cb_received_data)(fastcgi_connection *fcon, GByteArray *data); /* data == NULL => eof */ + void (*cb_request_aborted)(fastcgi_connection *fcon); + void (*cb_reset_connection)(fastcgi_connection *fcon); /* cleanup custom data before fcon is freed, not for keep-alive */ +}; + +struct fastcgi_queue { + GQueue queue; + gsize offset; /* offset in first chunk */ + gsize length; + gboolean closed; +}; + +struct fastcgi_connection { +/* custom user data */ + gpointer data; + +/* read/write */ + GHashTable *environ; /* GString -> GString */ + +/* read only */ + fastcgi_server *fsrv; + guint fcon_id; /* index in server con array */ + gboolean closing; /* "dead" connection */ + + /* current request */ + guint16 requestID; + guint16 role; + guint8 flags; + +/* private data */ + unsigned char headerbuf[8]; + guint headerbuf_used; + gboolean first; + + struct { + guint8 version; + guint8 type; + guint16 requestID; + guint16 contentLength; + guint8 paddingLength; + } current_header; + + guint content_remaining, padding_remaining; + + GByteArray *buffer, *parambuf; + + gint fd; + ev_io fd_watcher; + + gboolean read_suspended; + + /* write queue */ + fastcgi_queue write_queue; +}; + +fastcgi_server *fastcgi_server_create(struct ev_loop *loop, gint socketfd, const fastcgi_callbacks *callbacks, guint max_connections); +void fastcgi_server_stop(fastcgi_server *fsrv); /* stop accepting new connections, closes listening socket */ +void fastcgi_server_free(fastcgi_server *fsrv); + +void fastcgi_suspend_read(fastcgi_connection *fcon); +void fastcgi_resume_read(fastcgi_connection *fcon); + +void fastcgi_end_request(fastcgi_connection *fcon, gint32 appStatus, enum FCGI_ProtocolStatus status); +void fastcgi_send_out(fastcgi_connection *fcon, GString *data); +void fastcgi_send_err(fastcgi_connection *fcon, GString *data); +void fastcgi_send_out_bytearray(fastcgi_connection *fcon, GByteArray *data); +void fastcgi_send_err_bytearray(fastcgi_connection *fcon, GByteArray *data); + +void fastcgi_connection_close(fastcgi_connection *fcon); /* shouldn't be needed */ + +void fastcgi_queue_append_string(fastcgi_queue *queue, GString *buf); +void fastcgi_queue_append_bytearray(fastcgi_queue *queue, GByteArray *buf); +void fastcgi_queue_clear(fastcgi_queue *queue); + +/* return values: 0 ok, -1 error, -2 con closed */ +gint fastcgi_queue_write(int fd, fastcgi_queue *queue, gsize max_write); + +char** fastcgi_build_env(fastcgi_connection *con); +const gchar* fastcgi_connection_environ_lookup(fastcgi_connection *fcon, const gchar* key, gsize keylen); + +#endif diff --git a/libafcgi.pc.in b/libafcgi.pc.in new file mode 100644 index 0000000..74d1d16 --- /dev/null +++ b/libafcgi.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: libafcgi +Description: asynchronous FastCGI library +Version: @VERSION@ +Requires: glib-2.0 +Libs: -L${libdir} -lafcgi +Cflags: -I${includedir} |