at.c revision 87230
1/*
2 *  at.c : Put file into atrun queue
3 *  Copyright (C) 1993, 1994 Thomas Koenig
4 *
5 *  Atrun & Atq modifications
6 *  Copyright (C) 1993  David Parsons
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. The name of the author(s) may not be used to endorse or promote
14 *    products derived from this software without specific prior written
15 *    permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include <sys/cdefs.h>
30
31__FBSDID("$FreeBSD: head/usr.bin/at/at.c 87230 2001-12-02 20:23:02Z markm $");
32
33#define _USE_BSD 1
34
35/* System Headers */
36
37#include <sys/types.h>
38#include <sys/stat.h>
39#include <sys/wait.h>
40#include <sys/param.h>
41#include <ctype.h>
42#include <dirent.h>
43#include <err.h>
44#include <errno.h>
45#include <fcntl.h>
46#include <pwd.h>
47#include <signal.h>
48#include <stddef.h>
49#include <stdio.h>
50#include <stdlib.h>
51#include <string.h>
52#include <time.h>
53#include <unistd.h>
54#include <utmp.h>
55#ifndef __FreeBSD__
56#include <getopt.h>
57#else
58#include <locale.h>
59#endif
60
61#if (MAXLOGNAME-1) > UT_NAMESIZE
62#define LOGNAMESIZE UT_NAMESIZE
63#else
64#define LOGNAMESIZE (MAXLOGNAME-1)
65#endif
66
67/* Local headers */
68
69#include "at.h"
70#include "panic.h"
71#include "parsetime.h"
72#include "perm.h"
73
74#define MAIN
75#include "privs.h"
76
77/* Macros */
78
79#ifndef ATJOB_DIR
80#define ATJOB_DIR "/usr/spool/atjobs/"
81#endif
82
83#ifndef LFILE
84#define LFILE ATJOB_DIR ".lockfile"
85#endif
86
87#ifndef ATJOB_MX
88#define ATJOB_MX 255
89#endif
90
91#define ALARMC 10 /* Number of seconds to wait for timeout */
92
93#define SIZE 255
94#define TIMESIZE 50
95
96enum { ATQ, ATRM, AT, BATCH, CAT };	/* what program we want to run */
97
98/* File scope variables */
99
100const char *no_export[] =
101{
102    "TERM", "TERMCAP", "DISPLAY", "_"
103} ;
104static int send_mail = 0;
105
106/* External variables */
107
108extern char **environ;
109int fcreated;
110char atfile[] = ATJOB_DIR "12345678901234";
111
112char *atinput = (char*)0;	/* where to get input from */
113char atqueue = 0;		/* which queue to examine for jobs (atq) */
114char atverify = 0;		/* verify time instead of queuing job */
115char *namep;
116
117/* Function declarations */
118
119static void sigc(int signo);
120static void alarmc(int signo);
121static char *cwdname(void);
122static void writefile(time_t runtimer, char queue);
123static void list_jobs(void);
124static long nextjob(void);
125
126/* Signal catching functions */
127
128static void sigc(int signo __unused)
129{
130/* If the user presses ^C, remove the spool file and exit
131 */
132    if (fcreated)
133    {
134	PRIV_START
135	    unlink(atfile);
136	PRIV_END
137    }
138
139    _exit(EXIT_FAILURE);
140}
141
142static void alarmc(int signo __unused)
143{
144    char buf[1024];
145
146    /* Time out after some seconds. */
147    strlcpy(buf, namep, sizeof(buf));
148    strlcat(buf, ": file locking timed out\n", sizeof(buf));
149    write(STDERR_FILENO, buf, strlen(buf));
150    sigc(0);
151}
152
153/* Local functions */
154
155static char *cwdname(void)
156{
157/* Read in the current directory; the name will be overwritten on
158 * subsequent calls.
159 */
160    static char *ptr = NULL;
161    static size_t size = SIZE;
162
163    if (ptr == NULL)
164	if ((ptr = malloc(size)) == NULL)
165	    errx(EXIT_FAILURE, "virtual memory exhausted");
166
167    while (1)
168    {
169	if (ptr == NULL)
170	    panic("out of memory");
171
172	if (getcwd(ptr, size-1) != NULL)
173	    return ptr;
174
175	if (errno != ERANGE)
176	    perr("cannot get directory");
177
178	free (ptr);
179	size += SIZE;
180	if ((ptr = malloc(size)) == NULL)
181	    errx(EXIT_FAILURE, "virtual memory exhausted");
182    }
183}
184
185static long
186nextjob()
187{
188    long jobno;
189    FILE *fid;
190
191    if ((fid = fopen(ATJOB_DIR ".SEQ", "r+")) != (FILE*)0) {
192	if (fscanf(fid, "%5lx", &jobno) == 1) {
193	    rewind(fid);
194	    jobno = (1+jobno) % 0xfffff;	/* 2^20 jobs enough? */
195	    fprintf(fid, "%05lx\n", jobno);
196	}
197	else
198	    jobno = EOF;
199	fclose(fid);
200	return jobno;
201    }
202    else if ((fid = fopen(ATJOB_DIR ".SEQ", "w")) != (FILE*)0) {
203	fprintf(fid, "%05lx\n", jobno = 1);
204	fclose(fid);
205	return 1;
206    }
207    return EOF;
208}
209
210static void
211writefile(time_t runtimer, char queue)
212{
213/* This does most of the work if at or batch are invoked for writing a job.
214 */
215    long jobno;
216    char *ap, *ppos, *mailname;
217    struct passwd *pass_entry;
218    struct stat statbuf;
219    int fdes, lockdes, fd2;
220    FILE *fp, *fpin;
221    struct sigaction act;
222    char **atenv;
223    int ch;
224    mode_t cmask;
225    struct flock lock;
226
227#ifdef __FreeBSD__
228    (void) setlocale(LC_TIME, "");
229#endif
230
231/* Install the signal handler for SIGINT; terminate after removing the
232 * spool file if necessary
233 */
234    act.sa_handler = sigc;
235    sigemptyset(&(act.sa_mask));
236    act.sa_flags = 0;
237
238    sigaction(SIGINT, &act, NULL);
239
240    ppos = atfile + strlen(ATJOB_DIR);
241
242    /* Loop over all possible file names for running something at this
243     * particular time, see if a file is there; the first empty slot at any
244     * particular time is used.  Lock the file LFILE first to make sure
245     * we're alone when doing this.
246     */
247
248    PRIV_START
249
250    if ((lockdes = open(LFILE, O_WRONLY | O_CREAT, S_IWUSR | S_IRUSR)) < 0)
251	perr("cannot open lockfile " LFILE);
252
253    lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = 0;
254    lock.l_len = 0;
255
256    act.sa_handler = alarmc;
257    sigemptyset(&(act.sa_mask));
258    act.sa_flags = 0;
259
260    /* Set an alarm so a timeout occurs after ALARMC seconds, in case
261     * something is seriously broken.
262     */
263    sigaction(SIGALRM, &act, NULL);
264    alarm(ALARMC);
265    fcntl(lockdes, F_SETLKW, &lock);
266    alarm(0);
267
268    if ((jobno = nextjob()) == EOF)
269	perr("cannot generate job number");
270
271    sprintf(ppos, "%c%5lx%8lx", queue,
272	    jobno, (unsigned long) (runtimer/60));
273
274    for(ap=ppos; *ap != '\0'; ap ++)
275	if (*ap == ' ')
276	    *ap = '0';
277
278    if (stat(atfile, &statbuf) != 0)
279	if (errno != ENOENT)
280	    perr("cannot access " ATJOB_DIR);
281
282    /* Create the file. The x bit is only going to be set after it has
283     * been completely written out, to make sure it is not executed in the
284     * meantime.  To make sure they do not get deleted, turn off their r
285     * bit.  Yes, this is a kluge.
286     */
287    cmask = umask(S_IRUSR | S_IWUSR | S_IXUSR);
288    if ((fdes = creat(atfile, O_WRONLY)) == -1)
289	perr("cannot create atjob file");
290
291    if ((fd2 = dup(fdes)) <0)
292	perr("error in dup() of job file");
293
294    if(fchown(fd2, real_uid, real_gid) != 0)
295	perr("cannot give away file");
296
297    PRIV_END
298
299    /* We no longer need suid root; now we just need to be able to write
300     * to the directory, if necessary.
301     */
302
303    REDUCE_PRIV(DAEMON_UID, DAEMON_GID)
304
305    /* We've successfully created the file; let's set the flag so it
306     * gets removed in case of an interrupt or error.
307     */
308    fcreated = 1;
309
310    /* Now we can release the lock, so other people can access it
311     */
312    lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = 0;
313    lock.l_len = 0;
314    fcntl(lockdes, F_SETLKW, &lock);
315    close(lockdes);
316
317    if((fp = fdopen(fdes, "w")) == NULL)
318	panic("cannot reopen atjob file");
319
320    /* Get the userid to mail to, first by trying getlogin(), which reads
321     * /etc/utmp, then from LOGNAME, finally from getpwuid().
322     */
323    mailname = getlogin();
324    if (mailname == NULL)
325	mailname = getenv("LOGNAME");
326
327    if ((mailname == NULL) || (mailname[0] == '\0')
328	|| (strlen(mailname) > LOGNAMESIZE) || (getpwnam(mailname)==NULL))
329    {
330	pass_entry = getpwuid(real_uid);
331	if (pass_entry != NULL)
332	    mailname = pass_entry->pw_name;
333    }
334
335    if (atinput != (char *) NULL)
336    {
337	fpin = freopen(atinput, "r", stdin);
338	if (fpin == NULL)
339	    perr("cannot open input file");
340    }
341    fprintf(fp, "#!/bin/sh\n# atrun uid=%ld gid=%ld\n# mail %*s %d\n",
342	(long) real_uid, (long) real_gid, LOGNAMESIZE, mailname, send_mail);
343
344    /* Write out the umask at the time of invocation
345     */
346    fprintf(fp, "umask %lo\n", (unsigned long) cmask);
347
348    /* Write out the environment. Anything that may look like a
349     * special character to the shell is quoted, except for \n, which is
350     * done with a pair of "'s.  Don't export the no_export list (such
351     * as TERM or DISPLAY) because we don't want these.
352     */
353    for (atenv= environ; *atenv != NULL; atenv++)
354    {
355	int export = 1;
356	char *eqp;
357
358	eqp = strchr(*atenv, '=');
359	if (ap == NULL)
360	    eqp = *atenv;
361	else
362	{
363	    size_t i;
364	    for (i=0; i<sizeof(no_export)/sizeof(no_export[0]); i++)
365	    {
366		export = export
367		    && (strncmp(*atenv, no_export[i],
368				(size_t) (eqp-*atenv)) != 0);
369	    }
370	    eqp++;
371	}
372
373	if (export)
374	{
375	    fwrite(*atenv, sizeof(char), eqp-*atenv, fp);
376	    for(ap = eqp;*ap != '\0'; ap++)
377	    {
378		if (*ap == '\n')
379		    fprintf(fp, "\"\n\"");
380		else
381		{
382		    if (!isalnum(*ap)) {
383			switch (*ap) {
384			  case '%': case '/': case '{': case '[':
385			  case ']': case '=': case '}': case '@':
386			  case '+': case '#': case ',': case '.':
387			  case ':': case '-': case '_':
388			    break;
389			  default:
390			    fputc('\\', fp);
391			    break;
392			}
393		    }
394		    fputc(*ap, fp);
395		}
396	    }
397	    fputs("; export ", fp);
398	    fwrite(*atenv, sizeof(char), eqp-*atenv -1, fp);
399	    fputc('\n', fp);
400
401	}
402    }
403    /* Cd to the directory at the time and write out all the
404     * commands the user supplies from stdin.
405     */
406    fprintf(fp, "cd ");
407    for (ap = cwdname(); *ap != '\0'; ap++)
408    {
409	if (*ap == '\n')
410	    fprintf(fp, "\"\n\"");
411	else
412	{
413	    if (*ap != '/' && !isalnum(*ap))
414		fputc('\\', fp);
415
416	    fputc(*ap, fp);
417	}
418    }
419    /* Test cd's exit status: die if the original directory has been
420     * removed, become unreadable or whatever
421     */
422    fprintf(fp, " || {\n\t echo 'Execution directory "
423	        "inaccessible' >&2\n\t exit 1\n}\n");
424
425    while((ch = getchar()) != EOF)
426	fputc(ch, fp);
427
428    fprintf(fp, "\n");
429    if (ferror(fp))
430	panic("output error");
431
432    if (ferror(stdin))
433	panic("input error");
434
435    fclose(fp);
436
437    /* Set the x bit so that we're ready to start executing
438     */
439
440    if (fchmod(fd2, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
441	perr("cannot give away file");
442
443    close(fd2);
444    fprintf(stderr, "Job %ld will be executed using /bin/sh\n", jobno);
445}
446
447static void
448list_jobs()
449{
450    /* List all a user's jobs in the queue, by looping through ATJOB_DIR,
451     * or everybody's if we are root
452     */
453    struct passwd *pw;
454    DIR *spool;
455    struct dirent *dirent;
456    struct stat buf;
457    struct tm runtime;
458    unsigned long ctm;
459    char queue;
460    long jobno;
461    time_t runtimer;
462    char timestr[TIMESIZE];
463    int first=1;
464
465#ifdef __FreeBSD__
466    (void) setlocale(LC_TIME, "");
467#endif
468
469    PRIV_START
470
471    if (chdir(ATJOB_DIR) != 0)
472	perr("cannot change to " ATJOB_DIR);
473
474    if ((spool = opendir(".")) == NULL)
475	perr("cannot open " ATJOB_DIR);
476
477    /*	Loop over every file in the directory
478     */
479    while((dirent = readdir(spool)) != NULL) {
480	if (stat(dirent->d_name, &buf) != 0)
481	    perr("cannot stat in " ATJOB_DIR);
482
483	/* See it's a regular file and has its x bit turned on and
484         * is the user's
485         */
486	if (!S_ISREG(buf.st_mode)
487	    || ((buf.st_uid != real_uid) && ! (real_uid == 0))
488	    || !(S_IXUSR & buf.st_mode || atverify))
489	    continue;
490
491	if(sscanf(dirent->d_name, "%c%5lx%8lx", &queue, &jobno, &ctm)!=3)
492	    continue;
493
494	if (atqueue && (queue != atqueue))
495	    continue;
496
497	runtimer = 60*(time_t) ctm;
498	runtime = *localtime(&runtimer);
499	strftime(timestr, TIMESIZE, "%+", &runtime);
500	if (first) {
501	    printf("Date\t\t\tOwner\tQueue\tJob#\n");
502	    first=0;
503	}
504	pw = getpwuid(buf.st_uid);
505
506	printf("%s\t%s\t%c%s\t%ld\n",
507	       timestr,
508	       pw ? pw->pw_name : "???",
509	       queue,
510	       (S_IXUSR & buf.st_mode) ? "":"(done)",
511	       jobno);
512    }
513    PRIV_END
514}
515
516static void
517process_jobs(int argc, char **argv, int what)
518{
519    /* Delete every argument (job - ID) given
520     */
521    int i;
522    struct stat buf;
523    DIR *spool;
524    struct dirent *dirent;
525    unsigned long ctm;
526    char queue;
527    long jobno;
528
529    PRIV_START
530
531    if (chdir(ATJOB_DIR) != 0)
532	perr("cannot change to " ATJOB_DIR);
533
534    if ((spool = opendir(".")) == NULL)
535	perr("cannot open " ATJOB_DIR);
536
537    PRIV_END
538
539    /*	Loop over every file in the directory
540     */
541    while((dirent = readdir(spool)) != NULL) {
542
543	PRIV_START
544	if (stat(dirent->d_name, &buf) != 0)
545	    perr("cannot stat in " ATJOB_DIR);
546	PRIV_END
547
548	if(sscanf(dirent->d_name, "%c%5lx%8lx", &queue, &jobno, &ctm)!=3)
549	    continue;
550
551	for (i=optind; i < argc; i++) {
552	    if (atoi(argv[i]) == jobno) {
553		if ((buf.st_uid != real_uid) && !(real_uid == 0))
554		    errx(EXIT_FAILURE, "%s: not owner", argv[i]);
555		switch (what) {
556		  case ATRM:
557
558		    PRIV_START
559
560		    if (unlink(dirent->d_name) != 0)
561		        perr(dirent->d_name);
562
563		    PRIV_END
564
565		    break;
566
567		  case CAT:
568		    {
569			FILE *fp;
570			int ch;
571
572			PRIV_START
573
574			fp = fopen(dirent->d_name,"r");
575
576			PRIV_END
577
578			if (!fp) {
579			    perr("cannot open file");
580			}
581			while((ch = getc(fp)) != EOF) {
582			    putchar(ch);
583			}
584		    }
585		    break;
586
587		  default:
588		    errx(EXIT_FAILURE, "internal error, process_jobs = %d",
589			what);
590	        }
591	    }
592	}
593    }
594} /* delete_jobs */
595
596int
597main(int argc, char **argv)
598{
599    int c;
600    char queue = DEFAULT_AT_QUEUE;
601    char queue_set = 0;
602    char *pgm;
603
604    int program = AT;			/* our default program */
605    const char *options = "q:f:mvldbVc";/* default options for at */
606    int disp_version = 0;
607    time_t timer;
608
609    RELINQUISH_PRIVS
610
611    /* Eat any leading paths
612     */
613    if ((pgm = strrchr(argv[0], '/')) == NULL)
614	pgm = argv[0];
615    else
616        pgm++;
617
618    namep = pgm;
619
620    /* find out what this program is supposed to do
621     */
622    if (strcmp(pgm, "atq") == 0) {
623	program = ATQ;
624	options = "q:vV";
625    }
626    else if (strcmp(pgm, "atrm") == 0) {
627	program = ATRM;
628	options = "V";
629    }
630    else if (strcmp(pgm, "batch") == 0) {
631	program = BATCH;
632	options = "f:q:mvV";
633    }
634
635    /* process whatever options we can process
636     */
637    opterr=1;
638    while ((c=getopt(argc, argv, options)) != -1)
639	switch (c) {
640	case 'v':   /* verify time settings */
641	    atverify = 1;
642	    break;
643
644	case 'm':   /* send mail when job is complete */
645	    send_mail = 1;
646	    break;
647
648	case 'f':
649	    atinput = optarg;
650	    break;
651
652	case 'q':    /* specify queue */
653	    if (strlen(optarg) > 1)
654		usage();
655
656	    atqueue = queue = *optarg;
657	    if (!(islower(queue)||isupper(queue)))
658		usage();
659
660	    queue_set = 1;
661	    break;
662
663	case 'd':
664	    if (program != AT)
665		usage();
666
667	    program = ATRM;
668	    options = "V";
669	    break;
670
671	case 'l':
672	    if (program != AT)
673		usage();
674
675	    program = ATQ;
676	    options = "q:vV";
677	    break;
678
679	case 'b':
680	    if (program != AT)
681		usage();
682
683	    program = BATCH;
684	    options = "f:q:mvV";
685	    break;
686
687	case 'V':
688	    disp_version = 1;
689	    break;
690
691	case 'c':
692	    program = CAT;
693	    options = "";
694	    break;
695
696	default:
697	    usage();
698	    break;
699	}
700    /* end of options eating
701     */
702
703    if (disp_version)
704	fprintf(stderr, "%s version " VERSION "\n"
705	    "Bug reports to: ig25@rz.uni-karlsruhe.de (Thomas Koenig)\n",
706	    namep);
707
708    /* select our program
709     */
710    if(!check_permission())
711	errx(EXIT_FAILURE, "you do not have permission to use this program");
712    switch (program) {
713    case ATQ:
714
715	REDUCE_PRIV(DAEMON_UID, DAEMON_GID)
716
717	list_jobs();
718	break;
719
720    case ATRM:
721
722	REDUCE_PRIV(DAEMON_UID, DAEMON_GID)
723
724	process_jobs(argc, argv, ATRM);
725	break;
726
727    case CAT:
728
729	process_jobs(argc, argv, CAT);
730	break;
731
732    case AT:
733	timer = parsetime(argc, argv);
734	if (atverify)
735	{
736	    struct tm *tm = localtime(&timer);
737	    fprintf(stderr, "%s\n", asctime(tm));
738	}
739	writefile(timer, queue);
740	break;
741
742    case BATCH:
743	if (queue_set)
744	    queue = toupper(queue);
745	else
746	    queue = DEFAULT_BATCH_QUEUE;
747
748	if (argc > optind)
749	    timer = parsetime(argc, argv);
750	else
751	    timer = time(NULL);
752
753	if (atverify)
754	{
755	    struct tm *tm = localtime(&timer);
756	    fprintf(stderr, "%s\n", asctime(tm));
757	}
758
759        writefile(timer, queue);
760	break;
761
762    default:
763	panic("internal error");
764	break;
765    }
766    exit(EXIT_SUCCESS);
767}
768