/*
 * $Id: party.c,v 1.6 2002/08/23 13:38:15 howardjp Exp $
 *
 * Copyright (c) 1990
 *      Jan Wolter.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by Jan Wolter
 *      and his contributors.
 * 4. Neither the name of Jan Wolter nor the names of his contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY JAN WOLTER AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL JAN WOLTER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*  
 *	The party program simulates a party line, where any number of users
 *	can talk at the same time.  It was originally developed under System
 *	III Unix, and as such communicates through a log file rather than
 *	using interprocess communications.  It can only be used between
 *	different computers if they can share the log file via some sort
 *	of NFS-like mechanism.  It should however work under most any version
 *	of Unix.
 *	
 *      This program may be freely distributed without permission of the
 *	author.  No permission is needed to run this software anywhere.
 *
 *						Dr. Jan Wolter
 *						janc@cybernet.org
 *						janc@izzy.net
 * 
 *  RELEASE HISTORY (version numbers before 2.4 were assigned retroactively)
 *
 *  version 1.0:   Original two-process party program by Marcus Watts.
 *  version 1.8:   Jan Wolter's bug fixes, changes in output format, added
 *                 partytab
 *  version 2.0:   Single process rewrite by Jan Wolter.
 *  version 2.1:   Added user settable options.
 *  version 2.2:   Added multiple channels, external filter control.
 *  version 2.3:   Major internal clean-up, reorganized output filters, special
 *	           commands added, noises added.
 *  version 2.4:   Added "who" command, BSD compatibility.  The Grex release.
 *  version 2.5:   Added volatile channels.  :name command.
 *  version 2.6:   Minor bug fixes, System V and SCO portability patches.
 *  version 2.7:   Closable channels, BSDI bug fixes, other minor bug fixes.
 *  version 2.8:   Login names more than 8 characters for next BSDI version;
 *                 norepeat option added; sorted :who listings; showevent.
 *  version 2.9:   Idle time outs, capacity limits, intro commands, no control
 *                 characters in shelled command name
 *  version 2.10:  Add "raw" option and :ignore and :notice commands.
 *  version 2.10a: SIGTERM makes a clean exit.  Fixed Marcus's SIGPIPE bug.
 *                 :list in three columns.  Added "uniquename".  Better ttyname
 *                 checking.  Use MAIL environment variable.  Fixed /dev/tty
 *                 cloaking bug.  :ignore tracks name changes.
 *  version 2.12:  Minor fixes for FreeBSD - howardjp
 */

#include "party.h"
#include "opt.h"
#include <sys/stat.h>
#include <setjmp.h>
#include <errno.h>
#include <ctype.h>
#ifdef VAR_ARGS
#include <stdarg.h>
#endif /*VAR_ARGS*/

char *version= "2.12";		/* current party version */

int inhelp=0;			/* Are we printing help text? */
long tailed_from=0L;		/* File offset we last tailed back from */
FILE *wfd;			/* Party file open for write */
int rst;			/* Party file open for read */
char *channel= NULL;		/* Current channel name (NULL is outside)*/
char *progname;			/* Program name */
char mailfile[80];		/* Name of mail file */
int  mailfuse;			/* counter for deciding when to check mail */
long mailsize;			/* old size of mail file */
int real_id, eff_id;		/* My real and effective group or user id */
char inbuf[BFSZ+INDENT+2];      /* Text buffer - first 10 for "name:   " */
char *txbuf= inbuf + INDENT;    /* Text buffer - pointer to respose portion */
				/*               of inbuf */
FILE *debug= NULL;		/* Debug file, if one is open */
vint (*oldsigpipe)();		/* Old SIGPIPE handler */

jmp_buf jenv;

main(argc,argv)
int argc;
char **argv;
{
register int n;
char ch, *pnl;
long lastaction;

	progname= leafname(argv[0]);	/* Get the program name */

	/* Look self up in utmp */
	if (finduser(logtty,logname,&logtime)) exit(1);

	setrealname();			/* Get the user name */
	initopts();			/* Set default options */
	readtab();			/* Read options from partytab */
	if (who_open()) exit(1);	/* Open the partytmp file */

	/* Check if we are just doing a pwho command */
	if (opt[OPT_USERLIST].yes)
	{
		who_list(stdout,'c');
		exit(0);
	}

	/* Check if there is room for one more in party - race conditions
	 * could allow the capacity to be exceeded if two people enter at
	 * once, but who cares?
	 */
	if (opt[OPT_CAPACITY].yes &&
	    who_count() >= atoi(opt[OPT_CAPACITY].str))
	{
		printexec(stderr,opt[OPT_FULLMESG].str);
		exit(1);
	}

	/* If possible, set OPT_COLS from TERMCAP at this point */
	setcols();

	/* Read options from PARTYOPTS env var */
	if (opt[OPT_ENV].yes && (pnl= getenv("PARTYOPTS")))
	    parseopts(pnl,0);

	/* Read options from parameters */
	if (opt[OPT_ARG].yes)
	    for (n= 1; n < argc; n++)
		parseopts(argv[n],2);

#ifdef SUID
	real_id= getuid(); eff_id= geteuid();
#endif
#ifdef SGID
	real_id= getgid(); eff_id= getegid();
#endif

	initmodes();
	setmailfile();
	initoutput();
	if (who_enter()) done(1);

	printexec(stderr,opt[OPT_INTRO].str);

	/* Put up the signal handlers */
	signal(SIGHUP,(void (*)())hup);
	signal(SIGQUIT,(void (*)())intr);
	signal(SIGINT,(void (*)())intr);
	signal(SIGALRM,(void (*)())alrm);
	signal(SIGTERM,(void (*)())term);
	oldsigpipe= signal(SIGPIPE,SIG_IGN);
#ifdef BSD
	signal(SIGTSTP,(void (*)())susp);
#endif /*BSD*/
#ifdef WINDOWS
	signal(SIGWINCH,(void (*)())setcols);
#endif /*WINDOWS*/

	if (join_party(opt[OPT_START].str)) exit(1);

	/* Get in cbreak/noecho mode */
	STTY(0,&cbreak);

	/* Main loop */
	lastaction= time((long *)0);
	for(;;)
	{
	    /* Copy text while I can -- otherwise go to input mode */
	    /* Someday I need to think about a select() call in here */
	    if (output())
	    {
		/* Wait four seconds for a command */
		alarm(4);
		if (!setjmp(jenv))
		{
		    read(0,&ch,1);	/* Alarm busts us out of here */
		    alarm(0);
		    docmd(ch);
		    lastaction= time((long *)0);
		}
		/* Check for new mail or idleness */
		if (mailfuse-- == 0)
		{
		    struct stat mailstat;

		    /* Check if he has been idle a long time */
		    if (opt[OPT_IDLEOUT].yes &&
			time((long *)0) - lastaction >
			   atoi(opt[OPT_IDLEOUT].str)*60)
		    {
			fprintf(stderr,
			  "You have been idle in party for more than %s minutes\nBye!\n",
			  opt[OPT_IDLEOUT].str);
			done(0);
		    }

		    /* Check if size of mailfile has increased */
		    mailfuse= MAILDELAY;
		    if (stat(mailfile,&mailstat))
			mailsize= 0L;
		    else
		    {
			if (mailstat.st_size > mailsize)
			    fprintf(stderr,"You have more mail\n");
			mailsize= mailstat.st_size;
		    }
		}
	    }
	}
}


alrm()
{
	signal(SIGALRM,(void (*)())alrm);
	longjmp(jenv,1);
}


/* HELP: Prints a file name to the screen.  An interrupt brings you back to
 * party.  If complain is false, it silently does nothing when the file
 * doesn't exist.
 */

help(filename,complain)
char *filename;
int complain;
{
register int hf;
register int n;

	if ((hf= open(filename,0)) < 0)
	{
	    if (complain) err("Cannot open file %s\n",filename);
	}
	else
	{
	    if (!setjmp(jenv))
	    {
		inhelp= 1;
		while((n= read(hf,txbuf,BFSZ)) > 0)
		    write(1,txbuf,n);
	    }
	    close(hf);
	    inhelp= 0;
	}
}


/* READFILE: This reads a file into the partylog, much as if you had typed it
 * in, except that each line is prepended with a space instead of your logname.
 * Files must be readable *both* to party and to the user. 
 */
readfile(filename)
char *filename;
{
FILE *fp;
int readlim= convert(opt[OPT_READLIM].str);
int lines= 0;

	if (readlim == 0)
	{
		err("File reading not enabled in this channel\n");
		return;
	}

	/* Can only open files if user and party can read it */
	be_user();
	if (access(filename,4) || (fp= fopen(filename,"r")) == NULL)
	{
	    err("Cannot open input file %s\n",filename);
	    be_party();
	    return;
	}
	be_party();

	/* Print user's name */
	txbuf[0]= '\n';
	txbuf[1]= '\0';
	LOCK(wfd);
	fseek(wfd,0L,2);
	fputs(inbuf,wfd);
	fflush(wfd);
	
	txbuf[0]= ' ';
	while ((lines++ < readlim)  && fgets(txbuf+1,BFSZ-1,fp))
	{
		fseek(wfd,0L,2);
		fputs(txbuf,wfd);
		if (strchr(txbuf,'\n') == NULL) fputc('\n',wfd);
		fflush(wfd);
	}
	UNLOCK(wfd);
	fclose(fp);
}


/* CHN_FILE_NAME:  return the channel file name for the current channel.
 * Somebody needs to free this.
 */

char *chn_file_name(chn,keeplog)
char *chn;
int keeplog;
{
char *file;

    file= (char *)malloc((mtype)(strlen(opt[OPT_DIR].str)+CHN_LEN+6));
    sprintf(file,"%s/%s.%s",
	opt[OPT_DIR].str,
	chn,
	keeplog?"log":"tmp");
    return(file);
}


/* JOIN_PARTY: changes from the current channel to the new channel.  Either one
 * of those could be NULL.  Returns 1 if the named channel doesn't exist.
 */

join_party(nch)
char *nch;
{
char *file;
FILE *tmp_wfd;
int tmp_rst,oumask,waskept;
long now= time((long *)0);
char newchannel[CHN_LEN+1];
static char ch[CHN_LEN+1];

	if (debug) db("join_party %s\n",nch?nch:"(nil)");

	waskept= opt[OPT_KEEPLOG].yes;

	/* If we are to enter a new channel, make sure we can */
	if (nch != NULL)
	{
	    /* Make a local copy of name */
	    strncpy(newchannel,nch,CHN_LEN);
	    newchannel[CHN_LEN]= '\0';

	    if (badname(newchannel))
	    {
		err("improper channel name\n");
		return(1);
	    }

#ifndef NOCLOSE
	    if (!enter_closed(newchannel))
		return(1);
#endif /*NOCLOSE*/

	    /* Set the channel options */
	    if (chnopt(newchannel))
	    {
		err("channel %s does not exist\n",newchannel);
		return(1);
	    }
	    if (debug) printopts(debug,0,' ',"all");

	    /* Construct the channel log file name */
	    file= chn_file_name(newchannel,opt[OPT_KEEPLOG].yes);

	    /* Open the partyfile to read - if it doesn't exist create it */
	    if ((!opt[OPT_MAYCLOSE].yes && access(file,4)) ||
		(tmp_rst= open(file,O_RDONLY)) < 0)
	    {
		/* I don't have read access, or the file doesn't exist */
		oumask= umask(000);
		if (!access(file,0) ||
		   (tmp_rst= open(file,O_RDONLY|O_CREAT,
			opt[OPT_MAYCLOSE].yes ? DEP_MODE : CHN_MODE)) < 0)
		{
		    err("channel %s not accessible\n(%s unreadable)\n",
			newchannel,file);
		    free(file);
		    umask(oumask);
		    if (channel != NULL) chnopt(channel);
		    return(1);
		}
		umask(oumask);
	    }

	    /* Open partyfile to write */
	    if ((tmp_wfd= fopen(file,"a")) == NULL)
	    {
		err("Cannot write partyfile %s\n",file);
		close(tmp_rst);
		free(file);
		if (channel != NULL) chnopt(channel);
		return(1);
	    }
	    free(file);

	    if (debug) db("join_party: new channel open\n");
	}

	/* If we were in some channel, get out */
	if (channel != NULL)
	{
		if (nch == NULL)
			sprintf(txbuf,"---- %s leaving (%.12s)\n",
				name,ctime(&now)+4);
		else
			sprintf(txbuf,
				"---- %s switching to channel %s (%.12s)\n",
				name,newchannel,ctime(&now)+4);
		write(out_fd,txbuf,strlen(txbuf));

		/* Put departure message in the file */
		append(txbuf,wfd);

		/* Close the old files */
		fclose(wfd);
		close(rst);

		/* If it was a temporary channel and it is empty, delete it */
		if (!waskept && who_empty(channel))
		{
		    file= chn_file_name(channel,0);
		    unlink(file);
		    free(file);
#ifndef NOCLOSE
		    file= usr_file_name(channel);
		    unlink(file);
		    free(file);
#endif /*NOCLOSE*/
		}
	    if (debug) db("join_party: old channel closed\n");
	}

	/* If we are to enter a new channel, finish the job */
	if (nch != NULL)
	{
		/* Make the new file the current file */
		rst= tmp_rst;
		wfd= tmp_wfd;
		tailed_from= 0L;

		/* Set streams to close on exec */
		fcntl(rst,F_SETFD,1);
		fcntl(fileno(wfd),F_SETFD,1);

		/* Print Channel Banner */
		if (channel != NULL)
		{
		    if (opt[OPT_CHANINTRO].yes)
			printexec(stderr,opt[OPT_CHANINTRO].str);
		    else
			fprintf(stderr,
		        "\n================== channel %s ==================\n",
			newchannel);
		    fprintf(stderr,"Options:");
		    if (printopts(stderr,8,' ',"chan"))
			fprintf(stderr,"normal\n\n");
		    else
			fputc('\n',stderr);
		}

		/* Get the user's name or pseudonym */
		setname(newchannel);

		/* Put in a join message */
		LOCK(wfd);
		fseek(wfd,0L,2);
		if (channel == NULL)
		    fprintf(wfd,"---- %s joining (%.12s)\n",
			    name,ctime(&now)+4);
		else
		    fprintf(wfd,"---- %s joining from channel %s (%.12s)\n",
			    name,channel,ctime(&now)+4);
		fflush(wfd);
		UNLOCK(wfd);

		/* Wind back a few lines */
		lseek(rst,0L,2);			/* Goto end of file */
		backup(convert(opt[OPT_BACK].str));

		strcpy(channel=ch,newchannel);

		who_chan();
	}
	else
		who_exit();

	return(0);
}


/* SETMAILFILE:  Start monitering the user's mail directory so we can notify
 * him when he has new mail.
 */

setmailfile()
{
struct stat mailstat;
char *env;

	if ((env= getenv("MAIL")) != NULL)
	    strcpy(mailfile,env);
	else
	{
	    strcpy(mailfile,opt[OPT_MAILDIR].str);
	    strcat(mailfile,"/");
	    strcat(mailfile,realname);
	}
	if (stat(mailfile,&mailstat))
		mailsize= 0L;
	else
		mailsize= mailstat.st_size;
	mailfuse= MAILDELAY;
}

/* LEAFNAME: Return the trailing name from a pathname.
 */

char *leafname(c)
char *c;
{
register char *t;

	if ((t= strrchr(c,'/')) == NULL)
	    return(c);
	else
	    return(t+1);
}


/* BADNAME:  Check a channel name to make sure it contains only legal
 * characters: namely printable, non-space, non-dot, non-slash characters.  We
 * could probably stand dots, but we don't.
 */
badname(chn)
char *chn;
{
char *badchr= " ./#";

	for(;*chn;chn++)
		if (!isascii(*chn) || !isprint(*chn) || index(badchr,*chn))
			return(1);
	return(0);
}

/* CONVERT:  Convert a string to a non-negative integer.  This does a bit more
 * checking than atoi().  It returns -1 if the string is not syntactially
 * correct.
 */

int convert(c)
char *c;
{
register int n=0;

	/* Skip leading spaces and tabs */
	while (*c == ' ' || *c == '\t')
		c++;

	for ( ; *c != '\n' && *c != '\0'; c++)
	{
		if (*c >= '0' && *c <= '9')
			n= n*10 + *c - '0';
		else
			return (-1);
	}
	return(n);
}


#ifdef L_LOCKING
/* Keep calling locking() until it works or until we get sick and tired of
 * calling it.  This apparantly needs to be done because it doesn't block
 * properly. There may be problems with this.
 */

chk_lock(file,request)
FILE *file;
int request;
{
register int i= 5;

	while (locking(fileno(file), request, -1L) && i--)
	    sleep(1);
}
#endif /*L_LOCKING*/


/* BACKUP:  This seeks backwards by the given number of lines.
 */

long backup(lines)
int lines;
{
#define BUB_SIZE 512
char bub[BUB_SIZE];
register char *p;
long off;
int n;

	if (lines <= 0) return;

	p= bub;
	off= lseek(rst,0L,1);

	/* Scan backwards, counting newlines */
	do
	    if (p-- == bub)
	    {
		if (off == 0L) break;
		/* Fetch a new block of data */
		off -= (n= (off > BUB_SIZE) ? BUB_SIZE : off);
		lseek(rst, off, 0);
		read(rst, bub, n);
		p= bub + n - 1;
	    }
	while ( *p != '\n' || lines-- > 0);

	/* Position file pointer */
	return(lseek(rst, off+(p-bub)+1, 0));
}


/* INTR: Handle user interrupts.  If he is tailing back or printing a help
 * file, just interrupt that.
 */

intr()
{

	if (inhelp)
 	{
#ifdef SYSIII
		signal(SIGQUIT,(void (*)())intr);
		signal(SIGINT,(void (*)())intr);
#endif /*SYSIII*/
		longjmp(jenv,1);
	}
	if (tailed_from > lseek(rst,0L,1))
	{
#ifdef SYSIII
		signal(SIGQUIT,(void (*)())intr);
		signal(SIGINT,(void (*)())intr);
#endif /*SYSIII*/
		printf("\nTailback Interrupted...\n");
		lseek(rst,tailed_from,0);
		return;
	}

	alarm(0);

	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);

	join_party(NULL);

	STTY(0,&cooked);
	kill_filter();
	exit(0);
}


/* TERM:  Exit the program because we got a SIGTERM. This is like a hup(), but
 * prints a message to the user. */

term()
{
	if (debug) db("killed\n");

	alarm(0);
	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);

	join_party(NULL);

	kill_filter();
	STTY(0,&cooked);

	printf("\nParty Process Killed.\n");

	exit(0);
}


/* DONE:  Exit the program at the user's command.  This should be used just
 * about everywhere you would normally call exit.
 */

done(rc)
int rc;
{

	if (debug) db("exiting\n");

	alarm(0);

	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);

	join_party(NULL);
	stop_filter();
	STTY(0,&cooked);
	exit(rc);
}

/* HUP:  Handle Hangups.  The only difference betwen this and done() is that
 * we are more brutal about killing off the filter.
 */

hup()
{
	if (debug) db("hangup\n");

	alarm(0);
	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);

	join_party(NULL);

	kill_filter();
	STTY(0,&cooked);
	exit(0);
}

/* SUSP: Suspend execution temporarily.
 */

#ifdef BSD
susp()
{
#ifdef F_TERMIOS
struct termios old;
#endif
#ifdef F_STTY
struct sgttyb old;
#endif
int mask;
int was_shelled;

	if (debug) db("suspended\n");

	if (!(was_shelled= who_isout())) who_shout("^");
	alarm(0);
	mask= sigblock(sigmask(SIGTSTP));

	GTTY(0,&old);
	STTY(0,&cooked);

	signal(SIGTSTP,SIG_DFL);
	sigsetmask(0);
	kill(getpid(),SIGTSTP);

	/* STOP HERE */

	sigsetmask(mask);
	signal(SIGTSTP,(void (*)())susp);

	initmodes();
	STTY(0,&old);
	if (!was_shelled) who_shin();

	if (debug) db("restarted\n");
}
#endif /*BSD*/

#ifdef VAR_ARGS
vint db(char *msg, ...)
{
va_list ap;
long now;

    if (debug)
    {
        va_start(ap, msg);

	now= time((long *)0);
        fprintf(debug,"%8.8s:  ",ctime(&now)+11);
        vfprintf(debug,msg,ap);
	fflush(debug);
    }
    va_end(ap);
}
#else
vint db(msg,arg1,arg2,arg3,arg4)
char *msg, *arg1, *arg2, *arg3, *arg4;
{
long now;

    if (debug)
    {
	now= time((long *)0);
        fprintf(debug,"%8.8s:  ",ctime(&now)+11);
        fprintf(debug,msg,arg1,arg2,arg3,arg4);
	fflush(debug);
    }
}
#endif /*VAR_ARGS*/


#ifdef VAR_ARGS
vint err(char *msg, ...)
{
va_list ap;
long now;

    va_start(ap, msg);
    fprintf(stderr,"%s error: ",progname);
    vfprintf(stderr,msg,ap);

    if (debug)
    {

	now= time((long *)0);
	fprintf(debug,"%8.8s:  ",ctime(&now)+11);
	vfprintf(debug,msg,ap);
	fflush(debug);
    }
    va_end(ap);
}
#else
vint err(msg,arg1,arg2,arg3,arg4)
char *msg, *arg1, *arg2, *arg3, *arg4;
{
long now;

    fprintf(stderr,"%s error: ",progname);
    fprintf(stderr,msg,arg1,arg2,arg3,arg4);

    if (debug)
    {
	now= time((long *)0);
        fprintf(debug,"%8.8s:  ",ctime(&now)+11);
        fprintf(debug,msg,arg1,arg2,arg3,arg4);
	fflush(debug);
    }
}
#endif /*VAR_ARGS*/


syntax highlighted by Code2HTML, v. 0.9.1