#include "bbs.h"

sigjmp_buf env, env2, envsigint;
boolean bbsdaemon = TRUE;
SIGSET_T old_blocksigset;
sigmaskstatusenum blocksigsetstatus = SIGMASK_UNSET;

int main(int argc, char *argv[], char *environ[])
  /*
  Der Daemon (bbsd)
  */
{
  PID_T dpid, dkillpid;
  int k, sockd, len;
  int status, logfilefd;
  char conffile[PATH_MAX+1], bbsdpath[PATH_MAX+1], c;
  struct sockaddr_un saun;
  confrecordtyp confrecord;
  sessionrecordtyp sessionrecords[MAXSESSIONS];
  boolean argserror=FALSE, daemon=FALSE;
  extern int optind, opterr, optopt;
  extern char *optarg;

  /* Environment loeschen, Variable vorbesetzen und Optionen einlesen */
  environ[0] = (char *)NULL;
  sessionrecords[0].pid = 0;
  confrecord.curses_on = FALSE;
  strcpy(conffile,CONFFILE);
  strcpy(bbsdpath,BBSDPATH);
  len = 0;
  opterr = FALSE;
  while ((c = getopt(argc, argv, "dc:p:")) != EOF) {
    switch (c) {
    case 'd':
      daemon = TRUE;
      break;
    case 'c':
      strmaxcpy(conffile,optarg,PATH_MAX);
      len += strlen(optarg);
      break;
    case 'p':
      strmaxcpy(bbsdpath,optarg,PATH_MAX);
      len += strlen(optarg);
      break;
    case '?':
      fprintf(stderr,"bbsd: unrecognized option: -%c\n", optopt);
      argserror = TRUE;
    }
    len += 3;
  }
  if (argserror || (optind < argc)) {
    fprintf(stderr,"usage: bbsd [-d] [-c conffile] [-p bbsdpath]\n");
    exit(-1);
  }
  
#ifdef BBSDNAME
  /* Daemon untern neuem Namen (argv[0]) fuer 'ps' neu starten */
  if (strcmp(argv[0],BBSDNAME) != 0) {
    argv[0] = BBSDNAME;
    execv(bbsdpath,argv);
    openlog("bbsd",LOG_PID,LOG_BBS);
    bgerror("bbsd","execl: %m");
    perror("bbsd: execl");
    closelog();
    exit(1);
  }
  BZERO(argv[1],len*sizeof(char));
#endif

  openlog("bbsd",LOG_PID,LOG_BBS);
  if (readconffile(&confrecord,conffile) < 0) {
    bgerror("bbsd","Terminated: cannot read conffile");
    fprintf(stderr,"Terminated: cannot read conffile\n");
    exit(1);
  }
  if (strcmp(confrecord.bbsdpath,bbsdpath) != 0) {
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd",
    "Terminated: different pathes for bbsd between conffile(%s) and p-option(%s)",
    confrecord.bbsdpath,bbsdpath);
    exit(1);
  }
  
  /* Bei Bedarf effectiv-uid wechseln */
#ifdef BBSDUID
  if (changeuid(confrecord.bbsduid) < 0) {
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd",
             "Terminated: changeuid failed");
    exit(1);
  }
#endif

  /* Socket zur Kommunikation mit den Clients einrichten */
  if ((sockd=socket(AF_UNIX,SOCK_STREAM,0))<0) {
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd","socket: %m");
    exit(1);
  }
  BZERO(&saun, sizeof(saun));
  saun.sun_family = AF_UNIX;
  strcpy(saun.sun_path,confrecord.sockpath);
  len = SUN_LEN(&saun);
#ifdef SCM_RIGHTS
  saun.sun_len = len;
#endif
  if (bind(sockd,(struct sockaddr *)&saun,len)<0) {
    if (errno==EINVAL) {
      /* es laeuft bereits ein Daemon */
      exit(0);
    }
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd","bind %s: %m",
             confrecord.sockpath);
    unlink(confrecord.sockpath);
    exit(1);
  }
  if (listen(sockd,5)<0) {
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd",
             "listen %s: %m", confrecord.sockpath);
    unlink(confrecord.sockpath);
    exit(1);
  }
  
  /* Lockfile loeschen */
  unlink(confrecord.lockpath);
  
  /* Logfile oeffnen */
  umask(022);
  if ((logfilefd=open(confrecord.logfile,O_WRONLY|O_CREAT|O_APPEND,0644))<0) {
    errormsg(E_SYSLOG|E_USER|E_CONSOLE,&confrecord,"bbsd","bbsd",
             "open Logfile %s: %m", confrecord.logfile);
    unlink(confrecord.sockpath);
    exit(1);
  }
  
  dpid = getpid();
  /* in den Hintergrund */
  if (daemon) {
    dpid = disconnect(&confrecord);
  }
  
  /* Daemonenkiller installieren */
  dkillpid = -1;
#ifdef DAEMONENKILLER
  if (confrecord.bbsdwatchtime > 0) {
    dkillpid = daemonenkiller(daemon,argv[0],dpid,&confrecord);
  }
#endif

  /* Pidfiles anlegen */
  writepidsfile(&confrecord,dpid,dkillpid);
  bgerror("bbsd","Daemon started");

  if (sigsetjmp(env,(int)TRUE)==0) {  /* als Rueckkehrpunkt fuer manche Signale */
    /* Signale in den Signalhaendler umlenken */
    setsighandler(SIGUSR1, dsighandler);
    setsighandler(SIGINT, dsighandler);
    setsighandler(SIGTERM,dsighandler);  
    if (daemon) {
      setsighandler(SIGHUP, SIG_IGN);
      setsighandler(SIGQUIT, SIG_IGN);
    }
    else {
      setsighandler(SIGHUP,dsighandler);
      setsighandler(SIGQUIT,dsighandler);
    }
    if (sigsetjmp(env2,(int)TRUE) != 0) {
      unlink(confrecord.lockpath);
      rmdeadsessions(sessionrecords);
    }
    if (sigsetjmp(envsigint,(int)TRUE) != 0) {
      dumpbbsdstatus(DUMPDIR,sessionrecords,&confrecord,conffile);
    }
    setsighandler(SIGPIPE, dsighandler);
    /* Anforderungen von den Clients abarbeiten */
    do {
      doaction(sockd,logfilefd,sessionrecords,&confrecord);
    } while (TRUE);
  }
  
  /* auf Daemonenkiller warten */
  if (dkillpid > 0) {
    /* Daemonenkiller killen */
    kill(dkillpid,SIGTERM);
    while (wait(&status)!=dkillpid);
    bgerror("bbsd","Daemonenkiller exited normally");
  }

  close(logfilefd);                  /* Logfile schliessen */
  unlink(confrecord.sockpath);       /* Kommunikationssocket entfernen */
  unlink(confrecord.lockpath);       /* Lockfile entfernen */
  removepidsfile(&confrecord);       /* Pid-Files entfernen */
  /* allen Clients ein SIGTERM senden */
  for (k=0;sessionrecords[k].pid!=(PID_T)0;k++) {
    kill(sessionrecords[k].pid,SIGTERM);
  }
  bgerror("bbsd","Daemon exited normally");
  closelog();
  return 0;
}


void dsighandler(int sig)
  /*
  Signale abarbeiten (Daemon)
   */
{
  switch (sig) {
  case SIGHUP:
  case SIGQUIT:
  case SIGTERM:
    bgerror("dsighandler","got signal %i",sig);
    siglongjmp(env,1);
    exit(1);
    break;
  case SIGINT:
    bgerror("dsighandler","got signal sigint, dumping status in %s",DUMPDIR);
    siglongjmp(envsigint,1);
    exit(1);
    break;
  case SIGPIPE:
    bgerror("dsighandler","got signal sigpipe");
    siglongjmp(env2,1);
    exit(1);
    break;
  case SIGUSR1:
    bgerror("dsighandler","got signal sigusr1, checking clinets");
    siglongjmp(env2,1);
    exit(1);
    break;
  }
}


int rmdeadsessions(sessionrecordtyp sessionrecords[])
  /*
  alle Sessions auf Existens pruefen und gegebenfalls entfernen.
  Anzahl der Sessions wird zurueckgegeben.
  */
{
  int k, n=0;

  while (sessionrecords[n].pid!=0) {
    if (kill(sessionrecords[n].pid,0)<0) {
      if (errno==ESRCH) {
	k = n;
	while (sessionrecords[k].pid!=0) {
	  cpysr(&(sessionrecords[k]), &(sessionrecords[k+1]));
	  k++;
	}
      }
      else {
	n++;
      }
    }
    else {
      n++;
    }
  }
  return(n);
}


void cpysr(sessionrecordtyp *srdest, const sessionrecordtyp *srsrc)
  /*
  Sessionrecord kopieren
  */
{
  srdest->pid = srsrc->pid;
  srdest->status = srsrc->status;
  strcpy(srdest->tty,srsrc->tty);
  strcpy(srdest->user,srsrc->user);
  srdest->talking_to = srsrc->talking_to;
}


int dumpbbsdstatus(const char *dumpdir,sessionrecordtyp sessionrecords[],
                   confrecordtyp *confrecord, const char *conffile)
  /*
  Zustand vom Daemon (Daten) dumpen
  */
{
  int n;
  char dumppath[PATH_MAX+1];
  FILE *fp;
  sessionrecordtyp sr;
  
  sprintf(dumppath,"%s/bbsddump.%ld", dumpdir, (long)getpid());
  if ((fp=fopen(dumppath,"a")) == NULL) {
    bgerror("dumpbbsdstatus","cannot open %s: %m",dumppath);
    return(-1);
  }
  fprintf(fp,"*** BBSD (%ld) STATUS from %s ***\n\n", (long)getpid(),
          gettime());
  fprintf(fp,"CONFFILE: %s\n", conffile);
  fprintf(fp,"Sessions:\n");
  n = 0;
  sr = sessionrecords[n++];
  while (sr.pid != (PID_T)0) {
    fprintf(fp,"PID %ld, USER %s at TTY %s STATUS %d, TALKING_TO %ld\n",
            (long)sr.pid, sr.user, sr.tty, sr.status, (long)sr.talking_to);
    sr = sessionrecords[n++];
  }
  fclose(fp);
  
  return(0);
}


int changeuid(const char *uid_name)
  /*
  effective uid wechseln auf den User mit dem Namen uid_name
  */
{
  UID_T neweuid, euid;
  struct passwd *pwent;
  
  if (uid_name == NULL)  return 0;
  if ((pwent=getpwnam(uid_name)) == NULL) {
    bgerror("changuid","user %s not found",uid_name);
    return(-1);
  }
  neweuid = pwent->pw_uid;
  euid = geteuid();
  if (euid == neweuid)  return 0;
  if (euid != 0) {
    bgerror("changuid","bbsd must be suid root");
    return(-1);
  }
  if (setuid(neweuid) < 0) {
    bgerror("changuid","Changeing euid to %s failed",uid_name);
    return(-1);
  }
  return 0;
}


PID_T disconnect(confrecordtyp *confrecord)
  /*
  Programm in Daemon umwandeln
  Zurueckgegeben wird der pid des Daemons
  */
{
  PID_T pid;
#ifdef NO_SETSID
  int fd;
#endif
  
  if ((pid=fork())<0) {
    errormsg(E_SYSLOG|E_USER,confrecord,"bbsd","disconnect","fork: %m");
    unlink(confrecord->sockpath);
    exit(1);
  }
  if (pid!=0) {
    exit(0);
  }
  fflush(stdout);
  close(0);
  close(1);
  close(2);
  pid = getpid();
#ifdef NO_SETSID
  setpgrp(0,pid);
  if ((fd=open("/dev/tty",O_RDWR))<0) {
/*
    bgerror("disconnect","/dev/tty");
    exit(1);
*/
  }
  else {
    ioctl(fd, (int) TIOCNOTTY, (char *) 0);
    close(fd);
  }
#else
  setsid();
#endif
  return pid;
}


PID_T daemonenkiller(const boolean daemon, char *ps_name, const PID_T dpid,
                     confrecordtyp *confrecord)
  /*
  Daemonenkiller starten, das ist ein neues Programm, das regelmaessig
  die Anzahl der Clients prueft, und den Daemon killt, wenn keine Clients
  mehr vorhanden sind.
  */
{
  PID_T pid;
  sessionrecordtyp sr[1];
  boolean startkiller = FALSE;
  
  if ((pid=fork())<0) {
    bgerror("daemonenkiller","fork: %m");
    exit(1);
  }
  if (pid==0) {
    bbsdaemon = FALSE;
#ifdef BBSDNAME
    strcpy(ps_name, DAEMONENKILLERNAME);
#endif
    if (sigsetjmp(env2,(int)TRUE)==0) {  /* als Rueckkehrpunkt fuer manche Signale */
      setsighandler(SIGHUP,kdsighandler);
      setsighandler(SIGINT,kdsighandler);
      setsighandler(SIGQUIT,kdsighandler);
      setsighandler(SIGTERM,kdsighandler);
      setsighandler(SIGUSR1,SIG_IGN);
      while(! startkiller) {
        if (access(confrecord->lockpath,F_OK)<0) {
	  startkiller = (getsessions(sr,0,confrecord)>0);
	}
        sleep(confrecord->bbsdwatchtime);
      }
      while (TRUE) {
        /* Clients pruefen */
        if (access(confrecord->lockpath,F_OK)<0 && daemon) {
          if (getsessions(sr,0,confrecord)==0) {
	    bbslog(LOG_FILE, confrecord,"Daemon exited (no clients)");
	    kill(dpid,SIGTERM);
	    exit(0);
	  }
        }
        sleep(confrecord->bbsdwatchtime);
      }
    }
    kill(dpid,SIGTERM);
    exit(0);
  }
  return pid;
}


void kdsighandler(int sig)
  /*
  Signale abarbeiten (Daemonenkiller)
  */
{
  switch (sig) {
  case SIGHUP:
  case SIGINT:
  case SIGQUIT:
  case SIGTERM:
    bgerror("kdsighandler","got signal %i",sig);
    siglongjmp(env2,1);
    exit(1);
    break;
  }
}


int doaction(const int sockd, const int logfilefd,
             sessionrecordtyp sessionrecords[],
             confrecordtyp *confrecord)
  /*
  Abarbeitungsschleife fuer die Jobs von den Clients
  */
{
  int flen, ns, n, befehl;
  char str[STRLEN+1];
  struct sockaddr_un fsaun;
  SIGSET_T sigdoactionmask;
  
  /* Verbindungsanforderung vom Client annehmen */
  BZERO(&fsaun, sizeof(fsaun));
  fsaun.sun_family = AF_UNIX;
  strcpy(fsaun.sun_path,confrecord->sockpath);
  flen = SUN_LEN(&fsaun);
#ifdef SCM_RIGHTS
  fsaun.sun_len = flen;
#endif
  if ((ns=accept(sockd,(struct sockaddr *)&fsaun,&flen))<0) {
    bgerror("doaction","accept %s: %m",confrecord->sockpath);
    return -1;
  }
  /* Signale blockieren */
  sigemptyset(&sigdoactionmask);
  sigaddset(&sigdoactionmask, SIGUSR1);
  sigaddset(&sigdoactionmask, SIGINT);
  sigprocmask(SIG_BLOCK,&sigdoactionmask,NULL);
  /* Authorisierung */
  if (recvn(ns,(void *)&n,(SIZE_T)sizeof(int),0)<0) {
    bgerror("doaction","recv %s: %m",confrecord->sockpath);
    return -1;
  }
  if (recvn(ns,(void *)str,(SIZE_T)n,0)<0) {
    bgerror("doaction","recv %s: %m",confrecord->sockpath);
    return -1;
  }
  if (strncmp(str,confrecord->authkey,STRLEN) != 0) {
    sleep(2);
    n = -1;
    sendn(ns,(void *)&n,(SIZE_T)sizeof(int),0);
    close(ns);
    bgerror("doaction","wrong authorisation");
    return n;
  }
  sendn(ns,(void *)&n,(SIZE_T)sizeof(int),0);
  /* Befehl lesen */
  if (recvn(ns,(void *)str,(SIZE_T)BEFSTRLEN,0)<0) {
    bgerror("doaction","recv %s: %m",confrecord->sockpath);
    return -1;
  }
  if (str[0]!='#' || str[3]!='\n') {
    bgerror("doaction","wrong command-format");
    return -1;
  }
  befehl = atoi(&(str[1]));
  /* Befehl ausfuehren, waehrend der Ausfuehrung locken wenn daemonenkiller 
     laeuft */
  if (confrecord->bbsdwatchtime > 0) {
    n = mklockfile(confrecord->lockpath);
  }
  else {
    n = 0;
  }
  if (n == 0) {
    switch (befehl) {
      case DO_LOG:
        n = dputinlog(logfilefd,ns);
        break;
      case DO_SENDSTATUS:
        n = dsendstatustodaemon(sessionrecords,confrecord,ns);
	break;
      case DO_TALKINIT:
        n = diniconnecttouser(sessionrecords,confrecord,ns);
	break;
      case DO_TALKABORT:
        n = dabortconnecttouser(sessionrecords,confrecord,ns);
	break;
      case DO_TALKREMOTE:
        n = dremconnecttouser(sessionrecords,confrecord,ns);
	break;
      case DO_GETSESSIONS:
        n = dgetsessions(sessionrecords,ns);
        break;
      case DO_ADDSESSION:
        n = daddsession(sessionrecords,ns);
        break;
      case DO_REMOVESESSION:
        n = dremovesession(sessionrecords,ns);
        break;
      case DO_GETUSERRECORD:
        n = dgetuserrecord(ns,confrecord);
        break;
      case DO_SAVEUSERRECORD:
        n = dsaveuserrecord(ns,confrecord);
        break;
      default:
        syslog(LOG_ERR,"unknown command");
        n = -2;
    }
    /* Befehl quittieren */
    sendn(ns,(void *)&n,(SIZE_T)sizeof(int),0);
    close(ns);
  }
  else {
    bgerror("doaction","lock failed, command could not be performed");
  }
  if (confrecord->bbsdwatchtime > 0) {
    unlink(confrecord->lockpath);
  }
  sigprocmask(SIG_UNBLOCK,&sigdoactionmask,NULL);
  return n;
}


int writepidsfile(confrecordtyp *confrecord, const PID_T dpid,
                  const PID_T kdpid)
  /*
  Liest eventuell vorhandene Pid-Files vom Daemon und Daemonenkiller
  und sendet SIGTERM an sie.
  Legt neue Pid-Files an
  */
{
  PID_T pid;
  char str[S_STRLEN+1];
  FILE *fd;
  
  if (access(confrecord->bbsdpidpath,F_OK)==0) {
    if ((fd=fopen(confrecord->bbsdpidpath,"r")) != NULL) {
      fgetnln(str,S_STRLEN,fd);
      fclose(fd);
      /* hier droht eine Race-Condition: SIGTERM killt den alten Daemon,
         der dann seinerseits sein Pid-File loeschen will, das jedoch hier
	 neu angelegt wird ...
      */
      pid = (PID_T)atoi(str);
      if (kill(pid,SIGTERM) == 0) {
        bgerror("writepidsfile","old Daemon[%i] killed",pid);
	while(kill(pid,0)==0)  sleep(2);  /* wegen Race-Condition */
      }
      else {
        bgerror("writepidsfile","stale pidfile %s[%i]",confrecord->bbsdpidpath,pid);
      }
    }
    else {
      bgerror("writepidsfile","pidfile %s exists but unreadable",confrecord->bbsdpidpath);
    }
    unlink(confrecord->bbsdpidpath);
  }
  if ((fd=fopen(confrecord->bbsdpidpath,"w")) == NULL) {
    bgerror("writepidsfile","cannot create pidsfile %s: %m",confrecord->bbsdpidpath);
    return(-1);
  }
  fprintf(fd,"%ld\n",(long)dpid);
  fclose(fd);
  
  if (access(confrecord->bbsdkpidpath,F_OK)==0) {
    if ((fd=fopen(confrecord->bbsdkpidpath,"r")) != NULL) {
      fgetnln(str,S_STRLEN,fd);
      fclose(fd);
      pid = (PID_T)atoi(str);
      if (kill(pid,SIGTERM) == 0) {
        bgerror("writepidsfile","old Daemonkiller[%i] killed",pid);
	while(kill(pid,0)==0)  sleep(2);  /* wegen Race-Condition */
      }
      else {
        bgerror("writepidsfile","stale pidfile (killer) %s[%i]",confrecord->bbsdkpidpath,pid);
      }
    }
    else {
      bgerror("writepidsfile","pidfile (killer) %s exists but unreadable",
              confrecord->bbsdkpidpath);
    }
    unlink(confrecord->bbsdkpidpath);
  }
  if (kdpid > 0) {
    if ((fd=fopen(confrecord->bbsdkpidpath,"w")) == NULL) {
      bgerror("writepidsfile","cannot create pidsfile (killer) %s: %m",
              confrecord->bbsdkpidpath);
      return(-1);
    }
    fprintf(fd,"%ld\n",(long)kdpid);
    fclose(fd);
  }
  
  return 0;
}
  
  
int removepidsfile(confrecordtyp *confrecord)
{
  unlink(confrecord->bbsdpidpath);
  unlink(confrecord->bbsdkpidpath);
  return 0;
}
