67a6d14e31f66edd4fca0ece6b89a987094145a1
[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                     if(errno != EPIPE)
66                         flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
67                     return(1);
68                 }
69             }
70             if(pfds[1].revents & POLLHUP)
71                 return(1);
72         }
73     }
74     return(0);
75 }
76
77 static char *absolutify(char *file)
78 {
79     char cwd[1024];
80     
81     if(*file != '/') {
82         getcwd(cwd, sizeof(cwd));
83         return(sprintf2("%s/%s", cwd, file));
84     }
85     return(sstrdup(file));
86 }
87
88 static pid_t forkchild(int inpath, char **prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
89 {
90     char *qp, **env, *name;
91     int inp[2], outp[2];
92     pid_t pid;
93     char *pi;
94     int (*execfun)(const char *, char *const []);
95
96     pipe(inp);
97     pipe(outp);
98     if((pid = fork()) < 0) {
99         flog(LOG_ERR, "callcgi: could not fork");
100         exit(1);
101     }
102     if(pid == 0) {
103         dup2(inp[0], 0);
104         dup2(outp[1], 1);
105         close(inp[0]);
106         close(inp[1]);
107         close(outp[0]);
108         close(outp[1]);
109         if((qp = strchr(url, '?')) != NULL)
110             *(qp++) = 0;
111         putenv(sprintf2("SERVER_SOFTWARE=ashd/%s", VERSION));
112         putenv("GATEWAY_INTERFACE=CGI/1.1");
113         if(getenv("HTTP_VERSION"))
114             putenv(sprintf2("SERVER_PROTOCOL=%s", getenv("HTTP_VERSION")));
115         putenv(sprintf2("REQUEST_METHOD=%s", method));
116         name = url;
117         /* XXX: This is an ugly hack (I think), but though I can think
118          * of several alternatives, none seem to be better. */
119         if(*rest && (strlen(url) >= strlen(rest)) &&
120            !strcmp(rest, url + strlen(url) - strlen(rest))) {
121             name = sprintf2("%.*s", (int)(strlen(url) - strlen(rest)), url);
122         }
123         if((pi = unquoteurl(rest)) == NULL)
124             pi = rest;
125         if(!strcmp(name, "/")) {
126             /*
127              * Normal CGI behavior appears to be to always let
128              * PATH_INFO begin with a slash and never let SCRIPT_NAME
129              * end with one. That conflicts, however, with some
130              * behaviors, such as "mounting" CGI applications on a
131              * directory element of the URI space -- a handler
132              * responding to "/foo/" would not be able to tell that it
133              * is not called "/foo", which makes a large difference,
134              * not least in relation to URI reconstruction and
135              * redirections. A common practical case is CGI-handled
136              * index files in directories. Therefore, this only
137              * handles the nonconditional case of the root directory
138              * and leaves other decisions to the previous handler
139              * handing over the request to callcgi. It is unclear if
140              * there is a better way to handle the problem.
141              */
142             name[0] = 0;
143             pi = sprintf2("/%s", pi);
144         }
145         putenv(sprintf2("PATH_INFO=%s", pi));
146         putenv(sprintf2("SCRIPT_NAME=%s", name));
147         putenv(sprintf2("QUERY_STRING=%s", qp?qp:""));
148         if(getenv("REQ_HOST"))
149             putenv(sprintf2("SERVER_NAME=%s", getenv("REQ_HOST")));
150         if(getenv("REQ_X_ASH_SERVER_ADDRESS"))
151             putenv(sprintf2("SERVER_ADDR=%s", getenv("REQ_X_ASH_SERVER_ADDRESS")));
152         if(getenv("REQ_X_ASH_SERVER_PORT"))
153             putenv(sprintf2("SERVER_PORT=%s", getenv("REQ_X_ASH_SERVER_PORT")));
154         if(getenv("REQ_X_ASH_PROTOCOL") && !strcmp(getenv("REQ_X_ASH_PROTOCOL"), "https"))
155             putenv("HTTPS=on");
156         if(getenv("REQ_X_ASH_ADDRESS"))
157             putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
158         if(getenv("REQ_X_ASH_PORT"))
159             putenv(sprintf2("REMOTE_PORT=%s", getenv("REQ_X_ASH_PORT")));
160         if(getenv("REQ_X_ASH_REMOTE_USER"))
161             putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
162         if(getenv("REQ_CONTENT_TYPE"))
163             putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
164         if(getenv("REQ_CONTENT_LENGTH"))
165             putenv(sprintf2("CONTENT_LENGTH=%s", getenv("REQ_CONTENT_LENGTH")));
166         for(env = environ; *env; env++) {
167             if(!strncmp(*env, "REQ_", 4))
168                 putenv(sprintf2("HTTP_%s", (*env) + 4));
169         }
170         /*
171          * This is (understandably) missing from the CGI
172          * specification, but PHP seems to require it.
173          */
174         execfun = inpath?execvp:execv;
175         if(file != NULL)
176             putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
177         execfun(prog[0], prog);
178         exit(127);
179     }
180     close(inp[0]);
181     close(outp[1]);
182     *infd = inp[1];
183     *outfd = outp[0];
184     return(pid);
185 }
186
187 static void trim(struct charbuf *buf)
188 {
189     char *p;
190     
191     for(p = buf->b; (p - buf->b < buf->d) && isspace(*p); p++);
192     memmove(buf->b, p, buf->d -= (p - buf->b));
193     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
194 }
195
196 static char **parsecgiheaders(FILE *s)
197 {
198     int c, state;
199     struct charvbuf hbuf;
200     struct charbuf buf;
201     
202     bufinit(hbuf);
203     bufinit(buf);
204     state = 0;
205     while(1) {
206         c = fgetc(s);
207     again:
208         if(state == 0) {
209             if(c == '\r') {
210             } else if(c == '\n') {
211                 break;
212             } else if(c == EOF) {
213                 goto fail;
214             } else {
215                 state = 1;
216                 goto again;
217             }
218         } else if(state == 1) {
219             if(c == ':') {
220                 trim(&buf);
221                 bufadd(buf, 0);
222                 bufadd(hbuf, buf.b);
223                 bufinit(buf);
224                 state = 2;
225             } else if(c == '\r') {
226             } else if(c == '\n') {
227                 goto fail;
228             } else if(c == EOF) {
229                 goto fail;
230             } else {
231                 bufadd(buf, c);
232             }
233         } else if(state == 2) {
234             if(c == '\r') {
235             } else if(c == '\n') {
236                 trim(&buf);
237                 bufadd(buf, 0);
238                 bufadd(hbuf, buf.b);
239                 bufinit(buf);
240                 state = 0;
241             } else if(c == EOF) {
242                 goto fail;
243             } else {
244                 bufadd(buf, c);
245             }
246         }
247     }
248     bufadd(hbuf, NULL);
249     return(hbuf.b);
250     
251 fail:
252     buffree(hbuf);
253     buffree(buf);
254     return(NULL);
255 }
256
257 static char *defstatus(int code)
258 {
259     if(code == 200)
260         return("OK");
261     else if(code == 201)
262         return("Created");
263     else if(code == 202)
264         return("Accepted");
265     else if(code == 204)
266         return("No Content");
267     else if(code == 300)
268         return("Multiple Choices");
269     else if(code == 301)
270         return("Moved Permanently");
271     else if(code == 302)
272         return("Found");
273     else if(code == 303)
274         return("See Other");
275     else if(code == 304)
276         return("Not Modified");
277     else if(code == 307)
278         return("Moved Temporarily");
279     else if(code == 400)
280         return("Bad Request");
281     else if(code == 401)
282         return("Unauthorized");
283     else if(code == 403)
284         return("Forbidden");
285     else if(code == 404)
286         return("Not Found");
287     else if(code == 500)
288         return("Internal Server Error");
289     else if(code == 501)
290         return("Not Implemented");
291     else if(code == 503)
292         return("Service Unavailable");
293     else
294         return("Unknown status");
295 }
296
297 static void sendstatus(char **headers, FILE *out)
298 {
299     char **hp;
300     char *status, *location;
301     
302     hp = headers;
303     status = location = NULL;
304     while(*hp) {
305         if(!strcasecmp(hp[0], "status")) {
306             status = hp[1];
307             /* Clear this header, so that it is not transmitted by sendheaders. */
308             **hp = 0;
309         } else if(!strcasecmp(hp[0], "location")) {
310             location = hp[1];
311             hp += 2;
312         } else {
313             hp += 2;
314         }
315     }
316     if(status) {
317         if(strchr(status, ' '))
318             fprintf(out, "HTTP/1.1 %s\n", status);
319         else
320             fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
321     } else if(location) {
322         fprintf(out, "HTTP/1.1 303 See Other\n");
323     } else {
324         fprintf(out, "HTTP/1.1 200 OK\n");
325     }
326 }
327
328 static void sendheaders(char **headers, FILE *out)
329 {
330     while(*headers) {
331         if(**headers)
332             fprintf(out, "%s: %s\n", headers[0], headers[1]);
333         headers += 2;
334     }
335 }
336
337 static void usage(void)
338 {
339     flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] [-P PROGRAM ARGS... ;] METHOD URL REST");
340 }
341
342 int main(int argc, char **argv, char **envp)
343 {
344     int c;
345     char *file, *sp;
346     struct charvbuf prog;
347     int inpath, addfile, cd;
348     int infd, outfd;
349     FILE *in, *out;
350     char **headers;
351     pid_t child;
352     int estat;
353     
354     environ = envp;
355     signal(SIGPIPE, SIG_IGN);
356     
357     bufinit(prog);
358     inpath = 0;
359     addfile = 1;
360     cd = 0;
361     while((c = getopt(argc, argv, "cp:P:")) >= 0) {
362         switch(c) {
363         case 'c':
364             cd = 1;
365             break;
366         case 'p':
367             bufadd(prog, optarg);
368             inpath = 1;
369             break;
370         case 'P':
371             prog.d = 0;
372             bufadd(prog, optarg);
373             while(1) {
374                 if(optind >= argc) {
375                     flog(LOG_ERR, "callcgi: unterminated argument list for -P");
376                     exit(1);
377                 }
378                 if(!strcmp(argv[optind], ";")) {
379                     optind++;
380                     break;
381                 }
382                 bufadd(prog, argv[optind++]);
383             }
384             if(prog.d == 0) {
385                 flog(LOG_ERR, "callcgi: -P option needs at least a program name");
386                 exit(1);
387             }
388             inpath = 1;
389             addfile = 0;
390             break;
391         default:
392             usage();
393             exit(1);
394         }
395     }
396     
397     if(argc - optind < 3) {
398         usage();
399         exit(1);
400     }
401     if(((file = getenv("REQ_X_ASH_FILE")) == NULL) && (prog.d == 0)) {
402         flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
403         exit(1);
404     }
405     
406     if(cd) {
407         /* This behavior is encouraged by the CGI specification (RFC 3875, 7.2),
408          * but not strictly required, and I get the feeling it might break some
409          * relative paths here or there, so it's not the default for now. */
410         if((sp = strrchr(file, '/')) != NULL) {
411             *sp = 0;
412             if(chdir(file)) {
413                 *sp = '/';
414             } else {
415                 file = sp + 1;
416             }
417         }
418     }
419     
420     if(prog.d == 0)
421         bufadd(prog, file);
422     if(addfile && (file != NULL))
423         bufadd(prog, file);
424     bufadd(prog, NULL);
425     child = forkchild(inpath, prog.b, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
426     in = fdopen(infd, "w");
427     passdata(stdin, in);        /* Ignore errors, perhaps? */
428     fclose(in);
429     out = fdopen(outfd, "r");
430     if((headers = parsecgiheaders(out)) == NULL) {
431         flog(LOG_WARNING, "CGI handler returned invalid headers");
432         exit(1);
433     }
434     sendstatus(headers, stdout);
435     sendheaders(headers, stdout);
436     printf("\n");
437     if(passdata(out, stdout))
438         kill(child, SIGINT);
439     if(waitpid(child, &estat, 0) == child) {
440         if(WCOREDUMP(estat))
441             flog(LOG_WARNING, "CGI handler `%s' dumped core", prog.b[0]);
442         if(WIFEXITED(estat) && !WEXITSTATUS(estat))
443             return(0);
444         else
445             return(1);
446     }
447     flog(LOG_WARNING, "could not wait for CGI handler: %s", strerror(errno));
448     return(1);
449 }