/* 
#  This is version 0.3 of absent
#
#               Copyright (c) 1999 by AT&T.
#  Permission to use, copy, and modify this software without fee is
#  hereby granted, provided that this entire notice is included in all
#  copies of any software which is or includes a copy or modification
#  of this software and in all copies of the supporting documentation
#  for such software.
#
#  SOME PARTS OF ABSENT MAY BE RESTRICTED UNDER UNITED STATES EXPORT
#  REGULATIONS.
#
#  THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
#  IMPLIED WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE
#  ANY REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
#  MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
#  PURPOSE.
*/
#include "absentd.h"

static absentd_cf *cf;
int sem;
int nproc;

/* just a little utility. */
char *is_pushweb(struct sockaddr_in *sin) {
  /* just buzz through the list of addresses and find out if the received
     sockaddr matches any of them. we could conceivably do a lookup
     every time the connection arrives; but at this point, i think
     relying on the DNS once we're running doesn't actually help. */     
  char **a=cf->pwbaddrs, *b=(char *)&(sin->sin_addr);

  for(;*a && bcmp(*a, b, sizeof(struct in_addr)); a++);
  return *a;
}

/* signal handlers */
void die_gracefully(int sig) {
  /*  this is the exit handler for the client mainline; the
      important thing here is that we clean up the semaphore before
      we're out. */

  signal(SIGCLD, SIG_IGN);
  signal(SIGUSR1, SIG_IGN);
  alarm(0);
  switch(sig) {
  case SIGALRM:
    syslog(LOG_INFO, "Control connection ping failed.  Exiting.");
    break;
  case SIGUSR1:
    syslog(LOG_INFO, "Control connection given up.  Exiting.");
    break;
  case SIGTERM:
    syslog(LOG_INFO, "Killed.");
    break;
  default:
    syslog(LOG_INFO, "caught unexpected signal %d.  shutting down.");
  }
  while(waitpid(-1, 0, 0) > 0);
  /* sigh.  sysV semaphores.  this is the only safe place to catch
     this, but we're not going to get here if we get SIGKILL.
     we need to eliminate our use of sysV semaphores.  maybe before
     we release. */
  /* linux doesn't have a varargs semctl */
#ifdef __linux__
  semctl(sem, 0, IPC_RMID, (union semun)0);
#else  
  semctl(sem, 0, IPC_RMID);
#endif /* __linux__ */
  exit(0);
}

void just_die(int sig) {
  /* anything that won't be waiting around for its children to exit
     can use this */
  syslog(LOG_INFO, "going down on signal %d", sig);
  exit(0);
}

void child_exited(int sig) {
  waitpid(-1, 0, WNOHANG);
  nproc--;
  DEBUGLOG1("cleaned up child, nproc now = %d", nproc);
  signal(SIGCLD, child_exited);
}

/* random I/O routines */
int client_accept(int client, int ctrl) {
    /* accept a new connection on the DWT service port and send an OPEN
     control message to pushweb. */
  struct sockaddr_in sin;
  int sz, s, so_no=0;
  struct hostent *he=NULL;
  absent_cmsg m;
  struct linger li;

  sz=sizeof(struct sockaddr_in);
  if((s=accept(client, (struct sockaddr *)&sin, &sz)) > 0) {
    /* look everyone, we have GUESTS! */
    if(cf->paranoid &&
       !(he=gethostbyaddr((char *)&(sin.sin_addr), sizeof(sin.sin_addr),
			   sin.sin_family))) {
      syslog(LOG_WARNING,"failed to reverse-map client %s (%s). closing.",
	     strerror(h_errno), inet_ntoa(sin.sin_addr));
      close(s);
      return -1;
    }
    syslog(LOG_INFO,"DWT connect from %s on socket %d",
	   (he ? he->h_name : inet_ntoa(sin.sin_addr)), s);
    if (setsockopt(s, IPPROTO_TCP,TCP_NODELAY,(char*)&so_no,sizeof(int)) < 0)
      syslog(LOG_NOTICE,"couldn't disable nagle on %d: %s",s,strerror(errno));
    li.l_onoff=1;
    li.l_linger=ABSENTD_LINGER;
    if(setsockopt(s, SOL_SOCKET, SO_LINGER, (char *)&li, sizeof(li)) < 0)
      syslog(LOG_INFO, "Couldn't lingerify %d: %s", s, strerror(errno));
    m.msg=ABSENT_COPEN;
    m.id=htons(s);
    m.tstamp=htonl(time(NULL));
    bcopy(&sin, &m.sa_client, sizeof(struct sockaddr_in));
    hmac_md5((char *)&m.id,
	     sizeof(m.id) + sizeof(m.tstamp) + sizeof(struct sockaddr_in),
	     cf->key,sizeof(cf->key), m.mac);
    DEBUGLOG1("writing cmsg for %d", m.id);
    if((sz=write(ctrl, &m, sizeof(m))) < sizeof(m)) {
      /* we can't do much about this failure from here.  we'll have to
	 rely on our caller to take care of the problem. */
      syslog(LOG_ERR, "COPEN write failed (%s)", strerror(errno));
      close (s);
      return -1;
    }
    DEBUGLOG2("wrote %d bytes of cmsg for new client %d", sz, m.id);
    return s;
  }
  else {
    syslog(LOG_INFO, "client accept failed: %s", strerror(errno));
    return -1;
  }
}

int proxy_accept(int proxy) {
  /* get a new connection off proxy and do some initial setup and
     verification on it.  */
  struct sockaddr_in sin;
  int sz, s, so_no=0;

  DEBUGLOG1("I/O on pushweb server fd %d", proxy);
  sz=sizeof(struct sockaddr_in);
  if((s=accept(proxy,(struct sockaddr *)&sin, &sz)) >= 0) {
    if(is_pushweb(&sin)) {
      if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char*)&so_no,
		     sizeof(int)) < 0) {
	syslog(LOG_NOTICE, "Couldn't disable nagle on %d: %s",
	       s, strerror(errno));
      }
      DEBUGLOG1("new pushweb dfd %d.  waiting for cmsg.",s);
      return s;
    }
    else {
      syslog(LOG_ERR, "Control connection attempt from %s rejected",
	    inet_ntoa(sin.sin_addr));
      return -1;
    }
  }
  else {
    syslog(LOG_NOTICE, "proxy_accept failed: %s", strerror(errno));
    return -1;
  }
}

/* manage init files */
void absentd_cfdefaults(absentd_cf *theCF) {
  theCF->control_port=ABSENTD_DEFCPORT;
  theCF->pushweb_port=ABSENTD_DEFPPORT;
  theCF->pingfreq=ABSENTD_DEFPFREQ;
  theCF->pingout=ABSENTD_DEFPOUT;
  theCF->ctimeout=ABSENTD_DEFCOUT;
  theCF->paranoid=ABSENTD_DEFPNOID;
  theCF->maxconn=ABSENTD_DEFMCON;
  theCF->bsize=ABSENTD_DEFBSIZ;
  theCF->user=ABSENTD_DEFUSER;
  theCF->group=ABSENTD_DEFGROUP;
  theCF->nextcf=NULL;
  theCF->absent=NULL;
  theCF->pushweb=NULL;
  theCF->clienthost=NULL;
}

void absentd_finishdefaults(absentd_cf *theCF) {
  /* once we've constructed the base configuration, go through and fill
     in hostnames where they weren't specified.  it's probably overkill
     to be so frugal about MAXHOSTNAMELEN bytes by using the same string
     for everybody, but owell. */
  char *hn=NULL;
  struct in_addr **a;
  int i=0;
  struct hostent *he;

  while(theCF) {
    if(!theCF->pushweb) {
      /* if they're bozos, we can't do anything about it. */
      fprintf(stderr, "No remote host specified for port %d\n",
	      theCF->data_port);
      exit(19);
    }
    /* obviously, we don't want to go looking up pushweb every time
       we use it.  we cache it here. */
    if(!(he=gethostbyname(theCF->pushweb))) {
      fprintf(stderr, "hostname lookup failed for proxy host %s\n",
	      theCF->pushweb);
      exit(20);
    }
    /* all we care about is the address list. */
    for(a=(struct in_addr **)he->h_addr_list; *a && i < MAXADDRS; a++) {
      if(!(theCF->pwbaddrs[i]=(char *)malloc(he->h_length))) {
	syslog(LOG_ERR, "(1) malloc failed: %s", strerror(errno));
	exit(21);
      }
      bcopy((char *)*a, theCF->pwbaddrs[i++], he->h_length);
    }
    if(!(theCF->pwbaddrs[i]=(char *)malloc(he->h_length))) {
      syslog(LOG_ERR, "(2) malloc failed: %s", strerror(errno));
      exit(22);
    }
    bzero(theCF->pwbaddrs[i], sizeof(theCF->pwbaddrs[i]));
    if(!theCF->absent) {
      if(!hn) {
	hn=(char *)malloc(MAXHOSTNAMELEN + 1);
	gethostname(hn, MAXHOSTNAMELEN);
      }
      theCF->absent=hn;
    }
    if(!theCF->clienthost)
      theCF->clienthost=theCF->absent;
    theCF=theCF->nextcf;
  }
}

void absentd_readcf(char *fn) {
  /* trivial configuration file reader.  makes no efforts to be safe
     or sane.  don't run absentd setuid, and don't let randoms write
     your configuration files. */
  char *cfnames[]={"keyfile", "controlport", "pushwebport",
		   "absenthost", "pushwebhost", "clienthost",
		   "pingfreq", "pingtimeout", "clienttimeout",
		   "paranoid", "maxclients", "buffsize",
		   "user", "group", NULL};
  enum { CFKEYFILE, CFCONTROLPORT, CFPUSHWEBPORT,
	 CFABSENTHOST, CFPUSHWEBHOST, CFCLIENTHOST,
	 CFPINGFREQ, CFPINGOUT, CFCTIMEOUT, CFPARANOID,
	 CFMAXCONN, CFBUFSIZ, CFUSER, CFGROUP} cftypes;
  char sstart[]="<port";
  char ssend[]="</port>";
  char cfline[132], *cfname, *cfval;
  FILE *fp;
  struct passwd *pw;
  struct group *gr;
  absentd_cf *curcf=cf;
  int incf=0;

  if(!(fp=fopen(fn, "r"))) {
    perror(fn);
    exit(2);
  }
  while(fgets(cfline, 132, fp)) {
    *(index(cfline, '\n'))='\0';
    cfname=cfline;
    while(*cfname && isspace(*cfname)) cfname++;
    if(*cfname == '\0' || *cfname == '#') continue;
    
    if(*cfname == '<') {
      if(!strncasecmp(cfname, sstart, 5)) {
	if(!incf)
	  incf=1;
	if(!curcf) {
	  curcf=(absentd_cf *)malloc(sizeof(absentd_cf));
	  cf=curcf;
	}
	else {
	  curcf->nextcf=(absentd_cf *)malloc(sizeof(absentd_cf));
	  curcf=curcf->nextcf;
	}
	for(cfval=cfname; *cfval && !isspace(*cfval); cfval++);
	if(!*cfval) {
	  fprintf(stderr, "Section start with no port number at %s\n", cfline);
	  exit(3);
	}
	curcf->data_port=atoi(cfval);
	absentd_cfdefaults(curcf);
      }
      else if(!strncasecmp(cfname, ssend, 7))
	incf=0;
      continue;
    }
    else if(!incf) {
      fprintf(stderr, "Configuration line %s outside <PORT> section\n",cfline);
      exit(4);
    }
    cfval=index(cfline, '=');
    if(!cfval) {
      fprintf(stderr, "Error parsing %s\n", cfline);
      exit(5);
    }
    cfval++;
    while(*cfval && isspace(*cfval)) cfval++;
    if(*cfval == '\0') {
      fprintf(stderr, "Error parsing %s (cfval missing?)\n", cfline);
      exit(6);
    }
    for((int)cftypes=0;
	cfnames[(int)cftypes] &&
	  strncasecmp(cfnames[(int)cftypes], cfname,
		      strlen(cfnames[(int)cftypes]));
	(int)cftypes++);
    switch(cftypes) {
    case CFKEYFILE:
      read_key(NULL, curcf->key, cfval);
      break;
    case CFCONTROLPORT:
      curcf->control_port=atoi(cfval);
      break;
    case CFPUSHWEBPORT:
      curcf->pushweb_port=atoi(cfval);
      break;
    case CFABSENTHOST:
      curcf->absent=strdup(cfval);
      break;
    case CFPUSHWEBHOST:
      curcf->pushweb=strdup(cfval);
      break;
    case CFCLIENTHOST:
      curcf->clienthost=strdup(cfval);
      break;
    case CFPINGFREQ:
      curcf->pingfreq=atoi(cfval);
      break;
    case CFPINGOUT:
      curcf->pingout=atoi(cfval);
      break;
    case CFCTIMEOUT:
      curcf->ctimeout=atoi(cfval);
      break;
    case CFPARANOID:
      curcf->paranoid=atoi(cfval);
      break;
    case CFMAXCONN:
      curcf->maxconn=atoi(cfval);
      break;
    case CFBUFSIZ:
      curcf->bsize=atoi(cfval);
      break;
    case CFUSER:
      pw=getpwnam(cfval);
      if(!pw) {
	fprintf(stderr, "user %s not found (in %s)\n", cfval, cfline);
	exit(7);
      }
      curcf->user=pw->pw_uid;
      break;
    case CFGROUP:
      gr=getgrnam(cfval);
      if(!gr) {
	fprintf(stderr, "group %s not found (in %s)\n", cfval, cfline);
	exit(7);
      }
      curcf->user=gr->gr_gid;
      break;
    default:
      printf("unrecognized configuration keyword in %s\n",cfline);
    }
  }
  fclose(fp);
  absentd_finishdefaults(cf);
}

void absentd_netInit(int *ctrl, int *proxy, int *client) {
  /* do the network-related stuff we can do once and forget about --
     calls to socket(), listen(), stuff like that. */
  struct sockaddr_in sin;
  struct hostent *he;
  int so_yes;

  /* control socket... */
  if((*ctrl=socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    syslog(LOG_ERR, "Control socket: %s", strerror(errno));
    exit(8);
  }
  so_yes=1;
  setsockopt(*ctrl, SOL_SOCKET, SO_REUSEADDR, (char *)&so_yes, sizeof(int));
  /* allow sane behaviour on multihomed machines */
  if(!(he=gethostbyname(cf->absent))) {
    syslog(LOG_ERR, "%s: %s", cf->absent, strerror(h_errno));
    exit(9);
  }
  sin.sin_family=he->h_addrtype;
  sin.sin_port=htons(cf->control_port);
  bcopy((char *)he->h_addr, (char *)&(sin.sin_addr), he->h_length);
  if(bind(*ctrl, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
    syslog(LOG_ERR, "Control socket bind: %s", strerror(errno));
    exit(10);
  }
  if(listen(*ctrl, 16) < 0) {
    syslog(LOG_ERR,"Control socket listen: %s", strerror(errno));
    exit(11);
  }
  if((*proxy=socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    syslog(LOG_ERR,"pushweb socket: %s", strerror(errno));
    exit(12);
    close(*proxy);
  }
  so_yes=1;
  setsockopt(*proxy, SOL_SOCKET, SO_REUSEADDR, (char *)&so_yes, sizeof(int));
  sin.sin_port=htons(cf->pushweb_port);
  if(bind(*proxy, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
    syslog(LOG_ERR,"pushweb socket bind: %s", strerror(errno));
    exit(13);
  }
  if(listen(*proxy, 16) < 0) {
    syslog(LOG_ERR,"pushweb socket listen: %s", strerror(errno));
    close(*proxy);
    exit(14);
  }
  /* finally, the client port */
  if((*client=socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    syslog(LOG_ERR, "Client socket: %s", strerror(errno));
    exit(15);
  }
  so_yes=1;
  setsockopt(*client, SOL_SOCKET, SO_REUSEADDR, (char *)&so_yes, sizeof(int));
  if(!(he=gethostbyname(cf->clienthost))) {
    syslog(LOG_ERR, "%s: %s", cf->clienthost, strerror(h_errno));
    exit(16);
  }
  sin.sin_family=he->h_addrtype;
  sin.sin_port=htons(cf->data_port);
  bcopy((char *)he->h_addr, (char *)&(sin.sin_addr), he->h_length);
  if(bind(*client, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
    syslog(LOG_ERR, "Client socket bind: %s", strerror(errno));
    exit(17);
  }
  if(listen(*client, 16) < 0) {
    syslog(LOG_ERR,"Client socket listen: %s", strerror(errno));
    exit(18);
  }
}

/* variations on main().  absentd has a rather byzantine process structure,
   and each of the following routines is a mainline for one particular kind
   of process.  each service hosted on a particular absentd runs server_main;
   children are forked from this as new control connections arrive.  these
   fork client_main for individual dwt connections.  */
void client_main(int server, int sem, fd_set *dwtfds, int p) {
  /* this is a complex and potentially ugly situation.  we're particularly
     vulnerable to replay here; the tricky critical section below is 
     intended to deal with this.  once we've snarfed a file descriptor
     to work on, we write back to our parent process so's it can close
     its copy of the socket.  after that, we're pretty much on our own. */
  absent_cmsg m;
  int sz, hit=1, nfds, coffset=0, poffset=0, csize=0, psize=0, did_read=0;
  time_t timeout;
  fd_set readfds, writefds;
  struct sembuf lock={0, -1, SEM_UNDO}, unlock={0, 1, SEM_UNDO};
  unsigned char *cbuf, *pbuf;
  struct timeval stimeout;
  
  signal(SIGINT,  just_die);
  signal(SIGQUIT, just_die);
  signal(SIGTERM, just_die);
  signal(SIGUSR1, just_die);
  /* XXX FIXME XXX */
  signal(SIGALRM, just_die);
  alarm(cf->pingout);
  if((sz=read(server, &m, sizeof(m))) < sizeof(m)) {
    syslog(LOG_ERR, "pushweb read failed: %s", strerror(errno));
    exit(1);
  }
  alarm(0);

  DEBUGLOG1("read %d bytes of hello message", sz);
  m.id=ntohs(m.id);
  DEBUGLOG1("proxy fd is %d... going after dwtfds semaphore", m.id);
  signal(SIGUSR1, SIG_IGN);
  if(semop(sem, &lock, 1) < 0) {
    /* most likely, we're just supposed to die.  theoretically, we could
       handle the connection before we go away since we don't really need
       any more help from the parent process at this point, but hey,
       if it's really important, they'll call back. */
    syslog(LOG_INFO, "failed to acquire DWTFDS semaphore (%s)",
	   strerror(errno));
    exit(1);
  }
  DEBUGLOG0("got dwtfds semaphore.");
  /* ++ critical ++ */
  if(FD_ISSET(m.id, dwtfds))
    FD_CLR(m.id, dwtfds);
  else
    hit=0;
  /* -- critical -- */
  if(semop(sem, &unlock, 1) < 0) {
    /* this is the more serious case.  we really want to exit to force
       the undo on the semaphore if it's just brokenness on our part. */
    syslog(LOG_ALERT, "failed to release dwtfds semaphore (%s)",
	   strerror(errno));
    exit(1);
  }
  DEBUGLOG0("released dwtfds semaphore.");
  shmdt((void *)dwtfds);
  if(!hit) {
    syslog(LOG_INFO, "Bogus or late CHELO for %d.  Ignored.", m.id);
    exit(1);
  }
  DEBUGLOG1("handling %d...", m.id);
  write(p, &(m.id), sizeof(m.id));
  close(p);
  DEBUGLOG2("plumbed dwt %d to pushweb %d.", m.id, server);
  nfds=(m.id > server ? m.id : server)+1;
  FD_ZERO(&writefds);
  cbuf=(unsigned char *)malloc(cf->bsize);
  pbuf=(unsigned char *)malloc(cf->bsize);
  timeout=time(NULL)+cf->ctimeout;
  stimeout.tv_usec=0;
  while(1) {
    if((stimeout.tv_sec=timeout - time(NULL)) <= 0) {
      /* an initial pass at preventing the slow-drip DoS.
	 there's still a variant on that here; the client could
	 repeatedly request the server cert and effectively lock
	 down the connection forever.  the RIGHT solution to this
	 is to have _two_ timeouts: the second should be a longer global
	 timeout for the entire session.  */
      syslog(LOG_INFO, "Timer expired for %d<->%d.  Closing.", m.id,server);
      shutdown(m.id, SHUT_RDWR);
      exit(0);
    }
    FD_ZERO(&readfds);
    FD_SET(m.id, &readfds);
    FD_SET(server, &readfds);
    DEBUGLOG1("above select, nfds=%d", nfds);
    if(select(nfds, &readfds, &writefds, NULL, &stimeout) >= 0) {
      /* there's an implicit policy decision here: after the select timeout,
	 you get one more chance to generate data from pushweb.  if this
	 doesn't result in a timeout reset, the connection will die. */
      DEBUGLOG0("below select");
      /* OVER the undertow... */
      if(FD_ISSET(m.id, &readfds) && (psize < cf->bsize)) {
	/* case 1: read ready from client */
	sz=read(m.id, (unsigned char *)((int)pbuf + psize),cf->bsize - psize);
	DEBUGLOG4("1: fd=%d, read=%d, sz=%d, off=%d",m.id,sz,psize,poffset);
	if(sz > 0) {
	  FD_SET(server, &writefds);
	  did_read=1;
	  psize += sz;
	}
	else {
	  DEBUGLOG2("1: closing %d<->%d", m.id, server);
	  /* we don't want to just call close() here, or the socket will
	     stick around until all the other simultaneous children die.
	     in practice this isn't a really big deal, except if somebody's
	     downloading something biiiiiiiiiiiiiiiiiiiig.  no sense making
	     everyone suffer.  shutdown() forces the tcp connection down. */
	  shutdown(m.id, SHUT_RDWR);
	  exit(0);
	}
      }
      /* UNDER the overpass... */
      if(FD_ISSET(server, &readfds) && csize < cf->bsize) {
	/* 2: read ready from pushweb */
	sz=read(server,(unsigned char *)((int)cbuf+csize),cf->bsize - csize);
	DEBUGLOG4("2: fd=%d, read=%d, sz=%d, off=%d",server,sz,csize,coffset);
	if(sz > 0) {
	  /* reiterating, the only time we reset the timeout is when we
	     actually see data from pushweb. */
	  timeout=time(NULL)+cf->ctimeout;
	  FD_SET(m.id, &writefds);
	  did_read=1;
	  csize += sz;
	}
	else {
	  DEBUGLOG2("2: closing %d<->%d", m.id, server);
	  shutdown(m.id, SHUT_RDWR);
	  exit(0);
	}
      }
      /* around the FUTURE... */
      if(FD_ISSET(m.id, &writefds) && (csize - coffset > 0)) {
	/* 3. write ready to dwt */
	sz=write(m.id,(unsigned char *)((int)cbuf+coffset),csize - coffset);
	DEBUGLOG4("3: fd=%d, write=%d, off=%d, sz=%d",m.id,sz,coffset,csize);
	if(sz > 0)
	  coffset += sz;
	else {
	  if(!did_read) {
	    /* the idea behind the did_read flag is that we want to
	       _attempt_ a write immediately after a read; however,
	       the write might fail.  we ONLY want to treat write
	       failures as fatal for the connection if select() has
	       claimed the write was safe (i.e., if we've selected
	       but not read).  it may take us a while to discover
	       this. */
	    DEBUGLOG2("3: closing %d<->%d", m.id, server);
	    shutdown(m.id, SHUT_RDWR);
	    exit(0);
	  }
	  else {
	    DEBUGLOG1("3: write failed on unselected fd", m.id);
	    FD_SET(m.id, &writefds);
	  }
	}
      }
      if(csize == coffset) {
	csize=(coffset=0);
	FD_CLR(m.id, &writefds);
      }
      /* and BEYOND REPAIR!! */
      /*        - ZtP        */
      if(FD_ISSET(server, &writefds) && (psize - poffset > 0)) {
	/*4. write ready to pushweb */
	sz=write(server,(unsigned char *)((int)pbuf+poffset),psize-poffset);
	DEBUGLOG4("4:fd=%d,write=%d, off=%d, sz=%d",server,sz,poffset,psize);
	if(sz > 0)
	  poffset += sz;
	else {
	  if(!did_read) {
	    /* see above: they probably closed the connection on us.  punt. */
	    DEBUGLOG2("2d: closing %d<->%d", m.id, server);
	    shutdown(m.id, SHUT_RDWR);
	    exit(0);
	  }
	  else {
	    DEBUGLOG1("2d: write failed on unselected fd %d", server);
	    FD_SET(server, &writefds);
	  }
	} 
      }
      if(psize == poffset) {
	psize=(poffset=0);
	FD_CLR(server, &writefds);
      }
    }
    else {
      /* this is the select() < 0 case.  0 means select timeout, which we
	 handle at the top of the loop. */
      syslog(LOG_ERR, "Select failed: %s", strerror(errno));
      shutdown(m.id, SHUT_RDWR);
      exit(0);
    }
  }
}
  
void control_main(int srv, int proxy, int ctrl) {
  /* handle control connections.  this is the first bit of nonblocking
     ugliness.  we can't block on any of the open file descriptors
     at this point (or anywhere after we've read the initial control
     message) because each process is doing too many jobs.  sigh.  there
     doesn't seem to be a good solution for this.
     */
  absent_cmsg m;
  int s, sz, r, c, nfds, shm, cl, p[2], t;
  fd_set readfds, *dwtfds;
  struct timeval timeout;
  struct sockaddr_in sin;
  union semun setv;
  struct sembuf lock={0, -1, SEM_UNDO}, unlock={0, 1, SEM_UNDO};

  sem=-1;
  signal(SIGINT,  die_gracefully);
  signal(SIGQUIT, die_gracefully);
  signal(SIGTERM, die_gracefully);
  signal(SIGALRM, die_gracefully);
  
  alarm(cf->pingout);
  if((sz=read(ctrl, &m, sizeof(m))) < sizeof(m)) {
    syslog(LOG_ERR, "Control channel read failed: %s", strerror(errno));
    exit(1);
  }
  alarm(0);
  if(!check_MAC((char *)&(m.tstamp), sizeof(m.tstamp),
		cf->key, sizeof(cf->key), m.mac)) {
    syslog(LOG_NOTICE, "Control channel authentication failed.");
    exit(1);
  }
  if(!check_time(ntohl(m.tstamp))) {
    syslog(LOG_NOTICE, "Control channel time-check failed.");
    exit(1);
  }
  /* otherwise, we've got a control channel.   hopefully kill off any other
     control channel processors, and start working on new client connections.
     this is kinda ugly.  if any connections are waiting around on the old
     control connection, they'll just lose.  but the assumption here is
     that if this happens, the old control connection's dead or terminally
     ill anyway, so, ummm, whatever. */
  syslog(LOG_INFO, "taking over control channel.  new channel %d", ctrl);
  kill(0, SIGUSR1);
  signal(SIGUSR1, die_gracefully);
  signal(SIGCLD, child_exited);
  /* this shared memory segment is used to communicate the state of
     our client connections.  we point to an fd set where, if bit n
     is set, the client on fd n is still waiting for a server callback.
     save your letters and phone calls, i KNOW it's a hack.  the
     semaphore we create protects the shared memory segment.

     The primary reason for the ridiculous complexity here has to do with
     preventing a rather icky little DoS: an attacker could quickly
     replay the COPEN_R message to tie up an arbitrary number of client
     processes and spraying the client messages all over the place.
     we could handle this by processing the rendezvous in this process,
     but i'd like to keep this simple.  it's a tricky call, really, and
     this may not be the best possible design (surprise)
  */
  shm=shmget(IPC_PRIVATE, sizeof(fd_set), 00600);
  sem=semget(IPC_PRIVATE, 1, 00600);
  DEBUGLOG2("Shared memory segment %d, semaphore %d created.", shm, sem);
  pipe(p);
  nproc=0;
  setv.val=1;
  semctl(sem, 0, SETVAL, setv);
  dwtfds=(fd_set *)shmat(shm, 0, 0);
  shmctl(shm, IPC_RMID, NULL);
  FD_ZERO(dwtfds);

  timeout.tv_usec=0;
  timeout.tv_sec=cf->pingfreq;
  nfds=(srv < proxy ? proxy : srv);
  nfds=(nfds < ctrl ? ctrl : nfds);
  nfds=(nfds < p[0] ? p[0] : nfds) + 1;
  fcntl(p[0], F_SETFL, (fcntl(p[0], F_GETFL) | O_NONBLOCK));
  /* ok.  done with all the random setup.  let's get to work. */
  while(1) {
    FD_ZERO(&readfds);
    FD_SET(srv, &readfds);
    FD_SET(proxy, &readfds);
    FD_SET(ctrl, &readfds);
    FD_SET(p[0], &readfds);
    DEBUGLOG1("above select.  nfds=%d", nfds);
    r=select(nfds, &readfds, NULL, NULL, &timeout);
    DEBUGLOG4("Below select.  r=%d, u=%d, s=%d, errno=%d",
	      r, timeout.tv_usec, timeout.tv_sec, errno);
    if(r > 0) {
      if(FD_ISSET(p[0], &readfds)) {
	/* the pipe is a one-way channel to child processes.  they use
	   it to inform us which file descriptor they intend to handle.
	   it's important that this be first.  child processes are doing
	   blocking writes on it. */
	if(read(p[0], &cl, sizeof(int)) == sizeof(int)) {
	  DEBUGLOG1("child is handling %d.", cl);
	  close(cl);
	}
	else
	  syslog(LOG_INFO, "pipe ready but read failed: %s", strerror(errno));
      }
      if(FD_ISSET(srv, &readfds) && (nproc < cf->maxconn)) {
	/* this is somewhat ugly.  we have to hold the incoming connection
	   until we get the return call from absent.  i wish we could just
	   fork here, but with only one proxy port in accept(), we can't.
	   
	   fixme: have this clear srv from readfds and have the reaper
	   function reset it. otherwise we'll loop really fast here as
	   select unblocks uselessly. */
	DEBUGLOG0("new client.  attempting to accept it.");
	sz=sizeof(sin);
	if((c=client_accept(srv,ctrl)) >= 0) {
	  DEBUGLOG0("requesting parent lock");
	  if(semop(sem, &lock, 1) < 0) {
	    syslog(LOG_ERR, "semaphore decrement faied (%s), exiting.",
		   strerror(errno));
	    die_gracefully(SIGUSR1);
	  }
	  signal(SIGUSR1, SIG_IGN);
	  DEBUGLOG1("parent lock, setting %d", c);
	  /* ++ critical ++ */
	  FD_SET(c, dwtfds);
	  /* -- critical -- */
	  if(semop(sem, &unlock, 1) < 0) {
	    syslog(LOG_ERR, "semaphore increment faied (%s), exiting.",
		   strerror(errno));
	    die_gracefully(SIGUSR1);
	  }
	  signal(SIGUSR1, die_gracefully);
	  DEBUGLOG0("parent out of critical section");
	}
      }
      if(FD_ISSET(proxy, &readfds)) {
	/* match up the incoming connection with a waiting client,
	   then fork off a child to do the work of being a proxy. */
	DEBUGLOG0("proxy accept");
	sz=sizeof(sin);
	if((s=proxy_accept(proxy)) >= 0) {
	  switch(r=fork()) {
	  case 0:
	    close(proxy);
	    close(srv);
	    close(p[0]);
	    /* closing the remaining sockets will have to happen later. */
	    client_main(s, sem, dwtfds, p[1]);  /* never returns */
	    break;
	  case -1:
	    syslog(LOG_ERR, "client fork failed: %s", strerror(errno));
	  default:
	    nproc++;
	    close(s);
	  }
	}
      }
      if(FD_ISSET(ctrl, &readfds)) {
	/* should be an unusual case.  it's a ping or ping-response,
	   or maybe the control connection died. */
	DEBUGLOG0("read-ready on control channel.  ping, maybe?");
	if((sz=read(ctrl, &m, sizeof(m))) > 0)
	  /* fixme: check if < sizeof(cmsg) */
	  switch(m.msg) {
	  case ABSENT_PING:
	    m.msg=ABSENT_PONG;
	    m.tstamp=htonl(time(NULL));
	    hmac_md5((char *)&(m.tstamp), sizeof(m.tstamp),
		     cf->key, sizeof(cf->key), m.mac);
	    /* fixme: check mac and close control if wrong to avoid DoS */
	    sz=write(ctrl, &m, sizeof(m));
	    break;
	  case ABSENT_PONG:
	    t=alarm(0);
	    DEBUGLOG3("pong.   tstamp=%ld, rtt=%d, approx skew=%d",
		      m.tstamp, cf->pingout-t, time(NULL) - m.tstamp - (t/2));
	    break;
	  default:
	    syslog(LOG_NOTICE, "bogus cmsg %d", m.msg);
	  }
	else {
	  /* well, hrm.  we need to get rid of the control channel and
	     accept a new one.  i'm not sure how i feel about this, but
	     i guess this is the only way to go, sigh.  */
	  syslog(LOG_NOTICE, "control seems to be losing.  Closing it.");
	  close(srv);
	  close(proxy);
	  close(ctrl);
	  kill(0, SIGUSR1);
	}
      }
    }
    else {
      /* the select probably timed out.  send a ping. */
      if(r==0) {
	DEBUGLOG0("select timed out.  ping.");
	m.msg=ABSENT_PING;
	m.tstamp=htonl(time(NULL));
	hmac_md5((char *)&(m.tstamp), sizeof(m.tstamp),
		 cf->key, sizeof(cf->key), m.mac);
	sz=write(ctrl, &m, sizeof(m));
	DEBUGLOG1("ping sent, size=%d", sz);
	alarm(cf->pingout);
      }
      else
	DEBUGLOG2("select failed: %d (%s)", errno, strerror(errno));
    }
  }
  /* NOTREACHED */
}

void server_main() {
  /* accept new control connections */
  int f, s, sz, control, proxy, client;
  struct sockaddr_in sin;

  absentd_netInit(&control, &proxy, &client);
  /* later, we'll want to be able to send SIGUSR1 to the process group */
  setsid();
  setgid(cf->group);
  setuid(cf->user);
  while(1) {
    sz=sizeof(struct sockaddr_in);
    DEBUGLOG3("going into control accept with fds co=%d cl=%d pr=%d",
	      control, client, proxy);
    if((s=accept(control, (struct sockaddr *)&sin, &sz)) >= 0) {
      f=fork();
      if(f < 0) {
	syslog(LOG_ERR, "Control connection: couldn't fork (%s)",
	       strerror(errno));
	exit(1);
      }
      else if(!f) {
	/* never returns */
	close(control);
	control_main(client, proxy, s);
      }
      else
	close(s);
    }
    else {
      syslog(LOG_ERR, "control accept failed (!?): %s", strerror(errno));
      exit(1);
    }
  }
}

int main(int argc, char **argv) {
  pid_t pid;
  char *logident;

  absentd_readcf(argc < 2 ? ABSENTD_CFILE : argv[1]);
  /* the 80 is utterly gratuitous here.  this string must be long enough
     to hold ABSENTD_IDENT, the text " (port ", and the length of the
     longest ascii representation of an int, plus one. */
  logident=(char *)malloc(strlen(ABSENTD_IDENT) + 80);
  while(cf) {
    pid=fork();
    if(pid == -1) {
      fprintf(stderr, "Couldn't fork: %s.  Buy a bigger process table.\n",
	      strerror(errno));
      exit(1);
    }
    else if(!pid) {
      sprintf(logident, "%s (port %d)", ABSENTD_IDENT, cf->data_port);
      openlog(logident, ABSENTD_LOGOPT, ABSENTD_LOGFAC);
      fclose(stdin);
      fclose(stdout);
      fclose(stderr);
      signal(SIGINT,  just_die);
      signal(SIGQUIT, just_die);
      signal(SIGTERM, just_die);
      /* sigusr1 and sigcld should only be caught by things below
	 server_main() in the process tree */
      signal(SIGUSR1, SIG_IGN);
      signal(SIGCLD, SIG_IGN);
      server_main();
    }
    printf("Absentd started on port %d, pid %ld\n", cf->data_port, (long)pid);
    cf=cf->nextcf;
  }
  exit(0);
}
