examples: Added some commentary.
[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
28 #ifdef HAVE_CONFIG_H
29 #include <config.h>
30 #endif
31 #include <utils.h>
32 #include <log.h>
33 #include <req.h>
34
35 static char **environ;
36
37 static int passdata(FILE *in, FILE *out)
38 {
39     int ret;
40     char buf[65536];
41     struct pollfd pfds[2];
42     
43     while(!feof(in)) {
44         memset(pfds, 0, sizeof(struct pollfd) * 2);
45         pfds[0].fd = fileno(in);
46         pfds[0].events = POLLIN;
47         pfds[1].fd = fileno(out);
48         pfds[1].events = POLLHUP;
49         ret = poll(pfds, 2, -1);
50         if(ret < 0) {
51             if(errno != EINTR) {
52                 flog(LOG_ERR, "callcgi: error in poll: %s", strerror(errno));
53                 return(1);
54             }
55         }
56         if(ret > 0) {
57             if(pfds[0].revents & (POLLIN | POLLERR | POLLHUP)) {
58                 ret = fread(buf, 1, 65536, in);
59                 if(ferror(in)) {
60                     flog(LOG_ERR, "callcgi: could not read input: %s", strerror(errno));
61                     return(1);
62                 }
63                 if(fwrite(buf, 1, ret, out) != ret) {
64                     flog(LOG_ERR, "callcgi: could not write output: %s", strerror(errno));
65                     return(1);
66                 }
67             }
68             if(pfds[1].revents & POLLHUP)
69                 return(1);
70         }
71     }
72     return(0);
73 }
74
75 static char *absolutify(char *file)
76 {
77     char cwd[1024];
78     
79     if(*file != '/') {
80         getcwd(cwd, sizeof(cwd));
81         return(sprintf2("%s/%s", cwd, file));
82     }
83     return(sstrdup(file));
84 }
85
86 static pid_t forkchild(int inpath, char *prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
87 {
88     int i;
89     char *qp, **env, *name;
90     int inp[2], outp[2];
91     pid_t pid;
92     char *unqr;
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         close(inp[1]);
102         close(outp[0]);
103         dup2(inp[0], 0);
104         dup2(outp[1], 1);
105         for(i = 3; i < FD_SETSIZE; i++)
106             close(i);
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         unqr = unquoteurl(rest);
115         putenv(sprintf2("PATH_INFO=%s", unqr?unqr:rest));
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         putenv(sprintf2("SCRIPT_NAME=%s", name));
124         putenv(sprintf2("QUERY_STRING=%s", qp?qp:""));
125         if(getenv("REQ_HOST"))
126             putenv(sprintf2("SERVER_NAME=%s", getenv("REQ_HOST")));
127         if(getenv("REQ_X_ASH_SERVER_PORT"))
128             putenv(sprintf2("SERVER_PORT=%s", getenv("REQ_X_ASH_SERVER_PORT")));
129         if(getenv("REQ_X_ASH_PROTOCOL") && !strcmp(getenv("REQ_X_ASH_PROTOCOL"), "https"))
130             putenv("HTTPS=on");
131         if(getenv("REQ_X_ASH_ADDRESS"))
132             putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
133         if(getenv("REQ_X_ASH_REMOTE_USER"))
134             putenv(sprintf2("REMOTE_USER=%s", getenv("REQ_X_ASH_REMOTE_USER")));
135         if(getenv("REQ_CONTENT_TYPE"))
136             putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
137         if(getenv("REQ_CONTENT_LENGTH"))
138             putenv(sprintf2("CONTENT_LENGTH=%s", getenv("REQ_CONTENT_LENGTH")));
139         for(env = environ; *env; env++) {
140             if(!strncmp(*env, "REQ_", 4))
141                 putenv(sprintf2("HTTP_%s", (*env) + 4));
142         }
143         /*
144          * This is (understandably) missing from the CGI
145          * specification, but PHP seems to require it.
146          */
147         putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
148         if(inpath)
149             execlp(prog, prog, file, NULL);
150         else
151             execl(prog, prog, file, NULL);
152         exit(127);
153     }
154     close(inp[0]);
155     close(outp[1]);
156     *infd = inp[1];
157     *outfd = outp[0];
158     return(pid);
159 }
160
161 static void trim(struct charbuf *buf)
162 {
163     char *p;
164     
165     for(p = buf->b; (p - buf->b < buf->d) && isspace(*p); p++);
166     memmove(buf->b, p, buf->d -= (p - buf->b));
167     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
168 }
169
170 static char **parsecgiheaders(FILE *s)
171 {
172     int c, state;
173     struct charvbuf hbuf;
174     struct charbuf buf;
175     
176     bufinit(hbuf);
177     bufinit(buf);
178     state = 0;
179     while(1) {
180         c = fgetc(s);
181     again:
182         if(state == 0) {
183             if(c == '\r') {
184             } else if(c == '\n') {
185                 break;
186             } else if(c == EOF) {
187                 goto fail;
188             } else {
189                 state = 1;
190                 goto again;
191             }
192         } else if(state == 1) {
193             if(c == ':') {
194                 trim(&buf);
195                 bufadd(buf, 0);
196                 bufadd(hbuf, buf.b);
197                 bufinit(buf);
198                 state = 2;
199             } else if(c == '\r') {
200             } else if(c == '\n') {
201                 goto fail;
202             } else if(c == EOF) {
203                 goto fail;
204             } else {
205                 bufadd(buf, c);
206             }
207         } else if(state == 2) {
208             if(c == '\r') {
209             } else if(c == '\n') {
210                 trim(&buf);
211                 bufadd(buf, 0);
212                 bufadd(hbuf, buf.b);
213                 bufinit(buf);
214                 state = 0;
215             } else if(c == EOF) {
216                 goto fail;
217             } else {
218                 bufadd(buf, c);
219             }
220         }
221     }
222     bufadd(hbuf, NULL);
223     return(hbuf.b);
224     
225 fail:
226     buffree(hbuf);
227     buffree(buf);
228     return(NULL);
229 }
230
231 static char *defstatus(int code)
232 {
233     if(code == 200)
234         return("OK");
235     else if(code == 201)
236         return("Created");
237     else if(code == 202)
238         return("Accepted");
239     else if(code == 204)
240         return("No Content");
241     else if(code == 300)
242         return("Multiple Choices");
243     else if(code == 301)
244         return("Moved Permanently");
245     else if(code == 302)
246         return("Found");
247     else if(code == 303)
248         return("See Other");
249     else if(code == 304)
250         return("Not Modified");
251     else if(code == 307)
252         return("Moved Temporarily");
253     else if(code == 400)
254         return("Bad Request");
255     else if(code == 401)
256         return("Unauthorized");
257     else if(code == 403)
258         return("Forbidden");
259     else if(code == 404)
260         return("Not Found");
261     else if(code == 500)
262         return("Internal Server Error");
263     else if(code == 501)
264         return("Not Implemented");
265     else if(code == 503)
266         return("Service Unavailable");
267     else
268         return("Unknown status");
269 }
270
271 static void sendstatus(char **headers, FILE *out)
272 {
273     char **hp;
274     char *status, *location;
275     
276     hp = headers;
277     status = location = NULL;
278     while(*hp) {
279         if(!strcasecmp(hp[0], "status")) {
280             status = hp[1];
281             /* Clear this header, so that it is not transmitted by sendheaders. */
282             **hp = 0;
283         } else if(!strcasecmp(hp[0], "location")) {
284             location = hp[1];
285             hp += 2;
286         } else {
287             hp += 2;
288         }
289     }
290     if(status) {
291         if(strchr(status, ' '))
292             fprintf(out, "HTTP/1.1 %s\n", status);
293         else
294             fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
295     } else if(location) {
296         fprintf(out, "HTTP/1.1 303 See Other\n");
297     } else {
298         fprintf(out, "HTTP/1.1 200 OK\n");
299     }
300 }
301
302 static void sendheaders(char **headers, FILE *out)
303 {
304     while(*headers) {
305         if(**headers)
306             fprintf(out, "%s: %s\n", headers[0], headers[1]);
307         headers += 2;
308     }
309 }
310
311 static void usage(void)
312 {
313     flog(LOG_ERR, "usage: callcgi [-c] [-p PROGRAM] METHOD URL REST");
314 }
315
316 int main(int argc, char **argv, char **envp)
317 {
318     int c;
319     char *file, *prog, *sp;
320     int inpath, cd;
321     int infd, outfd;
322     FILE *in, *out;
323     char **headers;
324     pid_t child;
325     
326     environ = envp;
327     signal(SIGPIPE, SIG_IGN);
328     
329     prog = NULL;
330     inpath = 0;
331     cd = 0;
332     while((c = getopt(argc, argv, "cp:")) >= 0) {
333         switch(c) {
334         case 'c':
335             cd = 1;
336             break;
337         case 'p':
338             prog = optarg;
339             inpath = 1;
340             break;
341         default:
342             usage();
343             exit(1);
344         }
345     }
346     
347     if(argc - optind < 3) {
348         usage();
349         exit(1);
350     }
351     if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
352         flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
353         exit(1);
354     }
355     
356     if(cd) {
357         /* This behavior is encouraged by the CGI specification (RFC 3875, 7.2),
358          * but not strictly required, and I get the feeling it might break some
359          * relative paths here or there, so it's not the default for now. */
360         if((sp = strrchr(file, '/')) != NULL) {
361             *sp = 0;
362             if(chdir(file)) {
363                 *sp = '/';
364             } else {
365                 file = sp + 1;
366             }
367         }
368     }
369     
370     if(prog == NULL)
371         prog = file;
372     child = forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
373     in = fdopen(infd, "w");
374     passdata(stdin, in);        /* Ignore errors, perhaps? */
375     fclose(in);
376     out = fdopen(outfd, "r");
377     if((headers = parsecgiheaders(out)) == NULL) {
378         flog(LOG_WARNING, "CGI handler returned invalid headers");
379         exit(1);
380     }
381     sendstatus(headers, stdout);
382     sendheaders(headers, stdout);
383     printf("\n");
384     if(passdata(out, stdout))
385         kill(child, SIGINT);
386     return(0);
387 }