chiark / gitweb /
Break much of cgi-fcgi-interp.c out into prefork.[ch]
[chiark-utils.git] / cprogs / cgi-fcgi-interp.c
1 /*
2  * "Interpreter" that you can put in #! like this
3  *   #!/usr/bin/cgi-fcgi-interp [<options>] <interpreter>
4  */
5 /*
6  * cgi-fcgi-interp.[ch] - Convenience wrapper for cgi-fcgi
7  *
8  * Copyright 2016 Ian Jackson
9  * Copyright 1982,1986,1993 The Regents of the University of California
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation; either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public
22  * License along with this file; if not, consult the Free Software
23  * Foundation's website at www.fsf.org, or the GNU Project website at
24  * www.gnu.org.
25  *
26  * See below for a BSD 3-clause notice regarding timespeccmp.
27  */
28 /*
29  * The result is a program which looks, when executed via the #!
30  * line, like a CGI program.  But the script inside will be executed
31  * via <interpreter> in an fcgi context.
32  *
33  * Options:
34  *
35  *  <interpreter>
36  *          The real interpreter to use.  Eg "perl".  Need not
37  *          be an absolute path; will be fed to execvp.
38  *
39  *  -G<ident-info>
40  *          Add <ident-info> to the unique identifying information for
41  *          this fcgi program.  May be repeated; order is significant.
42  *
43  *  -E<ident-info-env-var>
44  *          Look <ident-info-env-var> up in the environment and add
45  *          <ident-info-env-var>=<value> as if specified with -G.  If
46  *          the variable is unset in the environment, it is as if
47  *          -G<ident-info-env-var> was specified.
48  *
49  *  -g<ident>
50  *          Use <ident> rather than hex(sha256(<interp>\0<script>\0))
51  *          as the basename of the leafname of the fcgi rendezvous
52  *          socket.  If <ident> contains only hex digit characters it
53  *          ought to be no more than 32 characters.  <ident> should
54  *          not contain spaces or commas (see below).
55  *
56  *  -M<numservers>
57  *         Start <numservers> instances of the program.  This
58  *         determines the maximum concurrency.  (Note that unlike
59  *         speedy, the specified number of servers is started
60  *         right away.)  The default is 4.
61  *
62  *  -c<interval>
63  *         Stale server check interval, in seconds.  The worker
64  *         process group will get a SIGTERM when it is no longer
65  *         needed to process new requests.  Ideally it would continue
66  *         to serve any existing requests.  The SIGTERM will arrive no
67  *         earlier than <interval> after the last request arrived at
68  *         the containing webserver.  Default is 300.
69  *
70  *  -D
71  *         Debug mode.  Do not actually run program.  Instead, print
72  *         out what we would do.
73  *
74  * <options> and <interpreter> can be put into a single argument
75  * to cgi-fcgi-interp, separated by spaces or commas.  <interpreter>
76  * must come last.
77  *
78  * cgi-fcgi-interp automatically expires old sockets, including
79  * ones where the named script is out of date.
80  */
81 /*
82  * Uses one of two directories
83  *   /var/run/user/<UID>/cgi-fcgi-interp/
84  *   ~/.cgi-fcgi-interp/<node>/
85  * and inside there uses these paths
86  *   s<ident>
87  *   l<ident>    used to lock around garbage collection
88  *
89  * If -M<ident> is not specified then an initial substring of the
90  * lowercase hex of the sha256 of <interp>\0<script>\0 is
91  * used.  The substring is chosen so that the whole path is 10 bytes
92  * shorter than sizeof(sun_path).  But always at least 33 characters.
93  *
94  * <node> is truncated at the first `.' and after the first 32
95  * characters.
96  *
97  * Algorithm:
98  *  - see if /var/run/user exists
99  *       if so, lstat /var/run/user/<UID> and check that
100  *         we own it and it's X700; if not, fail
101  *         if it's ok then <base> is /var/run/user/<UID>
102  *       otherwise, look for and maybe create ~/.cgi-fcgi-interp
103  *         (where ~ is HOME or from getpwuid)
104  *         and then <base> is ~/.cgi-fcgi-interp/<node>
105  *  - calculate pathname (checking <ident> length is OK)
106  *  - check for and maybe create <base>
107  *  - stat and lstat the <script>
108  *  - stat the socket and check its timestamp
109  *       if it is too old, unlink it
110  *  - dup stderr, mark no cloexec
111  *  - set CHIARKUTILS_CGIFCGIINTERP_STAGE2=<stderr-copy-fd>
112  *  - run     cgi-fcgi -connect SOCKET <script>
113  *
114  * When CHIARKUTILS_CGIFCGIINTERP_STAGE2 is set, --stage2 does this:
115  *  - dup2 <was-stderr> to fd 2
116  *  - open /dev/null and expect fd 1 (and if not, close it)
117  *  - become a new process group
118  *  - lstat <socket> to find its inum, mtime
119  *  - fork/exec <interp> <script>
120  *  - periodically lstat <interp> and <script> and
121  *      if mtime is newer than our start time
122  *      kill process group (at second iteration)
123  */
124
125 #include "prefork.h"
126
127 static const char *stage2;
128
129 const struct cmdinfo cmdinfos[]= {
130   { "help",   0, .call=of_help                                         },
131   { 0, 'g',   1,                    .sassignto= &ident                 },
132   { 0, 'G',   1, .call= ident_addstring                                },
133   { 0, 'E',   1, .call= off_ident_addenv                               },
134   { 0, 'M',   1, .call=of_iassign,  .iassignto= &numservers            },
135   { 0, 'D',   0,                    .iassignto= &debugmode,    .arg= 1 },
136   { 0, 'c',   1, .call=of_iassign,  .iassignto= &check_interval        },
137   { 0 }
138 };
139
140 void fusagemessage(FILE *f) {
141   fprintf(f, "usage: #!/usr/bin/cgi-fcgi-interp [<options>]\n");
142 }
143
144 static int stderr_copy;
145
146 static void make_stderr_copy(void) {
147   stderr_copy = dup(2);
148   if (stderr_copy < 0) diee("dup stderr (for copy for stage2)");
149 }
150
151 static void prep_stage2(void) {
152   int r;
153   
154   const char *stage2_val = m_asprintf("%d", stderr_copy);
155   r = setenv(STAGE2_VAR, stage2_val, 1);
156   if (r) diee("set %s (to announce to stage2)", STAGE2_VAR);
157 }
158
159 /* stage2 predeclarations */
160 static void record_baseline_time(void);
161 static void become_pgrp(void);
162 static void setup_handlers(void);
163 static void spawn_script(void);
164 static void queue_alarm(void);
165 static void start_logging(void);
166 static void await_something(void);
167
168 int main(int argc, const char *const *argv) {
169   int r;
170
171   stage2 = getenv(STAGE2_VAR);
172   if (stage2) {
173     int stderrfd = atoi(stage2);
174     assert(stderrfd>2);
175
176     r = dup2(stderrfd, 2);
177     assert(r==2);
178
179     r = open("/dev/null",O_WRONLY);
180     if (r<0) diee("open /dev/null as stdout");
181     if (r>=3) close(r);
182     else if (r!=1) die("open /dev/null for stdout gave bad fd %d",r);
183
184     r = close(stderrfd);
185     if (r) diee("close saved stderr fd");
186   }
187
188   script = process_opts(argc, argv);
189
190   if (!stage2) {
191     
192     find_socket_path();
193
194     bool isgarbage = check_garbage();
195
196     if (debugmode) {
197       printf("socket: %s\n",socket_path);
198       printf("interp: %s\n",interp);
199       printf("script: %s\n",script);
200       printf("garbage: %d\n",isgarbage);
201       exit(0);
202     }
203
204     if (isgarbage)
205       tidy_garbage();
206
207     make_stderr_copy();
208     prep_stage2();
209
210     execlp("cgi-fcgi",
211            "cgi-fcgi", "-connect", socket_path,
212            script,
213            m_asprintf("%d", numservers),
214            (char*)0);
215     diee("exec cgi-fcgi");
216     
217   } else { /*stage2*/
218
219     record_baseline_time();
220     become_pgrp();
221     setup_handlers();
222     spawn_script();
223     queue_alarm();
224     start_logging();
225     await_something();
226     abort();
227
228   }
229 }
230
231 /* stage2 */
232
233 /* It is most convenient to handle the recheck timeout, as well as
234  * child death, in signal handlers.  Our signals all block each other,
235  * and the main program has signals blocked except in sigsuspend, so
236  * we don't need to worry about async-signal-safety, or errno. */
237
238 static struct stat baseline_time;
239 static pid_t script_child, stage2_pgrp;
240 static bool out_of_date;
241 static int errpipe;
242
243 static void record_baseline_time(void) {
244   stab_mtimenow(&baseline_time);
245 }
246
247 static void become_pgrp(void) {
248   int r;
249
250   stage2_pgrp = getpid();
251
252   r = setpgid(0,0);
253   if (r) diee("(stage2) setpgid");
254 }
255
256 static void atexit_handler(void) {
257   int r;
258
259   sighandler_t sigr = signal(SIGTERM,SIG_IGN);
260   if (sigr == SIG_ERR) warninge("(stage2) signal(SIGTERM,SIG_IGN)");
261
262   r = killpg(stage2_pgrp,SIGTERM);
263   if (r) warninge("(stage) killpg failed");
264 }
265
266 static void alarm_handler(int dummy) {
267   if (out_of_date)
268     /* second timeout */
269     exit(0); /* transfers control to atexit_handler */
270
271   out_of_date = check_garbage_vs(&baseline_time);
272   queue_alarm();
273 }
274
275 static void child_handler(int dummy) {
276   for (;;) {
277     int status;
278     pid_t got = waitpid(-1, &status, WNOHANG);
279     if (got == (pid_t)-1) diee("(stage2) waitpid");
280     if (got != script_child) {
281       warning("(stage2) waitpid got status %d for unknown child [%lu]",
282               status, (unsigned long)got);
283       continue;
284     }
285     if (WIFEXITED(status)) {
286       int v = WEXITSTATUS(status);
287       if (v) warning("program failed with error exit status %d", v);
288       exit(status);
289     } else if (WIFSIGNALED(status)) {
290       int s = WTERMSIG(status);
291       warning("program died due to fatal signal %s%s",
292               strsignal(s), WCOREDUMP(status) ? " (core dumped" : "");
293       assert(status & 0xff);
294       exit(status & 0xff);
295     } else {
296       die("program failed with crazy wait status %#x", status);
297     }
298   }
299   exit(127);
300 }
301
302 static void setup_handlers(void) {
303   struct sigaction sa;
304   int r;
305
306   r = atexit(atexit_handler);
307   if (r) diee("(stage2) atexit");
308
309   sigemptyset(&sa.sa_mask);
310   sigaddset(&sa.sa_mask, SIGALRM);
311   sigaddset(&sa.sa_mask, SIGCHLD);
312   sa.sa_flags = 0;
313
314   r = sigprocmask(SIG_BLOCK, &sa.sa_mask, 0);
315   if (r) diee("(stage2) sigprocmask(SIG_BLOCK,)");
316
317   sa.sa_handler = alarm_handler;
318   r = sigaction(SIGALRM, &sa, 0);
319   if (r) diee("(stage2) sigaction SIGALRM");
320
321   sa.sa_flags |= SA_NOCLDSTOP;
322   sa.sa_handler = child_handler;
323   r = sigaction(SIGCHLD, &sa, 0);
324   if (r) diee("(stage2) sigaction SIGCHLD");
325 }
326
327 static void spawn_script(void) {
328   int r;
329   int errpipes[2];
330
331   r = pipe(errpipes);
332   if (r) diee("(stage2) pipe");
333
334   script_child = fork();
335   if (script_child == (pid_t)-1) diee("(stage2) fork");
336   if (!script_child) {
337     r = close(errpipes[0]);
338     if (r) diee("(stage2 child) close errpipes[0]");
339
340     r = dup2(errpipes[1], 2);
341     if (r != 2) diee("(stage2 child) dup2 stderr");
342
343     execlp(interp,
344            interp, script, (char*)0);
345     diee("(stage2) exec interpreter (`%s', for `%s')\n",interp,script);
346   }
347
348   r = close(errpipes[1]);
349   if (r) diee("(stage2) close errpipes[1]");
350
351   errpipe = errpipes[0];
352   r = fcntl(errpipe, F_SETFL, O_NONBLOCK);
353   if (r) diee("(stage2) set errpipe nonblocking");
354 }
355
356 static void queue_alarm(void) {
357   alarm(check_interval);
358 }
359
360 static void start_logging(void) {
361   int r;
362
363   openlog(script, LOG_NOWAIT|LOG_PID, LOG_USER);
364   logging = 1;
365   r = dup2(1,2);
366   if (r!=2) diee("dup2 stdout to stderr");
367 }
368
369 static void errpipe_readable(void) {
370   static char buf[1024];
371   static int pending;
372
373   /* %: does not contain newlines
374    * _: empty (garbage)
375    */ 
376
377   /*           %%%%%%%%%%%__________________ */
378   /*                      ^ pending          */
379
380   for (;;) {
381     int avail = sizeof(buf) - pending;
382     ssize_t got = read(errpipe, buf+pending, avail);
383     if (got==-1) {
384       if (errno==EINTR) continue;
385       else if (errno==EWOULDBLOCK || errno==EAGAIN) return;
386       else diee("(stage2) errpipe read");
387       got = 0;
388     } else if (got==0) {
389       warning("program closed its stderr fd");
390       errpipe = -1;
391       return;
392     }
393     int scanned = pending;
394     pending += got;
395     int eaten = 0;
396     for (;;) {
397       const char *newline = memchr(buf+scanned, '\n', pending-scanned);
398       int printupto, eat;
399       if (newline) {
400         printupto = newline-buf;
401         eat = printupto + 1;
402       } else if (!eaten && pending==sizeof(buf)) { /* overflow */
403         printupto = pending;
404         eat = printupto;
405       } else {
406         break;
407       }
408       syslog(LOG_ERR,"stderr: %.*s", printupto-eaten, buf+eaten);
409       eaten += eat;
410       scanned = eaten;
411     }
412     pending -= eaten;
413     memmove(buf, buf+eaten, pending);
414   }
415 }     
416
417 static void await_something(void) {
418   int r;
419   sigset_t mask;
420   sigemptyset(&mask);
421
422   for (;;) {
423     fd_set rfds;
424     FD_ZERO(&rfds);
425     if (errpipe >= 0)
426       FD_SET(errpipe, &rfds);
427     r = pselect(errpipe+1, &rfds,0,0, 0, &mask);
428     if (r==-1) {
429       if (errno != EINTR) diee("(stage2) sigsuspend");
430       continue;
431     }
432     assert(r>0);
433     assert(FD_ISSET(errpipe, &rfds));
434     errpipe_readable();
435   }
436 }