/*
 * sprofile.c
 *
 */

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <gdbm.h>
#include <thr.h>
#include <alloca.h>
#include <string.h>
#include <macros.h>
#include <syslog.h>
#include <profile.h>	/* from krb5/src/util/profile */
#include <com_err.h>
#include <gss.h>

/* globals set at startup */

static int		VERBOSE;
static char		INI[1024];
static int		FACILITY;
static int		PRIORITY;
static char		PROGNAME[256];
static char		HOSTNAME[256];
static char		SERVICE[1024];
static char		MECHANISM[1024];
static time_t		STARTTIME;
static int		PID;
static int		PORT;
static GDBM_FILE	dbf;
static MUTEX		dbflock;
static char		DBNAME[1024];
static gss_cred_id_t	SERVER_CREDS;     


/*******************************************************************************
 *
 * Debugging code
 *
 ******************************************************************************/

static void logger(int priority, char *msg, ...)
{
  va_list args;

  va_start(args, msg);

  if (priority <= PRIORITY)
    vsyslog(priority, msg, args);

  if (VERBOSE) {
    time_t now = time(NULL);
    char buf[0x10000], *p=buf;
    p += sprintf(p, "%.*s %s sprofile[%d]: ", 15, ctime(&now)+4, HOSTNAME, PID);
    p += vsprintf(p, msg, args);
    p += sprintf(p, "\n");
    fprintf(stderr, buf);
    fflush(stderr);
  }

  va_end(args);
}

#define DIE() __die(__FILE__,__LINE__)
static int __die(char *file, int line)
{
  printf("Died in \"%s\" on line %d\n", file, line);
  perror(NULL);
  exit(1);
}



/*******************************************************************************
 *
 * startup utility functions
 *
 ******************************************************************************/

static void setup(int argc, char *argv[], char *env[])
{
  int			retval;
  char			c, *errmsg;

  profile_t		profile;
  char			**values;
  const char		*names[3];

  /* must be root */
  if (geteuid()) {
    printf("Must be root!\n");
    exit(2);
  }

  /* detach from shell process */
  detach();

  /* remember our program name */
  strcpy(PROGNAME, argv[0]);

  /* get our pid */
  PID = getpid();

  /* remember our startup time */
  STARTTIME = time(NULL);

  /* set defaults */
  VERBOSE = 0;
  strcpy(INI, "/etc/ecat.conf");

  /* get options */
  while((c = getopt(argc, argv, "i:v")) != EOF)
    switch (c) {
    case 'i':
      strcpy(INI, optarg);
      break;
    case 'v':
      ++VERBOSE;
      break;
    default:
      goto Usage;
    }
  if (optind != argc) goto Usage;

  /* read sprofile section of INI file */
  initialize_prof_error_table();

  names[0] = INI;
  names[1] = NULL;
  if (retval = profile_init(names, &profile)) goto INIerror;

  names[0] = "sprofile";
  names[1] = "hostname";
  names[2] = NULL;
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  strcpy(HOSTNAME, *values);
  free(*values);
  free(values);

  names[1] = "port";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  PORT = atoi(*values);
  free(*values);
  free(values);

  names[1] = "service";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  strcpy(SERVICE, *values);
  free(*values);
  free(values);

  names[1] = "mechanism";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  strcpy(MECHANISM, *values);
  free(*values);
  free(values);

  names[1] = "facility";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  FACILITY = (16+atoi(*values))<<3;
  free(*values);
  free(values);

  names[1] = "priority";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  PRIORITY = atoi(*values);
  free(*values);
  free(values);

  names[1] = "database";
  if (retval = profile_get_values(profile, names, &values)) goto INIerror;
  strcpy(DBNAME, *values);
  free(*values);
  free(values);

  profile_release(profile);


  /* start logging */
  openlog("sprofiled", LOG_PID|LOG_CONS|LOG_NDELAY, FACILITY);

  /* open the database */
  if (!(dbf = gdbm_open(DBNAME, 0, GDBM_READER, 0, NULL))) DIE();

  /* GSS initialization */
  errmsg = server_acquire_creds(SERVICE, MECHANISM, GSS_C_ACCEPT, &SERVER_CREDS);

  if (errmsg) {
    logger(LOG_ALERT, "%s: %s", SERVICE, errmsg);
    goto Usage;
  }

  /* return */
  return;

  /* print usage and die */
 Usage:
  fprintf(stderr, "usage: %s [Options]\n", PROGNAME);
  fprintf(stderr, "\nOptions:\n");
  fprintf(stderr, "\t-i %%s\tprofile [\"%s\"]\n", "/etc/ecat.conf");
  fprintf(stderr, "\t-v\tverbose\n");
  exit(2);

  /* error with INI file */
 INIerror:
  com_err(argv[0], retval, "while initializing profile.");
  exit(3);
}


/*******************************************************************************
 *
 * fetch()
 *
 * threadsafe lookup of key in database
 *
 ******************************************************************************/

static
datum fetch(datum key)
{
  datum	data;

  LOCK(&dbflock);

  data = gdbm_fetch(dbf, key);

  UNLOCK(&dbflock);

  return data;
}



/*******************************************************************************
 *
 * query()
 *
 * Start routine for an query thread.
 *
 ******************************************************************************/

void* query(void* vsock)
{
  int			sock = (int)vsock;
  char			client_name[1024];
  OM_uint32		maj_stat, min_stat, ignore;
  int			conf_state;
  datum			key;
  datum			data;

  /* initialize things we must free */
  gss_buffer_desc	cleartext = GSS_C_EMPTY_BUFFER;
  gss_buffer_desc	encrypted = GSS_C_EMPTY_BUFFER;
  gss_ctx_id_t		context = GSS_C_NO_CONTEXT;
  char			*errmsg = NULL;

  /* make our socket non-blocking */
  if (fcntl(sock, F_SETFL, O_NDELAY|fcntl(sock, F_GETFL))) DIE();

  /* Establish a context with the client */
  errmsg = server_establish_context(sock, SERVER_CREDS, &context, &cleartext);
  if (errmsg) {
    logger(LOG_ERR, "%s", errmsg);
    goto Bail;
  }

  /* note the client name */
  strcpy(client_name, (char*)cleartext.value);
  (void)gss_release_buffer(&ignore, &cleartext);

  /* Receive the sealed message token */
  if (recv_token(sock, &encrypted) < 0) {
    logger(LOG_ERR, "%s", errmsg = unix_errmsg("receiving order", errno));
    goto Bail;
  }

  /* Unseal the message token */
  maj_stat = gss_unseal(&min_stat, context, &encrypted, &cleartext, NULL, NULL);
  if (maj_stat != GSS_S_COMPLETE) {
    logger(LOG_ERR, "%s", errmsg = gss_errmsg("unsealing message", maj_stat, min_stat));
    goto Bail;
  }
  (void)gss_release_buffer(&ignore, &encrypted);


  /* lookup the request */
  key.dptr = cleartext.value;
  key.dsize = cleartext.length;
  logger(LOG_DEBUG, "thread %d: read \"%.*s\"", SELF(), key.dsize, key.dptr);

  data = fetch(key);
  (void)gss_release_buffer(&ignore, &cleartext);

  /* seal the reply */
  if (data.dptr) {
    cleartext.length = data.dsize;
    cleartext.value = data.dptr;
  } else {
    cleartext.length = 1;
    cleartext.value = "";
  }
  maj_stat = gss_seal(&min_stat, context, 1, GSS_C_QOP_DEFAULT,
		      &cleartext, &conf_state, &encrypted);
  if (data.dptr) free(data.dptr);
  if (maj_stat != GSS_S_COMPLETE) {
    logger(LOG_ERR, "%s", errmsg = gss_errmsg("sealing message", maj_stat, min_stat));
    goto Bail;
  }

  /* Send the reply message to the client */
  if (send_token(sock, &encrypted) < 0) {
    logger(LOG_ERR, "%s", errmsg = unix_errmsg("sending reply", errno));
    goto Bail;
  }

  logger(LOG_DEBUG, "thread %d: wrote reply of %d bytes", SELF(), encrypted.length);

  /* Free memory, close sock and return */
 Bail:
  (void)gss_release_buffer(&ignore, &encrypted);
  if (context != GSS_C_NO_CONTEXT) {
    (void)gss_delete_sec_context(&ignore, &context, &encrypted);
    /* N.b.: output token is not sent, since client exits.... */
    (void)gss_release_buffer(&ignore, &encrypted);
  }
  free(errmsg);
  close(sock);
  return NULL;
}


/*******************************************************************************
 *
 * main()
 *
 * Start routine for the main thread.
 *
 ******************************************************************************/

main(int argc, char *argv[], char *env[])
{
  int			sock, nsock, n;
  struct sockaddr_in	me, client;
  fd_set		fdset;
  sigset_t		sigmask;
  struct hostent	*h;

  /* boilerplate setup */
  setup(argc, argv, env);

  /* create a non-blocking socket */
  sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
  if (sock < 0) DIE();
  if (fcntl(sock, F_SETFL, O_NDELAY|fcntl(sock, F_GETFL))) DIE();

  /* allow reuse of port */
  n = 1;
  if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&n, sizeof(n))) DIE();

  /* bind our name to socket */
  memset(&me, '\0', sizeof(me));
  me.sin_family = AF_INET;
  if (!(h = gethostbyname(HOSTNAME))) DIE();
  me.sin_port = PORT;
  me.sin_addr.s_addr = htonl(((struct in_addr *)(h->h_addr_list[0]))->s_addr);
  if (bind(sock, (struct sockaddr *)&me, sizeof(me)) < 0) DIE();

  /* say hello */
  logger(LOG_INFO, "Secure profile daemon starting up....\n"
	 "Date\t\t%s"
	 "Hostname\t%s\n"
	 "Port\t\t%d\n"
	 "Service\t\t%s\n"
	 "Mechanism\t%s\n"
	 "GDBM file\t%s\n"
	 "Process id\t%d\n"
	 "Main tid\t%d\n",
	 ctime(&STARTTIME), HOSTNAME, PORT, SERVICE, MECHANISM, DBNAME, PID, SELF());


  /* start the signal catcher */
  sigemptyset(&sigmask);
  sigaddset(&sigmask, SIGHUP);
  sigaddset(&sigmask, SIGINT);
  sigaddset(&sigmask, SIGQUIT);
  sigaddset(&sigmask, SIGTERM);
  if (spawnCatcher(&sigmask)) DIE();

  /* specify backlog limit for incoming connnections */
  if (listen(sock, SOMAXCONN) < 0) DIE();

  /* go into infinite dispatch loop */
  logger(LOG_DEBUG, "Main thread (tid %d) entering dispatch loop", SELF());

  for(FD_ZERO(&fdset);;) {
    FD_SET(sock, &fdset);
    if (select(sock+1, &fdset, NULL, NULL, NULL) != 1) continue;

    /* accept the connection */
    n = sizeof(client);
    nsock = accept(sock, (struct sockaddr *)&client, &n);
    logger(LOG_DEBUG, "thread %d: accepted connection from %s:%d on socket %d",
	   SELF(), inet_ntoa(client.sin_addr), client.sin_port, nsock);

    /* create the thread to handle connection */
    if (spawnThread(query, nsock)) DIE();
  }
}
