chiark / gitweb /
cgi-fcgi-interp: wip check_garbage
[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  *   #!/usr/bin/cgi-fcgi-interp [<options>],<interpreter>
5  */
6 /*
7  * cgi-fcgi-interp.[ch] - C helpers common to the whole of chiark-utils
8  *
9  * Copyright 2016 Ian Jackson
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 /*
27  * The result is a program which looks, when executed via the #!
28  * line, like a CGI program.  But the script inside will be executed
29  * via <interpreter> in an fcgi context.
30  *
31  * Options:
32  *
33  *  <interpreter>
34  *          The real interpreter to use.  Eg "perl".  Need not
35  *          be an absolute path; will be fed to execvp.
36  *
37  *  -g<ident>
38  *          Use <ident> rather than hex(sha256(<script>))
39  *          as the basename of the leafname of the fcgi rendezvous
40  *          socket.  If <ident> contains only hex digit characters it
41  *          ought to be no more than 32 characters.  <ident> should
42  *          not contain spaces or commas (see below).
43  *
44  *  -M<numservers>
45  *         Start <numservers> instances of the program.  This
46  *         determines the maximum concurrency.  (Note that unlike
47  *         speedy, the specified number of servers is started
48  *         right away.)  The default is 4.
49  *
50  *  -D
51  *         Debug mode.  Do not actually run program.  Instead, print
52  *         out what we would do.
53  *
54  * <options> and <interpreter> can be put into a single argument
55  * to cgi-fcgi-interp, separated by spaces or commas.  <interpreter>
56  * must come last.
57  *
58  * cgi-fcgi-interp automatically expires old sockets, including
59  * ones where the named script is out of date.
60  */
61
62 /*
63  * Uses one of two directories
64  *   /var/run/user/<UID>/cgi-fcgi-interp/
65  *   ~/.cgi-fcgi-interp/<node>/
66  * and inside there uses these paths
67  *   s<ident>
68  *   g<inum>
69  *
70  * If -M<ident> is not specified then an initial substricg of the
71  * lowercase hex of the sha256 of the <script> (ie, our argv[1]) is
72  * used.  The substring is chosen so that the whole path is 10 bytes
73  * shorter than sizeof(sun_path).  But always at least 33 characters.
74  *
75  * <node> is truncated at the first `.' and after the first 32
76  * characters.
77  *
78  * Algorithm:
79  *  - see if /var/run/user exists
80  *       if so, lstat /var/run/user/<UID> and check that
81  *         we own it and it's X700; if not, fail
82  *         if it's ok then <base> is /var/run/user/<UID>
83  *       otherwise, look for and maybe create ~/.cgi-fcgi-interp
84  *         (where ~ is HOME or from getpwuid)
85  *         and then <base> is ~/.cgi-fcgi-interp/<node>
86  *  - calculate pathname (checking <ident> length is OK)
87  *  - check for and maybe create <base>
88  *  - stat and lstat the <script>
89  *  - stat the socket and check its timestamp
90  *       if it is too old, rename it to g<inum>.<pid> (where
91  *       <inum> and <pid> are in decimal)
92  *       and run garbage collection
93  *  - run  cgi-fcgi -connect SOCKET SCRIPT
94  */
95
96 #include "common.h"
97
98 #include <stdio.h>
99 #include <stdlib.h>
100 #include <string.h>
101 #include <errno.h>
102 #include <stdbool.h>
103 #include <assert.h>
104 #include <limits.h>
105
106 #include <sys/types.h>
107 #include <sys/stat.h>
108 #include <sys/utsname.h>
109 #include <sys/socket.h>
110 #include <sys/un.h>
111 #include <unistd.h>
112 #include <pwd.h>
113 #include <err.h>
114
115 #include <nettle/sha.h>
116
117 #include "myopt.h"
118
119 #define die  common_die
120 #define diee common_diee
121
122 #define MINHEXHASH 33
123
124 static const char *interp, *ident;
125 static int numservers, debugmode;
126
127 void diee(const char *m) {
128   err(127, "error: %s failed", m);
129 }
130
131 static void fusagemessage(FILE *f) {
132   fprintf(f, "usage: #!/usr/bin/cgi-fcgi-interp [<options>]\n");
133 }
134
135 void usagemessage(void) { fusagemessage(stderr); }
136
137 static void of_help(const struct cmdinfo *ci, const char *val) {
138   fusagemessage(stdout);
139   if (ferror(stdout)) diee("write usage message to stdout");
140   exit(0);
141 }
142
143 static void of_iassign(const struct cmdinfo *ci, const char *val) {
144   long v;
145   char *ep;
146   errno= 0; v= strtol(val,&ep,10);
147   if (!*val || *ep || errno || v<INT_MIN || v>INT_MAX)
148     badusage("bad integer argument `%s' for --%s",val,ci->olong);
149   *ci->iassignto = v;
150 }
151
152 #define MAX_OPTS 5
153
154 static const struct cmdinfo cmdinfos[]= {
155   { "help",   0, .call= of_help               },
156   { 0, 'g',   1, .sassignto= &ident           },
157   { 0, 'M',   1, .call=of_iassign, .iassignto= &numservers      },
158   { 0, 'D',   0, .iassignto= &debugmode, .arg= 1 },
159   { 0 }
160 };
161
162 static uid_t us;
163 static const char *run_base, *script, *socket_path;
164
165 static bool find_run_base_var_run(void) {
166   struct stat stab;
167   char *try;
168   int r;
169
170   try = m_asprintf("%s/%lu", "/var/run/user", us);
171   r = lstat(try, &stab);
172   if (r<0) {
173     if (errno == ENOENT ||
174         errno == ENOTDIR ||
175         errno == EACCES ||
176         errno == EPERM)
177       return 0; /* oh well */
178     diee("stat /var/run/user/UID");
179   }
180   if (!S_ISDIR(stab.st_mode)) {
181     warnx("%s not a directory, falling back to ~\n", try);
182     return 0;
183   }
184   if (stab.st_uid != us) {
185     warnx("%s not owned by uid %lu, falling back to ~\n", try,
186           (unsigned long)us);
187     return 0;
188   }
189   if (stab.st_mode & 0077) {
190     warnx("%s writeable by group or other, falling back to ~\n", try);
191     return 0;
192   }
193   run_base = m_asprintf("%s/%s", try, "cgi-fcgi-interp");
194   return 1;
195 }
196
197 static bool find_run_base_home(void) {
198   struct passwd *pw;
199   struct utsname ut;
200   char *dot, *try;
201   int r;
202
203   pw = getpwuid(us);  if (!pw) diee("getpwent(uid)");
204
205   r = uname(&ut);   if (r) diee("uname(2)");
206   dot = strchr(ut.nodename, '.');
207   if (dot) *dot = 0;
208   if (sizeof(ut.nodename) > 32)
209     ut.nodename[32] = 0;
210
211   try = m_asprintf("%s/%s/%s", pw->pw_dir, ".cgi-fcgi-interp", ut.nodename);
212   run_base = try;
213   return 1;
214 }
215
216 static void find_socket_path(void) {
217   struct sockaddr_un sun;
218   int r;
219
220   us = getuid();  if (us==(uid_t)-1) diee("getuid");
221
222   find_run_base_var_run() ||
223     find_run_base_home() ||
224     (abort(),0);
225
226   int maxidentlen = sizeof(sun.sun_path) - strlen(run_base) - 10 - 2;
227
228   if (!ident) {
229     if (maxidentlen < MINHEXHASH)
230       errx(127,"base directory `%s'"
231            " leaves only %d characters for id hash"
232            " which is too little (<%d)",
233            run_base, maxidentlen, MINHEXHASH);
234
235     int identlen = maxidentlen > 64 ? 64 : maxidentlen;
236     char *hexident = xmalloc(identlen + 2);
237     struct sha256_ctx sc;
238     unsigned char bbuf[32];
239     int i;
240
241     sha256_init(&sc);
242     sha256_update(&sc,strlen(interp)+1,interp);
243     sha256_update(&sc,strlen(script)+1,script);
244     sha256_digest(&sc,sizeof(bbuf),bbuf);
245
246     for (i=0; i<identlen; i += 2)
247       sprintf(hexident+i, "%02x", bbuf[i/2]);
248
249     hexident[identlen] = 0;
250     ident = hexident;
251   }
252
253   if (strlen(ident) > maxidentlen)
254     errx(127, "base directory `%s' plus ident `%s' too long"
255          " (with spare) for socket (max ident %d)\n",
256          run_base, ident, maxidentlen);
257
258   r = mkdir(run_base, 0700);
259   if (r) {
260     if (!(errno == EEXIST))
261       err(127,"mkdir %s",run_base);
262   }
263
264   socket_path = m_asprintf("%s/g%s",run_base,ident);
265 }  
266
267 static bool stab_isnewer(const struct stat *a, const struct stat *b) {
268   return 0;
269 }
270
271 static bool check_garbage(void) {
272   struct stat sock_stab, script_stab;
273   int r;
274
275   r = lstat(script, &script_stab);
276   if (r) err(127,"lstat script (%s)",script);
277
278   r = lstat(socket_path, &sock_stab);
279   if (r) {
280     if ((errno == ENOENT))
281       return 0; /* well, no garbage then */
282     err(127,"stat socket (%s)",socket_path);
283   }
284
285   if (stab_isnewer(&script_stab, &sock_stab))
286     return 1;
287
288   if (S_ISLNK(script_stab.st_mode)) {
289     r = stat(script, &script_stab);
290     if (r) err(127,"stat script (%s0",script);
291
292     if (stab_isnewer(&script_stab, &sock_stab))
293       return 1;
294   }
295
296   return 0;
297 }
298
299 static void shbang_opts(const char *const **argv_io,
300                         const struct cmdinfo *cmdinfos) {
301   myopt(argv_io, cmdinfos);
302
303   interp = *(*argv_io)++;
304   if (!interp) errx(127,"need interpreter argument");
305 }
306
307 int main(int argc, const char *const *argv) {
308   const char *smashedopt;
309
310   if (argc>=2 &&
311       (smashedopt = argv[1]) &&
312       smashedopt[0]=='-' &&
313       (strchr(smashedopt,' ') || strchr(smashedopt,','))) {
314     /* single argument containg all the options and <interp> */
315     argv += 2; /* eat argv[0] and smashedopt */
316     const char *split_args[MAX_OPTS+1];
317     int split_argc = 0;
318     split_args[split_argc++] = argv[0];
319     for (;;) {
320       if (split_argc >= MAX_OPTS) errx(127,"too many options in combined arg");
321       split_args[split_argc++] = smashedopt;
322       if (smashedopt[0] != '-') /* never true on first iteration */
323         break;
324       char *delim = strchr(smashedopt,' ');
325       if (!delim) delim = strchr(smashedopt,',');
326       if (!delim)
327         errx(127,"combined arg lacks <interpreter>");
328       *delim = 0;
329       smashedopt = delim+1;
330     }
331     assert(split_argc <= MAX_OPTS);
332     split_args[split_argc++] = 0;
333
334     const char *const *split_argv = split_args;
335
336     shbang_opts(&split_argv, cmdinfos);
337     /* sets interp */
338     if (!split_argv) errx(127,"combined arg too many non-option arguments");
339   } else {
340     shbang_opts(&argv, cmdinfos);
341   }
342
343   script = *argv++;
344   if (!script) errx(127,"need script argument");
345   if (*argv) errx(127,"too many arguments");
346
347   find_socket_path();
348
349   check_garbage();
350
351   if (debugmode) {
352     printf("socket: %s\n",socket_path);
353     printf("interp: %s\n",interp);
354     printf("script: %s\n",script);
355   }
356
357   exit(0);
358 }