#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; }