/* ========================================================================
 * Copyright 2006 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * ========================================================================
 */

#define _GNU_SOURCE
#include <system.h>
#include <general.h>
#include <sys/stat.h>
#include <sys/socket.h>

#include "id_table.h"
#include "wp_uidmapper_lib.h"


/* Makefile should #define:
 * 
 * WP_UIDMAPPER_SOCKET
 */

unsigned long send_full(int fd,void *buf,unsigned long len,int flags) {
  unsigned long total,i;  
  for(total = 0; total < len; total += i) {
    i = send(fd,(char*)buf + total, len - total,flags);
    if(i == -1) return -1;
  }
  return total;
}

static char *socketname = WP_UIDMAPPER_SOCKET;
static int   socketname_remove = 0;

void socketname_cleanup(void) {
  unlink(socketname);
}

void quit_handler(int signal) {
  exit(signal);
}

int main(int argc, char *argv[]) {
  extern char *optarg;
  extern int optind, opterr, optopt;

  int debug,log_opt;
  mode_t sockmode;
  
  id_table_range *range;
  id_table table;
  struct sockaddr_un sun,rsun;
  struct sigaction sa;
  int is_err,i,ssock,uid;
  unsigned int kbuf[WP_KEY_LEN];
  char rbuf[WP_BUF_SIZE],cbuf[WP_BUF_SIZE],rcmd;
  struct msghdr rmh,smh;
  struct iovec riov[3],siov[1];
#ifndef DGRAM_MODE
  int csock,rsun_len;
#endif
  struct ucred cred;
  
  /*
   * process command line arguments
   */

  debug = 0;
  log_opt = 0;
  sockmode = 0600;

  for(is_err = 0; !is_err && ((i = getopt(argc,argv,"dlrm:s:u:")) != -1); ) {
    switch(i) {
    case 'd': debug++; break;
    case 'l': log_opt |= LOG_PERROR; break;
    case 'm': sockmode = strtol(optarg,NULL,0); break;
    case 'r': socketname_remove = 1; break;
    case 's': socketname = optarg; break;
    case 'u': umask(strtol(optarg,NULL,0)); break;
    case '?': is_err = 1; break;
    }
  }
  if((optind + 1) == argc) {
    range = id_table_range_new(argv[optind]);
    if(!range) is_err = 1;
  } else {
    is_err = 1;
  }
  if(is_err) {
    fprintf(stderr,"Usage: uidmapper [-d] [-l] [-r] [-m mode] [-s socketname] [-u umask] uidranges\n");
    exit(1);
  }

  /*
   * main initialization
   */

  openlog(argv[0],log_opt,LOG_MAIL);
  
  if(id_table_init(&table,range)) {
    syslog(LOG_ERR,"could not initialize tables: %s\n",strerror(errno));
    exit(1);
  }
  id_table_range_delete(range);
  
  sa.sa_handler = quit_handler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = 0;
  sigaction(SIGHUP,&sa,0);
  sigaction(SIGINT,&sa,0);
  sigaction(SIGQUIT,&sa,0);  
  sigaction(SIGTERM,&sa,0);

  if(socketname_remove)
    socketname_cleanup();

  /*
   * open socket
   */
  
#ifdef DGRAM_MODE
  ssock = socket(AF_UNIX,SOCK_DGRAM,0);
#else
  ssock = socket(AF_UNIX,SOCK_STREAM,0);
#endif
  if(ssock < 0) {
    syslog(LOG_ERR,"%s: socket: %s\n",socketname,strerror(errno));
    exit(1);
  }
  /* sun.sun_len = strlen(socketname) + 1; */
  sun.sun_family = AF_UNIX;
  strcpy(sun.sun_path,socketname);  
  if(bind(ssock,(struct sockaddr*)&sun,sizeof(sun))) {
    syslog(LOG_ERR,"%s: bind: %s\n",socketname,strerror(errno));
    exit(1);
  }
  atexit(socketname_cleanup);
  fchmod(ssock,sockmode);

#ifndef DGRAM_MODE
  if(listen(ssock,8)) {
    syslog(LOG_ERR,"%s: listen: %s\n",socketname,strerror(errno));
    exit(1);
  }
#endif

#ifdef DGRAM_MODE 
  if(debug >= 1) syslog(LOG_INFO,"SOCK_DGRAM socket opened");
#else
  if(debug >= 1) syslog(LOG_INFO,"SOCK_STREAM socket opened");
#endif

  /*
   * accept commands
   */
  
  riov[0].iov_base = &rcmd;
  riov[0].iov_len = 1;
  riov[1].iov_base = kbuf;
  riov[1].iov_len = WP_KEY_LEN * sizeof(unsigned int);
  riov[2].iov_base = rbuf;
  riov[2].iov_len = WP_BUF_SIZE - 1;
  rmh.msg_name = &rsun;
  rmh.msg_namelen = sizeof(rsun);
  rmh.msg_iov = riov;  
  rmh.msg_iovlen = 3;
  rmh.msg_control = cbuf;
  rmh.msg_controllen = riov[0].iov_len + riov[1].iov_len + riov[2].iov_len;
  rmh.msg_flags = 0;

  /* siov[0].iov_base */
  /* siov[0].iov_len */
  smh.msg_name = NULL;
  smh.msg_namelen = 0;
  smh.msg_iov = siov;
  /* smh.msg_iovlen */
  smh.msg_control = NULL;
  smh.msg_controllen = 0;
  smh.msg_flags = 0;

#ifndef DGRAM_MODE
  csock = -1;
#endif

  while(1) {
#ifdef DGRAM_MODE
    i = recvmsg(ssock,&rmh,0);
#else
    if(csock >= 0) close(csock);
    rsun.sun_family = AF_UNIX;
    rsun_len = sizeof(rsun);
    csock = accept(ssock,(struct sockaddr*)&rsun,&rsun_len);
    if(csock == -1) {
      syslog(LOG_ERR,"accept: %s\n",strerror(errno));
      break;
    }
    if(debug >= 2) {
      i = sizeof(cred);
      if(getsockopt(csock,SOL_SOCKET,SO_PEERCRED,&cred,&i) == -1) {
	syslog(LOG_INFO,"getsockopt(SO_PEERCRED) failed: %s",strerror(errno));
      } else {
	syslog(LOG_INFO,"connection from pid=%i uid=%i gid=%i\n",
	       cred.pid,cred.uid,cred.gid);
      }
    }
    i = recvmsg(csock,&rmh,0);
#endif

    if(i == -1) {
      syslog(LOG_ERR,"recvmsg: %s\n",strerror(errno));
      break;
    }
    if(debug >= 2) 
      syslog(LOG_INFO,"recvd datagram [size=%i] from %s [size=%i]\n",
	     i,rsun.sun_path,rmh.msg_namelen);
    
    /* check that datagram is well formed */
    if(i < 1) {
      syslog(LOG_WARNING,"recv: recvd datagram that is too small\n");
      continue;
    }
    i--;
#ifdef DGRAM_MODE
    smh.msg_name = rmh.msg_name;
    smh.msg_namelen = rmh.msg_namelen;
#endif
    
    if(rcmd == 'u') {
      if(!i) {
	syslog(LOG_WARNING,"recv: recvd 'u' datagram with no payload\n");
	continue;
      }
      rbuf[i - (WP_KEY_LEN * sizeof(unsigned int))] = 0;
      uid = id_table_create_id(&table,rbuf,kbuf);
      if(debug >= 1) syslog(LOG_INFO,"request uid(%s) = %i\n",rbuf,uid);
      if(uid == -1) {
	char sbuf[2 * WP_BUF_SIZE],*sep = strerror(errno);
	sprintf(sbuf,"id_table_create_id(%s,[",rbuf);
	for(i = 0; i < WP_KEY_LEN; i++)
	  sprintf(sbuf + strlen(sbuf), "%u,", kbuf[i]);

	sprintf(sbuf + strlen(sbuf) - 1, "]): %s\n",sep);
	syslog(LOG_ERR,sbuf);
	/* break; hobble along rather than die */
      }
      
      siov[0].iov_base = &uid;
      siov[0].iov_len = sizeof(uid);
      smh.msg_iovlen = 1;
#ifdef DGRAM_MODE
      if(sendmsg(ssock,&smh,0) == -1)
#else
      if(sendmsg(csock,&smh,0) == -1)
#endif
	syslog(LOG_WARNING,"sendmsg: %s\n",strerror(errno));

    } else if(rcmd == 'n') {
      if(i != sizeof(uid)) {
	syslog(LOG_WARNING,"recv: recvd 'n' datagram with invalid payload\n");
	continue;
      }
      
      memcpy(&uid,kbuf,sizeof(uid));
      siov[0].iov_base = id_table_get_name(&table,uid);
      if(debug >= 1)
	syslog(LOG_INFO,"request name(%d) = %s\n",uid,siov[0].iov_base);
      
      if(siov[0].iov_base) {
	siov[0].iov_len = strlen(siov[0].iov_base);
	smh.msg_iovlen = 1;
      } else {
	smh.msg_iovlen = 0;
      } 
#ifdef DGRAM_MODE
      if(sendmsg(ssock,&smh,0) == -1) 
#else
      if(sendmsg(csock,&smh,0) == -1)
#endif
	syslog(LOG_WARNING,"sendmsg: %s\n",strerror(errno));
      
    } else if(rcmd == 'c') {
      if(debug >= 1) syslog(LOG_INFO,"request clear()");
      if(id_table_remove_stale(&table)) {
	syslog(LOG_WARNING,"id_table_remove_stale: %s\n",strerror(errno));
	break;
      }	
    }
#ifdef HONOR_QUIT
    else if(rcmd == 'q') {
      if(debug >= 1) syslog(LOG_INFO,"quit requested");
      break;
    }
#endif
    else {
      syslog(LOG_WARNING,"invalid request received");
    }
  }
#ifndef DGRAM_MODE
  if(csock >= 0) close(csock);
#endif

  /*
   * clean up (never really get this far)
   */

  id_table_destroy(&table);
  close(ssock);
  exit(0);
  return 0;
}
