chiark / gitweb /
prefork: Move STAGE2_VAR back to cgi-fcgi-interp
[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 #define STAGE2_VAR "CHIARKUTILS_CGIFCGIINTERP_STAGE2"
128
129 static const char *stage2;
130
131 const char our_name[] = "cgi-fcgi-interp";
132
133 const struct cmdinfo cmdinfos[]= {
134   { "help",   0, .call=of_help                                         },
135   { 0, 'g',   1,                    .sassignto= &ident                 },
136   { 0, 'G',   1, .call= ident_addstring                                },
137   { 0, 'E',   1, .call= off_ident_addenv                               },
138   { 0, 'M',   1, .call=of_iassign,  .iassignto= &numservers            },
139   { 0, 'D',   0,                    .iassignto= &debugmode,    .arg= 1 },
140   { 0, 'c',   1, .call=of_iassign,  .iassignto= &check_interval        },
141   { 0 }
142 };
143
144 void fusagemessage(FILE *f) {
145   fprintf(f, "usage: #!/usr/bin/cgi-fcgi-interp [<options>]\n");
146 }
147
148 static int stderr_copy;
149
150 static void make_stderr_copy(void) {
151   stderr_copy = dup(2);
152   if (stderr_copy < 0) diee("dup stderr (for copy for stage2)");
153 }
154
155 static void prep_stage2(void) {
156   int r;
157   
158   const char *stage2_val = m_asprintf("%d", stderr_copy);
159   r = setenv(STAGE2_VAR, stage2_val, 1);
160   if (r) diee("set %s (to announce to stage2)", STAGE2_VAR);
161 }
162
163 /* stage2 predeclarations */
164 static void record_baseline_time(void);
165 static void become_pgrp(void);
166 static void setup_handlers(void);
167 static void spawn_script(void);
168 static void queue_alarm(void);
169 static void start_logging(void);
170 static void await_something(void);
171
172 int main(int argc, const char *const *argv) {
173   int r;
174
175   stage2 = getenv(STAGE2_VAR);
176   if (stage2) {
177     int stderrfd = atoi(stage2);
178     assert(stderrfd>2);
179
180     r = dup2(stderrfd, 2);
181     assert(r==2);
182
183     r = open("/dev/null",O_WRONLY);
184     if (r<0) diee("open /dev/null as stdout");
185     if (r>=3) close(r);
186     else if (r!=1) die("open /dev/null for stdout gave bad fd %d",r);
187
188     r = close(stderrfd);
189     if (r) diee("close saved stderr fd");
190   }
191
192   script = process_opts(argc, argv);
193
194   if (!stage2) {
195     
196     find_socket_path();
197
198     bool isgarbage = check_garbage();
199
200     if (debugmode) {
201       printf("socket: %s\n",socket_path);
202       printf("interp: %s\n",interp);
203       printf("script: %s\n",script);
204       printf("garbage: %d\n",isgarbage);
205       exit(0);
206     }
207
208     if (isgarbage)
209       tidy_garbage();
210
211     make_stderr_copy();
212     prep_stage2();
213
214     execlp("cgi-fcgi",
215            "cgi-fcgi", "-connect", socket_path,
216            script,
217            m_asprintf("%d", numservers),
218            (char*)0);
219     diee("exec cgi-fcgi");
220     
221   } else { /*stage2*/
222
223     record_baseline_time();
224     become_pgrp();
225     setup_handlers();
226     spawn_script();
227     queue_alarm();
228     start_logging();
229     await_something();
230     abort();
231
232   }
233 }
234
235 /* stage2 */
236
237 /* It is most convenient to handle the recheck timeout, as well as
238  * child death, in signal handlers.  Our signals all block each other,
239  * and the main program has signals blocked except in sigsuspend, so
240  * we don't need to worry about async-signal-safety, or errno. */
241
242 static struct stat baseline_time;
243 static pid_t script_child, stage2_pgrp;
244 static bool out_of_date;
245 static int errpipe;
246
247 static void record_baseline_time(void) {
248   stab_mtimenow(&baseline_time);
249 }
250
251 static void become_pgrp(void) {
252   int r;
253
254   stage2_pgrp = getpid();
255
256   r = setpgid(0,0);
257   if (r) diee("(stage2) setpgid");
258 }
259
260 static void atexit_handler(void) {
261   int r;
262
263   sighandler_t sigr = signal(SIGTERM,SIG_IGN);
264   if (sigr == SIG_ERR) warninge("(stage2) signal(SIGTERM,SIG_IGN)");
265
266   r = killpg(stage2_pgrp,SIGTERM);
267   if (r) warninge("(stage) killpg failed");
268 }
269
270 static void alarm_handler(int dummy) {
271   if (out_of_date)
272     /* second timeout */
273     exit(0); /* transfers control to atexit_handler */
274
275   out_of_date = check_garbage_vs(&baseline_time);
276   queue_alarm();
277 }
278
279 static void child_handler(int dummy) {
280   for (;;) {
281     int status;
282     pid_t got = waitpid(-1, &status, WNOHANG);
283     if (got == (pid_t)-1) diee("(stage2) waitpid");
284     if (got != script_child) {
285       warning("(stage2) waitpid got status %d for unknown child [%lu]",
286               status, (unsigned long)got);
287       continue;
288     }
289     if (WIFEXITED(status)) {
290       int v = WEXITSTATUS(status);
291       if (v) warning("program failed with error exit status %d", v);
292       exit(status);
293     } else if (WIFSIGNALED(status)) {
294       int s = WTERMSIG(status);
295       warning("program died due to fatal signal %s%s",
296               strsignal(s), WCOREDUMP(status) ? " (core dumped" : "");
297       assert(status & 0xff);
298       exit(status & 0xff);
299     } else {
300       die("program failed with crazy wait status %#x", status);
301     }
302   }
303   exit(127);
304 }
305
306 static void setup_handlers(void) {
307   struct sigaction sa;
308   int r;
309
310   r = atexit(atexit_handler);
311   if (r) diee("(stage2) atexit");
312
313   sigemptyset(&sa.sa_mask);
314   sigaddset(&sa.sa_mask, SIGALRM);
315   sigaddset(&sa.sa_mask, SIGCHLD);
316   sa.sa_flags = 0;
317
318   r = sigprocmask(SIG_BLOCK, &sa.sa_mask, 0);
319   if (r) diee("(stage2) sigprocmask(SIG_BLOCK,)");
320
321   sa.sa_handler = alarm_handler;
322   r = sigaction(SIGALRM, &sa, 0);
323   if (r) diee("(stage2) sigaction SIGALRM");
324
325   sa.sa_flags |= SA_NOCLDSTOP;
326   sa.sa_handler = child_handler;
327   r = sigaction(SIGCHLD, &sa, 0);
328   if (r) diee("(stage2) sigaction SIGCHLD");
329 }
330
331 static void spawn_script(void) {
332   int r;
333   int errpipes[2];
334
335   r = pipe(errpipes);
336   if (r) diee("(stage2) pipe");
337
338   script_child = fork();
339   if (script_child == (pid_t)-1) diee("(stage2) fork");
340   if (!script_child) {
341     r = close(errpipes[0]);
342     if (r) diee("(stage2 child) close errpipes[0]");
343
344     r = dup2(errpipes[1], 2);
345     if (r != 2) diee("(stage2 child) dup2 stderr");
346
347     execlp(interp,
348            interp, script, (char*)0);
349     diee("(stage2) exec interpreter (`%s', for `%s')\n",interp,script);
350   }
351
352   r = close(errpipes[1]);
353   if (r) diee("(stage2) close errpipes[1]");
354
355   errpipe = errpipes[0];
356   r = fcntl(errpipe, F_SETFL, O_NONBLOCK);
357   if (r) diee("(stage2) set errpipe nonblocking");
358 }
359
360 static void queue_alarm(void) {
361   alarm(check_interval);
362 }
363
364 static void start_logging(void) {
365   int r;
366
367   openlog(script, LOG_NOWAIT|LOG_PID, LOG_USER);
368   logging = 1;
369   r = dup2(1,2);
370   if (r!=2) diee("dup2 stdout to stderr");
371 }
372
373 static void errpipe_readable(void) {
374   static char buf[1024];
375   static int pending;
376
377   /* %: does not contain newlines
378    * _: empty (garbage)
379    */ 
380
381   /*           %%%%%%%%%%%__________________ */
382   /*                      ^ pending          */
383
384   for (;;) {
385     int avail = sizeof(buf) - pending;
386     ssize_t got = read(errpipe, buf+pending, avail);
387     if (got==-1) {
388       if (errno==EINTR) continue;
389       else if (errno==EWOULDBLOCK || errno==EAGAIN) return;
390       else diee("(stage2) errpipe read");
391       got = 0;
392     } else if (got==0) {
393       warning("program closed its stderr fd");
394       errpipe = -1;
395       return;
396     }
397     int scanned = pending;
398     pending += got;
399     int eaten = 0;
400     for (;;) {
401       const char *newline = memchr(buf+scanned, '\n', pending-scanned);
402       int printupto, eat;
403       if (newline) {
404         printupto = newline-buf;
405         eat = printupto + 1;
406       } else if (!eaten && pending==sizeof(buf)) { /* overflow */
407         printupto = pending;
408         eat = printupto;
409       } else {
410         break;
411       }
412       syslog(LOG_ERR,"stderr: %.*s", printupto-eaten, buf+eaten);
413       eaten += eat;
414       scanned = eaten;
415     }
416     pending -= eaten;
417     memmove(buf, buf+eaten, pending);
418   }
419 }     
420
421 static void await_something(void) {
422   int r;
423   sigset_t mask;
424   sigemptyset(&mask);
425
426   for (;;) {
427     fd_set rfds;
428     FD_ZERO(&rfds);
429     if (errpipe >= 0)
430       FD_SET(errpipe, &rfds);
431     r = pselect(errpipe+1, &rfds,0,0, 0, &mask);
432     if (r==-1) {
433       if (errno != EINTR) diee("(stage2) sigsuspend");
434       continue;
435     }
436     assert(r>0);
437     assert(FD_ISSET(errpipe, &rfds));
438     errpipe_readable();
439   }
440 }