#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 #ifndef UNIX_PATH_MAX #define UNIX_PATH_MAX 108 #endif extern char **environ; static char * const * inherited_environ; static const char **allowed_programs; static size_t allowed_programs_count; 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; 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; 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; 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) { 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)) { 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 bool is_allowed_program(const char *program) { size_t i; if (!allowed_programs_count) return true; for (i = 0; i < allowed_programs_count; i++) { if (!strcmp(allowed_programs[i], program)) return true; } return false; } 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; char *p; 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: 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); if (!is_allowed_program(filename)) cgi_error("403 Forbidden", "The given script is not allowed to execute", filename); p = getenv("FCGI_CHDIR"); if (p == 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 = '/'; } else if (strcmp(p, "-") != 0) { if (chdir(p) < 0) { cgi_error("403 Forbidden", "Cannot chdir to FCGI_CHDIR directory", p); } } execl(filename, filename, (void *)NULL); cgi_error("502 Bad Gateway", "Cannot execute script", filename); default: 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 volatile sig_atomic_t sigint_received ; static void sigint_handler(int __attribute__((__unused__))dummy) { sigint_received = 1; FCGX_ShutdownPending(); // Or we could send SIGUSR1 } static void fcgiwrap_main(void) { struct sigaction a; signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // Use sigaction for SIGINT so we can avoid SA_RESTART and actually react a.sa_handler = sigint_handler; a.sa_flags = 0; sigemptyset( &a.sa_mask ); sigaction( SIGINT, &a, NULL ); sigaction( SIGTERM, &a, NULL ); inherited_environ = environ; while (FCGI_Accept() >= 0 && !sigint_received) { handle_fcgi_request(); } } static volatile sig_atomic_t nrunning; static void sigchld_handler(int dummy) { int status; while ((dummy = waitpid(-1, &status, WNOHANG)) > 0) { if (nrunning > 0) nrunning--; } } 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; } if (listen_on_fd(fd) < 0) { return -1; } return fd; } int main(int argc, char **argv) { int nchildren = 1; char *socket_url = NULL; int fd = 0; int c; while ((c = getopt(argc, argv, "c:hfs:p:")) != -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" " -p <path>\t\tRestrict execution to this script. (repeated options will be merged)\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 'p': allowed_programs = realloc(allowed_programs, (allowed_programs_count + 1) * sizeof (char *)); if (!allowed_programs) abort(); allowed_programs[allowed_programs_count++] = strdup(optarg); break; case '?': if (optopt == 'c' || optopt == 's' || optopt == 'p') 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) { if (listen_on_fd(SD_LISTEN_FDS_START) < 0) { return 1; } } else #endif if (socket_url) { fd = setup_socket(socket_url); if (fd < 0) { return 1; } } prefork(nchildren); fcgiwrap_main(); if (fd) { const char *p = socket_url; close(fd); if (socket_url) { if (!strncmp(p, "unix:", sizeof("unix:") - 1)) { p += sizeof("unix:") - 1; unlink(p); } } } return 0; } |
* 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 |
#ifndef UNIX_PATH_MAX |
#define UNIX_PATH_MAX 108 |
#endif |
extern char **environ; |
static char * const * inherited_environ; |
static const char **allowed_programs; |
static size_t allowed_programs_count; |
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; |
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; |
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; |
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) |
{ |
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)) { |
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 bool is_allowed_program(const char *program) { |
size_t i; |
if (!allowed_programs_count) |
return true; |
for (i = 0; i < allowed_programs_count; i++) { |
if (!strcmp(allowed_programs[i], program)) |
return true; |
} |
return false; |
} |
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; |
char *p; |
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: |
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); |
if (!is_allowed_program(filename)) |
cgi_error("403 Forbidden", "The given script is not allowed to execute", filename); |
p = getenv("FCGI_CHDIR"); |
if (p == 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 = '/'; |
} else if (strcmp(p, "-") != 0) { |
if (chdir(p) < 0) { |
cgi_error("403 Forbidden", "Cannot chdir to FCGI_CHDIR directory", p); |
} |
} |
execl(filename, filename, (void *)NULL); |
cgi_error("502 Bad Gateway", "Cannot execute script", filename); |
default: |
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 volatile sig_atomic_t sigint_received ; |
static void sigint_handler(int __attribute__((__unused__))dummy) |
{ |
sigint_received = 1; |
FCGX_ShutdownPending(); // Or we could send SIGUSR1 |
} |
static void fcgiwrap_main(void) |
{ |
struct sigaction a; |
signal(SIGCHLD, SIG_IGN); |
signal(SIGPIPE, SIG_IGN); |
// Use sigaction for SIGINT so we can avoid SA_RESTART and actually react |
a.sa_handler = sigint_handler; |
a.sa_flags = 0; |
sigemptyset( &a.sa_mask ); |
sigaction( SIGINT, &a, NULL ); |
sigaction( SIGTERM, &a, NULL ); |
inherited_environ = environ; |
while (FCGI_Accept() >= 0 && !sigint_received) { |
handle_fcgi_request(); |
} |
} |
static volatile sig_atomic_t nrunning; |
static void sigchld_handler(int dummy) |
{ |
int status; |
while ((dummy = waitpid(-1, &status, WNOHANG)) > 0) { |
if (nrunning > 0) |
nrunning--; |
} |
} |
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; |
} |
if (listen_on_fd(fd) < 0) { |
return -1; |
} |
return fd; |
} |
int main(int argc, char **argv) |
{ |
int nchildren = 1; |
char *socket_url = NULL; |
int fd = 0; |
int c; |
while ((c = getopt(argc, argv, "c:hfs:p:")) != -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" |
" -p <path>\t\tRestrict execution to this script. (repeated options will be merged)\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 'p': |
allowed_programs = realloc(allowed_programs, (allowed_programs_count + 1) * sizeof (char *)); |
if (!allowed_programs) |
abort(); |
allowed_programs[allowed_programs_count++] = strdup(optarg); |
break; |
case '?': |
if (optopt == 'c' || optopt == 's' || optopt == 'p') |
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) { |
if (listen_on_fd(SD_LISTEN_FDS_START) < 0) { |
return 1; |
} |
} else |
#endif |
if (socket_url) { |
fd = setup_socket(socket_url); |
if (fd < 0) { |
return 1; |
} |
} |
prefork(nchildren); |
fcgiwrap_main(); |
if (fd) { |
const char *p = socket_url; |
close(fd); |
if (socket_url) { |
if (!strncmp(p, "unix:", sizeof("unix:") - 1)) { |
p += sizeof("unix:") - 1; |
unlink(p); |
} |
} |
} |
return 0; |
}