doc: Documented htpipe.
[ashd.git] / src / callcgi.c
index 14430aa..2b1c9e6 100644 (file)
@@ -24,6 +24,7 @@
 #include <ctype.h>
 #include <signal.h>
 #include <sys/poll.h>
+#include <sys/wait.h>
 
 #ifdef HAVE_CONFIG_H
 #include <config.h>
@@ -61,7 +62,8 @@ static int passdata(FILE *in, FILE *out)
                    return(1);
                }
                if(fwrite(buf, 1, ret, out) != ret) {
-                   flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
+                   if(errno != EPIPE)
+                       flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
                    return(1);
                }
            }
@@ -83,13 +85,13 @@ static char *absolutify(char *file)
     return(sstrdup(file));
 }
 
-static pid_t 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, *name;
     int inp[2], outp[2];
     pid_t pid;
-    char *unqr;
+    char *pi;
+    int (*execfun)(const char *, char *const []);
 
     pipe(inp);
     pipe(outp);
@@ -98,12 +100,12 @@ static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *u
        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;
        putenv(sprintf2("SERVER_SOFTWARE=ashd/%s", VERSION));
@@ -111,8 +113,6 @@ static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *u
        if(getenv("HTTP_VERSION"))
            putenv(sprintf2("SERVER_PROTOCOL=%s", getenv("HTTP_VERSION")));
        putenv(sprintf2("REQUEST_METHOD=%s", method));
-       unqr = unquoteurl(rest);
-       putenv(sprintf2("PATH_INFO=%s", unqr?unqr:rest));
        name = url;
        /* XXX: This is an ugly hack (I think), but though I can think
         * of several alternatives, none seem to be better. */
@@ -120,16 +120,43 @@ static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *u
           !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_ADDRESS"))
+           putenv(sprintf2("SERVER_ADDR=%s", getenv("REQ_X_ASH_SERVER_ADDRESS")));
        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_PORT"))
+           putenv(sprintf2("REMOTE_PORT=%s", getenv("REQ_X_ASH_PORT")));
        if(getenv("REQ_X_ASH_REMOTE_USER"))
            putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
        if(getenv("REQ_CONTENT_TYPE"))
@@ -144,11 +171,10 @@ static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *u
         * This is (understandably) missing from the CGI
         * specification, but PHP seems to require it.
         */
-       putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
-       if(inpath)
-           execlp(prog, prog, file, NULL);
-       else
-           execl(prog, prog, file, NULL);
+       execfun = inpath?execvp:execv;
+       if(file != NULL)
+           putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
+       execfun(prog[0], prog);
        exit(127);
     }
     close(inp[0]);
@@ -310,34 +336,58 @@ static void sendheaders(char **headers, FILE *out)
 
 static void usage(void)
 {
-    flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] METHOD URL REST");
+    flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] [-P PROGRAM ARGS... ;] METHOD URL REST");
 }
 
 int main(int argc, char **argv, char **envp)
 {
     int c;
-    char *file, *prog, *sp;
-    int inpath, cd;
+    char *file, *sp;
+    struct charvbuf prog;
+    int inpath, addfile, cd;
     int infd, outfd;
     FILE *in, *out;
     char **headers;
     pid_t child;
+    int estat;
     
     environ = envp;
     signal(SIGPIPE, SIG_IGN);
     
-    prog = NULL;
+    bufinit(prog);
     inpath = 0;
+    addfile = 1;
     cd = 0;
-    while((c = getopt(argc, argv, "cp:")) >= 0) {
+    while((c = getopt(argc, argv, "cp:P:")) >= 0) {
        switch(c) {
        case 'c':
            cd = 1;
            break;
        case 'p':
-           prog = optarg;
+           bufadd(prog, optarg);
            inpath = 1;
            break;
+       case 'P':
+           prog.d = 0;
+           bufadd(prog, optarg);
+           while(1) {
+               if(optind >= argc) {
+                   flog(LOG_ERR, "callcgi: unterminated argument list for -P");
+                   exit(1);
+               }
+               if(!strcmp(argv[optind], ";")) {
+                   optind++;
+                   break;
+               }
+               bufadd(prog, argv[optind++]);
+           }
+           if(prog.d == 0) {
+               flog(LOG_ERR, "callcgi: -P option needs at least a program name");
+               exit(1);
+           }
+           inpath = 1;
+           addfile = 0;
+           break;
        default:
            usage();
            exit(1);
@@ -348,7 +398,7 @@ int main(int argc, char **argv, char **envp)
        usage();
        exit(1);
     }
-    if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
+    if(((file = getenv("REQ_X_ASH_FILE")) == NULL) && (prog.d == 0)) {
        flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
        exit(1);
     }
@@ -367,9 +417,12 @@ int main(int argc, char **argv, char **envp)
        }
     }
     
-    if(prog == NULL)
-       prog = file;
-    child = forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
+    if(prog.d == 0)
+       bufadd(prog, file);
+    if(addfile && (file != NULL))
+       bufadd(prog, file);
+    bufadd(prog, NULL);
+    child = forkchild(inpath, prog.b, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
     in = fdopen(infd, "w");
     passdata(stdin, in);       /* Ignore errors, perhaps? */
     fclose(in);
@@ -383,5 +436,15 @@ int main(int argc, char **argv, char **envp)
     printf("\n");
     if(passdata(out, stdout))
        kill(child, SIGINT);
-    return(0);
+    fclose(out);
+    if(waitpid(child, &estat, 0) == child) {
+       if(WCOREDUMP(estat))
+           flog(LOG_WARNING, "CGI handler `%s' dumped core", prog.b[0]);
+       if(WIFEXITED(estat) && !WEXITSTATUS(estat))
+           return(0);
+       else
+           return(1);
+    }
+    flog(LOG_WARNING, "could not wait for CGI handler: %s", strerror(errno));
+    return(1);
 }