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