]> ruderich.org/simon Gitweb - wall-notify/wall-notify.git/blob - src/wall-notify.c
set O_CLOEXEC on TTY slave/master file descriptors
[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 set_cloexec(int fd) {
71     int flags = fcntl(fd, F_GETFD);
72     if (flags == -1) {
73         return 0;
74     }
75     if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
76         return 0;
77     }
78     return 1;
79 }
80
81 static int open_tty(int public) {
82     int ptm;
83     const char *name;
84
85     ptm = posix_openpt(O_RDWR);
86     if (ptm < 0) {
87         return -1;
88     }
89     if (grantpt(ptm) != 0) {
90         return -1;
91     }
92     if (!set_cloexec(ptm)) {
93         return -1;
94     }
95
96     /* Prevent write access for other users so they can't use wall to send
97      * messages to this program. */
98     if (!public) {
99         name = ptsname(ptm);
100         if (!name) {
101             return -1;
102         }
103         if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
104             return -1;
105         }
106     }
107
108     if (unlockpt(ptm) != 0) {
109         return -1;
110     }
111
112     return ptm;
113 }
114
115 #ifdef USE_UTMPX
116 static const char *skip_prefix(const char *string, const char *prefix) {
117     size_t length = strlen(prefix);
118
119     if (!strncmp(string, prefix, length)) {
120         return string + length;
121     } else {
122         return string;
123     }
124 }
125 static int set_utmpx(short type, int ptm) {
126     struct utmpx entry;
127
128     const char *tty, *user, *id, *line;
129     struct timeval now;
130
131     user = getpwuid(getuid())->pw_name;
132     gettimeofday(&now, NULL);
133
134     tty = ptsname(ptm);
135     if (!tty) {
136         return 0;
137     }
138
139     id   = skip_prefix(tty, "/dev/pts/");
140     line = skip_prefix(tty, "/dev/");
141
142     /* Create utmp entry for the given terminal. */
143     memset(&entry, 0, sizeof(entry));
144
145     snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
146     snprintf(entry.ut_id,   sizeof(entry.ut_id),   "%s", id);
147     snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
148
149     entry.ut_pid  = getpid();
150     entry.ut_type = type;
151     entry.ut_tv.tv_sec  = now.tv_sec;
152     entry.ut_tv.tv_usec = now.tv_usec;
153
154     /* Write the entry to the utmp file. */
155     setutxent();
156     if (!pututxline(&entry)) {
157         return 0;
158     }
159     endutxent();
160
161     return 1;
162 }
163 #endif
164 static int login(int ptm) {
165 #if defined(USE_UTEMPTER)
166     int result = utempter_add_record(ptm, NULL);
167     /* Exit value of utempter_*() is not correct on all systems, e.g.
168      * FreeBSD() always returns 0. Checking the utmpx database manually is
169      * difficult because we don't know the exact values for ut_id and ut_line,
170      * therefore we only check the return value on systems known to return a
171      * useful value. */
172 # ifndef __linux
173     result = 1;
174 # endif
175     return result;
176 #elif defined(USE_UTMPX)
177     return set_utmpx(USER_PROCESS, ptm);
178 #else
179 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
180 #endif
181 }
182 static int logout(int ptm) {
183 #if defined(USE_UTEMPTER)
184     int result = utempter_remove_record(ptm);
185     /* See above. */
186 # ifndef __linux
187     result = 1;
188 # endif
189     return result;
190 #elif defined(USE_UTMPX)
191     return set_utmpx(DEAD_PROCESS, ptm);
192 #else
193 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
194 #endif
195 }
196
197 static int wait_for_write(int fd, int timeout) {
198     fd_set rfds;
199     struct timeval tv;
200     int result;
201
202     FD_ZERO(&rfds);
203     FD_SET(fd, &rfds);
204
205     tv.tv_sec  = timeout;
206     tv.tv_usec = 0;
207
208     result = select(fd + 1, &rfds, NULL, NULL, &tv);
209     if (result < 0) {
210         perror("select");
211         return 0;
212     }
213     if (result == 0) {
214         /* Timeout. */
215         return 0;
216     }
217
218     /* Got more data to read. */
219     return 1;
220 }
221
222 #ifdef USE_UTMPX
223 static void drop_privileges(void) {
224     uid_t uid, ruid, euid, suid;
225     gid_t gid, rgid, egid, sgid;
226
227     uid = getuid();
228     gid = getgid();
229
230     /* Drop all privileges. */
231     if (setresgid(gid, gid, gid) != 0) {
232         perror("setresgid");
233         exit(EXIT_FAILURE);
234     }
235     if (setresuid(uid, uid, uid) != 0) {
236         perror("setresuid");
237         exit(EXIT_FAILURE);
238     }
239
240     /* Verify all privileges were dropped. */
241     if (getresuid(&ruid, &euid, &suid) != 0) {
242         perror("getresuid");
243         exit(EXIT_FAILURE);
244     }
245     if (getresgid(&rgid, &egid, &sgid) != 0) {
246         perror("getresgid");
247         exit(EXIT_FAILURE);
248     }
249     if (uid == ruid && uid == euid && uid == suid
250             && gid == rgid && gid == egid && gid == sgid) {
251         /* Everything fine. */
252         return;
253     }
254
255     fprintf(stderr, "failed to drop privileges, aborting\n");
256     exit(EXIT_FAILURE);
257 }
258 #endif
259
260 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
261     int fds[2];
262     FILE *fh;
263
264     pid_t pid;
265
266     if (pipe(fds) != 0) {
267         perror("pipe");
268         return;
269     }
270
271     fh = fdopen(fds[1] /* write side */, "w");
272     if (!fh) {
273         perror("fdopen");
274         close(fds[0]);
275         close(fds[1]);
276         return;
277     }
278
279     pid = fork();
280     if (pid < 0) {
281         perror("fork");
282         goto out;
283     } else if (pid == 0) {
284         /* child */
285
286 #ifdef USE_UTMPX
287         drop_privileges();
288 #endif
289
290         close(fds[1]); /* write side */
291
292         /* Pass read side as stdin to the program. */
293         if (dup2(fds[0], STDIN_FILENO) < 0) {
294             perror("dup2");
295             exit(EXIT_FAILURE);
296         }
297         close(fds[0]);
298
299         /* Double fork so `wall-notify` doesn't have to wait for the
300          * notification process to terminate. We can't just use
301          * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
302          * doesn't work if SIGCHLD is ignored. */
303         pid = fork();
304         if (pid < 0) {
305             perror("fork");
306             exit(EXIT_FAILURE);
307         } else if (pid == 0) {
308             execvp(argv[0], argv);
309             perror("execvp");
310             exit(EXIT_FAILURE);
311         }
312
313         exit(EXIT_SUCCESS);
314     }
315     /* father */
316
317     if (fwrite(buffer, 1, length, fh) != length) {
318         perror("fwrite");
319         /* continue to perform cleanup */
320     }
321
322 out:
323     close(fds[0]); /* read side */
324     fclose(fh);
325
326     if (waitpid(pid, NULL, 0) < 0) {
327         perror("waitpid");
328     }
329 }
330 static void handle_wall(int fd, char **argv) {
331     char buffer[4096];
332     ssize_t r;
333
334     assert(SSIZE_MAX <= SIZE_MAX);
335     while ((r = read(fd, buffer, sizeof(buffer))) > 0) {
336         size_t space;
337         ssize_t r2;
338
339         /* To prevent partial messages (sometimes it takes multiple reads to
340          * get the complete message) wait for a short time to get the rest of
341          * the message. */
342         space = sizeof(buffer) - (size_t)r;
343         while (space > 0 && wait_for_write(fd, 1 /* second */)) {
344             r2 = read(fd, buffer + r, space);
345             if (r2 <= 0) {
346                 break;
347             }
348             r += r2;
349             space -= (size_t)r2;
350         }
351
352         pass_buffer_to_program(buffer, (size_t)r, argv);
353     }
354 }
355 #ifndef DONT_USE_X11
356 static void *x11_event_loop_thread(void *unused) {
357     Display *display;
358     XEvent event;
359
360     (void)unused;
361
362     pthread_detach(pthread_self());
363
364     display = XOpenDisplay(NULL);
365     if (!display) {
366         fprintf(stderr, "failed to connect to X server\n");
367         exit(EXIT_FAILURE);
368     }
369
370     /* Do nothing. We just want to die if the X11 session is closed. */
371     while (1) {
372         XNextEvent(display, &event);
373     }
374 }
375 #endif
376
377 static void usage(const char *argv0) {
378     fprintf(stderr, "usage: %s [-X] [-m] [-v] <cmd args..>\n", argv0);
379     fprintf(stderr, "Pass wall messages to <cmd args..>.\n");
380     fprintf(stderr, "\n");
381     fprintf(stderr, "-X quit when the current X session terminates\n");
382     fprintf(stderr, "-m allow messages from normal users (like `mesg y`)\n");
383     fprintf(stderr, "-v display version\n");
384 #ifdef DONT_USE_X11
385     fprintf(stderr, "\n");
386     fprintf(stderr, "compiled without X11 support, -X disabled\n");
387 #endif
388     exit(EXIT_FAILURE);
389 }
390
391
392 int main(int argc, char **argv) {
393     int option, enable_x11, mesg_yes;
394     int ptm, pts;
395     char *name;
396
397     /* Don't display error messages for unknown options. */
398     opterr = 0;
399
400     enable_x11 = 0;
401     mesg_yes = 0;
402
403     /*
404      * Glibc violates POSIX by default and skips over non-option arguments and
405      * parses later arguments which look like options as well. But we want to
406      * pass everything except the options unmodified to execvp(). Prefixing
407      * the optstring with "+" fixes this behaver. This is not POSIX
408      * compatible, but the option should be ignored on other systems.
409      */
410     while ((option = getopt(argc, argv, "+Xmhv")) != -1) {
411         switch (option) {
412             case 'X':
413                 enable_x11 = 1;
414                 break;
415             case 'm':
416                 mesg_yes = 1;
417                 break;
418             case 'h':
419                 usage(argv[0]);
420                 break;
421             case 'v':
422                 printf("%s%s\n",
423                        PACKAGE_STRING,
424                        strlen(GIT_VERSION) ? " (Git " GIT_VERSION ")" : "");
425                 exit(0);
426                 break;
427             default:
428                 fprintf(stderr, "%s: unknown option '%s'!\n\n",
429                                 argv[0], argv[optind - 1]);
430                 usage(argv[0]);
431                 break;
432         }
433     }
434
435     /* No arguments remaining, abort. */
436     if (!argv[optind]) {
437         usage(argv[0]);
438     }
439     /* Arguments for notification program. */
440     argv += optind;
441
442     ptm = open_tty(mesg_yes);
443     if (ptm < 0) {
444         perror("open_tty");
445         exit(EXIT_FAILURE);
446     }
447     name = ptsname(ptm);
448     if (!name) {
449         perror("ptsname");
450         exit(EXIT_FAILURE);
451     }
452
453 #ifdef DEBUG
454     printf("%s\n", name);
455 #endif
456
457     /* We need to open the slave or reading from the master yields EOF after
458      * the first wall write to it. */
459     pts = open(name, O_RDWR | O_CLOEXEC);
460     if (pts < 0) {
461         perror(name);
462         exit(EXIT_FAILURE);
463     }
464
465     /* Start a thread which connects to X11. This way we die if the user logs
466      * out of its X11 session. */
467     if (enable_x11) {
468 #ifdef DONT_USE_X11
469         fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
470         usage(argv0);
471 #else
472         pthread_t tid;
473
474         if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
475             perror("pthread_create");
476             exit(EXIT_FAILURE);
477         }
478 #endif
479     }
480
481     /* Cleanup on signals. Necessary before login(). */
482     setup_signals();
483
484     if (!login(ptm)) {
485         perror("login");
486         exit(EXIT_FAILURE);
487     }
488
489     /* Main loop. Handle all wall messages sent to our PTY. */
490     handle_wall(ptm, argv);
491
492     if (!logout(ptm)) {
493         perror("logout");
494         exit(EXIT_FAILURE);
495     }
496
497     close(ptm);
498     close(pts);
499
500     return EXIT_SUCCESS;
501 }