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