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