1/*	$OpenBSD: at.c,v 1.84 2023/03/08 04:43:10 guenther Exp $	*/
2
3/*
4 *  at.c : Put file into atrun queue
5 *  Copyright (C) 1993, 1994  Thomas Koenig
6 *
7 *  Atrun & Atq modifications
8 *  Copyright (C) 1993  David Parsons
9 *
10 *  Traditional BSD behavior and other significant modifications
11 *  Copyright (C) 2002-2003  Todd C. Miller
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 * 1. Redistributions of source code must retain the above copyright
17 *    notice, this list of conditions and the following disclaimer.
18 * 2. The name of the author(s) may not be used to endorse or promote
19 *    products derived from this software without specific prior written
20 *    permission.
21 *
22 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
23 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
24 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
26 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 */
33
34#include <sys/types.h>
35#include <sys/stat.h>
36
37#include <bitstring.h>                  /* for structs.h */
38#include <ctype.h>
39#include <dirent.h>
40#include <err.h>
41#include <errno.h>
42#include <fcntl.h>
43#include <limits.h>
44#include <pwd.h>
45#include <signal.h>
46#include <stdarg.h>
47#include <stdio.h>
48#include <stdlib.h>
49#include <string.h>
50#include <syslog.h>
51#include <time.h>
52#include <unistd.h>
53
54#include "pathnames.h"
55#include "macros.h"
56#include "structs.h"
57#include "funcs.h"
58#include "globals.h"
59
60#include "at.h"
61
62#define ALARMC 10		/* Number of seconds to wait for timeout */
63#define TIMESIZE 50		/* Size of buffer passed to strftime() */
64
65/* Variables to remove from the job's environment. */
66char *no_export[] =
67{
68	"TERM", "TERMCAP", "DISPLAY", "_", "SHELLOPTS", "BASH_VERSINFO",
69	"EUID", "GROUPS", "PPID", "UID", "SSH_AUTH_SOCK", "SSH_AGENT_PID",
70};
71
72static int program = AT;	/* default program mode */
73static char atfile[PATH_MAX];	/* path to the at spool file */
74static char user_name[MAX_UNAME];/* invoking user name */
75static int fcreated;		/* whether or not we created the file yet */
76static char atqueue = 0;	/* which queue to examine for jobs (atq) */
77static char vflag = 0;		/* show completed but unremoved jobs (atq) */
78static char force = 0;		/* suppress errors (atrm) */
79static char interactive = 0;	/* interactive mode (atrm) */
80static int send_mail = 0;	/* whether we are sending mail */
81static uid_t user_uid;		/* user's real uid */
82static gid_t user_gid;		/* user's real gid */
83static gid_t spool_gid;		/* gid for writing to at spool */
84
85static void sigc(int);
86static void writefile(const char *, time_t, char);
87static void list_jobs(int, char **, int, int);
88static time_t ttime(char *);
89static __dead void fatal(const char *, ...)
90    __attribute__((__format__ (printf, 1, 2)));
91static __dead void fatalx(const char *, ...)
92    __attribute__((__format__ (printf, 1, 2)));
93static __dead void usage(void);
94static int rmok(long long);
95time_t parsetime(int, char **);
96
97/*
98 * Something fatal has happened, print error message and exit.
99 */
100static __dead void
101fatal(const char *fmt, ...)
102{
103	va_list ap;
104
105	va_start(ap, fmt);
106	vwarn(fmt, ap);
107	va_end(ap);
108
109	if (fcreated)
110		unlink(atfile);
111
112	exit(EXIT_FAILURE);
113}
114
115/*
116 * Something fatal has happened, print error message and exit.
117 */
118static __dead void
119fatalx(const char *fmt, ...)
120{
121	va_list ap;
122
123	va_start(ap, fmt);
124	vwarnx(fmt, ap);
125	va_end(ap);
126
127	if (fcreated)
128		unlink(atfile);
129
130	exit(EXIT_FAILURE);
131}
132
133static void
134sigc(int signo)
135{
136	/* If the user presses ^C, remove the spool file and exit. */
137	if (fcreated)
138		(void)unlink(atfile);
139
140	_exit(EXIT_FAILURE);
141}
142
143static int
144strtot(const char *nptr, char **endptr, time_t *tp)
145{
146	long long ll;
147
148	errno = 0;
149	ll = strtoll(nptr, endptr, 10);
150	if (*endptr == nptr)
151		return (-1);
152	if (ll < 0 || (errno == ERANGE && ll == LLONG_MAX) || (time_t)ll != ll)
153		return (-1);
154	*tp = (time_t)ll;
155	return (0);
156}
157
158static int
159newjob(time_t runtimer, int queue)
160{
161	int fd, i;
162
163	/*
164	 * If we have a collision, try shifting the time by up to
165	 * two minutes.  Perhaps it would be better to try different
166	 * queues instead...
167	 */
168	for (i = 0; i < 120; i++) {
169		snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL,
170		    (long long)runtimer, queue);
171		fd = open(atfile, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR);
172		if (fd >= 0)
173			return (fd);
174		runtimer++;
175	}
176	return (-1);
177}
178
179/*
180 * This does most of the work if at or batch are invoked for
181 * writing a job.
182 */
183static void
184writefile(const char *cwd, time_t runtimer, char queue)
185{
186	const char *ap;
187	char *mailname, *shell;
188	char timestr[TIMESIZE];
189	struct passwd *pass_entry;
190	struct tm runtime;
191	int fd;
192	FILE *fp;
193	struct sigaction act;
194	char **atenv;
195	int ch;
196	mode_t cmask;
197	extern char **environ;
198
199	/*
200	 * Install the signal handler for SIGINT; terminate after removing the
201	 * spool file if necessary
202	 */
203	bzero(&act, sizeof act);
204	act.sa_handler = sigc;
205	sigemptyset(&act.sa_mask);
206	act.sa_flags = 0;
207	sigaction(SIGINT, &act, NULL);
208
209	/*
210	 * Create the file. The x bit is only going to be set after it has
211	 * been completely written out, to make sure it is not executed in
212	 * the meantime.  To make sure they do not get deleted, turn off
213	 * their r bit.  Yes, this is a kluge.
214	 */
215	cmask = umask(S_IRUSR | S_IWUSR | S_IXUSR);
216	if ((fd = newjob(runtimer, queue)) == -1)
217		fatal("unable to create atjob file");
218
219	/*
220	 * We've successfully created the file; let's set the flag so it
221	 * gets removed in case of an interrupt or error.
222	 */
223	fcreated = 1;
224
225	if ((fp = fdopen(fd, "w")) == NULL)
226		fatal("unable to reopen atjob file");
227
228	/*
229	 * Get the userid to mail to, first by trying getlogin(), which asks
230	 * the kernel, then from $LOGNAME or $USER, finally from getpwuid().
231	 */
232	mailname = getlogin();
233	if (mailname == NULL && (mailname = getenv("LOGNAME")) == NULL)
234		mailname = getenv("USER");
235
236	if ((mailname == NULL) || (mailname[0] == '\0') ||
237	    (strlen(mailname) > MAX_UNAME) || (getpwnam(mailname) == NULL)) {
238		mailname = user_name;
239	}
240
241	/*
242	 * Get the shell to run the job under.  First check $SHELL, falling
243	 * back to the user's shell in the password database or, failing
244	 * that, /bin/sh.
245	 */
246	if ((shell = getenv("SHELL")) == NULL || *shell == '\0') {
247		pass_entry = getpwuid(user_uid);
248		if (pass_entry != NULL && *pass_entry->pw_shell != '\0')
249			shell = pass_entry->pw_shell;
250		else
251			shell = _PATH_BSHELL;
252	}
253
254	(void)fprintf(fp, "#!/bin/sh\n# atrun uid=%lu gid=%lu\n# mail %*s %d\n",
255	    (unsigned long)user_uid, (unsigned long)spool_gid,
256	    MAX_UNAME, mailname, send_mail);
257
258	/* Write out the umask at the time of invocation */
259	(void)fprintf(fp, "umask %o\n", cmask);
260
261	/*
262	 * Write out the environment. Anything that may look like a special
263	 * character to the shell is quoted, except for \n, which is done
264	 * with a pair of "'s.  Don't export the no_export list (such as
265	 * TERM or DISPLAY) because we don't want these.
266	 */
267	for (atenv = environ; *atenv != NULL; atenv++) {
268		int export = 1;
269		char *eqp;
270
271		eqp = strchr(*atenv, '=');
272		if (eqp == NULL)
273			eqp = *atenv;
274		else {
275			int i;
276
277			for (i = 0;i < sizeof(no_export) /
278			    sizeof(no_export[0]); i++) {
279				export = export
280				    && (strncmp(*atenv, no_export[i],
281					(size_t) (eqp - *atenv)) != 0);
282			}
283			eqp++;
284		}
285
286		if (export) {
287			(void)fputs("export ", fp);
288			(void)fwrite(*atenv, sizeof(char), eqp - *atenv, fp);
289			for (ap = eqp; *ap != '\0'; ap++) {
290				if (*ap == '\n')
291					(void)fprintf(fp, "\"\n\"");
292				else {
293					if (!isalnum((unsigned char)*ap)) {
294						switch (*ap) {
295						case '%': case '/': case '{':
296						case '[': case ']': case '=':
297						case '}': case '@': case '+':
298						case '#': case ',': case '.':
299						case ':': case '-': case '_':
300							break;
301						default:
302							(void)fputc('\\', fp);
303							break;
304						}
305					}
306					(void)fputc(*ap, fp);
307				}
308			}
309			(void)fputc('\n', fp);
310		}
311	}
312	/*
313	 * Cd to the directory at the time and write out all the
314	 * commands the user supplies from stdin.
315	 */
316	(void)fputs("cd ", fp);
317	for (ap = cwd; *ap != '\0'; ap++) {
318		if (*ap == '\n')
319			fprintf(fp, "\"\n\"");
320		else {
321			if (*ap != '/' && !isalnum((unsigned char)*ap))
322				(void)fputc('\\', fp);
323
324			(void)fputc(*ap, fp);
325		}
326	}
327	/*
328	 * Test cd's exit status: die if the original directory has been
329	 * removed, become unreadable or whatever.
330	 */
331	(void)fprintf(fp, " || {\n\t echo 'Execution directory inaccessible'"
332	    " >&2\n\t exit 1\n}\n");
333
334	if ((ch = getchar()) == EOF)
335		fatalx("unexpected EOF");
336
337	/* We want the job to run under the user's shell. */
338	fprintf(fp, "%s << '_END_OF_AT_JOB'\n", shell);
339
340	do {
341		(void)fputc(ch, fp);
342	} while ((ch = getchar()) != EOF);
343
344	(void)fprintf(fp, "\n_END_OF_AT_JOB\n");
345	(void)fflush(fp);
346	if (ferror(fp))
347		fatalx("write error");
348
349	if (ferror(stdin))
350		fatalx("read error");
351
352	/*
353	 * Set the x bit so that we're ready to start executing
354	 */
355	if (fchmod(fileno(fp), S_IRUSR | S_IWUSR | S_IXUSR) == -1)
356		fatal("fchmod");
357
358	(void)fclose(fp);
359
360	/* Poke cron so it knows to reload the at spool. */
361	poke_daemon(RELOAD_AT);
362
363	runtime = *localtime(&runtimer);
364	strftime(timestr, TIMESIZE, "%a %b %e %T %Y", &runtime);
365	(void)fprintf(stderr, "commands will be executed using %s\n", shell);
366	(void)fprintf(stderr, "job %s at %s\n", &atfile[sizeof(_PATH_AT_SPOOL)],
367	    timestr);
368
369	syslog(LOG_INFO, "(%s) CREATE (%s)", user_name,
370	    &atfile[sizeof(_PATH_AT_SPOOL)]);
371}
372
373/* Sort by creation time. */
374static int
375byctime(const void *v1, const void *v2)
376{
377	const struct atjob *j1 = *(const struct atjob **)v1;
378	const struct atjob *j2 = *(const struct atjob **)v2;
379
380	return (j1->ctime < j2->ctime) ? -1 : (j1->ctime > j2->ctime);
381}
382
383/* Sort by job number (and thus execution time). */
384static int
385byjobno(const void *v1, const void *v2)
386{
387	const struct atjob *j1 = *(struct atjob **)v1;
388	const struct atjob *j2 = *(struct atjob **)v2;
389
390	if (j1->runtimer == j2->runtimer)
391		return (j1->queue - j2->queue);
392	return (j1->runtimer - j2->runtimer);
393}
394
395static void
396print_job(struct atjob *job, int n, int shortformat)
397{
398	struct passwd *pw;
399	struct tm runtime;
400	char timestr[TIMESIZE];
401	static char *ranks[] = {
402		"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"
403	};
404
405	runtime = *localtime(&job->runtimer);
406	if (shortformat) {
407		strftime(timestr, TIMESIZE, "%a %b %e %T %Y", &runtime);
408		(void)printf("%lld.%c\t%s\n", (long long)job->runtimer,
409		    job->queue, timestr);
410	} else {
411		pw = getpwuid(job->uid);
412		/* Rank hack shamelessly stolen from lpq */
413		if (n / 10 == 1)
414			printf("%3d%-5s", n,"th");
415		else
416			printf("%3d%-5s", n, ranks[n % 10]);
417		strftime(timestr, TIMESIZE, "%b %e, %Y %R", &runtime);
418		(void)printf("%-21.18s%-11.8s%10lld.%c   %c%s\n",
419		    timestr, pw ? pw->pw_name : "???",
420		    (long long)job->runtimer, job->queue, job->queue,
421		    (S_IXUSR & job->mode) ? "" : " (done)");
422	}
423}
424
425/*
426 * List all of a user's jobs in the queue, by looping through
427 * _PATH_AT_SPOOL, or all jobs if we are root.  If argc is > 0, argv
428 * contains the list of users whose jobs shall be displayed. By
429 * default, the list is sorted by execution date and queue.  If
430 * csort is non-zero jobs will be sorted by creation/submission date.
431 */
432static void
433list_jobs(int argc, char **argv, int count_only, int csort)
434{
435	struct passwd *pw;
436	struct dirent *dirent;
437	struct atjob **atjobs, **newatjobs, *job;
438	struct stat stbuf;
439	time_t runtimer;
440	char **jobs;
441	uid_t *uids;
442	char queue, *ep;
443	DIR *spool;
444	int job_matches, jobs_len, uids_len;
445	int dfd, i, shortformat;
446	size_t numjobs, maxjobs;
447
448	syslog(LOG_INFO, "(%s) LIST (%s)", user_name,
449	    user_uid ? user_name : "ALL");
450
451	/* Convert argv into a list of jobs and uids. */
452	jobs = NULL;
453	uids = NULL;
454	jobs_len = uids_len = 0;
455
456	if (argc) {
457		if ((jobs = reallocarray(NULL, argc, sizeof(char *))) == NULL ||
458		    (uids = reallocarray(NULL, argc, sizeof(uid_t))) == NULL)
459			fatal(NULL);
460
461		for (i = 0; i < argc; i++) {
462			if (strtot(argv[i], &ep, &runtimer) == 0 &&
463			    *ep == '.' && isalpha((unsigned char)*(ep + 1)) &&
464			    *(ep + 2) == '\0')
465				jobs[jobs_len++] = argv[i];
466			else if ((pw = getpwnam(argv[i])) != NULL) {
467				if (pw->pw_uid != user_uid && user_uid != 0)
468					fatalx("only the superuser may "
469					    "display other users' jobs");
470				uids[uids_len++] = pw->pw_uid;
471			} else
472				fatalx("unknown user %s", argv[i]);
473		}
474	}
475
476	shortformat = strcmp(__progname, "at") == 0;
477
478	if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1 ||
479	    (spool = fdopendir(dfd)) == NULL)
480		fatal(_PATH_AT_SPOOL);
481
482	if (fstat(dfd, &stbuf) != 0)
483		fatal(_PATH_AT_SPOOL);
484
485	/*
486	 * The directory's link count should give us a good idea
487	 * of how many files are in it.  Fudge things a little just
488	 * in case someone adds a job or two.
489	 */
490	numjobs = 0;
491	maxjobs = stbuf.st_nlink + 4;
492	atjobs = reallocarray(NULL, maxjobs, sizeof(struct atjob *));
493	if (atjobs == NULL)
494		fatal(NULL);
495
496	/* Loop over every file in the directory. */
497	while ((dirent = readdir(spool)) != NULL) {
498		if (fstatat(dfd, dirent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW) != 0)
499			fatal("%s", dirent->d_name);
500
501		/*
502		 * See it's a regular file and has its x bit turned on and
503		 * is the user's
504		 */
505		if (!S_ISREG(stbuf.st_mode)
506		    || ((stbuf.st_uid != user_uid) && !(user_uid == 0))
507		    || !(S_IXUSR & stbuf.st_mode || vflag))
508			continue;
509
510		if (strtot(dirent->d_name, &ep, &runtimer) == -1)
511			continue;
512		if (*ep != '.' || !isalpha((unsigned char)*(ep + 1)) ||
513		    *(ep + 2) != '\0')
514			continue;
515		queue = *(ep + 1);
516
517		if (atqueue && (queue != atqueue))
518			continue;
519
520		/* Check against specified jobs and/or user(s). */
521		job_matches = (argc == 0) ? 1 : 0;
522		if (!job_matches) {
523			for (i = 0; i < jobs_len; i++) {
524				if (strcmp(dirent->d_name, jobs[i]) == 0) {
525					job_matches = 1;
526					break;
527				}
528			}
529		}
530		if (!job_matches) {
531			for (i = 0; i < uids_len; i++) {
532				if (uids[i] == stbuf.st_uid) {
533					job_matches = 1;
534					break;
535				}
536			}
537		}
538		if (!job_matches)
539			continue;
540
541		if (count_only) {
542			numjobs++;
543			continue;
544		}
545
546		job = malloc(sizeof(struct atjob));
547		if (job == NULL)
548			fatal(NULL);
549		job->runtimer = runtimer;
550		job->ctime = stbuf.st_ctime;
551		job->uid = stbuf.st_uid;
552		job->mode = stbuf.st_mode;
553		job->queue = queue;
554		if (numjobs == maxjobs) {
555			size_t newjobs = maxjobs * 2;
556			newatjobs = recallocarray(atjobs, maxjobs,
557			    newjobs, sizeof(job));
558			if (newatjobs == NULL)
559				fatal(NULL);
560			atjobs = newatjobs;
561			maxjobs = newjobs;
562		}
563		atjobs[numjobs++] = job;
564	}
565	free(uids);
566	closedir(spool);
567
568	if (count_only || numjobs == 0) {
569		if (numjobs == 0 && !shortformat)
570			warnx("no files in queue");
571		else if (count_only)
572			printf("%zu\n", numjobs);
573		free(atjobs);
574		return;
575	}
576
577	/* Sort by job run time or by job creation time. */
578	qsort(atjobs, numjobs, sizeof(struct atjob *),
579	    csort ? byctime : byjobno);
580
581	if (!shortformat)
582		(void)puts(" Rank     Execution Date     Owner          "
583		    "Job       Queue");
584
585	for (i = 0; i < numjobs; i++) {
586		print_job(atjobs[i], i + 1, shortformat);
587		free(atjobs[i]);
588	}
589	free(atjobs);
590}
591
592static int
593rmok(long long job)
594{
595	int ch, junk;
596
597	printf("%lld: remove it? ", job);
598	ch = getchar();
599	while ((junk = getchar()) != EOF && junk != '\n')
600		;
601	return (ch == 'y' || ch == 'Y');
602}
603
604/*
605 * Loop through all jobs in _PATH_AT_SPOOL and display or delete ones
606 * that match argv (may be job or username), or all if argc == 0.
607 * Only the superuser may display/delete other people's jobs.
608 */
609static int
610process_jobs(int argc, char **argv, int what)
611{
612	struct stat stbuf;
613	struct dirent *dirent;
614	struct passwd *pw;
615	time_t runtimer;
616	uid_t *uids;
617	char **jobs, *ep;
618	FILE *fp;
619	DIR *spool;
620	int job_matches, jobs_len, uids_len;
621	int error, i, ch, changed, dfd;
622
623	if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1 ||
624	    (spool = fdopendir(dfd)) == NULL)
625		fatal(_PATH_AT_SPOOL);
626
627	/* Convert argv into a list of jobs and uids. */
628	jobs = NULL;
629	uids = NULL;
630	jobs_len = uids_len = 0;
631	if (argc > 0) {
632		if ((jobs = reallocarray(NULL, argc, sizeof(char *))) == NULL ||
633		    (uids = reallocarray(NULL, argc, sizeof(uid_t))) == NULL)
634			fatal(NULL);
635
636		for (i = 0; i < argc; i++) {
637			if (strtot(argv[i], &ep, &runtimer) == 0 &&
638			    *ep == '.' && isalpha((unsigned char)*(ep + 1)) &&
639			    *(ep + 2) == '\0')
640				jobs[jobs_len++] = argv[i];
641			else if ((pw = getpwnam(argv[i])) != NULL) {
642				if (user_uid != pw->pw_uid && user_uid != 0) {
643					fatalx("only the superuser may %s "
644					    "other users' jobs",
645					    what == ATRM ? "remove" : "view");
646				}
647				uids[uids_len++] = pw->pw_uid;
648			} else
649				fatalx("unknown user %s", argv[i]);
650		}
651	}
652
653	/* Loop over every file in the directory */
654	changed = 0;
655	while ((dirent = readdir(spool)) != NULL) {
656		if (fstatat(dfd, dirent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW) != 0)
657			fatal("%s", dirent->d_name);
658
659		if (stbuf.st_uid != user_uid && user_uid != 0)
660			continue;
661
662		if (strtot(dirent->d_name, &ep, &runtimer) == -1)
663			continue;
664		if (*ep != '.' || !isalpha((unsigned char)*(ep + 1)) ||
665		    *(ep + 2) != '\0')
666			continue;
667
668		/* Check runtimer against argv; argc==0 means do all. */
669		job_matches = (argc == 0) ? 1 : 0;
670		if (!job_matches) {
671			for (i = 0; i < jobs_len; i++) {
672				if (jobs[i] != NULL &&
673				    strcmp(dirent->d_name, jobs[i]) == 0) {
674					jobs[i] = NULL;
675					job_matches = 1;
676					break;
677				}
678			}
679		}
680		if (!job_matches) {
681			for (i = 0; i < uids_len; i++) {
682				if (uids[i] == stbuf.st_uid) {
683					job_matches = 1;
684					break;
685				}
686			}
687		}
688
689		if (job_matches) {
690			switch (what) {
691			case ATRM:
692				if (!interactive ||
693				    (interactive && rmok(runtimer))) {
694					if (unlinkat(dfd, dirent->d_name, 0) == 0) {
695						syslog(LOG_INFO,
696						    "(%s) DELETE (%s)",
697						    user_name, dirent->d_name);
698						changed = 1;
699					} else if (!force)
700						fatal("%s", dirent->d_name);
701					if (!force && !interactive)
702						warnx("%s removed",
703						    dirent->d_name);
704				}
705				break;
706
707			case CAT:
708				i = openat(dfd, dirent->d_name,
709				    O_RDONLY|O_NOFOLLOW);
710				if (i == -1 || (fp = fdopen(i, "r")) == NULL)
711					fatal("%s", dirent->d_name);
712				syslog(LOG_INFO, "(%s) CAT (%s)",
713				    user_name, dirent->d_name);
714
715				while ((ch = getc(fp)) != EOF)
716					putchar(ch);
717
718				fclose(fp);
719				break;
720
721			default:
722				fatalx("internal error");
723				break;
724			}
725		}
726	}
727	closedir(spool);
728
729	for (error = 0, i = 0; i < jobs_len; i++) {
730		if (jobs[i] != NULL) {
731			if (!force)
732				warnx("%s: no such job", jobs[i]);
733			error++;
734		}
735	}
736	free(jobs);
737	free(uids);
738
739	/* If we modied the spool, poke cron so it knows to reload. */
740	if (changed)
741		poke_daemon(RELOAD_AT);
742
743	return (error);
744}
745
746#define	ATOI2(s)	((s) += 2, ((s)[-2] - '0') * 10 + ((s)[-1] - '0'))
747
748/*
749 * Adapted from date(1)
750 */
751static time_t
752ttime(char *arg)
753{
754	time_t now, then;
755	struct tm *lt;
756	int yearset;
757	char *dot, *p;
758
759	if (time(&now) == (time_t)-1 || (lt = localtime(&now)) == NULL)
760		fatal("unable to get current time");
761
762	/* Valid date format is [[CC]YY]MMDDhhmm[.SS] */
763	for (p = arg, dot = NULL; *p != '\0'; p++) {
764		if (*p == '.' && dot == NULL)
765			dot = p;
766		else if (!isdigit((unsigned char)*p))
767			goto terr;
768	}
769	if (dot == NULL)
770		lt->tm_sec = 0;
771	else {
772		*dot++ = '\0';
773		if (strlen(dot) != 2)
774			goto terr;
775		lt->tm_sec = ATOI2(dot);
776		if (lt->tm_sec > 61)	/* could be leap second */
777			goto terr;
778	}
779
780	yearset = 0;
781	switch(strlen(arg)) {
782	case 12:			/* CCYYMMDDhhmm */
783		lt->tm_year = ATOI2(arg) * 100;
784		lt->tm_year -= 1900;	/* Convert to Unix time */
785		yearset = 1;
786		/* FALLTHROUGH */
787	case 10:			/* YYMMDDhhmm */
788		if (yearset) {
789			yearset = ATOI2(arg);
790			lt->tm_year += yearset;
791		} else {
792			yearset = ATOI2(arg);
793			/* POSIX logic: [00,68]=>20xx, [69,99]=>19xx */
794			lt->tm_year = yearset;
795			if (yearset < 69)
796				lt->tm_year += 100;
797		}
798		/* FALLTHROUGH */
799	case 8:				/* MMDDhhmm */
800		lt->tm_mon = ATOI2(arg);
801		if (lt->tm_mon > 12 || lt->tm_mon == 0)
802			goto terr;
803		--lt->tm_mon;		/* Convert from 01-12 to 00-11 */
804		lt->tm_mday = ATOI2(arg);
805		if (lt->tm_mday > 31 || lt->tm_mday == 0)
806			goto terr;
807		lt->tm_hour = ATOI2(arg);
808		if (lt->tm_hour > 23)
809			goto terr;
810		lt->tm_min = ATOI2(arg);
811		if (lt->tm_min > 59)
812			goto terr;
813		break;
814	default:
815		goto terr;
816	}
817
818	lt->tm_isdst = -1;		/* mktime will deduce DST. */
819	then = mktime(lt);
820	if (then == (time_t)-1) {
821    terr:
822		fatalx("illegal time specification: [[CC]YY]MMDDhhmm[.SS]");
823	}
824	if (then < now)
825		fatalx("cannot schedule jobs in the past");
826	return (then);
827}
828
829static __dead void
830usage(void)
831{
832	/* Print usage and exit.  */
833	switch (program) {
834	case AT:
835	case CAT:
836		(void)fprintf(stderr,
837		    "usage: at [-bm] [-f file] [-l [job ...]] [-q queue] "
838		    "-t time_arg | timespec\n"
839		    "       at -c | -r job ...\n");
840		break;
841	case ATQ:
842		(void)fprintf(stderr,
843		    "usage: atq [-cnv] [-q queue] [name ...]\n");
844		break;
845	case ATRM:
846		(void)fprintf(stderr,
847		    "usage: atrm [-afi] [[job] [name] ...]\n");
848		break;
849	case BATCH:
850		(void)fprintf(stderr,
851		    "usage: batch [-m] [-f file] [-q queue] [timespec]\n");
852		break;
853	}
854	exit(EXIT_FAILURE);
855}
856
857int
858main(int argc, char **argv)
859{
860	time_t timer = -1;
861	char *atinput = NULL;			/* where to get input from */
862	char queue = DEFAULT_AT_QUEUE;
863	char queue_set = 0;
864	char *options = "q:f:t:bcdlmrv";	/* default options for at */
865	char cwd[PATH_MAX];
866	struct passwd *pw;
867	int ch;
868	int aflag = 0;
869	int cflag = 0;
870	int nflag = 0;
871
872	if (pledge("stdio rpath wpath cpath fattr getpw unix id", NULL) == -1)
873		fatal("pledge");
874
875	openlog(__progname, LOG_PID, LOG_CRON);
876
877	if (argc < 1)
878		usage();
879
880	user_uid = getuid();
881	user_gid = getgid();
882	spool_gid = getegid();
883
884	/* find out what this program is supposed to do */
885	if (strcmp(__progname, "atq") == 0) {
886		program = ATQ;
887		options = "cnvq:";
888	} else if (strcmp(__progname, "atrm") == 0) {
889		program = ATRM;
890		options = "afi";
891	} else if (strcmp(__progname, "batch") == 0) {
892		program = BATCH;
893		options = "f:q:mv";
894	}
895
896	/* process whatever options we can process */
897	while ((ch = getopt(argc, argv, options)) != -1) {
898		switch (ch) {
899		case 'a':
900			aflag = 1;
901			break;
902
903		case 'i':
904			interactive = 1;
905			force = 0;
906			break;
907
908		case 'v':	/* show completed but unremoved jobs */
909			/*
910			 * This option is only useful when we are invoked
911			 * as atq but we accept (and ignore) this flag in
912			 * the other programs for backwards compatibility.
913			 */
914			vflag = 1;
915			break;
916
917		case 'm':	/* send mail when job is complete */
918			send_mail = 1;
919			break;
920
921		case 'f':
922			if (program == ATRM) {
923				force = 1;
924				interactive = 0;
925			} else
926				atinput = optarg;
927			break;
928
929		case 'q':	/* specify queue */
930			if (strlen(optarg) > 1)
931				usage();
932
933			atqueue = queue = *optarg;
934			if (!(islower((unsigned char)queue) ||
935			    isupper((unsigned char)queue)))
936				usage();
937
938			queue_set = 1;
939			break;
940
941		case 'd':		/* for backwards compatibility */
942		case 'r':
943			program = ATRM;
944			options = "";
945			break;
946
947		case 't':
948			timer = ttime(optarg);
949			break;
950
951		case 'l':
952			program = ATQ;
953			options = "cnvq:";
954			break;
955
956		case 'b':
957			program = BATCH;
958			options = "f:q:mv";
959			break;
960
961		case 'c':
962			if (program == ATQ) {
963				cflag = 1;
964			} else {
965				program = CAT;
966				options = "";
967			}
968			break;
969
970		case 'n':
971			nflag = 1;
972			break;
973
974		default:
975			usage();
976			break;
977		}
978	}
979	argc -= optind;
980	argv += optind;
981
982	switch (program) {
983	case AT:
984	case BATCH:
985		if (atinput != NULL) {
986			if (setegid(user_gid) != 0)
987				fatal("setegid(user_gid)");
988			if (freopen(atinput, "r", stdin) == NULL)
989				fatal("%s", atinput);
990			if (setegid(spool_gid) != 0)
991				fatal("setegid(spool_gid)");
992		}
993
994		if (pledge("stdio rpath wpath cpath fattr getpw unix", NULL)
995		    == -1)
996			fatal("pledge");
997		break;
998
999	case ATQ:
1000	case CAT:
1001		if (pledge("stdio rpath getpw", NULL) == -1)
1002			fatal("pledge");
1003		break;
1004
1005	case ATRM:
1006		if (pledge("stdio rpath cpath getpw unix", NULL) == -1)
1007			fatal("pledge");
1008		break;
1009
1010	default:
1011		fatalx("internal error");
1012		break;
1013	}
1014
1015	if ((pw = getpwuid(user_uid)) == NULL)
1016	    fatalx("unknown uid %u", user_uid);
1017	if (strlcpy(user_name, pw->pw_name, sizeof(user_name)) >= sizeof(user_name))
1018	    fatalx("username too long");
1019
1020	if (getcwd(cwd, sizeof(cwd)) == NULL)
1021		fatal("unable to get current working directory");
1022
1023	if (!allowed(pw->pw_name, _PATH_AT_ALLOW, _PATH_AT_DENY)) {
1024		syslog(LOG_WARNING, "(%s) AUTH (at command not allowed)",
1025		    pw->pw_name);
1026		fatalx("you do not have permission to use at.");
1027	}
1028
1029	/* select our program */
1030	switch (program) {
1031	case ATQ:
1032		list_jobs(argc, argv, nflag, cflag);
1033		break;
1034
1035	case ATRM:
1036	case CAT:
1037		if ((aflag && argc) || (!aflag && !argc))
1038			usage();
1039		return process_jobs(argc, argv, program);
1040		break;
1041
1042	case AT:
1043		/* Time may have been specified via the -t flag. */
1044		if (timer == -1) {
1045			if (argc == 0)
1046				usage();
1047			else if ((timer = parsetime(argc, argv)) == -1)
1048				return EXIT_FAILURE;
1049		}
1050		writefile(cwd, timer, queue);
1051		break;
1052
1053	case BATCH:
1054		if (queue_set)
1055			queue = toupper((unsigned char)queue);
1056		else
1057			queue = DEFAULT_BATCH_QUEUE;
1058
1059		if (argc == 0)
1060			timer = time(NULL);
1061		else if ((timer = parsetime(argc, argv)) == -1)
1062			return EXIT_FAILURE;
1063
1064		writefile(cwd, timer, queue);
1065		break;
1066
1067	default:
1068		fatalx("internal error");
1069		break;
1070	}
1071	return EXIT_SUCCESS;
1072}
1073