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