/*
 * userv - overlord.c
 * daemon main program, collects request and forks handlers
 *
 * userv is copyright Ian Jackson and other contributors.
 * See README for full authorship information.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with userv; if not, see <http://www.gnu.org/licenses/>.
 */

#include <unistd.h>
#include <signal.h>
#include <syslog.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <fnmatch.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <time.h>
#include <dirent.h>
#include <sys/un.h>

#include "config.h"
#include "common.h"
#include "daemon.h"

pid_t overlordpid;

static pid_t checkpid= -1, detachpid= -1;
static sig_atomic_t needcheck= 1; /* 2 means we half-expect the server to be down */

static void checkstalepipes(void) {
  /* There is an unimportant race here.  If there is a stale pipe but
   * another pair of processes with the same pids is about to create a
   * new one we can check that the pipe is stale before they recreate
   * it but then only remove it afterwards; then we remove the pipe
   * they're actually using causing that invocation to fail with
   * ENOENT on the pipe.  However, this can only happen if things are
   * already shafted, because we check for stale pipes at startup
   * before any children have been started, and then only when a child
   * dies unhelpfully - and we actually have to have some stale pipes
   * for the race to exist.
   */
  DIR *dir;
  struct dirent *de;
  struct stat stab;
  time_t now;
  unsigned long timediff;
  int r;
  
  if (time(&now) == -1) { syslog(LOG_ERR,"get current time: %m"); return; }
  dir= opendir(".");
  if (!dir) { syslog(LOG_ERR,"open directory " VARDIR ": %m"); return; }
  while ((errno=0, de= readdir(dir))) {
    if (fnmatch(PIPEPATTERN,de->d_name,FNM_PATHNAME|FNM_PERIOD)) continue;
    r= lstat(de->d_name,&stab); if (r && errno==ENOENT) continue;
    if (r) { syslog(LOG_ERR,"could not stat `" VARDIR "/%s': %m",de->d_name); continue; }
    timediff= (unsigned long)now - (unsigned long)stab.st_ctime;
    if (timediff >= (~0UL>>1) || timediff < 3600) continue;
    if (unlink(de->d_name) && errno!=ENOENT)
      syslog(LOG_ERR,"could not remove stale pipe `%s': %m",de->d_name);
  }
  if (errno) syslog(LOG_ERR,"read directory " VARDIR ": %m");
  if (closedir(dir)) syslog(LOG_ERR,"close directory " VARDIR ": %m");
}

static void sighandler_chld(int x) {
  pid_t r;
  int status, es;

  es= errno;
  for (;;) {
    r= waitpid((pid_t)-1,&status,WNOHANG);
    if (!r || (r==-1 && errno==ECHILD)) break;
    if (r==-1) { syslog(LOG_ERR,"wait in sigchild handler gave error: %m"); break; }
    if (r==detachpid) {
      if (WIFEXITED(status) && WEXITSTATUS(status)==4) _exit(4);
      fprintf(stderr,"uservd: detaching child failed with unexpected code %d\n",status);
      exit(6);
    }
    if (r==checkpid) {
      if (WIFEXITED(status)) {
	if (!WEXITSTATUS(status)) {
	  syslog(LOG_WARNING,"no longer the uservd - exiting");
	  _exit(2);
	} else if (WEXITSTATUS(status)!=1) {
	  syslog(LOG_ERR,"check pid %ld exited with status %d",
		 (long)checkpid,WEXITSTATUS(status));
	}
      } else if (WIFSIGNALED(status)) {
	if (WTERMSIG(status) == SIGALRM && !WCOREDUMP(status)) {
	  syslog(LOG_WARNING,"check timed out; no longer the uservd - exiting");
	  _exit(2);
	} else {
	  syslog(LOG_ERR,"check pid %ld %s due to signal %s",
		 (long)checkpid,
		 WCOREDUMP(status) ? "dumped core" : "died",
		 strsignal(WTERMSIG(status)));
	}
      } else {
	syslog(LOG_ERR,"check pid %ld died due to unknown reason, code %d",
	       (long)checkpid,status);
      }
      checkpid= -1;
      alarm(USERVD_MYSELF_CHECK);
    } else {
      if (WIFSIGNALED(status)) {
	syslog(LOG_ERR,"call pid %ld %s due to signal %s",
	       (long)r,
	       WCOREDUMP(status) ? "dumped core" : "died",
	       strsignal(WTERMSIG(status)));
      } else if (!WIFEXITED(status)) {
	syslog(LOG_ERR,"call pid %ld died due to unknown reason, code %d",
	       (long)r,status);
      } else if (WEXITSTATUS(status)==10) {
	needcheck= 2;
      } else if (WEXITSTATUS(status)>12) {
	if (WEXITSTATUS(status)>24)
	  syslog(LOG_ERR,"call pid %ld exited with status %d >24",
		 (long)r,WEXITSTATUS(status));
	checkstalepipes();
	needcheck= 1;
      }
    }
  }
  errno= es;
  return;
}

static void sighandler_usr1(int x) {
  _exit(0);
}

static void sighandler_alrm(int x) {
  needcheck= 1;
}

static void sighandler_termint(int sig) {
  syslog(LOG_NOTICE,"terminating due to signal %s",strsignal(sig));
  _exit(1);
}

static void blocksignals(int how) {
  int r;
  sigset_t set;

  sigemptyset(&set);
  sigaddset(&set,SIGCHLD);
  sigaddset(&set,SIGALRM);
  sigaddset(&set,SIGTERM);
  sigaddset(&set,SIGINT);
  r= sigprocmask(how,&set,0); assert(!r);
}

static void NONRETURNING docheck(int needwanted) {
#ifndef DEBUG
  /* This subprocess exits with status 0 if the parent should die,
   * 1 if it should not, and something else if it fails horribly.
   */
  int sfd, r, remain;
  unsigned char *p;
  struct opening_msg opening_mbuf;
  struct request_msg request_mbuf;
  unsigned long endmagic;
  struct sigaction sig;
  struct sockaddr_un ssockname;

  openlog(USERVDCHECK_LOGIDENT,LOG_NDELAY|LOG_PID,USERVD_LOGFACILITY);

  sigemptyset(&sig.sa_mask);
  sig.sa_flags= 0;
  sig.sa_handler= SIG_IGN;
  if (sigaction(SIGPIPE,&sig,0)) { syslog(LOG_ERR,"ignore sigpipe"); exit(1); }

  sig.sa_handler= SIG_DFL;
  if (sigaction(SIGALRM,&sig,0)) { syslog(LOG_ERR,"default sigalarm"); exit(1); }

  sfd= socket(AF_UNIX,SOCK_STREAM,0);
  if (!sfd) { syslog(LOG_ERR,"ignore sigpipe"); exit(1); }

  assert(sizeof(ssockname.sun_path) > sizeof(RENDEZVOUS));
  ssockname.sun_family= AF_UNIX;
  strcpy(ssockname.sun_path,RENDEZVOUS);

  r= connect(sfd,(struct sockaddr*)&ssockname,sizeof(ssockname));
  if (r) {
    if (errno == ECONNREFUSED || errno == ENOENT) {
      if (needwanted != 2)
	syslog(LOG_WARNING,"real uservd daemon is not running: %m");
      exit(0);
    }
    syslog(LOG_ERR,"unable to connect to uservd daemon: %m"); exit(1);
  }

  alarm(USERVD_MYSELF_TIMEOUT);
  remain= sizeof(opening_mbuf); p= (unsigned char*)&opening_mbuf;
  while (remain) {
    r= read(sfd,p,remain);
    if (r<0) { syslog(LOG_ERR,"read from server: %m"); exit(1); }
    if (r==0) { syslog(LOG_ERR,"unexpected EOF from server"); exit(1); }
    remain-= r; p+= r;
  }
  if (opening_mbuf.magic != OPENING_MAGIC) {
    syslog(LOG_WARNING,"magic number mismatch");
    exit(0);
  }
  if (memcmp(opening_mbuf.protocolchecksumversion,protocolchecksumversion,PCSUMSIZE)) {
    syslog(LOG_WARNING,"protocol checksum mismatch");
    exit(0);
  }
  if (opening_mbuf.overlordpid != overlordpid) {
    syslog(LOG_WARNING,"overlord pid mismatch");
    exit(0);
  }
  memset(&request_mbuf,0,sizeof(request_mbuf));
  request_mbuf.magic= REQUEST_MAGIC;
  request_mbuf.clientpid= -1;
  request_mbuf.serviceuserlen= 0;
  request_mbuf.servicelen= 0;
  request_mbuf.loginnamelen= 0;
  request_mbuf.spoofed= 0;
  request_mbuf.cwdlen= 0;
  request_mbuf.overridelen= -1;
  request_mbuf.callinguid= -1;
  request_mbuf.ngids= 0;
  request_mbuf.nreadfds= 0;
  request_mbuf.nwritefds= 0;
  request_mbuf.nargs= 0;
  request_mbuf.nvars= 0;
  r= write(sfd,&request_mbuf,sizeof(request_mbuf));
  if (r==sizeof(request_mbuf)) {
    endmagic= REQUEST_END_MAGIC;
    r= write(sfd,&endmagic,sizeof(endmagic));
    (void)r;
  }
  syslog(LOG_NOTICE,"uservd[%ld] is running",(long)overlordpid);
#endif
  exit(1);
}

static void NONRETURNING startupsyscallerr(const char *what) {
  fprintf(stderr,
	  "uservd: system call failed during startup:\n"
	  "uservd: %s: %s\n",
	  what,strerror(errno));
  exit(4);
}

int main(int argc, char *const *argv) {
  int mfd, sfd, nfd, e, r, becomedaemon;
  socklen_t csocklen;
  struct sigaction sigact;
  struct sockaddr_un ssockname, csockname;
  pid_t child, parentpid, sid;

#ifdef NDEBUG
  abort(); /* Do not disable assertions in this security-critical code ! */
#endif

  becomedaemon= 0;
  
  if (argv[1] && !strcmp(argv[1],"-daemon")) {
    becomedaemon= 1;
    argv++; argc--;
  }
  if (argc>1) { fputs("usage: uservd [-daemon]\n",stderr); exit(3); }

  openlog(USERVD_LOGIDENT,LOG_NDELAY|LOG_PID,USERVD_LOGFACILITY);

  if (chdir(VARDIR)) startupsyscallerr("cannot change to " VARDIR);
  checkstalepipes();

  overlordpid= parentpid= getpid();
  if (parentpid==-1) startupsyscallerr("cannot getpid");

  mfd= socket(AF_UNIX,SOCK_STREAM,0);
  if (mfd<0) startupsyscallerr("cannot create master socket");

  assert(sizeof(ssockname.sun_path) > sizeof(RENDEZVOUS));
  ssockname.sun_family= AF_UNIX;
  strcpy(ssockname.sun_path,RENDEZVOUS);
  unlink(RENDEZVOUS);
  r= bind(mfd,(struct sockaddr*)&ssockname,sizeof(ssockname));
  if (r) startupsyscallerr("cannot bind master socket");
  if (listen(mfd,5)) startupsyscallerr("cannot listen on master socket");

  sigemptyset(&sigact.sa_mask);
  sigaddset(&sigact.sa_mask,SIGCHLD);
  sigaddset(&sigact.sa_mask,SIGALRM);
  sigact.sa_flags= SA_NOCLDSTOP;

  sigact.sa_handler= sighandler_chld;
  if (sigaction(SIGCHLD,&sigact,0)) startupsyscallerr("cannot setup sigchld handler");

  sigact.sa_handler= sighandler_alrm;
  if (sigaction(SIGALRM,&sigact,0)) startupsyscallerr("cannot setup sigalrm handler");

  if (becomedaemon) {
    sigact.sa_handler= sighandler_usr1;
    if (sigaction(SIGUSR1,&sigact,0)) startupsyscallerr("cannot setup sigusr1 handler");
    
    detachpid= fork(); if (detachpid==-1) startupsyscallerr("cannot fork to detach");
    if (detachpid) {
      pause();
      fputs("uservd: pause unexpectedly returned during detach\n",stderr);
      exit(4);
    }
    sigact.sa_handler= SIG_DFL;
    if (sigaction(SIGUSR1,&sigact,0)) startupsyscallerr("cannot restore sigusr1");
  }

  sigact.sa_handler= sighandler_termint;
  if (sigaction(SIGTERM,&sigact,0)) startupsyscallerr("cannot setup sigterm handler");
  if (sigaction(SIGINT,&sigact,0)) startupsyscallerr("cannot setup sigint handler");

  if (becomedaemon) {
    nfd= open("/dev/null",O_RDWR);
    if (nfd<0) startupsyscallerr("cannot open /dev/null");
    sid= setsid(); if (sid == -1) startupsyscallerr("cannot create new session");
    overlordpid= getpid();
    if (overlordpid == -1) startupsyscallerr("getpid after detach");
    if (dup2(nfd,0)<0 || dup2(nfd,1)<0)
      startupsyscallerr("cannot dup /dev/null for stdin/out");
    r= kill(parentpid,SIGUSR1); if (r) startupsyscallerr("send SIGUSR1 to detach");
    r= dup2(nfd,2);
    if (r<0) { syslog(LOG_CRIT,"cannot dup /dev/null for stderr: %m"); exit(5); }
    close(nfd);
  }

  syslog(LOG_NOTICE,"started");

  for (;;) {
    if (needcheck) {
      while (checkpid==-1) {
	checkpid= fork();
	if (checkpid!=-1) {
	  if (!checkpid) docheck(needcheck);
	  break;
	} else if (errno==EAGAIN) {
	  syslog(LOG_ERR,"fork for check - will wait and retry: %m");
	  alarm(USERVD_CHECKFORK_RETRY);
	  break;
	} else if (errno!=EINTR) {
	  syslog(LOG_CRIT,"fork for check: %m"); exit(5);
	}
      }
      needcheck= 0;
    }
    csocklen= sizeof(csockname);
    blocksignals(SIG_UNBLOCK);
    sfd= accept(mfd,(struct sockaddr*)&csockname,&csocklen);
    e= errno;
    blocksignals(SIG_BLOCK);
    if (sfd<0) {
      errno= e;
      if (errno == EINTR) continue;
      if (errno == ENOMEM || errno == EPROTO || errno == EAGAIN) {
        syslog(LOG_ERR,"unable to accept connection: %m");
	continue;
      } else {
	syslog(LOG_CRIT,"unable to accept new connections: %m");
	exit(5);
      }
    }
    child= nondebug_fork();
    if (child == (pid_t)-1) {
      syslog(LOG_ERR,"unable to fork server: %m");
      close(sfd);
      continue;
    }
    if (!child) {
      close(mfd);
      closelog();
      blocksignals(SIG_UNBLOCK);
      servicerequest(sfd);
    }
    close(sfd);
  }
}
