httrcall: Implemented optional limiting of child processes.
[ashd.git] / src / callscgi.c
CommitLineData
f89ce57a
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/*
20 * XXX: All the various ways to start a child process makes this
21 * program quite ugly at the moment. It is unclear whether it is
22 * meaningfully possible to unify them better than they currently are.
23 */
24
25#include <stdlib.h>
26#include <stdio.h>
27#include <unistd.h>
28#include <string.h>
29#include <fcntl.h>
30#include <ctype.h>
31#include <sys/socket.h>
32#include <sys/un.h>
33#include <netinet/in.h>
34#include <netdb.h>
cc5adcf9 35#include <signal.h>
f89ce57a
FT
36#include <errno.h>
37
38#ifdef HAVE_CONFIG_H
39#include <config.h>
40#endif
41#include <utils.h>
42#include <req.h>
62e76c42 43#include <resp.h>
f89ce57a
FT
44#include <log.h>
45#include <mt.h>
46#include <mtio.h>
47
48static char **progspec;
49static char *sockid, *unspec, *inspec;
50static int nolisten;
51static struct sockaddr *curaddr;
52static size_t caddrlen;
53static int cafamily, isanon;
54static pid_t child;
55
56static struct addrinfo *resolv(int flags)
57{
58 int ret;
59 struct addrinfo *ai, h;
60 char *name, *srv, *p;
61
62 if((p = strchr(inspec, ':')) != NULL) {
63 name = smalloc(p - inspec + 1);
64 memcpy(name, inspec, p - inspec);
65 name[p - inspec] = 0;
66 srv = p + 1;
67 } else {
68 name = sstrdup("localhost");
69 srv = inspec;
70 }
71 memset(&h, 0, sizeof(h));
72 h.ai_family = AF_UNSPEC;
73 h.ai_socktype = SOCK_STREAM;
74 h.ai_flags = flags;
75 ret = getaddrinfo(name, srv, &h, &ai);
76 free(name);
77 if(ret != 0) {
78 flog(LOG_ERR, "could not resolve TCP specification `%s': %s", inspec, gai_strerror(ret));
79 exit(1);
80 }
81 return(ai);
82}
83
84static char *mksockid(char *sockid)
85{
86 char *home;
87
88 home = getenv("HOME");
89 if(home && !access(sprintf3("%s/.ashd/sockets/", home), X_OK))
90 return(sprintf3("%s/.ashd/sockets/scgi-p-%s", home, sockid));
91 return(sprintf3("/tmp/scgi-%i-%s", getuid(), sockid));
92}
93
94static char *mkanonid(void)
95{
96 char *home;
97 char *tmpl;
98 int fd;
99
100 home = getenv("HOME");
101 if(home && !access(sprintf3("%s/.ashd/sockets/", home), X_OK))
1376c0cd 102 tmpl = sprintf2("%s/.ashd/sockets/scgi-a-XXXXXX", home);
f89ce57a
FT
103 else
104 tmpl = sprintf2("/tmp/scgi-a-%i-XXXXXX", getuid());
105 if((fd = mkstemp(tmpl)) < 0) {
106 flog(LOG_ERR, "could not create anonymous socket `%s': %s", tmpl, strerror(errno));
107 exit(1);
108 }
1376c0cd 109 close(fd);
f89ce57a
FT
110 unlink(tmpl);
111 return(tmpl);
112}
113
114static void startlisten(void)
115{
116 int i, fd;
117 struct addrinfo *ai, *cai;
118 char *unpath;
119 struct sockaddr_un unm;
120 char *aname;
121
122 isanon = 0;
123 if(inspec != NULL) {
124 fd = -1;
125 for(cai = ai = resolv(AI_PASSIVE); cai != NULL; cai = cai->ai_next) {
126 if((fd = socket(cai->ai_family, cai->ai_socktype, cai->ai_protocol)) < 0)
127 continue;
128 if(bind(fd, cai->ai_addr, cai->ai_addrlen)) {
129 close(fd);
130 fd = -1;
131 continue;
132 }
133 if(listen(fd, 128)) {
134 close(fd);
135 fd = -1;
136 continue;
137 }
138 break;
139 }
140 freeaddrinfo(ai);
141 if(fd < 0) {
142 flog(LOG_ERR, "could not bind to specified TCP address: %s", strerror(errno));
143 exit(1);
144 }
145 } else if((unspec != NULL) || (sockid != NULL)) {
146 if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
147 flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
148 exit(1);
149 }
150 if(unspec != NULL)
151 unpath = unspec;
152 else
153 unpath = mksockid(sockid);
154 unlink(unpath);
155 unm.sun_family = AF_UNIX;
156 strcpy(unm.sun_path, unpath);
157 if(bind(fd, (struct sockaddr *)&unm, sizeof(unm))) {
158 flog(LOG_ERR, "could not bind Unix socket to `%s': %s", unspec, strerror(errno));
159 exit(1);
160 }
161 if(listen(fd, 128)) {
162 flog(LOG_ERR, "listen: %s", strerror(errno));
163 exit(1);
164 }
165 } else {
166 if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
167 flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
168 exit(1);
169 }
170 memset(&unm, 0, sizeof(unm));
171 aname = mkanonid();
172 unm.sun_family = AF_UNIX;
173 strcpy(unm.sun_path, aname);
174 free(aname);
175 if(bind(fd, (struct sockaddr *)&unm, sizeof(unm))) {
176 flog(LOG_ERR, "could not bind Unix socket to `%s': %s", unspec, strerror(errno));
177 exit(1);
178 }
179 if(listen(fd, 128)) {
180 flog(LOG_ERR, "listen: %s", strerror(errno));
181 exit(1);
182 }
183
184 curaddr = smalloc(caddrlen = sizeof(unm));
185 memcpy(curaddr, &unm, sizeof(unm));
186 cafamily = AF_UNIX;
187 isanon = 1;
188 }
189 if((child = fork()) < 0) {
190 flog(LOG_ERR, "could not fork: %s", strerror(errno));
191 exit(1);
192 }
193 if(child == 0) {
194 dup2(fd, 0);
195 for(i = 3; i < FD_SETSIZE; i++)
196 close(i);
197 execvp(*progspec, progspec);
7b9c1cff 198 flog(LOG_ERR, "callscgi: %s: %s", *progspec, strerror(errno));
107aa2ef 199 _exit(127);
f89ce57a
FT
200 }
201 close(fd);
202}
203
204static void startnolisten(void)
205{
02cb2a3d 206 int i, fd;
f89ce57a
FT
207
208 if((child = fork()) < 0) {
209 flog(LOG_ERR, "could not fork: %s", strerror(errno));
210 exit(1);
211 }
212 if(child == 0) {
213 for(i = 3; i < FD_SETSIZE; i++)
214 close(i);
02cb2a3d
FT
215 if((fd = open("/dev/null", O_RDONLY)) < 0) {
216 flog(LOG_ERR, "/dev/null: %s", strerror(errno));
107aa2ef 217 _exit(127);
02cb2a3d
FT
218 }
219 dup2(fd, 0);
220 close(fd);
f89ce57a 221 execvp(*progspec, progspec);
7b9c1cff 222 flog(LOG_ERR, "callscgi: %s: %s", *progspec, strerror(errno));
107aa2ef 223 _exit(127);
f89ce57a
FT
224 }
225}
226
227static int sconnect(void)
228{
229 int fd;
230 int err;
231 socklen_t errlen;
232
233 fd = socket(cafamily, SOCK_STREAM, 0);
234 fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
235 while(1) {
236 if(connect(fd, curaddr, caddrlen)) {
237 if(errno == EINPROGRESS) {
238 block(fd, EV_WRITE, 30);
239 errlen = sizeof(err);
240 if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) || ((errno = err) != 0)) {
241 close(fd);
242 return(-1);
243 }
244 return(fd);
245 }
246 close(fd);
247 return(-1);
248 }
249 return(fd);
250 }
251}
252
253static int econnect(void)
254{
255 int fd;
256 struct addrinfo *ai, *cai;
257 int tries;
258 char *unpath;
259 struct sockaddr_un unm;
260
261 tries = 0;
262retry:
263 if(inspec != NULL) {
264 fd = -1;
265 for(cai = ai = resolv(0); cai != NULL; cai = cai->ai_next) {
266 if((fd = socket(cai->ai_family, cai->ai_socktype, cai->ai_protocol)) < 0)
267 continue;
268 if(connect(fd, cai->ai_addr, cai->ai_addrlen)) {
269 close(fd);
270 fd = -1;
271 continue;
272 }
273 break;
274 }
f89ce57a 275 if(fd < 0) {
02cb2a3d
FT
276 if(tries++ < nolisten) {
277 sleep(1);
278 goto retry;
279 }
f89ce57a
FT
280 flog(LOG_ERR, "could not connect to specified TCP address: %s", strerror(errno));
281 exit(1);
282 }
283 curaddr = smalloc(caddrlen = cai->ai_addrlen);
284 memcpy(curaddr, cai->ai_addr, caddrlen);
285 cafamily = cai->ai_family;
286 isanon = 0;
287 freeaddrinfo(ai);
288 return(fd);
289 } else if((unspec != NULL) || (sockid != NULL)) {
290 if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
291 flog(LOG_ERR, "could not create Unix socket: %s", strerror(errno));
292 exit(1);
293 }
294 if(unspec != NULL)
295 unpath = unspec;
296 else
297 unpath = mksockid(sockid);
298 unlink(unpath);
299 unm.sun_family = AF_UNIX;
300 strcpy(unm.sun_path, unpath);
301 if(connect(fd, (struct sockaddr *)&unm, sizeof(unm))) {
302 close(fd);
303 if(tries++ < nolisten) {
304 sleep(1);
305 goto retry;
306 }
307 flog(LOG_ERR, "could not connect to Unix socket `%s': %s", unspec, strerror(errno));
308 exit(1);
309 }
310 curaddr = smalloc(caddrlen = sizeof(unm));
311 memcpy(curaddr, &unm, sizeof(unm));
312 cafamily = AF_UNIX;
313 isanon = 0;
314 return(fd);
315 } else {
425eccf6 316 flog(LOG_ERR, "callscgi: cannot use an anonymous socket without a program to start");
f89ce57a
FT
317 exit(1);
318 }
319}
320
321static int startconn(void)
322{
323 if(*progspec) {
324 if(nolisten == 0)
325 startlisten();
326 else
327 startnolisten();
328 }
329 if(curaddr != NULL)
330 return(sconnect());
331 return(econnect());
332}
333
334static void killcuraddr(void)
335{
336 if(curaddr == NULL)
337 return;
338 if(isanon) {
339 unlink(((struct sockaddr_un *)curaddr)->sun_path);
340 if(child > 0)
341 kill(child, SIGTERM);
342 }
343 free(curaddr);
344 curaddr = NULL;
345}
346
347static int reconn(void)
348{
349 int fd;
350
351 if(curaddr != NULL) {
352 if((fd = sconnect()) >= 0)
353 return(fd);
354 killcuraddr();
355 }
356 return(startconn());
357}
358
359static off_t passdata(FILE *in, FILE *out)
360{
361 size_t read;
362 off_t total;
363 char buf[8192];
364
365 total = 0;
366 while(!feof(in)) {
367 read = fread(buf, 1, sizeof(buf), in);
368 if(ferror(in))
369 return(-1);
370 if(fwrite(buf, 1, read, out) != read)
371 return(-1);
372 total += read;
373 }
374 return(total);
375}
376
377static void bufaddenv(struct charbuf *dst, char *name, char *fmt, ...)
378{
379 va_list args;
380
381 bufcatstr2(*dst, name);
382 va_start(args, fmt);
383 bvprintf(dst, fmt, args);
384 va_end(args);
385 bufadd(*dst, 0);
386}
387
388static char *absolutify(char *file)
389{
390 static int inited = 0;
391 static char cwd[1024];
392
393 if(*file != '/') {
394 if(!inited) {
395 getcwd(cwd, sizeof(cwd));
396 inited = 1;
397 }
398 return(sprintf2("%s/%s", cwd, file));
399 }
400 return(sstrdup(file));
401}
402
403/* Mostly copied from callcgi. */
404static void mkcgienv(struct hthead *req, struct charbuf *dst)
405{
406 int i;
53d666ca 407 char *url, *pi, *tmp, *qp, *h, *p;
f89ce57a
FT
408
409 bufaddenv(dst, "SERVER_SOFTWARE", "ashd/%s", VERSION);
410 bufaddenv(dst, "GATEWAY_INTERFACE", "CGI/1.1");
411 bufaddenv(dst, "SCGI", "1");
412 bufaddenv(dst, "SERVER_PROTOCOL", "%s", req->ver);
413 bufaddenv(dst, "REQUEST_METHOD", "%s", req->method);
414 bufaddenv(dst, "REQUEST_URI", "%s", req->url);
f89ce57a
FT
415 url = sstrdup(req->url);
416 if((qp = strchr(url, '?')) != NULL)
417 *(qp++) = 0;
418 /* XXX: This is an ugly hack (I think), but though I can think of
419 * several alternatives, none seem to be better. */
2d65add0
FT
420 if(*req->rest && (strlen(url) >= strlen(req->rest)) &&
421 !strcmp(req->rest, url + strlen(url) - strlen(req->rest))) {
53d666ca
FT
422 url[strlen(url) - strlen(req->rest)] = 0;
423 }
424 if((pi = unquoteurl(req->rest)) == NULL)
425 pi = sstrdup(req->rest);
426 if(!strcmp(url, "/")) {
427 /* This seems to be normal CGI behavior, but see callcgi.c for
428 * details. */
429 url[0] = 0;
430 pi = sprintf2("/%s", tmp = pi);
431 free(tmp);
f89ce57a 432 }
53d666ca
FT
433 bufaddenv(dst, "PATH_INFO", pi);
434 bufaddenv(dst, "SCRIPT_NAME", url);
f89ce57a 435 bufaddenv(dst, "QUERY_STRING", "%s", qp?qp:"");
53d666ca 436 free(pi);
b77076c4 437 free(url);
f89ce57a
FT
438 if((h = getheader(req, "Host")) != NULL)
439 bufaddenv(dst, "SERVER_NAME", "%s", h);
1af656d2
FT
440 if((h = getheader(req, "X-Ash-Server-Address")) != NULL)
441 bufaddenv(dst, "SERVER_ADDR", "%s", h);
f89ce57a
FT
442 if((h = getheader(req, "X-Ash-Server-Port")) != NULL)
443 bufaddenv(dst, "SERVER_PORT", "%s", h);
381f9919
FT
444 if((h = getheader(req, "X-Ash-Remote-User")) != NULL)
445 bufaddenv(dst, "REMOTE_USER", "%s", h);
9230295c 446 if(((h = getheader(req, "X-Ash-Protocol")) != NULL) && !strcmp(h, "https"))
f89ce57a
FT
447 bufaddenv(dst, "HTTPS", "on");
448 if((h = getheader(req, "X-Ash-Address")) != NULL)
449 bufaddenv(dst, "REMOTE_ADDR", "%s", h);
1af656d2
FT
450 if((h = getheader(req, "X-Ash-Port")) != NULL)
451 bufaddenv(dst, "REMOTE_PORT", "%s", h);
f89ce57a
FT
452 if((h = getheader(req, "Content-Type")) != NULL)
453 bufaddenv(dst, "CONTENT_TYPE", "%s", h);
454 if((h = getheader(req, "Content-Length")) != NULL)
455 bufaddenv(dst, "CONTENT_LENGTH", "%s", h);
456 else
457 bufaddenv(dst, "CONTENT_LENGTH", "0");
e25bf4f9
FT
458 if((h = getheader(req, "X-Ash-File")) != NULL) {
459 h = absolutify(h);
460 bufaddenv(dst, "SCRIPT_FILENAME", "%s", h);
461 free(h);
462 }
f89ce57a 463 for(i = 0; i < req->noheaders; i++) {
02cb2a3d 464 h = sprintf2("HTTP_%s", req->headers[i][0]);
f89ce57a
FT
465 for(p = h; *p; p++) {
466 if(isalnum(*p))
467 *p = toupper(*p);
468 else
469 *p = '_';
470 }
471 bufcatstr2(*dst, h);
472 free(h);
473 bufcatstr2(*dst, req->headers[i][1]);
474 }
475}
476
f89ce57a
FT
477static struct hthead *parseresp(FILE *in)
478{
479 struct hthead *resp;
480 char *st, *p;
481
482 omalloc(resp);
483 resp->ver = sstrdup("HTTP/1.1");
484 if(parseheaders(resp, in)) {
485 freehthead(resp);
486 return(NULL);
487 }
488 if((st = getheader(resp, "Status")) != NULL) {
489 if((p = strchr(st, ' ')) != NULL) {
490 *(p++) = 0;
491 resp->code = atoi(st);
492 resp->msg = sstrdup(p);
493 } else {
494 resp->code = atoi(st);
62e76c42 495 resp->msg = sstrdup(httpdefstatus(resp->code));
f89ce57a
FT
496 }
497 headrmheader(resp, "Status");
498 } else if(getheader(resp, "Location")) {
499 resp->code = 303;
500 resp->msg = sstrdup("See Other");
501 } else {
502 resp->code = 200;
503 resp->msg = sstrdup("OK");
504 }
505 return(resp);
506}
507
508static void serve(struct muth *muth, va_list args)
509{
510 vavar(struct hthead *, req);
511 vavar(int, fd);
512 vavar(int, sfd);
513 FILE *is, *os;
514 struct charbuf head;
515 struct hthead *resp;
516
517 sfd = reconn();
518 is = mtstdopen(fd, 1, 60, "r+");
519 os = mtstdopen(sfd, 1, 600, "r+");
520
521 bufinit(head);
522 mkcgienv(req, &head);
523 fprintf(os, "%zi:", head.d);
524 fwrite(head.b, head.d, 1, os);
525 fputc(',', os);
526 buffree(head);
527 if(passdata(is, os) < 0)
528 goto out;
529
530 if((resp = parseresp(os)) == NULL)
531 goto out;
532 writeresp(is, resp);
533 freehthead(resp);
534 fputc('\n', is);
535 if(passdata(os, is) < 0)
536 goto out;
537
538out:
539 freehthead(req);
540 fclose(is);
541 fclose(os);
542}
543
544static void listenloop(struct muth *muth, va_list args)
545{
546 vavar(int, lfd);
547 int fd;
548 struct hthead *req;
549
550 while(1) {
551 block(0, EV_READ, 0);
552 if((fd = recvreq(lfd, &req)) < 0) {
553 if(errno != 0)
554 flog(LOG_ERR, "recvreq: %s", strerror(errno));
555 break;
556 }
557 mustart(serve, req, fd);
558 }
559}
560
fd735432
FT
561static void sigign(int sig)
562{
563}
564
9c86e336
FT
565static void sigexit(int sig)
566{
254eb937 567 shutdown(0, SHUT_RDWR);
9c86e336
FT
568}
569
f89ce57a
FT
570static void usage(FILE *out)
571{
425eccf6 572 fprintf(out, "usage: callscgi [-h] [-N RETRIES] [-i ID] [-u UNIX-PATH] [-t [HOST:]TCP-PORT] [PROGRAM [ARGS...]]\n");
f89ce57a
FT
573}
574
575int main(int argc, char **argv)
576{
577 int c;
578
579 while((c = getopt(argc, argv, "+hN:i:u:t:")) >= 0) {
580 switch(c) {
581 case 'h':
582 usage(stdout);
583 exit(0);
584 case 'N':
585 nolisten = atoi(optarg);
586 break;
587 case 'i':
588 sockid = optarg;
589 break;
590 case 'u':
591 unspec = optarg;
592 break;
593 case 't':
594 inspec = optarg;
595 break;
596 default:
597 usage(stderr);
598 exit(1);
599 }
600 }
601 progspec = argv + optind;
602 if(((sockid != NULL) + (unspec != NULL) + (inspec != NULL)) > 1) {
425eccf6 603 flog(LOG_ERR, "callscgi: at most one of -i, -u or -t may be given");
f89ce57a
FT
604 exit(1);
605 }
606 signal(SIGCHLD, SIG_IGN);
fd735432 607 signal(SIGPIPE, sigign);
9c86e336
FT
608 signal(SIGINT, sigexit);
609 signal(SIGTERM, sigexit);
f89ce57a
FT
610 mustart(listenloop, 0);
611 atexit(killcuraddr);
612 ioloop();
613 return(0);
614}