Fixed a bug in callcgi.
[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
27 #ifdef HAVE_CONFIG_H
28 #include <config.h>
29 #endif
30 #include <utils.h>
31 #include <log.h>
32
33 static char **environ;
34
35 static void passdata(FILE *in, FILE *out)
36 {
37     int ret;
38     char *buf;
39     
40     buf = smalloc(65536);
41     while(!feof(in)) {
42         ret = fread(buf, 1, 65536, in);
43         if(ferror(in)) {
44             flog(LOG_ERR, "sendfile: could not read input: %s", strerror(errno));
45             break;
46         }
47         if(fwrite(buf, 1, ret, out) != ret) {
48             flog(LOG_ERR, "sendfile: could not write output: %s", strerror(errno));
49             break;
50         }
51     }
52     free(buf);
53 }
54
55 static char *absolutify(char *file)
56 {
57     char cwd[1024];
58     
59     if(*file != '/') {
60         getcwd(cwd, sizeof(cwd));
61         return(sprintf2("%s/%s", cwd, file));
62     }
63     return(sstrdup(file));
64 }
65
66 static void forkchild(int inpath, char *prog, char *file, char *method, char *url, char *rest, int *infd, int *outfd)
67 {
68     int i;
69     char *qp, **env;
70     int inp[2], outp[2];
71     pid_t pid;
72
73     pipe(inp);
74     pipe(outp);
75     if((pid = fork()) < 0) {
76         flog(LOG_ERR, "callcgi: could not fork");
77         exit(1);
78     }
79     if(pid == 0) {
80         close(inp[1]);
81         close(outp[0]);
82         dup2(inp[0], 0);
83         dup2(outp[1], 1);
84         for(i = 3; i < FD_SETSIZE; i++)
85             close(i);
86         if((qp = strchr(url, '?')) != NULL)
87             *(qp++) = 0;
88         /*
89          * XXX: Currently missing:
90          *  SERVER_NAME (Partially)
91          *  SERVER_PORT
92          */
93         putenv(sprintf2("SERVER_SOFTWARE=ashd/%s", VERSION));
94         putenv("GATEWAY_INTERFACE=CGI/1.1");
95         if(getenv("HTTP_VERSION"))
96             putenv(sprintf2("SERVER_PROTOCOL=%s", getenv("HTTP_VERSION")));
97         putenv(sprintf2("REQUEST_METHOD=%s", method));
98         putenv(sprintf2("PATH_INFO=%s", rest));
99         putenv(sprintf2("SCRIPT_NAME=%s", url));
100         putenv(sprintf2("QUERY_STRING=%s", qp?qp:""));
101         if(getenv("REQ_HOST"))
102             putenv(sprintf2("SERVER_NAME=%s", getenv("REQ_HOST")));
103         if(getenv("REQ_X_ASH_ADDRESS"))
104             putenv(sprintf2("REMOTE_ADDR=%s", getenv("REQ_X_ASH_ADDRESS")));
105         if(getenv("REQ_CONTENT_TYPE"))
106             putenv(sprintf2("CONTENT_TYPE=%s", getenv("REQ_CONTENT_TYPE")));
107         if(getenv("REQ_CONTENT_LENGTH"))
108             putenv(sprintf2("CONTENT_LENGTH=%s", getenv("REQ_CONTENT_LENGTH")));
109         for(env = environ; *env; env++) {
110             if(!strncmp(*env, "REQ_", 4))
111                 putenv(sprintf2("HTTP_%s", (*env) + 4));
112         }
113         /*
114          * This is (understandably) missing from the CGI
115          * specification, but PHP seems to require it.
116          */
117         putenv(sprintf2("SCRIPT_FILENAME=%s", absolutify(file)));
118         if(inpath)
119             execlp(prog, prog, file, NULL);
120         else
121             execl(prog, prog, file, NULL);
122         exit(127);
123     }
124     close(inp[0]);
125     close(outp[1]);
126     *infd = inp[1];
127     *outfd = outp[0];
128 }
129
130 static void trim(struct charbuf *buf)
131 {
132     char *p;
133     
134     for(p = buf->b; (p - buf->b < buf->d) && isspace(*p); p++);
135     memmove(buf->b, p, buf->d -= (p - buf->b));
136     for(p = buf->b + buf->d - 1; (p > buf->b) && isspace(*p); p--, buf->d--);
137 }
138
139 static char **parseheaders(FILE *s)
140 {
141     int c, state;
142     struct charvbuf hbuf;
143     struct charbuf buf;
144     
145     bufinit(hbuf);
146     bufinit(buf);
147     state = 0;
148     while(1) {
149         c = fgetc(s);
150     again:
151         if(state == 0) {
152             if(c == '\r') {
153             } else if(c == '\n') {
154                 break;
155             } else if(c == EOF) {
156                 goto fail;
157             } else {
158                 state = 1;
159                 goto again;
160             }
161         } else if(state == 1) {
162             if(c == ':') {
163                 trim(&buf);
164                 bufadd(buf, 0);
165                 bufadd(hbuf, buf.b);
166                 bufinit(buf);
167                 state = 2;
168             } else if(c == '\r') {
169             } else if(c == '\n') {
170                 goto fail;
171             } else if(c == EOF) {
172                 goto fail;
173             } else {
174                 bufadd(buf, c);
175             }
176         } else if(state == 2) {
177             if(c == '\r') {
178             } else if(c == '\n') {
179                 trim(&buf);
180                 bufadd(buf, 0);
181                 bufadd(hbuf, buf.b);
182                 bufinit(buf);
183                 state = 0;
184             } else if(c == EOF) {
185                 goto fail;
186             } else {
187                 bufadd(buf, c);
188             }
189         }
190     }
191     bufadd(hbuf, NULL);
192     return(hbuf.b);
193     
194 fail:
195     buffree(hbuf);
196     buffree(buf);
197     return(NULL);
198 }
199
200 static char *defstatus(int code)
201 {
202     if(code == 200)
203         return("OK");
204     else if(code == 201)
205         return("Created");
206     else if(code == 202)
207         return("Accepted");
208     else if(code == 204)
209         return("No Content");
210     else if(code == 300)
211         return("Multiple Choices");
212     else if(code == 301)
213         return("Moved Permanently");
214     else if(code == 302)
215         return("Found");
216     else if(code == 303)
217         return("See Other");
218     else if(code == 304)
219         return("Not Modified");
220     else if(code == 307)
221         return("Moved Temporarily");
222     else if(code == 400)
223         return("Bad Request");
224     else if(code == 401)
225         return("Unauthorized");
226     else if(code == 403)
227         return("Forbidden");
228     else if(code == 404)
229         return("Not Found");
230     else if(code == 500)
231         return("Internal Server Error");
232     else if(code == 501)
233         return("Not Implemented");
234     else if(code == 503)
235         return("Service Unavailable");
236     else
237         return("Unknown status");
238 }
239
240 static void sendstatus(char **headers, FILE *out)
241 {
242     char **hp;
243     char *status, *location;
244     
245     hp = headers;
246     status = location = NULL;
247     while(*hp) {
248         if(!strcasecmp(hp[0], "status")) {
249             status = hp[1];
250             /* Clear this header, so that it is not transmitted by sendheaders. */
251             **hp = 0;
252         } else if(!strcasecmp(hp[0], "location")) {
253             location = hp[1];
254             hp += 2;
255         } else {
256             hp += 2;
257         }
258     }
259     if(status) {
260         if(strchr(status, ' '))
261             fprintf(out, "HTTP/1.1 %s\n", status);
262         else
263             fprintf(out, "HTTP/1.1 %i %s\n", atoi(status), defstatus(atoi(status)));
264     } else if(location) {
265         fprintf(out, "HTTP/1.1 303 See Other\n");
266     } else {
267         fprintf(out, "HTTP/1.1 200 OK\n");
268     }
269 }
270
271 static void sendheaders(char **headers, FILE *out)
272 {
273     while(*headers) {
274         if(**headers)
275             fprintf(out, "%s: %s\n", headers[0], headers[1]);
276         headers += 2;
277     }
278 }
279
280 static void usage(void)
281 {
282     flog(LOG_ERR, "usage: callcgi [-p PROGRAM] METHOD URL REST");
283 }
284
285 int main(int argc, char **argv, char **envp)
286 {
287     int c;
288     char *file, *prog;
289     int inpath;
290     int infd, outfd;
291     FILE *in, *out;
292     char **headers;
293     
294     environ = envp;
295     signal(SIGPIPE, SIG_IGN);
296     
297     prog = NULL;
298     inpath = 0;
299     while((c = getopt(argc, argv, "p:")) >= 0) {
300         switch(c) {
301         case 'p':
302             prog = optarg;
303             inpath = 1;
304             break;
305         default:
306             usage();
307             exit(1);
308         }
309     }
310     
311     if(argc - optind < 3) {
312         usage();
313         exit(1);
314     }
315     if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
316         flog(LOG_ERR, "callcgi: needs to be called with the X-Ash-File header");
317         exit(1);
318     }
319     if(prog == NULL)
320         prog = file;
321     forkchild(inpath, prog, file, argv[optind], argv[optind + 1], argv[optind + 2], &infd, &outfd);
322     in = fdopen(infd, "w");
323     passdata(stdin, in);
324     fclose(in);
325     out = fdopen(outfd, "r");
326     if((headers = parseheaders(out)) == NULL) {
327         flog(LOG_WARNING, "CGI handler returned invalid headers");
328         exit(1);
329     }
330     sendstatus(headers, stdout);
331     sendheaders(headers, stdout);
332     printf("\n");
333     passdata(out, stdout);
334     return(0);
335 }