/*
* at.c : Put file into atd queue
* Copyright (C) 1993, 1994, 1995, 1996, 1997 Thomas Koenig
* Copyright (C) 2002, 2005 Ryan Murray
*
* Atrun & Atq modifications
* Copyright (C) 1993 David Parsons
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
/* System Headers */
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
#include <ctype.h>
#ifdef HAVE_DIRENT_H
#include <dirent.h>
#elif HAVE_SYS_DIRENT_H
#include <sys/dirent.h>
#elif HAVE_SYS_DIR_H
#include <sys/dir.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#elif defined(HAVE_SYS_FCNTL_H)
#include <sys/fcntl.h>
#endif
#include <pwd.h>
#include <grp.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
/* Local headers */
#include "at.h"
#include "panic.h"
#include "parsetime.h"
#include "perm.h"
#include "posixtm.h"
#include "privs.h"
/* Macros */
#ifndef ATJOB_MX
#define ATJOB_MX 255
#endif
#define ALARMC 10 /* Number of seconds to wait for timeout */
#define SIZE 255
#define TIMEFORMAT_POSIX "%a %b %e %T %Y"
#define TIMESIZE 50
#define DEFAULT_QUEUE 'a'
#define BATCH_QUEUE 'b'
enum {
ATQ, BATCH, ATRM, AT, CAT
}; /* what program we want to run */
/* Global variables */
uid_t real_uid, effective_uid;
gid_t real_gid, effective_gid;
uid_t daemon_uid = (uid_t) - 3;
gid_t daemon_gid = (gid_t) - 3;
/* File scope variables */
char *no_export[] =
{
"TERM", "DISPLAY", "_", "SHELLOPTS", "BASH_VERSINFO", "EUID", "GROUPS", "PPID", "UID"
};
static int send_mail = 0;
/* External variables */
extern char **environ;
int fcreated;
char *namep;
char atfile[] = ATJOB_DIR "/12345678901234";
char *atinput = (char *) 0; /* where to get input from */
char atqueue = 0; /* which queue to examine for jobs (atq) */
char atverify = 0; /* verify time instead of queuing job */
/* Function declarations */
static void sigc(int signo);
static void alarmc(int signo);
static char *cwdname(void);
static void writefile(time_t runtimer, char queue);
static void list_jobs(void);
/* Signal catching functions */
static RETSIGTYPE
sigc(int signo)
{
/* If the user presses ^C, remove the spool file and exit
*/
if (fcreated) {
PRIV_START
/*
We need the unprivileged uid here since the file is owned by the real
(not effective) uid.
*/
unlink(atfile);
PRIV_END
}
exit(EXIT_FAILURE);
}
static void
alarmc(int signo)
{
/* Time out after some seconds
*/
panic("File locking timed out");
}
/* Local functions */
static char *
cwdname(void)
{
/* Read in the current directory; the name will be overwritten on
* subsequent calls.
*/
static char *ptr = NULL;
static size_t size = SIZE;
if (ptr == NULL)
ptr = (char *) mymalloc(size);
while (1) {
if (ptr == NULL)
panic("Out of memory");
if (getcwd(ptr, size - 1) != NULL)
return ptr;
if (errno != ERANGE)
perr("Cannot get current working directory");
free(ptr);
size += SIZE;
ptr = (char *) mymalloc(size);
}
}
static long
nextjob()
{
long jobno;
FILE *fid;
jobno = 0;
fid = fopen(LFILE, "r+");
if (fid != NULL) {
fscanf(fid, "%5lx", &jobno);
rewind(fid);
} else {
fid = fopen(LFILE, "w");
if (fid == NULL)
return EOF;
}
jobno = (1 + jobno) % 0xfffff; /* 2^20 jobs enough? */
fprintf(fid, "%05lx\n", jobno);
if (ferror(fid))
jobno = EOF;
if (fclose(fid) != 0)
jobno = EOF;
return jobno;
}
static void
writefile(time_t runtimer, char queue)
{
/* This does most of the work if at or batch are invoked for writing a job.
*/
long jobno;
char *ap, *ppos, *mailname;
struct passwd *pass_entry;
struct stat statbuf;
int fd, lockdes, fd2;
FILE *fp, *fpin;
struct sigaction act;
char **atenv;
int ch;
mode_t cmask;
struct flock lock;
struct tm *runtime;
char timestr[TIMESIZE];
pid_t pid;
int istty;
int kill_errno;
int rc;
int mailsize = 128;
struct timeval tv;
struct timezone tz;
long int i;
gettimeofday(&tv, &tz);
srandom(getpid()+tv.tv_usec);
/* Install the signal handler for SIGINT; terminate after removing the
* spool file if necessary
*/
memset(&act, 0, sizeof act);
act.sa_handler = sigc;
sigemptyset(&(act.sa_mask));
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
ppos = atfile + strlen(ATJOB_DIR) + 1;
#ifdef _SC_LOGIN_NAME_MAX
errno = 0;
rc = sysconf(_SC_LOGIN_NAME_MAX);
if (rc > 0)
mailsize = rc;
#else
# ifdef LOGIN_NAME_MAX
mailsize = LOGIN_NAME_MAX;
# endif
#endif
/* Loop over all possible file names for running something at this
* particular time, see if a file is there; the first empty slot at any
* particular time is used. Lock the file LFILE first to make sure
* we're alone when doing this.
*/
PRIV_START
if ((lockdes = open(LFILE, O_WRONLY)) < 0)
perr("Cannot open lockfile " LFILE);
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
act.sa_handler = alarmc;
sigemptyset(&(act.sa_mask));
act.sa_flags = 0;
/* Set an alarm so a timeout occurs after ALARMC seconds, in case
* something is seriously broken.
*/
sigaction(SIGALRM, &act, NULL);
alarm(ALARMC);
fcntl(lockdes, F_SETLKW, &lock);
alarm(0);
if ((jobno = nextjob()) == EOF)
perr("Cannot generate job number");
(void)snprintf(ppos, sizeof(atfile) - (ppos - atfile),
"%c%5lx%8lx", queue, jobno, (unsigned long) (runtimer / 60));
for (ap = ppos; *ap != '\0'; ap++)
if (*ap == ' ')
*ap = '0';
if (stat(atfile, &statbuf) != 0)
if (errno != ENOENT)
perr("Cannot access " ATJOB_DIR);
/* Create the file. The x bit is only going to be set after it has
* been completely written out, to make sure it is not executed in the
* meantime. To make sure they do not get deleted, turn off their r
* bit. Yes, this is a kluge.
*/
cmask = umask(S_IRUSR | S_IWUSR | S_IXUSR);
if ((seteuid(effective_uid)) < 0)
perr("Error in seteuid: %s", errno);
if ((fd = open(atfile, O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, S_IRUSR)) == -1)
perr("Cannot create atjob file %.500s", atfile);
if ((fd2 = dup(fd)) < 0)
perr("Error in dup() of job file");
if (fchown(fd2, real_uid, real_gid) != 0)
perr("Cannot give real_uid and real_gid the file");
PRIV_END
/* We've successfully created the file; let's set the flag so it
* gets removed in case of an interrupt or error.
*/
fcreated = 1;
/* Now we can release the lock, so other people can access it
*/
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
fcntl(lockdes, F_SETLKW, &lock);
close(lockdes);
if ((fp = fdopen(fd, "w")) == NULL)
panic("Cannot reopen atjob file");
/* Get the userid to mail to, first by trying getlogin(), which reads
* /var/run/utmp, then from LOGNAME, finally from getpwuid().
*/
mailname = getlogin();
if (mailname == NULL)
mailname = getenv("LOGNAME");
if (mailname == NULL || mailname[0] == '\0' || getpwnam(mailname) == NULL) {
pass_entry = getpwuid(real_uid);
if (pass_entry != NULL)
mailname = pass_entry->pw_name;
}
if ((mailname == NULL) || (mailname[0] == '\0')
|| (strlen(mailname) > mailsize) ) {
panic("Cannot find username to mail output to");
}
if (atinput != (char *) NULL) {
fpin = freopen(atinput, "r", stdin);
if (fpin == NULL)
perr("Cannot open input file %.500s", atinput);
}
fprintf(fp, "#!/bin/sh\n# atrun uid=%d gid=%d\n# mail %s %d\n",
real_uid, real_gid, mailname, send_mail);
/* Write out the umask at the time of invocation
*/
fprintf(fp, "umask %lo\n", (unsigned long) cmask);
/* Write out the environment. Anything that may look like a
* special character to the shell is quoted, except for \n, which is
* done with a pair of ""'s. Dont't export the no_export list (such
* as TERM or DISPLAY) because we don't want these.
*/
for (atenv = environ; *atenv != NULL; atenv++) {
int export = 1;
char *eqp;
/* Only accept alphanumerics and underscore in variable names.
* Also require the name to not start with a digit.
* Some shells don't like other variable names.
*/
{
char *p = *atenv;
if (isdigit(*p))
export = 0;
for (; *p != '=' && *p != '\0'; ++p) {
if (!isalnum(*p) && *p != '_') {
export = 0;
break;
}
}
}
eqp = strchr(*atenv, '=');
if (ap == NULL)
eqp = *atenv;
else {
unsigned int i;
for (i = 0; i < sizeof(no_export) / sizeof(no_export[0]); i++) {
export = export
&& ((((size_t) (eqp - *atenv)) != strlen(no_export[i]))
||(strncmp(*atenv, no_export[i],(size_t) (eqp - *atenv)) != 0)
);
}
eqp++;
}
if (export) {
fwrite(*atenv, sizeof(char), eqp - *atenv, fp);
for (ap = eqp; *ap != '\0'; ap++) {
if (*ap == '\n')
fprintf(fp, "\"\n\"");
else {
if (!isalnum(*ap)) {
switch (*ap) {
case '%':
case '/':
case '{':
case '[':
case ']':
case '=':
case '}':
case '@':
case '+':
case '#':
case ',':
case '.':
case ':':
case '-':
case '_':
break;
default:
fputc('\\', fp);
break;
}
}
fputc(*ap, fp);
}
}
fputs("; export ", fp);
fwrite(*atenv, sizeof(char), eqp - *atenv - 1, fp);
fputc('\n', fp);
}
}
/* Cd to the directory at the time and write out all the
* commands the user supplies from stdin.
*/
fprintf(fp, "cd ");
for (ap = cwdname(); *ap != '\0'; ap++) {
if (*ap == '\n')
fprintf(fp, "\"\n\"");
else {
if (*ap != '/' && !isalnum(*ap))
fputc('\\', fp);
fputc(*ap, fp);
}
}
/* Test cd's exit status: die if the original directory has been
* removed, become unreadable or whatever
*/
fprintf(fp, " || {\n\t echo 'Execution directory "
"inaccessible' >&2\n\t exit 1\n}\n");
i = random();
fprintf(fp, "${SHELL:-/bin/sh} << \'marcinDELIMITER%08lx\'\n", i);
istty = isatty(fileno(stdin));
if (istty) {
fprintf(stderr, "at> ");
fflush(stderr);
}
while ((ch = getchar()) != EOF) {
fputc(ch, fp);
if (ch == '\n' && istty) {
fprintf(stderr, "at> ");
fflush(stderr);
}
}
if (istty) {
fprintf(stderr, "<EOT>\n");
}
fprintf(fp, "marcinDELIMITER%08lx\n", i);
if (ferror(fp))
panic("Output error");
fflush(fp);
if (ferror(fp))
panic("Output error");
if (ferror(stdin))
panic("Input error");
if (fclose(fp) != 0)
panic("Output error");
/* Set the x bit so that we're ready to start executing
*/
if (fchmod(fd2, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
perr("Cannot give away file");
close(fd2);
runtime = localtime(&runtimer);
strftime(timestr, TIMESIZE, TIMEFORMAT_POSIX, runtime);
fprintf(stderr, "job %ld at %s\n", jobno, timestr);
/* Signal atd, if present. Usual precautions taken... */
fd = open(PIDFILE, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "Can't open " PIDFILE " to signal atd. No atd running?\n");
return;
}
if (fstat(fd, &statbuf) == -1)
return;
if ((statbuf.st_uid != 0) || !S_ISREG(statbuf.st_mode) ||
(statbuf.st_mode & (S_IWGRP | S_IWOTH)))
return;
fp = fdopen(fd, "r");
if (fp == NULL)
return;
if (fscanf(fp, "%d", &pid) != 1)
return;
kill_errno = 0;
PRIV_START
if (kill(pid, SIGHUP) == -1)
kill_errno = errno;
PRIV_END
switch (kill_errno) {
case 0:
break;
case EINVAL:
panic("kill returned EINVAL");
break;
case EPERM:
fprintf(stderr,"Can't signal atd (permission denied)\n");
break;
case ESRCH:
fprintf(stderr, "Warning: at daemon not running\n");
break;
default:
panic("kill returned impossible error number");
break;
}
return;
}
static void
list_jobs(void)
{
/* List all a user's jobs in the queue, by looping through ATJOB_DIR,
* or everybody's if we are root
*/
DIR *spool;
struct dirent *dirent;
struct stat buf;
struct tm *runtime;
unsigned long ctm;
char queue;
long jobno;
time_t runtimer;
char timestr[TIMESIZE];
struct passwd *pwd;
PRIV_START
if (chdir(ATJOB_DIR) != 0)
perr("Cannot change to " ATJOB_DIR);
if ((spool = opendir(".")) == NULL)
perr("Cannot open " ATJOB_DIR);
/* Loop over every file in the directory
*/
while ((dirent = readdir(spool)) != NULL) {
if (stat(dirent->d_name, &buf) != 0)
perr("Cannot stat in " ATJOB_DIR);
/* See it's a regular file and is the user's */
if (!S_ISREG(buf.st_mode)
|| ((buf.st_uid != real_uid) && !(real_uid == 0))
|| atverify)
continue;
if (sscanf(dirent->d_name, "%c%5lx%8lx", &queue, &jobno, &ctm) != 3)
continue;
if (atqueue && (queue != atqueue))
continue;
runtimer = 60 * (time_t) ctm;
runtime = localtime(&runtimer);
strftime(timestr, TIMESIZE, TIMEFORMAT_POSIX, runtime);
if ((pwd = getpwuid(buf.st_uid)))
printf("%ld\t%s %c %s\n", jobno, timestr, queue, pwd->pw_name);
else
printf("%ld\t%s %c\n", jobno, timestr, queue);
}
PRIV_END
}
static int
process_jobs(int argc, char **argv, int what)
{
/* Delete every argument (job - ID) given
*/
int i;
struct stat buf;
DIR *spool;
struct dirent *dirent;
unsigned long ctm;
char queue;
long jobno;
int rc = EXIT_SUCCESS;
int done;
for (i = optind; i < argc; i++) {
done = 0;
PRIV_START
if (chdir(ATJOB_DIR) != 0)
perr("Cannot change to " ATJOB_DIR);
if ((spool = opendir(".")) == NULL)
perr("Cannot open " ATJOB_DIR);
PRIV_END
/* Loop over every file in the directory
*/
while ((dirent = readdir(spool)) != NULL) {
PRIV_START
if (stat(dirent->d_name, &buf) != 0)
perr("Cannot stat in " ATJOB_DIR);
PRIV_END
if (sscanf(dirent->d_name, "%c%5lx%8lx", &queue, &jobno, &ctm) != 3)
continue;
if (atoi(argv[i]) == jobno) {
if ((buf.st_uid != real_uid) && !(real_uid == 0)) {
fprintf(stderr, "%s: Not owner\n", argv[i]);
exit(EXIT_FAILURE);
}
switch (what) {
case ATRM:
/*
We need the unprivileged uid here since the file is owned by the real
(not effective) uid.
*/
PRIV_START
if (queue == '=') {
fprintf(stderr, "Warning: deleting running job\n");
}
if (unlink(dirent->d_name) != 0) {
perr("Cannot unlink %.500s", dirent->d_name);
rc = EXIT_FAILURE;
}
PRIV_END
done = 1;
break;
case CAT:
{
FILE *fp;
int ch;
PRIV_START
fp = fopen(dirent->d_name, "r");
if (fp) {
while ((ch = getc(fp)) != EOF) {
putchar(ch);
}
done = 1;
}
else {
perr("Cannot open %.500s", dirent->d_name);
rc = EXIT_FAILURE;
}
PRIV_END
}
break;
default:
fprintf(stderr,
"Internal error, process_jobs = %d\n", what);
exit(EXIT_FAILURE);
break;
}
}
}
closedir(spool);
if (done != 1) {
fprintf(stderr, "Cannot find jobid %s\n", argv[i] );
rc = EXIT_FAILURE;
}
}
return rc;
} /* delete_jobs */
/* Global functions */
void *
mymalloc(size_t n)
{
void *p;
if ((p = malloc(n)) == (void *) 0) {
fprintf(stderr, "Virtual memory exhausted\n");
exit(EXIT_FAILURE);
}
return p;
}
int
main(int argc, char **argv)
{
int c;
char queue = DEFAULT_QUEUE;
char queue_set = 0;
char *pgm;
int program = AT; /* our default program */
char *options = "q:f:MmbvlrdhVct:"; /* default options for at */
int disp_version = 0;
time_t timer = 0;
struct passwd *pwe;
struct group *ge;
RELINQUISH_PRIVS
if ((pwe = getpwnam(DAEMON_USERNAME)) == NULL)
perr("Cannot get uid for " DAEMON_USERNAME);
daemon_uid = pwe->pw_uid;
if ((ge = getgrnam(DAEMON_GROUPNAME)) == NULL)
perr("Cannot get gid for " DAEMON_GROUPNAME);
daemon_gid = ge->gr_gid;
/* Eat any leading paths
*/
if ((pgm = strrchr(argv[0], '/')) == NULL)
pgm = argv[0];
else
pgm++;
namep = pgm;
/* find out what this program is supposed to do
*/
if (strcmp(pgm, "atq") == 0) {
program = ATQ;
options = "hq:V";
} else if (strcmp(pgm, "atrm") == 0) {
program = ATRM;
options = "hV";
}
/* process whatever options we can process
*/
opterr = 1;
while ((c = getopt(argc, argv, options)) != EOF)
switch (c) {
case 'h':
usage();
exit (0);
case 'v': /* verify time settings */
atverify = 1;
break;
case 'm': /* send mail when job is complete */
send_mail = 1;
break;
case 'M': /* don't send mail, even when job failed */
send_mail = -1;
break;
case 'f':
atinput = optarg;
break;
case 'q': /* specify queue */
if (strlen(optarg) > 1)
usage();
atqueue = queue = *optarg;
if (!(islower(queue) || isupper(queue)) & (queue != '='))
usage();
queue_set = 1;
break;
case 'r':
case 'd':
if (program != AT)
usage();
program = ATRM;
options = "V";
break;
case 'l':
if (program != AT)
usage();
program = ATQ;
options = "q:V";
break;
case 'b':
if (program != AT)
usage();
program = BATCH;
options = "";
break;
case 'V':
disp_version = 1;
break;
case 'c':
program = CAT;
options = "";
break;
case 't':
if (!posixtime(&timer, optarg, PDS_LEADING_YEAR | PDS_CENTURY | PDS_SECONDS)) {
fprintf(stderr, "invalid date format: %s\n", optarg);
exit(EXIT_FAILURE);
}
/* drop seconds */
timer -= timer % 60;
break;
default:
usage();
break;
}
/* end of options eating
*/
if (disp_version) {
fprintf(stderr, "at version " VERSION "\n");
if (argc == 2)
exit(EXIT_SUCCESS);
}
/* select our program
*/
if (!check_permission()) {
fprintf(stderr, "You do not have permission to use %.100s.\n", namep);
exit(EXIT_FAILURE);
}
switch (program) {
int i;
case ATQ:
REDUCE_PRIV(daemon_uid, daemon_gid)
list_jobs();
break;
case ATRM:
REDUCE_PRIV(daemon_uid, daemon_gid)
if (argc > optind) {
for (i = optind; i < argc ; i++ )
if (strspn(argv[i],"0123456789") != strlen(argv[i])) {
fprintf(stderr,"at: unknown jobid: %s\n", argv[i]);
exit(EXIT_FAILURE);
}
return process_jobs(argc, argv, ATRM);
}
else
usage();
break;
case CAT:
if (argc > optind) {
for (i = optind; i < argc ; i++ )
if (strspn(argv[i],"0123456789") != strlen(argv[i])) {
fprintf(stderr,"at: unknown jobid: %s", argv[i]);
exit(EXIT_FAILURE);
}
return process_jobs(argc, argv, CAT);
}
else
usage();
break;
case AT:
if (argc > optind) {
if (timer != 0) {
fprintf(stderr, "Cannot give time twice.\n");
exit(EXIT_FAILURE);
}
timer = parsetime(time(0), argc - optind, argv + optind);
}
if (timer == 0) {
fprintf(stderr, "Garbled time\n");
exit(EXIT_FAILURE);
}
if (atverify) {
struct tm *tm = localtime(&timer);
fprintf(stderr, "%s\n", asctime(tm));
}
/* POSIX.2 allows the shell specified by the user's SHELL environment
variable, the login shell from the user's password database entry,
or /bin/sh to be the command interpreter that processes the at-job.
It also alows a warning diagnostic to be printed. Because of the
possible variance, we always output the diagnostic. */
fprintf(stderr, "warning: commands will be executed using /bin/sh\n");
writefile(timer, queue);
break;
case BATCH:
if (queue_set)
queue = toupper(queue);
else
queue = BATCH_QUEUE;
if (argc > optind) {
if (timer != 0) {
fprintf(stderr, "Cannot give time twice.\n");
exit(EXIT_FAILURE);
}
timer = parsetime(time(0), argc, argv);
} else if (timer == 0)
timer = time(NULL);
if (atverify) {
struct tm *tm = localtime(&timer);
fprintf(stderr, "%s\n", asctime(tm));
}
writefile(timer, queue);
break;
default:
panic("Internal error");
break;
}
exit(EXIT_SUCCESS);
}