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

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

  $Id: ringd.c,v 1.7 2004/08/21 10:19:29 sqrt Exp $

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

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

  Copyright (C) 1999-2004  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 "ringd.h"
#include "daemon.h"


/* define this if you want debugging messages in the log */
/*#undef DEBUG */
#define DEBUG

/* this is the size of the message buffer */
#define BUFSIZE 80


int debug;

struct socket_list {
  int refcount;
  int socket;
  struct sockaddr_in addr;
} socket_list[RINGD_MAX_SOCK];


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

  open_and_bind()

*****************************************************************************/
int
open_and_bind(const struct sockaddr_in *addr) {
  const int one=1;
  int sock;
  int i;
  int slot = -1;


  /***************************************************************************
    Find first empty slot in socket_list
  ***************************************************************************/
  for (i=0; i < RINGD_MAX_SOCK; i++) {
    if (socket_list[i].refcount == 0) {
      slot = i;
      break;
    }
  }

  if (slot == -1) {
    /* XLOG-DOC:SOS:0187:socket_max_reached
     * The maximum number of sockets is reached. If you need more sockets
     * change RINGD_MAX_SOCK and recompile. */
    xlog_printf(xlog_sos, 0x0187, "socket_max_reached");
    return -1;
  }


  /***************************************************************************
    Create socket and bind it
  ***************************************************************************/
  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock == -1) {
    /* XLOG-DOC:ADM:0188:socket_error
     * The socket system call returned an error. */
    xlog_printf(xlog_sos, 0x0188, "socket_error errno=%d errmsg='%s'", errno, strerror(errno));
    return -1;
  }

  if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)) == -1) {
    /* XLOG-DOC:ADM:0189:setsockopt_error
     * The setsockopt system call returned an error. */
    xlog_printf(xlog_adm, 0x0189, "setsockopt_error errno=%d errmsg='%s'", errno, strerror(errno));
  }

  if (bind(sock, (struct sockaddr*)addr, sizeof(struct sockaddr_in)) == -1) {
    /* XLOG-DOC:SOS:018a:bind_error
     * The bind system call returned an error. */
    xlog_printf(xlog_sos, 0x018a, "bind_error errno=%d errmsg='%s'", errno, strerror(errno));
    close(sock);
    return -1;
  }

  
  socket_list[slot].refcount = 1;
  socket_list[slot].socket   = sock;
  socket_list[slot].addr     = *addr;

  return sock;
}


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

  read_port(char *arg)

*****************************************************************************/
int
read_port(char *arg)
{
  static int ports[] = { ALLOWED_PORTS, 0 };
  long int port;
  int i;
  char *tptr;

  port = strtol(arg, &tptr, 10);
  if (*tptr) {  /* trailing chars after number */
    /* XLOG-DOC:ADM:018f:port_invalid
     * The port number given as second argument to a ringd command is
     * syntactically invalid. */
    xlog_printf(xlog_adm, 0x018e, "port_invalid");
    return 0;
  }

  if (port < 1 || port > 65535) {
    /* XLOG-DOC:ADM:0190:port_out_of_range
     * The port number given as second argument to a ringd command is
     * out of range. */
    xlog_printf(xlog_adm, 0x0190, "port_out_of_range");
    return 0;
  }

  for (i=0; ports[i]; i++) {
    if (ports[i] == port) return (int)port;
  }

  /* XLOG-DOC:ADM:0191:port_forbidden
   * The port number given as second argument to a ringd command is
   * forbidden by configuration (change ALLOWED_PORTS and recompile). */
  xlog_printf(xlog_adm, 0x0191, "port_forbidden");
  return 0;
}


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

  handle_request()

  Handle a single bind request.

*****************************************************************************/
void
handle_request(int control_socket)
{
  char buf[BUFSIZE];
  struct sockaddr_in server_addr;
  int sock=-1;
  int len;
  unsigned short int port;
  char *space;
  struct sockaddr_un from;
  socklen_t fromlen=sizeof(struct sockaddr_un);
#define UNIX_MAX_PATH 108
  char addr[UNIX_MAX_PATH+1];
  char command;
  int n;


  /***************************************************************************
    Get request from control socket.
  ***************************************************************************/
  len = recvfrom(control_socket, buf, sizeof(buf)-1, 0, (struct sockaddr *)&from, &fromlen);
  if (len <= 0) {
    DEBUG0(DG_IO, "handle_request", "recvfrom <= 0");
    return;
  }
  buf[len] = 0;
  DEBUG1(DG_IO, "handle_request", "received request: '%s'", buf);
  strncpy(addr, from.sun_path, fromlen);
  addr[fromlen] = '\0';


  /***************************************************************************
    Find command.
  ***************************************************************************/
  command = buf[0];
  if (! strchr("ockl", command)) {
    /* XLOG-DOC:ADM:018c:unknown_command
     * ringd received an unknown command. */
    xlog_printf(xlog_adm, 0x018c, "unknown_command cmd='%c'", command);
    uds_send(control_socket, addr, "- Syntax error in request", -1);
    return;
  }

  if (strchr("ock", command)) {
    /*************************************************************************
      Check for space as 2nd char
    *************************************************************************/
    if (buf[1] != ' ') {
      /* XLOG-DOC:ADM:018d:space_char_missing
       * ringd misses the space char at the 2nd position in the command
       * string which separates the command from arguments. */
      xlog_printf(xlog_adm, 0x018c, "space_char_missing");
      uds_send(control_socket, addr, "- Syntax error in request", -1);
      return;
    }

    /*************************************************************************
      Find the space char in request, that is separating IP number and port.
    *************************************************************************/
    space = strchr(buf+2, ' ');
    if (! space) {
      /* XLOG-DOC:ADM:018e:space_char_missing
       * ringd misses the space char in the command string which separates
       * the first from the second argument */
      xlog_printf(xlog_adm, 0x018e, "space_char_missing");
      uds_send(control_socket, addr, "- Syntax error in request", -1);
      return;
    }
    *space++ = 0;


    /*************************************************************************
      Get and check port.
    *************************************************************************/
    port = read_port(space);
    if (! port) {
      uds_send(control_socket, addr, "- Illegal port", -1);
      return;
    }


    /*************************************************************************
      Get IP number from request and fill in 'server_addr' struct.
    *************************************************************************/
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);

    if (! strcmp(buf+2, "ANY")) {
      server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
      if (! inet_aton(buf+2, &server_addr.sin_addr)) {
	/* XLOG-DOC:ADM:0192:invalid_ip
	 * The port number given as first argument to a ringd command is
	 * is invalid. */
	xlog_printf(xlog_adm, 0x0192, "invalid_ip");
	uds_send(control_socket, addr, "- Invalid IP number", -1);
	return;
      }
    }

    DEBUG3(DG_MAIN, "handle_request", "Got command %c for IP %s port %d", command, buf+2, port);

  } else {
    DEBUG1(DG_MAIN, "handle_request", "Got command %c", command);
  }


  if (command == 'o') {
    /*************************************************************************
      Check if we already opened this port at some time in the past.
    *************************************************************************/
    for (n=0; n < RINGD_MAX_SOCK; n++) {
      if (socket_list[n].refcount == 0) continue;
      if (!memcmp(&server_addr, &(socket_list[n].addr), sizeof(struct sockaddr_in))) {
	socket_list[n].refcount++;
	sock = socket_list[n].socket;
	DEBUG0(DG_MAIN, "handle_request", "socket matched");
	break;
      }
    }


    /*************************************************************************
      Open socket and bind it to requested IP number and port.
    *************************************************************************/
    if (sock < 0) {
      sock = open_and_bind(&server_addr);
      if (sock < 0) {
	uds_send(control_socket, addr, "- failed", -1);
	return;
      }
    }

    /*************************************************************************
      Send message with 'enclosed' file descriptor.
    *************************************************************************/
    DEBUG2(DG_MAIN, "handle_request", "control_socket=%d sock=%d\n", control_socket, sock);
    (void) uds_send(control_socket, addr, "+ OK", sock);
  } else if (command == 'l') {
    for (n=0; n < RINGD_MAX_SOCK; n++) {
      if (socket_list[n].refcount == 0) continue;
      /* XLOG-DOC:INF:018b:port_list
       * List of currently opend ports. */
      xlog_printf(xlog_inf, 0x018b, "port_list n=%d ip=%s port=%d refcount=%d", n, print_ip(&(socket_list[n].addr), NULL), ntohs(socket_list[n].addr.sin_port), socket_list[n].refcount);
    }
    (void) uds_send(control_socket, addr, "+ OK", -1);
  } else if ((command == 'c') || (command == 'k')) {
    for (n=0; n < RINGD_MAX_SOCK; n++) {
      if (socket_list[n].refcount == 0) continue;
      if (!memcmp(&server_addr, &(socket_list[n].addr), sizeof(struct sockaddr_in))) {
	DEBUG0(DG_MAIN, "handle_request", "socket matched");
	if (command == 'c') {				/* close command */
	  socket_list[n].refcount--;
	  if (socket_list[n].refcount == 0) {
	    close(socket_list[n].socket);
	  }
	} else {					/* kill command */
	  socket_list[n].refcount = 0;
	  close(socket_list[n].socket);
	}
	break;
      }
    }
    (void) uds_send(control_socket, addr, "+ OK", -1);
  }

  DEBUG0(DG_MAIN, "handle_request", "done");
  return;
}


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

  print_usage()

*****************************************************************************/
void
print_usage(const char *prg)
{
  printf("Usage: %s [OPTION] ...\n", prg);
  printf("Ringd binds sockets for non-root programs.\n");
  printf("Options:\n");
#if 0
  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);
#endif
  printf("  -u, --user=USER        set owner for UNIX domain socket\n");
  printf("  -g, --group=GROUP      set group for UNIX domain socket \n");
  printf("      --help             print this help message\n");
  printf("      --version          print version information\n");
  printf("\n");
}


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

  main()

*****************************************************************************/
int
main(int argc, char *argv[])
{
  int control_socket;
  int sig;
  struct timeval timeout;
  fd_set fdset;
  int i;
  int c;
  char user[30];
  char group[30];
  uid_t uid;
  gid_t gid;
  char *prgname = argv[0];

  static struct option long_options[] = {
    { "help",          no_argument,       0, 0 },
    { "version",       no_argument,       0, 0 },
    { "user",          required_argument, 0, 'u' },
    { "group",         required_argument, 0, 'g' },
    { NULL,            0,                 0, 0 }
  };

#ifdef DEBUG
    debug = DG_ALL;
#endif


  /***************************************************************************
    Set defaults for various config options.
  ***************************************************************************/
  user[0] = '\0';
  group[0] = '\0';
  uid = RINGD_DEFAULT_UID;
  gid = RINGD_DEFAULT_GID;


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

    switch (c) {
      case 0:
        if (! strcmp(long_options[option_index].name, "version")) {
          printf("ringd - 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 'u':         /* user */
        (void) strlcpy(user, optarg, sizeof(user));
        break;
      case 'g':         /* group */
        (void) strlcpy(group, optarg, sizeof(group));
        break;
#if 0
      case 'l':		/* logfile */
        (void) strlcpy(conf.logfile, optarg, sizeof(conf.logfile));
        break;
      case 'r':		/* rundir */
        (void) strlcpy(conf.rundir, optarg, sizeof(conf.rundir));
        break;
#endif
      default:
        fprintf(stderr, "Try `%s --help' for more information.\n", prgname);
        exit(RCODE_CMDLINE);
    }
  }

  if (! user[0]) {
    fprintf(stderr, "%s: missing -u option.\n", prgname);
    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);
  }


  /***************************************************************************
    Check user and group names and find corresponding uid and gid.
  ***************************************************************************/
  {
    struct passwd *pwd;
    struct group *grp;
    pwd = getpwnam(user);
    if (! pwd) {
      fprintf(stderr, "%s: user `%s' not found\n", prgname, user);
      exit(RCODE_CMDLINE);
    }

    if (pwd->pw_uid) uid = pwd->pw_uid;
    else {
      fprintf(stderr, "%s: user `%s' is uid 0. Don't do that!\n", prgname, user);
      exit(RCODE_CMDLINE);
    }

    if (group[0]) {
      grp = getgrnam(group);
      if (! grp) {
        fprintf(stderr, "%s: group `%s' not in /etc/group.\n", prgname, group);
        exit(RCODE_CMDLINE);
      }
      gid = grp->gr_gid;
    } else {
      gid = pwd->pw_gid;
      grp = getgrgid(gid);
      if (! grp) {
        fprintf(stderr, "%s: group `%s' not in /etc/group.\n", prgname, group);
        exit(RCODE_CMDLINE);
      }
      (void) strlcpy(group, grp->gr_name, sizeof(group));
    }
  }


  /***************************************************************************
    Open log file
  ***************************************************************************/
  if (xlog_open(RINGD_PRG_NAME, RINGD_LOG_NAME, RINGD_LOG_MODE) < 0) {
    fprintf(stderr, "Could not open log file: %s\n", RINGD_LOG_NAME);
    exit(RCODE_ERR);
  }

  /***************************************************************************
    Become a daemon and initialize signal handling
  ***************************************************************************/
  daemonize(RINGD_PRG_NAME);
  signal_init(0);

  /* XLOG-DOC:INF:0183:start
   * ringd has started. */
  xlog_printf(xlog_inf, 0x0183, "start");



  /***************************************************************************
    Open pid file now and write pid into it.
  ***************************************************************************/
  write_pidfile(RUN_DIR, RINGD_PID_TEMPL, RINGD_PID_FILE);


  /***************************************************************************
    Open control socket.
  ***************************************************************************/
  unlink(RINGD_SOCK_FILE);	/* in case there is one left from last run */
  control_socket = uds_socket(RINGD_SOCK_FILE, RINGD_SOCK_UMASK);
  if (control_socket < 0) {
    /* XLOG-DOC:SOS:0184:open_socket
     * ringd can't open the unix domain socket to listen for commands. */
    xlog_printf(xlog_sos, 0x0184, "open_socket");
    goto clean_exit;
  }
  if (chown(RINGD_SOCK_FILE, uid, gid) != 0) {
    /* XLOG-DOC:SOS:0185:chown_socket
     * ringd can't chown its unix domain socket. */
    xlog_printf(xlog_sos, 0x0185, "chown_socket");
    goto clean_exit;
  }


  /***************************************************************************
    Initialize socket list
  ***************************************************************************/
  for (i=0; i<RINGD_MAX_SOCK; i++) {
    socket_list[i].refcount=0;
  }


  /***************************************************************************
    Main loop to handle requests.
  ***************************************************************************/
  while (1) {
    timeout.tv_sec  = 1;
    timeout.tv_usec = 0;

    FD_ZERO(&fdset);
    FD_SET(control_socket, &fdset);
    if (select(FD_SETSIZE, &fdset, (fd_set *) 0, (fd_set *) 0, &timeout) > 0) {
      handle_request(control_socket);
    }

    sig = signal_next();
    if (sig == SIGHUP) {
      DEBUG0(DG_MAIN, "main", "Got SIGHUP. Reopening Logfile.");
      xlog_reopen(RINGD_LOG_NAME, RINGD_LOG_MODE);
    } else if (sig) {
      /* XLOG-DOC:INF:0186:quit
       * ringd go a signal and will quit now. */
      xlog_printf(xlog_inf, 0x0186, "quit sig=%d", sig);
      break;
    }
  }

clean_exit:
  unlink(RINGD_SOCK_FILE);
  unlink_in_rundir(RUN_DIR, RINGD_PID_TEMPL, RINGD_PID_FILE);
  return 0;
}


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


syntax highlighted by Code2HTML, v. 0.9.1