/*
* Copyright (c) 2007-2013 Grzegorz Nosek
*
* 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.
*/
#define NO_FCGI_DEFINES
#include "config.h"
#include <stdarg.h>
#include <fcgi_stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#include <stdbool.h>
#include <sys/wait.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#endif
/* glibc doesn't seem to export it */
#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 108
#endif
extern char **environ;
static char * const * inherited_environ;
static const char * blacklisted_env_vars[] = {
"AUTH_TYPE",
"CONTENT_LENGTH",
"CONTENT_TYPE",
"GATEWAY_INTERFACE",
"PATH_INFO",
"PATH_TRANSLATED",
"QUERY_STRING",
"REMOTE_ADDR",
"REMOTE_HOST",
"REMOTE_IDENT",
"REMOTE_USER",
"REQUEST_METHOD",
"SCRIPT_NAME",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
NULL,
};
static int stderr_to_fastcgi = 0;
#define FCGI_BUF_SIZE 4096
static int write_all(int fd, char *buf, size_t size)
{
size_t nleft = size;
while (nleft > 0) {
ssize_t nwritten = write(fd, buf, nleft);
if (nwritten < 0)
return nleft - size; /* zero or negative to indicate error */
buf += nwritten;
nleft -= nwritten;
}
return size;
}
#define MAX_VA_SENTINEL INT_MIN
static int max_va(int p1, ...)
{
va_list va;
int max = p1;
int p;
va_start(va, p1);
do {
p = va_arg(va, int);
if (p > max)
max = p;
} while (p != MAX_VA_SENTINEL);
va_end(va);
return max;
}
enum reply_state_t {
REPLY_STATE_INIT,
REPLY_STATE_HEADER,
REPLY_STATE_CR,
REPLY_STATE_LF,
REPLY_STATE_2CR,
REPLY_STATE_2LF,
REPLY_STATE_BODY,
REPLY_STATE_MAX
};
enum char_class_t {
CC_NORMAL,
CC_CR,
CC_LF,
CC_MAX
};
#define ACTION_MASK (15 << 4)
#define ACTION_EMIT 0
#define ACTION_ERROR (1 << 4)
#define ACTION_END (2 << 4)
#define ACTION_SKIP (3 << 4)
#define ACTION_EXTRA_CR (4 << 4)
#define ACTION_EXTRA_LF (5 << 4)
static const unsigned char header_state_machine[REPLY_STATE_MAX][CC_MAX] = {
[REPLY_STATE_INIT] = {
[CC_NORMAL] = REPLY_STATE_HEADER,
[CC_CR] = ACTION_ERROR,
[CC_LF] = ACTION_ERROR,
},
[REPLY_STATE_HEADER] = {
[CC_NORMAL] = REPLY_STATE_HEADER,
[CC_CR] = REPLY_STATE_CR,
[CC_LF] = REPLY_STATE_LF | ACTION_EXTRA_CR,
},
[REPLY_STATE_CR] = {
[CC_NORMAL] = REPLY_STATE_HEADER | ACTION_EXTRA_LF,
[CC_CR] = REPLY_STATE_CR | ACTION_SKIP,
[CC_LF] = REPLY_STATE_LF,
},
[REPLY_STATE_LF] = {
[CC_NORMAL] = REPLY_STATE_HEADER,
[CC_CR] = REPLY_STATE_2CR,
[CC_LF] = REPLY_STATE_2LF | ACTION_EXTRA_CR,
},
[REPLY_STATE_2CR] = {
[CC_NORMAL] = REPLY_STATE_BODY | ACTION_EXTRA_LF,
[CC_CR] = REPLY_STATE_CR | ACTION_SKIP,
[CC_LF] = REPLY_STATE_2LF,
},
[REPLY_STATE_2LF] = {
[CC_NORMAL] = REPLY_STATE_BODY | ACTION_END,
[CC_CR] = REPLY_STATE_BODY | ACTION_END,
[CC_LF] = REPLY_STATE_BODY | ACTION_END,
},
[REPLY_STATE_BODY] = {
[CC_NORMAL] = REPLY_STATE_BODY | ACTION_END,
[CC_CR] = REPLY_STATE_BODY | ACTION_END,
[CC_LF] = REPLY_STATE_BODY | ACTION_END,
},
};
struct fcgi_context {
int fd_stdin;
int fd_stdout;
int fd_stderr;
unsigned int reply_state;
pid_t cgi_pid;
};
static void fcgi_finish(struct fcgi_context *fc, const char* msg)
{
if (fc->reply_state == REPLY_STATE_INIT) {
FCGI_puts("Status: 502 Bad Gateway\nContent-type: text/plain\n");
FCGI_printf("An error occurred while %s\n", msg);
}
if (fc->fd_stdin >= 0) close(fc->fd_stdin);
if (fc->fd_stdout >= 0) close(fc->fd_stdout);
if (fc->fd_stderr >= 0) close(fc->fd_stderr);
if (fc->cgi_pid)
kill(SIGTERM, fc->cgi_pid);
}
static const char * fcgi_pass_fd(struct fcgi_context *fc, int *fdp, FCGI_FILE *ffp, char *buf, size_t bufsize)
{
ssize_t nread;
char *p = buf;
unsigned char cclass, next_state;
nread = read(*fdp, buf, bufsize);
if (nread > 0) {
while (p < buf + nread) {
if (*p == '\r') {
cclass = CC_CR;
} else if (*p == '\n') {
cclass = CC_LF;
} else {
cclass = CC_NORMAL;
}
next_state = header_state_machine[fc->reply_state][cclass];
fc->reply_state = next_state & ~ACTION_MASK;
switch(next_state & ACTION_MASK) {
case ACTION_ERROR:
return "parsing CGI reply";
case ACTION_END:
goto out_of_loop;
case ACTION_SKIP:
goto next_char;
case ACTION_EXTRA_CR:
if (FCGI_fputc('\r', ffp) == EOF) return "writing CGI reply";
break;
case ACTION_EXTRA_LF:
if (FCGI_fputc('\n', ffp) == EOF) return "writing CGI reply";
break;
}
if (FCGI_fputc(*p, ffp) == EOF) {
return "writing CGI reply";
}
next_char:
p++;
}
out_of_loop:
if (p < buf + nread) {
if (FCGI_fwrite(p, 1, buf + nread - p, ffp) != (size_t)(buf + nread - p)) {
return "writing CGI reply";
}
}
} else {
if (nread < 0) {
return "reading CGI reply";
}
close(*fdp);
*fdp = -1;
}
return NULL;
}
static const char * fcgi_pass_raw_fd(int *fdp, int fd_out, char *buf, size_t bufsize)
{
ssize_t nread;
nread = read(*fdp, buf, bufsize);
if (nread > 0) {
if (write_all(fd_out, buf, nread) != nread) {
return "writing CGI reply";
}
} else {
if (nread < 0) {
return "reading CGI reply";
}
close(*fdp);
*fdp = -1;
}
return NULL;
}
static bool fcgi_pass_request(struct fcgi_context *fc)
{
char buf[FCGI_BUF_SIZE];
ssize_t nread;
/* eat the whole request and pass it to CGI */
while ((nread = FCGI_fread(buf, 1, sizeof(buf), FCGI_stdin)) > 0) {
if (write_all(fc->fd_stdin, buf, nread) <= 0) {
fcgi_finish(fc, "reading the request");
return false;
}
}
close(fc->fd_stdin);
fc->fd_stdin = -1;
return true;
}
static void fcgi_pass(struct fcgi_context *fc)
{
char buf[FCGI_BUF_SIZE];
fd_set rset;
int maxfd = 1 + max_va(fc->fd_stdout, fc->fd_stderr, MAX_VA_SENTINEL);
int nready;
const char *err;
if (!fcgi_pass_request(fc))
return;
/* now pass CGI reply back */
while (fc->fd_stdout >= 0 || fc->fd_stderr >= 0) {
FD_ZERO(&rset);
if (fc->fd_stdout >= 0) FD_SET(fc->fd_stdout, &rset);
if (fc->fd_stderr >= 0) FD_SET(fc->fd_stderr, &rset);
nready = select(maxfd, &rset, NULL, NULL, NULL);
if (nready < 0) {
if (errno == EAGAIN) continue;
fcgi_finish(fc, "waiting for CGI reply");
return;
}
if (fc->fd_stdout >= 0 && FD_ISSET(fc->fd_stdout, &rset)) {
err = fcgi_pass_fd(fc, &fc->fd_stdout, FCGI_stdout, buf, sizeof(buf));
if (err) {
fcgi_finish(fc, err);
return;
}
}
if (fc->fd_stderr >= 0 && FD_ISSET(fc->fd_stderr, &rset)) {
if (stderr_to_fastcgi)
err = fcgi_pass_fd(fc, &fc->fd_stderr, FCGI_stderr, buf, sizeof(buf));
else
err = fcgi_pass_raw_fd(&fc->fd_stderr, 2, buf, sizeof(buf));
if (err) {
fcgi_finish(fc, err);
return;
}
}
}
fc->cgi_pid = 0;
fcgi_finish(fc, "reading CGI reply (no response received)");
}
static int check_file_perms(const char *path)
{
struct stat ls;
struct stat fs;
if (lstat(path, &ls) < 0) {
return -ENOENT;
} else if (S_ISREG(ls.st_mode)) {
if (ls.st_mode & S_IXUSR) {
return 0;
} else {
return -EACCES;
}
} else if (!S_ISLNK(ls.st_mode)) {
return -EACCES;
}
if (stat(path, &fs) < 0) {
return -ENOENT;
} else if (S_ISREG(fs.st_mode)) {
if (fs.st_mode & S_IXUSR) {
return 0;
} else {
return -EACCES;
}
} else {
return -EACCES;
}
}
static char *get_cgi_filename(void) /* and fixup environment */
{
int buflen = 1, docrootlen;
char *buf = NULL;
char *docroot, *scriptname, *p;
int rf_len;
char *pathinfo = NULL;
if ((p = getenv("SCRIPT_FILENAME"))) {
if (check_file_perms(p) != 0)
goto err;
return strdup(p);
}
if ((p = getenv("DOCUMENT_ROOT"))) {
docroot = p;
docrootlen = strlen(p);
buflen += docrootlen;
} else {
goto err;
}
if ((p = getenv("SCRIPT_NAME"))) {
buflen += strlen(p);
scriptname = p;
} else {
goto err;
}
buf = malloc(buflen);
if (!buf) goto err;
strcpy(buf, docroot);
strcpy(buf + docrootlen, scriptname);
pathinfo = strdup(buf);
if (!pathinfo) {
goto err;
}
while(1) {
switch(check_file_perms(buf)) {
case -EACCES:
goto err;
case 0:
rf_len = strlen(buf);
if (rf_len < buflen - 1) {
setenv("PATH_INFO", pathinfo + rf_len, 1);
setenv("SCRIPT_NAME", buf + docrootlen, 1);
} else {
unsetenv("PATH_INFO");
}
free(pathinfo);
return buf;
default:
p = strrchr(buf, '/');
if (!p) goto err;
*p = 0;
}
}
err:
free(pathinfo);
free(buf);
return NULL;
}
static int blacklisted_env(const char *var_name, const char *var_name_end)
{
const char **p;
if (var_name_end - var_name > 4 && !strncmp(var_name, "HTTP", 4)) {
/* HTTP_*, HTTPS */
return 1;
}
for (p = blacklisted_env_vars; *p; p++) {
if (!strcmp(var_name, *p)) {
return 1;
}
}
return 0;
}
static void inherit_environment(void)
{
char * const * p;
char *q;
for (p = inherited_environ; *p; p++) {
q = strchr(*p, '=');
if (!q) {
fprintf(stderr, "Suspect value in environment: %s\n", *p);
continue;
}
*q = 0;
if (!getenv(*p) && !blacklisted_env(*p, q)) {
*q = '=';
putenv(*p);
}
*q = '=';
}
}
static void cgi_error(const char *message, const char *reason, const char *filename)
{
printf("Status: %s\r\nContent-Type: text/plain\r\n\r\n%s\r\n",
message, message);
fflush(stdout);
if (filename) {
fprintf(stderr, "%s (%s)\n", reason, filename);
} else {
fputs(reason, stderr);
fputc('\n', stderr);
}
_exit(99);
}
static void handle_fcgi_request(void)
{
int pipe_in[2];
int pipe_out[2];
int pipe_err[2];
char *filename;
char *last_slash;
pid_t pid;
struct fcgi_context fc;
if (pipe(pipe_in) < 0) goto err_pipein;
if (pipe(pipe_out) < 0) goto err_pipeout;
if (pipe(pipe_err) < 0) goto err_pipeerr;
switch((pid = fork())) {
case -1:
goto err_fork;
case 0: /* child */
close(pipe_in[1]);
close(pipe_out[0]);
close(pipe_err[0]);
dup2(pipe_in[0], 0);
dup2(pipe_out[1], 1);
dup2(pipe_err[1], 2);
close(pipe_in[0]);
close(pipe_out[1]);
close(pipe_err[1]);
close(FCGI_fileno(FCGI_stdout));
signal(SIGCHLD, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
filename = get_cgi_filename();
inherit_environment();
if (!filename)
cgi_error("403 Forbidden", "Cannot get script name, are DOCUMENT_ROOT and SCRIPT_NAME (or SCRIPT_FILENAME) set and is the script executable?", NULL);
last_slash = strrchr(filename, '/');
if (!last_slash)
cgi_error("403 Forbidden", "Script name must be a fully qualified path", filename);
*last_slash = 0;
if (chdir(filename) < 0)
cgi_error("403 Forbidden", "Cannot chdir to script directory", filename);
*last_slash = '/';
execl(filename, filename, (void *)NULL);
cgi_error("502 Bad Gateway", "Cannot execute script", filename);
default: /* parent */
close(pipe_in[0]);
close(pipe_out[1]);
close(pipe_err[1]);
fc.fd_stdin = pipe_in[1];
fc.fd_stdout = pipe_out[0];
fc.fd_stderr = pipe_err[0];
fc.reply_state = REPLY_STATE_INIT;
fc.cgi_pid = pid;
fcgi_pass(&fc);
}
return;
err_fork:
close(pipe_err[0]);
close(pipe_err[1]);
err_pipeerr:
close(pipe_out[0]);
close(pipe_out[1]);
err_pipeout:
close(pipe_in[0]);
close(pipe_in[1]);
err_pipein:
FCGI_puts("Status: 502 Bad Gateway\nContent-type: text/plain\n");
FCGI_puts("System error");
}
static void fcgiwrap_main(void)
{
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
inherited_environ = environ;
while (FCGI_Accept() >= 0) {
handle_fcgi_request();
}
}
static volatile sig_atomic_t nrunning;
static void sigchld_handler(int dummy)
{
int status;
while ((dummy = waitpid(-1, &status, WNOHANG)) != -1) {
/* sanity check */
if (nrunning > 0)
nrunning--;
/* we _should_ print something about the exit code
* but the sighandler context is _very_ bad for this
*/
}
}
static void prefork(int nchildren)
{
int startup = 1;
if (nchildren == 1) {
return;
}
signal(SIGCHLD, sigchld_handler);
while (1) {
while (nrunning < nchildren) {
pid_t pid = fork();
if (pid == 0) {
return;
} else if (pid != -1) {
nrunning++;
} else {
if (startup) {
fprintf(stderr, "Failed to prefork: %s\n", strerror(errno));
exit(1);
} else {
fprintf(stderr, "Failed to fork: %s\n", strerror(errno));
break;
}
}
}
startup = 0;
pause();
}
}
static int listen_on_fd(int fd) {
int one = 1;
if (listen(fd, 511) < 0) {
perror("Failed to listen");
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one) < 0) {
perror("Failed to enable SO_REUSEADDR");
return -1;
}
if (dup2(fd, 0) < 0) {
perror("Failed to move socket to fd 0");
return -1;
}
if (close(fd) < 0) {
perror("Failed to close original socket");
return -1;
}
return 0;
}
static int setup_socket(char *url) {
char *p = url;
char *q;
int fd;
int port;
size_t sockaddr_size;
union {
struct sockaddr sa;
struct sockaddr_un sa_un;
struct sockaddr_in sa_in;
struct sockaddr_in6 sa_in6;
} sa;
if (!strncmp(p, "unix:", sizeof("unix:") - 1)) {
p += sizeof("unix:") - 1;
if (strlen(p) >= UNIX_PATH_MAX) {
fprintf(stderr, "Socket path too long, exceeds %d characters\n",
UNIX_PATH_MAX);
return -1;
}
sockaddr_size = sizeof sa.sa_un;
sa.sa_un.sun_family = AF_UNIX;
strcpy(sa.sa_un.sun_path, p);
} else if (!strncmp(p, "tcp:", sizeof("tcp:") - 1)) {
p += sizeof("tcp:") - 1;
q = strchr(p, ':');
if (!q) {
goto invalid_url;
}
port = atoi(q+1);
if (port <= 0 || port > 65535) {
goto invalid_url;
}
sockaddr_size = sizeof sa.sa_in;
sa.sa_in.sin_family = AF_INET;
sa.sa_in.sin_port = htons(port);
*q = 0;
if (inet_pton(AF_INET, p, &sa.sa_in.sin_addr) < 1) {
goto invalid_url;
}
} else if (!strncmp(p, "tcp6:[", sizeof("tcp6:[") - 1)) {
p += sizeof("tcp6:[") - 1;
q = strchr(p, ']');
if (!q || !q[0] || q[1] != ':') {
goto invalid_url;
}
port = atoi(q+2);
if (port <= 0 || port > 65535) {
goto invalid_url;
}
sockaddr_size = sizeof sa.sa_in6;
sa.sa_in6.sin6_family = AF_INET6;
sa.sa_in6.sin6_port = htons(port);
*q = 0;
if (inet_pton(AF_INET6, p, &sa.sa_in6.sin6_addr) < 1) {
goto invalid_url;
}
} else {
invalid_url:
fprintf(stderr, "Valid socket URLs are:\n"
"unix:/path/to/socket for Unix sockets\n"
"tcp:dot.ted.qu.ad:port for IPv4 sockets\n"
"tcp6:[ipv6_addr]:port for IPv6 sockets\n");
return -1;
}
fd = socket(sa.sa.sa_family, SOCK_STREAM, 0);
if (fd < 0) {
perror("Failed to create socket");
return -1;
}
if (bind(fd, &sa.sa, sockaddr_size) < 0) {
perror("Failed to bind");
return -1;
}
return listen_on_fd(fd);
}
int main(int argc, char **argv)
{
int nchildren = 1;
char *socket_url = NULL;
int c;
while ((c = getopt(argc, argv, "c:hfs:")) != -1) {
switch (c) {
case 'f':
stderr_to_fastcgi++;
break;
case 'h':
printf("Usage: %s [OPTION]\nInvokes CGI scripts as FCGI.\n\n"
PACKAGE_NAME" version "PACKAGE_VERSION"\n\n"
"Options are:\n"
" -f\t\t\tSend CGI's stderr over FastCGI\n"
" -c <number>\t\tNumber of processes to prefork\n"
" -s <socket_url>\tSocket to bind to (say -s help for help)\n"
" -h\t\t\tShow this help message and exit\n"
"\nReport bugs to Grzegorz Nosek <"PACKAGE_BUGREPORT">.\n"
PACKAGE_NAME" home page: <http://nginx.localdomain.pl/wiki/FcgiWrap>\n",
argv[0]
);
return 0;
case 'c':
nchildren = atoi(optarg);
break;
case 's':
socket_url = strdup(optarg);
break;
case '?':
if (optopt == 'c' || optopt == 's')
fprintf(stderr, "Option -%c requires an argument.\n", optopt);
else if (isprint(optopt))
fprintf(stderr, "Unknown option `-%c'.\n", optopt);
else
fprintf(stderr,
"Unknown option character `\\x%x'.\n",
optopt);
return 1;
default:
abort();
}
}
#ifdef HAVE_SYSTEMD
if (sd_listen_fds(true) > 0) {
/* systemd woke us up. we should never see more than one FD passed to us. */
if (listen_on_fd(SD_LISTEN_FDS_START) < 0) {
return 1;
}
} else
#endif
if (socket_url) {
if (setup_socket(socket_url) < 0) {
return 1;
}
free(socket_url);
}
prefork(nchildren);
fcgiwrap_main();
return 0;
}