callcgi: Don't mass-close child FDs.
[ashd.git] / src / callcgi.c
index 8798c17..3a28880 100644 (file)
 #include <errno.h>
 #include <ctype.h>
 #include <signal.h>
+#include <sys/poll.h>
+#include <sys/wait.h>
 
 #ifdef HAVE_CONFIG_H
 #include <config.h>
 #endif
 #include <utils.h>
 #include <log.h>
+#include <req.h>
 
 static char **environ;
 
-static void passdata(FILE *in, FILE *out)
+static int passdata(FILE *in, FILE *out)
 {
     int ret;
-    char *buf;
+    char buf[65536];
+    struct pollfd pfds[2];
     
-    buf = smalloc(65536);
     while(!feof(in)) {
-       ret = fread(buf, 1, 65536, in);
-       if(ferror(in)) {
-           flog(LOG_ERR, "sendfile: could not read input: %s", strerror(errno));
-           break;
+       memset(pfds, 0, sizeof(struct pollfd) * 2);
+       pfds[0].fd = fileno(in);
+       pfds[0].events = POLLIN;
+       pfds[1].fd = fileno(out);
+       pfds[1].events = POLLHUP;
+       ret = poll(pfds, 2, -1);
+       if(ret < 0) {
+           if(errno != EINTR) {
+               flog(LOG_ERR, "callcgi: error in poll: %s", strerror(errno));
+               return(1);
+           }
        }
-       if(fwrite(buf, 1, ret, out) != ret) {
-           flog(LOG_ERR, "sendfile: could not write output: %s", strerror(errno));
-           break;
+       if(ret > 0) {
+           if(pfds[0].revents & (POLLIN | POLLERR | POLLHUP)) {
+               ret = fread(buf, 1, 65536, in);
+               if(ferror(in)) {
+                   flog(LOG_ERR, "callcgi: could not read input: %s", strerror(errno));
+                   return(1);
+               }
+               if(fwrite(buf, 1, ret, out) != ret) {
+                   flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
+                   return(1);
+               }
+           }
+           if(pfds[1].revents & POLLHUP)
+               return(1);
        }
     }
-    free(buf);
+    return(0);
+}
+
+static char *absolutify(char *file)
+{
+    char cwd[1024];
+    
+    if(*file != '/') {
+       getcwd(cwd, sizeof(cwd));
+       return(sprintf2("%s/%s", cwd, file));
+    }
+    return(sstrdup(file));
 }
 
-static void forkchild(int inpath, char *prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
+static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
 {
-    int i;
-    char *qp, **env;
+    char *qp, **env, *name;
     int inp[2], outp[2];
     pid_t pid;
+    char *pi;
 
     pipe(inp);
     pipe(outp);
@@ -66,31 +98,61 @@ static void forkchild(int inpath, char *prog, char *file, char *method, char *ur
        exit(1);
     }
     if(pid == 0) {
-       close(inp[1]);
-       close(outp[0]);
        dup2(inp[0], 0);
        dup2(outp[1], 1);
-       for(i = 3; i < FD_SETSIZE; i++)
-           close(i);
+       close(inp[0]);
+       close(inp[1]);
+       close(outp[0]);
+       close(outp[1]);
        if((qp = strchr(url, '?')) != NULL)
            *(qp++) = 0;
-       /*
-        * XXX: Currently missing:
-        *  SERVER_NAME (Partially)
-        *  SERVER_PORT
-        */
        putenv(sprintf2("SERVER_SOFTWARE=ashd/%s", VERSION));
        putenv("GATEWAY_INTERFACE=CGI/1.1");
        if(getenv("HTTP_VERSION"))
-           putenv(sprintf2("SERVER_PROTOCOL=HTTP/%s", getenv("HTTP_VERSION")));
+           putenv(sprintf2("SERVER_PROTOCOL=%s", getenv("HTTP_VERSION")));
        putenv(sprintf2("REQUEST_METHOD=%s", method));
-       putenv(sprintf2("PATH_INFO=%s", rest));
-       putenv(sprintf2("SCRIPT_NAME=%s", url));
+       name = url;
+       /* XXX: This is an ugly hack (I think), but though I can think
+        * of several alternatives, none seem to be better. */
+       if(*rest && (strlen(url) >= strlen(rest)) &&
+          !strcmp(rest, url + strlen(url) - strlen(rest))) {
+           name = sprintf2("%.*s", (int)(strlen(url) - strlen(rest)), url);
+       }
+       if((pi = unquoteurl(rest)) == NULL)
+           pi = rest;
+       if(!strcmp(name, "/")) {
+           /*
+            * Normal CGI behavior appears to be to always let
+            * PATH_INFO begin with a slash and never let SCRIPT_NAME
+            * end with one. That conflicts, however, with some
+            * behaviors, such as "mounting" CGI applications on a
+            * directory element of the URI space -- a handler
+            * responding to "/foo/" would not be able to tell that it
+            * is not called "/foo", which makes a large difference,
+            * not least in relation to URI reconstruction and
+            * redirections. A common practical case is CGI-handled
+            * index files in directories. Therefore, this only
+            * handles the nonconditional case of the root directory
+            * and leaves other decisions to the previous handler
+            * handing over the request to callcgi. It is unclear if
+            * there is a better way to handle the problem.
+            */
+           name[0] = 0;
+           pi = sprintf2("/%s", pi);
+       }
+       putenv(sprintf2("PATH_INFO=%s", pi));
+       putenv(sprintf2("SCRIPT_NAME=%s", name));
        putenv(sprintf2("QUERY_STRING=%s", qp?qp:""));
        if(getenv("REQ_HOST"))
            putenv(sprintf2("SERVER_NAME=%s", getenv("REQ_HOST")));
+       if(getenv("REQ_X_ASH_SERVER_PORT"))
+           putenv(sprintf2("SERVER_PORT=%s", getenv("REQ_X_ASH_SERVER_PORT")));
+       if(getenv("REQ_X_ASH_PROTOCOL") && !strcmp(getenv("REQ_X_ASH_PROTOCOL"), "https"))
+           putenv("HTTPS=on");
        if(getenv("REQ_X_ASH_ADDRESS"))
            putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
+       if(getenv("REQ_X_ASH_REMOTE_USER"))
+           putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
        if(getenv("REQ_CONTENT_TYPE"))
            putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
        if(getenv("REQ_CONTENT_LENGTH"))
@@ -103,7 +165,7 @@ static void forkchild(int inpath, char *prog, char *file, char *method, char *ur
         * This is (understandably) missing from the CGI
         * specification, but PHP seems to require it.
         */
-       putenv(sprintf2("SCRIPT_FILENAME=%s", file));
+       putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
        if(inpath)
            execlp(prog, prog, file, NULL);
        else
@@ -114,6 +176,7 @@ static void forkchild(int inpath, char *prog, char *file, char *method, char *ur
     close(outp[1]);
     *infd = inp[1];
     *outfd = outp[0];
+    return(pid);
 }
 
 static void trim(struct charbuf *buf)
@@ -125,7 +188,7 @@ static void trim(struct charbuf *buf)
     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
 }
 
-static char **parseheaders(FILE *s)
+static char **parsecgiheaders(FILE *s)
 {
     int c, state;
     struct charvbuf hbuf;
@@ -186,6 +249,46 @@ fail:
     return(NULL);
 }
 
+static char *defstatus(int code)
+{
+    if(code == 200)
+       return("OK");
+    else if(code == 201)
+       return("Created");
+    else if(code == 202)
+       return("Accepted");
+    else if(code == 204)
+       return("No Content");
+    else if(code == 300)
+       return("Multiple Choices");
+    else if(code == 301)
+       return("Moved Permanently");
+    else if(code == 302)
+       return("Found");
+    else if(code == 303)
+       return("See Other");
+    else if(code == 304)
+       return("Not Modified");
+    else if(code == 307)
+       return("Moved Temporarily");
+    else if(code == 400)
+       return("Bad Request");
+    else if(code == 401)
+       return("Unauthorized");
+    else if(code == 403)
+       return("Forbidden");
+    else if(code == 404)
+       return("Not Found");
+    else if(code == 500)
+       return("Internal Server Error");
+    else if(code == 501)
+       return("Not Implemented");
+    else if(code == 503)
+       return("Service Unavailable");
+    else
+       return("Unknown status");
+}
+
 static void sendstatus(char **headers, FILE *out)
 {
     char **hp;
@@ -206,11 +309,14 @@ static void sendstatus(char **headers, FILE *out)
        }
     }
     if(status) {
-       fprintf(out, "HTTP/1.1 %s\r\n", status);
+       if(strchr(status, ' '))
+           fprintf(out, "HTTP/1.1 %s\n", status);
+       else
+           fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
     } else if(location) {
-       fprintf(out, "HTTP/1.1 303 See Other\r\n");
+       fprintf(out, "HTTP/1.1 303 See Other\n");
     } else {
-       fprintf(out, "HTTP/1.1 200 OK\r\n");
+       fprintf(out, "HTTP/1.1 200 OK\n");
     }
 }
 
@@ -218,32 +324,38 @@ static void sendheaders(char **headers, FILE *out)
 {
     while(*headers) {
        if(**headers)
-           fprintf(out, "%s: %s\r\n", headers[0], headers[1]);
+           fprintf(out, "%s: %s\n", headers[0], headers[1]);
        headers += 2;
     }
 }
 
 static void usage(void)
 {
-    flog(LOG_ERR, "usage: callcgi [-p PROGRAM] METHOD URL REST");
+    flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] METHOD URL REST");
 }
 
 int main(int argc, char **argv, char **envp)
 {
     int c;
-    char *file, *prog;
-    int inpath;
+    char *file, *prog, *sp;
+    int inpath, cd;
     int infd, outfd;
     FILE *in, *out;
     char **headers;
+    pid_t child;
+    int estat;
     
     environ = envp;
     signal(SIGPIPE, SIG_IGN);
     
     prog = NULL;
     inpath = 0;
-    while((c = getopt(argc, argv, "p:")) >= 0) {
+    cd = 0;
+    while((c = getopt(argc, argv, "cp:")) >= 0) {
        switch(c) {
+       case 'c':
+           cd = 1;
+           break;
        case 'p':
            prog = optarg;
            inpath = 1;
@@ -262,20 +374,45 @@ int main(int argc, char **argv, char **envp)
        flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
        exit(1);
     }
+    
+    if(cd) {
+       /* This behavior is encouraged by the CGI specification (RFC 3875, 7.2),
+        * but not strictly required, and I get the feeling it might break some
+        * relative paths here or there, so it's not the default for now. */
+       if((sp = strrchr(file, '/')) != NULL) {
+           *sp = 0;
+           if(chdir(file)) {
+               *sp = '/';
+           } else {
+               file = sp + 1;
+           }
+       }
+    }
+    
     if(prog == NULL)
        prog = file;
-    forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
+    child = forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
     in = fdopen(infd, "w");
-    passdata(stdin, in);
+    passdata(stdin, in);       /* Ignore errors, perhaps? */
     fclose(in);
     out = fdopen(outfd, "r");
-    if((headers = parseheaders(out)) == NULL) {
+    if((headers = parsecgiheaders(out)) == NULL) {
        flog(LOG_WARNING, "CGI handler returned invalid headers");
        exit(1);
     }
     sendstatus(headers, stdout);
     sendheaders(headers, stdout);
-    printf("\r\n");
-    passdata(out, stdout);
-    return(0);
+    printf("\n");
+    if(passdata(out, stdout))
+       kill(child, SIGINT);
+    if(waitpid(child, &estat, 0) == child) {
+       if(WCOREDUMP(estat))
+           flog(LOG_WARNING, "CGI handler `%s' dumped core", prog);
+       if(WIFEXITED(estat) && !WEXITSTATUS(estat))
+           return(0);
+       else
+           return(1);
+    }
+    flog(LOG_WARNING, "could not wait for CGI handler: %s", strerror(errno));
+    return(1);
 }