/*
* Receive wall messages and pass them to a notification program via stdin.
*
* Copyright (C) 2014 Simon Ruderich
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef USE_UTEMPTER
# include
#endif
#ifdef USE_UTMPX
# include
#endif
#ifndef DONT_USE_X11
# include
# include
#endif
static sig_atomic_t signaled = 0;
static void sig_handler(int signal) {
(void)signal;
signaled = 1;
}
static void setup_signals(void) {
struct sigaction action;
memset(&action, 0, sizeof(action));
sigemptyset(&action.sa_mask);
action.sa_handler = sig_handler;
/* Handle all important signals which might be sent to us so we break out
* of the read()-loop below and can perform our cleanup. */
sigaction(SIGHUP, &action, NULL);
sigaction(SIGINT, &action, NULL);
sigaction(SIGQUIT, &action, NULL);
sigaction(SIGUSR1, &action, NULL);
sigaction(SIGUSR2, &action, NULL);
}
static int set_cloexec(int fd) {
int flags = fcntl(fd, F_GETFD);
if (flags == -1) {
return 0;
}
if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
return 0;
}
return 1;
}
static int open_tty(int public) {
int ptm;
const char *name;
ptm = posix_openpt(O_RDWR);
if (ptm < 0) {
return -1;
}
if (grantpt(ptm) != 0) {
return -1;
}
if (!set_cloexec(ptm)) {
return -1;
}
/* Prevent write access for other users so they can't use wall to send
* messages to this program. */
if (!public) {
name = ptsname(ptm);
if (!name) {
return -1;
}
if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
return -1;
}
}
if (unlockpt(ptm) != 0) {
return -1;
}
return ptm;
}
#ifdef USE_UTMPX
static const char *skip_prefix(const char *string, const char *prefix) {
size_t length = strlen(prefix);
if (!strncmp(string, prefix, length)) {
return string + length;
} else {
return string;
}
}
static int set_utmpx(short type, int ptm) {
struct utmpx entry;
const char *tty, *user, *id, *line;
struct timeval now;
user = getpwuid(getuid())->pw_name;
gettimeofday(&now, NULL);
tty = ptsname(ptm);
if (!tty) {
return 0;
}
id = skip_prefix(tty, "/dev/pts/");
line = skip_prefix(tty, "/dev/");
/* Create utmp entry for the given terminal. */
memset(&entry, 0, sizeof(entry));
snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
snprintf(entry.ut_id, sizeof(entry.ut_id), "%s", id);
snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
entry.ut_pid = getpid();
entry.ut_type = type;
entry.ut_tv.tv_sec = now.tv_sec;
entry.ut_tv.tv_usec = now.tv_usec;
/* Write the entry to the utmp file. */
setutxent();
if (!pututxline(&entry)) {
return 0;
}
endutxent();
return 1;
}
#endif
static int login(int ptm) {
#if defined(USE_UTEMPTER)
int result = utempter_add_record(ptm, NULL);
/* Exit value of utempter_*() is not correct on all systems, e.g.
* FreeBSD() always returns 0. Checking the utmpx database manually is
* difficult because we don't know the exact values for ut_id and ut_line,
* therefore we only check the return value on systems known to return a
* useful value. */
# ifndef __linux
result = 1;
# endif
return result;
#elif defined(USE_UTMPX)
return set_utmpx(USER_PROCESS, ptm);
#else
# error "neither USE_UTEMPTER nor USE_UTMPX defined"
#endif
}
static int logout(int ptm) {
#if defined(USE_UTEMPTER)
int result = utempter_remove_record(ptm);
/* See above. */
# ifndef __linux
result = 1;
# endif
return result;
#elif defined(USE_UTMPX)
return set_utmpx(DEAD_PROCESS, ptm);
#else
# error "neither USE_UTEMPTER nor USE_UTMPX defined"
#endif
}
static int wait_for_write(int fd, int timeout) {
fd_set rfds;
struct timeval tv;
int result;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
tv.tv_sec = timeout;
tv.tv_usec = 0;
result = select(fd + 1, &rfds, NULL, NULL, &tv);
if (result < 0) {
perror("select");
return 0;
}
if (result == 0) {
/* Timeout. */
return 0;
}
/* Got more data to read. */
return 1;
}
#ifdef USE_UTMPX
static void drop_privileges(void) {
uid_t uid, ruid, euid, suid;
gid_t gid, rgid, egid, sgid;
uid = getuid();
gid = getgid();
/* Drop all privileges. */
if (setresgid(gid, gid, gid) != 0) {
perror("setresgid");
exit(EXIT_FAILURE);
}
if (setresuid(uid, uid, uid) != 0) {
perror("setresuid");
exit(EXIT_FAILURE);
}
/* Verify all privileges were dropped. */
if (getresuid(&ruid, &euid, &suid) != 0) {
perror("getresuid");
exit(EXIT_FAILURE);
}
if (getresgid(&rgid, &egid, &sgid) != 0) {
perror("getresgid");
exit(EXIT_FAILURE);
}
if (uid == ruid && uid == euid && uid == suid
&& gid == rgid && gid == egid && gid == sgid) {
/* Everything fine. */
return;
}
fprintf(stderr, "failed to drop privileges, aborting\n");
exit(EXIT_FAILURE);
}
#endif
static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
int fds[2];
FILE *fh;
pid_t pid;
if (pipe(fds) != 0) {
perror("pipe");
return;
}
fh = fdopen(fds[1] /* write side */, "w");
if (!fh) {
perror("fdopen");
close(fds[0]);
close(fds[1]);
return;
}
pid = fork();
if (pid < 0) {
perror("fork");
goto out;
} else if (pid == 0) {
/* child */
#ifdef USE_UTMPX
drop_privileges();
#endif
close(fds[1]); /* write side */
/* Pass read side as stdin to the program. */
if (dup2(fds[0], STDIN_FILENO) < 0) {
perror("dup2");
exit(EXIT_FAILURE);
}
close(fds[0]);
/* Double fork so `wall-notify` doesn't have to wait for the
* notification process to terminate. We can't just use
* signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
* doesn't work if SIGCHLD is ignored. */
pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
/* father */
if (fwrite(buffer, 1, length, fh) != length) {
perror("fwrite");
/* continue to perform cleanup */
}
out:
close(fds[0]); /* read side */
fclose(fh);
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid");
}
}
static void handle_wall(int fd, char **argv) {
char buffer[4096];
ssize_t r;
assert(SSIZE_MAX <= SIZE_MAX);
while (!signaled && (r = read(fd, buffer, sizeof(buffer))) > 0) {
size_t space;
ssize_t r2;
/* To prevent partial messages (sometimes it takes multiple reads to
* get the complete message) wait for a short time to get the rest of
* the message. */
space = sizeof(buffer) - (size_t)r;
while (space > 0 && wait_for_write(fd, 1 /* second */)) {
r2 = read(fd, buffer + r, space);
if (r2 <= 0) {
break;
}
r += r2;
space -= (size_t)r2;
}
pass_buffer_to_program(buffer, (size_t)r, argv);
}
}
#ifndef DONT_USE_X11
static void *x11_event_loop_thread(void *unused) {
Display *display;
XEvent event;
(void)unused;
pthread_detach(pthread_self());
display = XOpenDisplay(NULL);
if (!display) {
fprintf(stderr, "failed to connect to X server\n");
exit(EXIT_FAILURE);
}
/* Do nothing. We just want to die if the X11 session is closed. */
while (1) {
XNextEvent(display, &event);
}
}
#endif
static void usage(const char *argv0) {
fprintf(stderr, "usage: %s [-X] [-m] [-v] \n", argv0);
fprintf(stderr, "Pass wall messages to .\n");
fprintf(stderr, "\n");
fprintf(stderr, "-X quit when the current X session terminates\n");
fprintf(stderr, "-m allow messages from normal users (like `mesg y`)\n");
fprintf(stderr, "-v display version\n");
#ifdef DONT_USE_X11
fprintf(stderr, "\n");
fprintf(stderr, "compiled without X11 support, -X disabled\n");
#endif
exit(EXIT_FAILURE);
}
int main(int argc, char **argv) {
int option, enable_x11, mesg_yes;
const char *argv0;
int ptm, pts;
char *name;
/* Don't display error messages for unknown options. */
opterr = 0;
enable_x11 = 0;
mesg_yes = 0;
argv0 = argv[0];
/*
* Glibc violates POSIX by default and skips over non-option arguments and
* parses later arguments which look like options as well. But we want to
* pass everything except the options unmodified to execvp(). Prefixing
* the optstring with "+" fixes this behaver. This is not POSIX
* compatible, but the option should be ignored on other systems.
*/
while ((option = getopt(argc, argv, "+Xmhv")) != -1) {
switch (option) {
case 'X':
enable_x11 = 1;
break;
case 'm':
mesg_yes = 1;
break;
case 'h':
usage(argv0);
break;
case 'v':
printf("%s%s\n",
PACKAGE_STRING,
strlen(GIT_VERSION) ? " (Git " GIT_VERSION ")" : "");
exit(EXIT_SUCCESS);
break;
default:
fprintf(stderr, "%s: unknown option '%s'!\n\n",
argv0, argv[optind - 1]);
usage(argv0);
break;
}
}
/* No arguments remaining, abort. */
if (!argv[optind]) {
usage(argv0);
}
/* Arguments for notification program. */
argv += optind;
ptm = open_tty(mesg_yes);
if (ptm < 0) {
perror("open_tty");
exit(EXIT_FAILURE);
}
name = ptsname(ptm);
if (!name) {
perror("ptsname");
exit(EXIT_FAILURE);
}
#ifdef DEBUG
printf("%s\n", name);
#endif
/* We need to open the slave or reading from the master yields EOF after
* the first wall write to it. */
pts = open(name, O_RDWR | O_CLOEXEC);
if (pts < 0) {
perror(name);
exit(EXIT_FAILURE);
}
/* Start a thread which connects to X11. This way we die if the user logs
* out of its X11 session. */
if (enable_x11) {
#ifdef DONT_USE_X11
fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
usage(argv0);
#else
pthread_t tid;
if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
#endif
}
/* Cleanup on signals. Necessary before login(). */
setup_signals();
if (!login(ptm)) {
perror("login");
exit(EXIT_FAILURE);
}
/* Main loop. Handle all wall messages sent to our PTY. */
handle_wall(ptm, argv);
if (!logout(ptm)) {
perror("logout");
exit(EXIT_FAILURE);
}
close(ptm);
close(pts);
return EXIT_SUCCESS;
}