/*****************************************************************************

  POPular -- A POP3 server and proxy for large mail systems

  $Id: pproxy.c,v 1.58 2003/11/24 19:23:30 sqrt Exp $

  http://www.remote.org/jochen/mail/popular/

******************************************************************************

  Copyright (C) 1999-2003  Jochen Topf <jochen@remote.org>

  This program 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 2 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 this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA

*****************************************************************************/

#if HAVE_CONFIG_H
# include <config.h>
#endif

#include "popular.h"
#include "pproxy.h"
#include "daemon.h"
#include "ctrl.h"
#include "ringd.h"


/* pointer to the memory mapped file used for config, status and stats */
struct shmem_proxy *sp;

/* configuration variables */
struct pproxyconfig conf;

/* file descriptor of log file */
static int logfd;

/* number of the last session slot that was allocated */
static int current_session;

/* file descriptor of state file */
static int statusfd;

/* first capability in linked list */
struct capa *first_capa = NULL;

/* first pdm module in linked list */
struct pdm_module *first_pdm_module = NULL;

/* pointer to pointer of last pdm module in linked list */
struct pdm_module **last_pdm_module = &first_pdm_module;

int debug;

int got_shutdown = 0;

static int delayed_shutdown = 0;


/*****************************************************************************

  free_session()

  (This is O(n), would be nice to have a better data structure to make
  this faster. But do profiling first... Maybe the child should return
  its session number as exit code?)

*****************************************************************************/
void
free_session(pid_t pid)
{
  int i;
  for (i=0; i < conf.maxsession; i++) {
    if (sp->session[i].pid == pid) {
      sp->stat.bytesin  += sp->session[i].bytesin;
      sp->stat.bytesout += sp->session[i].bytesout;
      if (sp->session[i].mailcheck_retry >= 0) {
        sp->stat.mailcheck_time += sp->session[i].mailcheck_time;
        sp->stat.mailcheck_retry[sp->session[i].mailcheck_retry]++;
      }
      memset(&sp->session[i], 0, sizeof(struct proxy_session));
      sp->used_sessions--;
      return;
    }
  }
  /* XLOG-DOC:SOS:007c:no_session_found
   * The pproxy parent process got a process id from wait() that he doesn't
   * know about, i.e. that shouldn't be one of his children. This should
   * never happen and probably means that there is some serious data
   * corruption or a bug in the program. pproxy will immediately exit. */
  xlog_printf(xlog_sos, 0x007c, "no_session_found pid=%d", pid);
  exit(RCODE_OK);
}


/*****************************************************************************

  pproxy_shutdown()

*****************************************************************************/
void
pproxy_shutdown(int which)
{
  if (which & SHUTDOWN_CHILDREN) {
    int i;
    for (i=0; i < conf.maxsession; i++) {
      if (sp->session[i].state != sstFree) kill(sp->session[i].pid, SIGTERM);
    }
    /* XLOG-DOC:INF:012c:shutdown_children
     * The pproxy server is shutting down all its children. */
    xlog_printf(xlog_inf, 0x012c, "shutdown_children");
  }

  if (which & SHUTDOWN_DELAYED) {
    flush_virt_serv(vsstOffline);
    delayed_shutdown = 1;
    /* XLOG-DOC:INF:012e:shutdown_delayed
     * The pproxy server closed all listening ports. After all children have
     * died, it will close down. */
    xlog_printf(xlog_inf, 0x012e, "shutdown_delayed");
  }

  if (which & SHUTDOWN_SERVER) {
    flush_virt_serv(vsstOffline);
    unlink_in_rundir(conf.rundir, PPROXY_STATE_TEMPL, PPROXY_STATE_FILE);
    unlink_in_rundir(conf.rundir, PPROXY_SOCK_TEMPL, PPROXY_SOCK_FILE);
    unlink_in_rundir(conf.rundir, PPROXY_PID_TEMPL, PPROXY_PID_FILE);

    /* XLOG-DOC:INF:007d:shutdown_server
     * The pproxy server is shut down. */
    xlog_printf(xlog_inf, 0x007d, "shutdown_server");
    exit(RCODE_OK);
  }
}


/*****************************************************************************

  connect_to_ringd()

*****************************************************************************/
int
connect_to_ringd()
{
  int local_socket;
  char socket_name[MAXBUF];

  snprintf(socket_name, sizeof(socket_name)-1, "%s/%s.sock.%d", RUN_DIR, PPROXY_PRG_NAME, getpid());

  local_socket = uds_socket(socket_name, RINGD_SOCK_UMASK);
  if (local_socket < 0) {
    /* XLOG-DOC:SOS:0181:sock_open_failed
     * Pproxy tried to open a unix domain socket to talk to the ringd and
     * this failed */
    xlog_printf(xlog_sos, 0x0181, "sock_open_failed errno=%d errmsg='%s'", errno, strerror(errno));
    return -1;
  }
  if (uds_connect(local_socket, RINGD_SOCK_FILE) < 0) {
    /* XLOG-DOC:SOS:0182:sock_connect_failed
     * Pproxy tried to connect to the unix domain socket of the ringd and
     * this failed */
    xlog_printf(xlog_sos, 0x0182, "sock_connect_failed errno=%d errmsg='%s'", errno, strerror(errno));
    return -1;
  }

  return local_socket;
}


/*****************************************************************************

  close_connection_to_ringd()

*****************************************************************************/
void
close_connection_to_ringd(int c)
{
  char socket_name[MAXBUF];

  close(c);

  snprintf(socket_name, sizeof(socket_name)-1, "%s/%s.sock.%d", RUN_DIR, PPROXY_PRG_NAME, getpid());
  unlink(socket_name);
}



/*****************************************************************************

  bind_and_listen()

  returns fd

*****************************************************************************/
int
bind_and_listen(const struct sockaddr_in *sin)
{
  char buf[MAXBUF];
  int local_socket;
  int fd;

  /* XLOG-DOC:INF:0084:open_port
   * Pproxy has been instructed to listen to a new IP number and port. */
  xlog_printf(xlog_inf, 0x0084, "open_port ip='%s' port=%d", print_ip(sin, NULL), ntohs(sin->sin_port));

  snprintf(buf, sizeof(buf)-1, "o %s %d", print_ip(sin, NULL), ntohs(sin->sin_port));

  local_socket = connect_to_ringd();
  if (local_socket < 0) return -1;

  if (send(local_socket, buf, strlen(buf)+1, 0) != strlen(buf)+1) {
    /* XLOG-DOC:SOS:0085:port_open_send_failed
     * Sending a port open request to the ringd process failed. */
    xlog_printf(xlog_sos, 0x0085, "port_open_send_failed errno=%d errmsg='%s'", errno, strerror(errno));
    close_connection_to_ringd(local_socket);
    return -1;
  }

  {
    char buf[MAXBUF];
    int result = uds_recv(local_socket, NULL, buf, sizeof(buf), &fd);
    close_connection_to_ringd(local_socket);
    if (result < 0) {
      /* XLOG-DOC:SOS:0086:port_open_failed
       * Opening of the port failed for some reason on the pproxy side. */
      xlog_printf(xlog_sos, 0x0086, "port_open_failed errno=%d errmsg='%s'", errno, strerror(errno));
      return -1;
    } else if (result == 0) {
      /* XLOG-DOC:SOS:0143:port_open_no_fd
       * The opening and binding of the port by the ringd process failed
       * for some reason. The message from the ringd process is printed
       * with this message. */
      xlog_printf(xlog_sos, 0x0143, "port_open_no_fd msg='%s'", buf);
      return -1;
    }
  }

  if (listen(fd, conf.backlog) != 0) {
    /* XLOG-DOC:SOS:0087:listen_failed
     * The listen() system call on the just opened and bound socket
     * failed. */
    xlog_printf(xlog_sos, 0x0087, "listen_failed errno=%d errmsg='%s'", errno, strerror(errno));
    close(fd);
    return -1;
  }

  return fd;
}


/*****************************************************************************

  close_listen_port()

*****************************************************************************/
int
close_listen_port(const struct sockaddr_in *sin)
{
  char buf[MAXBUF];
  int local_socket;
  int fd;

  /* XLOG-DOC:INF:0193:close_port
   * Pproxy has been instructed to listen to a new IP number and port. */
  xlog_printf(xlog_inf, 0x0193, "close_port ip='%s' port=%d", print_ip(sin, NULL), ntohs(sin->sin_port));

  snprintf(buf, sizeof(buf)-1, "c %s %d", print_ip(sin, NULL), ntohs(sin->sin_port));

  local_socket = connect_to_ringd();
  if (local_socket < 0) return -1;

  if (send(local_socket, buf, strlen(buf)+1, 0) != strlen(buf)+1) {
    /* XLOG-DOC:SOS:0194:port_close_send_failed
     * Sending a port close request to the ringd process failed. */
    xlog_printf(xlog_sos, 0x0194, "port_close_send_failed errno=%d errmsg='%s'", errno, strerror(errno));
    close_connection_to_ringd(local_socket);
    return -1;
  }

  {
    char buf[MAXBUF];
    int result = uds_recv(local_socket, NULL, buf, sizeof(buf), &fd);
    close_connection_to_ringd(local_socket);
    if (result < 0) {
      /* XLOG-DOC:SOS:0195:port_close_failed
       * Closing of the port failed for some reason on the pproxy side. */
      xlog_printf(xlog_sos, 0x0195, "port_close_failed errno=%d errmsg='%s'", errno, strerror(errno));
      return -1;
    }
  }

  return 0;
}


/*****************************************************************************

  print_usage()

*****************************************************************************/
void
print_usage(const char *prg)
{
  printf("Usage: %s [OPTION] ...\n", prg);
  printf("POP proxy server.\n");
  printf("Options:\n");
  printf("  -l, --logfile=FILE     set name of logfile\n");
  printf("                         (default: `%s')\n", PPROXY_LOG_FILE);
  printf("  -r, --rundir=DIR       set name of directory for pid and state file,\n");
  printf("                         and for ctrl socket (default: `%s')\n", RUN_DIR);
  printf("      --help             print this help message\n");
  printf("      --version          print version information\n");
  printf("\n");
}


/*****************************************************************************

  new_session()

*****************************************************************************/
void
new_session(struct virt_serv *vserv)
{
  struct sockaddr remote;
  socklen_t remotelen=sizeof(remote);
  int n;
  pid_t pid;
  int clientfd;

  if (! vserv) return;

  do {
    clientfd = accept(vserv->fd, &remote, &remotelen);
  } while (clientfd < 0 && errno == EINTR);

  if (clientfd < 0) {
    /* XLOG-DOC:ERR:0146:accept_failed
     * The accept () system call failed for some reason. */
    xlog_printf(xlog_err, 0x0146, "accept_failed errno=%d errmsg='%s'", errno, strerror(errno));
    return;
  }

  /* find next free session */
  do {
    current_session = (current_session + 1) % conf.maxsession;
  } while (sp->session[current_session].state != sstFree);

  memset(&(sp->session[current_session]), 0, sizeof(sp->session[current_session]));
  sp->session[current_session].fd_client = clientfd;
  sp->session[current_session].state = sstNew;
  sp->session[current_session].statetime = time(NULL);
  sp->session[current_session].mailcheck_retry = -1;

  pid = fork();

  switch (pid) {
    case -1:							/* error */
      /* XLOG-DOC:SOS:0089:fork_failed
       * Pproxy tried to fork a child and that failed. This probably means
       * that the system is overloaded. The connection to the client will
       * be closed and we try to keep going. */
      xlog_printf(xlog_sos, 0x0089, "fork_failed errno=%d errmsg='%s'", errno, strerror(errno));
      close(clientfd);
      sp->session[current_session].state = sstFree;
      break;
    case 0:							/* child */
      DEBUG0(DG_MAIN, "main", "child started");
      /* close all unneeded file descriptors */
      for (n=0; n < 255; n++) {
	if (n == statusfd) continue;	/* don't close status fd */
	if (n == clientfd) continue;	/* don't close new connection */
	if (n == logfd) continue;	/* don't close logging fd */
	close(n);
      }
      child_main(&(sp->session[current_session]), current_session, vserv);
      DEBUG0(DG_MAIN, "main", "child: should never be here");
      exit(RCODE_OK);
    default:							/* parent */
      close(clientfd);
      DEBUG1(DG_MAIN, "main", "parent: new process %d started", pid);
      sp->session[current_session].pid = pid;
      sp->stat.connections++;
      sp->used_sessions++;
      break;
  }
}


/*****************************************************************************

  main()

*****************************************************************************/
int
main(int argc, char *argv[])
{
  int pcontrol_fd;
  int c;
  char *prgname = argv[0];

  static struct option long_options[] = {
    { "help",          no_argument,       0, 0 },
    { "version",       no_argument,       0, 0 },
    { "rundir",        required_argument, 0, 'r' },
    { "logfile",       required_argument, 0, 'l' },
    { NULL,            0,                 0, 0 }
  };


  /***************************************************************************
    Set defaults for various config options.
  ***************************************************************************/
  init_config_vars();
  debug = 0;

  {
    char *p;
    (void) gethostname(conf.sidprefix, sizeof(conf.sidprefix));
    conf.sidprefix[sizeof(conf.sidprefix)-1] = '\0';
    p = strchr(conf.sidprefix, '.');
    if (p) *p = '\0';
  }


  /***************************************************************************
    Parse command line options.
  ***************************************************************************/
  while (1) {
    int option_index = 0;
    c = getopt_long(argc, argv, "l:r:", long_options, &option_index);
 
    if (c == -1) break;

    switch (c) {
      case 0:
        if (! strcmp(long_options[option_index].name, "version")) {
          printf("pproxy - POPular POP server suite %s\n", VERSION);
          exit(RCODE_OK);
        } else if (! strcmp(long_options[option_index].name, "help")) {
          print_usage(prgname);
          exit(RCODE_OK);
        } else {
          fprintf(stderr, "%s: unknown option: %s\n", prgname, long_options[option_index].name);
          exit(RCODE_INTERNAL);
        }
        break;
      case 'l':		/* logfile */
        (void) strlcpy(conf.logfile, optarg, sizeof(conf.logfile));
        break;
      case 'r':		/* rundir */
        (void) strlcpy(conf.rundir, optarg, sizeof(conf.rundir));
        break;
      default:
        fprintf(stderr, "Try `%s --help' for more information.\n", prgname);
        exit(RCODE_CMDLINE);
    }
  }

  if (optind != argc) {
    fprintf(stderr, "%s: invalid extra arguments.\nTry `%s --help' for more information.\n", prgname, prgname);
    exit(RCODE_CMDLINE);
  }


  /***************************************************************************
    This program should not be run as root and not be setuid or setgid.
  ***************************************************************************/
  if (!getuid() || !geteuid()) {
    fprintf(stderr, "%s: don't start this program as `root'.\n", prgname);
    exit(RCODE_CMDLINE);
  }

  if (getuid() != geteuid() || getgid() != getegid()) {
    fprintf(stderr, "%s: don't make this program setuid or setgid.\n", prgname);
    exit(RCODE_CMDLINE);
  }


  /***************************************************************************
    Fork to become a daemon.
  ***************************************************************************/
  daemonize(prgname);
  conf.pid = getpid();
  signal_init(0);


  /***************************************************************************
    Open log.
  ***************************************************************************/
  if ((logfd = xlog_open(PPROXY_PRG_NAME, conf.logfile, PPROXY_LOG_MODE)) < 0) {
    fprintf(stderr, "%s: couldn't open logfile '%s': %s\n", prgname, conf.logfile, strerror(errno));
    exit(RCODE_ERR);
  }
  /* XLOG-DOC:INF:008a:start
   * PProxy has started and is mostly initialized. */
  xlog_printf(xlog_inf, 0x008a, "start version='%s' rundir='%s'", VERSION, conf.rundir);


  /***************************************************************************
    Open pid file now and write pid into it.
  ***************************************************************************/
  write_pidfile(conf.rundir, PPROXY_PID_TEMPL, PPROXY_PID_FILE);


  /***************************************************************************
    Open control socket.
  ***************************************************************************/
  pcontrol_fd =
    open_ctrl_socket(conf.rundir, PPROXY_SOCK_TEMPL, PPROXY_SOCK_FILE, PPROXY_SOCK_UMASK);


  /***************************************************************************
    Preparing shared mmaped file for status and statistical infos.
  ***************************************************************************/
  sp = prepare_statefile(conf.rundir, PPROXY_STATE_TEMPL, PPROXY_STATE_FILE, sizeof(struct shmem_proxy), &statusfd);
  (void) strlcpy(sp->magic, "POPULAR", sizeof(sp->magic));
  (void) strlcpy(sp->type,  "PROXY",   sizeof(sp->type));
  sp->version       = SHMEM_VERSION;
  sp->starttime     = time(NULL);
  sp->max_virt_serv = MAX_VIRT_SERV;
  sp->max_backend   = MAX_BACKEND;
  sp->max_sessions  = MAX_SESSION;
  sp->n_sessions    = conf.maxsession;


  /***************************************************************************
    Initialize TLS if compiled in.
  ***************************************************************************/
#ifdef USE_TLS
  io_tls_main_setup();
#endif


  /***************************************************************************
    OK. Here we are at last: The main loop.
  ***************************************************************************/
  current_session=0;
  DEBUG0(DG_MAIN, "main", "start main loop");
  while (1) {
    fd_set set;
    int rc;
    int sig;

    FD_ZERO(&set);
    FD_SET(pcontrol_fd, &set);

    /* add virtual server fds to fd set only if there are free slots and
       the load is not too high */
    if (sess_ok(sp->used_sessions, conf.maxsession, NULL, NULL) && load_ok(conf.maxlocalload)) fdset_virt_serv(&set);

    rc = rel_select(&set, 1, 0);
    if (rc < 0) {
      /* XLOG-DOC:SOS:008e:select_error
       * The select () syscall in the main loop returned an error that is
       * not handled. This should never happen. */
      xlog_printf(xlog_sos, 0x008e, "select_error errno=%d errmsg='%s'", errno, strerror(errno));
      continue;
    }

    while ((sig = signal_next())) {
      switch (sig) {
	case SIGQUIT: 
	  pproxy_shutdown(SHUTDOWN_SERVER);
	  break;
	case SIGINT:
	  pproxy_shutdown(SHUTDOWN_CHILDREN);
	  break;
	case SIGTERM:
	  pproxy_shutdown(SHUTDOWN_CHILDREN | SHUTDOWN_SERVER);
	  break;
	case SIGHUP:
	  logfd = xlog_reopen(conf.logfile, PPROXY_LOG_MODE);
	  break;
      }
    }

    reapchildren(&free_session);
    if (delayed_shutdown && ! sp->used_sessions) {
      /* XLOG-DOC:INF:0130:all_children_gone
       * All children have died and the server got a delayed shutdown
       * command earlier. It will shutdown now. */
      xlog_printf(xlog_inf, 0x0130, "all_children_gone");
      pproxy_shutdown(SHUTDOWN_SERVER);
    }

    /* we have to shortcut here, because content of 'set' is allowed to be
       garbage if rc < 0 */
    if (rc <= 0) continue;

    /* any commands pending on the control socket ? */
    if (FD_ISSET(pcontrol_fd, &set)) {
      got_shutdown = 0;
      control_command(pcontrol_fd);
      /* in case conf.maxsession was changed by a 'set' command. */
      sp->n_sessions = conf.maxsession;
      if (got_shutdown) pproxy_shutdown(got_shutdown);
      continue;
    }

    /* calls new_session for all virtual servers that have new connections
       pending */
    fd_isset_virt_serv(&set, conf.maxsession - sp->used_sessions);
  }

  /* XLOG-DOC:BUG:008f:left_main_loop
   * Somehow the main loop was terminated. This should never happen. */
  xlog_printf(xlog_bug, 0x008f, "left_main_loop");
  exit(RCODE_ERR);
}


/** THE END *****************************************************************/


syntax highlighted by Code2HTML, v. 0.9.1