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