/*-
 * Copyright (c) 2004 Free (Olivier Beyssac)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <grp.h>
#include <pwd.h>
#include <sys/time.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <syslog.h>
#include "options.h"
#include "parse_args.h"
#include "parse_config.h"
#include "net.h"
#include "cmd.h"
#include "iptree.h"
#include "daemon.h"
#include "netlist.h"
#include "utils.h"


#define BANNER "220 "PROGNAME" "PROGRAM_VERSION"\r\n"

#ifdef FD_SETSIZE
#define MAX_CLIENTS (FD_SETSIZE >= 160 ? 150 : FD_SETSIZE - 10)
#else
#define MAX_CLIENTS (150)
#endif /* FD_SETSIZE */

struct options opt; /* global runtime options/parameters */
iptree ipt;         /* global iptree */
int ncli = 0;       /* clients connected */
int listen_sd;      /* listening socket */


/* Information about clients */
struct {
  int sd;
  struct sockaddr_in addr;
  int reply;
  int mode;
  time_t last;
} client[MAX_CLIENTS];


#define close_client(i)  \
  do {                   \
    close(client[i].sd); \
    client[i].sd = -1;   \
    client[i].last = 0;  \
    ncli--;              \
  } while(0)


static void dump_data(void)
{
  iptree_dump2files(ipt);

  opt.last_dump = time(NULL);
}


/* Exit cleanly */
static void exit_signal_handler(const int signo)
{
  int i;
  
  syslog(LOG_INFO, "terminated by signal %d, closing all connections", signo);
  close(listen_sd);
  for (i = 0; i < MAX_CLIENTS; i++)
    if (client[i].sd != -1) {
      if (opt.log_level >= 3)
	syslog(LOG_INFO, "closing slot %d", i);
      close(client[i].sd);
    }

  /* Dump data to disk before exiting */
  dump_data();

  if (opt.daemon && unlink(opt.pid_filename) < 0)
    syslog(LOG_ERR, "unlink: %s", strerror(errno));
  
  exit(EXIT_SUCCESS);
}


/* Show some stats when we catch USR1 */
static void usr1_signal_handler(const int signo)
{
  struct rusage u;
  time_t t = time(NULL);
  
  syslog(LOG_INFO, "caught signal %d", signo);
  syslog(LOG_INFO, "start time: %lu (age: %lu secs)",
	 (unsigned long)opt.start_time, (unsigned long)t - opt.start_time);
  syslog(LOG_INFO, "submissions: %lu", opt.submissions);
  syslog(LOG_INFO, "insertions in BL: %lu", opt.insertqueries);
  syslog(LOG_INFO, "notifies: %lu", opt.notifies);
  syslog(LOG_INFO, "decrements: %lu", opt.decrqueries);
  syslog(LOG_INFO, "positive BL queries: %lu", opt.positive_blqueries);
  syslog(LOG_INFO, "total BL queries: %lu", opt.blqueries);
  syslog(LOG_INFO, "bad requests: %lu", opt.bad_requests);
  syslog(LOG_INFO, "denied requests: %lu", opt.denied_requests);
  syslog(LOG_INFO, "clients: %d/%d", ncli, MAX_CLIENTS);
  iptree_show_stats(ipt, t);
  if (getrusage(RUSAGE_SELF, &u) == -1) {
    syslog(LOG_ERR, "getrusage(): %s", strerror(errno));
    return;
  }
  syslog(LOG_INFO, "utime=%li.%lis - stime=%li.%lis",
	 u.ru_utime.tv_sec, u.ru_utime.tv_usec,
	 u.ru_stime.tv_sec, u.ru_stime.tv_usec);
  syslog(LOG_INFO, "maxrss=%li", u.ru_maxrss);
  syslog(LOG_INFO, "ixrss=%li - idrss=%li", u.ru_ixrss, u.ru_idrss);
  syslog(LOG_INFO, "minflt=%li - majflt=%li", u.ru_minflt, u.ru_majflt);
  syslog(LOG_INFO, "nswap=%li", u.ru_nswap);
  syslog(LOG_INFO, "inblock=%li - oublock=%li", u.ru_inblock, u.ru_oublock);
  syslog(LOG_INFO, "msgsnd=%li - msgrcv=%li", u.ru_msgsnd, u.ru_msgrcv);
  syslog(LOG_INFO, "nsignals=%li", u.ru_nsignals);
  syslog(LOG_INFO, "nvcsw=%lu - nivcsw=%lu", u.ru_nvcsw, u.ru_nivcsw);

#ifdef SHOW_PROC_STATUS
  sprintf(filename, "/proc/%d/status", getpid());
  if ((f = fopen(filename, "r")) == NULL) {
    syslog(LOG_ERR, "fopen: %s", strerror(errno));
    return;
  }
  while (fgets(buf, sizeof(buf), f))
    syslog(LOG_INFO, buf);
  fclose(f);
#endif /* PROC_STATUS_EXISTS */
}


/* Dump data to disk when catching USR2 */
static void usr2_signal_handler(const int signo)
{
  syslog(LOG_INFO, "caught signal %d", signo);
  dump_data();
}


/* Catch terminated childs */
static void chld_signal_handler(const int signo)
{
  pid_t pid;
  int status;

  status = signo; /* don't like gcc warnings :) */

  while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
    if (opt.log_level >= 3)
      syslog(LOG_INFO, "child %d terminated", pid);
}


static void usage(void)
{
  printf("Usage: %s [-v] [-p port] [-n] [-l loglevel]\n", opt.progname);
  printf("\nOptions:\n");
  printf("  -h         this help\n");
  printf("  -v         output version information and exit\n");
  printf("  -f file    use a specific configuration file\n");
  printf("             (default: %s)\n", opt.config_file);
  printf("  -n         do not fork as a daemon\n");
  printf("  -a ip      address to bind to (default: %s)\n",
	 opt.listening_ip ? opt.listening_ip : "any");
  printf("  -p port    port to listen to (default: %s)\n", opt.port);
  printf("  -t number  min. time interval before blacklisting (default: %lu secs)\n", (unsigned long)opt.interval);
  printf("  -m number  maximum submissions in time interval (default %d)\n",
	 opt.max_req);
  printf("  -i number  IP list size (default: %d)\n", opt.list_size);
  printf("  -b number  blacklist size (default: %d)\n", opt.blacklist_size);
  printf("  -e number  blacklist expiration (default: %lu secs)\n",
	 (unsigned long)opt.blacklist_expiration);
  printf("  -l number  log level (0-3, default: %d)\n", opt.log_level);
  printf("  -u user    user to run as\n");
  printf("  -g group   group to run as\n");
  printf("  -T number  client timeout (default %lu secs)\n",
	 (unsigned long)opt.client_timeout);
  printf("  -I file    which file to dump IP list do\n");
  printf("             (default: `%s')\n", opt.wl_filename);
  printf("  -B file    which file to dump blacklist to\n");
  printf("             (default: `%s')\n", opt.bl_filename);
  printf("  -P file    PID filename (default: `%s')\n", opt.pid_filename);
  printf("  -A file    ACL filename (default: `%s')\n", opt.acl_filename);
  printf("  -W file    whitelist filename (default: `%s')\n",
	 opt.whitelist_filename);
}


/* Return a free slot for a client.
   FIXME: should be linear and keep maxsd more optimal for select() below */
static int get_client_slot(const int max)
{
  int i, j;

  if (max == MAX_CLIENTS)
    return -1;
  
  for (i = 0, j = MAX_CLIENTS - 1;
       i < MAX_CLIENTS && j != i && client[i].sd != -1 && client[j].sd != -1;
       i++, j--);

  if (i < MAX_CLIENTS && client[i].sd == -1)
    return i;
  if (j > 0 && client[j].sd == -1)
    return j;

  return -1;
}


/* Reload ACL and whitelist files if they changed and if they contain
   valid information */
static void reload_files(void)
{
  struct stat sb;
  netlist nl;
#if 0
  struct options sv_opt;
  
  /* Untested code. */
  /* Config */
  if (stat(opt.config_file, &sb) == 0) {
    if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.config_mtime) {
      /* File modified */
      opt.config_mtime = sb.st_mtime;

      if (opt.log_level >= 2)
	syslog(LOG_INFO, "%s changed on disk, reloading", opt.config_file);
      memcpy(&sv_opt, &opt, sizeof(struct options));
      if (!parse_config()) {
	syslog(LOG_ERR, "error while parsing %s, restoring old configuration",
	       opt.config_file);
	memcpy(&opt, &sv_opt, sizeof(struct options));
      }
    }
  } else if (errno == ENOENT && opt.config_mtime) {
    /* File removed */
    if (opt.log_level >= 2)
      syslog(LOG_INFO, "%s disappeared", opt.config_file);
    opt.config_mtime = 0;
  }
#endif
  
  /* ACL */
  if (stat(opt.acl_filename, &sb) == 0) {
    if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.acl_mtime) {
      /* File modified */
      opt.acl_mtime = sb.st_mtime;

      if (opt.log_level >= 2)
	syslog(LOG_INFO, "%s changed on disk, reloading", opt.acl_filename);
      if ((nl = netlist_init()) == NULL) {
	syslog(LOG_ERR, "netlist_init() failed for ACL, keeping old configuration");
      }
      else if (netlist_acl_getfromfile(nl, opt.acl_filename) == -1) {
	syslog(LOG_ERR, "error while parsing %s, keeping old configuration",
	       opt.acl_filename);
	free(nl);
      } else {
	netlist_free(opt.acl);
	opt.acl = nl;
      }
    }
  } else if (errno == ENOENT && opt.acl_mtime) {
    /* File removed */
    if (opt.log_level >= 2)
      syslog(LOG_INFO, "%s disappeared, cleaning list", opt.acl_filename);
    netlist_free(opt.acl);
    opt.acl = netlist_init();
    opt.acl_mtime = 0;
  }
  
  /* Whitelist */
  if (stat(opt.whitelist_filename, &sb) == 0) {
    if (S_ISREG(sb.st_mode) && sb.st_mtime > opt.whitelist_mtime) {
      /* File modified */
      opt.whitelist_mtime = sb.st_mtime;
      
      if (opt.log_level >= 2)
	syslog(LOG_INFO, "%s changed on disk, reloading",
	       opt.whitelist_filename);
      if ((nl = netlist_init()) == NULL) {
	syslog(LOG_ERR, "netlist_init() failed for whitelist, keeping old configuration");
      }
      else if (netlist_whitelist_getfromfile(nl, opt.whitelist_filename) == -1) {
	syslog(LOG_ERR, "error while parsing %s, keeping old configuration",
	       opt.whitelist_filename);
	free(nl);
      } else {
	netlist_free(opt.whitelist);
	opt.whitelist = nl;
      }
    }
  } else if (errno == ENOENT && opt.whitelist_mtime) {
    /* File removed */
    if (opt.log_level >= 2)
      syslog(LOG_INFO, "%s disappeared, cleaning list",
	     opt.whitelist_filename);
    netlist_free(opt.whitelist);
    opt.whitelist = netlist_init();
    opt.whitelist_mtime = 0;
  }
}


/* Set effective user and group.  Exit with 1 on error */
static void set_eugid(void)
{
  int id;
  struct passwd *pw;
  struct group *gr;

  if (opt.gid != NULL) {
    if ((gr = getgrnam(opt.gid)) == NULL
	&& ((id = xstrtol(opt.gid)) < 0 || (gr = getgrgid(id)) == NULL)) {
      fprintf(stderr, "%s: Invalid group: %s\n", opt.progname, opt.gid);
      exit(EXIT_FAILURE);
    }
    if (setegid(gr->gr_gid) == -1) {
      perror("setegid");
      exit(EXIT_FAILURE);
    }
    if (setgroups(1, &gr->gr_gid) == -1) {
      perror("setgroups");
      exit(EXIT_FAILURE);
    }
  }
  
  if (opt.uid != NULL) {
    if ((pw = getpwnam(opt.uid)) == NULL
	&& ((id = xstrtol(opt.uid)) < 0 || (pw = getpwuid(id)) == NULL)) {
      fprintf(stderr, "%s: Invalid user: %s\n", opt.progname, opt.uid);
      exit(EXIT_FAILURE);
    }
    if (seteuid(pw->pw_uid) == -1) {
      perror("seteuid");
      exit(EXIT_FAILURE);
    }
  }
}


/* Fill the buffer with the reply code and human understandable string */
static void buildreply(const int code, char *buffer, const size_t maxsize)
{
  size_t len;
  
  switch (code) {
  case 200:
    snprintf(buffer, maxsize, "%d OK", code);
    break;
  case 421:
    snprintf(buffer, maxsize, "%d Blacklisted", code);
    break;
  case 500:
    snprintf(buffer, maxsize, "%d Syntax error", code);
    break;
  case 600:
    snprintf(buffer, maxsize, "%d Authorization failed", code);
    break;
  default:
    snprintf(buffer, maxsize, "%d", code);
  }

  len = strlen(buffer);
  
  if (len >= maxsize - 2)
    sprintf(buffer + len - 2, "\r\n");
  else
      sprintf(buffer + len, "\r\n");
  
  return;
}


extern int main(int argc, char **argv)
{
  int i, client_sd, maxsd, ret;
  struct sockaddr_in cliaddr;
  size_t csize = sizeof(cliaddr);
  ssize_t rd, wd;
  char buf[MAX_CMD_LEN+1];
  fd_set rset, svrset, wset, svwset;
  int slot, errors;
  struct sigaction sa;
  struct stat sb;
  struct timeval tv;
  
  openlog(PROGNAME, LOG_PID, LOG_DAEMON);

  xsigaction(sa, SIGINT, exit_signal_handler);
  xsigaction(sa, SIGTERM, exit_signal_handler);
  xsigaction(sa, SIGUSR1, usr1_signal_handler);
  xsigaction(sa, SIGUSR2, usr2_signal_handler);
  xsigaction(sa, SIGCHLD, chld_signal_handler);

  options_init(argv[0]);
  parse_args_for_config_file(argc, argv);
  if (!parse_config())
    exit(EXIT_FAILURE);

  if ((ret = parse_args(argc, argv)) == -1) {
    usage();
    exit(EXIT_FAILURE);
  } else if (ret == 0) {
    usage();
    exit(EXIT_SUCCESS);
  }

  if (netlist_acl_getfromfile(opt.acl, opt.acl_filename) == -1) {
    fprintf(stderr, "%s: Error while parsing %s\n",
            opt.progname, opt.acl_filename);
    exit(EXIT_FAILURE);
  }

  if (netlist_whitelist_getfromfile(opt.whitelist,
				    opt.whitelist_filename) == -1) {
    fprintf(stderr, "%s: Error while parsing %s\n",
            opt.progname, opt.whitelist_filename);
    exit(EXIT_FAILURE);
  }

  if (stat(opt.config_file, &sb) == 0 && S_ISREG(sb.st_mode))
    opt.config_mtime = sb.st_mtime;
  if (stat(opt.acl_filename, &sb) == 0 && S_ISREG(sb.st_mode))
    opt.acl_mtime = sb.st_mtime;
  if (stat(opt.whitelist_filename, &sb) == 0 && S_ISREG(sb.st_mode))
    opt.whitelist_mtime = sb.st_mtime;

  set_eugid();

  if (opt.log_level >= 3)
    options_log();

  opt.syslog = 1;
  if (opt.daemon)
    daemon_init();
  
  umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);

  if ((listen_sd = net_server_sock_create(opt.listening_ip, opt.port)) == -1)
    exit(EXIT_FAILURE);

  net_socket_set_nb(listen_sd);
 
  ipt = iptree_init();

  /* Initialize all client slots */
  for (i = 0; i < MAX_CLIENTS; i++) {
    client[i].sd = -1;
    client[i].reply = -1;
    client[i].last = 0;
  }
  
  FD_ZERO(&svrset);
  FD_SET(listen_sd, &svrset);
  FD_ZERO(&svwset);
  
  /* FIXME: should optimize maxsd count for select() */
  maxsd = MAX_CLIENTS;

  syslog(LOG_INFO, "listening on port %s", opt.port);
  
  errors = 0;
  
  for (;;) {
    int nsel;
    time_t t = time(NULL);

    if (t - opt.last_dump > opt.dump_delay)
      dump_data();

    /* Close idle connections */
    if (ncli)
      for (i = 0; i < MAX_CLIENTS; i++)
	if (client[i].sd != -1 && client[i].last
	    && t - client[i].last >= opt.client_timeout) {
	  if (opt.log_level >= 2)
	    syslog(LOG_INFO, "connection with %s closed (timeout)",
		   inet_ntoa(client[i].addr.sin_addr));
	  if (FD_ISSET(client[i].sd, &svrset))
	    FD_CLR(client[i].sd, &svrset);
	  if (FD_ISSET(client[i].sd, &svwset))
	    FD_CLR(client[i].sd, &svwset);
	  close_client(i);
	}
    
    /* Reload ACL and whitelist files if needed */
    reload_files();
    
    rset = svrset;
    wset = svwset;
    
    /* Wake up at least every 60 seconds to perform some cleanup */
    tv.tv_sec = 60;
    tv.tv_usec = 0;
    if ((nsel = select(maxsd + 1, &rset, &wset, NULL, &tv)) == -1
	&& errno != EINTR) {
      syslog(LOG_ERR, "error in select(): %s", strerror(errno));
      if (++errors >= 3) {
	syslog(LOG_ERR, "too many errors, aborting");
	close(listen_sd);
	exit(EXIT_FAILURE);
      }
      continue;
    }

    if (FD_ISSET(listen_sd, &rset)) {
      /* First look for incoming connections */
      nsel--;
      client_sd = accept(listen_sd, (struct sockaddr *)&cliaddr, &csize);
      if (client_sd < 0) {
	if (errno != EINTR && errno != EWOULDBLOCK)
	  syslog(LOG_ERR, "accept: %s (errno=%d)", strerror(errno), errno);
      } else {
	slot = get_client_slot(ncli);
	if (slot == -1)
	  close(client_sd);
	else {
	  int mode = netlist_getmode(opt.acl, ntohl(cliaddr.sin_addr.s_addr));

	  if (!mode) {
	    close(client_sd);
	    syslog(LOG_INFO, "closed unauthorized connection from %s",
		   inet_ntoa(cliaddr.sin_addr));
	  } else {
	    ncli++;
	    net_socket_set_nb(client_sd);
	    client[slot].sd = client_sd;
	    client[slot].addr.sin_addr = cliaddr.sin_addr;
	    client[slot].mode = mode;
	    client[slot].last = time(NULL);
	    if (opt.log_level == 2)
	      syslog(LOG_INFO, "connect from %s (%d/%d) [%d]",
		     inet_ntoa(cliaddr.sin_addr), ncli, MAX_CLIENTS, mode);

	    write(client_sd, BANNER, strlen(BANNER));
	    FD_SET(client_sd, &svrset);
	  } /* if (!mode) [...] else */
	} /* if (slot == -1) [...] else */
      } /* if (client_sd < 0) [...] else */
    } /* if (FD_ISSET(listen_sd, &rset) */
    
    /* Then have a look at open connections */
    for (i = 0; nsel > 0 && i < maxsd; i++) {
      if (client[i].sd != -1) {
	if (FD_ISSET(client[i].sd, &rset)) {
	  /* Something to read */
	  nsel--;
	  if ((rd = read(client[i].sd, &buf, MAX_CMD_LEN)) <= 0) {
	    if (rd < 0 && (errno == EINTR || errno == EWOULDBLOCK))
	      continue;
	    
	    if (rd < 0)
	      syslog(LOG_ERR, "read: %s", strerror(errno));
	    
	    FD_CLR(client[i].sd, &svrset);
	    close_client(i);
	  } else {
	    client[i].reply = read_cmd(buf, rd, inet_ntoa(cliaddr.sin_addr),
				       ipt, client[i].mode);
	    client[i].last = time(NULL);
	    FD_CLR(client[i].sd, &svrset);
	    FD_SET(client[i].sd, &svwset);
	  }
	} else if (FD_ISSET(client[i].sd, &wset)) {
	  /* Something to write */
	  nsel--;
	  buildreply(client[i].reply, buf, MAX_CMD_LEN + 1);
	  wd = write(client[i].sd, &buf, strlen(buf));
	  if (wd < 0 && (errno == EINTR || errno == EWOULDBLOCK))
	    continue;

	  if (wd < 0)
	    syslog(LOG_ERR, "write: %s", strerror(errno));
	  
	  FD_CLR(client[i].sd, &svwset);
	  close_client(i);

	  if (opt.log_level == 2)
	    syslog(LOG_INFO, "connection with %s closed (%d/%d)",
		   inet_ntoa(client[i].addr.sin_addr), ncli, MAX_CLIENTS);
	  
	} /* if (FD_ISSET(client[i].sd, &rset)) [...] else [...] */
      } /* if (client[i].sd != -1) */
    } /* for (i = 0; i < MAX_CLIENTS; i++) */
  } /* for (;;) */

  /* Can never be called but exit with a polite code */
  exit(EXIT_SUCCESS);
}
