/*
 * ProFTPD: mod_sql -- SQL frontend
 * Copyright (c) 1998-1999 Johnie Ingram.
 * Copyright (c) 2001 Andrew Houghton.
 *  
 * 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, Andrew Houghton and other respective copyright
 * holders give permission to link this program with OpenSSL, and distribute
 * the resulting executable, without including the source code for OpenSSL in
 * the source distribution.
 *
 * $Id: mod_sql.c,v 1.80 2004/08/03 00:44:31 castaglia Exp $
 */

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

#define _MOD_VERSION "mod_sql/4.11"

#if defined(HAVE_CRYPT_H) && !defined(AIX4) && !defined(AIX5)
# include <crypt.h>
#endif

#ifdef HAVE_LIMITS_H
# include <limits.h>
#endif

/* Uncomment the following define to allow OpenSSL hashed password checking;  
 * you'll also need to link with OpenSSL's crypto library ( -lcrypto ) 
 */
/* #define HAVE_OPENSSL */

#ifdef HAVE_OPENSSL
# include <openssl/evp.h>
#endif

/* default information for tables and fields */
#define MOD_SQL_DEF_USERTABLE         "users"
#define MOD_SQL_DEF_USERNAMEFIELD     "userid"
#define MOD_SQL_DEF_USERUIDFIELD      "uid"
#define MOD_SQL_DEF_USERGIDFIELD      "gid"
#define MOD_SQL_DEF_USERPASSWORDFIELD "passwd"
#define MOD_SQL_DEF_USERSHELLFIELD    "shell"
#define MOD_SQL_DEF_USERHOMEDIRFIELD  "homedir"

#define MOD_SQL_DEF_GROUPTABLE        "groups"
#define MOD_SQL_DEF_GROUPNAMEFIELD    "groupname"
#define MOD_SQL_DEF_GROUPGIDFIELD     "gid"
#define MOD_SQL_DEF_GROUPMEMBERSFIELD "members"

/* default minimum id / default uid / default gid info. 
 * uids and gids less than MOD_SQL_MIN_USER_UID and
 * MOD_SQL_MIN_USER_GID, respectively, get automatically
 * mapped to the defaults, below.  These can be
 * overridden using directives
 */
#define MOD_SQL_MIN_USER_UID 999
#define MOD_SQL_MIN_USER_GID 999
#define MOD_SQL_DEF_UID 65533
#define MOD_SQL_DEF_GID 65533

#define MOD_SQL_BUFSIZE 32

/* Named Query defines */
#define SQL_SELECT_C "SELECT"
#define SQL_INSERT_C "INSERT"
#define SQL_UPDATE_C "UPDATE"
#define SQL_FREEFORM_C "FREEFORM"

/* authmask defines */
#define SQL_AUTH_USERS             (1<<0)
#define SQL_AUTH_GROUPS            (1<<1)
#define SQL_AUTH_USERS_DEFINITIVE  (1<<2)
#define SQL_AUTH_GROUPS_DEFINITIVE (1<<3)
#define SQL_AUTH_USERSET           (1<<4)
#define SQL_AUTH_GROUPSET          (1<<5)
#define SQL_FAST_USERSET           (1<<6)
#define SQL_FAST_GROUPSET          (1<<7)

#define SQL_GROUPS             (cmap.authmask & SQL_AUTH_GROUPS)
#define SQL_USERS              (cmap.authmask & SQL_AUTH_USERS)
#define SQL_GROUPSET           (cmap.authmask & SQL_AUTH_GROUPSET)
#define SQL_USERSET            (cmap.authmask & SQL_AUTH_USERSET)
#define SQL_FASTGROUPS         (cmap.authmask & SQL_FAST_GROUPSET)
#define SQL_FASTUSERS          (cmap.authmask & SQL_FAST_USERSET)
#define SQL_GROUPGOD           (cmap.authmask & SQL_AUTH_GROUPS_DEFINITIVE)
#define SQL_USERGOD            (cmap.authmask & SQL_AUTH_USERS_DEFINITIVE)

/*
 * externs, function signatures.. whatever necessary to make
 * the compiler happy..
 */
extern pr_response_t *resp_list,*resp_err_list;
static char *_sql_where(pool *p, int cnt, ...);
MODRET cmd_getgrent(cmd_rec *);
MODRET cmd_setgrent(cmd_rec *);
MODRET sql_lookup(cmd_rec *);

pool *sql_pool;

/*
 * cache typedefs
 */

#define CACHE_SIZE         13

typedef struct cache_entry {
  struct cache_entry *list_next;
  struct cache_entry *bucket_next;
  void *data;
} cache_entry_t;

/* this struct holds invariant information for the current session */

static struct {
  /*
   * info valid after getpwnam
   */

  char *authuser;               /* current authorized user */
  struct passwd *authpasswd;    /* and their passwd struct */

  /*
   * generic status information
   */

  int status;                   /* is mod_sql on? */
  int authmask;                 /* authentication mask.
                                 * see set_sqlauthenticate for info */

  /*
   * user table and field information
   */

  char *usrtable;               /* user info table name */
  char *usrfield;               /* user name field */
  char *pwdfield;               /* user password field */
  char *uidfield;               /* user uid field */
  char *gidfield;               /* user gid field */
  char *homedirfield;           /* user homedir field */
  char *shellfield;             /* user login shell field */
  char *userwhere;              /* users where clause */
  char *usercustom;		/* custom users query */

  /*
   * group table and field information
   */

  char *grptable;               /* group info table name */
  char *grpfield;               /* group name field */
  char *grpgidfield;            /* group gid field */
  char *grpmembersfield;        /* group members field */
  char *groupwhere;             /* groups where clause */
  char *groupcustom;		/* custom groups query */

  /*
   * other information
   */

  array_header *authlist;       /* auth handler list */
  char *defaulthomedir;         /* default homedir if no field specified */
  int buildhomedir;             /* create homedir if it doesn't exist? */

  uid_t minid;                  /* users UID must be this or greater */
  uid_t minuseruid;             /* users UID must be this or greater */
  gid_t minusergid;             /* users UID must be this or greater */
  uid_t defaultuid;             /* default UID if none in database */
  gid_t defaultgid;             /* default GID if none in database */

  cache_entry_t *curr_group;    /* next group in group array for getgrent */
  cache_entry_t *curr_passwd;   /* next passwd in passwd array for getpwent */
  int group_cache_filled;
  int passwd_cache_filled;

  /* Cache negative, as well as positive, lookups */
  unsigned char negative_cache;

  /*
   * mod_ratio data -- someday this needs to be removed from mod_sql
   */

  char *sql_fstor;              /* fstor int(11) NOT NULL DEFAULT '0', */
  char *sql_fretr;              /* fretr int(11) NOT NULL DEFAULT '0', */
  char *sql_bstor;              /* bstor int(11) NOT NULL DEFAULT '0', */
  char *sql_bretr;              /* bretr int(11) NOT NULL DEFAULT '0', */

  char *sql_frate;              /* frate int(11) NOT NULL DEFAULT '5', */
  char *sql_fcred;              /* fcred int(2) NOT NULL DEFAULT '15', */
  char *sql_brate;              /* brate int(11) NOT NULL DEFAULT '5', */
  char *sql_bcred;              /* bcred int(2) NOT NULL DEFAULT '150000', */

  /*
   * precomputed strings
   */
  char *usrfields;
  char *grpfields;
}
cmap;

/*
 * cache functions
 */

typedef unsigned int ( * val_func ) ( const void * ); 
typedef int ( * cmp_func ) ( const void *, const void * );

typedef struct {
  /* memory pool for this object */
  pool *pool;

  /* cache buckets */
  cache_entry_t *buckets[ CACHE_SIZE ];

  /* cache functions */
  val_func hash_val;
  cmp_func cmp;

  /* list pointers */
  cache_entry_t *head;

  /* list size */
  unsigned int nelts;
} cache_t;

cache_t *group_name_cache;
cache_t *group_gid_cache;
cache_t *passwd_name_cache;
cache_t *passwd_uid_cache;

static cache_t *make_cache( pool *p, val_func hash_val, cmp_func cmp )
{
  cache_t *res;

  if ( ( p == NULL ) || ( hash_val == NULL ) || 
       ( cmp == NULL ) )
    return NULL;

  res = ( cache_t * ) pcalloc( p, sizeof( cache_t ) );

  res->pool = p;
  res->hash_val = hash_val;
  res->cmp = cmp;

  res->head = NULL;

  res->nelts = 0;

  return res;
}

static cache_entry_t *cache_addentry( cache_t *cache, void *data )
{
  cache_entry_t *entry;
  int hashval;

  if ( ( cache == NULL ) || ( data == NULL ) )
    return NULL;

  /* create the entry */
  entry = ( cache_entry_t * ) pcalloc( cache->pool, 
				       sizeof( cache_entry_t ) );
  entry->data = data;

  /* deal with the list */

  if ( cache->head == NULL ) {
    cache->head = entry;
  } else {
    entry->list_next = cache->head;
    cache->head = entry;
  }

  /* deal with the buckets */
  hashval = cache->hash_val( data ) % CACHE_SIZE;
  if ( cache->buckets[ hashval ] == NULL ) {
    cache->buckets[ hashval ] = entry;
  } else {
    entry->bucket_next = cache->buckets[ hashval ];
    cache->buckets[ hashval ] = entry;
  }
  
  cache->nelts++;

  return entry;
}

static void *cache_findvalue( cache_t *cache, void *data )
{
  cache_entry_t *entry;
  int hashval;

  if ( ( cache == NULL ) || ( data == NULL ) ) return NULL;
  
  hashval = cache->hash_val( data ) % CACHE_SIZE;

  entry = cache->buckets[ hashval ];
  while ( entry != NULL ) {
    if ( cache->cmp( data, entry->data ) )
      break;
    else
      entry = entry->bucket_next;
  }

  return ( ( entry == NULL ) ? NULL : entry->data );
}

cmd_rec *_sql_make_cmd(pool *p, int argc, ...) {
  register unsigned int i = 0;
  pool *newpool = NULL;
  cmd_rec *cmd = NULL;
  va_list args;

  newpool = make_sub_pool(p);
  cmd = pcalloc(newpool, sizeof(cmd_rec));
  cmd->argc = argc;
  cmd->stash_index = -1;
  cmd->pool = newpool;
  
  cmd->argv = pcalloc(newpool, sizeof(void *) * (argc));
  cmd->tmp_pool = newpool;
  cmd->server = main_server;

  va_start(args, argc);

  for (i = 0; i < argc; i++)
    cmd->argv[i] = (void *) va_arg(args, char *);

  va_end(args);

  return cmd;
}

static void _sql_check_cmd(cmd_rec *cmd, char *msg) {
  if ((!cmd) || (!cmd->tmp_pool)) {
    pr_log_pri(PR_LOG_ERR, _MOD_VERSION ": '%s' was passed an invalid cmd_rec. "
	    "Shutting down.", msg);
    sql_log(DEBUG_WARN, "'%s' was passed an invalid cmd_rec. Shutting down.",
      msg);
    end_login(1);
  }    
  return;
}

static modret_t *_sql_check_response(modret_t *mr) {
  if (!MODRET_ISERROR(mr))
    return mr;

  sql_log(DEBUG_WARN, "%s", "unrecoverable backend error");
  sql_log(DEBUG_WARN, "error: '%s'", mr->mr_numeric);
  sql_log(DEBUG_WARN, "message: '%s'", mr->mr_message);

  end_login(1);

  /* make the compiler happy */
  return NULL;
}

static modret_t *_sql_dispatch(cmd_rec *cmd, char *cmdname) {
  modret_t *mr = NULL;
  register unsigned int i = 0;

  for (i = 0; sql_cmdtable[i].command; i++) {
    if (strcmp(cmdname, sql_cmdtable[i].command) == 0) {
      pr_signals_block();
      mr = sql_cmdtable[i].handler(cmd);
      pr_signals_unblock();
      return mr;
    }
  }

  sql_log(DEBUG_WARN, "unknown backend handler '%s'", cmdname);
  return ERROR(cmd);
}

/*****************************************************************
 *
 * AUTHENTICATION FUNCTIONS
 *
 *****************************************************************/

static modret_t *check_auth_crypt(cmd_rec *cmd, const char *c_clear,
    const char *c_hash) {
  int success = 0;

  if (*c_hash == '\0')
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  success = !strcmp((char *) crypt(c_clear, c_hash), c_hash);

  return success ? HANDLED(cmd) : ERROR_INT(cmd, PR_AUTH_BADPWD);
}

static modret_t *check_auth_plaintext(cmd_rec *cmd, const char *c_clear,
    const char *c_hash) {
  int success = 0;

  if (*c_hash == '\0')
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  success = !strcmp(c_clear, c_hash);

  return success ? HANDLED(cmd) : ERROR_INT(cmd, PR_AUTH_BADPWD);
}

static modret_t *check_auth_empty(cmd_rec *cmd, const char *c_clear,
    const char *c_hash) {
  int success = 0;

  success = !strcmp(c_hash, "");

  return success ? HANDLED(cmd) : ERROR_INT(cmd, PR_AUTH_BADPWD);
}

static modret_t *check_auth_backend(cmd_rec *cmd, const char *c_clear,
    const char *c_hash) {
  modret_t *mr = NULL;

  if (*c_hash == '\0')
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 3, "default", c_clear,
    c_hash), "sql_checkauth");

  return mr;
}

#ifdef HAVE_OPENSSL
static modret_t *check_auth_openssl(cmd_rec *cmd, const char *c_clear,
    const char *c_hash) {
  /*
   * c_clear : plaintext password provided by user 
   * c_hash  : combination digest name and hashed 
   *           value, of the form {digest}hash 
   */

  EVP_MD_CTX md_ctxt;
  EVP_ENCODE_CTX base64_ctxt;
  const EVP_MD *md;
  unsigned char mdval[EVP_MAX_MD_SIZE];
  int mdlen, res;

  char buf[EVP_MAX_KEY_LENGTH];

  char *digestname;             /* ptr to name of the digest function */
  char *hashvalue;              /* ptr to hashed value we're comparing to */
  char *copyhash;               /* temporary copy of the c_hash string */

  if (c_hash[0] != '{') 
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  /* We need a copy of c_hash. */
  copyhash = pstrdup(cmd->tmp_pool, c_hash);

  digestname = copyhash + 1;

  hashvalue = (char *) strchr(copyhash, '}');

  if (hashvalue == NULL)
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  *hashvalue = '\0';
  hashvalue++;

  OpenSSL_add_all_digests();

  md = EVP_get_digestbyname(digestname);

  if (!md)
    return ERROR_INT(cmd, PR_AUTH_BADPWD);

  EVP_DigestInit(&md_ctxt, md);
  EVP_DigestUpdate(&md_ctxt, c_clear, strlen(c_clear));
  EVP_DigestFinal(&md_ctxt, mdval, &mdlen);

  EVP_EncodeInit(&base64_ctxt);
  EVP_EncodeBlock(buf, mdval, mdlen);

  res = strcmp(buf, hashvalue);

  return res ? ERROR_INT(cmd, PR_AUTH_BADPWD) : HANDLED(cmd);
}
#endif

/*
 * support for general-purpose authentication schemes 
 */

#define PLAINTEXT_AUTH_FLAG     1<<0
#define CRYPT_AUTH_FLAG         1<<1
#define BACKEND_AUTH_FLAG       1<<2
#define EMPTY_AUTH_FLAG         1<<3
#ifdef HAVE_OPENSSL
#define OPENSSL_AUTH_FLAG       1<<4
#endif

typedef modret_t *(*auth_func_ptr) (cmd_rec *, const char *, const char *);

typedef struct {
  char *name;
  auth_func_ptr check_function;
  int flag;
}
auth_type_entry;

static auth_type_entry supported_auth_types[] = {
  {"Plaintext", check_auth_plaintext, PLAINTEXT_AUTH_FLAG},
  {"Crypt", check_auth_crypt, CRYPT_AUTH_FLAG},
  {"Backend", check_auth_backend, BACKEND_AUTH_FLAG},
  {"Empty", check_auth_empty, EMPTY_AUTH_FLAG},
#ifdef HAVE_OPENSSL
  {"OpenSSL", check_auth_openssl, OPENSSL_AUTH_FLAG},
#endif
  /*
   * add additional encryption types below 
   */
  {NULL, NULL, 0}
};

static auth_type_entry *get_auth_entry(char *name) {
  auth_type_entry *ate = supported_auth_types;

  while (ate->name) {
    if (strcasecmp(ate->name, name) == 0)
      return ate;
    ate++;
  }
  return NULL;
}

/*****************************************************************
 *
 * INTERNAL HELPER FUNCTIONS
 *
 *****************************************************************/

/* find who core thinks is the user, and return a (backend-escaped) 
 * version of that name */
static char *_sql_realuser(cmd_rec *cmd) {
  modret_t *mr = NULL;
  char *user = NULL;

  /* this is the userid given by the user */
  user = (char *) get_param_ptr(main_server->conf, C_USER, FALSE);

  /* do we need to check for useralias?
   * see mod_time.c, get_user_cmd_times() */

  mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", user),
    "sql_escapestring");
  _sql_check_response(mr);

  return mr ? (char *) mr->data : NULL;
}

char *_sql_where(pool *p, int cnt, ...) {
  int tcnt;
  int flag;
  int len;
  char *res, *tchar;
  va_list dummy;

  flag = 0;

  len = 0;
  va_start(dummy,cnt);
  for (tcnt = 0; tcnt<cnt; tcnt++) {
    res = va_arg(dummy, char *);
    if (res != NULL && *res != '\0') {
      if (flag++) len += 5;
      len += strlen(res);
      len += 2;
    }
  }
  va_end(dummy);

  if (!len) return NULL;

  res = (char *) pcalloc(p, sizeof(char) * (len+1));
  flag = 0;

  va_start(dummy,cnt);
  for (tcnt = 0; tcnt<cnt; tcnt++) {
    tchar = va_arg(dummy, char *);
    if (tchar != NULL && *tchar != '\0') {
      if (flag++) sstrcat(res, " and ", len+1);
      sstrcat(res, "(", len+1);
      sstrcat(res, tchar, len+1);
      sstrcat(res, ")", len+1);
    }
  }
  va_end(dummy);

  return res;
}

static int _sql_strcmp(const char *s1, const char *s2) {
  if ((s1 == NULL) || (s2 == NULL))
    return 1;

  return strcmp(s1, s2);
}

static unsigned int _group_gid(const void *val) {
  if (val == NULL)
    return 0;

  return ((struct group *) val)->gr_gid;
} 

static unsigned int _group_name(const void *val) {
  char *name;
  int cnt;
  unsigned int nameval = 0;

  if (val == NULL)
    return 0;

  name = ((struct group *) val)->gr_name;

  if (name == NULL)
    return 0;

  for (cnt = 0; cnt < strlen(name); cnt++) {
    nameval += name[cnt];
  }

  return nameval;
}

static int _groupcmp (const void *val1, const void *val2) {
  if ((val1 == NULL) || (val2 == NULL))
    return 0;
  
  /* either the groupnames match or the gids match */
  
  if (_sql_strcmp(((struct group *) val1)->gr_name,
      ((struct group *) val2)->gr_name) == 0)
    return 1;

  if (((struct group *) val1)->gr_gid == ((struct group *) val2)->gr_gid)
    return 1;

  return 0;
}

static unsigned int _passwd_uid(const void *val) {
  if (val == NULL)
    return 0;

  return ((struct passwd *) val)->pw_uid;
} 

static unsigned int _passwd_name(const void *val) {
  char *name;
  int cnt;
  unsigned int nameval = 0;

  if (val == NULL)
    return 0;

  name = ((struct passwd *) val)->pw_name;

  if (name == NULL)
    return 0;

  for (cnt = 0; cnt < strlen(name); cnt++) {
    nameval += name[cnt];
  }

  return nameval;
}

static int _passwdcmp(const void *val1, const void *val2) {
  if ((val1 == NULL) || (val2 == NULL))
     return 0;
  
  /* either the usernames match or the uids match */
  if (_sql_strcmp(((struct passwd *) val1)->pw_name,
      ((struct passwd *) val2)->pw_name)  == 0)
    return 1;

  if (((struct passwd *) val1)->pw_uid == ((struct passwd *) val2)->pw_uid)
    return 1;

  return 0;
}

static void show_group(pool *p, struct group *g) {
  char **member = NULL, *members = "";

  if (g == NULL) {
    sql_log(DEBUG_INFO, "%s", "NULL group to show_group()");
    return;
  }

  member = g->gr_mem;

  while (*member != NULL) {
    members = pstrcat(p, members, *members ? ", " : "", *member, NULL);
    member++;
  } 

  sql_log(DEBUG_INFO, "+ grp.gr_name : %s", g->gr_name);
  sql_log(DEBUG_INFO, "+ grp.gr_gid  : %lu", (unsigned long) g->gr_gid);
  sql_log(DEBUG_INFO, "+ grp.gr_mem  : %s", members);

  return;
}

static void show_passwd(struct passwd *p) {
  if (p == NULL) {
    sql_log(DEBUG_INFO, "%s", "NULL passwd to show_passwd()");
    return;
  }

  sql_log(DEBUG_INFO, "+ pwd.pw_name  : %s", p->pw_name);
  sql_log(DEBUG_INFO, "+ pwd.pw_uid   : %lu", (unsigned long) p->pw_uid);
  sql_log(DEBUG_INFO, "+ pwd.pw_gid   : %lu", (unsigned long) p->pw_gid);
  sql_log(DEBUG_INFO, "+ pwd.pw_dir   : %s", p->pw_dir);
  sql_log(DEBUG_INFO, "+ pwd.pw_shell : %s", p->pw_shell);

  return;
}

static int build_homedir(cmd_rec *cmd, char *path, mode_t omode, uid_t uid,
    gid_t gid) {
  struct stat st;
  mode_t old_umask;
  int retval = 0;
  char *local_ptr;
  char *local_path;
  int userdir_flag = 0;
  gid_t p_gid;
  uid_t p_uid;

  sql_log(DEBUG_FUNC, ">>> build_homedir(%s,omode,%i,%i)", path, uid, gid);

  /* we assume we're handed a null-terminated string defining the
   * user's home directory. we walk it, directory by directory,
   * creating it if it doesn't exist.  path must start with '/'
   */

  if (path[0] != '/') {
    sql_log(DEBUG_WARN, "%s", "no '/' at start of user's homedir");
    sql_log(DEBUG_FUNC, "%s", "<<< build_homedir");
    return -1;
  }

  /* sanity check -- make sure the path doesn't exist */
  if (!pr_fsio_stat(path, &st)) {
    sql_log(DEBUG_WARN, "%s", "user's homedir already exists");
    sql_log(DEBUG_FUNC, "%s", "<<< build_homedir");
    return 0;

  } else if (errno != ENOENT) {
    sql_log(DEBUG_WARN, "problem with stat of user's homedir: %s",
      strerror(errno));
    sql_log(DEBUG_FUNC, "%s", "<<< build_homedir");
    return -1;
  }

  /* make our local copy of path, adding a '/' if necessary..
   * after this call, we're *guaranteed* a terminating '/'.  We use
   * this info later. */

  if ( path[(strlen(path) - 1)] == '/' )
    local_path = pstrdup(cmd->tmp_pool, path);
  else
    local_path = pstrcat(cmd->tmp_pool, path, "/", NULL);

  /* gain root for dir creation process */
  p_gid = getegid();
  p_uid = geteuid();
  PRIVS_ROOT

  /* skip the leading '/' */
  local_ptr = local_path + 1;

  while ( ( local_ptr = strchr( local_ptr, '/' ) ) != NULL ) {
    *local_ptr = '\0';

    if ( *(local_ptr + 1) == '\0' )
      userdir_flag = 1;

    if ( pr_fsio_stat( local_path, &st ) ) {
      /* if the stat failed.. */
      if (errno == ENOENT) {
	/* and it's 'cause the directory doesn't exist */
	if ( !userdir_flag ) {
	  /* if it's an intermediate dir */
	  if ( pr_fsio_mkdir(local_path, S_IRWXU | S_IRWXG | S_IRWXO ) ) {
            PRIVS_RELINQUISH
	    return -1;
	  } else {
	    pr_fsio_chown(local_path, p_uid, p_gid );
	  }
	} else {
	  /* this is the user's homedir, and the final directory  */
	  old_umask = umask(0);
	  umask( old_umask & ~(S_IWUSR | S_IXUSR | S_IRUSR) );
	  if ( pr_fsio_mkdir(local_path, omode) ) {
	    umask( old_umask );
            PRIVS_RELINQUISH
	    return -1;
	  } else {
	    pr_fsio_chown(local_path, uid, gid);
	  }
	  umask( old_umask );
	}
      } else {
	/* we failed for a reason other than no such
	 * directory, so we return an error */
        PRIVS_RELINQUISH
	return -1;
      }
    }
    
    /* fix local_ptr, and bump it */
    *local_ptr = '/';
    local_ptr++;
  }

  /* relinquish root privileges */
  PRIVS_RELINQUISH

  sql_log(DEBUG_FUNC, "%s", "<<< build_homedir");
  return (retval);
}

/* _sql_addpasswd: creates a passwd and adds it to the passwd struct
 *  cache if it doesn't already exist.  Returns the created passwd
 *  struct, or the pre-existing struct if there was one.
 *
 * DOES NOT CHECK ARGUMENTS.  CALLING FUNCTIONS NEED TO MAKE SURE
 * THEY PASS VALID DATA
 */
static struct passwd *_sql_addpasswd(cmd_rec *cmd, char *username,
    char *password, uid_t uid, gid_t gid, char *shell, char *dir) {
  struct passwd *cached = NULL;
  struct passwd *pwd = NULL;

  pwd = pcalloc(cmd->tmp_pool, sizeof(struct passwd));
  pwd->pw_uid = uid;
  pwd->pw_name = username;

  /* check to make sure the entry doesn't exist in the cache */
  if ( ((cached = (struct passwd *) 
	 cache_findvalue(passwd_name_cache, pwd)) != NULL)) {
    pwd = cached;
    sql_log(DEBUG_INFO, "cache hit for user '%s'", pwd->pw_name);

  } else {
    pwd = pcalloc(sql_pool, sizeof(struct passwd));

    if (username)
      pwd->pw_name = pstrdup(sql_pool, username);

    if (password)
      pwd->pw_passwd = pstrdup(sql_pool, password);
    
    pwd->pw_uid = uid;
    pwd->pw_gid = gid;
   
    if (shell) 
      pwd->pw_shell = pstrdup(sql_pool, shell);
    if (dir)
      pwd->pw_dir = pstrdup(sql_pool, dir);
    
    cache_addentry(passwd_name_cache, pwd);
    cache_addentry(passwd_uid_cache, pwd);

    sql_log(DEBUG_INFO, "cache miss for user '%s'", pwd->pw_name);
    sql_log(DEBUG_INFO, "user '%s' cached", pwd->pw_name);
    show_passwd(pwd);
  }

  return pwd;
}

static struct passwd *_sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
  sql_data_t *sd = NULL;
  modret_t *mr = NULL;
  struct passwd *pwd = NULL;
  char uidstr[MOD_SQL_BUFSIZE] = {'\0'};
  char *usrwhere, *where;
  char *realname = NULL;
  int i = 0;

  char *username = NULL;
  char *password = NULL;
  char *shell = NULL;
  char *dir = NULL;
  uid_t uid = 0;
  gid_t gid = 0;

  if (p == NULL) {
    sql_log(DEBUG_WARN, "%s", "_sql_getpasswd called with NULL passwd struct");
    sql_log(DEBUG_WARN, "%s", "THIS SHOULD NEVER HAPPEN");
    return NULL;
  }

  if (!cmap.homedirfield &&
      !cmap.defaulthomedir)
    return NULL;

  /* check to see if the passwd already exists in one of the passwd caches */
  if (((pwd = (struct passwd *) 
	 cache_findvalue(passwd_name_cache, p)) != NULL) ||
       ((pwd = (struct passwd *) 
	 cache_findvalue(passwd_uid_cache, p)) != NULL)) {
    sql_log(DEBUG_AUTH, "cache hit for user '%s'", pwd->pw_name);

    /* Check for negatively cached passwds, which will have NULL
     * passwd/home/shell.
     */
    if (!pwd->pw_dir) {
      sql_log(DEBUG_AUTH, "negative cache entry for user '%s'", pwd->pw_name);
      return NULL;
    }

    return pwd;
  }

  if (p->pw_name != NULL) {
    realname = pr_str_strip(cmd->tmp_pool, p->pw_name);

    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", realname),
      "sql_escapestring" );
    _sql_check_response(mr);

    username = (char *) mr->data;

    usrwhere = pstrcat(cmd->tmp_pool, cmap.usrfield, "='", username, "'", NULL);

    sql_log(DEBUG_WARN, "cache miss for user '%s'", realname);

  } else {
    /* assume we have a uid */
    snprintf(uidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) p->pw_uid);
    sql_log(DEBUG_WARN, "cache miss for uid '%s'", uidstr);

    if (cmap.uidfield)
      usrwhere = pstrcat(cmd->tmp_pool, cmap.uidfield, " = ", uidstr, NULL);

    else {
      sql_log(DEBUG_WARN, "no user uid field configured, declining to "
        "lookup uid '%s'", uidstr);

      /* If no uid field has been configured, return now and let other
       * modules possibly have a chance at resolving this UID to a name.
       */
      return NULL;
    }
  }

  if (!cmap.usercustom) { 
    where = _sql_where(cmd->tmp_pool, 2, usrwhere, cmap.userwhere );

    mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 5, "default",
      cmap.usrtable, cmap.usrfields, where, "1"), "sql_select");

    _sql_check_response(mr);

    if (MODRET_HASDATA(mr))
      sd = (sql_data_t *) mr->data;

  } else {

    mr = sql_lookup(_sql_make_cmd(cmd->tmp_pool, 3, "default", cmap.usercustom,
      realname ? realname : "NULL"));

    _sql_check_response(mr);

    if (MODRET_HASDATA(mr)) {
      array_header *ah = (array_header *) mr->data;
      sd = pcalloc(cmd->tmp_pool, sizeof(sql_data_t));

      /* Assume the query only returned 1 row. */
      sd->fnum = ah->nelts;

      if (sd->fnum) {
        sd->rnum = 1;
        sd->data = (char **) ah->elts;

      } else {
        sd->rnum = 0;
        sd->data = NULL;
      }
    }
  }

  /* if we have no data.. */
  if (sd == NULL ||
      sd->rnum == 0) {
    if (!cmap.negative_cache) {
      return NULL;

    } else {

      /* If doing caching of negative lookups, cache this failed lookup.
       * Use the default UID and GID.
       */
      return _sql_addpasswd(cmd, username, NULL, p->pw_uid, p->pw_gid,
        NULL, NULL);
    }
  }

  i = 0;

  username = sd->data[i++];
  password = sd->data[i++];
  
  uid = cmap.defaultuid;
  if (cmap.uidfield) {
    if (sd->data[i]) {
      uid = atoi(sd->data[i++]);

    } else {
      i++;
    }
  }

  gid = cmap.defaultgid;
  if (cmap.gidfield) {
    if (sd->data[i]) {
      gid = atoi(sd->data[i++]);

    } else {
      i++;
    }
  }

  dir = cmap.defaulthomedir;
  if (sd->data[i]) {
    if (strcmp(sd->data[i], "") == 0 ||
        strcmp(sd->data[i], "NULL") == 0)

      /* Leave dir pointing to the SQLDefaultHomedir, if any. */
      i++;

    else
      dir = sd->data[i++];
  }

  if (cmap.shellfield) {
    if (sd->fnum < i || !sd->data[i]) {

      /* Make sure that, if configured, the shell value is valid, and scream
       * if it is not.
       */
      sql_log(DEBUG_WARN, "NULL shell column value, setting to \"\"");
      shell = "";

    } else {
      shell = sd->data[i];
    }

  } else
    shell = "";
  
  if (uid < cmap.minuseruid)
    uid = cmap.defaultuid;
  if (gid < cmap.minusergid)
    gid = cmap.defaultgid;

  return _sql_addpasswd(cmd, username, password, uid, gid, shell, dir);
}

/* _sql_addgroup: creates a group and adds it to the group struct
 *  cache if it doesn't already exist.  Returns the created group
 *  struct, or the pre-existing struct if there was one.
 *
 * DOES NOT CHECK ARGUMENTS.  CALLING FUNCTIONS NEED TO MAKE SURE
 * THEY PASS VALID DATA
 */
static struct group *_sql_addgroup(cmd_rec *cmd, char *groupname, gid_t gid,
    array_header *ah) {
  struct group *cached = NULL;
  struct group *grp = NULL;

  int cnt = 0;

  grp = pcalloc(cmd->tmp_pool, sizeof(struct group));
  grp->gr_gid = gid;
  grp->gr_name = groupname;

  /* check to make sure the entry doesn't exist in the cache */
  if ((cached = (struct group *) cache_findvalue( group_name_cache, grp)) != NULL) {
    grp = cached;
    sql_log(DEBUG_INFO, "cache hit for group '%s'", grp->gr_name);

  } else {
    grp = pcalloc(sql_pool, sizeof(struct group));

    if (groupname)
      grp->gr_name = pstrdup(sql_pool, groupname);

    grp->gr_gid = gid;

    /* finish filling in the group */
    grp->gr_mem = (char **) pcalloc(sql_pool, sizeof(char *) * (ah->nelts + 1));

    for ( cnt = 0; cnt < ah->nelts; cnt++ ) {
      grp->gr_mem[cnt] = pstrdup(sql_pool, ((char **) ah->elts)[cnt]);
    }

    grp->gr_mem[ ah->nelts ]='\0';

    cache_addentry( group_name_cache, grp );
    cache_addentry( group_gid_cache, grp );

    sql_log(DEBUG_INFO, "cache miss for group '%s'", grp->gr_name);
    sql_log(DEBUG_INFO, "group '%s' cached", grp->gr_name);
    show_group(cmd->tmp_pool, grp);
  }

  return grp;
}

static struct group *_sql_getgroup(cmd_rec *cmd, struct group *g) {
  struct group *grp = NULL;
  modret_t *mr = NULL;
  int cnt = 0;
  sql_data_t *sd = NULL;
  char *groupname = NULL;
  char gidstr[MOD_SQL_BUFSIZE] = { '\0' };
  char **rows = NULL;
  int numrows = 0;
  array_header *ah = NULL;
  char *members = NULL;
  char *member = NULL;
  char *grpwhere;
  char *where;
  char *iterator;

  gid_t gid = 0;
  
  if (g == NULL) {
    sql_log(DEBUG_WARN, "%s", "_sql_getgroup called with NULL group struct");
    sql_log(DEBUG_WARN, "%s", "THIS SHOULD NEVER HAPPEN");
    return NULL;
  }

  /* check to see if the group already exists in one of the group caches */
  if (((grp = (struct group *) cache_findvalue(group_name_cache, g))!=NULL) ||
      ((grp = (struct group *) cache_findvalue(group_gid_cache, g))!=NULL)) {
    sql_log(DEBUG_AUTH, "cache hit for group %s", grp->gr_name);

    /* check for negatively cached groups, which will have NULL gr_mem. */
    if ( !grp->gr_mem ) {
      sql_log(DEBUG_AUTH, "negative cache entry for group '%s'", grp->gr_name);
      return NULL;
    }

    return grp;
  }

  if (g->gr_name != NULL) {
    groupname = g->gr_name;
    sql_log(DEBUG_WARN, "cache miss for group '%s'", groupname);

  } else {
    /* get groupname from gid */
    snprintf(gidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) g->gr_gid);

    sql_log(DEBUG_WARN, "cache miss for gid '%s'", gidstr);

    if (cmap.grpgidfield)
      grpwhere = pstrcat(cmd->tmp_pool, cmap.grpgidfield, " = ", gidstr, NULL);

    else {
      sql_log(DEBUG_WARN, "no group gid field configured, declining to lookup "
        "gid '%s'", gidstr);

      /* If no gid field has been configured, return now and let other
       * modules possibly have a chance at resolving this GID to a name.
       */
      return NULL;
    }

    where = _sql_where(cmd->tmp_pool, 2, grpwhere, cmap.groupwhere);

    mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 5,
				       "default",
				       cmap.grptable,
				       cmap.grpfield,
				       where,
				       "1" ),
			"sql_select" );
    _sql_check_response(mr);

    sd = (sql_data_t *) mr->data;

    /* if we have no data.. */
    if (sd->rnum == 0) return NULL;

    groupname = sd->data[0];
  }

  grpwhere = pstrcat(cmd->tmp_pool, cmap.grpfield, " = '", groupname, "'", NULL);
  where = _sql_where(cmd->tmp_pool, 2, grpwhere, cmap.groupwhere);
  
  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4,
				     "default",
				     cmap.grptable,
				     cmap.grpfields,
				     where ),
		      "sql_select" );
  _sql_check_response(mr);
  
  sd = (sql_data_t *) mr->data;

  /* if we have no data.. */
  if (sd->rnum == 0) {
    if (!cmap.negative_cache) {
      return NULL;

    } else {

      /* if doing caching of negative lookups, cache this failed lookup. */
      return _sql_addgroup( cmd, groupname, g->gr_gid, NULL );
    }
  }
 
  rows = sd->data;
  numrows = sd->rnum;
  
  gid = (gid_t) strtoul(rows[1], NULL, 10);
  
  /*
   * painful.. we need to walk through the returned rows and fill in our
   * members. Every third element in a row is a member field, and every
   * member field can have multiple members.
   */
  
  ah = make_array(cmd->tmp_pool, 10, sizeof(char *));
  
  for (cnt = 0; cnt < numrows; cnt++) {
    members = rows[(cnt * 3) + 2];
    iterator=members;
    
    /* if the row is null, continue.. */
    if (members == NULL) continue;
    
    /* for each member in the list, toss 'em into the array.  no
     * need to copy the string -- _sql_addgroup will do it for us 
     */
    for (member = strsep(&iterator, ","); member;
        member = strsep(&iterator, ",")) {
      if (*member=='\0') continue;
      *((char **) push_array(ah)) = member;
    }      
  }
  
  return _sql_addgroup( cmd, groupname, gid, ah );
}

static void _setstats(cmd_rec *cmd, int fstor, int fretr, int bstor,
    int bretr) {
  /*
   * if anyone has a better way of doing this, let me know.. 
   */
  char query[256] = { '\0' };
  char *usrwhere, *where;
  modret_t *mr = NULL;

  snprintf(query, sizeof(query),
           "%s = %s + %i, %s = %s + %i, %s = %s + %i, %s = %s + %i",
           cmap.sql_fstor, cmap.sql_fstor, fstor,
           cmap.sql_fretr, cmap.sql_fretr, fretr,
           cmap.sql_bstor, cmap.sql_bstor, bstor,
	   cmap.sql_bretr, cmap.sql_bretr, bretr);

  usrwhere = pstrcat(cmd->tmp_pool, cmap.usrfield, " = '", _sql_realuser(cmd), "'", NULL);
  where = _sql_where(cmd->tmp_pool, 2, usrwhere, cmap.userwhere );

  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4, "default", cmap.usrtable,
				query, where ), "sql_update" );
  _sql_check_response(mr);

}

static int _sql_getgroups(cmd_rec *cmd) {
  struct passwd *pw = NULL, lpw;
  struct group *grp, lgr;
  char *grpwhere = NULL, *where = NULL, **rows = NULL;
  sql_data_t *sd = NULL;
  modret_t *mr = NULL;
  array_header *gids = NULL, *groups = NULL;
  char *name = cmd->argv[0];
  int numrows = 0;
  register unsigned int i = 0;

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

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

  lpw.pw_uid = -1;
  lpw.pw_name = name;
  
  /* Retrieve the necessary info */
  if (!name || !(pw = _sql_getpasswd(cmd, &lpw)))
    return -1;

  /* Populate the first group ID and name */
  if (gids)
    *((gid_t *) push_array(gids)) = pw->pw_gid;

  lgr.gr_gid = pw->pw_gid;
  lgr.gr_name = NULL;

  if (groups && (grp = _sql_getgroup(cmd, &lgr)) != NULL)
    *((char **) push_array(groups)) = pstrdup(permanent_pool, grp->gr_name);

  /* Use a single SELECT:
   *
   *  SELECT groupname,gid,members FROM groups
   *    WHERE members LIKE '%,<user>,%' OR LIKE '<user>,%' OR LIKE '%,<user>';
   *
   */

  grpwhere = pstrcat(cmd->tmp_pool,
    cmap.grpmembersfield, " = '", name, "' OR ",
    cmap.grpmembersfield, " LIKE '", name, ",%' OR ",
    cmap.grpmembersfield, " LIKE '%,", name, "' OR ",
    cmap.grpmembersfield, " LIKE '%,", name, ",%'", NULL);

  where = _sql_where(cmd->tmp_pool, 2, grpwhere, cmap.groupwhere);
  
  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4,
                                     "default",
                                     cmap.grptable,
                                     cmap.grpfields,
                                     where ),
                      "sql_select" );
  _sql_check_response(mr);
  
  sd = (sql_data_t *) mr->data;

  /* if we have no data... */
  if (sd->rnum == 0) return -1;

  rows = sd->data;
  numrows = sd->rnum;

  for (i = 0; i < numrows; i++) {
    char *groupname = sd->data[(i * 3)];
    gid_t gid = (gid_t) atoi(sd->data[(i * 3) +1]);
    char *memberstr = sd->data[(i * 3) + 2], *member = NULL;
    array_header *members = make_array(cmd->tmp_pool, 2, sizeof(char *));

    *((gid_t *) push_array(gids)) = gid;
    *((char **) push_array(groups)) = pstrdup(permanent_pool, groupname);

    /* For each member in the list, toss 'em into the array.  no
     * need to copy the string -- _sql_addgroup will do it for us
     */
    for (member = strsep(&memberstr, ","); member;
        member = strsep(&memberstr, ",")) {
      if (*member == '\0')
        continue;
      *((char **) push_array(members)) = member;
    }

    /* Add this group data to the group cache. */
    _sql_addgroup( cmd, groupname, gid, members );
  }

  if (gids && gids->nelts > 0)
    return gids->nelts;

  else if (groups && groups->nelts)
    return groups->nelts;

  /* default */
  return -1;
}

/*****************************************************************
 *
 * CLIENT COMMAND HANDLERS
 *
 *****************************************************************/

MODRET post_cmd_stor(cmd_rec *cmd) {
  _sql_check_cmd(cmd, "post_cmd_stor");

  sql_log(DEBUG_FUNC, "%s", ">>> post_cmd_stor");

  if (!cmap.status) return DECLINED(cmd);

  if (cmap.sql_fstor)
    _setstats(cmd, 1, 0, session.xfer.total_bytes, 0);

  sql_log(DEBUG_FUNC, "%s", "<<< post_cmd_stor");

  return DECLINED(cmd);
}

MODRET post_cmd_retr(cmd_rec *cmd) {
  _sql_check_cmd(cmd, "post_cmd_retr");

  if (!cmap.status) return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> post_cmd_retr");

  if (cmap.sql_fretr)
    _setstats(cmd, 0, 1, 0, session.xfer.total_bytes);

  sql_log(DEBUG_FUNC, "%s", "<<< post_cmd_retr");

  return DECLINED(cmd);
}

static char *resolve_tag(cmd_rec *cmd, char tag) {
  char arg[256] = {'\0'}, *argp;

  switch(tag) {
  case 'A': {
      char *pass;

      argp = arg;
      pass = get_param_ptr(main_server->conf, C_PASS, FALSE);
      if (!pass)
	pass = "UNKNOWN";
      
      sstrncpy( argp, pass, sizeof(arg));
    }
    break;

  case 'a':
    argp = arg;
    sstrncpy(argp, pr_netaddr_get_ipstr(session.c->remote_addr), sizeof(arg));
    break;

  case 'b':
    argp = arg;
    if (session.xfer.p)
      snprintf(argp, sizeof(arg), "%" PR_LU, session.xfer.total_bytes);
    else
      sstrncpy( argp, "0", sizeof(arg));
    break;

  case 'c':
    argp = arg;
    sstrncpy(argp, session.class ? session.class->cls_name : "-", sizeof(arg));
    break;

  case 'd':
    argp = arg;

    if (strcmp(cmd->argv[0], C_CDUP) == 0 ||
        strcmp(cmd->argv[0], C_CWD) == 0 ||
        strcmp(cmd->argv[0], C_MKD) == 0 ||
        strcmp(cmd->argv[0], C_RMD) == 0 ||
        strcmp(cmd->argv[0], C_XCWD) == 0 ||
        strcmp(cmd->argv[0], C_XCUP) == 0 ||
        strcmp(cmd->argv[0], C_XMKD) == 0 ||
        strcmp(cmd->argv[0], C_XRMD) == 0) {
      char *tmp = strrchr(cmd->arg, '/');

      sstrncpy(argp, tmp ? tmp : cmd->arg, sizeof(arg));

    } else
      sstrncpy(argp, "", sizeof(arg));

    break;

  case 'D':
    argp = arg;

    if (strcmp(cmd->argv[0], C_CDUP) == 0 ||
        strcmp(cmd->argv[0], C_MKD) == 0 ||
        strcmp(cmd->argv[0], C_RMD) == 0 ||
        strcmp(cmd->argv[0], C_XCUP) == 0 ||
        strcmp(cmd->argv[0], C_XMKD) == 0 ||
        strcmp(cmd->argv[0], C_XRMD) == 0) {
      sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE), sizeof(arg));

    } else if (strcmp(cmd->argv[0], C_CWD) == 0 ||
               strcmp(cmd->argv[0], C_XCWD) == 0) {

      /* Note: by this point in the dispatch cycle, the current working
       * directory has already been changed.  For the CWD/XCWD commands,
       * this means that dir_abs_path() may return an improper path,
       * with the target directory being reported twice.  To deal with this,
       * don't use dir_abs_path(), and use pr_fs_getvwd()/pr_fs_getcwd()
       * instead.
       */

      if (session.chroot_path) {
        /* Chrooted session. */
        sstrncpy(arg, strcmp(pr_fs_getvwd(), "/") ?
          pdircat(cmd->tmp_pool, session.chroot_path, pr_fs_getvwd(), NULL) :
          session.chroot_path, sizeof(arg));

      } else

        /* Non-chrooted session. */
        sstrncpy(arg, pr_fs_getcwd(), sizeof(arg));

    } else
      sstrncpy(argp, "", sizeof(arg));

    break;

  case 'f':
    argp = arg;
    if (session.xfer.p && session.xfer.path) {
      sstrncpy(argp, dir_abs_path(cmd->tmp_pool, session.xfer.path, TRUE),
        sizeof(arg));

    } else {

      /* Some commands (i.e. DELE, MKD, RMD, XMKD, and XRMD) have associated
       * filenames that are not stored in the session.xfer structure; these
       * should be expanded properly as well.
       */
      if (strcmp(cmd->argv[0], C_DELE) == 0 ||
          strcmp(cmd->argv[0], C_MKD) == 0 ||
          strcmp(cmd->argv[0], C_RMD) == 0 ||
          strcmp(cmd->argv[0], C_XMKD) == 0 ||
          strcmp(cmd->argv[0], C_XRMD) == 0)
        sstrncpy(arg, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE), sizeof(arg));

      else
        /* All other situations get a "-".  */
        sstrncpy(argp, "-", sizeof(arg));
    }
    break;

  case 'F':
    argp = arg;
    if (session.xfer.p && session.xfer.path) {
      sstrncpy(argp, session.xfer.path, sizeof(arg));

    } else {
      /* Some commands (i.e. DELE) have associated filenames that are not
       * stored in the session.xfer structure; these should be expanded
       * properly as well.
       */
      if (strcmp(cmd->argv[0], C_DELE) == 0)
        sstrncpy(arg, cmd->arg, sizeof(arg));

      else
        sstrncpy(argp, "-", sizeof(arg));
    }
    break;

  case 'J':
    argp = arg;
    sstrncpy(argp, cmd->arg, sizeof(arg));
    break;

  case 'h':
    argp = arg;
    sstrncpy(argp, session.c->remote_name, sizeof(arg));
    break;

  case 'L':
    argp = arg;
    sstrncpy(argp, pr_netaddr_get_ipstr(session.c->local_addr), sizeof(arg));
    break;

  case 'l':
    argp = arg;
    sstrncpy(argp, session.ident_user, sizeof(arg));
    break;

  case 'm':
    argp = arg;
    sstrncpy(argp, cmd->argv[0], sizeof(arg));
    break;

  case 'P':
    argp = arg;
    snprintf(argp, sizeof(arg), "%u",(unsigned int)getpid());
    break;

  case 'p': 
    argp = arg;
    snprintf(argp, sizeof(arg), "%d", cmd->server->ServerPort);
    break;

  case 'r':
    argp = arg;
    if(!strcasecmp(cmd->argv[0], C_PASS) && session.hide_password)
      sstrncpy(argp, C_PASS " (hidden)", sizeof(arg));
    else
      sstrncpy(argp, get_full_cmd(cmd), sizeof(arg));
    break;

  case 's': {
      pr_response_t *r;
      argp = arg;
      
      r = (resp_list ? resp_list : resp_err_list);
      
      for (; r && !r->num; r=r->next);

      if (r && r->num)
        sstrncpy(argp, r->num, sizeof(arg));
      else
        sstrncpy(argp, "-", sizeof(arg));
    }
    break;

  case 'T':
    argp = arg;
    if (session.xfer.p) {
      struct timeval end_time;
      
      gettimeofday(&end_time, NULL);
      end_time.tv_sec -= session.xfer.start_time.tv_sec;

      if (end_time.tv_usec >= session.xfer.start_time.tv_usec)
        end_time.tv_usec -= session.xfer.start_time.tv_usec;

      else {
        end_time.tv_usec = 1000000L - (session.xfer.start_time.tv_usec -
          end_time.tv_usec);
        end_time.tv_sec--;
      }
      
      snprintf(argp, sizeof(arg), "%lu.%03lu", (unsigned long) end_time.tv_sec,
        (unsigned long) (end_time.tv_usec / 1000));

    } else
      sstrncpy(argp, "0.0", sizeof(arg));

    break;

  case 'U':
    argp = arg;
    {
      char *login_user = get_param_ptr(main_server->conf, C_USER, FALSE);

      if (!login_user)
        login_user = "root";

      sstrncpy(argp, login_user, sizeof(arg));
    }
    break;

  case 'u':
    argp = arg;

    if (!session.user) {
      char *u;

      u = get_param_ptr(main_server->conf, "UserName", FALSE);
      if (!u)
        u = "root";

      sstrncpy(argp, u, sizeof(arg));

    } else
      sstrncpy(argp, session.user, sizeof(arg));

    break;

  case 'V':
    argp = arg;
    sstrncpy(argp, cmd->server->ServerFQDN, sizeof(arg));
    break;

  case 'v':
    argp = arg;
    sstrncpy(argp, cmd->server->ServerName, sizeof(arg));
    break;

  case '%':
    argp = "%";
    break;

  default:
    argp = "{UNKNOWN TAG}";
    break;
  }

  return pstrdup( cmd->tmp_pool, argp );
}

static char *_named_query_type(cmd_rec *cmd, char *name) {
  config_rec *c = NULL;
  char *query = NULL;

  query = pstrcat(cmd->tmp_pool, "SQLNamedQuery_", name, NULL);
  c = find_config(main_server->conf, CONF_PARAM, query, FALSE);

  if (c)
    return c->argv[0];

  return NULL;
}

static modret_t *_process_named_query(cmd_rec *cmd, char *name) {
  config_rec *c;
  char *query, *tmp, *argp;
  char outs[4096] = {'\0'}, *outsp;
  char *esc_arg = NULL;
  modret_t *mr = NULL;
  int num = 0;
  char *argc = 0;
  char *endptr = NULL;

  sql_log(DEBUG_FUNC, "%s", ">>> _process_named_query");

  /* check for a query by that name */

  query = pstrcat(cmd->tmp_pool, "SQLNamedQuery_", name, NULL);

  c = find_config(main_server->conf, CONF_PARAM, query, FALSE);
  if (c) {
    /* select string fixup */
    memset(outs, '\0', sizeof(outs));
    outsp = outs;

    for (tmp = c->argv[1]; *tmp; ) {
      if(*tmp == '%') {
	if (*(++tmp) == '{') {
	  char *tmp_query;
	  
	  if (*tmp!='\0') tmp_query = ++tmp;
	  
	  /* find the argument number to use */
	  while ( *tmp && *tmp!='}' ) tmp++;
	  
	  argc = pstrndup(cmd->tmp_pool, tmp_query, (tmp - tmp_query));
	  if (argc) {
	    num = strtol(argc, &endptr, 10);
	    
	    if ((*endptr != '\0') || (num < 0) || 
		((cmd->argc - 3 ) < num)) {
	      return ERROR_MSG(cmd, _MOD_VERSION, "reference out-of-bounds in query");
	    }
	  } else {
	    return ERROR_MSG(cmd, _MOD_VERSION, "malformed reference %{?} in query");
	  }
	   
	  esc_arg = cmd->argv[num+2];
	} else {
	  argp=resolve_tag( cmd, *tmp);
	  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", 
					     argp ), "sql_escapestring" );
	  _sql_check_response(mr);
	  esc_arg = (char *) mr->data;
	}
	
	strcat( outs, esc_arg );
	outsp += strlen(esc_arg);
	
	if ( *tmp!='\0' ) tmp++;
      } else {
	*outsp++ = *tmp++;
      }
    }
      
    *outsp++ = 0;

    /* construct our return data based on the type of query */
    if (!strcasecmp(c->argv[0], SQL_UPDATE_C)) {
      query = pstrcat(cmd->tmp_pool, c->argv[2], " SET ", outs, NULL);
      mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", query), 
			  "sql_update");
    } else if (!strcasecmp(c->argv[0], SQL_INSERT_C)) {
      query = pstrcat(cmd->tmp_pool, "INTO ", c->argv[2], " VALUES (",
		     outs, ")", NULL);
      mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", query), 
			  "sql_insert");
    } else if (!strcasecmp(c->argv[0], SQL_FREEFORM_C)) {
      mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", outs), 
			  "sql_query");
    } else if (!strcasecmp(c->argv[0], SQL_SELECT_C)) {
      mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", outs), 
			  "sql_select");
    } else {
      mr = ERROR_MSG(cmd, _MOD_VERSION, "unknown NamedQuery type");
    }
  } else {
    mr = ERROR(cmd);
  }
 
  sql_log(DEBUG_FUNC, "%s", "<<< _process_named_query");

  return mr;
}

MODRET log_master(cmd_rec *cmd) {
  char *name = NULL;
  char *qname = NULL;
  char *type = NULL;
  config_rec *c = NULL;
  modret_t *mr = NULL;

  _sql_check_cmd(cmd, "log_master");

  if (!cmap.status) return DECLINED(cmd);
  
  /* handle explicit queries */
  name = pstrcat(cmd->tmp_pool, "SQLLog_", cmd->argv[0], NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);

  if (c) {
    do {
      sql_log(DEBUG_FUNC, "%s", ">>> log_master");

      qname = c->argv[0];
      type = _named_query_type(cmd, qname);

      if (type) {
	if ((!strcasecmp(type, SQL_UPDATE_C)) || 
	    (!strcasecmp(type, SQL_FREEFORM_C)) ||
	    (!strcasecmp(type, SQL_INSERT_C))) {
	  mr = _process_named_query( cmd, qname );
	  if (c->argc == 2) _sql_check_response(mr);
	} else {
	  sql_log(DEBUG_WARN, "named query '%s' is not an INSERT, UPDATE, or "
            "FREEFORM query", qname);
	}
      } else {
	sql_log(DEBUG_WARN, "named query '%s' cannot be found", qname);
      }

      sql_log(DEBUG_FUNC, "%s", "<<< log_master");
    } while((c = find_config_next(c, c->next, 
				  CONF_PARAM, name, FALSE)) != NULL);
  }
  
  /* handle implit queries */
  name = pstrcat(cmd->tmp_pool, "SQLLog_*", NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);

  if (c) {
    do {
      sql_log(DEBUG_FUNC, "%s", ">>> log_master");

      qname = c->argv[0];
      type = _named_query_type(cmd, qname);

      if (type) {
	if ((!strcasecmp(type, SQL_UPDATE_C)) || 
	    (!strcasecmp(type, SQL_FREEFORM_C)) ||
	    (!strcasecmp(type, SQL_INSERT_C))) {
	  mr = _process_named_query( cmd, qname );
	  if (c->argc == 2) _sql_check_response(mr);
	} else {
	  sql_log(DEBUG_WARN, "named query '%s' is not an INSERT, UPDATE, or "
            "FREEFORM query", qname);
	}
      } else {
	sql_log(DEBUG_WARN, "named query '%s' cannot be found", qname);
      }

      sql_log(DEBUG_FUNC, "%s", "<<< log_master");
    } while((c = find_config_next(c, c->next, 
				  CONF_PARAM, name, FALSE)) != NULL);
  }

  return DECLINED(cmd);
}

MODRET err_master(cmd_rec *cmd) {
  char *name = NULL;
  char *qname = NULL;
  char *type = NULL;
  config_rec *c = NULL;
  modret_t *mr = NULL;

  _sql_check_cmd(cmd, "err_master");

  if (!cmap.status) return DECLINED(cmd);
  
  /* handle explicit errors */
  name = pstrcat(cmd->tmp_pool, "SQLLog_ERR_", cmd->argv[0], NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);

  if (c) {
    do {
      sql_log(DEBUG_FUNC, "%s", ">>> err_master");

      qname = c->argv[0];
      type = _named_query_type(cmd, qname);

      if (type) {
	if ((!strcasecmp(type, SQL_UPDATE_C)) || 
	    (!strcasecmp(type, SQL_FREEFORM_C)) ||
	    (!strcasecmp(type, SQL_INSERT_C))) {
	  mr = _process_named_query( cmd, qname );
	  if (c->argc == 2) _sql_check_response(mr);
	} else {
	  sql_log(DEBUG_WARN, "named query '%s' is not an INSERT, UPDATE, or "
            "FREEFORM query", qname);
	}
      } else {
	sql_log(DEBUG_WARN, "named query '%s' cannot be found", qname);
      }

      sql_log(DEBUG_FUNC, "%s", "<<< err_master");
    } while((c = find_config_next(c, c->next, 
				  CONF_PARAM, name, FALSE)) != NULL);
  }
  
  /* handle implicit errors */
  name = pstrcat(cmd->tmp_pool, "SQLLog_ERR_*", NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);

  if (c) {
    do {
      sql_log(DEBUG_FUNC, "%s", ">>> err_master");

      qname = c->argv[0];
      type = _named_query_type(cmd, qname);

      if (type) {
	if ((!strcasecmp(type, SQL_UPDATE_C)) || 
	    (!strcasecmp(type, SQL_FREEFORM_C)) ||
	    (!strcasecmp(type, SQL_INSERT_C))) {
	  mr = _process_named_query( cmd, qname );
	  if (c->argc == 2) _sql_check_response(mr);
	} else {
	  sql_log(DEBUG_WARN, "named query '%s' is not an INSERT, UPDATE, or "
            "FREEFORM query", qname);
	}
      } else {
	sql_log(DEBUG_WARN, "named query '%s' cannot be found", qname);
      }

      sql_log(DEBUG_FUNC, "%s", "<<< err_master");
    } while((c = find_config_next(c, c->next, 
				  CONF_PARAM, name, FALSE)) != NULL);
  }

  return DECLINED(cmd);
}

MODRET info_master(cmd_rec *cmd) {
  char *type = NULL;
  char *name = NULL;
  config_rec *c = NULL;
  char outs[4096] = {'\0'}, *outsp;
  char *argp = NULL; 
  char *tmp = NULL;
  modret_t *mr = NULL;
  sql_data_t *sd = NULL;

  _sql_check_cmd(cmd, "info_master");

  if (!cmap.status) return DECLINED(cmd);

  /* process explicit handlers */
  name = pstrcat(cmd->tmp_pool, "SQLShowInfo_", cmd->argv[0], NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);
  if (c) {
    sql_log(DEBUG_FUNC, "%s", ">>> info_master");

    /* we now have at least one config_rec.  Take the output string from 
     * each, and process it -- resolve tags, and when we find a named 
     * query, run it and get info from it. 
     */

    do {
      memset(outs, '\0', sizeof(outs));
      outsp = outs;

      for (tmp = c->argv[1]; *tmp; ) {
	if(*tmp == '%') {
	  /* is the tag a named_query reference?  If so, process the 
	   * named query, otherwise process it as a normal tag.. 
	   */
	  
	  if (*(++tmp) == '{') {
	    char *query;

	    if (*tmp!='\0') query = ++tmp;
	    
	    /* get the name of the query */
	    while ( *tmp && *tmp!='}' ) tmp++;
	    
	    query = pstrndup(cmd->tmp_pool, query, (tmp - query));

	    /* make sure it's a SELECT query */
	    
	    type = _named_query_type(cmd, query);
	    if (type && ((!strcasecmp(type, SQL_SELECT_C )) ||
			 (!strcasecmp(type, SQL_FREEFORM_C )))) {
	      mr = _process_named_query(cmd, query);
	      
	      if (MODRET_ISERROR(mr)) {
		argp = "{null}";
	      } else {
		sd = (sql_data_t *) mr->data;
		if ((sd->rnum == 0) || (!sd->data[0]))
		  argp = "{null}";
		else
		  argp = sd->data[0];
	      }
	    } else {
	      argp = "{null}";
	    }
	  } else {
	    argp=resolve_tag( cmd, *tmp);
	  }

	  sstrcat(outs, argp, sizeof(outs));
	  outsp += strlen(argp);

	  if (*tmp!='\0') tmp++;
	} else {
	  *outsp++ = *tmp++;
	}
      }
      
      *outsp++ = 0;

      /* add the response */
      pr_response_add( c->argv[0], outs);

    } while((c = find_config_next(c, c->next, CONF_PARAM, name, FALSE)) != NULL);

    sql_log(DEBUG_FUNC, "%s", "<<< info_master");
  }

  /* process implicit handlers */
  name = pstrcat(cmd->tmp_pool, "SQLShowInfo_*", NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);
  if (c) {
    sql_log(DEBUG_FUNC, "%s", ">>> info_master");

    /* we now have at least one config_rec.  Take the output string from 
     * each, and process it -- resolve tags, and when we find a named 
     * query, run it and get info from it. 
     */

    do {
      memset(outs, '\0', sizeof(outs));
      outsp = outs;

      for (tmp = c->argv[1]; *tmp; ) {
	if(*tmp == '%') {
	  /* is the tag a named_query reference?  If so, process the 
	   * named query, otherwise process it as a normal tag.. 
	   */
	  
	  if (*(++tmp) == '{') {
	    char *query;

	    if (*tmp!='\0') query = ++tmp;
	    
	    /* get the name of the query */
	    while ( *tmp && *tmp!='}' ) tmp++;
	    
	    query = pstrndup(cmd->tmp_pool, query, (tmp - query));

	    /* make sure it's a SELECT query */
	    
	    type = _named_query_type(cmd, query);
	    if (type && ((!strcasecmp(type, SQL_SELECT_C )) ||
			 (!strcasecmp(type, SQL_FREEFORM_C )))) {
	      mr = _process_named_query(cmd, query);
	      
	      if (MODRET_ISERROR(mr)) {
		argp = "{null}";
	      } else {
		sd = (sql_data_t *) mr->data;
		if ((sd->rnum == 0) || (!sd->data[0]))
		  argp = "{null}";
		else
		  argp = sd->data[0];
	      }
	    } else {
	      argp = "{null}";
	    }
	  } else {
	    argp=resolve_tag( cmd, *tmp);
	  }

	  sstrcat(outs, argp, sizeof(outs));
	  outsp += strlen(argp);

	  if (*tmp!='\0') tmp++;
	} else {
	  *outsp++ = *tmp++;
	}
      }
      
      *outsp++ = 0;

      /* add the response */
      pr_response_add( c->argv[0], outs);

    } while((c = find_config_next(c, c->next, CONF_PARAM, name, FALSE)) != NULL);

    sql_log(DEBUG_FUNC, "%s", "<<< info_master");
  }

  return DECLINED(cmd);
}

MODRET errinfo_master(cmd_rec *cmd) {
  char *type = NULL;
  char *name = NULL;
  config_rec *c = NULL;
  char outs[4096] = {'\0'}, *outsp;
  char *argp = NULL; 
  char *tmp = NULL;
  modret_t *mr = NULL;
  sql_data_t *sd = NULL;

  _sql_check_cmd(cmd, "errinfo_master");

  if (!cmap.status) return DECLINED(cmd);

  /* process explicit handlers */
  name = pstrcat(cmd->tmp_pool, "SQLShowInfo_ERR_", cmd->argv[0], NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);
  if (c) {
    sql_log(DEBUG_FUNC, "%s", ">>> errinfo_master");

    /* we now have at least one config_rec.  Take the output string from 
     * each, and process it -- resolve tags, and when we find a named 
     * query, run it and get info from it. 
     */

    do {
      memset(outs, '\0', sizeof(outs));
      outsp = outs;

      for (tmp = c->argv[1]; *tmp; ) {
	if(*tmp == '%') {
	  /* is the tag a named_query reference?  If so, process the 
	   * named query, otherwise process it as a normal tag.. 
	   */
	  
	  if (*(++tmp) == '{') {
	    char *query;

	    if (*tmp!='\0') query = ++tmp;
	    
	    /* get the name of the query */
	    while ( *tmp && *tmp!='}' ) tmp++;
	    
	    query = pstrndup(cmd->tmp_pool, query, (tmp - query));

	    /* make sure it's a SELECT query */
	    
	    type = _named_query_type(cmd, query);
	    if (type && ((!strcasecmp(type, SQL_SELECT_C )) ||
			 (!strcasecmp(type, SQL_FREEFORM_C )))) {
	      mr = _process_named_query(cmd, query);
	      
	      if (MODRET_ISERROR(mr)) {
		argp = "{null}";
	      } else {
		sd = (sql_data_t *) mr->data;
		if ((sd->rnum == 0) || (!sd->data[0]))
		  argp = "{null}";
		else
		  argp = sd->data[0];
	      }
	    } else {
	      argp = "{null}";
	    }
	  } else {
	    argp=resolve_tag( cmd, *tmp);
	  }

	  sstrcat(outs, argp, sizeof(outs));
	  outsp += strlen(argp);

	  if (*tmp!='\0') tmp++;
	} else {
	  *outsp++ = *tmp++;
	}
      }
      
      *outsp++ = 0;

      /* add the response */
      pr_response_add_err( c->argv[0], outs);

    } while((c = find_config_next(c, c->next, CONF_PARAM, name, FALSE)) != NULL);

    sql_log(DEBUG_FUNC, "%s", "<<< errinfo_master");
  }

  /* process implicit handlers */
  name = pstrcat(cmd->tmp_pool, "SQLShowInfo_ERR_*", NULL);
  
  c = find_config(main_server->conf, CONF_PARAM, name, FALSE);
  if (c) {
    sql_log(DEBUG_FUNC, "%s", ">>> errinfo_master");

    /* we now have at least one config_rec.  Take the output string from 
     * each, and process it -- resolve tags, and when we find a named 
     * query, run it and get info from it. 
     */

    do {
      memset(outs, '\0', sizeof(outs));
      outsp = outs;

      for (tmp = c->argv[1]; *tmp; ) {
	if(*tmp == '%') {
	  /* is the tag a named_query reference?  If so, process the 
	   * named query, otherwise process it as a normal tag.. 
	   */
	  
	  if (*(++tmp) == '{') {
	    char *query;

	    if (*tmp!='\0') query = ++tmp;
	    
	    /* get the name of the query */
	    while ( *tmp && *tmp!='}' ) tmp++;
	    
	    query = pstrndup(cmd->tmp_pool, query, (tmp - query));

	    /* make sure it's a SELECT query */
	    
	    type = _named_query_type(cmd, query);
	    if (type && ((!strcasecmp(type, SQL_SELECT_C )) ||
			 (!strcasecmp(type, SQL_FREEFORM_C )))) {
	      mr = _process_named_query(cmd, query);
	      
	      if (MODRET_ISERROR(mr)) {
		argp = "{null}";
	      } else {
		sd = (sql_data_t *) mr->data;
		if ((sd->rnum == 0) || (!sd->data[0]))
		  argp = "{null}";
		else
		  argp = sd->data[0];
	      }
	    } else {
	      argp = "{null}";
	    }
	  } else {
	    argp=resolve_tag( cmd, *tmp);
	  }

	  sstrcat(outs, argp, sizeof(outs));
	  outsp += strlen(argp);

	  if (*tmp!='\0') tmp++;
	} else {
	  *outsp++ = *tmp++;
	}
      }
      
      *outsp++ = 0;

      /* add the response */
      pr_response_add( c->argv[0], outs);

    } while((c = find_config_next(c, c->next, CONF_PARAM, name, FALSE)) != NULL);

    sql_log(DEBUG_FUNC, "%s", "<<< errinfo_master");
  }

  return DECLINED(cmd);
}

/* sql_lookup: used by third-party modules to get data via a SQL query.  
 * Third party module must pass a legitimate cmd_rec (including tmp_pool), 
 * and the cmd_rec must have only one argument: the name of a SQLNamedQuery.
 *
 * Returns:
 *
 * DECLINED if mod_sql isn't on
 * ERROR    if named query doesn't exist
 * 
 * SHUTS DOWN if query caused an error
 * 
 * otherwise:
 *
 * array_header * in the data slot with the returned data.  It is up to the
 * calling function to know how many pieces of data to expect, and how to
 * parse them.
 */
MODRET sql_lookup(cmd_rec *cmd) {
  char *type = NULL;
  modret_t *mr = NULL;
  sql_data_t *sd = NULL;
  array_header *ah = NULL;
  int cnt = 0;

  _sql_check_cmd(cmd, "sql_lookup");

  if (!cmap.status) return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> sql_lookup");

  if (cmd->argc < 1) return ERROR(cmd);

  type = _named_query_type(cmd, cmd->argv[1]);
  if (type && ((!strcasecmp(type, SQL_SELECT_C )) ||
	       (!strcasecmp(type, SQL_FREEFORM_C )))) {
    mr = _process_named_query(cmd, cmd->argv[1]);
    
    if (!MODRET_ISERROR(mr)) {
      sd = (sql_data_t *) mr->data;

      ah = make_array(session.pool, (sd->rnum * sd->fnum) , sizeof(char *));

      /* the right way to do this is to preserve the abstraction of the array
       * header so things don't blow up when it gets freed */
      for (cnt =0; cnt< (sd->rnum * sd->fnum); cnt++) {
	*((char **) push_array(ah)) = sd->data[cnt];
      }

      mr = mod_create_data(cmd, (void *) ah);
    } else {
      /* we have an error, log it and die */
      _sql_check_response(mr);
    }
  } else {
    mr = ERROR(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< sql_lookup");
  return mr;
}

MODRET sql_change(cmd_rec *cmd) {
  char *type = NULL;
  modret_t *mr = NULL;

  _sql_check_cmd(cmd, "sql_change");

  if (!cmap.status) return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> sql_change");

  if (cmd->argc < 1) return ERROR(cmd);

  type = _named_query_type(cmd, cmd->argv[1]);
  if (type && ((!strcasecmp(type, SQL_INSERT_C)) || 
	       (!strcasecmp(type, SQL_UPDATE_C)) ||
	       (!strcasecmp(type, SQL_FREEFORM_C)))) {
    /* fixup the cmd_rec */

    mr = _process_named_query(cmd, cmd->argv[1]);
    
    if (MODRET_ISERROR(mr)) {
      /* we have an error, log it and die */
      _sql_check_response(mr);
    }
  } else {
    mr = ERROR(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< sql_change");
  return mr;
}

MODRET sql_escapestr(cmd_rec *cmd) {
  modret_t *mr;

  sql_log(DEBUG_FUNC, "%s", ">>> sql_escapestr");

  mr =_sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", cmd->argv[0]),
    "sql_escapestring");

  sql_log(DEBUG_FUNC, "%s", "<<< sql_escapestr");

  return mr;
}

/*****************************************************************
 *
 * AUTH COMMAND HANDLERS
 *
 *****************************************************************/

MODRET cmd_setpwent(cmd_rec *cmd) {
  sql_data_t *sd = NULL;
  modret_t *mr = NULL;
  char *where = NULL;
  int i = 0, cnt = 0;

  char *username = NULL;
  char *password = NULL;
  char *shell = NULL;
  char *dir = NULL;
  uid_t uid = 0;
  gid_t gid = 0;
  
  struct passwd lpw;

  _sql_check_cmd(cmd, "cmd_setpwent");

  if (!SQL_USERSET)
    return (SQL_USERGOD ? mod_create_data(cmd,(void *)0):DECLINED(cmd));

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_setpwent");

  /* if we've already filled the passwd cache, just reset the curr_passwd */
  if ( cmap.passwd_cache_filled ) {
    cmap.curr_passwd = passwd_name_cache->head;
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_setpwent");
    return (SQL_USERGOD ? mod_create_data( cmd, (void *) 1 ) : DECLINED(cmd));
  }

  /* single select or not? */
  if (SQL_FASTUSERS) {
    /* retrieve our list of passwds */
    where = _sql_where(cmd->tmp_pool, 1, cmap.userwhere );

    mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4, "default",
				       cmap.usrtable, cmap.usrfields, where ),
			"sql_select" );
    _sql_check_response(mr);
  
    sd = (sql_data_t *) mr->data;
    
    /* walk through the array, adding users to the cache */
    for ( i = 0, cnt = 0; cnt < sd->rnum; cnt++ ) {
      username = sd->data[i++];

      /* if the username is NULL, skip it */
      if ( username == NULL ) continue;

      password = sd->data[i++];
      
      uid = cmap.defaultuid;
      if (cmap.uidfield) {
	if (sd->data[i]) {
	  uid = atoi(sd->data[i++]);
	} else {
	  i++;
	}
      }
      
      gid = cmap.defaultgid;
      if (cmap.gidfield) {
	if (sd->data[i]) {
	  gid = atoi(sd->data[i++]);
	} else {
	  i++;
	}
      }

      dir = cmap.defaulthomedir;
      if (sd->data[i]) {
        if (strcmp(sd->data[i], "") == 0 ||
            strcmp(sd->data[i], "NULL") == 0)

          /* Leave dir pointing to the SQLDefaultHomedir, if any. */
          i++;

        else
          dir = sd->data[i++];
      }

      if (cmap.shellfield)
	shell = sd->data[i++];
      else
	shell =  "";
      
      if (uid < cmap.minuseruid)
	uid = cmap.defaultuid;
      if (gid < cmap.minusergid)
	gid = cmap.defaultgid;
      
      _sql_addpasswd(cmd, username, password, uid, gid, shell, dir);
    } 
  } else {
    /* retrieve our list of passwds */
    where = _sql_where(cmd->tmp_pool, 1, cmap.userwhere );
    
    mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4, "default",
				       cmap.usrtable, cmap.usrfield, where ),
			"sql_select" );
    _sql_check_response(mr);
    
    sd = (sql_data_t *) mr->data;
    
    for ( cnt = 0; cnt < sd->rnum; cnt++ ) {
      username = sd->data[cnt];
      
      /* if the username is NULL for whatever reason, skip it */
      if ( username == NULL ) continue;
      
      /* otherwise, add it to the cache */
      lpw.pw_uid = -1;
      lpw.pw_name = username;
      _sql_getpasswd(cmd, &lpw);
    }
  }
  
  cmap.passwd_cache_filled = 1;
  cmap.curr_passwd = passwd_name_cache->head;

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_setpwent");

  return (SQL_USERGOD ? mod_create_data( cmd, (void *) 1 ) : DECLINED(cmd));
}

MODRET cmd_getpwent(cmd_rec *cmd) {
  struct passwd *pw;
  modret_t *mr;

  _sql_check_cmd(cmd, "cmd_getpwent");

  if (!SQL_USERSET)
    return (SQL_USERGOD ? mod_create_data(cmd,(void *)NULL):DECLINED(cmd));

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getpwent");

  /* make sure our passwd cache is complete  */
  if ( !cmap.passwd_cache_filled ) {
    mr = cmd_setpwent(cmd);
    if ( mr->data == ( void * ) 0 ) {
      /* something didn't work in the setpwent call */
      sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwent");
      return (SQL_USERGOD  ? mod_create_data( cmd, (void *) NULL ) : DECLINED(cmd));
    }
  }

  if (cmap.curr_passwd != NULL) {
    pw = (struct passwd *) cmap.curr_passwd->data;
    cmap.curr_passwd = cmap.curr_passwd->list_next;

  } else {
    pw = NULL;
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwent");

  if (pw == NULL || pw->pw_uid == (uid_t) -1)
    return (SQL_USERGOD ? mod_create_data( cmd, (void *) pw) : DECLINED(cmd) );

  return mod_create_data( cmd, (void *) pw);
}

MODRET cmd_endpwent(cmd_rec *cmd) {
  _sql_check_cmd(cmd, "cmd_endpwent");

  if (!SQL_USERSET)
    return (SQL_USERGOD ? mod_create_data(cmd,(void *)0):DECLINED(cmd));
  
  sql_log(DEBUG_FUNC, "%s", ">>> cmd_endpwent");
  
  cmap.curr_passwd = NULL;
  
  sql_log(DEBUG_FUNC, "%s", "<<< cmd_endpwent");
  
  return (SQL_USERGOD ? mod_create_data( cmd, (void *) 0 ) : DECLINED(cmd));
}

MODRET cmd_setgrent(cmd_rec *cmd) {
  modret_t *mr = NULL;
  sql_data_t *sd = NULL;
  int cnt = 0;
  struct group lgr;
  gid_t gid;
  char *groupname = NULL;
  char *grp_mem = NULL;
  char *where = NULL;
  array_header *ah =NULL;
  char *iterator = NULL;
  char *member = NULL;

  _sql_check_cmd(cmd, "cmd_setgrent");

  if (!SQL_GROUPSET)
    return (SQL_GROUPGOD ? mod_create_data(cmd,(void *)0):DECLINED(cmd));

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_setgrent");

  /* if we've already filled the passwd group, just reset curr_group */
  if ( cmap.group_cache_filled ) {
    cmap.curr_group = group_name_cache->head;
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_setgrent");
    return (SQL_GROUPGOD ? mod_create_data( cmd, (void *) 1 ) : DECLINED(cmd));
  }

  if (SQL_FASTGROUPS) {
    /* retrieve our list of groups */
    where = _sql_where(cmd->tmp_pool, 1, cmap.groupwhere);
    
    mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 6, "default",
				       cmap.grptable, cmap.grpfields, where, NULL ),
			"sql_select");
    _sql_check_response(mr);
    
    sd = (sql_data_t *) mr->data;
    
    /* for each group, fill our array header and call _sql_addgroup */

    for ( cnt = 0; cnt < sd->rnum; cnt ++ ) {
      /* if the groupname is NULL for whatever reason, skip the row */
      groupname = sd->data[cnt * 3];
      if ( groupname == NULL ) continue;

      gid = (gid_t) atol(sd->data[(cnt * 3) + 1]);
      grp_mem = sd->data[(cnt * 3) + 2];
      
      ah = make_array(cmd->tmp_pool, 10, sizeof(char *));
      iterator = grp_mem;

      for (member = strsep(&iterator, " ,"); member; member = strsep(&iterator, " ,")) {
	if (*member=='\0') continue;
	*((char **) push_array(ah)) = member;
      }

      _sql_addgroup(cmd, groupname, gid, ah);
    }
  } else {
    /* retrieve our list of groups */
    where = _sql_where(cmd->tmp_pool, 1, cmap.groupwhere);
    
    mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 6, "default",
				       cmap.grptable, cmap.grpfield, where, NULL, "DISTINCT" ),
			"sql_select");
    _sql_check_response(mr);
    
    sd = (sql_data_t *) mr->data;
    
    for ( cnt = 0; cnt < sd->rnum; cnt++ ) {
      groupname = sd->data[cnt];
      
      /* if the groupname is NULL for whatever reason, skip it */
      if ( groupname == NULL ) continue;
      
      /* otherwise, add it to the cache */
      lgr.gr_gid = -1;
      lgr.gr_name = groupname;
      
      _sql_getgroup(cmd, &lgr);
    }
  }
  
  cmap.group_cache_filled = 1;
  cmap.curr_group = group_name_cache->head;

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_setgrent");

  return (SQL_GROUPGOD ? mod_create_data( cmd, (void *) 1 ) : DECLINED(cmd));
}

MODRET cmd_getgrent(cmd_rec *cmd) {
  struct group *gr;
  modret_t *mr;

  _sql_check_cmd(cmd, "cmd_getgrent");

  if (!SQL_GROUPSET)
    return (SQL_GROUPGOD ? mod_create_data(cmd,(void *)NULL):DECLINED(cmd));

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getgrent");

  /* make sure our group cache is complete  */
  if ( !cmap.group_cache_filled ) {
    mr = cmd_setgrent(cmd);
    if ( mr->data == ( void * ) 0 ) {
      /* something didn't work in the setgrent call */
      sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrent");
      return (SQL_GROUPGOD ? mod_create_data( cmd, (void *) NULL ) : DECLINED(cmd));
    }
  }

  if (cmap.curr_group != NULL) {
    gr = (struct group *) cmap.curr_group->data;
    cmap.curr_group = cmap.curr_group->list_next;

  } else {
    gr = NULL;
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrent");

  if (gr == NULL || gr->gr_gid == (gid_t) -1)
    return (SQL_GROUPGOD ? mod_create_data( cmd, (void *) gr) : DECLINED(cmd));

  return mod_create_data( cmd, (void *) gr);
}

MODRET cmd_endgrent(cmd_rec *cmd) {
  _sql_check_cmd(cmd, "cmd_endgrent");

  if (!SQL_GROUPSET)
    return (SQL_GROUPGOD ? mod_create_data(cmd,(void *)0):DECLINED(cmd));

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_endgrent");

  cmap.curr_group = NULL;

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_endgrent");

  return (SQL_GROUPGOD ? mod_create_data( cmd, (void *) 0 ) : DECLINED(cmd));
}

MODRET cmd_getpwnam(cmd_rec *cmd) {
  struct passwd *pw;
  struct passwd lpw;

  _sql_check_cmd(cmd, "getpwnam");

  if (!SQL_USERS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getpwnam");

  lpw.pw_uid = -1;
  lpw.pw_name = cmd->argv[0];
  pw = _sql_getpasswd(cmd, &lpw);

  if (pw == NULL || pw->pw_uid == (uid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwnam");
    return SQL_USERGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwnam");

  return mod_create_data(cmd, pw);
}

MODRET cmd_getpwuid(cmd_rec *cmd) {
  struct passwd *pw;
  struct passwd lpw;

  _sql_check_cmd(cmd, "cmd_getpwuid");

  if (!SQL_USERS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getpwuid");

  lpw.pw_uid = (uid_t) cmd->argv[0];
  lpw.pw_name = NULL;
  pw = _sql_getpasswd(cmd, &lpw);

  if (pw == NULL || pw->pw_uid == (uid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwuid");
    return SQL_USERGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getpwuid");

  return mod_create_data(cmd, pw);
}

MODRET cmd_getgrnam(cmd_rec *cmd) {
  struct group *gr;
  struct group lgr;

  _sql_check_cmd(cmd, "cmd_getgrname");

  if (!SQL_GROUPS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getgrnam");

  lgr.gr_gid = -1;
  lgr.gr_name = cmd->argv[0];
  gr = _sql_getgroup(cmd, &lgr);

  if (gr == NULL || gr->gr_gid == (gid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrnam");
    return SQL_GROUPGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrnam");
  return mod_create_data(cmd, gr);
}

MODRET cmd_getgrgid(cmd_rec *cmd) {
  struct group *gr;
  struct group lgr;

  _sql_check_cmd(cmd, "cmd_getgrgid");

  if (!SQL_GROUPS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getgrgid");

  lgr.gr_gid = (gid_t) cmd->argv[0];
  lgr.gr_name = NULL;
  gr = _sql_getgroup(cmd, &lgr);

  if (gr == NULL || gr->gr_gid == (gid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrgid");
    return SQL_GROUPGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgrgid");
  return mod_create_data(cmd, gr);
}

MODRET cmd_auth(cmd_rec *cmd) {
  char *user = NULL;
  struct passwd lpw, *pw;
  modret_t *mr = NULL;

  _sql_check_cmd(cmd, "cmd_auth");

  if (!SQL_USERS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_auth");

  /* fix up the username, removing leading and trailing whitespace */
  user = pr_str_strip(cmd->tmp_pool, cmd->argv[0]);

  /* escape our username */
  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 2, "default", 
				     user ), "sql_escapestring" );
  _sql_check_response(mr);
  
  user = (char *) mr->data;

  lpw.pw_uid = -1;
  lpw.pw_name = cmd->argv[0];

  if ((pw = _sql_getpasswd(cmd, &lpw)) && 
      !auth_check(cmd->tmp_pool, pw->pw_passwd, cmd->argv[0], cmd->argv[1])) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_auth");
    return HANDLED(cmd);
  } else {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_auth");
    return ( SQL_USERGOD ? ERROR_INT(cmd, PR_AUTH_BADPWD) : DECLINED(cmd) );
  }
}

MODRET cmd_check(cmd_rec *cmd) {
  /*
   * should we bother to see if the hashed password is what we have in the
   * database? or do we simply assume it is, and ignore the fact that we're
   * being passed the username, too? 
   */

  array_header *ah = cmap.authlist;
  auth_type_entry *auth_entry;
  char *c_hash;
  char *c_clear;
  int success = 0;
  int cnt = 0;
  struct passwd lpw;
  struct stat st;
  modret_t *mr = NULL;

  _sql_check_cmd(cmd, "cmd_check");

  if (!SQL_USERS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_check");

  if (cmd->argv[0] == NULL) {
    sql_log(DEBUG_AUTH, "%s", "NULL hashed password");
  } else if (cmd->argv[2] == NULL) {
    sql_log(DEBUG_AUTH, "%s", "NULL clear password");
  } else {
    c_hash = cmd->argv[0];
    c_clear = cmd->argv[2];

    if (!ah)
      sql_log(DEBUG_AUTH, "%s", "warning: no SQLAuthTypes configured");

    for (cnt = 0; ah && cnt < ah->nelts; cnt++) {
      auth_entry = ((auth_type_entry **) ah->elts)[cnt];
      sql_log(DEBUG_AUTH, "checking auth_type %s", auth_entry->name);

      mr = auth_entry->check_function(cmd, c_clear, c_hash);
      if (!MODRET_ISERROR(mr)) {
	sql_log(DEBUG_AUTH, "'%s' auth handler reports success",
          auth_entry->name);
	success = 1;
	break;
      }
    }
  }

  if (success) {
    /* this and the associated hack in cmd_uid2name are to support
     * uid reuse in the database -- people (for whatever reason) are
     * reusing uids/gids multiple times, and the displayed owner in a 
     * LIST or NLST needs to match the current user if possible.  This
     * depends on the fact that if we get success, the user exists in the
     * database ( -- is this always true? ).
     */

    lpw.pw_uid = -1;
    lpw.pw_name = cmd->argv[1];
    cmap.authpasswd = _sql_getpasswd(cmd, &lpw);

    /*
     * finally, build the user's homedir if necessary 
     */
    
    if (cmap.buildhomedir && cmap.authpasswd &&
	(stat(cmap.authpasswd->pw_dir, &st) == -1 && errno == ENOENT)) {
      build_homedir(cmd, cmap.authpasswd->pw_dir, 
		    S_IRWXU | S_IRWXG | S_IRWXO, 
		    cmap.authpasswd->pw_uid,
		    cmap.authpasswd->pw_gid);
    }
    
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_check");
    return HANDLED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_check");

  if (!success)
    return SQL_USERGOD ? ERROR(cmd) : DECLINED(cmd);

  return mr;
}

MODRET cmd_uid2name(cmd_rec *cmd) {
  char *uid_name = NULL;
  struct passwd *pw;
  struct passwd lpw;
  char uidstr[MOD_SQL_BUFSIZE] = {'\0'};

  _sql_check_cmd(cmd, "cmd_uid2name");

  if (!SQL_USERS)
    return DECLINED(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_uid2name");

  lpw.pw_uid = (uid_t) cmd->argv[0];
  lpw.pw_name = NULL;

  /* check to see if we're looking up the current user */
  if ( cmap.authpasswd && (lpw.pw_uid == cmap.authpasswd->pw_uid)) {
    sql_log(DEBUG_INFO, "%s", "matched current user");
    pw = cmap.authpasswd;
  } else {
    pw = _sql_getpasswd(cmd, &lpw);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_uid2name");

  if (pw == NULL) {
    if (!SQL_USERGOD)
      return DECLINED(cmd);

    snprintf( uidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) cmd->argv[0]);
    return mod_create_data(cmd, uidstr);
  }

  /* In the case of a lookup of a negatively cached UID, the pw_name
   * member will be NULL, which causes an undesired handling by
   * the core code.  Handle this case separately.
   */
  if (pw->pw_name)
    uid_name = pw->pw_name;

  else {
    snprintf( uidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) cmd->argv[0]);
    uid_name = uidstr;
  }

  return mod_create_data(cmd, uid_name);
}

MODRET cmd_gid2name(cmd_rec *cmd) {
  char *gid_name = NULL;
  struct group *gr;
  struct group lgr;
  char gidstr[MOD_SQL_BUFSIZE]={'\0'};

  _sql_check_cmd(cmd, "cmd_gid2name");

  if (!SQL_GROUPS) {
    return DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_gid2name");

  lgr.gr_gid = (gid_t) cmd->argv[0];
  lgr.gr_name = NULL;
  gr = _sql_getgroup(cmd, &lgr);

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_gid2name");

  if (gr == NULL) {
    if (!SQL_GROUPGOD)
      return DECLINED(cmd);

    snprintf( gidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) cmd->argv[0]);
    return mod_create_data(cmd, gidstr);
  }

  /* In the case of a lookup of a negatively cached GID, the gr_name
   * member will be NULL, which causes an undesired handling by
   * the core code.  Handle this case separately.
   */
  if (gr->gr_name)
    gid_name = gr->gr_name;

  else {
    snprintf( gidstr, MOD_SQL_BUFSIZE, "%lu", (unsigned long) cmd->argv[0]);
    gid_name = gidstr;
  }

  return mod_create_data(cmd, gid_name);
}

MODRET cmd_name2uid(cmd_rec *cmd) {
  struct passwd *pw;
  struct passwd lpw;

  _sql_check_cmd(cmd, "cmd_name2uid");

  if (!SQL_USERS) {
    return DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_name2uid");

  lpw.pw_uid = -1;
  lpw.pw_name = cmd->argv[0];

  /* check to see if we're looking up the current user */
  if (cmap.authpasswd && 
      (strcmp(lpw.pw_name, cmap.authpasswd->pw_name) == 0)) {
    sql_log(DEBUG_INFO, "%s", "matched current user");
    pw = cmap.authpasswd;
  } else {
    pw = _sql_getpasswd(cmd, &lpw);
  }

  if (pw == NULL || pw->pw_uid == (uid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_name2uid");
    return SQL_USERGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_name2uid");

  return mod_create_data(cmd, (void *) pw->pw_uid);
}

MODRET cmd_name2gid(cmd_rec *cmd) {
  struct group *gr;
  struct group lgr;

  _sql_check_cmd(cmd, "cmd_name2gid");

  if (!SQL_GROUPS) {
    return DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_name2gid");

  lgr.gr_gid = -1;
  lgr.gr_name = cmd->argv[0];
  gr = _sql_getgroup(cmd, &lgr);

  if (gr == NULL || gr->gr_gid == (gid_t) -1) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_name2gid");
    return SQL_GROUPGOD ? ERROR(cmd) : DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_name2gid");

  return mod_create_data(cmd, (void *) gr->gr_gid);
}

MODRET cmd_getgroups(cmd_rec *cmd) {
  int res;

  _sql_check_cmd(cmd, "cmd_getgroups");

  if (!SQL_GROUPS) {
    return DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getgroups");

  res = _sql_getgroups(cmd);

  if (res < 0) {
    sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgroups");
    return SQL_GROUPGOD ? ERROR(cmd) : DECLINED(cmd); 
  }

  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getgroups");

  return mod_create_data(cmd, (void *) res);
}

MODRET cmd_getstats(cmd_rec *cmd) {
  modret_t *mr;
  char *query;
  sql_data_t *sd;
  char *usrwhere, *where;

  _sql_check_cmd(cmd, "cmd_getstats");

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getstats");

  if (!cmap.sql_fstor) {
    return DECLINED(cmd);
  }

  usrwhere = pstrcat(cmd->tmp_pool, cmap.usrfield, " = '", _sql_realuser(cmd), "'", NULL);
  where = _sql_where(cmd->tmp_pool, 2, usrwhere, cmap.userwhere );
  
  query = pstrcat(cmd->tmp_pool, cmap.sql_fstor, ", ",
		  cmap.sql_fretr, ", ", cmap.sql_bstor, ", ",
		  cmap.sql_bretr, NULL );
  
  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4, "default",
				     cmap.usrtable, query, where ),
		      "sql_select" );
  _sql_check_response(mr);
  
  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getstats");

  sd = mr->data;

  if (sd->rnum == 0)
    return ERROR(cmd);

  return mod_create_data(cmd, sd->data);
}

MODRET cmd_getratio(cmd_rec *cmd) {
  modret_t *mr;
  char *query;
  sql_data_t *sd;
  char *usrwhere, *where;

  _sql_check_cmd(cmd, "cmd_getratio");

  if (!cmap.sql_frate) {
    return DECLINED(cmd);
  }

  sql_log(DEBUG_FUNC, "%s", ">>> cmd_getratio");

  usrwhere = pstrcat(cmd->tmp_pool, cmap.usrfield, " = '", _sql_realuser(cmd), "'", NULL);
  where = _sql_where(cmd->tmp_pool, 2, usrwhere, cmap.userwhere );
  
  query = pstrcat(cmd->tmp_pool, cmap.sql_frate, ", ",
		  cmap.sql_fcred, ", ", cmap.sql_brate, ", ",
		  cmap.sql_bcred, NULL);
  
  mr = _sql_dispatch( _sql_make_cmd( cmd->tmp_pool, 4, "default",
				     cmap.usrtable, query, where ),
		      "sql_select" );
  _sql_check_response(mr);
  
  sql_log(DEBUG_FUNC, "%s", "<<< cmd_getratio");

  sd = mr->data;

  if (sd->rnum == 0)
    return ERROR(cmd);

  return mod_create_data(cmd, sd->data);
}

/*****************************************************************
 *
 * CONFIGURATION DIRECTIVE HANDLERS
 *
 *****************************************************************/

MODRET set_sqlratiostats(cmd_rec * cmd)
{
  int b;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL);

  switch (cmd->argc - 1) {
  default:
    CONF_ERROR(cmd, "requires a boolean or 4 field names: "
               "fstor fretr bstor bretr");
  case 1:
    if ((b = get_boolean(cmd, 1)) == -1)
      CONF_ERROR(cmd, "requires a boolean or 4 field names: "
                 "fstor fretr bstor bretr");
    if (b)
      add_config_param_str("SQLRatioStats", 4,
                           "fstor", "fretr", "bstor", "bretr");
    break;

  case 4:
    add_config_param_str("SQLRatioStats", 4,
                         (void *) cmd->argv[1], (void *) cmd->argv[2],
                         (void *) cmd->argv[3], (void *) cmd->argv[4]);
  }

  return HANDLED(cmd);
}

MODRET set_sqlnegativecache(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 a Boolean parameter");

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

  return HANDLED(cmd);
}

MODRET set_sqlratios(cmd_rec * cmd)
{
  int b;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL);

  switch (cmd->argc - 1) {
  default:
    CONF_ERROR(cmd, "requires a boolean or 4 field names: "
               "frate fcred brate bcred");
  case 1:
    if ((b = get_boolean(cmd, 1)) == -1)
      CONF_ERROR(cmd, "requires a boolean or 4 field names: "
                 "frate fcred brate bcred");
    if (b)
      add_config_param_str("SQLRatios", 4,
                           "frate", "fcred", "brate", "bcred");
    break;

  case 4:
    add_config_param_str("SQLRatios", 4,
                         (void *) cmd->argv[1], (void *) cmd->argv[2],
                         (void *) cmd->argv[3], (void *) cmd->argv[4]);
  }

  return HANDLED(cmd);
}

MODRET add_virtualstr(char * name, cmd_rec * cmd)
{
  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  add_config_param_str(name, 1, (void *) cmd->argv[1]);

  return HANDLED(cmd);
}

MODRET add_virtualbool(char * name, cmd_rec * cmd)
{
  int b;
  config_rec *c;

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

  b = get_boolean(cmd, 1);
  if (b == -1)
    CONF_ERROR(cmd, "requires a boolean value");

  c = add_config_param(name, 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
  *((unsigned char *) c->argv[0]) = b;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

/* usage: SQLUserInfo table(s) usernamefield passwdfield uid gid homedir
 *           shell | custom:/<sql-named-query>
 */
MODRET set_sqluserinfo(cmd_rec * cmd) {

  if (cmd->argc-1 != 1 && cmd->argc-1 != 7)
    CONF_ERROR(cmd, "missing parameters");
  CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL);

  if (cmd->argc-1 == 1) {
    char *tmp = NULL;

    /* If only one paramter is used, it must be of the "custom:/" form. */
    if (strncmp("custom:/", cmd->argv[1], 8) != 0)
      CONF_ERROR(cmd, "badly formatted parameter");

    tmp = strchr(cmd->argv[1], '/');

    add_config_param_str("SQLCustomUserInfo", 1, tmp+1);
    return HANDLED(cmd);
  }

  /* required to exist - not even going to check them. */
  add_config_param_str("SQLUserTable", 1, (void *) cmd->argv[1]);
  add_config_param_str("SQLUsernameField", 1, (void *) cmd->argv[2]);
  add_config_param_str("SQLPasswordField", 1, (void *) cmd->argv[3]);

  /* These could be "NULL" */
  if (strncasecmp("null", cmd->argv[4], 4) != 0)
    add_config_param_str("SQLUidField", 1, (void *) cmd->argv[4]);

  if (strncasecmp("null", cmd->argv[5], 4) != 0)
    add_config_param_str("SQLGidField", 1, (void *) cmd->argv[5]);

  if (strncasecmp("null", cmd->argv[6], 4) != 0)
    add_config_param_str("SQLHomedirField", 1, (void *) cmd->argv[6]);

  if (strncasecmp("null", cmd->argv[7], 4) != 0)
    add_config_param_str("SQLShellField", 1, (void *) cmd->argv[7]);

  return HANDLED(cmd);
}

MODRET set_sqluserwhereclause(cmd_rec * cmd) {
  return add_virtualstr( "SQLUserWhereClause", cmd);
}

/* usage: SQLGroupInfo table(s) groupnamefield gidfield membersfield */
MODRET set_sqlgroupinfo(cmd_rec *cmd) {

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

  /* required to exist - not even going to check them. */
  add_config_param_str("SQLGroupTable", 1, (void *) cmd->argv[1]);
  add_config_param_str("SQLGroupnameField", 1, (void *) cmd->argv[2]);
  add_config_param_str("SQLGroupGIDField", 1, (void *) cmd->argv[3]);
  add_config_param_str("SQLGroupMembersField", 1, (void *) cmd->argv[4]);

  return HANDLED(cmd);
}

MODRET set_sqlgroupwhereclause(cmd_rec *cmd) {
  return add_virtualstr( "SQLGroupWhereClause", cmd);
}

MODRET set_sqldefaulthomedir(cmd_rec *cmd) {
  return add_virtualstr("SQLDefaultHomedir", cmd);
}

MODRET set_sqlhomedirondemand(cmd_rec *cmd) {
  return add_virtualbool( "SQLHomedirOnDemand", cmd);
}

/* usage: SQLLog cmdlist query-name */
MODRET set_sqllog(cmd_rec *cmd) {
  config_rec * c;
  char *name, *namep;
  char *cmds;
  char *iterator;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  if ((cmd->argc < 3) || (cmd->argc > 4)) {
    CONF_ERROR( cmd, "expected cmdlist query-name [IGNORE_ERRORS]" );
  }

  /* for each element in the command list, add a 'SQLLog_CMD' config_rec..
   * this is an optimization that speeds up logging and also simplifies the
   * logging code, since there's no need to run through and parse a bunch
   * of potenitally unused SQLLog statements each time any command is run.
   */
  
  cmds= cmd->argv[1];
  iterator=cmds;

  for (name = strsep( &iterator, ", " ); name; name = strsep( &iterator, ", ")) {
    if ( *name=='\0' ) continue;
    for (namep = name; *namep != '\0'; namep++)
      *namep =  toupper( *namep );
    
    name = pstrcat( cmd->tmp_pool, "SQLLog_", name, NULL);
    
    if ((cmd->argc == 4) && (!strcasecmp(cmd->argv[3], "IGNORE_ERRORS"))) {
      c = add_config_param_str(name, 2, cmd->argv[2], "ignore");
    } else {
      c = add_config_param_str(name, 1, cmd->argv[2]);
    }

    c->flags |= CF_MERGEDOWN_MULTI;
  }
  
  return HANDLED(cmd);
}

MODRET set_sqllogfile(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: SQLNamedQuery name type query-string */
MODRET set_sqlnamedquery(cmd_rec * cmd) {

  config_rec *c = NULL;
  char *name = NULL;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  if (cmd->argc < 3) {
    CONF_ERROR( cmd, "requires at least 2 arguments" );
  }

  name = pstrcat( cmd->tmp_pool, "SQLNamedQuery_", cmd->argv[1], NULL );

  if (!strcasecmp(cmd->argv[2], "SELECT")) {
    if (cmd->argc != 4) 
      CONF_ERROR(cmd, "expected 'SELECT' query-string");

    c=add_config_param_str(name, 2, SQL_SELECT_C, cmd->argv[3] );
  } else if (!strcasecmp(cmd->argv[2], "FREEFORM")) {
    if (cmd->argc != 4) 
      CONF_ERROR(cmd, "expected 'FREEFORM' query-string");

    c=add_config_param_str(name, 2, SQL_FREEFORM_C, cmd->argv[3] );
  } else if (!strcasecmp(cmd->argv[2], "INSERT")) {
    if (cmd->argc != 5) 
      CONF_ERROR(cmd, "expected 'INSERT' query-string table-name");

    c=add_config_param_str(name, 3, SQL_INSERT_C, cmd->argv[3], cmd->argv[4] );
  } else if (!strcasecmp(cmd->argv[2], "UPDATE")) {
    if (cmd->argc != 5) 
      CONF_ERROR(cmd, "expected 'UPDATE' query-string table-name");

    c=add_config_param_str(name, 3, SQL_UPDATE_C, cmd->argv[3], cmd->argv[4] );
  } else {
    CONF_ERROR(cmd, "type must be SELECT, INSERT, UPDATE, or FREEFORM");
  }

  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

/* usage: SQLShowInfo cmdlist numeric format-string */
MODRET set_sqlshowinfo(cmd_rec * cmd) {

  config_rec *c = NULL;
  char *iterator = NULL;
  char *namep = NULL;
  char *name = NULL;
  char *cmds = NULL;

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

  cmds = pstrdup(cmd->tmp_pool, cmd->argv[1]);
  iterator = cmds;

  for (name = strsep( &iterator, ", " ); name; name=strsep( &iterator, ", ")) {
    if ( *name=='\0' ) continue;
    for (namep = name; *namep != '\0'; namep++)
      *namep=toupper(*namep);
    
    name = pstrcat( cmd->tmp_pool, "SQLShowInfo_", name, NULL);
    
    c = add_config_param_str(name, 2, cmd->argv[2], cmd->argv[3] );
    c->flags |= CF_MERGEDOWN_MULTI;
  }

  return HANDLED(cmd);
}

MODRET set_sqlauthenticate(cmd_rec * cmd) {
  config_rec *c = NULL;
  char *arg = NULL;
  int authmask = 0;
  int cnt = 0;

  int groupset_flag = 0;
  int userset_flag = 0;
  int groups_flag = 0;
  int users_flag = 0;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  if ((cmd->argc<2) || (cmd->argc>5))
    CONF_ERROR(cmd, "requires 1 to 4 arguments. Check the mod_sql docs.");

  /* we're setting our authmask here -- we have a bunch of checks needed to
   * make sure users aren't trying to screw around with us.
   */

  if ((cmd->argc == 2) && !strcasecmp(cmd->argv[1], "on")) {
    authmask = SQL_AUTH_GROUPSET | SQL_AUTH_USERSET | 
      SQL_AUTH_USERS | SQL_AUTH_GROUPS;
  } else if (!((cmd->argc == 2) && !strcasecmp(cmd->argv[1], "off"))) {
    for (cnt = 1; cnt < cmd->argc; cnt++) {
      arg = cmd->argv[cnt];
      
      if (!strncasecmp("groupset", arg, 8)) {
	if (groupset_flag)
	  CONF_ERROR(cmd, "groupset already set");
	
	if (!strcasecmp("groupsetfast", arg)) {
	  authmask |= SQL_FAST_GROUPSET;
	} else if (strlen(arg) > 8) {
	  CONF_ERROR(cmd, "unknown argument");
	}
	
	authmask |= SQL_AUTH_GROUPSET;
	groupset_flag = 1;
      } else if (!strncasecmp("userset", arg, 7)) {
	if (userset_flag)
	  CONF_ERROR(cmd, "userset already set");
	
	if (!strcasecmp("usersetfast", arg)) {
	  authmask |= SQL_FAST_USERSET;
	} else if (strlen(arg) > 7) {
	  CONF_ERROR(cmd, "unknown argument");
	}
	
	authmask |= SQL_AUTH_USERSET;
	userset_flag = 1;
      } else if (!strncasecmp("groups", arg, 6)) {
	if (groups_flag)
	  CONF_ERROR(cmd, "groups already set");
	
	if (!strcasecmp("groups*", arg)) {
          pr_log_debug(DEBUG1, "%s: use of * in SQLAuthenticate has been deprecated.  Use AuthOrder for setting authoritativeness", cmd->argv[0]);

	} else if (strlen(arg) > 6) {
	  CONF_ERROR(cmd, "unknown argument");
	}
	
	authmask |= SQL_AUTH_GROUPS;
	groups_flag = 1;
      } else if (!strncasecmp("users", arg, 5)) {
	if (users_flag)
	  CONF_ERROR(cmd, "users already set");
	
	if (!strcasecmp("users*", arg)) {
          pr_log_debug(DEBUG1, "%s: use of * in SQLAuthenticate has been deprecated.  Use AuthOrder for setting authoritativeness", cmd->argv[0]);

	} else if (strlen(arg) > 5) {
	  CONF_ERROR(cmd, "unknown argument");
	}
	
	authmask |= SQL_AUTH_USERS;
	users_flag = 1;
      } else {
	CONF_ERROR( cmd, "unknown argument");
      }
    } 
  }
  
  /* finally, fixup if we've received groupset with no groups,
   * or userset with no users
   */
  if ((groupset_flag && !groups_flag) ||
      (userset_flag && !users_flag)) {
    CONF_ERROR( cmd, "groupset and userset have no meaning without "
		"a corresponding groups or users argument.");
  }

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

  return HANDLED(cmd);
}

/* logging stuff */

static char *sql_logfile = NULL;
static int sql_logfd = -1;

static int sql_closelog(void) {

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

  return 0;
}

int sql_log(int level, 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 (!sql_logfile)
    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_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(sql_logfd, buf, strlen(buf)) < 0)
    return -1;

  return 0;
}

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

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

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

  pr_signals_block();
  PRIVS_ROOT
  res = pr_log_openfile(sql_logfile, &sql_logfd, 0640);
  PRIVS_RELINQUISH
  pr_signals_unblock();

  return res;
}

MODRET set_sqlconnectinfo(cmd_rec * cmd) {
  config_rec *c;
  char *info = NULL;
  char *user = "";
  char *pass = "";
  char *ttl = NULL;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  if ((cmd->argc < 2) || (cmd->argc > 5))
    CONF_ERROR(cmd, "requires 1 to 4 arguments.  Check the mod_sql docs.");

  if (cmd->argc > 1)
    info = cmd->argv[1];

  if (cmd->argc > 2)
    user = cmd->argv[2];

  if (cmd->argc > 3)
    pass = cmd->argv[3];

  if (cmd->argc > 4)
    ttl = cmd->argv[4];
  else
    ttl = "0";

  c = add_config_param_str("SQLConnectInfo", 4,
                           (void *) info, (void *) user, (void *) pass, (void *) ttl);

  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqlauthtypes(cmd_rec * cmd) {
  config_rec *c;
  array_header *ah;
  auth_type_entry *auth_entry;
  auth_type_entry **auth_handle;
  int cnt;

  CHECK_CONF(cmd, CONF_ROOT | CONF_GLOBAL | CONF_VIRTUAL);

  /*
   * need *at least* one handler 
   */
  if (cmd->argc < 2) {
    CONF_ERROR(cmd, "expected at least one handler type");
  }

  ah = make_array( permanent_pool, cmd->argc - 1, sizeof(auth_type_entry *));

  /*
   * walk through our cmd->argv 
   */
  for (cnt = 1; cnt < cmd->argc; cnt++) {
    auth_entry = get_auth_entry(cmd->argv[cnt]);
    if (auth_entry == NULL) {
      sql_log(DEBUG_WARN, "unknown auth handler '%s'", cmd->argv[cnt]);
      CONF_ERROR(cmd, "unknown auth handler");
    }

    auth_handle = (auth_type_entry **) push_array(ah);
    *auth_handle = auth_entry;
  }

  c = add_config_param("SQLAuthTypes", 1, (void *) ah);
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqlminid(cmd_rec * cmd) {
  config_rec *c;
  unsigned long val;
  char *endptr = NULL;

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

  val = strtoul(cmd->argv[1], &endptr, 10);

  if (*endptr != '\0') {
    CONF_ERROR(cmd, "requires a numeric argument");
  }

  /*
   * whee! need to check if in the legal range for uid_t and gid_t 
   */
  /*
   * however, I can't think of a cross-platform way of doing this.. if
   * anyone knows of a way to find the MAX uid_t/gid_t, let me know.. 
   */
  if ((val == ULONG_MAX) && (errno == ERANGE)) {
    CONF_ERROR(cmd, "the value given is outside the legal range");
  }

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
  *((unsigned long *) c->argv[0]) = val;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqlminuseruid(cmd_rec * cmd) {
  config_rec *c = NULL;
  unsigned long val;
  char *endptr = NULL;

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

  val = strtoul(cmd->argv[1], &endptr, 10);

  if (*endptr != '\0') {
    CONF_ERROR(cmd, "requires a numeric argument");
  }

  /*
   * whee! need to check if in the legal range for uid_t
   */
  /*
   * however, I can't think of a cross-platform way of doing this.. if
   * anyone knows of a way to find the MAX uid_t, let me know.. 
   */
  if ((val == ULONG_MAX) && (errno == ERANGE)) {
    CONF_ERROR(cmd, "the value given is outside the legal range");
  }

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
  *((uid_t *) c->argv[0]) = val;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqlminusergid(cmd_rec * cmd) {
  config_rec *c = NULL;
  unsigned long val;
  char *endptr = NULL;

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

  val = strtoul(cmd->argv[1], &endptr, 10);

  if (*endptr != '\0') {
    CONF_ERROR(cmd, "requires a numeric argument");
  }

  /*
   * whee! need to check if in the legal range for gid_t 
   */
  /*
   * however, I can't think of a cross-platform way of doing this.. if
   * anyone knows of a way to find the MAX gid_t, let me know.. 
   */
  if ((val == ULONG_MAX) && (errno == ERANGE)) {
    CONF_ERROR(cmd, "the value given is outside the legal range");
  }

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(gid_t));
  *((gid_t *) c->argv[0]) = val;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqldefaultuid(cmd_rec * cmd) {
  config_rec *c;
  uid_t val;
  char *endptr = NULL;

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

  val = strtoul(cmd->argv[1], &endptr, 10);

  if (*endptr != '\0') {
    CONF_ERROR(cmd, "requires a numeric argument");
  }

  /*
   * whee! need to check is in the legal range for uid_t 
   */
  if ((val == ULONG_MAX) && (errno == ERANGE)) {
    CONF_ERROR(cmd, "the value given is outside the legal range");
  }

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(uid_t));
  *((uid_t *) c->argv[0]) = val;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

MODRET set_sqldefaultgid(cmd_rec * cmd) {
  config_rec *c;
  gid_t val;
  char *endptr = NULL;

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

  val = strtoul(cmd->argv[1], &endptr, 10);

  if (*endptr != '\0') {
    CONF_ERROR(cmd, "requires a numeric argument");
  }

  /*
   * whee! need to check is in the legal range for gid_t 
   */
  if ((val == ULONG_MAX) && (errno == ERANGE)) {
    CONF_ERROR(cmd, "the value given is outside the legal range");
  }

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(gid_t));
  *((gid_t *) c->argv[0]) = val;
  c->flags |= CF_MERGEDOWN;

  return HANDLED(cmd);
}

/*****************************************************************
 *
 * INITIALIZATION / FORK HANDLERS
 *
 *****************************************************************/

static void sql_exit_cb(void) {
  config_rec *c = NULL;

  /* Note: most of this code hacked out of log_master(), which
   * brings to mind the idea of reorganizing the code a little, so
   * that this function can call a function to do this, instead of
   * handling it itself; that function to be used by/in log_master
   * as well.
   */

  if (!cmap.status)
    return;

  /* handle EXIT queries */
  c = find_config(main_server->conf, CONF_PARAM, "SQLLog_EXIT", FALSE);

  while (c) {
    char *qname = NULL, *type = NULL;
    cmd_rec *cmd = NULL;

    qname = c->argv[0];

    /* Since we're exiting the process here (or soon, anyway), we can
     * get away with using the config_rec's pool.
     */
    cmd = _sql_make_cmd(c->pool, 1, "EXIT");
    type = _named_query_type(cmd, qname);

    if (type) {
      if ((!strcasecmp(type, SQL_UPDATE_C)) ||
          (!strcasecmp(type, SQL_FREEFORM_C)) ||
          (!strcasecmp(type, SQL_INSERT_C))) {

        sql_log(DEBUG_FUNC, "running named query '%s' at exit", qname);
        _process_named_query( cmd, qname );

      } else {
        sql_log(DEBUG_WARN, "named query '%s' is not an INSERT, UPDATE, or "
          "FREEFORM query", qname);
      }

    } else
      sql_log(DEBUG_WARN, "named query '%s' cannot be found", qname);

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

  sql_closelog();
  return;
}

static int sql_getconf(void) {
  char *authstr = NULL;
  config_rec *c = NULL;
  void *temp_ptr = NULL;
  unsigned char *negative_cache = NULL;
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  sql_data_t *sd = NULL;
  int percall = 0, res = 0;
  char *fieldset = NULL;
  pool *tmp_pool = NULL;

  /* Build a temporary pool */
  tmp_pool = make_sub_pool(session.pool);

  /* Open any configured SQLLogFile */
  if ((res = sql_openlog()) < 0) {
    if (res == -1)
      pr_log_pri(PR_LOG_NOTICE, "notice: unable to open SQLLogFile: %s",
        strerror(errno));

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

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

  /* Get our backend info and toss it up */
  cmd = _sql_make_cmd(tmp_pool, 1, "foo");
  mr = _sql_dispatch(cmd, "sql_identify");
  _sql_check_response(mr);

  sd = (sql_data_t *) mr->data;

  sql_log(DEBUG_INFO, "backend module '%s'", sd->data[0]);
  sql_log(DEBUG_INFO, "backend api    '%s'", sd->data[1]);

  SQL_FREE_CMD(cmd);

  sql_log(DEBUG_FUNC, "%s", ">>> sql_getconf");

  sql_pool = make_sub_pool(session.pool);

  group_name_cache = make_cache(sql_pool, _group_name, _groupcmp);
  passwd_name_cache = make_cache(sql_pool, _passwd_name, _passwdcmp);
  group_gid_cache = make_cache(sql_pool, _group_gid, _groupcmp);
  passwd_uid_cache = make_cache(sql_pool, _passwd_uid, _passwdcmp);

  cmap.group_cache_filled = 0;
  cmap.passwd_cache_filled = 0;

  cmap.curr_group = NULL;
  cmap.curr_passwd = NULL;

  /*
   * construct our internal cache structure for this fork 
   */

  memset(&cmap, 0, sizeof(cmap));

  temp_ptr = get_param_ptr(main_server->conf, "SQLAuthenticate", FALSE);

  if (temp_ptr)
    cmap.authmask = *((int *) temp_ptr);
  else
    cmap.authmask = SQL_AUTH_GROUPS|SQL_AUTH_USERS|SQL_AUTH_GROUPSET|SQL_AUTH_USERSET;

  if ((negative_cache = get_param_ptr(main_server->conf, "SQLNegativeCache",
      FALSE)) != NULL)
    cmap.negative_cache = *negative_cache;

  else
    cmap.negative_cache = FALSE;

  /* SQLHomedirOnDemand defaults to NO */
  temp_ptr = get_param_ptr(main_server->conf, "SQLHomedirOnDemand", FALSE);

  if (temp_ptr && (*((unsigned char *) temp_ptr) == TRUE))
    cmap.buildhomedir = TRUE;
  else
    cmap.buildhomedir = FALSE;

  cmap.defaulthomedir = get_param_ptr(main_server->conf, "SQLDefaultHomedir", FALSE);

  temp_ptr = get_param_ptr(main_server->conf, "SQLUserTable", FALSE);
  
  /* if we have no SQLUserTable, SQLUserInfo was not used -- default all */
  
  if (!temp_ptr) {
    cmap.usrtable = MOD_SQL_DEF_USERTABLE;
    cmap.usrfield = MOD_SQL_DEF_USERNAMEFIELD;
    cmap.pwdfield = MOD_SQL_DEF_USERPASSWORDFIELD;
    cmap.uidfield = MOD_SQL_DEF_USERUIDFIELD;
    cmap.gidfield = MOD_SQL_DEF_USERGIDFIELD;
    cmap.homedirfield = MOD_SQL_DEF_USERHOMEDIRFIELD;
    cmap.shellfield = MOD_SQL_DEF_USERSHELLFIELD;

    /* It's possible that a custom UserInfo query was configured.  Check for
     * this.
     */
    temp_ptr = get_param_ptr(main_server->conf, "SQLCustomUserInfo", FALSE);

    if (temp_ptr) {
      config_rec *custom_c = NULL;
      char *named_query = pstrcat(tmp_pool, "SQLNamedQuery_", temp_ptr, NULL);

      if ((custom_c = find_config(main_server->conf, CONF_PARAM, named_query,
          FALSE)) == NULL) {
        sql_log(DEBUG_INFO, "error: unable to resolve custom "
          "SQLNamedQuery name '%s'", temp_ptr);

      } else
        cmap.usercustom = temp_ptr;
    }

  } else {
    cmap.usrtable = temp_ptr;
    cmap.usrfield = get_param_ptr(main_server->conf, "SQLUsernameField", FALSE);
    cmap.pwdfield = get_param_ptr(main_server->conf, "SQLPasswordField", FALSE);
    cmap.uidfield = get_param_ptr(main_server->conf, "SQLUidField", FALSE);
    cmap.gidfield = get_param_ptr(main_server->conf, "SQLGidField", FALSE);
    cmap.homedirfield = get_param_ptr(main_server->conf, "SQLHomedirField", FALSE);
    cmap.shellfield = get_param_ptr(main_server->conf, "SQLShellField", FALSE);
  }

  /* build the userfieldset */
  fieldset = pstrcat(tmp_pool, cmap.usrfield, ", ", cmap.pwdfield, NULL);
  if (cmap.uidfield)
    fieldset = pstrcat(tmp_pool, fieldset, ", ", cmap.uidfield, NULL);
  if (cmap.gidfield)
    fieldset = pstrcat(tmp_pool, fieldset, ", ", cmap.gidfield, NULL);
  if (cmap.homedirfield)
    fieldset = pstrcat(tmp_pool, fieldset, ", ", cmap.homedirfield, NULL);
  if (cmap.shellfield)
    fieldset = pstrcat(tmp_pool, fieldset, ", ", cmap.shellfield, NULL);
  cmap.usrfields = pstrdup(sql_pool, fieldset);

  temp_ptr = get_param_ptr(main_server->conf, "SQLGroupTable", FALSE);
  
  /* if we have no temp_ptr, SQLGroupInfo was not used - default all */
  if (!temp_ptr) {
    cmap.grptable = MOD_SQL_DEF_GROUPTABLE;
    cmap.grpfield = MOD_SQL_DEF_GROUPNAMEFIELD;
    cmap.grpgidfield = MOD_SQL_DEF_GROUPGIDFIELD;
    cmap.grpmembersfield = MOD_SQL_DEF_GROUPMEMBERSFIELD;
  } else {
    cmap.grptable = get_param_ptr(main_server->conf, "SQLGroupTable", FALSE);
    cmap.grpfield = get_param_ptr(main_server->conf, "SQLGroupnameField", FALSE);
    cmap.grpgidfield = get_param_ptr(main_server->conf, "SQLGroupGIDField", FALSE);
    cmap.grpmembersfield = get_param_ptr(main_server->conf, "SQLGroupMembersField", FALSE);
  }

  /* build the groupfieldset */
  fieldset = pstrcat(tmp_pool, cmap.grpfield, ", ", cmap.grpgidfield,
		     ", ", cmap.grpmembersfield, NULL);
  cmap.grpfields = pstrdup(sql_pool, fieldset);

  temp_ptr = get_param_ptr(main_server->conf, "SQLUserWhereClause", FALSE);
  cmap.userwhere = temp_ptr ? pstrcat(sql_pool, "(", temp_ptr, ")", NULL) : "";

  temp_ptr = get_param_ptr(main_server->conf, "SQLGroupWhereClause", FALSE);
  cmap.groupwhere = temp_ptr ? pstrcat(sql_pool, "(", temp_ptr, ")", NULL) : "";

  temp_ptr = get_param_ptr(main_server->conf, "SQLAuthTypes", FALSE);
  cmap.authlist = temp_ptr;

  if (!cmap.authlist)
    sql_log(DEBUG_INFO, "%s", "error: no SQLAuthTypes configured");

  temp_ptr = get_param_ptr(main_server->conf, "SQLMinID", FALSE);
  if ( temp_ptr ) {
    cmap.minuseruid = *((unsigned long *) temp_ptr);
    cmap.minusergid = *((unsigned long *) temp_ptr);

  } else {
    temp_ptr = get_param_ptr(main_server->conf, "SQLMinUserUID", FALSE);
    cmap.minuseruid = temp_ptr ? *((uid_t *) temp_ptr) : MOD_SQL_MIN_USER_UID;

    temp_ptr = get_param_ptr(main_server->conf, "SQLMinUserGID", FALSE);
    cmap.minusergid = temp_ptr ? *((gid_t *) temp_ptr) : MOD_SQL_MIN_USER_GID;
  }

  temp_ptr = get_param_ptr(main_server->conf, "SQLDefaultUID", FALSE);
  cmap.defaultuid = temp_ptr ? *((uid_t *) temp_ptr) : MOD_SQL_DEF_UID;

  temp_ptr = get_param_ptr(main_server->conf, "SQLDefaultGID", FALSE);
  cmap.defaultgid = temp_ptr ? *((gid_t *) temp_ptr) : MOD_SQL_DEF_GID;

  if ((c = find_config(main_server->conf, CONF_PARAM, "SQLRatioStats", FALSE))) {
    cmap.sql_fstor = c->argv[0];
    cmap.sql_fretr = c->argv[1];
    cmap.sql_bstor = c->argv[2];
    cmap.sql_bretr = c->argv[3];
  }

  if ((c = find_config(main_server->conf, CONF_PARAM, "SQLRatios", FALSE))) {
    if (!cmap.sql_fstor) {
      pr_log_pri(PR_LOG_WARNING, _MOD_VERSION
              ": warning: SQLRatios directive ineffective without SQLRatioStats on");
      sql_log(DEBUG_WARN, "%s",
                "warning: SQLRatios directive ineffective without SQLRatioStats on");
    }
    cmap.sql_frate = c->argv[0];
    cmap.sql_fcred = c->argv[1];
    cmap.sql_brate = c->argv[2];
    cmap.sql_bcred = c->argv[3];
  }

  if ((!cmap.homedirfield) && (!cmap.defaulthomedir)) {
    cmap.authmask ^= SQL_AUTH_USERS;
    pr_log_pri(PR_LOG_WARNING, _MOD_VERSION ": warning: no homedir field and no default specified. User authentication is OFF");
    sql_log(DEBUG_WARN, "%s", "warning: no homedir field and no default specified. User authentication is OFF");
  }

  if (!(c = find_config(main_server->conf, CONF_PARAM, "SQLConnectInfo", FALSE))) {
    cmap.authmask = 0;
    cmap.status = 0;
    cmap.sql_fstor = NULL;
    cmap.sql_frate = NULL;
    sql_log(DEBUG_WARN, "%s", "warning: no SQLConnectInfo specified. mod_sql is OFF");
  } else {
    if (!strcasecmp(c->argv[3], "percall")) percall = 1;

    cmd = _sql_make_cmd(tmp_pool, 5, "default", c->argv[1], c->argv[2], c->argv[0], c->argv[3]);
    mr = _sql_dispatch(cmd,"sql_defineconnection");
    _sql_check_response(mr);
    SQL_FREE_CMD(cmd);
  
    if (!percall) {
      cmd = _sql_make_cmd( tmp_pool, 1, "default" );
      mr = _sql_dispatch(cmd, "sql_open");
      _sql_check_response(mr);
      SQL_FREE_CMD(cmd);
      sql_log(DEBUG_INFO, "%s", "backend successfully connected.");

    } else {
      sql_log(DEBUG_INFO, "%s",
        "backend will not be checked until first use.");
    }

    cmap.status = 1;
  }

  sql_log(DEBUG_INFO, "mod_sql status     : %s", cmap.status ? "on" : "off" );

  sql_log(DEBUG_INFO, "negative_cache     : %s",
            cmap.negative_cache ? "on" : "off" );

  authstr = "";

  if (SQL_USERS) {
    if (SQL_USERGOD) {
      authstr = pstrcat( tmp_pool, authstr, "users* ", NULL);
    } else {
      authstr = pstrcat( tmp_pool, authstr, "users ", NULL);
    }
  }

  if (SQL_GROUPS) {
    if (SQL_GROUPGOD) {
      authstr = pstrcat( tmp_pool, authstr, "groups* ", NULL);
    } else {
      authstr = pstrcat( tmp_pool, authstr, "groups ", NULL);
    }
  }

  if (SQL_USERSET) {
    if (SQL_FASTUSERS) {
      authstr = pstrcat( tmp_pool, authstr, "userset(fast) ", NULL);
    } else {
      authstr = pstrcat( tmp_pool, authstr, "userset ", NULL);
    }
  }

  if (SQL_GROUPSET) {
    if (SQL_FASTGROUPS) {
      authstr = pstrcat( tmp_pool, authstr, "groupset(fast)", NULL);
    } else {
      authstr = pstrcat( tmp_pool, authstr, "groupset", NULL);
    }
  }

  sql_log(DEBUG_INFO, "authenticate       : %s",
    (!authstr || *authstr=='\0') ? "off" : authstr);

  if (SQL_USERS || cmap.sql_fstor || cmap.sql_frate) {
    sql_log(DEBUG_INFO, "usertable          : %s", cmap.usrtable);
    sql_log(DEBUG_INFO, "userid field       : %s", cmap.usrfield);
  }
  if (SQL_USERS) {
    sql_log(DEBUG_INFO, "password field     : %s", cmap.pwdfield);
    sql_log(DEBUG_INFO, "uid field          : %s",
      (cmap.uidfield ? cmap.uidfield : "NULL"));
    sql_log(DEBUG_INFO, "gid field          : %s",
      (cmap.gidfield ? cmap.gidfield : "NULL"));
    if (cmap.homedirfield)
      sql_log(DEBUG_INFO, "homedir field      : %s", cmap.homedirfield);
    if (cmap.defaulthomedir)
      sql_log(DEBUG_INFO, "homedir(default)   : '%s'", cmap.defaulthomedir);
    sql_log(DEBUG_INFO, "shell field        : %s",
      (cmap.shellfield ? cmap.shellfield : "NULL"));
    sql_log(DEBUG_INFO, "homedirondemand    : %s",
      (cmap.buildhomedir ? "true" : "false"));
  }

  if (SQL_GROUPS) {
    sql_log(DEBUG_INFO, "group table        : %s", cmap.grptable);
    sql_log(DEBUG_INFO, "groupname field    : %s", cmap.grpfield);
    sql_log(DEBUG_INFO, "grp gid field      : %s", cmap.grpgidfield);
    sql_log(DEBUG_INFO, "grp members field  : %s", cmap.grpmembersfield);
  }

  if (SQL_USERS) {
    sql_log(DEBUG_INFO, "SQLMinUserUID      : %u", cmap.minuseruid);
    sql_log(DEBUG_INFO, "SQLMinUserGID      : %u", cmap.minusergid);
  }
   
  if (SQL_GROUPS) {
    sql_log(DEBUG_INFO, "SQLDefaultUID      : %u", cmap.defaultuid);
    sql_log(DEBUG_INFO, "SQLDefaultGID      : %u", cmap.defaultgid);
  }

  if (cmap.sql_fstor) {
    sql_log(DEBUG_INFO, "sql_fstor          : %s", cmap.sql_fstor);
    sql_log(DEBUG_INFO, "sql_fretr          : %s", cmap.sql_fretr);
    sql_log(DEBUG_INFO, "sql_bstor          : %s", cmap.sql_bstor);
    sql_log(DEBUG_INFO, "sql_bretr          : %s", cmap.sql_bretr);
  }

  if (cmap.sql_frate) {
    sql_log(DEBUG_INFO, "sql_frate          : %s", cmap.sql_frate);
    sql_log(DEBUG_INFO, "sql_fcred          : %s", cmap.sql_fcred);
    sql_log(DEBUG_INFO, "sql_brate          : %s", cmap.sql_brate);
    sql_log(DEBUG_INFO, "sql_bcred          : %s", cmap.sql_bcred);
  }

  sql_log(DEBUG_FUNC, "%s", "<<< sql_getconf");

  /* get rid of the temp pool */
  destroy_pool( tmp_pool );

  /* add our exit handler */
  pr_exit_register_handler(sql_exit_cb);

  return 0;
}

/*****************************************************************
 *
 * HANDLER TABLES
 *
 *****************************************************************/

static conftable sql_conftab[] = {
  {"SQLAuthenticate", set_sqlauthenticate, NULL},

  {"SQLConnectInfo", set_sqlconnectinfo, NULL},
  {"SQLAuthTypes", set_sqlauthtypes, NULL},

  {"SQLUserInfo", set_sqluserinfo, NULL},
  {"SQLUserWhereClause", set_sqluserwhereclause, NULL},

  {"SQLGroupInfo", set_sqlgroupinfo, NULL},
  {"SQLGroupWhereClause", set_sqlgroupwhereclause, NULL},

  {"SQLMinID", set_sqlminid, NULL},
  {"SQLMinUserUID", set_sqlminuseruid, NULL},
  {"SQLMinUserGID", set_sqlminusergid, NULL},
  {"SQLDefaultUID", set_sqldefaultuid, NULL},
  {"SQLDefaultGID", set_sqldefaultgid, NULL},

  {"SQLNegativeCache", set_sqlnegativecache, NULL},

  {"SQLRatios", set_sqlratios, NULL},
  {"SQLRatioStats", set_sqlratiostats, NULL},

  {"SQLDefaultHomedir", set_sqldefaulthomedir, NULL},
  {"SQLHomedirOnDemand", set_sqlhomedirondemand, NULL},

  {"SQLLog", set_sqllog, NULL},
  {"SQLLogFile", set_sqllogfile, NULL},
  {"SQLNamedQuery", set_sqlnamedquery, NULL},
  {"SQLShowInfo", set_sqlshowinfo, NULL},

  {NULL, NULL, NULL}
};

static cmdtable sql_cmdtab[] = {
  {PRE_CMD,      C_QUIT,       G_NONE, log_master,     FALSE, FALSE},
  {POST_CMD,     C_STOR,       G_NONE, post_cmd_stor,  FALSE, FALSE},
  {POST_CMD,     C_RETR,       G_NONE, post_cmd_retr,  FALSE, FALSE},
  {POST_CMD,     C_ANY,        G_NONE, info_master,    FALSE, FALSE},
  {POST_CMD_ERR, C_ANY,        G_NONE, errinfo_master, FALSE, FALSE},
  {LOG_CMD,      C_ANY,        G_NONE, log_master,     FALSE, FALSE},
  {LOG_CMD_ERR,  C_ANY,        G_NONE, err_master,     FALSE, FALSE},

  /* Module hooks */
  { HOOK,	"sql_change",	G_NONE,	sql_change,	FALSE, FALSE }, 
  { HOOK,	"sql_escapestr",G_NONE,	sql_escapestr,	FALSE, FALSE },
  { HOOK,	"sql_lookup",	G_NONE,	sql_lookup,	FALSE, FALSE },

  {0, NULL}
};

static authtable sql_authtab[] = {
  {0, "setpwent",	cmd_setpwent	},
  {0, "getpwent",	cmd_getpwent	},
  {0, "endpwent",	cmd_endpwent	},
  {0, "setgrent",	cmd_setgrent	},
  {0, "getgrent",	cmd_getgrent	},
  {0, "endgrent",	cmd_endgrent	},
  {0, "getpwnam",	cmd_getpwnam	},
  {0, "getpwuid",	cmd_getpwuid	},
  {0, "getgrnam",	cmd_getgrnam	},
  {0, "getgrgid",	cmd_getgrgid	},
  {0, "auth",		cmd_auth	},
  {0, "check",		cmd_check	},
  {0, "uid_name",	cmd_uid2name	},
  {0, "gid_name",	cmd_gid2name	},
  {0, "name_uid",	cmd_name2uid	},
  {0, "name_gid",	cmd_name2gid	},
  {0, "getgroups",	cmd_getgroups	},

  /* Note: these should be HOOKs, and in the cmdtab. */
  {0, "getstats",	cmd_getstats},
  {0, "getratio",	cmd_getratio},

  {0, NULL, NULL}
};

module sql_module = {

  /* Always NULL */
  NULL, NULL,

  /* Module API version */
  0x20,

  /* Module name */
  "sql",

  /* Module configuration directive table */
  sql_conftab,

  /* Module command handler table */
  sql_cmdtab,

  /* Module auth handler table */
  sql_authtab,

  /* Module initialization */
  NULL,

  /* Session initialization */
  sql_getconf
};

