/*
 * ProFTPD: mod_sql -- SQL frontend
 * Copyright (c) 1998-1999 Johnie Ingram.
 * Copyright (c) 2001 Andrew Houghton.
 * Copyright (c) 2004-2005 TJ Saunders
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exemption, 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.101 2005/12/13 17:54:53 castaglia Exp $
 */

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

#define MOD_SQL_VERSION			"mod_sql/4.2.1"

#if defined(HAVE_CRYPT_H) && !defined(AIX4) && !defined(AIX5)
# include <crypt.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"

/* SQLEngine flags */
#define SQL_ENGINE_FL_AUTH	0x001
#define SQL_ENGINE_FL_LOG	0x002

/* authmask defines */
#define SQL_AUTH_USERS             (1<<0)
#define SQL_AUTH_GROUPS            (1<<1)
#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)

/*
 * externs, function signatures.. whatever necessary to make
 * the compiler happy..
 */
extern pr_response_t *resp_list,*resp_err_list;

module sql_module;

static char *_sql_where(pool *p, int cnt, ...);
MODRET cmd_getgrent(cmd_rec *);
MODRET cmd_setgrent(cmd_rec *);
MODRET sql_lookup(cmd_rec *);

static pool *sql_pool = NULL;

/*
 * 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 engine;                   /* 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;

struct sql_backend {
  struct sql_backend *next, *prev;
  const char *backend;
  cmdtable *cmdtab;
};

static struct sql_backend *sql_backends = NULL;
static unsigned int sql_nbackends = 0;
static cmdtable *sql_cmdtable = NULL;

/*
 * 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 + 1));
  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);

  cmd->argv[argc] = NULL;

  return cmd;
}

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);
}

static struct sql_backend *sql_get_backend(const char *backend) {
  struct sql_backend *sb;

  if (!sql_backends)
    return NULL;

  for (sb = sql_backends; sb; sb = sb->next) {
    if (strcasecmp(sb->backend, backend) == 0)
      return sb;
  }

  return NULL;
}

/* This function is used by mod_sql backends, to register their
 * individual backend command tables with the main mod_sql module.
 */
int sql_register_backend(const char *backend, cmdtable *cmdtab) {
  struct sql_backend *sb;

  if (!backend || !cmdtab) {
    errno = EINVAL;
    return -1;
  }

  if (!sql_pool) {
    sql_pool = make_sub_pool(permanent_pool);
    pr_pool_tag(sql_pool, MOD_SQL_VERSION);
  }

  /* Check to see if this backend has already been registered. */
  sb = sql_get_backend(backend);
  if (sb) {
    errno = EEXIST;
    return -1;
  }

  sb = pcalloc(sql_pool, sizeof(struct sql_backend));
  sb->backend = backend;
  sb->cmdtab = cmdtab;

  if (sql_backends)
    sb->next = sql_backends;

  else
    sb->next = NULL;

  sql_backends = sb;
  sql_nbackends++;

  return 0;
}

/* Used by mod_sql backends to unregister their backend command tables
 * from the main mod_sql module.
 */
int sql_unregister_backend(const char *backend) {
  struct sql_backend *sb;

  if (!backend) {
    errno = EINVAL;
    return -1;
  }

  /* Check to see if this backend has been registered. */
  sb = sql_get_backend(backend);
  if (!sb) {
    errno = ENOENT;
    return -1;
  }

#if !defined(PR_SHARED_MODULE)
  /* If there is only one registered backend, it cannot be removed.
   */
  if (sql_nbackends == 1) {
    errno = EPERM;
    return -1;
  }

  /* Be sure to handle the case where this is the currently active backend. */
  if (sql_cmdtable &&
      sb->cmdtab == sql_cmdtable) {
    errno = EACCES;
    return -1;
  }
#endif

  /* Remove this backend from the linked list. */
  if (sb->prev)
    sb->prev->next = sb->next;
  else
    /* This backend is the start of the sql_backends list (prev is NULL),
     * so we need to update the list head pointer as well.
     */
    sql_backends = sb->next;

  if (sb->next)
    sb->next->prev = sb->prev;

  sb->prev = sb->next = NULL;

  sql_nbackends--;

  /* NOTE: a counter should be kept of the number of unregistrations,
   * as the memory for a registration is not freed on unregistration.
   */

  return 0;
}

/*****************************************************************
 *
 * 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;
}

static 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 = 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 = 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], *username = NULL;
  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>';
   */

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

  username = (char *) mr->data;

  grpwhere = pstrcat(cmd->tmp_pool,
    cmap.grpmembersfield, " = '", username, "' OR ",
    cmap.grpmembersfield, " LIKE '", username, ",%' OR ",
    cmap.grpmembersfield, " LIKE '%,", username, "' OR ",
    cmap.grpmembersfield, " LIKE '%,", username, ",%'", 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;
}

/* Command handlers
 */

MODRET sql_pre_pass(cmd_rec *cmd) {
  config_rec *c = NULL, *anon_config = NULL;
  char *user = NULL;

  if (cmap.engine == 0)
    return DECLINED(cmd);

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

  user = get_param_ptr(cmd->server->conf, C_USER, FALSE);
  if (!user) {
    sql_log(DEBUG_FUNC, "%s", "Missing user name, skipping");
    sql_log(DEBUG_FUNC, "%s", "<<< sql_pre_pass");
    return DECLINED(cmd);
  }

  /* Use the looked-up user name to determine whether this is to be
   * an anonymous session.
   */
  anon_config = pr_auth_get_anon_config(cmd->pool, &user, NULL, NULL);

  c = find_config(anon_config ? anon_config->subset : main_server->conf,
    CONF_PARAM, "SQLEngine", FALSE);
  if (c)
    cmap.engine = *((int *) c->argv[0]);

  sql_log(DEBUG_FUNC, "%s", "<<< sql_pre_pass");
  return DECLINED(cmd);
}

MODRET sql_post_stor(cmd_rec *cmd) {
  if (cmap.engine == 0)
    return DECLINED(cmd);

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

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

  sql_log(DEBUG_FUNC, "%s", "<<< sql_post_stor");
  return DECLINED(cmd);
}

MODRET sql_post_retr(cmd_rec *cmd) {
  if (cmap.engine == 0)
    return DECLINED(cmd);

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

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

  sql_log(DEBUG_FUNC, "%s", "<<< sql_post_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(pr_netaddr_get_sess_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 (strcmp(cmd->argv[0], C_RNTO) == 0) {
      sstrncpy(argp, dir_abs_path(cmd->tmp_pool, cmd->arg, TRUE), sizeof(arg));

    } else 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;
    if (strcasecmp(cmd->argv[0], C_PASS) == 0 &&
        session.hide_password) {
      sstrncpy(argp, "(hidden)", sizeof(arg));

    } else {
      sstrncpy(argp, cmd->arg, sizeof(arg));
    }
    break;

  case 'h':
    argp = arg;
    sstrncpy(argp, pr_netaddr_get_sess_remote_name(), sizeof(arg));
    break;

  case 'L':
    argp = arg;
    sstrncpy(argp, pr_netaddr_get_ipstr(pr_netaddr_get_sess_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_SQL_VERSION,
                "reference out-of-bounds in query");
            }

          } else {
            return ERROR_MSG(cmd, MOD_SQL_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;
        }

        /* XXX Should be sstrcat(). */
        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) == 0) {
      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) == 0) {
      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) == 0) {
      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", outs),
        "sql_query");

    } else if (strcasecmp(c->argv[0], SQL_SELECT_C) == 0) {
      mr = _sql_dispatch(_sql_make_cmd(cmd->tmp_pool, 2, "default", outs),
        "sql_select");

    } else {
      mr = ERROR_MSG(cmd, MOD_SQL_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;

  if (!(cmap.engine & SQL_ENGINE_FL_LOG))
    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;

  if (!(cmap.engine & SQL_ENGINE_FL_LOG))
    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;

  if (!(cmap.engine & SQL_ENGINE_FL_LOG))
    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], "%s", 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], "%s", outs);

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

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

  return DECLINED(cmd);
}