Added a callfcgi program, probably a bit buggy.
authorFredrik Tolf <fredrik@dolda2000.com>
Sat, 30 Oct 2010 08:33:01 +0000 (10:33 +0200)
committerFredrik Tolf <fredrik@dolda2000.com>
Sat, 30 Oct 2010 08:33:01 +0000 (10:33 +0200)
src/.gitignore
src/Makefile.am
src/callfcgi.c [new file with mode: 0644]

index 3bf4717..a277c39 100644 (file)
@@ -8,3 +8,4 @@
 /callscgi
 /accesslog
 /htextauth
+/callfcgi
index ed1f0ce..3d2c292 100644 (file)
@@ -1,7 +1,7 @@
 SUBDIRS = dirplex
 
 bin_PROGRAMS = htparser sendfile callcgi patplex userplex htls \
-               callscgi accesslog htextauth
+               callscgi accesslog htextauth callfcgi
 noinst_PROGRAMS = debugsink
 
 htparser_SOURCES = htparser.c htparser.h plaintcp.c ssl-gnutls.c
diff --git a/src/callfcgi.c b/src/callfcgi.c
new file mode 100644 (file)
index 0000000..a394a42
--- /dev/null
@@ -0,0 +1,845 @@
+/*
+    ashd - A Sane HTTP Daemon
+    Copyright (C) 2008  Fredrik Tolf <fredrik@dolda2000.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ * XXX: This program is mostly copied from callscgi. It may be
+ * reasonable to unify some of their shared code in a source file.
+ */
+
+/*
+ * XXX: All the various ways to start a child process makes this
+ * program quite ugly at the moment. It is unclear whether it is
+ * meaningfully possible to unify them better than they currently are.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <sys/signal.h>
+#include <errno.h>
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <utils.h>
+#include <req.h>
+#include <log.h>
+#include <mt.h>
+#include <mtio.h>
+
+#define FCGI_BEGIN_REQUEST 1
+#define FCGI_ABORT_REQUEST 2
+#define FCGI_END_REQUEST 3
+#define FCGI_PARAMS 4
+#define FCGI_STDIN 5
+#define FCGI_STDOUT 6
+#define FCGI_STDERR 7
+
+static char **progspec;
+static char *sockid, *unspec, *inspec;
+static int nolisten;
+static struct sockaddr *curaddr;
+static size_t caddrlen;
+static int cafamily, isanon;
+static pid_t child;
+
+static struct addrinfo *resolv(int flags)
+{
+    int ret;
+    struct addrinfo *ai, h;
+    char *name, *srv, *p;
+    
+    if((p = strchr(inspec, ':')) != NULL) {
+       name = smalloc(p - inspec + 1);
+       memcpy(name, inspec, p - inspec);
+       name[p - inspec] = 0;
+       srv = p + 1;
+    } else {
+       name = sstrdup("localhost");
+       srv = inspec;
+    }
+    memset(&h, 0, sizeof(h));
+    h.ai_family = AF_UNSPEC;
+    h.ai_socktype = SOCK_STREAM;
+    h.ai_flags = flags;
+    ret = getaddrinfo(name, srv, &h, &ai);
+    free(name);
+    if(ret != 0) {
+       flog(LOG_ERR, "could not resolve TCP specification `%s': %s", inspec, gai_strerror(ret));
+       exit(1);
+    }
+    return(ai);
+}
+
+static char *mksockid(char *sockid)
+{
+    char *home;
+    
+    home = getenv("HOME");
+    if(home && !access(sprintf3("%s/.ashd/sockets/", home), X_OK))
+       return(sprintf3("%s/.ashd/sockets/fcgi-p-%s", home, sockid));
+    return(sprintf3("/tmp/fcgi-%i-%s", getuid(), sockid));
+}
+
+static char *mkanonid(void)
+{
+    char *home;
+    char *tmpl;
+    int fd;
+    
+    home = getenv("HOME");
+    if(home && !access(sprintf3("%s/.ashd/sockets/", home), X_OK))
+       tmpl = sprintf2("%s/.ashd/sockets/fcgi-a-XXXXXX", home);
+    else
+       tmpl = sprintf2("/tmp/fcgi-a-%i-XXXXXX", getuid());
+    if((fd = mkstemp(tmpl)) < 0) {
+       flog(LOG_ERR, "could not create anonymous socket `%s': %s", tmpl, strerror(errno));
+       exit(1);
+    }
+    close(fd);
+    unlink(tmpl);
+    return(tmpl);
+}
+
+static void startlisten(void)
+{
+    int i, fd;
+    struct addrinfo *ai, *cai;
+    char *unpath;
+    struct sockaddr_un unm;
+    char *aname;
+    
+    isanon = 0;
+    if(inspec != NULL) {
+       fd = -1;
+       for(cai = ai = resolv(AI_PASSIVE); cai != NULL; cai = cai->ai_next) {
+           if((fd = socket(cai->ai_family, cai->ai_socktype, cai->ai_protocol)) < 0)
+               continue;
+           if(bind(fd, cai->ai_addr, cai->ai_addrlen)) {
+               close(fd);
+               fd = -1;
+               continue;
+           }
+           if(listen(fd, 128)) {
+               close(fd);
+               fd = -1;
+               continue;
+           }
+           break;
+       }
+       freeaddrinfo(ai);
+       if(fd < 0) {
+           flog(LOG_ERR, "could not bind to specified TCP address: %s", strerror(errno));
+           exit(1);
+       }
+    } else if((unspec != NULL) || (sockid != NULL)) {
+       if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+           flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
+           exit(1);
+       }
+       if(unspec != NULL)
+           unpath = unspec;
+       else
+           unpath = mksockid(sockid);
+       unlink(unpath);
+       unm.sun_family = AF_UNIX;
+       strcpy(unm.sun_path, unpath);
+       if(bind(fd, (struct sockaddr *)&unm, sizeof(unm))) {
+           flog(LOG_ERR, "could not bind Unix socket to `%s': %s", unspec, strerror(errno));
+           exit(1);
+       }
+       if(listen(fd, 128)) {
+           flog(LOG_ERR, "listen: %s", strerror(errno));
+           exit(1);
+       }
+    } else {
+       if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+           flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
+           exit(1);
+       }
+       memset(&unm, 0, sizeof(unm));
+       aname = mkanonid();
+       unm.sun_family = AF_UNIX;
+       strcpy(unm.sun_path, aname);
+       free(aname);
+       if(bind(fd, (struct sockaddr *)&unm, sizeof(unm))) {
+           flog(LOG_ERR, "could not bind Unix socket to `%s': %s", unspec, strerror(errno));
+           exit(1);
+       }
+       if(listen(fd, 128)) {
+           flog(LOG_ERR, "listen: %s", strerror(errno));
+           exit(1);
+       }
+       
+       curaddr = smalloc(caddrlen = sizeof(unm));
+       memcpy(curaddr, &unm, sizeof(unm));
+       cafamily = AF_UNIX;
+       isanon = 1;
+    }
+    if((child = fork()) < 0) {
+       flog(LOG_ERR, "could not fork: %s", strerror(errno));
+       exit(1);
+    }
+    if(child == 0) {
+       dup2(fd, 0);
+       for(i = 3; i < FD_SETSIZE; i++)
+           close(i);
+       execvp(*progspec, progspec);
+       flog(LOG_ERR, "callfcgi: %s: %s", *progspec, strerror(errno));
+       exit(127);
+    }
+    close(fd);
+}
+
+static void startnolisten(void)
+{
+    int i, fd;
+    
+    if((child = fork()) < 0) {
+       flog(LOG_ERR, "could not fork: %s", strerror(errno));
+       exit(1);
+    }
+    if(child == 0) {
+       for(i = 3; i < FD_SETSIZE; i++)
+           close(i);
+       if((fd = open("/dev/null", O_RDONLY)) < 0) {
+           flog(LOG_ERR, "/dev/null: %s", strerror(errno));
+           exit(127);
+       }
+       dup2(fd, 0);
+       close(fd);
+       execvp(*progspec, progspec);
+       flog(LOG_ERR, "callfcgi: %s: %s", *progspec, strerror(errno));
+       exit(127);
+    }
+}
+
+static int sconnect(void)
+{
+    int fd;
+    int err;
+    socklen_t errlen;
+
+    fd = socket(cafamily, SOCK_STREAM, 0);
+    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
+    while(1) {
+       if(connect(fd, curaddr, caddrlen)) {
+           if(errno == EINPROGRESS) {
+               block(fd, EV_WRITE, 30);
+               errlen = sizeof(err);
+               if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) || ((errno = err) != 0)) {
+                   close(fd);
+                   return(-1);
+               }
+               return(fd);
+           }
+           close(fd);
+           return(-1);
+       }
+       return(fd);
+    }
+}
+
+static int econnect(void)
+{
+    int fd;
+    struct addrinfo *ai, *cai;
+    int tries;
+    char *unpath;
+    struct sockaddr_un unm;
+    
+    tries = 0;
+retry:
+    if(inspec != NULL) {
+       fd = -1;
+       for(cai = ai = resolv(0); cai != NULL; cai = cai->ai_next) {
+           if((fd = socket(cai->ai_family, cai->ai_socktype, cai->ai_protocol)) < 0)
+               continue;
+           if(connect(fd, cai->ai_addr, cai->ai_addrlen)) {
+               close(fd);
+               fd = -1;
+               continue;
+           }
+           break;
+       }
+       if(fd < 0) {
+           if(tries++ < nolisten) {
+               sleep(1);
+               goto retry;
+           }
+           flog(LOG_ERR, "could not connect to specified TCP address: %s", strerror(errno));
+           exit(1);
+       }
+       curaddr = smalloc(caddrlen = cai->ai_addrlen);
+       memcpy(curaddr, cai->ai_addr, caddrlen);
+       cafamily = cai->ai_family;
+       isanon = 0;
+       freeaddrinfo(ai);
+       return(fd);
+    } else if((unspec != NULL) || (sockid != NULL)) {
+       if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+           flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
+           exit(1);
+       }
+       if(unspec != NULL)
+           unpath = unspec;
+       else
+           unpath = mksockid(sockid);
+       unlink(unpath);
+       unm.sun_family = AF_UNIX;
+       strcpy(unm.sun_path, unpath);
+       if(connect(fd, (struct sockaddr *)&unm, sizeof(unm))) {
+           close(fd);
+           if(tries++ < nolisten) {
+               sleep(1);
+               goto retry;
+           }
+           flog(LOG_ERR, "could not connect to Unix socket `%s': %s", unspec, strerror(errno));
+           exit(1);
+       }
+       curaddr = smalloc(caddrlen = sizeof(unm));
+       memcpy(curaddr, &unm, sizeof(unm));
+       cafamily = AF_UNIX;
+       isanon = 0;
+       return(fd);
+    } else {
+       flog(LOG_ERR, "callfcgi: cannot use an anonymous socket without a program to start");
+       exit(1);
+    }
+}
+
+static int startconn(void)
+{
+    if(*progspec) {
+       if(nolisten == 0)
+           startlisten();
+       else
+           startnolisten();
+    }
+    if(curaddr != NULL)
+       return(sconnect());
+    return(econnect());
+}
+
+static void killcuraddr(void)
+{
+    if(curaddr == NULL)
+       return;
+    if(isanon) {
+       unlink(((struct sockaddr_un *)curaddr)->sun_path);
+       if(child > 0)
+           kill(child, SIGTERM);
+    }
+    free(curaddr);
+    curaddr = NULL;
+}
+
+static int reconn(void)
+{
+    int fd;
+    
+    if(curaddr != NULL) {
+       if((fd = sconnect()) >= 0)
+           return(fd);
+       killcuraddr();
+    }
+    return(startconn());
+}
+
+static off_t passdata(FILE *in, FILE *out)
+{
+    size_t read;
+    off_t total;
+    char buf[8192];
+    
+    total = 0;
+    while(!feof(in)) {
+       read = fread(buf, 1, sizeof(buf), in);
+       if(ferror(in))
+           return(-1);
+       if(fwrite(buf, 1, read, out) != read)
+           return(-1);
+       total += read;
+    }
+    return(total);
+}
+
+static void bufcatkv(struct charbuf *dst, char *key, char *val)
+{
+    size_t kl, vl;
+    
+    if((kl = strlen(key)) < 128) {
+       bufadd(*dst, kl);
+    } else {
+       bufadd(*dst, ((kl & 0x7f000000) >> 24) | 0x80);
+       bufadd(*dst, (kl & 0x00ff0000) >> 16);
+       bufadd(*dst, (kl & 0x0000ff00) >> 8);
+       bufadd(*dst, kl & 0x000000ff);
+    }
+    if((vl = strlen(val)) < 128) {
+       bufadd(*dst, vl);
+    } else {
+       bufadd(*dst, ((vl & 0x7f000000) >> 24) | 0x80);
+       bufadd(*dst, (vl & 0x00ff0000) >> 16);
+       bufadd(*dst, (vl & 0x0000ff00) >> 8);
+       bufadd(*dst, vl & 0x000000ff);
+    }
+    bufcat(*dst, key, kl);
+    bufcat(*dst, val, vl);
+}
+
+static void bufaddenv(struct charbuf *dst, char *name, char *fmt, ...)
+{
+    va_list args;
+    char *val = NULL;
+    
+    va_start(args, fmt);
+    val = vsprintf2(fmt, args);
+    va_end(args);
+    bufcatkv(dst, name, val);
+    free(val);
+}
+
+static char *absolutify(char *file)
+{
+    static int inited = 0;
+    static char cwd[1024];
+    
+    if(*file != '/') {
+       if(!inited) {
+           getcwd(cwd, sizeof(cwd));
+           inited = 1;
+       }
+       return(sprintf2("%s/%s", cwd, file));
+    }
+    return(sstrdup(file));
+}
+
+/* Mostly copied from callcgi. */
+static void mkcgienv(struct hthead *req, struct charbuf *dst)
+{
+    int i;
+    char *url, *qp, *h, *p;
+    
+    bufaddenv(dst, "SERVER_SOFTWARE", "ashd/%s", VERSION);
+    bufaddenv(dst, "GATEWAY_INTERFACE", "CGI/1.1");
+    bufaddenv(dst, "SERVER_PROTOCOL", "%s", req->ver);
+    bufaddenv(dst, "REQUEST_METHOD", "%s", req->method);
+    bufaddenv(dst, "REQUEST_URI", "%s", req->url);
+    bufaddenv(dst, "PATH_INFO", req->rest);
+    url = sstrdup(req->url);
+    if((qp = strchr(url, '?')) != NULL)
+       *(qp++) = 0;
+    /* XXX: This is an ugly hack (I think), but though I can think of
+     * several alternatives, none seem to be better. */
+    if(*req->rest && (strlen(url) >= strlen(req->rest)) &&
+       !strcmp(req->rest, url + strlen(url) - strlen(req->rest))) {
+       bufaddenv(dst, "SCRIPT_NAME", "%.*s", (int)(strlen(url) - strlen(req->rest)), url);
+    } else {
+       bufaddenv(dst, "SCRIPT_NAME", "%s", url);
+    }
+    bufaddenv(dst, "QUERY_STRING", "%s", qp?qp:"");
+    if((h = getheader(req, "Host")) != NULL)
+       bufaddenv(dst, "SERVER_NAME", "%s", h);
+    if((h = getheader(req, "X-Ash-Server-Port")) != NULL)
+       bufaddenv(dst, "SERVER_PORT", "%s", h);
+    if(((h = getheader(req, "X-Ash-Protocol")) != NULL) && !strcmp(h, "https"))
+       bufaddenv(dst, "HTTPS", "on");
+    if((h = getheader(req, "X-Ash-Address")) != NULL)
+       bufaddenv(dst, "REMOTE_ADDR", "%s", h);
+    if((h = getheader(req, "Content-Type")) != NULL)
+       bufaddenv(dst, "CONTENT_TYPE", "%s", h);
+    if((h = getheader(req, "Content-Length")) != NULL)
+       bufaddenv(dst, "CONTENT_LENGTH", "%s", h);
+    else
+       bufaddenv(dst, "CONTENT_LENGTH", "0");
+    if((h = getheader(req, "X-Ash-File")) != NULL)
+       bufaddenv(dst, "SCRIPT_FILENAME", "%s", absolutify(h));
+    for(i = 0; i < req->noheaders; i++) {
+       h = sprintf2("HTTP_%s", req->headers[i][0]);
+       for(p = h; *p; p++) {
+           if(isalnum(*p))
+               *p = toupper(*p);
+           else
+               *p = '_';
+       }
+       bufcatkv(dst, h, req->headers[i][1]);
+       free(h);
+    }
+}
+
+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 struct hthead *parseresp(FILE *in)
+{
+    struct hthead *resp;
+    char *st, *p;
+    
+    omalloc(resp);
+    resp->ver = sstrdup("HTTP/1.1");
+    if(parseheaders(resp, in)) {
+       freehthead(resp);
+       return(NULL);
+    }
+    if((st = getheader(resp, "Status")) != NULL) {
+       if((p = strchr(st, ' ')) != NULL) {
+           *(p++) = 0;
+           resp->code = atoi(st);
+           resp->msg = sstrdup(p);
+       } else {
+           resp->code = atoi(st);
+           resp->msg = sstrdup(defstatus(resp->code));
+       }
+       headrmheader(resp, "Status");
+    } else if(getheader(resp, "Location")) {
+       resp->code = 303;
+       resp->msg = sstrdup("See Other");
+    } else {
+       resp->code = 200;
+       resp->msg = sstrdup("OK");
+    }
+    return(resp);
+}
+
+#define fputc2(b, f) if(fputc((b), (f)) == EOF) return(-1);
+
+static int sendrec(FILE *out, int type, int rid, char *data, size_t dlen)
+{
+    off_t off;
+    size_t cl;
+    int p;
+    
+    off = 0;
+    do {
+       cl = min(dlen - off, 65535);
+       p = (8 - (cl % 8)) % 8;
+       fputc2(1, out);
+       fputc2(type, out);
+       fputc2((rid & 0xff00) >> 8, out);
+       fputc2(rid & 0x00ff, out);
+       fputc2((cl & 0xff00) >> 8, out);
+       fputc2(cl & 0x00ff, out);
+       fputc2(p, out);
+       fputc2(0, out);
+       if(fwrite(data + off, 1, cl, out) != cl)
+           return(-1);
+       for(; p > 0; p--)
+           fputc2(0, out);
+    } while((off += cl) < dlen);
+    return(0);
+}
+
+#define fgetc2(f) ({int __c__ = fgetc(f); if(__c__ == EOF) return(-1); __c__;})
+
+static int recvrec(FILE *in, int *type, int *rid, char **data, size_t *dlen)
+{
+    int b1, b2, pl;
+    
+    if(fgetc2(in) != 1)
+       return(-1);
+    *type = fgetc2(in);
+    b1 = fgetc2(in);
+    b2 = fgetc2(in);
+    *rid = (b1 << 8) | b2;
+    b1 = fgetc2(in);
+    b2 = fgetc2(in);
+    *dlen = (b1 << 8) | b2;
+    pl = fgetc2(in);
+    if(fgetc2(in) != 0)
+       return(-1);
+    *data = smalloc(max(*dlen, 1));
+    if(fread(*data, 1, *dlen, in) != *dlen) {
+       free(data);
+       return(-1);
+    }
+    for(; pl > 0; pl--) {
+       if(fgetc(in) == EOF) {
+           free(data);
+           return(-1);
+       }
+    }
+    return(0);
+}
+
+static int begreq(FILE *out, int rid)
+{
+    char rec[] = {0, 1, 0, 0, 0, 0, 0, 0};
+    
+    return(sendrec(out, FCGI_BEGIN_REQUEST, rid, rec, 8));
+}
+
+static void mtiopipe(FILE **read, FILE **write)
+{
+    int fds[2];
+    
+    pipe(fds);
+    *read = mtstdopen(fds[0], 0, 600, "r");
+    *write = mtstdopen(fds[1], 0, 600, "w");
+}
+
+static void outplex(struct muth *muth, va_list args)
+{
+    vavar(FILE *, sk);
+    struct {
+       struct ch {
+           FILE *s;
+           int id;
+       } *b;
+       size_t s, d;
+    } outs;
+    int i;
+    struct ch ch;
+    int type, rid;
+    char *data;
+    size_t dlen;
+    
+    bufinit(outs);
+    while((ch.s = va_arg(args, FILE *)) != NULL) {
+       ch.id = va_arg(args, int);
+       bufadd(outs, ch);
+    }
+    data = NULL;
+    while(1) {
+       if(recvrec(sk, &type, &rid, &data, &dlen))
+           goto out;
+       if(rid != 1)
+           goto out;
+       for(i = 0; i < outs.d; i++) {
+           if(outs.b[i].id == type) {
+               if(outs.b[i].s != NULL) {
+                   if(dlen == 0) {
+                       fclose(outs.b[i].s);
+                       outs.b[i].s = NULL;
+                   } else {
+                       if(fwrite(data, 1, dlen, outs.b[i].s) != dlen)
+                           goto out;
+                   }
+               }
+               break;
+           }
+       }
+       free(data);
+       data = NULL;
+    }
+
+out:
+    if(data != NULL)
+       free(data);
+    for(i = 0; i < outs.d; i++) {
+       if(outs.b[i].s != NULL)
+           fclose(outs.b[i].s);
+    }
+    buffree(outs);
+    fclose(sk);
+}
+
+static void errhandler(struct muth *muth, va_list args)
+{
+    vavar(FILE *, in);
+    char buf[1024];
+    char *p;
+    
+    bufinit(buf);
+    while(fgets(buf, sizeof(buf), in) != NULL) {
+       p = buf + strlen(buf) - 1;
+       while((p >= buf) && (*p == '\n'))
+           *(p--) = 0;
+       if(buf[0])
+           flog(LOG_INFO, "child said: %s", buf);
+    }
+    fclose(in);
+}
+
+static void serve(struct muth *muth, va_list args)
+{
+    vavar(struct hthead *, req);
+    vavar(int, fd);
+    vavar(int, sfd);
+    FILE *is, *os, *outi, *outo, *erri, *erro;
+    struct charbuf head;
+    struct hthead *resp;
+    size_t read;
+    char buf[8192];
+    
+    sfd = reconn();
+    is = mtstdopen(fd, 1, 60, "r+");
+    os = mtstdopen(sfd, 1, 600, "r+");
+    
+    outi = NULL;
+    mtiopipe(&outi, &outo); mtiopipe(&erri, &erro);
+    mustart(outplex, mtstdopen(dup(sfd), 1, 600, "r+"), outo, FCGI_STDOUT, erro, FCGI_STDERR, NULL);
+    mustart(errhandler, erri);
+    
+    if(begreq(os, 1))
+       goto out;
+    bufinit(head);
+    mkcgienv(req, &head);
+    if(sendrec(os, FCGI_PARAMS, 1, head.b, head.d))
+       goto out;
+    if(sendrec(os, FCGI_PARAMS, 1, NULL, 0))
+       goto out;
+    buffree(head);
+    if(fflush(os))
+       goto out;
+    
+    while(!feof(is)) {
+       read = fread(buf, 1, sizeof(buf), is);
+       if(ferror(is))
+           goto out;
+       if(sendrec(os, FCGI_STDIN, 1, buf, read))
+           goto out;
+    }
+    if(sendrec(os, FCGI_STDIN, 1, NULL, 0))
+       goto out;
+    if(fflush(os))
+       goto out;
+    
+    if((resp = parseresp(outi)) == NULL)
+       goto out;
+    writeresp(is, resp);
+    freehthead(resp);
+    fputc('\n', is);
+    if(passdata(outi, is) < 0)
+       goto out;
+    
+out:
+    freehthead(req);
+    buffree(head);
+    shutdown(sfd, SHUT_RDWR);
+    if(outi != NULL)
+       fclose(outi);
+    fclose(is);
+    fclose(os);
+}
+
+static void listenloop(struct muth *muth, va_list args)
+{
+    vavar(int, lfd);
+    int fd;
+    struct hthead *req;
+    
+    while(1) {
+       block(0, EV_READ, 0);
+       if((fd = recvreq(lfd, &req)) < 0) {
+           if(errno != 0)
+               flog(LOG_ERR, "recvreq: %s", strerror(errno));
+           break;
+       }
+       mustart(serve, req, fd);
+    }
+}
+
+static void sigign(int sig)
+{
+}
+
+static void sigexit(int sig)
+{
+    exit(0);
+}
+
+static void usage(FILE *out)
+{
+    fprintf(out, "usage: callfcgi [-h] [-N RETRIES] [-i ID] [-u UNIX-PATH] [-t [HOST:]TCP-PORT] [PROGRAM [ARGS...]]\n");
+}
+
+int main(int argc, char **argv)
+{
+    int c;
+    
+    while((c = getopt(argc, argv, "+hN:i:u:t:")) >= 0) {
+       switch(c) {
+       case 'h':
+           usage(stdout);
+           exit(0);
+       case 'N':
+           nolisten = atoi(optarg);
+           break;
+       case 'i':
+           sockid = optarg;
+           break;
+       case 'u':
+           unspec = optarg;
+           break;
+       case 't':
+           inspec = optarg;
+           break;
+       default:
+           usage(stderr);
+           exit(1);
+       }
+    }
+    progspec = argv + optind;
+    if(((sockid != NULL) + (unspec != NULL) + (inspec != NULL)) > 1) {
+       flog(LOG_ERR, "callfcgi: at most one of -i, -u or -t may be given");
+       exit(1);
+    }
+    signal(SIGCHLD, SIG_IGN);
+    signal(SIGPIPE, sigign);
+    signal(SIGINT, sigexit);
+    signal(SIGTERM, sigexit);
+    mustart(listenloop, 0);
+    atexit(killcuraddr);
+    ioloop();
+    return(0);
+}