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