callcgi: Don't mass-close child FDs.
[ashd.git] / src / callcgi.c
1 /*
2     ashd - A Sane HTTP Daemon
3     Copyright (C) 2008  Fredrik Tolf <fredrik@dolda2000.com>
4
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <stdlib.h>
20 #include <string.h>
21 #include <stdio.h>
22 #include <unistd.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <signal.h>
26 #include <sys/poll.h>
27 #include <sys/wait.h>
28
29 #ifdef HAVE_CONFIG_H
30 #include <config.h>
31 #endif
32 #include <utils.h>
33 #include <log.h>
34 #include <req.h>
35
36 static char **environ;
37
38 static int passdata(FILE *in, FILE *out)
39 {
40     int ret;
41     char buf[65536];
42     struct pollfd pfds[2];
43     
44     while(!feof(in)) {
45         memset(pfds, 0, sizeof(struct pollfd) * 2);
46         pfds[0].fd = fileno(in);
47         pfds[0].events = POLLIN;
48         pfds[1].fd = fileno(out);
49         pfds[1].events = POLLHUP;
50         ret = poll(pfds, 2, -1);
51         if(ret < 0) {
52             if(errno != EINTR) {
53                 flog(LOG_ERR, "callcgi: error in poll: %s", strerror(errno));
54                 return(1);
55             }
56         }
57         if(ret > 0) {
58             if(pfds[0].revents & (POLLIN | POLLERR | POLLHUP)) {
59                 ret = fread(buf, 1, 65536, in);
60                 if(ferror(in)) {
61                     flog(LOG_ERR, "callcgi: could not read input: %s", strerror(errno));
62                     return(1);
63                 }
64                 if(fwrite(buf, 1, ret, out) != ret) {
65                     flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
66                     return(1);
67                 }
68             }
69             if(pfds[1].revents & POLLHUP)
70                 return(1);
71         }
72     }
73     return(0);
74 }
75
76 static char *absolutify(char *file)
77 {
78     char cwd[1024];
79     
80     if(*file != '/') {
81         getcwd(cwd, sizeof(cwd));
82         return(sprintf2("%s/%s", cwd, file));
83     }
84     return(sstrdup(file));
85 }
86
87 static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
88 {
89     char *qp, **env, *name;
90     int inp[2], outp[2];
91     pid_t pid;
92     char *pi;
93
94     pipe(inp);
95     pipe(outp);
96     if((pid = fork()) < 0) {
97         flog(LOG_ERR, "callcgi: could not fork");
98         exit(1);
99     }
100     if(pid == 0) {
101         dup2(inp[0], 0);
102         dup2(outp[1], 1);
103         close(inp[0]);
104         close(inp[1]);
105         close(outp[0]);
106         close(outp[1]);
107         if((qp = strchr(url, '?')) != NULL)
108             *(qp++) = 0;
109         putenv(sprintf2("SERVER_SOFTWARE=ashd/%s", VERSION));
110         putenv("GATEWAY_INTERFACE=CGI/1.1");
111         if(getenv("HTTP_VERSION"))
112             putenv(sprintf2("SERVER_PROTOCOL=%s", getenv("HTTP_VERSION")));
113         putenv(sprintf2("REQUEST_METHOD=%s", method));
114         name = url;
115         /* XXX: This is an ugly hack (I think), but though I can think
116          * of several alternatives, none seem to be better. */
117         if(*rest && (strlen(url) >= strlen(rest)) &&
118            !strcmp(rest, url + strlen(url) - strlen(rest))) {
119             name = sprintf2("%.*s", (int)(strlen(url) - strlen(rest)), url);
120         }
121         if((pi = unquoteurl(rest)) == NULL)
122             pi = rest;
123         if(!strcmp(name, "/")) {
124             /*
125              * Normal CGI behavior appears to be to always let
126              * PATH_INFO begin with a slash and never let SCRIPT_NAME
127              * end with one. That conflicts, however, with some
128              * behaviors, such as "mounting" CGI applications on a
129              * directory element of the URI space -- a handler
130              * responding to "/foo/" would not be able to tell that it
131              * is not called "/foo", which makes a large difference,
132              * not least in relation to URI reconstruction and
133              * redirections. A common practical case is CGI-handled
134              * index files in directories. Therefore, this only
135              * handles the nonconditional case of the root directory
136              * and leaves other decisions to the previous handler
137              * handing over the request to callcgi. It is unclear if
138              * there is a better way to handle the problem.
139              */
140             name[0] = 0;
141             pi = sprintf2("/%s", pi);
142         }
143         putenv(sprintf2("PATH_INFO=%s", pi));
144         putenv(sprintf2("SCRIPT_NAME=%s", name));
145         putenv(sprintf2("QUERY_STRING=%s", qp?qp:""));
146         if(getenv("REQ_HOST"))
147             putenv(sprintf2("SERVER_NAME=%s", getenv("REQ_HOST")));
148         if(getenv("REQ_X_ASH_SERVER_PORT"))
149             putenv(sprintf2("SERVER_PORT=%s", getenv("REQ_X_ASH_SERVER_PORT")));
150         if(getenv("REQ_X_ASH_PROTOCOL") && !strcmp(getenv("REQ_X_ASH_PROTOCOL"), "https"))
151             putenv("HTTPS=on");
152         if(getenv("REQ_X_ASH_ADDRESS"))
153             putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
154         if(getenv("REQ_X_ASH_REMOTE_USER"))
155             putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
156         if(getenv("REQ_CONTENT_TYPE"))
157             putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
158         if(getenv("REQ_CONTENT_LENGTH"))
159             putenv(sprintf2("CONTENT_LENGTH=%s", getenv("REQ_CONTENT_LENGTH")));
160         for(env = environ; *env; env++) {
161             if(!strncmp(*env, "REQ_", 4))
162                 putenv(sprintf2("HTTP_%s", (*env) + 4));
163         }
164         /*
165          * This is (understandably) missing from the CGI
166          * specification, but PHP seems to require it.
167          */
168         putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
169         if(inpath)
170             execlp(prog, prog, file, NULL);
171         else
172             execl(prog, prog, file, NULL);
173         exit(127);
174     }
175     close(inp[0]);
176     close(outp[1]);
177     *infd = inp[1];
178     *outfd = outp[0];
179     return(pid);
180 }
181
182 static void trim(struct charbuf *buf)
183 {
184     char *p;
185     
186     for(p = buf->b; (p - buf->b < buf->d) && isspace(*p); p++);
187     memmove(buf->b, p, buf->d -= (p - buf->b));
188     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
189 }
190
191 static char **parsecgiheaders(FILE *s)
192 {
193     int c, state;
194     struct charvbuf hbuf;
195     struct charbuf buf;
196     
197     bufinit(hbuf);
198     bufinit(buf);
199     state = 0;
200     while(1) {
201         c = fgetc(s);
202     again:
203         if(state == 0) {
204             if(c == '\r') {
205             } else if(c == '\n') {
206                 break;
207             } else if(c == EOF) {
208                 goto fail;
209             } else {
210                 state = 1;
211                 goto again;
212             }
213         } else if(state == 1) {
214             if(c == ':') {
215                 trim(&buf);
216                 bufadd(buf, 0);
217                 bufadd(hbuf, buf.b);
218                 bufinit(buf);
219                 state = 2;
220             } else if(c == '\r') {
221             } else if(c == '\n') {
222                 goto fail;
223             } else if(c == EOF) {
224                 goto fail;
225             } else {
226                 bufadd(buf, c);
227             }
228         } else if(state == 2) {
229             if(c == '\r') {
230             } else if(c == '\n') {
231                 trim(&buf);
232                 bufadd(buf, 0);
233                 bufadd(hbuf, buf.b);
234                 bufinit(buf);
235                 state = 0;
236             } else if(c == EOF) {
237                 goto fail;
238             } else {
239                 bufadd(buf, c);
240             }
241         }
242     }
243     bufadd(hbuf, NULL);
244     return(hbuf.b);
245     
246 fail:
247     buffree(hbuf);
248     buffree(buf);
249     return(NULL);
250 }
251
252 static char *defstatus(int code)
253 {
254     if(code == 200)
255         return("OK");
256     else if(code == 201)
257         return("Created");
258     else if(code == 202)
259         return("Accepted");
260     else if(code == 204)
261         return("No Content");
262     else if(code == 300)
263         return("Multiple Choices");
264     else if(code == 301)
265         return("Moved Permanently");
266     else if(code == 302)
267         return("Found");
268     else if(code == 303)
269         return("See Other");
270     else if(code == 304)
271         return("Not Modified");
272     else if(code == 307)
273         return("Moved Temporarily");
274     else if(code == 400)
275         return("Bad Request");
276     else if(code == 401)
277         return("Unauthorized");
278     else if(code == 403)
279         return("Forbidden");
280     else if(code == 404)
281         return("Not Found");
282     else if(code == 500)
283         return("Internal Server Error");
284     else if(code == 501)
285         return("Not Implemented");
286     else if(code == 503)
287         return("Service Unavailable");
288     else
289         return("Unknown status");
290 }
291
292 static void sendstatus(char **headers, FILE *out)
293 {
294     char **hp;
295     char *status, *location;
296     
297     hp = headers;
298     status = location = NULL;
299     while(*hp) {
300         if(!strcasecmp(hp[0], "status")) {
301             status = hp[1];
302             /* Clear this header, so that it is not transmitted by sendheaders. */
303             **hp = 0;
304         } else if(!strcasecmp(hp[0], "location")) {
305             location = hp[1];
306             hp += 2;
307         } else {
308             hp += 2;
309         }
310     }
311     if(status) {
312         if(strchr(status, ' '))
313             fprintf(out, "HTTP/1.1 %s\n", status);
314         else
315             fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
316     } else if(location) {
317         fprintf(out, "HTTP/1.1 303 See Other\n");
318     } else {
319         fprintf(out, "HTTP/1.1 200 OK\n");
320     }
321 }
322
323 static void sendheaders(char **headers, FILE *out)
324 {
325     while(*headers) {
326         if(**headers)
327             fprintf(out, "%s: %s\n", headers[0], headers[1]);
328         headers += 2;
329     }
330 }
331
332 static void usage(void)
333 {
334     flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] METHOD URL REST");
335 }
336
337 int main(int argc, char **argv, char **envp)
338 {
339     int c;
340     char *file, *prog, *sp;
341     int inpath, cd;
342     int infd, outfd;
343     FILE *in, *out;
344     char **headers;
345     pid_t child;
346     int estat;
347     
348     environ = envp;
349     signal(SIGPIPE, SIG_IGN);
350     
351     prog = NULL;
352     inpath = 0;
353     cd = 0;
354     while((c = getopt(argc, argv, "cp:")) >= 0) {
355         switch(c) {
356         case 'c':
357             cd = 1;
358             break;
359         case 'p':
360             prog = optarg;
361             inpath = 1;
362             break;
363         default:
364             usage();
365             exit(1);
366         }
367     }
368     
369     if(argc - optind < 3) {
370         usage();
371         exit(1);
372     }
373     if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
374         flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
375         exit(1);
376     }
377     
378     if(cd) {
379         /* This behavior is encouraged by the CGI specification (RFC 3875, 7.2),
380          * but not strictly required, and I get the feeling it might break some
381          * relative paths here or there, so it's not the default for now. */
382         if((sp = strrchr(file, '/')) != NULL) {
383             *sp = 0;
384             if(chdir(file)) {
385                 *sp = '/';
386             } else {
387                 file = sp + 1;
388             }
389         }
390     }
391     
392     if(prog == NULL)
393         prog = file;
394     child = forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
395     in = fdopen(infd, "w");
396     passdata(stdin, in);        /* Ignore errors, perhaps? */
397     fclose(in);
398     out = fdopen(outfd, "r");
399     if((headers = parsecgiheaders(out)) == NULL) {
400         flog(LOG_WARNING, "CGI handler returned invalid headers");
401         exit(1);
402     }
403     sendstatus(headers, stdout);
404     sendheaders(headers, stdout);
405     printf("\n");
406     if(passdata(out, stdout))
407         kill(child, SIGINT);
408     if(waitpid(child, &estat, 0) == child) {
409         if(WCOREDUMP(estat))
410             flog(LOG_WARNING, "CGI handler `%s' dumped core", prog);
411         if(WIFEXITED(estat) && !WEXITSTATUS(estat))
412             return(0);
413         else
414             return(1);
415     }
416     flog(LOG_WARNING, "could not wait for CGI handler: %s", strerror(errno));
417     return(1);
418 }