callscgi: Exit properly on SIGTERM and SIGINT.
[ashd.git] / src / userplex.c
1 /*
2     ashd - A Sane HTTP Daemon
3     Copyright (C) 2008  Fredrik Tolf <fredrik@dolda2000.com>
4
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <stdlib.h>
20 #include <stdio.h>
21 #include <unistd.h>
22 #include <fcntl.h>
23 #include <string.h>
24 #include <signal.h>
25 #include <sys/socket.h>
26 #include <errno.h>
27 #include <pwd.h>
28 #include <grp.h>
29
30 #ifdef HAVE_CONFIG_H
31 #include <config.h>
32 #endif
33 #include <utils.h>
34 #include <log.h>
35 #include <req.h>
36 #include <resp.h>
37
38 struct user {
39     struct user *next, *prev;
40     char *name;
41     int fd;
42 };
43
44 static int ignore = 0;
45 static char *mgroup = NULL;
46 static char *dirname = "htpub";
47 static char **childspec;
48 static uid_t minuid = 0;
49 static struct user *users = NULL;
50
51 static void login(struct passwd *pwd)
52 {
53     int fd;
54     
55     if(getuid() == 0) {
56         if(initgroups(pwd->pw_name, pwd->pw_gid)) {
57             flog(LOG_ERR, "could not init group list for %s: %s", pwd->pw_name, strerror(errno));
58             exit(1);
59         }
60         if(setgid(pwd->pw_gid)) {
61             flog(LOG_ERR, "could not switch group for %s: %s", pwd->pw_name, strerror(errno));
62             exit(1);
63         }
64         if(setuid(pwd->pw_uid)) {
65             flog(LOG_ERR, "could not switch user to %s: %s", pwd->pw_name, strerror(errno));
66             exit(1);
67         }
68     } else {
69         if(getuid() != pwd->pw_uid)
70             exit(1);
71     }
72     if(chdir(pwd->pw_dir)) {
73         flog(LOG_ERR, "could not change to home directory for %s: %s", pwd->pw_name, strerror(errno));
74         exit(1);
75     }
76     putenv(sprintf2("HOME=%s", pwd->pw_dir));
77     putenv(sprintf2("SHELL=%s", pwd->pw_shell));
78     putenv(sprintf2("USER=%s", pwd->pw_name));
79     putenv(sprintf2("LOGNAME", pwd->pw_name));
80     /* There's whole load of other stuff one could want to do here --
81      * getting Kerberos credentials, running PAM session modules, and
82      * who knows what. I'll add them along as I find them useful. */
83     if(((fd = open(".ashd/output", O_WRONLY | O_APPEND)) >= 0) ||
84        ((fd = open("/dev/null", 0)) >= 0)) {
85         dup2(fd, 1);
86         close(fd);
87     }
88     if(((fd = open(".ashd/error", O_WRONLY | O_APPEND)) >= 0) ||
89        ((fd = open("/dev/null", 0)) >= 0)) {
90         dup2(fd, 2);
91         close(fd);
92     }
93 }
94
95 static void execchild(struct passwd *pwd)
96 {
97     if(!ignore)
98         execl(".ashd/handler", ".ashd/handler", NULL);
99     if(dirname != NULL) {
100         if(access(dirname, X_OK | R_OK))
101             return;
102     }
103     execvp(childspec[0], childspec);
104 }
105
106 static int forkchild(char *usrnm)
107 {
108     struct passwd *pwd;
109     pid_t pid;
110     int i, fd[2];
111     
112     /* XXX: There should be a way for the child to report errors (like
113      * 404 when htpub doesn't exist), but for now I don't bother with
114      * that. I might return to it at some later time. */
115     if(socketpair(PF_UNIX, SOCK_SEQPACKET, 0, fd))
116         return(-1);
117     if((pwd = getpwnam(usrnm)) == NULL) {
118         flog(LOG_ERR, "already discovered user `%s' has disappeared", usrnm);
119         return(-1);
120     }
121     if((pid = fork()) < 0)
122         return(-1);
123     if(pid == 0) {
124         for(i = 3; i < FD_SETSIZE; i++) {
125             if(i != fd[0])
126                 close(i);
127         }
128         dup2(fd[0], 0);
129         close(fd[0]);
130         login(pwd);
131         execchild(pwd);
132         exit(127);
133     }
134     close(fd[0]);
135     return(fd[1]);
136 }
137
138 static void serve2(struct user *usr, struct hthead *req, int fd)
139 {
140     if(usr->fd < 0)
141         usr->fd = forkchild(usr->name);
142     if(sendreq(usr->fd, req, fd)) {
143         if(errno == EPIPE) {
144             /* Assume that the child has crashed and restart it. */
145             close(usr->fd);
146             usr->fd = forkchild(usr->name);
147             if(!sendreq(usr->fd, req, fd))
148                 return;
149         }
150         flog(LOG_ERR, "could not pass on request to user `%s': %s", usr->name, strerror(errno));
151         close(usr->fd);
152         usr->fd = -1;
153         simpleerror(fd, 500, "User Error", "The request handler for that user keeps crashing.");
154     }
155 }
156
157 static void initnew(struct hthead *req, int fd, char *usrnm)
158 {
159     struct user *usr;
160     struct passwd *pwd;
161     struct group *grp;
162     int i, valid;
163
164     pwd = getpwnam(usrnm);
165     if(pwd == NULL) {
166         simpleerror(fd, 404, "Not Found", "No such resource could be found.");
167         return;
168     }
169     if(pwd->pw_uid < minuid) {
170         simpleerror(fd, 404, "Not Found", "No such resource could be found.");
171         return;
172     }
173     if(mgroup) {
174         if((grp = getgrnam(mgroup)) == NULL) {
175             flog(LOG_ERR, "unknown group %s specified to userplex", mgroup);
176             simpleerror(fd, 500, "Configuration Error", "The server has been erroneously configured.");
177             return;
178         }
179         valid = 0;
180         if(grp->gr_gid == pwd->pw_gid) {
181             valid = 1;
182         } else {
183             for(i = 0; grp->gr_mem[i] != NULL; i++) {
184                 if(!strcmp(grp->gr_mem[i], usrnm)) {
185                     valid = 1;
186                     break;
187                 }
188             }
189         }
190         if(!valid) {
191             simpleerror(fd, 404, "Not Found", "No such resource could be found.");
192             return;
193         }
194     }
195     omalloc(usr);
196     usr->name = sstrdup(usrnm);
197     usr->fd = -1;
198     usr->next = users;
199     usr->prev = NULL;
200     if(users != NULL)
201         users->prev = usr;
202     users = usr;
203     serve2(usr, req, fd);
204 }
205
206 static void serve(struct hthead *req, int fd)
207 {
208     struct user *usr;
209     char *usrnm, *p;
210     
211     if((p = strchr(req->rest, '/')) == NULL) {
212         if(*req->rest)
213             stdredir(req, fd, 301, sprintf3("%s/", req->url));
214         else
215             simpleerror(fd, 404, "Not Found", "No such resource could be found.");
216         return;
217     }
218     *(p++) = 0;
219     usrnm = sstrdup(req->rest);
220     replrest(req, p);
221     for(usr = users; usr != NULL; usr = usr->next) {
222         if(!strcmp(usr->name, usrnm)) {
223             serve2(usr, req, fd);
224             goto out;
225         }
226     }
227     initnew(req, fd, usrnm);
228     
229 out:
230     free(usrnm);
231 }
232
233 static void usage(FILE *out)
234 {
235     fprintf(out, "usage: userplex [-hI] [-g GROUP] [-m MIN-UID] [-d PUB-DIR] [PROGRAM ARGS...]\n");
236 }
237
238 int main(int argc, char **argv)
239 {
240     struct hthead *req;
241     int c;
242     int fd;
243     struct charvbuf csbuf;
244     
245     while((c = getopt(argc, argv, "+hIg:m:d:")) >= 0) {
246         switch(c) {
247         case 'I':
248             ignore = 1;
249             break;
250         case 'm':
251             if((minuid = atoi(optarg)) < 1) {
252                 fprintf(stderr, "userplex: argument to -m must be greater than 0\n");
253                 exit(1);
254             }
255             break;
256         case 'g':
257             mgroup = optarg;
258             break;
259         case 'd':
260             dirname = optarg;
261             break;
262         case 'h':
263             usage(stdout);
264             exit(0);
265         default:
266             usage(stderr);
267             exit(1);
268         }
269     }
270     if(optind < argc) {
271         childspec = argv + optind;
272         dirname = NULL;
273     } else {
274         bufinit(csbuf);
275         bufadd(csbuf, "dirplex");
276         bufadd(csbuf, dirname);
277         bufadd(csbuf, NULL);
278         childspec = csbuf.b;
279     }
280     signal(SIGCHLD, SIG_IGN);
281     while(1) {
282         if((fd = recvreq(0, &req)) < 0) {
283             if(errno != 0)
284                 flog(LOG_ERR, "recvreq: %s", strerror(errno));
285             break;
286         }
287         serve(req, fd);
288         freehthead(req);
289         close(fd);
290     }
291     return(0);
292 }