/* * Copyright (c) 2007-2009 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_SYSTEMD #include #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 error_403(const char *reason, const char *filename) { puts("Status: 403 Forbidden\nContent-type: text/plain\n\n403"); 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]); signal(SIGCHLD, SIG_DFL); signal(SIGPIPE, SIG_DFL); filename = get_cgi_filename(); inherit_environment(); if (!filename) error_403("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) error_403("Script name must be a fully qualified path", filename); *last_slash = 0; if (chdir(filename) < 0) error_403("Cannot chdir to script directory", filename); *last_slash = '/'; execl(filename, filename, (void *)NULL); puts("Status: 502 Bad Gateway\nContent-type: text/plain\n\n502"); exit(99); 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" " -c \t\tNumber of processes to prefork\n" " -s \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: \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; }