/*
 * ProFTPD: mod_radius -- a module for RADIUS authentication and accounting
 *
 * Copyright (c) 2001-2005 TJ Saunders
 *
 * 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.
 *
 * As a special exemption, TJ Saunders gives permission to link this program
 * with OpenSSL, and distribute the resulting executable, without including
 * the source code for OpenSSL in the source distribution.
 *
 * This is mod_radius, contrib software for proftpd 1.2 and above.
 * For more information contact TJ Saunders <tj@castaglia.org>.
 *
 * This module is based in part on code in Alan DeKok's (aland@freeradius.org)
 * mod_auth_radius for Apache, in part on the FreeRADIUS project's code.
 *
 * $Id: mod_radius.c,v 1.31 2005/06/14 01:23:44 castaglia Exp $
 */

#define MOD_RADIUS_VERSION "mod_radius/0.8"

#include "conf.h"
#include "privs.h"

/* RADIUS information */

/* From RFC2865, RFC2866 */
#define RADIUS_AUTH_PORT	1812
#define RADIUS_ACCT_PORT	1813

#define RADIUS_PASSWD_LEN	16
#define RADIUS_VECTOR_LEN	16

/* From RFC2138 */
#define RADIUS_STRING_LEN	254

/* RADIUS attribute structures
 */
typedef struct {
  unsigned char type;
  unsigned char length;
  unsigned char data[1];
} radius_attrib_t;

/* RADIUS packet header
 */
typedef struct {
  unsigned char code;
  unsigned char id;
  unsigned short length;
  unsigned char digest[RADIUS_VECTOR_LEN];
  unsigned char data[2];
} radius_packet_t;

#define RADIUS_HEADER_LEN	20

/* RADIUS ID Definitions (see RFC2865)
 */
#define RADIUS_AUTH_REQUEST		1
#define RADIUS_AUTH_ACCEPT		2
#define RADIUS_AUTH_REJECT		3
#define RADIUS_ACCT_REQUEST		4
#define RADIUS_ACCT_RESPONSE		5
#define RADIUS_ACCT_STATUS		6
#define RADIUS_AUTH_CHALLENGE		11

/* RADIUS Attribute Definitions (see RFC2865)
 */
#define RADIUS_USER_NAME		1
#define RADIUS_PASSWORD			2
#define RADIUS_NAS_IP_ADDRESS		4
#define RADIUS_NAS_PORT			5
#define RADIUS_SERVICE_TYPE		6
#define RADIUS_OLD_PASSWORD		17
#define RADIUS_REPLY_MESSAGE		18
#define RADIUS_STATE			24
#define RADIUS_VENDOR_SPECIFIC		26
#define RADIUS_SESSION_TIMEOUT		27
#define RADIUS_IDLE_TIMEOUT		28
#define RADIUS_CALLING_STATION_ID	31
#define RADIUS_NAS_IDENTIFIER		32
#define RADIUS_ACCT_STATUS_TYPE		40
#define RADIUS_ACCT_INPUT_OCTETS	42
#define RADIUS_ACCT_OUTPUT_OCTETS	43
#define RADIUS_ACCT_SESSION_ID		44
#define RADIUS_ACCT_AUTHENTIC		45
#define RADIUS_ACCT_SESSION_TIME	46
#define RADIUS_ACCT_TERMINATE_CAUSE	49
#define RADIUS_NAS_PORT_TYPE		61

/* RADIUS service types
 */
#define RADIUS_SVC_LOGIN		1
#define RADIUS_SVC_AUTHENTICATE_ONLY	8

/* RADIUS status types
 */
#define RADIUS_ACCT_STATUS_START	1
#define RADIUS_ACCT_STATUS_STOP		2
#define RADIUS_ACCT_STATUS_ALIVE	3

/* RADIUS NAS port types
 */
#define RADIUS_NAS_PORT_TYPE_VIRTUAL	5

/* RADIUS authentication types
 */
#define RADIUS_AUTH_NONE		0
#define RADIUS_AUTH_RADIUS		1
#define RADIUS_AUTH_LOCAL		2

/* The RFC says 4096 octets max, and most packets are less than 256.
 * However, this number is just larger than the maximum MTU of just
 * most types of networks, except maybe for gigabit ethernet.
 */
#define RADIUS_PACKET_LEN		1600

/* Miscellaneous default values
 */
#define DEFAULT_RADIUS_TIMEOUT	30

typedef struct radius_server_obj {

  /* Next server in line */
  struct radius_server_obj *next;

  /* Memory pool for this object */
  pool *pool;

  /* RADIUS server IP address */
  pr_netaddr_t *addr;

  /* RADIUS server port */
  unsigned short port;

  /* RADIUS server shared secret */
  unsigned char *secret;

  /* How long to wait for RADIUS responses */
  unsigned int timeout;

} radius_server_t;

module radius_module;

static pool *radius_pool = NULL;
static unsigned char radius_engine = FALSE;
static radius_server_t *radius_acct_server = NULL;
static radius_server_t *radius_auth_server = NULL;

static int radius_logfd = -1;
static char *radius_logname = NULL;

static struct sockaddr radius_local_sock, radius_remote_sock;

/* For tracking various values not stored in the session struct */
static pr_netaddr_t *radius_local_addr = NULL;
static pr_netaddr_t *radius_remote_addr = NULL;
static char *radius_realm = NULL;
static time_t radius_session_start = 0;
static off_t radius_session_bytes_in = 0;
static off_t radius_session_bytes_out = 0;
static int radius_session_authtype = RADIUS_AUTH_LOCAL;
static unsigned char radius_auth_ok = FALSE;
static unsigned char radius_auth_reject = FALSE;

/* "Fake" user/group information for RADIUS users. */
static unsigned char radius_have_user_info = FALSE;
static struct passwd radius_passwd;

static unsigned char radius_have_group_info = FALSE;
static char *radius_prime_group_name = NULL;
static unsigned int radius_addl_group_count = 0;
static char **radius_addl_group_names = NULL;
static char *radius_addl_group_names_str = NULL;
static gid_t *radius_addl_group_ids = NULL;
static char *radius_addl_group_ids_str = NULL;

/* Other info */
static unsigned char radius_have_other_info = FALSE;

/* Vendor information, defaults to Unix (Vendor-Id of 4) */
static const char *radius_vendor_name = "Unix";
static unsigned int radius_vendor_id = 4;

/* Custom VSA IDs that may be used for server-supplied RadiusUserInfo
 * parameters.
 */
static int radius_uid_attr_id = 0;
static int radius_gid_attr_id = 0;
static int radius_home_attr_id = 0;
static int radius_shell_attr_id = 0;

/* Custom VSA IDs that may be used for server-supplied RadiusGroupInfo
 * parameters.
 */
static int radius_prime_group_name_attr_id = 0;
static int radius_addl_group_names_attr_id = 0;
static int radius_addl_group_ids_attr_id = 0;

/* For tracking the ID of the last accounting packet (to prevent the
 * same ID from being reused).
 */
static unsigned char radius_last_acct_pkt_id = 0;

/* Convenience macros. */
#define RADIUS_IS_VAR(str) \
  str[0] == '$' && str[1] == '(' && str[strlen(str)-1] == ')'

/* Function prototypes. */
static void radius_add_attrib(radius_packet_t *, unsigned char,
  const unsigned char *, size_t);
static void radius_add_passwd(radius_packet_t *, unsigned char,
  const char *, char *);
static void radius_build_packet(radius_packet_t *, const char *,
  const char *, char *);
static unsigned char radius_chk_var(char *);
static int radius_closelog(void);
static int radius_close_socket(int);
static void radius_get_acct_digest(radius_packet_t *, char *);
static radius_attrib_t *radius_get_attrib(radius_packet_t *, unsigned char);
static void radius_get_rnd_digest(radius_packet_t *);
static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *,
  unsigned char);
static int radius_log(const char *, ...);
static radius_server_t *radius_make_server(pool *);
static int radius_openlog(void);
static int radius_open_socket(void);
static unsigned char radius_parse_gids_str(pool *, char *, gid_t **,
  unsigned int *);
static unsigned char radius_parse_groups_str(pool *, char *, char ***,
  unsigned int *);
static void radius_parse_var(char *, int *, char **);
static void radius_process_accpt_packet(radius_packet_t *);
static void radius_process_group_info(config_rec *);
static void radius_process_user_info(config_rec *);
static radius_packet_t *radius_recv_packet(int, unsigned int);
static int radius_send_packet(int, radius_packet_t *, radius_server_t *);
static unsigned char radius_start_accting(void);
static unsigned char radius_stop_accting(void);
static int radius_verify_packet(radius_packet_t *, radius_packet_t *,
  char *);

/* Support functions
 */

static char *radius_argsep(char **arg) {
  char *ret = NULL, *dst = NULL;
  char quote_mode = 0;

  if (!arg || !*arg || !**arg)
    return NULL;

  while (**arg && isspace((int) **arg))
    (*arg)++;

  if (!**arg)
    return NULL;

  ret = dst = *arg;

  if (**arg == '\"') {
    quote_mode++;
    (*arg)++;
  }

  while (**arg && **arg != ',' &&
      (quote_mode ? (**arg != '\"') : (!isspace((int) **arg)))) {

    if (**arg == '\\' && quote_mode) {

      /* escaped char */
      if (*((*arg) + 1))
        *dst = *(++(*arg));
    }

    *dst++ = **arg;
    ++(*arg);
  }

  if (**arg)
    (*arg)++;

  *dst = '\0';
  return ret;
}

/* Check a "$(attribute-id:default)" string for validity. */
static unsigned char radius_chk_var(char *var) {
  int id = 0;
  char *tmp = NULL;

  /* Must be at least six characters. */
  if (strlen(var) < 7)
    return FALSE;

  /* Must start with '$(', and end with ')'. */
  if (!RADIUS_IS_VAR(var))
    return FALSE;

  /* Must have a ':'. */
  if ((tmp = strchr(var, ':')) == NULL)
    return FALSE;

  /* ':' must be between '(' and ')'. */
  if (tmp < (var + 3) || tmp > &var[strlen(var)-2])
    return FALSE;

  /* Parse out the component int/string. */
  radius_parse_var(var, &id, NULL);

  /* Int must be greater than zero. */
  if (id < 1)
    return FALSE;

  return TRUE;
}

/* Separate the given "$(attribute-id:default)" string into its constituent
 * custom attribute ID (int) and default (string) components.
 */
static void radius_parse_var(char *var, int *attr_id, char **attr_default) {
  pool *tmp_pool = make_sub_pool(radius_pool);
  char *var_cpy = pstrdup(tmp_pool, var), *tmp = NULL;

  /* First, strip off the "$()" variable characters. */
  var_cpy[strlen(var_cpy)-1] = '\0';
  var_cpy += 2;

  /* Find the delimiting ':' */
  tmp = strchr(var_cpy, ':');

  *tmp++ = '\0';

  if (attr_id)
    *attr_id = atoi(var_cpy);

  if (attr_default) {
    tmp = strchr(var, ':');

    /* Note: this works because the calling of this function by
     * radius_chk_var(), which occurs during the parsing process, uses
     * a NULL for this portion, so that the string stored in the config_rec
     * is not actually manipulated, as is done here.
     */
    var[strlen(var)-1] = '\0';

    *attr_default = ++tmp;
  }

  /* Clean up. */
  destroy_pool(tmp_pool);
}

static unsigned char radius_parse_gids_str(pool *p, char *gids_str, 
    gid_t **gids, unsigned int *ngids) {
  char *val = NULL;
  array_header *group_ids = make_array(p, 0, sizeof(gid_t *));

  /* Add each GID to the array. */
  while ((val = radius_argsep(&gids_str)) != NULL) {
    gid_t gid;
    char *endp = NULL;

    /* Make sure the given ID is a valid number. */
    gid = strtoul(val, &endp, 10);

    if (endp && *endp) {
      pr_log_pri(PR_LOG_NOTICE, "RadiusGroupInfo badly formed group ID: %s",
        val);
      return FALSE;
    }

    /* Push the ID into the ID array. */
    *((gid_t *) push_array(group_ids)) = gid;
  }

  *gids = (gid_t *) group_ids->elts;
  *ngids = group_ids->nelts;

  return TRUE;
}

static unsigned char radius_parse_groups_str(pool *p, char *groups_str,
    char ***groups, unsigned int *ngroups) {
  char *name = NULL;
  array_header *group_names = make_array(p, 0, sizeof(char **));

  /* Add each name to the array. */
  while ((name = radius_argsep(&groups_str)) != NULL) {
    char *tmp = pstrdup(p, name);

    /* Push the name into the name array. */
    *((char **) push_array(group_names)) = tmp;
  }

  *groups = (char **) group_names->elts;
  *ngroups = group_names->nelts;

  return TRUE;
}

static void radius_process_accpt_packet(radius_packet_t *packet) {

  /* First, parse the packet for any non-RadiusUserInfo attributes,
   * such as timeouts.  None are currently implemented, but...this would
   * be the place to do it.
   */
  /* radius_log("parsing packet for custom attribute IDs"); */

  /* Now, parse the packet for any server-supplied RadiusUserInfo attributes,
   * if RadiusUserInfo is indeed in effect.
   */

  if (!radius_have_user_info && !radius_have_group_info)
    /* Return now if there's no reason for doing extra work. */
    return;

  if (radius_uid_attr_id || radius_gid_attr_id ||
      radius_home_attr_id || radius_shell_attr_id) {
    radius_log("parsing packet for RadiusUserInfo attributes");

    /* These custom values will been supplied in the configuration file, and
     * set when the RadiusUserInfo config_rec is retrieved, during
     * session initialization.
     */

    if (radius_uid_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_uid_attr_id);

      if (attrib) {
        int uid = -1;

        /* Parse the attribute value into an int, then cast it into the
         * radius_passwd.pw_uid field.  Make sure it's a sane UID
         * (ie non-negative).
         */

        /* Dare we trust attrib->length? */
        memcpy(&uid, attrib->data, attrib->length);
        uid = ntohl(uid);

        if (uid < 0)
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal user ID: '%u'", radius_vendor_name, radius_uid_attr_id,
            uid);

        else {
          radius_passwd.pw_uid = uid;

          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "user ID: '%u'", radius_vendor_name, radius_uid_attr_id,
            radius_passwd.pw_uid);
        }

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "user ID: defaulting to '%u'", radius_vendor_name, radius_uid_attr_id,
          radius_passwd.pw_uid);
    }

    if (radius_gid_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_gid_attr_id);

      if (attrib) {
        int gid = -1;

        /* Parse the attribute value into an int, then cast it into the
         * radius_passwd.pw_gid field.  Make sure it's a sane GID
         * (ie non-negative).
         */

        /* Dare we trust attrib->length? */
        memcpy(&gid, attrib->data, attrib->length);
        gid = ntohl(gid);

        if (gid < 0)
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal group ID: '%u'", radius_vendor_name, radius_gid_attr_id,
            gid);

        else {
          radius_passwd.pw_gid = gid;

          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "group ID: '%u'", radius_vendor_name, radius_gid_attr_id,
            radius_passwd.pw_gid);
        }

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "group ID: defaulting to '%u'", radius_vendor_name,
          radius_gid_attr_id, radius_passwd.pw_gid);
    }

    if (radius_home_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_home_attr_id);

      if (attrib) {

        /* RADIUS strings are not NUL-terminated. */
        char *home = pcalloc(radius_pool, attrib->length + 1);

        /* Parse the attribute value into a string of the necessary length,
         * then replace radius_passwd.pw_dir with it.  Make sure it's a sane
         * home directory (ie starts with a '/').
         */

        /* Dare we trust attrib->length? */
        memcpy(home, attrib->data, attrib->length);

        if (*home != '/')
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal home: '%s'", radius_vendor_name, radius_home_attr_id,
            home);

        else {
          radius_passwd.pw_dir = home;

          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "home directory: '%s'", radius_vendor_name, radius_home_attr_id,
            radius_passwd.pw_dir);
        }

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "home directory: defaulting to '%s'", radius_vendor_name,
          radius_home_attr_id, radius_passwd.pw_dir);
    }

    if (radius_shell_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_shell_attr_id);

      if (attrib) {
        /* RADIUS strings are not NUL-terminated. */
        char *shell = pcalloc(radius_pool, attrib->length + 1);

        /* Parse the attribute value into a string of the necessary length,
         * then replace radius_passwd.pw_shell with it.  Make sure it's a sane
         * shell (ie starts with a '/').
         */

        /* Dare we trust attrib->length? */
        memcpy(shell, attrib->data, attrib->length);

        if (*shell != '/')
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal shell: '%s'", radius_vendor_name, radius_shell_attr_id,
            shell);

        else {
          radius_passwd.pw_shell = shell;

          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "shell: '%s'", radius_vendor_name, radius_shell_attr_id,
            radius_passwd.pw_shell);
        }

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "shell: defaulting to '%s'", radius_vendor_name, radius_shell_attr_id,
          radius_passwd.pw_shell);
    }
  }

  if (radius_prime_group_name_attr_id ||
      radius_addl_group_names_attr_id ||
      radius_addl_group_ids_attr_id) {
    unsigned int ngroups = 0, ngids = 0;
    char **groups = NULL;
    gid_t *gids = NULL;

    radius_log("parsing packet for RadiusGroupInfo attributes");

    if (radius_prime_group_name_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_prime_group_name_attr_id);

      if (attrib) {
        /* RADIUS strings are not NUL-terminated. */
        char *group_name = pcalloc(radius_pool, attrib->length + 1);

        /* Dare we trust attrib->length? */
        memcpy(group_name, attrib->data, attrib->length);

        radius_prime_group_name = group_name;

        radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
          "primary group name: '%s'", radius_vendor_name,
          radius_prime_group_name_attr_id, radius_prime_group_name);

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "prime group name: defaulting to '%s'", radius_vendor_name,
          radius_prime_group_name_attr_id, radius_prime_group_name);
    }

    if (radius_addl_group_names_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_addl_group_names_attr_id);

      if (attrib) {
        /* RADIUS strings are not NUL-terminated. */
        char *group_names = pcalloc(radius_pool, attrib->length + 1);
        char *group_names_str = NULL;

        /* Dare we trust attrib->length? */
        memcpy(group_names, attrib->data, attrib->length);

        /* Make a copy of the string, for logging purposes.  The parsing
         * of the original string will consume it.
         */
        group_names_str = pstrdup(radius_pool, group_names);

        if (!radius_parse_groups_str(radius_pool, group_names, &groups,
            &ngroups))
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal additional group names: '%s'", radius_vendor_name,
            radius_addl_group_names_attr_id, group_names_str);

        else
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "additional group names: '%s'", radius_vendor_name,
            radius_addl_group_names_attr_id, group_names_str);

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "additional group names: defaulting to '%s'", radius_vendor_name,
          radius_addl_group_names_attr_id, radius_addl_group_names_str);
    }     

    if (radius_addl_group_ids_attr_id) {
      radius_attrib_t *attrib = radius_get_vendor_attrib(packet,
        radius_addl_group_ids_attr_id);

      if (attrib) {
        /* RADIUS strings are not NUL-terminated. */
        char *group_ids = pcalloc(radius_pool, attrib->length + 1);
        char *group_ids_str = NULL;

        /* Dare we trust attrib->length? */
        memcpy(group_ids, attrib->data, attrib->length);

        /* Make a copy of the string, for logging purposes.  The parsing
         * of the original string will consume it.
         */
        group_ids_str = pstrdup(radius_pool, group_ids);

        if (!radius_parse_gids_str(radius_pool, group_ids, &gids, &ngids))
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "illegal additional group IDs: '%s'", radius_vendor_name,
            radius_addl_group_ids_attr_id, group_ids_str);

        else
          radius_log("packet includes '%s' Vendor-Specific Attribute %d for "
            "additional group IDs: '%s'", radius_vendor_name,
            radius_addl_group_ids_attr_id, group_ids_str);

      } else
        radius_log("packet lacks '%s' Vendor-Specific Attribute %d for "
          "additional group IDs: defaulting to '%s'", radius_vendor_name,
          radius_addl_group_ids_attr_id, radius_addl_group_ids_str);
    }    

    /* One last RadiusGroupInfo check: does the number of returned group
     * names match the number of returned group IDs?
     */
    if (ngroups == ngids) {
      radius_have_group_info = TRUE;
      radius_addl_group_count = ngroups;
      radius_addl_group_names = groups;
      radius_addl_group_ids = gids;

    } else
      radius_log("server provided mismatched number of group names (%u) "
        "and group IDs (%u), ignoring them", ngroups, ngids);
  }
}

static void radius_process_group_info(config_rec *c) {
  char *param = NULL;
  unsigned char have_illegal_value = FALSE;

  /* Parse out any configured attribute/defaults here. The stored strings will
   * already have been sanitized by the configuration handler, so I don't
   * need to worry about that here.
   */

  param = (char *) c->argv[0];

  if (RADIUS_IS_VAR(param)) {
    radius_parse_var(param, &radius_prime_group_name_attr_id,
      &radius_prime_group_name);

  } else 
    radius_prime_group_name = param;

  /* If the group count (c->argv[1]) is zero, then I know that the data
   * are VSA variable strings.  Otherwise, the group information has
   * already been parsed.
   */
  if (*((unsigned int *) c->argv[1]) == 0) {
    unsigned int ngroups = 0, ngids = 0;
    char **groups = NULL;
    gid_t *gids = NULL;

    radius_parse_var((char *) c->argv[2], &radius_addl_group_names_attr_id,
      &radius_addl_group_names_str);

    /* Now, parse the default value provided. */
    if (!radius_parse_groups_str(c->pool, radius_addl_group_names_str,
        &groups, &ngroups)) {
      radius_log("badly formatted RadiusGroupInfo default additional "
        "group names");
      have_illegal_value = TRUE;
    }

    radius_parse_var((char *) c->argv[3], &radius_addl_group_ids_attr_id,
      &radius_addl_group_ids_str);

    /* Similarly, parse the default value provided. */
    if (!radius_parse_gids_str(c->pool, radius_addl_group_ids_str,
        &gids, &ngids)) {
      radius_log("badly formatted RadiusGroupInfo default additional "
        "group IDs");
      have_illegal_value = TRUE;
    }

    if (!have_illegal_value && ngroups != ngids) {
      radius_log("mismatched number of RadiusGroupInfo default additional "
        "group names (%u) and IDs (%u)", ngroups, ngids);
      have_illegal_value = TRUE;
    }

    if (!have_illegal_value) {
      radius_have_group_info = TRUE;
      radius_addl_group_count = ngroups;
      radius_addl_group_names = groups;
      radius_addl_group_ids = gids;
    }

  } else {
    radius_have_group_info = TRUE;
    radius_addl_group_count = *((unsigned int *) c->argv[1]);
    radius_addl_group_names = (char **) c->argv[2];
    radius_addl_group_ids = (gid_t *) c->argv[3];
  }

  if (have_illegal_value) {
    radius_have_group_info = FALSE;
    radius_log("error with RadiusGroupInfo parameters, ignoring them");
  }
}

static void radius_process_user_info(config_rec *c) {
  char *param = NULL;
  unsigned char have_illegal_value = FALSE;
 
  /* radius_passwd.pw_name will be filled in later, after successful
   * authentication.  radius_passwd.pw_gecos will always be NULL, as there
   * is no practical need for this information.
   */

  radius_passwd.pw_passwd = NULL;
  radius_passwd.pw_gecos = NULL;

  /* Parse out any configured attribute/defaults here. The stored strings will
   * already have been sanitized by the configuration handler, so I don't
   * need to worry about that here.
   */

  /* Process the UID string. */
  param = (char *) c->argv[0];

  if (RADIUS_IS_VAR(param)) {
    char *endp = NULL, *value = NULL;

    radius_parse_var(param, &radius_uid_attr_id, &value);
    radius_passwd.pw_uid = (uid_t) strtoul(value, &endp, 10);

    if (radius_passwd.pw_uid == (uid_t) -1) {
      radius_log("illegal RadiusUserInfo default UID value: -1 not allowed");
      have_illegal_value = TRUE;
    }

    if (endp && *endp) {
      radius_log("illegal RadiusUserInfo default UID value: '%s' not a number",
        value);
      have_illegal_value = TRUE;
    }

  } else {

    char *endp = NULL;
    radius_passwd.pw_uid = (uid_t) strtoul(param, &endp, 10);

    if (radius_passwd.pw_uid == (uid_t) -1) {
      radius_log("illegal RadiusUserInfo UID value: -1 not allowed");
      have_illegal_value = TRUE;
    }

    if (endp && *endp) {
      radius_log("illegal RadiusUserInfo UID value: '%s' not a number", param);
      have_illegal_value = TRUE;
    }
  }

  /* Process the GID string. */
  param = (char *) c->argv[1];

  if (RADIUS_IS_VAR(param)) {
    char *endp = NULL, *value = NULL;

    radius_parse_var(param, &radius_gid_attr_id, &value);
    radius_passwd.pw_gid = (gid_t) strtoul(value, &endp, 10);

    if (radius_passwd.pw_gid == (gid_t) -1) {
      radius_log("illegal RadiusUserInfo default GID value: -1 not allowed");
      have_illegal_value = TRUE;
    }

    if (endp && *endp) {
      radius_log("illegal RadiusUserInfo default GID value: '%s' not a number",
        value);
      have_illegal_value = TRUE;
    }

  } else {

    char *endp = NULL;
    radius_passwd.pw_gid = (gid_t) strtoul(param, &endp, 10);

    if (radius_passwd.pw_gid == (gid_t) -1) {
      radius_log("illegal RadiusUserInfo GID value: -1 not allowed");
      have_illegal_value = TRUE;
    }

    if (endp && *endp) {
      radius_log("illegal RadiusUserInfo GID value: '%s' not a number", param);
      have_illegal_value = TRUE;
    }
  }

  /* Parse the home directory string. */
  param = (char *) c->argv[2];

  if (RADIUS_IS_VAR(param)) {
    radius_parse_var(param, &radius_home_attr_id, &radius_passwd.pw_dir);

    if (*radius_passwd.pw_dir != '/') {
      radius_log("illegal RadiusUserInfo default home value: '%s' "
        "not an absolute path", radius_passwd.pw_dir);
      have_illegal_value = TRUE;
    }

  } else

    /* Param already checked in this case. */
    radius_passwd.pw_dir = param;

  /* Process the shell string. */
  param = (char *) c->argv[3];
  
  if (RADIUS_IS_VAR(param)) {
    radius_parse_var(param, &radius_shell_attr_id, &radius_passwd.pw_shell);

    if (*radius_passwd.pw_shell != '/') {
      radius_log("illegal RadiusUserInfo default shell value: '%s' "
        "not an absolute path", radius_passwd.pw_shell);
      have_illegal_value = TRUE;
    }

  } else

    /* Param already checked in this case. */
    radius_passwd.pw_shell = param;

  if (!have_illegal_value)
    radius_have_user_info = TRUE;
  else
   radius_log("error with RadiusUserInfo parameters, ignoring them");
}

static unsigned char *radius_xor(unsigned char *p, unsigned char *q,
    size_t len) {
  register int i = 0;
  unsigned char *tmp = p;

  for (i = 0; i < len; i++)
    *(p++) ^= *(q++);

  return tmp;
}

/* Built-in MD5 */

/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991.
 *  All rights reserved.
 *
 * License to copy and use this software is granted provided that it
 * is identified as the "RSA Data Security, Inc. MD5 Message-Digest
 * Algorithm" in all material mentioning or referencing this software
 * or this function.
 *
 * License is also granted to make and use derivative works provided
 * that such works are identified as "derived from the RSA Data
 * Security, Inc. MD5 Message-Digest Algorithm" in all material
 * mentioning or referencing the derived work.
 *
 * RSA Data Security, Inc. makes no representations concerning either
 * the merchantability of this software or the suitability of this
 * software for any particular purpose. It is provided "as is"
 * without express or implied warranty of any kind.
 *
 * These notices must be retained in any copies of any part of this
 * documentation and/or software.
 */

/* MD5 context */
typedef struct {

  /* state (ABCD) */
  unsigned long state[4];

  /* number of bits, module 2^64 (LSB first) */
  unsigned long count[2];

  /* input buffer */
  unsigned char buffer[64];
} MD5_CTX;

static void MD5Init(MD5_CTX *);
static void MD5Update(MD5_CTX *, unsigned char *, unsigned int);
static void MD5Final(unsigned char[16], MD5_CTX *);

/* Note: these MD5 routines are taken from RFC 1321 */

#ifdef HAVE_MEMCPY
# define MD5_memcpy(a, b, c) memcpy((a), (b), (c))
# define MD5_memset(a, b, c) memset((a), (b), (c))
#endif

/* Constants for MD5Transform routine.
 */

#define S11 7
#define S12 12
#define S13 17
#define S14 22
#define S21 5
#define S22 9
#define S23 14
#define S24 20
#define S31 4
#define S32 11
#define S33 16
#define S34 23
#define S41 6
#define S42 10
#define S43 15
#define S44 21

static void MD5Transform(unsigned long[4], unsigned char[64]);
static void Encode(unsigned char *, unsigned long *, unsigned int);
static void Decode(unsigned long *, unsigned char *, unsigned int);

#ifndef HAVE_MEMCPY
static void MD5_memcpy(unsigned char *, unsigned char *, unsigned int);
static void MD5_memset(unsigned char *, int, unsigned int);
#endif

static unsigned char PADDING[64] = {
  0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

/* F, G, H and I are basic MD5 functions.
 */
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))

/* ROTATE_LEFT rotates x left n bits.
 */
#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))

/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
 * Rotation is separate from addition to prevent recomputation.
 */
#define FF(a, b, c, d, x, s, ac) { \
 (a) += F ((b), (c), (d)) + (x) + (unsigned long)(ac); \
 (a) = ROTATE_LEFT ((a), (s)); \
 (a) += (b); \
  }
#define GG(a, b, c, d, x, s, ac) { \
 (a) += G ((b), (c), (d)) + (x) + (unsigned long)(ac); \
 (a) = ROTATE_LEFT ((a), (s)); \
 (a) += (b); \
  }
#define HH(a, b, c, d, x, s, ac) { \
 (a) += H ((b), (c), (d)) + (x) + (unsigned long)(ac); \
 (a) = ROTATE_LEFT ((a), (s)); \
 (a) += (b); \
  }
#define II(a, b, c, d, x, s, ac) { \
 (a) += I ((b), (c), (d)) + (x) + (unsigned long)(ac); \
 (a) = ROTATE_LEFT ((a), (s)); \
 (a) += (b); \
  }

/* MD5 initialization. Begins an MD5 operation, writing a new context.
 */
static void MD5Init(MD5_CTX *context) {
  context->count[0] = context->count[1] = 0;

  /* Load magic initialization constants.
   */
  context->state[0] = 0x67452301;
  context->state[1] = 0xefcdab89;
  context->state[2] = 0x98badcfe;
  context->state[3] = 0x10325476;
}

/* MD5 block update operation. Continues an MD5 message-digest
 * operation, processing another message block, and updating the
 * context.
 */
static void MD5Update(MD5_CTX *context, unsigned char *input,
    unsigned int inputLen) {
  unsigned int i, index, partLen;

  /* Compute number of bytes mod 64 */
  index = (unsigned int)((context->count[0] >> 3) & 0x3F);

  /* Update number of bits */
  if ((context->count[0] += ((unsigned long)inputLen << 3))
       < ((unsigned long)inputLen << 3))
    context->count[1]++;
  context->count[1] += ((unsigned long)inputLen >> 29);

  partLen = 64 - index;

  /* Transform as many times as possible */
  if (inputLen >= partLen) {
    MD5_memcpy((unsigned char *) &context->buffer[index],
      (unsigned char *) input, partLen);
    MD5Transform(context->state, context->buffer);

    for (i = partLen; i + 63 < inputLen; i += 64)
      MD5Transform(context->state, &input[i]);

    index = 0;

  } else
    i = 0;

  /* Buffer remaining input */
  MD5_memcpy((unsigned char *) &context->buffer[index],
    (unsigned char *) &input[i], inputLen-i);
}

/* MD5 finalization. Ends an MD5 message-digest operation, writing the
 * the message digest and zeroizing the context.
 */
static void MD5Final(unsigned char digest[16], MD5_CTX *context) {
  unsigned char bits[8];
  unsigned int index, padLen;

  /* Save number of bits */
  Encode (bits, context->count, 8);

  /* Pad out to 56 mod 64.
   */
  index = (unsigned int) ((context->count[0] >> 3) & 0x3f);
  padLen = (index < 56) ? (56 - index) : (120 - index);
  MD5Update(context, PADDING, padLen);

  /* Append length (before padding) */
  MD5Update(context, bits, 8);

  /* Store state in digest */
  Encode(digest, context->state, 16);

  /* Zeroize sensitive information.
   */
  MD5_memset((unsigned char *) context, 0, sizeof(*context));
}

/* MD5 basic transformation. Transforms state based on block.
 */
static void MD5Transform(unsigned long state[4], unsigned char block[64]) {
  unsigned long a = state[0], b = state[1], c = state[2], d = state[3], x[16];

  Decode(x, block, 64);

  /* Round 1 */
  FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
  FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
  FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
  FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
  FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
  FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
  FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
  FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
  FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
  FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
  FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
  FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
  FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
  FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
  FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
  FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */

 /* Round 2 */
  GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
  GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
  GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
  GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
  GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
  GG (d, a, b, c, x[10], S22,  0x2441453); /* 22 */
  GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
  GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
  GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
  GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
  GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */

  GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
  GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
  GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
  GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
  GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */

  /* Round 3 */
  HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
  HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
  HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
  HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
  HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
  HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
  HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
  HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
  HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
  HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
  HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
  HH (b, c, d, a, x[ 6], S34,  0x4881d05); /* 44 */
  HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
  HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
  HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
  HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */

  /* Round 4 */
  II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
  II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
  II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
  II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
  II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
  II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
  II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
  II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
  II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
  II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
  II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
  II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
  II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
  II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
  II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
  II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */

  state[0] += a;
  state[1] += b;
  state[2] += c;
  state[3] += d;

  /* Zeroize sensitive information.
   */
  MD5_memset((unsigned char *) x, 0, sizeof(x));
}

/* Encodes input (unsigned long) into output (unsigned char). Assumes len is
 * a multiple of 4.
 */
static void Encode(unsigned char *output, unsigned long *input,
    unsigned int len) {
  unsigned int i, j;

  for (i = 0, j = 0; j < len; i++, j += 4) {
    output[j] = (unsigned char)(input[i] & 0xff);
    output[j+1] = (unsigned char)((input[i] >> 8) & 0xff);
    output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);
    output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);
  }
}

/* Decodes input (unsigned char) into output (unsigned long). Assumes len is
 * a multiple of 4.
 */
static void Decode(unsigned long *output, unsigned char *input,
    unsigned int len) {
  unsigned int i, j;

  for (i = 0, j = 0; j < len; i++, j += 4)
    output[i] = ((unsigned long)input[j]) | (((unsigned long)input[j+1]) << 8) |
    (((unsigned long)input[j+2]) << 16) | (((unsigned long)input[j+3]) << 24);
}

#ifndef HAVE_MEMCPY
/* Note: Replace "for loop" with standard memcpy if possible.
 */
static void MD5_memcpy(unsigned char *output, unsigned char *input,
    unsigned int len) {
  unsigned int i;

  for (i = 0; i < len; i++)
    output[i] = input[i];
}

/* Note: Replace "for loop" with standard memset if possible.
 */
static void MD5_memset(unsigned char *output, int value, unsigned int len) {
  unsigned int i;

  for (i = 0; i < len; i++)
    ((char *)output)[i] = (char)value;
}
#endif

/* Logging */

static int radius_closelog(void) {

  /* sanity check */
  if (radius_logfd != -1) {
    close(radius_logfd);
    radius_logfd = -1;
    radius_logname = NULL;
  }

  return 0;
}

static int radius_log(const char *fmt, ...) {
  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
  time_t timestamp = time(NULL);
  struct tm *t = NULL;
  va_list msg;

  /* sanity check */
  if (!radius_logname)
    return 0;

  t = localtime(&timestamp);

  /* prepend the timestamp */
  strftime(buf, sizeof(buf), "%b %d %H:%M:%S ", t);
  buf[sizeof(buf) - 1] = '\0';

  /* prepend a small header */
  snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
           MOD_RADIUS_VERSION "[%u]: ", (unsigned int) getpid());

  buf[sizeof(buf) - 1] = '\0';

  /* affix the message */
  va_start(msg, fmt);
  vsnprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), fmt, msg);
  va_end(msg);

  buf[sizeof(buf) - 1] = '\0';
  buf[strlen(buf)] = '\n';

  if (write(radius_logfd, buf, strlen(buf)) < 0)
    return -1;

  return 0;
}

static int radius_openlog(void) {
  int res = 0;

  /* Sanity checks */
  if ((radius_logname = (char *) get_param_ptr(main_server->conf,
      "RadiusLog", FALSE)) == NULL)
    return 0;

  if (!strcasecmp(radius_logname, "none")) {
    radius_logname = NULL;
    return 0;
  }

  pr_signals_block();
  PRIVS_ROOT
  res = pr_log_openfile(radius_logname, &radius_logfd, 0640);
  PRIVS_RELINQUISH
  pr_signals_unblock();

  return res;
}

/* RADIUS routines */

/* Add an attribute to a RADIUS packet. */
static void radius_add_attrib(radius_packet_t *packet, unsigned char type,
    const unsigned char *data, size_t datalen) {
  radius_attrib_t *attrib = NULL;

  attrib = (radius_attrib_t *) ((unsigned char *) packet +
    ntohs(packet->length));
  attrib->type = type;

  /* Total size of the attribute.  The "+ 2" takes into account the size
   * of the attribute identifier.
   */
  attrib->length = datalen + 2;

  /* Increment the size of the given packet. */
  packet->length = htons(ntohs(packet->length) + attrib->length);

  memcpy(attrib->data, data, datalen);
}

/* Add a RADIUS password attribute to the packet. */
static void radius_add_passwd(radius_packet_t *packet, unsigned char type,
    const char *passwd, char *secret) {

  MD5_CTX ctx, secret_ctx;
  radius_attrib_t *attrib = NULL;
  unsigned char calculated[RADIUS_VECTOR_LEN];
  char pwhash[256 + RADIUS_PASSWD_LEN];
  size_t pwlen = strlen(passwd);
  char *digest = NULL;
  register unsigned int i = 0;

  if (pwlen == 0) {
    pwlen = RADIUS_PASSWD_LEN;

  } if ((pwlen & (RADIUS_PASSWD_LEN - 1)) != 0) {

    /* Round up the length. */
    pwlen += (RADIUS_PASSWD_LEN - 1);

    /* Truncate the length, as necessary. */
    pwlen &= ~(RADIUS_PASSWD_LEN - 1);
  }

  /* Clear the buffers. */
  memset(pwhash, '\0', pwlen);
  memcpy(pwhash, passwd, pwlen);

  /* Find the password attribute. */
  attrib = radius_get_attrib(packet, RADIUS_PASSWORD);

  if (type == RADIUS_PASSWORD)
    digest = packet->digest;

  else
    digest = attrib->data;

  /* Encrypt the password.  Password: c[0] = p[0] ^ MD5(secret + digest) */
  MD5Init(&secret_ctx);
  MD5Update(&secret_ctx, secret, strlen(secret));

  /* Save this hash for later. */
  ctx = secret_ctx;

  MD5Update(&ctx, digest, RADIUS_VECTOR_LEN);

  /* Set the calculated digest. */
  MD5Final(calculated, &ctx);

  /* XOR the results. */
  radius_xor(pwhash, calculated, RADIUS_PASSWD_LEN);
  
  /* For each step through: e[i] = p[i] ^ MD5(secret + e[i-1]) */
  for (i = 1; i < (pwlen >> 4); i++) {

    /* Start with the old value of the MD5 sum. */
    ctx = secret_ctx;

    MD5Update(&ctx, &pwhash[(i-1) * RADIUS_PASSWD_LEN], RADIUS_PASSWD_LEN);

    /* Set the calculated digest. */
    MD5Final(calculated, &ctx);

    /* XOR the results. */
    radius_xor(&pwhash[i * RADIUS_PASSWD_LEN], calculated, RADIUS_PASSWD_LEN);
  }

  if (type == RADIUS_OLD_PASSWORD)
    attrib = radius_get_attrib(packet, RADIUS_OLD_PASSWORD);
  
  if (!attrib)
    radius_add_attrib(packet, type, pwhash, pwlen);

  else

    /* Overwrite the packet data. */
    memcpy(attrib->data, pwhash, pwlen);
}

static void radius_get_acct_digest(radius_packet_t *packet, char *secret) {
  MD5_CTX ctx;

  /* Clear the current digest (not needed yet for accounting packets) */
  memset(packet->digest, 0, RADIUS_VECTOR_LEN);

  MD5Init(&ctx);

  /* Add the packet data to the mix. */
  MD5Update(&ctx, (unsigned char *) packet, ntohs(packet->length));

  /* Add the secret to the mix. */
  MD5Update(&ctx, secret, strlen(secret));

  /* Set the calculated digest in place in the packet. */
  MD5Final(packet->digest, &ctx);
}

/* Obtain a random digest. */
static void radius_get_rnd_digest(radius_packet_t *packet) {
  MD5_CTX ctx;
  struct timeval tv;
  struct timezone tz;

  /* Use the time of day with the best resolution the system can give us,
   * often close to microsecond accuracy.
   */
  gettimeofday(&tv, &tz);

  /* Add in some (possibly) hard to guess information. */      
  tv.tv_sec ^= getpid() * getppid();
      
  /* Use MD5 to obtain (hopefully) cryptographically strong pseudo-random
   * numbers
   */
  MD5Init(&ctx);
  MD5Update(&ctx, (unsigned char *) &tv, sizeof(tv));
  MD5Update(&ctx, (unsigned char *) &tz, sizeof(tz));

  /* Set the calculated digest in the space provided. */
  MD5Final(packet->digest, &ctx);
}

/* RADIUS packet manipulation functions.
 */

/* Find an attribute in a RADIUS packet.  Note that the packet length
 * is always kept in network byte order.
 */
static radius_attrib_t *radius_get_attrib(radius_packet_t *packet,
    unsigned char type) {
  radius_attrib_t *attrib = (radius_attrib_t *) &packet->data;
  int len = ntohs(packet->length) - RADIUS_HEADER_LEN;

  while (attrib->type != type) {
    if (attrib->length == 0 ||
        (len -= attrib->length) <= 0) {

      /* Requested attribute not found. */
      return NULL;
    }

    /* Examine the next attribute in the packet. */
    attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
  }

  return attrib;
}

/* Find a Vendor-Specific Attribute (VSA) in a RADIUS packet.  Note that
 * the packet length is always kept in network byte order.
 *
 * In this first cut, the looked-for vendor is hardcoded to be Unix
 * (Vendor-Id of 4).
 */
static radius_attrib_t *radius_get_vendor_attrib(radius_packet_t *packet,
    unsigned char type) {
  radius_attrib_t *attrib = (radius_attrib_t *) &packet->data;
  int len = ntohs(packet->length) - RADIUS_HEADER_LEN;

  while (attrib) {
    unsigned int vendor_id = 0;
    radius_attrib_t *vsa = NULL;

    if (attrib->length <= 0)
      return NULL;

    if (attrib->type != RADIUS_VENDOR_SPECIFIC) {
      len -= attrib->length;
      attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
      continue;
    }

    /* The first four octets (bytes) of data will contain the Vendor-Id. */
    memcpy(&vendor_id, attrib->data, sizeof(unsigned int));
    vendor_id = ntohl(vendor_id);

    if (vendor_id != radius_vendor_id) {
      len -= attrib->length;
      attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
      continue;
    }

    /* Parse the data value for this attribute into a VSA structure. */
    vsa = (radius_attrib_t *) ((char *) attrib->data + sizeof(int));

    /* Does this VSA have the type requested? */
    if (vsa->type != type) {
      len -= attrib->length;
      attrib = (radius_attrib_t *) ((char *) attrib + attrib->length);
      continue;
    }

    /* Adjust the VSA length (I'm not sure why this is necessary, but a reading
     * of the FreeRADIUS sources show it to be.  Weird.)
     */
    vsa->length -= 2;
    return vsa;
  }

  return NULL;
}

/* Build a RADIUS packet, initializing some of the header and adding
 * common attributes.
 */
static void radius_build_packet(radius_packet_t *packet, const char *user,
    const char *passwd, char *secret) {
  unsigned int nas_port_type = htonl(RADIUS_NAS_PORT_TYPE_VIRTUAL);
  int nas_port = htonl(main_server->ServerPort);
  char *caller_id = NULL;

  /* Set the packet length. */
  packet->length = htons(RADIUS_HEADER_LEN);

  /* Obtain a random digest. */
  radius_get_rnd_digest(packet);

  /* Set the ID for the packet. */
  packet->id = packet->digest[0];
 
  /* Add the user attribute. */ 
  radius_add_attrib(packet, RADIUS_USER_NAME, user, strlen(user));

  /* Add the password attribute, if given. */
  if (passwd)
    radius_add_passwd(packet, RADIUS_PASSWORD, passwd, secret);

  else if (packet->code != RADIUS_ACCT_REQUEST)

    /* Add a NULL password if necessary. */
    radius_add_passwd(packet, RADIUS_PASSWORD, "", secret);

  /* Add a NAS identifier attribute of the service name: 'ftp'. */
  radius_add_attrib(packet, RADIUS_NAS_IDENTIFIER, "ftp", 3);

#ifndef PR_USE_IPV6
  /* Add a NAS IP address attribute. */
  radius_add_attrib(packet, RADIUS_NAS_IP_ADDRESS, (unsigned char *) &((struct in_addr *) pr_netaddr_get_inaddr(radius_local_addr))->s_addr,
    sizeof(((struct in_addr *) pr_netaddr_get_inaddr(radius_local_addr))->s_addr));
#endif /* PR_USE_IPV6 */

  /* Add a NAS port attribute. */
  radius_add_attrib(packet, RADIUS_NAS_PORT, (unsigned char *) &nas_port,
    sizeof(int));

  /* Add a NAS port type attribute. */ 
  radius_add_attrib(packet, RADIUS_NAS_PORT_TYPE,
    (unsigned char *) &nas_port_type, sizeof(int));

  /* Add the calling station ID attribute (this is the IP of the connecting
   * client).
   */
  caller_id = (char *) pr_netaddr_get_ipstr(radius_remote_addr); 

  radius_add_attrib(packet, RADIUS_CALLING_STATION_ID, caller_id,
    strlen(caller_id));
}

static radius_server_t *radius_make_server(pool *parent_pool) {
  pool *server_pool = NULL;
  radius_server_t *server = NULL;

  /* sanity check */
  if (!parent_pool)
    return NULL;

  /* allocate a subpool for the new rec */
  server_pool = make_sub_pool(parent_pool);

  /* allocate the rec from the subpool */
  server = (radius_server_t *) pcalloc(server_pool,
    sizeof(radius_server_t));

  /* set the members to sane default values */
  server->pool = server_pool;
  server->addr = NULL;
  server->port = RADIUS_AUTH_PORT;
  server->secret = NULL;
  server->timeout = DEFAULT_RADIUS_TIMEOUT;
  server->next = NULL;

  return server; 
}

static int radius_open_socket(void) {
  int sockfd = -1;
  struct sockaddr_in *radius_sockaddr_in = NULL;
  unsigned short local_port = 0;

  /* Obtain a socket descriptor. */
  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    radius_log("notice: unable to open socket for communication: %s",
      strerror(errno));
    return -1;
  }

  /* Set the members appropriately to bind to the descriptor. */
  memset((void *) &radius_local_sock, 0, sizeof(radius_local_sock));
  radius_sockaddr_in = (struct sockaddr_in *) &radius_local_sock;
  radius_sockaddr_in->sin_family = AF_INET;
  radius_sockaddr_in->sin_addr.s_addr = INADDR_ANY;

  /*  Use our process ID as a local port for RADIUS.
   */
  local_port = (getpid() & 0x7fff) + 1024;
  do {
    pr_signals_handle();

    local_port++;
    radius_sockaddr_in->sin_port = htons(local_port);

  } while ((bind(sockfd, &radius_local_sock, sizeof(radius_local_sock)) < 0) &&
    (local_port < USHRT_MAX));

  if (local_port >= USHRT_MAX) {
    close(sockfd);
    radius_log("notice: unable to bind to socket: no open local ports");
    return -1;
  }

  /* Done */
  return sockfd;
}

static int radius_close_socket(int sockfd) {
  return close(sockfd);
}

static radius_packet_t *radius_recv_packet(int sockfd, unsigned int timeout) {
  static unsigned char recvbuf[RADIUS_PACKET_LEN];
  radius_packet_t *packet = NULL;
  int res = 0, recvlen = -1, sockaddrlen = sizeof(struct sockaddr);
  struct timeval tv;
  fd_set rset;

  /* receive the response, waiting as necessary */
  memset(recvbuf, '\0', sizeof(recvbuf));

  tv.tv_sec = timeout;
  tv.tv_usec = 0;

  FD_ZERO(&rset);
  FD_SET(sockfd, &rset);

  res = select(sockfd + 1, &rset, NULL, NULL, &tv);

  if (res == 0) {
    radius_log("server failed to respond in %u seconds", timeout);
    return NULL;

  } else if (res < 0) {

    radius_log("error: unable to receive response: %s", strerror(errno));
    return NULL;
  }

  if ((recvlen = recvfrom(sockfd, (char *) recvbuf, RADIUS_PACKET_LEN,
      0, &radius_remote_sock, &sockaddrlen)) < 0) {
    radius_log("error reading packet: %s", strerror(errno));
    return NULL;
  }

  packet = (radius_packet_t *) recvbuf;

  /* Make sure the packet is of valid length. */
  if (ntohs(packet->length) != recvlen ||
      ntohs(packet->length) > RADIUS_PACKET_LEN) {
    radius_log("received corrupted packet");
    return NULL;
  }

  return packet;
}

static int radius_send_packet(int sockfd, radius_packet_t *packet,
    radius_server_t *server) {
  struct sockaddr_in *radius_sockaddr_in =
    (struct sockaddr_in *) &radius_remote_sock;

  /* Set the members appropriately to send to the descriptor. */
  memset((void *) &radius_remote_sock, '\0', sizeof(radius_remote_sock));
  radius_sockaddr_in->sin_family = AF_INET;
  radius_sockaddr_in->sin_addr.s_addr = pr_netaddr_get_addrno(server->addr);
  radius_sockaddr_in->sin_port = htons(server->port);

  if (sendto(sockfd, (char *) packet, ntohs(packet->length), 0, 
      &radius_remote_sock, sizeof(struct sockaddr_in)) < 0) {
    radius_log("error: unable to send packet: %s", strerror(errno));
    return -1;
  }

  radius_log("sending packet to %s:%u",
    inet_ntoa(radius_sockaddr_in->sin_addr),
    ntohs(radius_sockaddr_in->sin_port));

  return 0;
}


static unsigned char radius_start_accting(void) {
  int sockfd = -1, acct_status = 0, acct_authentic = 0;
  radius_packet_t *request = NULL, *response = NULL;
  radius_server_t *acct_server = NULL;
  unsigned char recvd_response = FALSE, *authenticated = NULL;

  /* Check to see if RADIUS accounting should be done. */
  if (!radius_engine || !radius_acct_server)
    return TRUE;

  /* Only do accounting for authenticated users. */
  authenticated = get_param_ptr(main_server->conf, "authenticated", FALSE);
  if (!authenticated || *authenticated == FALSE)
    return TRUE;

  /* Allocate a packet. */
  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));

  /* Open a RADIUS socket */
  sockfd = radius_open_socket();
  if (sockfd < 0) {
    radius_log("socket open failed");
    return FALSE;
  }

  /* Loop through the list of servers, trying each one until the packet is
   * successfully sent.
   */
  acct_server = radius_acct_server;

  while (acct_server) {
    char pid[10] = {'\0'};

    pr_signals_handle();

    /* Clear the packet. */
    memset(request, '\0', sizeof(radius_packet_t));

    /* Build the packet. */
    request->code = RADIUS_ACCT_REQUEST;
    radius_build_packet(request,
      radius_realm ? pstrcat(radius_pool, session.user, radius_realm, NULL) :
      session.user, NULL, acct_server->secret);

    radius_last_acct_pkt_id = request->id;

    /* Add accounting attributes. */
    acct_status = htonl(RADIUS_ACCT_STATUS_START);
    radius_add_attrib(request, RADIUS_ACCT_STATUS_TYPE,
      (unsigned char *) &acct_status, sizeof(int));

    snprintf(pid, sizeof(pid), "%08d", (int) getpid());
    radius_add_attrib(request, RADIUS_ACCT_SESSION_ID, pid, strlen(pid));

    acct_authentic = htonl(RADIUS_AUTH_LOCAL);
    radius_add_attrib(request, RADIUS_ACCT_AUTHENTIC,
      (unsigned char *) &acct_authentic, sizeof(int));

    /* Calculate the signature. */
    radius_get_acct_digest(request, acct_server->secret);

    /* Send the request. */
    radius_log("sending start acct request packet");
    if (radius_send_packet(sockfd, request, acct_server) < 0) {
      radius_log("packet send failed");
      acct_server = acct_server->next;
      continue;
    }

    /* Receive the response. */
    radius_log("receiving acct response packet");
    if ((response = radius_recv_packet(sockfd, acct_server->timeout)) == NULL) {
      radius_log("packet receive failed");
      acct_server = acct_server->next;
      continue;
    }

    radius_log("packet receive succeeded");
    recvd_response = TRUE;
    break;
  }

  /* Close the socket. */
  if (radius_close_socket(sockfd) < 0)
    radius_log("socket close failed");

  if (recvd_response) {

    /* Verify the response. */
    radius_log("verifying packet");
    if (radius_verify_packet(request, response, acct_server->secret) < 0)
      return FALSE;

    /* Handle the response. */
    switch (response->code) {
      case RADIUS_ACCT_RESPONSE:
        radius_log("accounting started for user '%s'", session.user);
        return TRUE;

      default:
        radius_log("notice: server returned unknown response code: %02x",
          response->code);
        return FALSE;
    }

  } else
    radius_log("error: no acct servers responded");

  /* Default return value. */
  return FALSE;
}

static unsigned char radius_stop_accting(void) {
  int sockfd = -1, acct_status = 0, acct_authentic = 0, now = 0;
  radius_packet_t *request = NULL, *response = NULL;
  radius_server_t *acct_server = NULL;
  unsigned char recvd_response = FALSE, *authenticated = NULL;

  /* Check to see if RADIUS accounting should be done. */
  if (!radius_engine || !radius_acct_server)
    return TRUE;

  /* Only do accounting for authenticated users. */
  authenticated = get_param_ptr(main_server->conf, "authenticated", FALSE);

  if (!authenticated || *authenticated == FALSE)
    return TRUE;

  /* Allocate a packet. */
  request = (radius_packet_t *) pcalloc(radius_pool, sizeof(radius_packet_t));

  /* Open a RADIUS socket */
  sockfd = radius_open_socket();
  if (sockfd < 0) {
    radius_log("socket open failed");
    return FALSE;
  }

  /* Loop through the list of servers, trying each one until the packet is
   * successfully sent.
   */
  acct_server = radius_acct_server;

  while (acct_server) {
    char pid[10] = {'\0'};

    pr_signals_handle();

    /* Clear the packet. */
    memset(request, '\0', sizeof(radius_packet_t));

    /* Build the packet. */
    request->code = RADIUS_ACCT_REQUEST;
    radius_build_packet(request,
      radius_realm ? pstrcat(radius_pool, session.user, radius_realm, NULL) :
      session.user, NULL, acct_server->secret);

    /* Use the ID of the last accounting packet sent, plus one.  Be sure
     * to handle the datatype overflow case.
     */
    if ((request->id = radius_last_acct_pkt_id + 1) == 0)
      request->id = 1;

    /* Add accounting attributes. */
    acct_status = htonl(RADIUS_ACCT_STATUS_STOP);
    radius_add_attrib(request, RADIUS_ACCT_STATUS_TYPE,
      (unsigned char *) &acct_status, sizeof(int));

    snprintf(pid, sizeof(pid), "%08d", (int) getpid());
    radius_add_attrib(request, RADIUS_ACCT_SESSION_ID, pid, strlen(pid));

    acct_authentic = htonl(RADIUS_AUTH_LOCAL);
    radius_add_attrib(request, RADIUS_ACCT_AUTHENTIC,
      (unsigned char *) &acct_authentic, sizeof(int));

    now = htonl(time(NULL) - radius_session_start);
    radius_add_attrib(request, RADIUS_ACCT_SESSION_TIME,
      (unsigned char *) &now, sizeof(int));

    radius_session_bytes_in = htonl(radius_session_bytes_in);
    radius_add_attrib(request, RADIUS_ACCT_INPUT_OCTETS,
      (unsigned char *) &radius_session_bytes_in, sizeof(int));

    radius_session_bytes_out = htonl(radius_session_bytes_out);
    radius_add_attrib(request, RADIUS_ACCT_OUTPUT_OCTETS,
      (unsigned char *) &radius_session_bytes_out, sizeof(int));

    /* Calculate the signature. */
    radius_get_acct_digest(request, acct_server->secret);

    /* Send the request. */
    radius_log("sending stop acct request packet");
    if (radius_send_packet(sockfd, request, acct_server) < 0) {
      radius_log("packet send failed");
      return FALSE;
    }

    /* Receive the response. */
    radius_log("receiving acct response packet");
    if ((response = radius_recv_packet(sockfd, acct_server->timeout)) == NULL) {
      radius_log("packet receive failed");
      return FALSE;
    }

    radius_log("packet receive succeeded");
    recvd_response = TRUE;
    break;
  }

  /* Close the socket. */
  if (radius_close_socket(sockfd) < 0)
    radius_log("socket close failed");

  if (recvd_response) {

    /* Verify the response. */
    radius_log("verifying packet");
    if (radius_verify_packet(request, response, acct_server->secret) < 0)
      return FALSE;

    /* Handle the response. */
    switch (response->code) {
      case RADIUS_ACCT_RESPONSE:
        radius_log("accounting ended for user '%s'", session.user);
        return TRUE;

      default:
        radius_log("notice: server returned unknown response code: %02x",
          response->code);
        return FALSE;
    }

  } else 
    radius_log("error: no acct servers responded");

  /* Default return value. */
  return FALSE;
}

/* Verify the response packet from the server. */
static int radius_verify_packet(radius_packet_t *req_packet, 
    radius_packet_t *resp_packet, char *secret) {
  MD5_CTX ctx;
  unsigned char calculated[RADIUS_VECTOR_LEN] = {'\0'};
  unsigned char replied[RADIUS_VECTOR_LEN] = {'\0'};

  /* sanity check */
  if (!req_packet || !resp_packet || !secret) {
    errno = EINVAL;
    return -1;
  }

  /* NOTE: add checks for too-big, too-small packets, invalid packet->length
   * values, etc.
   */

  /* Check that the packet IDs match. */
  if (resp_packet->id != req_packet->id) {
    radius_log("packet verification failed: response packet ID %d does not "
      "match the request packet ID %d", resp_packet->id, req_packet->id);
    return -1;
  }

  /* Make sure the buffers are void of junk values. */
  memset(calculated, '\0', sizeof(calculated));
  memset(replied, '\0', sizeof(replied));

  /* Save a copy of the response's digest. */
  memcpy(replied, resp_packet->digest, RADIUS_VECTOR_LEN);

  /* Copy in the digest sent in the request. */
  memcpy(resp_packet->digest, req_packet->digest, RADIUS_VECTOR_LEN);

  /* Re-calculate a digest from the given packet, and compare it against
   * the provided response digest:
   *   MD5(response packet header + digest + response packet data + secret)
   */
  MD5Init(&ctx);
  MD5Update(&ctx, (unsigned char *) resp_packet, ntohs(resp_packet->length));

  if (*secret)
    MD5Update(&ctx, secret, strlen(secret));

  /* Set the calculated digest. */
  MD5Final(calculated, &ctx);

  /* Do the digests match properly? */
  if (memcmp(calculated, replied, RADIUS_VECTOR_LEN) != 0) {
    radius_log("packet verification failed: mismatched digests");
    return -1;
  }

  return 0;
}

/* Authentication handlers
 */

MODRET radius_auth(cmd_rec *cmd) {

  /* This authentication check has already been performed; I just need
   * to report the results of that check now.
   */
  if (radius_auth_ok)
    return HANDLED(cmd);

  else if (radius_auth_reject)
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  /* Default return value. */
  return DECLINED(cmd);
}

MODRET radius_check(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_name2uid(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_name2gid(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_uid2name(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_gid2name(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_endgrent(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_getgrnam(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_getgrent(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_getgrgid(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_getgroups(cmd_rec *cmd) {

  if (radius_have_group_info) {
    array_header *gids = NULL, *groups = NULL;
    register unsigned int i = 0;

    /* Return the faked group information. */

    /* Don't forget to include the primary GID (with accompanying name!)
     * in the returned info -- if provided.  Otherwise, well...the user
     * is out of luck.
     */

    /* Check for NULL values */
    if (cmd->argv[1]) {
      gids = (array_header *) cmd->argv[1];

      if (radius_have_user_info)
         *((gid_t *) push_array(gids)) = radius_passwd.pw_gid;

      for (i = 0; i < radius_addl_group_count; i++)
        *((gid_t *) push_array(gids)) = radius_addl_group_ids[i];
    }

    if (cmd->argv[2]) {
      groups = (array_header *) cmd->argv[2];

      if (radius_have_user_info)
        *((char **) push_array(groups)) = radius_prime_group_name;

      for (i = 0; i < radius_addl_group_count; i++)
        *((char **) push_array(groups)) = radius_addl_group_names[i];
    }

    /* Increment this count, for the sake of proper reporting back to the
     * getgroups() caller.
     */
    if (radius_have_user_info)
      radius_addl_group_count++;

    return mod_create_data(cmd, (void *) &radius_addl_group_count);
  }

  return DECLINED(cmd);
}

MODRET radius_setgrent(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_endpwent(cmd_rec *cmd) {
  return DECLINED(cmd);
}

MODRET radius_getpwnam(cmd_rec *cmd) {

  if (radius_have_user_info) {

    if (!radius_passwd.pw_name)
      radius_passwd.pw_name = pstrdup(radius_pool, cmd->argv[0]);

    if (!strcmp(cmd->argv[0], radius_passwd.pw_name))

      /* Return the faked user information. */
      return mod_create_data(cmd, &radius_passwd);
  }

  /* Default response */
  return DECLINED(cmd);
}

MODRET radius_getpwent(cmd_rec *cmd) {

  if (radius_have_user_info)

    /* Return the faked user information. */
    return mod_create_data(cmd, &radius_passwd);

  /* Default response */
  return DECLINED(cmd);
}

MODRET radius_getpwuid(cmd_rec *cmd) {

  if (radius_have_user_info)

    /* Check that given UID matches faked UID before returning. */
    if (*((uid_t *) cmd->argv[0]) == radius_passwd.pw_uid)

      /* Return the faked user information. */
      return mod_create_data(cmd, &radius_passwd);

  /* Default response */
  return DECLINED(cmd);
}

MODRET radius_setpwent(cmd_rec *cmd) {
  return DECLINED(cmd);
}

/* Command handlers
 */

/* Perform the check with the RADIUS auth server(s) now, prior to the
 * actual handling of the PASS command by mod_auth, so that any of the
 * RadiusUserInfo parameters can be supplied by the RADIUS server.
 *
 * NOTE: first draft, does not honor UserAlias'd names (eg it uses the
 * username as supplied by the client.
 */
MODRET radius_pre_pass(cmd_rec *cmd) {
  int sockfd = -1;
  radius_packet_t *request = NULL, *response = NULL;
  radius_server_t *auth_server = NULL;
  unsigned char recvd_response = FALSE;
  unsigned long service;
  char *user = (char *) get_param_ptr(cmd->server->conf, C_USER, FALSE);

  /* Check to see whether RADIUS authentication should even be done. */
  if (!radius_engine || !radius_auth_server)
    return DECLINED(cmd);

  /* Allocate a packet. */
  request = (radius_packet_t *) pcalloc(cmd->tmp_pool,
    sizeof(radius_packet_t));

  /* Open a RADIUS socket */
  sockfd = radius_open_socket();
  if (sockfd < 0) {
    radius_log("socket open failed");
    return DECLINED(cmd);
  }

  /* Clear the OK flag. */
  radius_auth_ok = FALSE;

  /* If mod_radius expects to find VSAs in the returned packet, it needs
   * to send a service type of Login, otherwise, use the Authenticate-Only
   * service type.
   */
  if (radius_have_user_info ||
      radius_have_group_info ||
      radius_have_other_info)
    service = htonl(RADIUS_SVC_LOGIN);

  else
    service = htonl(RADIUS_SVC_AUTHENTICATE_ONLY);

  /* Loop through the list of servers, trying each one until the packet is
   * successfully sent.
   */
  auth_server = radius_auth_server;

  while (auth_server) {
    pr_signals_handle();

    /* Clear the packet. */
    memset(request, '\0', sizeof(radius_packet_t));

    /* Build the packet. */
    request->code = RADIUS_AUTH_REQUEST;
    radius_build_packet(request, radius_realm ?
      pstrcat(radius_pool, user, radius_realm, NULL) : user, cmd->arg,
      auth_server->secret);

    radius_add_attrib(request, RADIUS_SERVICE_TYPE, (unsigned char *) &service,
      sizeof(service));

    /* Send the request. */
    radius_log("sending auth request packet");
    if (radius_send_packet(sockfd, request, auth_server) < 0) {
      radius_log("packet send failed");
      auth_server = auth_server->next;
      continue;
    }

    /* Receive the response. */
    radius_log("receiving auth response packet");
    if ((response = radius_recv_packet(sockfd, auth_server->timeout)) == NULL) {
      radius_log("packet receive failed");
      auth_server = auth_server->next;
      continue;
    }

    radius_log("packet receive succeeded");
    recvd_response = TRUE;
    break;
  }

  /* Close the socket. */
  if (radius_close_socket(sockfd) < 0)
    radius_log("socket close failed");

  if (recvd_response) {

    /* Verify the response. */
    radius_log("verifying packet");
    if (radius_verify_packet(request, response, auth_server->secret) < 0)
      return DECLINED(cmd);

    /* Handle the response */
    switch (response->code) {
      case RADIUS_AUTH_ACCEPT:
        radius_log("authentication successful for user '%s'", user);

        radius_session_authtype = htonl(RADIUS_AUTH_RADIUS);

        /* Process the packet for custom attributes */
        radius_process_accpt_packet(response);

        radius_auth_ok = TRUE;
        break;

      case RADIUS_AUTH_REJECT:
        radius_log("authentication failed for user '%s'", user);
        radius_auth_ok = FALSE;
        radius_auth_reject = TRUE;
        break;

      case RADIUS_AUTH_CHALLENGE:

        /* Just log this case for now. */
        radius_log("authentication challenged for user '%s'", user);
        break;

      default:
        radius_log("notice: server returned unknown response code: %02x",
          response->code);
        break;
    }

  } else
    radius_log("error: no auth servers responded");

  return DECLINED(cmd);
}

MODRET radius_post_pass(cmd_rec *cmd) {

  /* Check to see if RADIUS accounting should be done. */
  if (!radius_engine || !radius_acct_server)
    return DECLINED(cmd);

  /* Fill in the username in the faked user info, if need be. */
  if (radius_have_user_info)
    radius_passwd.pw_name = session.user;

  if (!radius_start_accting())
    radius_log("error: unable to start accounting");

  return DECLINED(cmd);
}

MODRET radius_post_retr(cmd_rec *cmd) {
  radius_session_bytes_out += session.xfer.total_bytes;
  return DECLINED(cmd);
}

MODRET radius_post_stor(cmd_rec *cmd) {
  radius_session_bytes_in += session.xfer.total_bytes;
  return DECLINED(cmd);
}

/* Configuration handlers
 */

/* usage: RadiusAcctServer server[:port] shared-secret [timeout] */
MODRET set_radiusacctserver(cmd_rec *cmd) {
  config_rec *c = NULL;
  radius_server_t *radius_server = NULL;
  unsigned short server_port = 0;
  char *port = NULL;

  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
    CONF_ERROR(cmd, "missing parameters");

  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  /* Check to see if there's a port specified in the server name */
  if ((port = strchr(cmd->argv[1], ':')) != NULL) {

    /* Separate the server name from the port */
    *(port++) = '\0';

    if ((server_port = (unsigned short) atoi(port)) < 1024)
      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "port number must be greater "
        "than 1023", NULL));
  }

  if (pr_netaddr_get_addr(cmd->tmp_pool, cmd->argv[1], NULL) == NULL)
    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to resolve server address: ",
      cmd->argv[1], NULL));

  /* Allocate a RADIUS server rec and populate the members */
  radius_server = radius_make_server(radius_pool);

  radius_server->addr = pr_netaddr_get_addr(radius_server->pool, cmd->argv[1],
    NULL);
  radius_server->port = (server_port ? server_port : RADIUS_ACCT_PORT);
  radius_server->secret = pstrdup(radius_server->pool, cmd->argv[2]);

  if (cmd->argc-1 == 3) 
    if ((radius_server->timeout = atoi(cmd->argv[3])) < 0)
      CONF_ERROR(cmd, "timeout must be greater than or equal to zero");

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(radius_server_t *));
  *((radius_server_t **) c->argv[0]) = radius_server;

  return HANDLED(cmd);
}

/* usage: RadiusAuthServer server[:port] <shared-secret> [timeout] */
MODRET set_radiusauthserver(cmd_rec *cmd) {
  config_rec *c = NULL;
  radius_server_t *radius_server = NULL;
  unsigned short server_port = 0;
  char *port = NULL;

  if (cmd->argc-1 < 2 || cmd->argc-1 > 3)
    CONF_ERROR(cmd, "missing parameters");

  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  /* Check to see if there's a port specified in the server name */
  if ((port = strchr(cmd->argv[1], ':')) != NULL) {

    /* Separate the server name from the port */
    *(port++) = '\0';

    if ((server_port = (unsigned short) atoi(port)) < 1024)
      CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "port number must be greater "
        "than 1023", NULL));
  }

  if (pr_netaddr_get_addr(cmd->tmp_pool, cmd->argv[1], NULL) == NULL)
    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable resolve server address: ",
      cmd->argv[1], NULL));

  /* OK, allocate a RADIUS server rec and populate the members */
  radius_server = radius_make_server(radius_pool);

  radius_server->addr = pr_netaddr_get_addr(radius_server->pool, cmd->argv[1],
    NULL);
  radius_server->port = (server_port ? server_port : RADIUS_AUTH_PORT);
  radius_server->secret = pstrdup(radius_server->pool, cmd->argv[2]);

  if (cmd->argc-1 == 3)
    if ((radius_server->timeout = atoi(cmd->argv[3])) < 0)
      CONF_ERROR(cmd, "timeout must be greater than or equal to zero");

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(radius_server_t *));
  *((radius_server_t **) c->argv[0]) = radius_server;

  return HANDLED(cmd);
}

/* usage: RadiusEngine on|off */
MODRET set_radiusengine(cmd_rec *cmd) {
  int bool = -1;
  config_rec *c = NULL;

  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  if ((bool = get_boolean(cmd, 1)) == -1)
    CONF_ERROR(cmd, "expected Boolean parameter");

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(int));
  *((int *) c->argv[0]) = bool;

  return HANDLED(cmd);
}

/* usage: RadiusGroupInfo primary-name addl-names add-ids */
MODRET set_radiusgroupinfo(cmd_rec *cmd) {
  config_rec *c = NULL;
  unsigned char group_names_vsa = FALSE;
  unsigned char group_ids_vsa = FALSE;

  CHECK_ARGS(cmd, 3);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  group_names_vsa = radius_chk_var(cmd->argv[2]);
  group_ids_vsa = radius_chk_var(cmd->argv[3]);

  if ((group_names_vsa && !group_ids_vsa) ||
      (!group_names_vsa && group_ids_vsa))
    CONF_ERROR(cmd, "supplemental group names/IDs parameters must both either be VSA variables, or both not be variables");

  /* There will be four parameters to this config_rec:
   *
   *  primary-group-name, addl-group-count, addl-group-names, addl-group-ids
   */
  c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
  c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
  c->argv[1] = pcalloc(c->pool, sizeof(unsigned int));

  if (group_names_vsa && group_ids_vsa) {
    
    /* As VSA variables, the group names and IDs won't be resolved until
     * session time, so just store the variable strings as is.
     */
    *((unsigned int *) c->argv[1]) = 0;
    c->argv[2] = pstrdup(c->pool, cmd->argv[2]);
    c->argv[3] = pstrdup(c->pool, cmd->argv[3]);

  } else {
    unsigned int ngroups = 0, ngids = 0;
    char **groups = NULL;
    gid_t *gids = NULL;

    if (!radius_parse_groups_str(c->pool, cmd->argv[2], &groups, &ngroups))
      CONF_ERROR(cmd, "badly formatted group names");

    if (!radius_parse_gids_str(c->pool, cmd->argv[3], &gids, &ngids))
      CONF_ERROR(cmd, "badly formatted group IDs");

    if (ngroups != ngids)
      CONF_ERROR(cmd, "mismatched number of group names and IDs");

    *((unsigned int *) c->argv[1]) = ngroups;
    c->argv[2] = (void *) groups;
    c->argv[3] = (void *) gids;
  }

  return HANDLED(cmd);
}

/* usage: RadiusLog file|"none" */
MODRET set_radiuslog(cmd_rec *cmd) {
  if (cmd->argc-1 != 1)
    CONF_ERROR(cmd, "wrong number of parameters");
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
  return HANDLED(cmd);
}

/* usage: RadiusRealm string */
MODRET set_radiusrealm(cmd_rec *cmd) {
  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
  return HANDLED(cmd);
}

/* usage: RadiusUserInfo uid gid home shell */
MODRET set_radiususerinfo(cmd_rec *cmd) {
  CHECK_ARGS(cmd, 4);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  if (!radius_chk_var(cmd->argv[1])) {
    char *endp = NULL;

    /* Make sure it's a number, at least. */
    if (strtoul(cmd->argv[1], &endp, 10) < 0)
      CONF_ERROR(cmd, "negative UID not allowed");

    if (endp && *endp)
      CONF_ERROR(cmd, "invalid UID parameter: not a number");
  }

  if (!radius_chk_var(cmd->argv[2])) {
    char *endp = NULL;

    /* Make sure it's a number, at least. */
    if (strtoul(cmd->argv[2], &endp, 10) < 0)
      CONF_ERROR(cmd, "negative GID not allowed");

    if (endp && *endp)
      CONF_ERROR(cmd, "invalid GID parameter: not a number");
  } 

  if (!radius_chk_var(cmd->argv[3])) {

    /* Make sure the path is absolute, at least. */
    if (*(cmd->argv[3]) != '/')
      CONF_ERROR(cmd, "home relative path not allowed");
  }

  if (!radius_chk_var(cmd->argv[4])) {

    /* Make sure the path is absolute, at least. */
    if (*(cmd->argv[4]) != '/')
      CONF_ERROR(cmd, "shell relative path not allowed");
  }

  add_config_param_str(cmd->argv[0], 4, cmd->argv[1], cmd->argv[2],
    cmd->argv[3], cmd->argv[4]);

  return HANDLED(cmd);
}

/* usage: RadiusVendor name id */
MODRET set_radiusvendor(cmd_rec *cmd) {
  config_rec *c = NULL;
  long id = 0;
  char *tmp = NULL;

  CHECK_ARGS(cmd, 2);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  /* Make sure that the given vendor ID number is valid. */
  id = strtol(cmd->argv[2], &tmp, 10);

  if (tmp && *tmp)
    CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": vendor id '", cmd->argv[2],
      "' is not a valid number", NULL));

  if (id < 0)
    CONF_ERROR(cmd, "vendor id must be a positive number");

  c = add_config_param(cmd->argv[0], 2, NULL, NULL);
  c->argv[0] = pstrdup(c->pool, cmd->argv[1]);
  c->argv[1] = pcalloc(c->pool, sizeof(unsigned int));
  *((unsigned int *) c->argv[1]) = id;

  return HANDLED(cmd);
}

/* Event handlers
 */

static void radius_exit_ev(const void *event_data, void *user_data) {

  if (!radius_stop_accting())
    radius_log("error: unable to stop accounting");

  radius_closelog();
  return;
}

#if defined(PR_SHARED_MODULE)
static void radius_mod_unload_ev(const void *event_data, void *user_data) {
  if (strcmp("mod_radius.c", (const char *) event_data) == 0) {
    pr_event_unregister(&radius_module, NULL, NULL);

    if (radius_pool) {
      destroy_pool(radius_pool);
      radius_pool = NULL;
    }
  }
}
#endif /* PR_SHARED_MODULE */

static void radius_restart_ev(const void *event_data, void *user_data) {

  /* Re-allocate the pool used by this module. */
  if (radius_pool)
    destroy_pool(radius_pool);

  radius_pool = make_sub_pool(permanent_pool);
  pr_pool_tag(radius_pool, MOD_RADIUS_VERSION);

  return;
}

/* Initialization routines
 */

static int radius_sess_init(void) {
  int res = 0;
  config_rec *c = NULL;
  radius_server_t **current_server = NULL;

  res = radius_openlog();
  if (res < 0) {
    if (res == -1)
      pr_log_pri(PR_LOG_NOTICE, "notice: unable to open RadiusLog: %s",
        strerror(errno));

    else if (res == PR_LOG_WRITABLE_DIR)
      pr_log_pri(PR_LOG_NOTICE, "notice: unable to open RadiusLog: "
          "parent directory is world writeable");

    else if (res == PR_LOG_SYMLINK)
      pr_log_pri(PR_LOG_NOTICE, "notice: unable to open RadiusLog: "
          "cannot log to a symbolic link");
  }

  /* Is RadiusEngine on? */
  radius_engine = FALSE;
  if ((c = find_config(main_server->conf, CONF_PARAM, "RadiusEngine",
      FALSE)) != NULL) {

    if (*((int *) c->argv[0]) == TRUE)
      radius_engine = TRUE;
  }

  if (!radius_engine) {
    radius_log("RadiusEngine not enabled");
    radius_closelog();
    return 0;
  }

  /* Initialize session variables */
  time(&radius_session_start);
  radius_session_bytes_in = 0;
  radius_session_bytes_out = 0;

  /* Make a copy of netaddrs for later. */
  radius_local_addr = pr_netaddr_dup(radius_pool, session.c->local_addr);
  radius_remote_addr = pr_netaddr_dup(radius_pool, session.c->remote_addr);

  if ((c = find_config(main_server->conf, CONF_PARAM, "RadiusVendor",
      FALSE)) != NULL) {
    radius_vendor_name = c->argv[0];
    radius_vendor_id = *((unsigned int *) c->argv[1]);

    radius_log("RadiusVendor '%s' (Vendor-Id %u) configured",
      radius_vendor_name, radius_vendor_id);
  }

  /* Find any configured RADIUS servers for this session */
  c = find_config(main_server->conf, CONF_PARAM, "RadiusAcctServer", FALSE);

  /* Point to the start of the accounting server list. */
  current_server = &radius_acct_server;

  while (c) {
    *current_server = *((radius_server_t **) c->argv[0]);
    current_server = &(*current_server)->next;

    c = find_config_next(c, c->next, CONF_PARAM, "RadiusAcctServer", FALSE);
  }

  if (!radius_acct_server)
    radius_log("notice: no configured RadiusAcctServers, no accounting");

  c = find_config(main_server->conf, CONF_PARAM, "RadiusAuthServer", FALSE);

  /* Point to the start of the authentication server list. */
  current_server = &radius_auth_server;

  while (c) {
    *current_server = *((radius_server_t **) c->argv[0]);
    current_server = &(*current_server)->next;

    c = find_config_next(c, c->next, CONF_PARAM, "RadiusAuthServer", FALSE);
  }

  if (!radius_auth_server)
    radius_log("notice: no configured RadiusAuthServers, no authentication");

  /* Prepare any configured fake user information. */
  if ((c = find_config(main_server->conf, CONF_PARAM, "RadiusUserInfo",
      FALSE)) != NULL) {

    /* Process the parameter string stored in the found config_rec. */
    radius_process_user_info(c);

    /* Only use the faked information if authentication via RADIUS is
     * possible.  The radius_have_user_info flag will be set to
     * TRUE by radius_process_user_info(), unless there was some
     * illegal value.
     */
    if (!radius_auth_server)
      radius_have_user_info = FALSE;
  }

  /* Prepare any configured fake group information. */
  if ((c = find_config(main_server->conf, CONF_PARAM, "RadiusGroupInfo",
      FALSE)) != NULL) {

    /* Process the parameter string stored in the found config_rec. */
    radius_process_group_info(c);

    /* Only use the faked information if authentication via RADIUS is
     * possible.  The radius_have_group_info flag will be set to
     * TRUE by radius_process_group_info(), unless there was some
     * illegal value.
     */
    if (!radius_auth_server)
      radius_have_group_info = FALSE;
  }

  /* Check for a configured RadiusRealm.  If present, use username + realm
   * in RADIUS packets as the user name, else just use the username.
   */
  if ((radius_realm = get_param_ptr(main_server->conf, "RadiusRealm",
      FALSE)) != NULL)
    radius_log("using RadiusRealm '%s'", radius_realm);

  pr_event_register(&radius_module, "core.exit", radius_exit_ev, NULL);
  return 0;
}

static int radius_init(void) {

  /* Allocate a pool for this module's use. */
  radius_pool = make_sub_pool(permanent_pool);
  pr_pool_tag(radius_pool, MOD_RADIUS_VERSION);

#if defined(PR_SHARED_MODULE)
  pr_event_register(&radius_module, "core.module-unload",
    radius_mod_unload_ev, NULL);
#endif /* PR_SHARED_MODULE */

  /* Register a restart handler, to cleanup the pool. */
  pr_event_register(&radius_module, "core.restart", radius_restart_ev, NULL);

  return 0;
}

/* Module API tables
 */

static conftable radius_conftab[] = {
  { "RadiusAcctServer", 	set_radiusacctserver,	NULL },
  { "RadiusAuthServer", 	set_radiusauthserver,	NULL },
  { "RadiusEngine",		set_radiusengine,	NULL },
  { "RadiusGroupInfo",		set_radiusgroupinfo,	NULL },
  { "RadiusLog",		set_radiuslog,		NULL },
  { "RadiusRealm",		set_radiusrealm,	NULL },
  { "RadiusUserInfo",		set_radiususerinfo,	NULL },
  { "RadiusVendor",		set_radiusvendor,	NULL },
  { NULL }
};

static cmdtable radius_cmdtab[] = {
  { POST_CMD,		C_APPE,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { POST_CMD_ERR,	C_APPE,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { PRE_CMD,		C_PASS, G_NONE, radius_pre_pass,	FALSE, FALSE, CL_AUTH },
  { POST_CMD,		C_PASS, G_NONE, radius_post_pass, 	FALSE, FALSE, CL_AUTH },
  { POST_CMD,		C_RETR,	G_NONE,	radius_post_retr,	FALSE, FALSE },
  { POST_CMD_ERR,	C_RETR,	G_NONE,	radius_post_retr,	FALSE, FALSE },
  { POST_CMD,		C_STOR,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { POST_CMD_ERR,	C_STOR,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { POST_CMD,		C_STOU,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { POST_CMD_ERR,	C_STOU,	G_NONE,	radius_post_stor,	FALSE, FALSE },
  { 0, NULL }
};

static authtable radius_authtab[] = {
  { 0, "setpwent",  radius_setpwent },
  { 0, "setgrent",  radius_setgrent },
  { 0, "endpwent",  radius_endpwent },
  { 0, "endgrent",  radius_endgrent },
  { 0, "getpwent",  radius_getpwent },
  { 0, "getgrent",  radius_getgrent },
  { 0, "getpwnam",  radius_getpwnam },
  { 0, "getgrnam",  radius_getgrnam },
  { 0, "getpwuid",  radius_getpwuid },
  { 0, "getgrgid",  radius_getgrgid },
  { 0, "getgroups", radius_getgroups },
  { 0, "auth",      radius_auth     },
  { 0, "check",     radius_check    },
  { 0, "uid2name",  radius_uid2name },
  { 0, "gid2name",  radius_gid2name },
  { 0, "name2uid",  radius_name2uid },
  { 0, "name2gid",  radius_name2gid },
  { 0, NULL }
};

module radius_module = {

  /* Always NULL */
  NULL, NULL,

  /* Module API version 2.0 */
  0x20,

  /* Module name */
  "radius",

  /* Module configuration handler table */
  radius_conftab,

  /* Module command handler table */
  radius_cmdtab,

  /* Module authentication handler table */
  radius_authtab,

  /* Module initialization function */
  radius_init,

  /* Module session initialization function */
  radius_sess_init,

  /* Module version */
  MOD_RADIUS_VERSION
};
