lib: Reimplemented mtio-epoll timeout checking as a bin-heap.
[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_ADDRESS"))
149             putenv(sprintf2("SERVER_ADDR=%s", getenv("REQ_X_ASH_SERVER_ADDRESS")));
150         if(getenv("REQ_X_ASH_SERVER_PORT"))
151             putenv(sprintf2("SERVER_PORT=%s", getenv("REQ_X_ASH_SERVER_PORT")));
152         if(getenv("REQ_X_ASH_PROTOCOL") && !strcmp(getenv("REQ_X_ASH_PROTOCOL"), "https"))
153             putenv("HTTPS=on");
154         if(getenv("REQ_X_ASH_ADDRESS"))
155             putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
156         if(getenv("REQ_X_ASH_PORT"))
157             putenv(sprintf2("REMOTE_PORT=%s", getenv("REQ_X_ASH_PORT")));
158         if(getenv("REQ_X_ASH_REMOTE_USER"))
159             putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
160         if(getenv("REQ_CONTENT_TYPE"))
161             putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
162         if(getenv("REQ_CONTENT_LENGTH"))
163             putenv(sprintf2("CONTENT_LENGTH=%s", getenv("REQ_CONTENT_LENGTH")));
164         for(env = environ; *env; env++) {
165             if(!strncmp(*env, "REQ_", 4))
166                 putenv(sprintf2("HTTP_%s", (*env) + 4));
167         }
168         /*
169          * This is (understandably) missing from the CGI
170          * specification, but PHP seems to require it.
171          */
172         putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
173         if(inpath)
174             execlp(prog, prog, file, NULL);
175         else
176             execl(prog, prog, file, NULL);
177         exit(127);
178     }
179     close(inp[0]);
180     close(outp[1]);
181     *infd = inp[1];
182     *outfd = outp[0];
183     return(pid);
184 }
185
186 static void trim(struct charbuf *buf)
187 {
188     char *p;
189     
190     for(p = buf->b; (p - buf->b < buf->d) && isspace(*p); p++);
191     memmove(buf->b, p, buf->d -= (p - buf->b));
192     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
193 }
194
195 static char **parsecgiheaders(FILE *s)
196 {
197     int c, state;
198     struct charvbuf hbuf;
199     struct charbuf buf;
200     
201     bufinit(hbuf);
202     bufinit(buf);
203     state = 0;
204     while(1) {
205         c = fgetc(s);
206     again:
207         if(state == 0) {
208             if(c == '\r') {
209             } else if(c == '\n') {
210                 break;
211             } else if(c == EOF) {
212                 goto fail;
213             } else {
214                 state = 1;
215                 goto again;
216             }
217         } else if(state == 1) {
218             if(c == ':') {
219                 trim(&buf);
220                 bufadd(buf, 0);
221                 bufadd(hbuf, buf.b);
222                 bufinit(buf);
223                 state = 2;
224             } else if(c == '\r') {
225             } else if(c == '\n') {
226                 goto fail;
227             } else if(c == EOF) {
228                 goto fail;
229             } else {
230                 bufadd(buf, c);
231             }
232         } else if(state == 2) {
233             if(c == '\r') {
234             } else if(c == '\n') {
235                 trim(&buf);
236                 bufadd(buf, 0);
237                 bufadd(hbuf, buf.b);
238                 bufinit(buf);
239                 state = 0;
240             } else if(c == EOF) {
241                 goto fail;
242             } else {
243                 bufadd(buf, c);
244             }
245         }
246     }
247     bufadd(hbuf, NULL);
248     return(hbuf.b);
249     
250 fail:
251     buffree(hbuf);
252     buffree(buf);
253     return(NULL);
254 }
255
256 static char *defstatus(int code)
257 {
258     if(code == 200)
259         return("OK");
260     else if(code == 201)
261         return("Created");
262     else if(code == 202)
263         return("Accepted");
264     else if(code == 204)
265         return("No Content");
266     else if(code == 300)
267         return("Multiple Choices");
268     else if(code == 301)
269         return("Moved Permanently");
270     else if(code == 302)
271         return("Found");
272     else if(code == 303)
273         return("See Other");
274     else if(code == 304)
275         return("Not Modified");
276     else if(code == 307)
277         return("Moved Temporarily");
278     else if(code == 400)
279         return("Bad Request");
280     else if(code == 401)
281         return("Unauthorized");
282     else if(code == 403)
283         return("Forbidden");
284     else if(code == 404)
285         return("Not Found");
286     else if(code == 500)
287         return("Internal Server Error");
288     else if(code == 501)
289         return("Not Implemented");
290     else if(code == 503)
291         return("Service Unavailable");
292     else
293         return("Unknown status");
294 }
295
296 static void sendstatus(char **headers, FILE *out)
297 {
298     char **hp;
299     char *status, *location;
300     
301     hp = headers;
302     status = location = NULL;
303     while(*hp) {
304         if(!strcasecmp(hp[0], "status")) {
305             status = hp[1];
306             /* Clear this header, so that it is not transmitted by sendheaders. */
307             **hp = 0;
308         } else if(!strcasecmp(hp[0], "location")) {
309             location = hp[1];
310             hp += 2;
311         } else {
312             hp += 2;
313         }
314     }
315     if(status) {
316         if(strchr(status, ' '))
317             fprintf(out, "HTTP/1.1 %s\n", status);
318         else
319             fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
320     } else if(location) {
321         fprintf(out, "HTTP/1.1 303 See Other\n");
322     } else {
323         fprintf(out, "HTTP/1.1 200 OK\n");
324     }
325 }
326
327 static void sendheaders(char **headers, FILE *out)
328 {
329     while(*headers) {
330         if(**headers)
331             fprintf(out, "%s: %s\n", headers[0], headers[1]);
332         headers += 2;
333     }
334 }
335
336 static void usage(void)
337 {
338     flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] METHOD URL REST");
339 }
340
341 int main(int argc, char **argv, char **envp)
342 {
343     int c;
344     char *file, *prog, *sp;
345     int inpath, cd;
346     int infd, outfd;
347     FILE *in, *out;
348     char **headers;
349     pid_t child;
350     int estat;
351     
352     environ = envp;
353     signal(SIGPIPE, SIG_IGN);
354     
355     prog = NULL;
356     inpath = 0;
357     cd = 0;
358     while((c = getopt(argc, argv, "cp:")) >= 0) {
359         switch(c) {
360         case 'c':
361             cd = 1;
362             break;
363         case 'p':
364             prog = optarg;
365             inpath = 1;
366             break;
367         default:
368             usage();
369             exit(1);
370         }
371     }
372     
373     if(argc - optind < 3) {
374         usage();
375         exit(1);
376     }
377     if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
378         flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
379         exit(1);
380     }
381     
382     if(cd) {
383         /* This behavior is encouraged by the CGI specification (RFC 3875, 7.2),
384          * but not strictly required, and I get the feeling it might break some
385          * relative paths here or there, so it's not the default for now. */
386         if((sp = strrchr(file, '/')) != NULL) {
387             *sp = 0;
388             if(chdir(file)) {
389                 *sp = '/';
390             } else {
391                 file = sp + 1;
392             }
393         }
394     }
395     
396     if(prog == NULL)
397         prog = file;
398     child = forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
399     in = fdopen(infd, "w");
400     passdata(stdin, in);        /* Ignore errors, perhaps? */
401     fclose(in);
402     out = fdopen(outfd, "r");
403     if((headers = parsecgiheaders(out)) == NULL) {
404         flog(LOG_WARNING, "CGI handler returned invalid headers");
405         exit(1);
406     }
407     sendstatus(headers, stdout);
408     sendheaders(headers, stdout);
409     printf("\n");
410     if(passdata(out, stdout))
411         kill(child, SIGINT);
412     if(waitpid(child, &estat, 0) == child) {
413         if(WCOREDUMP(estat))
414             flog(LOG_WARNING, "CGI handler `%s' dumped core", prog);
415         if(WIFEXITED(estat) && !WEXITSTATUS(estat))
416             return(0);
417         else
418             return(1);
419     }
420     flog(LOG_WARNING, "could not wait for CGI handler: %s", strerror(errno));
421     return(1);
422 }