]> ruderich.org/simon Gitweb - wall-notify/wall-notify.git/blob - src/wall-notify.c
use getopt() to parse options
[wall-notify/wall-notify.git] / src / wall-notify.c
1 /*
2  * Receive wall messages and pass them to a notification program via stdin.
3  *
4  * Copyright (C) 2014  Simon Ruderich
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20
21 #include <config.h>
22
23 #include <assert.h>
24 #include <fcntl.h>
25 #include <limits.h>
26 #include <pwd.h>
27 #include <signal.h>
28 #include <stdint.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <sys/select.h>
33 #include <sys/stat.h>
34 #include <sys/time.h>
35 #include <sys/types.h>
36 #include <sys/wait.h>
37 #include <unistd.h>
38
39 #ifdef USE_UTEMPTER
40 # include <utempter.h>
41 #endif
42 #ifdef USE_UTMPX
43 # include <utmpx.h>
44 #endif
45 #ifndef DONT_USE_X11
46 # include <pthread.h>
47 # include <X11/Xlib.h>
48 #endif
49
50
51 static void sig_handler(int signal) {
52     (void)signal;
53 }
54 static void setup_signals(void) {
55     struct sigaction action;
56
57     memset(&action, 0, sizeof(action));
58     sigemptyset(&action.sa_mask);
59     action.sa_handler = sig_handler;
60
61     /* Handle all important signals which might be sent to us so we break out
62      * of the read()-loop below and can perform our cleanup. */
63     sigaction(SIGHUP,  &action, NULL);
64     sigaction(SIGINT,  &action, NULL);
65     sigaction(SIGQUIT, &action, NULL);
66     sigaction(SIGUSR1, &action, NULL);
67     sigaction(SIGUSR2, &action, NULL);
68 }
69
70 static int open_tty(void) {
71     int ptm;
72     const char *name;
73
74     ptm = posix_openpt(O_RDWR);
75     if (ptm < 0) {
76         return -1;
77     }
78     if (grantpt(ptm) != 0) {
79         return -1;
80     }
81
82     /* Prevent write access for other users so they can't use wall to send
83      * messages to this program. */
84     name = ptsname(ptm);
85     if (!name) {
86         return -1;
87     }
88     if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
89         return -1;
90     }
91
92     if (unlockpt(ptm) != 0) {
93         return -1;
94     }
95
96     return ptm;
97 }
98
99 #ifdef USE_UTMPX
100 static const char *skip_prefix(const char *string, const char *prefix) {
101     size_t length = strlen(prefix);
102
103     if (!strncmp(string, prefix, length)) {
104         return string + length;
105     } else {
106         return string;
107     }
108 }
109 static int set_utmpx(short type, int ptm) {
110     struct utmpx entry;
111
112     const char *tty, *user, *id, *line;
113     struct timeval now;
114
115     user = getpwuid(getuid())->pw_name;
116     gettimeofday(&now, NULL);
117
118     tty = ptsname(ptm);
119     if (!tty) {
120         return 0;
121     }
122
123     id   = skip_prefix(tty, "/dev/pts/");
124     line = skip_prefix(tty, "/dev/");
125
126     /* Create utmp entry for the given terminal. */
127     memset(&entry, 0, sizeof(entry));
128
129     snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
130     snprintf(entry.ut_id,   sizeof(entry.ut_id),   "%s", id);
131     snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
132
133     entry.ut_pid  = getpid();
134     entry.ut_type = type;
135     entry.ut_tv.tv_sec  = now.tv_sec;
136     entry.ut_tv.tv_usec = now.tv_usec;
137
138     /* Write the entry to the utmp file. */
139     setutxent();
140     if (!pututxline(&entry)) {
141         return 0;
142     }
143     endutxent();
144
145     return 1;
146 }
147 #endif
148 static int login(int ptm) {
149 #if defined(USE_UTEMPTER)
150     int result = utempter_add_record(ptm, NULL);
151     /* Exit value of utempter_*() is not correct on all systems, e.g.
152      * FreeBSD() always returns 0. Checking the utmpx database manually is
153      * difficult because we don't know the exact values for ut_id and ut_line,
154      * therefore we only check the return value on systems known to return a
155      * useful value. */
156 # ifndef __linux
157     result = 1;
158 # endif
159     return result;
160 #elif defined(USE_UTMPX)
161     return set_utmpx(USER_PROCESS, ptm);
162 #else
163 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
164 #endif
165 }
166 static int logout(int ptm) {
167 #if defined(USE_UTEMPTER)
168     int result = utempter_remove_record(ptm);
169     /* See above. */
170 # ifndef __linux
171     result = 1;
172 # endif
173     return result;
174 #elif defined(USE_UTMPX)
175     return set_utmpx(DEAD_PROCESS, ptm);
176 #else
177 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
178 #endif
179 }
180
181 static int wait_for_write(int fd, int timeout) {
182     fd_set rfds;
183     struct timeval tv;
184     int result;
185
186     FD_ZERO(&rfds);
187     FD_SET(fd, &rfds);
188
189     tv.tv_sec  = timeout;
190     tv.tv_usec = 0;
191
192     result = select(fd + 1, &rfds, NULL, NULL, &tv);
193     if (result < 0) {
194         perror("select");
195         return 0;
196     }
197     if (result == 0) {
198         /* Timeout. */
199         return 0;
200     }
201
202     /* Got more data to read. */
203     return 1;
204 }
205
206 #ifdef USE_UTMPX
207 static void drop_privileges(void) {
208     uid_t uid, ruid, euid, suid;
209     gid_t gid, rgid, egid, sgid;
210
211     uid = getuid();
212     gid = getgid();
213
214     /* Drop all privileges. */
215     if (setresuid(uid, uid, uid) != 0) {
216         perror("setresuid");
217         exit(EXIT_FAILURE);
218     }
219     if (setresgid(gid, gid, gid) != 0) {
220         perror("setresgid");
221         exit(EXIT_FAILURE);
222     }
223
224     /* Verify all privileges were dropped. */
225     if (getresuid(&ruid, &euid, &suid) != 0) {
226         perror("getresuid");
227         exit(EXIT_FAILURE);
228     }
229     if (getresgid(&rgid, &egid, &sgid) != 0) {
230         perror("getresgid");
231         exit(EXIT_FAILURE);
232     }
233     if (uid == ruid && uid == euid && uid == suid
234             && gid == rgid && gid == egid && gid == sgid) {
235         /* Everything fine. */
236         return;
237     }
238
239     fprintf(stderr, "failed to drop privileges, aborting\n");
240     exit(EXIT_FAILURE);
241 }
242 #endif
243
244 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
245     int fds[2];
246     FILE *fh;
247
248     pid_t pid;
249
250     if (pipe(fds) != 0) {
251         perror("pipe");
252         return;
253     }
254
255     fh = fdopen(fds[1] /* write side */, "w");
256     if (!fh) {
257         perror("fdopen");
258         close(fds[0]);
259         close(fds[1]);
260         return;
261     }
262
263     pid = fork();
264     if (pid < 0) {
265         perror("fork");
266         goto out;
267     } else if (pid == 0) {
268         /* child */
269
270 #ifdef USE_UTMPX
271         drop_privileges();
272 #endif
273
274         close(fds[1]); /* write side */
275
276         /* Pass read side as stdin to the program. */
277         if (dup2(fds[0], STDIN_FILENO) < 0) {
278             perror("dup2");
279             exit(EXIT_FAILURE);
280         }
281         close(fds[0]);
282
283         /* Double fork so `wall-notify` doesn't have to wait for the
284          * notification process to terminate. We can't just use
285          * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
286          * doesn't work if SIGCHLD is ignored. */
287         pid = fork();
288         if (pid < 0) {
289             perror("fork");
290             exit(EXIT_FAILURE);
291         } else if (pid == 0) {
292             execvp(argv[0], argv);
293             perror("execvp");
294             exit(EXIT_FAILURE);
295         }
296
297         exit(EXIT_SUCCESS);
298     }
299     /* father */
300
301     if (fwrite(buffer, 1, length, fh) != length) {
302         perror("fwrite");
303         /* continue to perform cleanup */
304     }
305
306 out:
307     close(fds[0]); /* read side */
308     fclose(fh);
309
310     if (waitpid(pid, NULL, 0) < 0) {
311         perror("waitpid");
312     }
313 }
314 static void handle_wall(int fd, char **argv) {
315     char buffer[4096];
316     ssize_t r;
317
318     assert(SSIZE_MAX <= SIZE_MAX);
319     while ((r = read(fd, buffer, sizeof(buffer))) > 0) {
320         size_t space;
321         ssize_t r2;
322
323         /* To prevent partial messages (sometimes it takes multiple reads to
324          * get the complete message) wait for a short time to get the rest of
325          * the message. */
326         space = sizeof(buffer) - (size_t)r;
327         while (space > 0 && wait_for_write(fd, 1 /* second */)) {
328             r2 = read(fd, buffer + r, space);
329             if (r2 <= 0) {
330                 break;
331             }
332             r += r2;
333             space -= (size_t)r2;
334         }
335
336         pass_buffer_to_program(buffer, (size_t)r, argv);
337     }
338 }
339 #ifndef DONT_USE_X11
340 static void *x11_event_loop_thread(void *unused) {
341     Display *display;
342     XEvent event;
343
344     (void)unused;
345
346     pthread_detach(pthread_self());
347
348     display = XOpenDisplay(NULL);
349     if (!display) {
350         fprintf(stderr, "failed to connect to X server\n");
351         exit(EXIT_FAILURE);
352     }
353
354     /* Do nothing. We just want to die if the X11 session is closed. */
355     while (1) {
356         XNextEvent(display, &event);
357     }
358 }
359 #endif
360
361 static void usage(const char *argv0) {
362     fprintf(stderr, "usage: %s [-X] <cmd args..>\n", argv0);
363     fprintf(stderr, "Pass wall messages to <cmd args..>.\n");
364     fprintf(stderr, "\n");
365     fprintf(stderr, "-X quit when the current X session terminates\n");
366 #ifdef DONT_USE_X11
367     fprintf(stderr, "\n");
368     fprintf(stderr, "compiled without X11 support, -X disabled\n");
369 #endif
370     exit(EXIT_FAILURE);
371 }
372
373
374 int main(int argc, char **argv) {
375     int option, enable_x11;
376     int ptm, pts;
377     char *name;
378
379     /* Don't display error messages for unknown options. */
380     opterr = 0;
381
382     enable_x11 = 0;
383
384     /*
385      * Glibc violates POSIX by default and skips over non-option arguments and
386      * parses later arguments which look like options as well. But we want to
387      * pass everything except the options unmodified to execvp(). Prefixing
388      * the optstring with "+" fixes this behaver. This is not POSIX
389      * compatible, but the option should be ignored on other systems.
390      */
391     while ((option = getopt(argc, argv, "+Xh")) != -1) {
392         switch (option) {
393             case 'X':
394                 enable_x11 = 1;
395                 break;
396             case 'h':
397                 usage(argv[0]);
398                 break;
399             default:
400                 fprintf(stderr, "%s: unknown option '%s'!\n\n",
401                                 argv[0], argv[optind - 1]);
402                 usage(argv[0]);
403                 break;
404         }
405     }
406
407     /* No arguments remaining, abort. */
408     if (!argv[optind]) {
409         usage(argv[0]);
410     }
411     /* Arguments for notification program. */
412     argv += optind;
413
414     ptm = open_tty();
415     if (ptm < 0) {
416         perror("open_tty");
417         exit(EXIT_FAILURE);
418     }
419     name = ptsname(ptm);
420     if (!name) {
421         perror("ptsname");
422         exit(EXIT_FAILURE);
423     }
424
425 #ifdef DEBUG
426     printf("%s\n", name);
427 #endif
428
429     /* We need to open the slave or reading from the master yields EOF after
430      * the first wall write to it. */
431     pts = open(name, O_RDWR);
432     if (pts < 0) {
433         perror(name);
434         exit(EXIT_FAILURE);
435     }
436
437     /* Start a thread which connects to X11. This way we die if the user logs
438      * out of its X11 session. */
439     if (enable_x11) {
440 #ifdef DONT_USE_X11
441         fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
442         usage(argv0);
443 #else
444         pthread_t tid;
445
446         if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
447             perror("pthread_create");
448             exit(EXIT_FAILURE);
449         }
450 #endif
451     }
452
453     /* Cleanup on signals. Necessary before login(). */
454     setup_signals();
455
456     if (!login(ptm)) {
457         perror("login");
458         exit(EXIT_FAILURE);
459     }
460
461     /* Main loop. Handle all wall messages sent to our PTY. */
462     handle_wall(ptm, argv);
463
464     if (!logout(ptm)) {
465         perror("logout");
466         exit(EXIT_FAILURE);
467     }
468
469     close(ptm);
470     close(pts);
471
472     return EXIT_SUCCESS;
473 }