chiark / gitweb /
cgi-fcgi-perl: wip, check_garbage
[chiark-utils.git] / cprogs / cgi-fcgi-perl.c
1 /*
2  * "Interpreter" that you can put in #! like this
3  *   #!/usr/bin/cgi-fcgi-perl [<options>]
4  *
5  * The result is a program which looks, when executed via the #!
6  * line, like a CGI program.  But the perl script inside will
7  * be executed via /usr/bin/perl in fcgi.
8  *
9  * Options:
10  *
11  *  -g<ident>
12  *          Use <ident> rather than hex(sha256(<script>))
13  *          as the basename of the leafname of the fcgi rendezvous
14  *          socket.  If <ident> contains only hex digit characters it
15  *          ought to be no more than 32 characters.
16  *
17  *  -M<numservers>
18  *         Start <numservers> instances of the program.  This
19  *         determines the maximum concurrency.  (Note that unlike
20  *         speedy, the specified number of servers is started
21  *         right away.)  The default is 4.
22  *
23  * cgi-fcgi-perl automatically expires old sockets, including
24  * ones where the named perl script is out of date.
25  */
26
27 /*
28  * Uses one of two directories
29  *   /var/run/user/<UID>/cgi-fcgi-perl/
30  *   ~/.cgi-fcgi-perl/<node>/
31  * and inside there uses these paths
32  *   s<ident>
33  *   g<inum>
34  *
35  * If -M<ident> is not specified then an initial substricg of the
36  * lowercase hex of the sha256 of the <script> (ie, our argv[1]) is
37  * used.  The substring is chosen so that the whole path is 10 bytes
38  * shorter than sizeof(sun_path).  But always at least 33 characters.
39  *
40  * <node> is truncated at the first `.' and after the first 32
41  * characters.
42  *
43  * Algorithm:
44  *  - see if /var/run/user exists
45  *       if so, lstat /var/run/user/<UID> and check that
46  *         we own it and it's X700; if not, fail
47  *         if it's ok then <base> is /var/run/user/<UID>
48  *       otherwise, look for and maybe create ~/.cgi-fcgi-perl
49  *         (where ~ is HOME or from getpwuid)
50  *         and then <base> is ~/.cgi-fcgi-perl/<node>
51  *  - calculate pathname (checking <ident> length is OK)
52  *  - check for and maybe create <base>
53  *  - stat and lstat the <script>
54  *  - stat the socket and check its timestamp
55  *       if it is too hold, rename it to g<inum> (where
56  *       <inum> is in decimal)
57  *       and run garbage collection
58  *  - run  cgi-fcgi -connect SOCKET SCRIPT
59  */
60
61 #include "common.h"
62
63 #include <stdio.h>
64 #include <stdlib.h>
65 #include <string.h>
66 #include <errno.h>
67 #include <stdbool.h>
68
69 #include <sys/types.h>
70 #include <sys/stat.h>
71 #include <sys/utsname.h>
72 #include <sys/socket.h>
73 #include <sys/un.h>
74 #include <unistd.h>
75 #include <pwd.h>
76 #include <err.h>
77
78 #include <nettle/sha.h>
79
80 #include "myopt.h"
81
82 #define die  common_die
83 #define diee common_diee
84
85 #define MINHEXHASH 33
86
87 static const char *ident;
88 static int numservers;
89
90 void diee(const char *m) {
91   err(127, "error: %s failed", m);
92 }
93
94 static void fusagemessage(FILE *f) {
95   fprintf(f, "usage: #!/usr/bin/cgi-fcgi-perl [<options>]\n");
96 }
97
98 void usagemessage(void) { fusagemessage(stderr); }
99
100 static void of_help(const struct cmdinfo *ci, const char *val) {
101   fusagemessage(stdout);
102   if (ferror(stdout)) diee("write usage message to stdout");
103   exit(0);
104 }
105
106 static const struct cmdinfo cmdinfos[]= {
107   { "help",   0, .call= of_help               },
108   { 0, 'g',   1, .sassignto= &ident           },
109   { 0, 'M',   1, .iassignto= &numservers      },
110   { 0 }
111 };
112
113 static uid_t us;
114 static const char *run_base, *command, *socket_path;
115
116 static bool find_run_base_var_run(void) {
117   struct stat stab;
118   char *try;
119   int r;
120
121   try = m_asprintf("%s/%lu", "/var/run/user", us);
122   r = lstat(try, &stab);
123   if (r<0) {
124     if (errno == ENOENT ||
125         errno == ENOTDIR ||
126         errno == EACCES ||
127         errno == EPERM)
128       return 0; /* oh well */
129     diee("stat /var/run/user/UID");
130   }
131   if (!S_ISDIR(stab.st_mode)) {
132     warnx("%s not a directory, falling back to ~\n", try);
133     return 0;
134   }
135   if (stab.st_uid != us) {
136     warnx("%s not owned by uid %lu, falling back to ~\n", try,
137           (unsigned long)us);
138     return 0;
139   }
140   if (stab.st_mode & 0077) {
141     warnx("%s writeable by group or other, falling back to ~\n", try);
142     return 0;
143   }
144   run_base = m_asprintf("%s/%s", try, "cgi-fcgi-perl");
145   return 1;
146 }
147
148 static bool find_run_base_home(void) {
149   struct passwd *pw;
150   struct utsname ut;
151   char *dot, *try;
152   int r;
153
154   pw = getpwuid(us);  if (!pw) diee("getpwent(uid)");
155
156   r = uname(&ut);   if (r) diee("uname(2)");
157   dot = strchr(ut.nodename, '.');
158   if (dot) *dot = 0;
159   if (sizeof(ut.nodename) > 32)
160     ut.nodename[32] = 0;
161
162   try = m_asprintf("%s/%s/%s", pw->pw_dir, ".cgi-fcgi-perl", ut.nodename);
163   run_base = try;
164   return 1;
165 }
166
167 static void find_socket_path(void) {
168   struct sockaddr_un sun;
169   int r;
170
171   us = getuid();  if (us==(uid_t)-1) diee("getuid");
172
173   find_run_base_var_run() ||
174     find_run_base_home() ||
175     (abort(),0);
176
177   int maxidentlen = sizeof(sun.sun_path) - strlen(run_base) - 10 - 2;
178
179   if (!ident) {
180     if (maxidentlen < MINHEXHASH)
181       errx(127,"cgi-fcgi-perl: base directory `%s'"
182            " leaves only %d characters for command name hash"
183            " which is too little (<%d)",
184            run_base, maxidentlen, MINHEXHASH);
185
186     int identlen = maxidentlen > 64 ? 64 : maxidentlen;
187     char *hexident = xmalloc(identlen + 2);
188     struct sha256_ctx sc;
189     unsigned char bbuf[32];
190     int i;
191
192     sha256_init(&sc);
193     sha256_update(&sc,strlen(command)+1,command);
194     sha256_digest(&sc,sizeof(bbuf),bbuf);
195
196     for (i=0; i<identlen; i += 2)
197       sprintf(hexident+i, "%02x", bbuf[i/2]);
198
199     hexident[identlen] = 0;
200     ident = hexident;
201   }
202
203   if (strlen(ident) > maxidentlen)
204     errx(127, "base directory `%s' plus ident `%s' too long"
205          " (with spare) for socket (max ident %d)\n",
206          run_base, ident, maxidentlen);
207
208   r = mkdir(run_base, 0700);
209   if (r) {
210     if (!(errno == EEXIST))
211       err(127,"mkdir %s",run_base);
212   }
213
214   socket_path = m_asprintf("%s/g%s",run_base,ident);
215 }  
216
217 static bool check_garbage(void) {
218   struct stat sock_stab, cmd_stab;
219   int r;
220
221   r = lstat(socket_path, &sock_stab);
222   if (r) {
223     if ((errno == ENOENT))
224       return 0; /* well, no garbage then */
225     err(127,"stat socket (%s)",socket_path);
226   }
227
228   r = lstat(command, &cmd_stab);
229   if (r) err(127,"lstat command (%s)",command);
230
231   return 0;
232 }
233
234 int main(int argc, const char *const *argv) {
235   myopt(&argv, cmdinfos);
236
237   command = *argv++;
238   if (!command) errx(127,"need command argument");
239   if (*argv) errx(127,"too many arguments");
240
241   find_socket_path();
242
243   check_garbage();
244
245   printf(">%s<\n",socket_path);
246
247   exit(0);
248 }