/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 1997, 1998 Public Flood Software
 * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver@tos.net>
 * Copyright (c) 2001, 2002, 2003, 2004 The ProFTPD Project team
 *
 * 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, Public Flood Software/MacGyver aka Habeeb J. Dihu
 * 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.
 */

/* Read configuration file(s), and manage server/configuration structures.
 * $Id: dirtree.c,v 1.167 2005/04/15 21:18:24 castaglia Exp $
 */

#include "conf.h"

#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif

#ifdef HAVE_REGEX_H
# include <regex.h>
#endif

xaset_t *server_list = NULL;
server_rec *main_server = NULL;
int tcpBackLog = PR_TUNABLE_DEFAULT_BACKLOG;
int SocketBindTight = FALSE;
char ServerType = SERVER_STANDALONE;
int ServerMaxInstances = 0;
int ServerUseReverseDNS = TRUE;
int TimeoutIdle = PR_TUNABLE_TIMEOUTIDLE;
int TimeoutNoXfer = PR_TUNABLE_TIMEOUTNOXFER;
int TimeoutStalled = PR_TUNABLE_TIMEOUTSTALLED;
char MultilineRFC2228 = 0;

/* From src/pool.c */
extern pool *global_config_pool;

/* Used by find_config_* */
xaset_t *find_config_top = NULL;

static void _mergedown(xaset_t *, int);

/* Used by get_param_int_next & get_param_ptr_next as "placeholders" */
static config_rec *_last_param_int = NULL;
static config_rec *_last_param_ptr = NULL;
static unsigned char _kludge_disable_umask = 0;

array_header *server_defines = NULL;

static int allow_dyn_config(void) {
  config_rec *c = NULL;
  unsigned int ctxt_precedence = 0;
  unsigned char have_user_limit, have_group_limit, have_class_limit,
    have_all_limit;
  unsigned char allow = TRUE;

  have_user_limit = have_group_limit = have_class_limit =
    have_all_limit = FALSE;

  c = find_config(CURRENT_CONF, CONF_PARAM, "AllowOverride", FALSE);

  while (c) {
    if (c->argc == 3) {
      if (!strcmp(c->argv[2], "user")) {

        if (pr_expr_eval_user_or((char **) &c->argv[3])) {
          if (*((unsigned int *) c->argv[1]) > ctxt_precedence) {

            /* Set the context precedence. */
            ctxt_precedence = *((unsigned int *) c->argv[1]);

            allow = *((int *) c->argv[0]);

            have_group_limit = have_class_limit = have_all_limit = FALSE;
            have_user_limit = TRUE;
          }
        }

      } else if (!strcmp(c->argv[2], "group")) {

        if (pr_expr_eval_group_and((char **) &c->argv[3])) {
          if (*((unsigned int *) c->argv[1]) > ctxt_precedence) {

            /* Set the context precedence. */
            ctxt_precedence = *((unsigned int *) c->argv[1]);

            allow = *((int *) c->argv[0]);

            have_user_limit = have_class_limit = have_all_limit = FALSE;
            have_group_limit = TRUE;
          }
        }

      } else if (!strcmp(c->argv[2], "class")) {

        if (pr_expr_eval_class_or((char **) &c->argv[3])) {
          if (*((unsigned int *) c->argv[1]) > ctxt_precedence) {

            /* Set the context precedence. */
            ctxt_precedence = *((unsigned int *) c->argv[1]);

            allow = *((int *) c->argv[0]);

            have_user_limit = have_group_limit = have_all_limit = FALSE;
            have_class_limit = TRUE;
          }
        }
      }

    } else {

      if (*((unsigned int *) c->argv[1]) > ctxt_precedence) {

        /* Set the context precedence. */
        ctxt_precedence = *((unsigned int *) c->argv[1]);

        allow = *((int *) c->argv[0]);

        have_user_limit = have_group_limit = have_class_limit = FALSE;
        have_all_limit = TRUE;
      }
    }

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

  /* Print out some nice debugging information. */
  if (have_user_limit || have_group_limit ||
      have_class_limit || have_all_limit) {
    pr_log_debug(DEBUG4, "AllowOverride %s %s%s .ftpaccess files",
      allow ? "allows" : "denies",
      have_user_limit ? "user " : have_group_limit ? "group " :
      have_class_limit ? "class " : "all",
      have_user_limit ? session.user : have_group_limit ? session.group :
      have_class_limit ? session.class->cls_name : "");
  }

  return allow;
}

/* Imported this function from modules/mod_ls.c -- it belongs more with the
 * dir_* functions here, rather than the ls_* functions there.
 */

/* Return true if dir is ".", "./", "../", or "..". */
int is_dotdir(const char *dir) {
  if (strcmp(dir, ".") == 0 || strcmp(dir, "./") == 0 ||
      strcmp(dir, "..") == 0 || strcmp(dir, "../") == 0)
    return TRUE;

  return FALSE;
}

/* Return true if str contains any of the glob(7) characters. */
int is_fnmatch(const char *str) {
  int have_bracket = 0;

  while (*str) {
    switch (*str) {
      case '?':
      case '*':
        return TRUE;

      case '\\':
        if (*str++ == '\0')
          return FALSE;
        break;

      case '[':
        have_bracket++;
        break;

      case ']':
        if (have_bracket)
          return TRUE;
        break;

      default:
        break;
    }

    str++;
  }

  return FALSE;
}

/* Lookup the best configuration set from which to retrieve configuration
 * values if the config_rec can appear in <Directory>.  This function
 * works around the issue caused by using the cached directory pointer
 * in session.dir_config.
 *
 * The issue with using session.dir_config is that it is assigned when
 * the client changes directories or doing other directory lookups, and so
 * dir_config may actually point to the configuration for a directory other
 * than the target directory for an uploaded, for example.  Unfortunately,
 * it is more expensive to lookup the configuration for the target directory
 * every time.  Perhaps some caching of looked up directory configurations
 * into a table, rather than a single pointer like session.dir_config,
 * might help.
 */
xaset_t *get_dir_ctxt(pool *p, char *dir_path) {
  config_rec *c = NULL;
  char *full_path = dir_path;

  if (session.chroot_path) {
    if (*dir_path != '/')
      full_path = pdircat(p, session.chroot_path, session.cwd, dir_path, NULL);

    else
      full_path = pdircat(p, session.chroot_path, dir_path, NULL);

  } else if (*dir_path != '/')
    full_path = pdircat(p, session.cwd, dir_path, NULL);

  c = dir_match_path(p, full_path);

  return c ? c->subset : session.anon_config ? session.anon_config->subset :
    main_server->conf;
}

/* Substitute any appearance of the %u variable in the given string with
 * the value.
 */
char *path_subst_uservar(pool *path_pool, char **path) {
  char *new_path = NULL, *substr = NULL, *substr_path = NULL;

  /* Sanity check. */
  if (!path_pool || !path || !*path) {
    errno = EINVAL;
    return NULL;
  }

  /* If no %u string present, do nothing. */
  if (!strstr(*path, "%u"))
    return *path;

  /* First, deal with occurrences of "%u[index]" strings.  Note that
   * with this syntax, the '[' and ']' characters become invalid in paths,
   * but only if that '[' appears after a "%u" string -- certainly not
   * a common phenomenon (I hope).  This means that in the future, an escape
   * mechanism may be needed in this function.  Caveat emptor.
   */

  substr_path = *path;

  while ((substr = strstr(substr_path, "%u[")) != NULL) {
    int i = 0;
    char *substr_end = NULL, *substr_dup = NULL, *endp = NULL;
    char ref_char[2] = {'\0', '\0'};

    /* Now, find the closing ']'. If not found, it is a syntax error;
     * continue on without processing this occurrence.
     */
    if ((substr_end = strchr(substr, ']')) == NULL)

      /* Just end here. */
      break;

    /* Make a copy of the entire substring. */
    substr_dup = pstrdup(path_pool, substr);

    /* The substr_end variable (used as an index) should work here, too
     * (trying to obtain the entire substring).
     */
    substr_dup[substr_end - substr + 1] = '\0';

    /* Advance the substring pointer by three characters, so that it is
     * pointing at the character after the '['.
     */
    substr += 3;

    /* If the closing ']' is the next character after the opening '[', it
     * is a syntax error.
     */
    if (substr_end == substr) {

      /* Do not forget to advance the substring search path pointer. */
      substr_path = substr;

      continue;
    }

    /* Temporarily set the ']' to '\0', to make it easy for the string
     * scanning below.
     */
    *substr_end = '\0';

    /* Scan the index string into a number, watching for bad strings. */
    i = strtol(substr, &endp, 10);

    if (endp && *endp) {
      substr_path = substr;
      continue;
    }

    /* Make sure that index is within bounds. */
    if (i < 0 || i > strlen(session.user) - 1) {

      /* Put the closing ']' back. */
      *substr_end = ']';

      /* Syntax error. Advance the substring search path pointer, and move
       * on.
       */
      substr_path = substr;

      continue;
    }

    ref_char[0] = session.user[i];

    /* Put the closing ']' back. */
    *substr_end = ']';

    /* Now, to substitute the whole "%u[index]" substring with the
     * referenced character/string.
     */
    substr_path = sreplace(path_pool, substr_path, substr_dup, ref_char, NULL);
  }

  /* Check for any bare "%u", and handle those if present. */
  if (strstr(substr_path, "%u"))
    new_path = sreplace(path_pool, substr_path, "%u", session.user, NULL);
  else
    new_path = substr_path;

  return new_path;
}

/* Check for configured HideFiles directives, and check the given filename
 * (not _path_, just filename) against those regexes if configured. Returns
 * FALSE if filename should be shown/listed, TRUE if it should not
 * be visible.
 */
unsigned char dir_hide_file(const char *path) {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
  char *file_name = NULL, *dir_name = NULL;
  config_rec *c = NULL;
  regex_t *regexp = NULL;
  pool *tmp_pool = make_sub_pool(session.pool);
  unsigned int ctxt_precedence = 0;
  unsigned char have_user_regex, have_group_regex, have_class_regex,
    have_all_regex, inverted = FALSE;

  pr_pool_tag(tmp_pool, "dir_hide_file() tmp pool");

  have_user_regex = have_group_regex = have_class_regex = have_all_regex =
    FALSE;

  /* Separate the given path into directory and file components. */
  dir_name = pstrdup(tmp_pool, path);

  if ((file_name = strrchr(dir_name, '/')) != NULL) {
    file_name = '\0';
    file_name++;

  } else
    file_name = dir_name;

  /* Check for any configured HideFiles */
  c = find_config(get_dir_ctxt(tmp_pool, dir_name), CONF_PARAM, "HideFiles",
    FALSE);

  while (c) {
    if (c->argc >= 4) {

      /* check for a specified "user" classifier first... */
      if (strcmp(c->argv[3], "user") == 0) {
        if (pr_expr_eval_user_or((char **) &c->argv[4])) {

          if (*((unsigned int *) c->argv[2]) > ctxt_precedence) {
            ctxt_precedence = *((unsigned int *) c->argv[2]);

            regexp = *((regex_t **) c->argv[0]);
            inverted = *((unsigned char *) c->argv[1]);

            have_group_regex = have_class_regex = have_all_regex = FALSE;
            have_user_regex = TRUE;
          }
        }

      /* ...then for a "group" classifier... */
      } else if (strcmp(c->argv[3], "group") == 0) {
        if (pr_expr_eval_group_and((char **) &c->argv[4])) {
          if (*((unsigned int *) c->argv[2]) > ctxt_precedence) {
            ctxt_precedence = *((unsigned int *) c->argv[2]);

            regexp = *((regex_t **) c->argv[0]);
            inverted = *((unsigned char *) c->argv[1]);

            have_user_regex = have_class_regex = have_all_regex = FALSE;
            have_group_regex = TRUE;
          }
        }

      /* ...finally, for a "class" classifier.  NOTE: mod_time's
       * class_expression functionality should really be added into the
       * core code at some point.  When that happens, then this code will
       * need to be updated to process class-expressions.
       */
      } else if (strcmp(c->argv[3], "class") == 0) {
        if (pr_expr_eval_class_or((char **) &c->argv[4])) {
          if (*((unsigned int *) c->argv[2]) > ctxt_precedence) {
            ctxt_precedence = *((unsigned int *) c->argv[2]);

            regexp = *((regex_t **) c->argv[0]);
            inverted = *((unsigned char *) c->argv[1]);

            have_user_regex = have_group_regex = have_all_regex = FALSE;
            have_class_regex = TRUE;
          }
        }
      }

    } else if (c->argc == 1) {

      /* This is the "none" HideFiles parameter. */
      destroy_pool(tmp_pool);
      return FALSE;

    } else {
      if (*((unsigned int *) c->argv[2]) > ctxt_precedence) {
        ctxt_precedence = *((unsigned int *) c->argv[2]);

        regexp = *((regex_t **) c->argv[0]);
        inverted = *((unsigned char *) c->argv[1]);

        have_user_regex = have_group_regex = have_class_regex = FALSE;
        have_all_regex = TRUE;
      }
    }

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


  if (have_user_regex || have_group_regex ||
      have_class_regex || have_all_regex) {

    pr_log_debug(DEBUG4, "checking HideFiles pattern for current %s",
      have_user_regex ? "user" : have_group_regex ? "group" :
      have_class_regex ? "class" : "session");

    if (regexec(regexp, file_name, 0, NULL, 0) != 0) {
      destroy_pool(tmp_pool);

      /* The file failed to match the HideFiles regex, which means it should
       * be treated as a "visible" file.  If the regex was 'inverted', though,
       * switch the result.
       */
      return (inverted ? TRUE : FALSE);

    } else {
      destroy_pool(tmp_pool);

      /* The file matched the HideFiles regex, which means it should be
       * considered a "hidden" file.  If the regex was 'inverted', though,
       * switch the result.
       */
      return (inverted ? FALSE : TRUE);
    }
  }

  destroy_pool(tmp_pool);
#endif /* !HAVE_REGEX_H and !HAVE_REGCOMP */

  /* Return FALSE by default. */
  return FALSE;	
}

unsigned char define_exists(const char *definition) {

  /* Check the list of specified definitions, if present.
   */
  if (server_defines) {
    char **defines = server_defines->elts;
    register unsigned int i = 0;

    for (i = 0; i < server_defines->nelts; i++) {
      if (defines[i] && !strcmp(defines[i], definition))
        return TRUE;
    }
  }

  /* default */
  return FALSE;
}

void kludge_disable_umask(void) {
  _kludge_disable_umask = TRUE;
}

void kludge_enable_umask(void) {
  _kludge_disable_umask = FALSE;
}

char *pr_str_get_word(char **cp, int flags) {
  char *ret, *dst;
  char quote_mode = 0;

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

  if (!(flags & PR_STR_FL_PRESERVE_WHITESPACE)) {
    while (**cp && isspace((int) **cp))
      (*cp)++;
  }

  if (!**cp)
    return NULL;

  ret = dst = *cp;

  if (!(flags & PR_STR_FL_PRESERVE_COMMENTS)) {
    /* Stop processing at start of an inline comment. */
    if (**cp == '#')
      return NULL;
  }

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

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

      /* Escaped char */
      if (*((*cp)+1))
        *dst = *(++(*cp));
    }

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

  if (**cp)
    (*cp)++;
  *dst = '\0';

  return ret;
}

cmd_rec *pr_cmd_alloc(pool *p, int argc, ...) {
  pool *newpool = NULL;
  cmd_rec *c = NULL;
  va_list args;

  newpool = make_sub_pool(p);
  pr_pool_tag(newpool, "pr_cmd_alloc() subpool");

  c = pcalloc(newpool, sizeof(cmd_rec));
  c->argc = argc;
  c->stash_index = -1;
  c->pool = newpool;
  c->tmp_pool = make_sub_pool(c->pool);
  pr_pool_tag(c->tmp_pool, "pr_cmd_alloc() tmp pool");

  if (argc) {
    register unsigned int i = 0;

    c->argv = pcalloc(newpool, sizeof(void *) * (argc + 1));
    va_start(args, argc);

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

    va_end(args);

    c->argv[argc] = NULL;
  }

  return c;
}

/* Adds a config_rec to the specified set */
config_rec *add_config_set(xaset_t **set, const char *name) {
  pool *conf_pool = NULL, *set_pool = NULL;
  config_rec *c, *parent = NULL;

  if (!*set) {

    /* Allocate a subpool from permanent_pool for the set. */
    set_pool = make_sub_pool(permanent_pool);
    pr_pool_tag(set_pool, "config set pool");

    *set = xaset_create(set_pool,NULL);
    (*set)->pool = set_pool;

    /* Now, make a subpool for the config_rec to be allocated. */
    conf_pool = make_sub_pool(set_pool);

  } else {

    /* Find the parent set for the config_rec to be allocated. */
    if ((*set)->xas_list)
      parent = ((config_rec*)((*set)->xas_list))->parent;

    /* Allocate a subpool for the config_rec from the parent's pool. */
    conf_pool = make_sub_pool((*set)->pool);
  }

  pr_pool_tag(conf_pool, "config_rec subpool");

  c = (config_rec *) pcalloc(conf_pool, sizeof(config_rec));

  c->pool = conf_pool;
  c->set = *set;
  c->parent = parent;
  if (name)
    c->name = pstrdup(conf_pool, name);
  xaset_insert_end(*set, (xasetmember_t*)c);

  return c;
}

/* Adds a config_rec to the given server.  If no server is specified, the
 * config_rec is added to the current "level".
 */
config_rec *add_config(server_rec *s, const char *name) {
  config_rec *parent = NULL, *c = NULL;
  pool *p = NULL;
  xaset_t **set = NULL;

  if (!s)
    s = pr_parser_server_ctxt_get();

  c = pr_parser_config_ctxt_get();

  if (c) {
    parent = c;
    p = c->pool;
    set = &c->subset;

  } else {
    parent = NULL;

    if (!s->conf || !s->conf->xas_list) {
      p = make_sub_pool(s->pool);
      pr_pool_tag(p, "add_config() subpool");

    } else
      p = ((config_rec*)s->conf->xas_list)->pool;

    set = &s->conf;
  }

  if (!*set)
    *set = xaset_create(p, NULL);

  c = add_config_set(set, name);
  c->parent = parent;

  return c;
}

array_header *pr_expr_create(pool *p, int *argc, char **argv) {
  array_header *acl = NULL;
  int cnt = *argc;
  char *s, *ent;

  if (cnt) {
    acl = make_array(p, cnt, sizeof(char *));

    while (cnt-- && *(++argv)) {
      s = pstrdup(p, *argv);

      while ((ent = get_token(&s, ",")) != NULL)
        if (*ent)
          *((char **) push_array(acl)) = ent;
    }

    *argc = acl->nelts;

  } else
    *argc = 0;

  return acl;
}

/* Boolean "class-expression" AND matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_class_and(char **expr) {
  unsigned char found;
  char *class;

  for (; *expr; expr++) {
    class = *expr;
    found = FALSE;

    if (*class == '!') {
      found = !found;
      class++;
    }

    if (!session.class && !found)
      return FALSE;

    if (session.class && strcmp(session.class->cls_name, class) == 0)
      found = !found;

    if (!found)
      return FALSE;
  }

  return TRUE;
}

/* Boolean "class-expression" OR matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_class_or(char **expr) {
  unsigned char found;
  char *class;

  for (; *expr; expr++) {
    class = *expr;
    found = FALSE;

    if (*class == '!') {
      found = !found;
      class++;
    }

    if (!session.class)
      return found;

    if (strcmp(session.class->cls_name, class) == 0)
      found = !found;

    if (found)
      return TRUE;
  }

  return FALSE;
}

/* Boolean "group-expression" AND matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_group_and(char **expr) {
  unsigned char found;
  char *grp;

  for (; *expr; expr++) {
    grp = *expr;
    found = FALSE;

    if (*grp == '!') {
      found = !found;
      grp++;
    }

    if (session.group && strcmp(session.group, grp) == 0)
      found = !found;

    else if (session.groups) {
      register int i = 0;

      for (i = session.groups->nelts-1; i >= 0; i--)
        if (strcmp(*(((char **) session.groups->elts) + i), grp) == 0) {
          found = !found;
          break;
        }
    }

    if (!found)
      return FALSE;
  }

  return TRUE;
}

/* Boolean "group-expression" OR matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_group_or(char **expr) {
  unsigned char found;
  char *grp;

  for (; *expr; expr++) {
    grp = *expr;
    found = FALSE;

    if (*grp == '!') {
      found = !found;
      grp++;
    }

    if (session.group && strcmp(session.group, grp) == 0)
      found = !found;

    else if (session.groups) {
      register int i = 0;

      for (i = session.groups->nelts-1; i >= 0; i--)
        if (strcmp(*(((char **) session.groups->elts) + i), grp) == 0) {
          found = !found;
          break;
        }
    }

    if (found)
      return TRUE;
  }

  return FALSE;
}

/* Boolean "user-expression" AND matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_user_and(char **expr) {
  unsigned char found;
  char *user;

  for (; *expr; expr++) {
    user = *expr;
    found = FALSE;

    if (*user == '!') {
      found = !found;
      user++;
    }

    if (strcmp(session.user, user) == 0)
      found = !found;

    if (!found) 
      return FALSE;
  }

  return TRUE;
}

/* Boolean "user-expression" OR matching, returns TRUE if the expression
 * evaluates to TRUE.
 */
unsigned char pr_expr_eval_user_or(char **expr) {
  unsigned char found;
  char *user;

  for (; *expr; expr++) {
    user = *expr;
    found = FALSE;

    if (*user == '!') {
      found = !found;
      user++;
    }

    if (strcmp(session.user, user) == 0)
      found = !found;

    if (found)
      return TRUE;
  }

  return FALSE;
}
/* Per-directory configuration */

static int _strmatch(register char *s1, register char *s2) {
  register int len = 0;

  while (*s1 && *s2 && *s1++ == *s2++)
    len++;

  return len;
}

static config_rec *recur_match_path(pool *p, xaset_t *s, char *path) {
  char *tmp_path = NULL;
  config_rec *c = NULL, *res = NULL;

  if (!s)
    return NULL;

  for (c = (config_rec *) s->xas_list; c; c = c->next)
    if (c->config_type == CONF_DIR) {
      tmp_path = c->name;

      if (c->argv[1]) {
        if (*(char *)(c->argv[1]) == '~')
          c->argv[1] = dir_canonical_path(c->pool, (char *) c->argv[1]);

        tmp_path = pdircat(p, (char *) c->argv[1], tmp_path, NULL);
      }

      /* Exact path match */
      if (strcmp(tmp_path, path) == 0)
        return c;

      if (!strstr(tmp_path, "/*")) {
        size_t tmplen = strlen(tmp_path);

        /* Trim a trailing path separator, if present. */
        if (*tmp_path && *(tmp_path + tmplen - 1) == '/' && tmplen > 1) {
          *(tmp_path + tmplen - 1) = '\0';

          if (strcmp(tmp_path, path) == 0)
            return c;
        }

        tmp_path = pdircat(p, tmp_path, "*", NULL);
      }

      /* Temporary measure until we figure what's going on with
       * gnu fnmatch
       *
       * Hmm...wonder what this is, and if it's still an issue.  I love
       * cryptic comments in other people's code. :)
       *
       * - MacGyver
       */

#if 0
      if (pr_fnmatch(tmp_path, path, PR_FNM_PATHNAME) == 0) {
#else
      if (pr_fnmatch(tmp_path, path, 0) == 0) {
#endif
        if (c->subset) {
          if ((res = recur_match_path(p, c->subset, path)))
            return res;
        }

        return c;
      }
    }

  return NULL;
}

config_rec *dir_match_path(pool *p, char *path) {
  config_rec *res = NULL;
  char *tmp = NULL;
  size_t tmplen;

  if (!p || !path || !*path)
    return NULL;

  tmp = pstrdup(p, path);
  tmplen = strlen(tmp);

  if (*(tmp + tmplen - 1) == '*') {
    *(tmp + tmplen - 1) = '\0';
    tmplen = strlen(tmp);
  }

  if (*(tmp + tmplen - 1) == '/' && tmplen > 1)
    *(tmp + tmplen - 1) = '\0';

  if (session.anon_config) {
    res = recur_match_path(p, session.anon_config->subset, tmp);

    if (!res) {
      if (session.chroot_path &&
          !strncmp(session.chroot_path, tmp, strlen(session.chroot_path)))
        return NULL;
    }
  }

  if (!res)
    res = recur_match_path(p, main_server->conf, tmp);

  return res;
}

/* Returns TRUE to allow, FALSE to deny. */
static int dir_check_op(pool *p, xaset_t *c, int op, const char *path,
    uid_t uid, gid_t gid, mode_t mode) {
  int res = TRUE;
  uid_t *u = NULL;
  gid_t *g = NULL;
  unsigned char *hide_no_access = NULL;

  /* Default is to allow. */
  if (!c)
    return TRUE;

  switch (op) {
    case OP_HIDE:
      u = get_param_ptr(c, "HideUser", FALSE);

      while (u &&
             *u != (uid_t) -1 &&
             (*u != uid || *u == session.uid))
        u = get_param_ptr_next("HideUser", FALSE);

      if (u &&
          *u == uid) {
        res = 0;
        break;
      }

      g = get_param_ptr(c, "HideGroup", FALSE);

      while (g &&
             *g != (gid_t) -1 &&
             (*g != gid || *g == session.gid))
        g = get_param_ptr_next("HideGroup", FALSE);

      if (g &&
          *g == gid) {
        res = 0;
        break;
      }

      hide_no_access = get_param_ptr(c, "HideNoAccess", FALSE);
      if (hide_no_access &&
          *hide_no_access == TRUE) {

        if (S_ISDIR(mode)) {
          /* Check to see if the mode of this directory allows the
           * current user to list its contents.
           */
          res = pr_fsio_access(path, X_OK, session.uid, session.gid,
            session.gids) == 0 ? TRUE : FALSE;

        } else {
          /* Check to see if the mode of this file allows the current
           * user to read it.
           */
          res = pr_fsio_access(path, R_OK, session.uid, session.gid,
            session.gids) == 0 ? TRUE : FALSE;
        }
      }
      break;

    case OP_COMMAND: {
      unsigned char *allow_all = get_param_ptr(c, "AllowAll", FALSE);
      unsigned char *deny_all = get_param_ptr(c, "DenyAll", FALSE);

      if (allow_all &&
          *allow_all == TRUE)
        /* No-op */
        ;

      else if (deny_all &&
               *deny_all == TRUE) {
        res = 0;
        errno = EACCES;
      }
    }
    break;
  }

  return res;
}

static int _check_user_access(xaset_t *set, char *name) {
  int res = 0;
  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);

  while (c) {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_REGEX) {
      regex_t *preg = (regex_t *) c->argv[1];

      if (regexec(preg, session.user, 0, NULL, 0) == 0) {
        res = TRUE;
        break;
      }

    } else
#endif /* HAVE_REGEX_H and HAVE_REGCOMP */

    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
      res = pr_expr_eval_user_or((char **) &c->argv[1]);

      if (res)
        break;

    } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
      res = pr_expr_eval_user_and((char **) &c->argv[1]);

      if (res)
        break;
    }

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

  return res;
}

static int _check_group_access(xaset_t *set, char *name) {
  int res = 0;
  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);

  while (c) {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_REGEX) {
      regex_t *preg = (regex_t *) c->argv[1];

      if (session.group && regexec(preg, session.group, 0, NULL, 0) == 0) {
        res = TRUE;
        break;

      } else if (session.groups) {
        register int i = 0;

        for (i = session.groups->nelts-1; i >= 0; i--)
          if (regexec(preg, *(((char **) session.groups->elts) + i), 0,
              NULL, 0) == 0) {
            res = TRUE;
            break;
          }
      }

    } else
#endif /* HAVE_REGEX_H and HAVE_REGCOMP */

    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
      res = pr_expr_eval_group_or((char **) &c->argv[1]);

      if (res)
        break;

    } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
      res = pr_expr_eval_group_and((char **) &c->argv[1]);

      if (res)
        break;
    }

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

  return res;
}

static int _check_class_access(xaset_t *set, char *name) {
  int res = 0;
  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);

  while (c) {
#if defined(HAVE_REGEX_H) && defined(HAVE_REGCOMP)
    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_REGEX) {
      regex_t *preg = (regex_t *) c->argv[1];

      if (session.class &&
          regexec(preg, session.class->cls_name, 0, NULL, 0) == 0) {
        res = TRUE;
        break;
      }

    } else
#endif /* HAVE_REGEX_H and HAVE_REGCOMP */

    if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_OR) {
      res = pr_expr_eval_class_or((char **) &c->argv[1]);

      if (res)
        break;

    } else if (*((unsigned char *) c->argv[0]) == PR_EXPR_EVAL_AND) {
      res = pr_expr_eval_class_and((char **) &c->argv[1]);

      if (res)
        break;
    }

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

  return res;
}

/* As of 1.2.0rc3, a '!' character in front of the IP address
 * negates the logic (i.e. doesn't match).
 *
 * Here are our rules for matching an IP/host list:
 *
 *   (negate-cond-1 && negate-cond-2 && ... negate-cond-n) &&
 *   (cond-1 || cond-2 || ... cond-n)
 *
 * This boils down to the following two rules:
 *
 *   1. ALL negative ('!') conditions must evaluate to logically TRUE.
 *   2. One (or more) normal conditions must evaluate to logically TRUE.
 */

/* Check an ACL for negated rules and make sure all of them evaluate to TRUE.
 * Default (if none exist) is TRUE.
 */
static int _check_ip_negative(const config_rec *c) {
  int aclc;
  pr_netacl_t **aclv;

  for (aclc = c->argc, aclv = (pr_netacl_t **) c->argv; aclc; aclc--, aclv++) {
    if (pr_netacl_get_negated(*aclv) == FALSE)
      continue;

    switch (pr_netacl_match(*aclv, session.c->remote_addr)) {
      case 1:
        /* This actually means we DIDN'T match, and it's ok to short circuit
         * everything (negative).
         */
        return FALSE;

      case -1:
        /* -1 signifies a NONE match, which isn't valid for negative
         * conditions.
         */
        pr_log_pri(PR_LOG_ERR, "ooops, it looks like !NONE was used in an ACL "
          "somehow.");
        return FALSE;

      default:
        /* This means our match is actually true and we can continue */
        break;
    }
  }

  /* If we got this far either all conditions were TRUE or there were no
   * conditions.
   */

  return TRUE;
}

/* Check an ACL for positive conditions, short-circuiting if ANY of them are
 * TRUE.  Default return is FALSE.
 */
static int _check_ip_positive(const config_rec *c) {
  int aclc;
  pr_netacl_t **aclv;

  for (aclc = c->argc, aclv = (pr_netacl_t **) c->argv; aclc; aclc--, aclv++) {
    if (pr_netacl_get_negated(*aclv) == TRUE)
      continue;

    switch (pr_netacl_match(*aclv, session.c->remote_addr)) {
      case 1:
        /* Found it! */
        return TRUE;

      case -1:
        /* Special value "NONE", meaning nothing can match, so we can
         * short-circuit on this as well.
         */
        return FALSE;

      default:
        /* No match, keep trying */
        break;
    }
  }

  /* default return value is FALSE */
  return FALSE;
}

static int _check_ip_access(xaset_t *set, char *name) {
  int res = FALSE;

  config_rec *c = find_config(set, CONF_PARAM, name, FALSE);

  while (c) {
    /* If the negative check failed (default is success), short-circuit and
     * return FALSE
     */
    if (_check_ip_negative(c) != TRUE)
      return FALSE;

    /* Otherwise, continue on with boolean or check */
    if (_check_ip_positive(c) == TRUE)
      res = TRUE;

    /* Continue on, in case there are other acls that need to be checked
     * (multiple acls are logically OR'd)
     */
    c = find_config_next(c, c->next, CONF_PARAM, name, FALSE);
  }

  return res;
}

/* 1 if allowed, 0 otherwise */

static int _check_limit_allow(config_rec *c) {
  unsigned char *allow_all = NULL;

  /* If session.groups is null, this means no authentication
   * attempt has been made, so we simply check for the
   * very existance of an AllowGroup, and assume (for now) it's
   * allowed.  This works because later calls to _check_limit_allow
   * WILL have filled in the group members and we can truely check
   * group membership at that time.  Same goes for AllowUser.
   */

  if (!session.user) {
    if (find_config(c->subset, CONF_PARAM, "AllowUser", FALSE))
      return 1;

  } else if (_check_user_access(c->subset, "AllowUser"))
    return 1;

  if (!session.groups) {
    if (find_config(c->subset, CONF_PARAM, "AllowGroup", FALSE))
      return 1;

  } else if (_check_group_access(c->subset, "AllowGroup"))
    return 1;

  if (session.class &&
      _check_class_access(c->subset, "AllowClass"))
    return 1;

  if (_check_ip_access(c->subset, "Allow"))
    return 1;

  allow_all = get_param_ptr(c->subset, "AllowAll", FALSE);
  if (allow_all && *allow_all == TRUE)
    return 1;

  return 0;
}

static int _check_limit_deny(config_rec *c) {
  unsigned char *deny_all = get_param_ptr(c->subset, "DenyAll", FALSE);

  if (deny_all && *deny_all == TRUE)
    return 1;

  if (session.user &&
      _check_user_access(c->subset, "DenyUser"))
    return 1;

  if (session.groups &&
      _check_group_access(c->subset, "DenyGroup"))
    return 1;

  if (session.class &&
      _check_class_access(c->subset, "DenyClass"))
    return 1;

  if (_check_ip_access(c->subset, "Deny"))
    return 1;

  return 0;
}

/* _check_limit returns 1 if allowed, 0 if implicitly allowed,
 * and -1 if implicitly denied and -2 if explicitly denied.
 */

static int _check_limit(config_rec *c) {
  int *tmp = get_param_ptr(c->subset, "Order", FALSE);
  int order = tmp ? *tmp : ORDER_ALLOWDENY;

  if (order == ORDER_DENYALLOW) {
    /* Check deny first */

    if (_check_limit_deny(c))
      /* Explicit deny */
      return -2;

    if (_check_limit_allow(c))
      /* Explicit allow */
      return 1;

    /* Implicit deny */
    return -1;
  }

  /* Check allow first */
  if (_check_limit_allow(c))
    /* Explicit allow */
    return 1;

  if (_check_limit_deny(c))
    /* Explicit deny */
    return -2;

  /* Implicit allow */
  return 0;
}

/* Note: if and == 1, the logic is short circuited so that the first
 * failure results in a FALSE return from the entire function, if and
 * == 0, an ORing operation is assumed and the function will return
 * TRUE if any <limit LOGIN> allows access.
 */

int login_check_limits(xaset_t *set, int recurse, int and, int *found) {
  int res = and;
  int rfound;
  config_rec *c;
  int argc;
  char **argv;

  *found = 0;

  if (!set || !set->xas_list)
    return TRUE;			/* default is to allow */

  /* First check top level */
  for (c = (config_rec *) set->xas_list; c; c = c->next)
    if (c->config_type == CONF_LIMIT) {
      for (argc = c->argc, argv = (char **) c->argv; argc; argc--, argv++)
        if (strcasecmp("LOGIN", *argv) == 0)
          break;

      if (argc) {
        if (and) {
          switch (_check_limit(c)) {
            case 1:
              res = (res && TRUE);
              (*found)++;
              break;

	    case -1:
            case -2:
              res = (res && FALSE);
              (*found)++;
              break;
          }

          if (!res)
            break;

        } else {
          switch (_check_limit(c)) {
            case 1:
              res = TRUE;

	    case -1:
            case -2:
              (*found)++;
              break;
          }
        }
      }
    }

  if ( ((res && and) || (!res && !and && *found)) && recurse ) {
    for (c = (config_rec *) set->xas_list; c; c = c->next)
      if (c->config_type == CONF_ANON && c->subset && c->subset->xas_list) {
       if (and) {
         res = (res && login_check_limits(c->subset, recurse, and, &rfound));
         (*found) += rfound;
         if (!res)
           break;

       } else {
         int rres;

         rres = login_check_limits(c->subset, recurse, and, &rfound);
         if (rfound)
           res = (res || rres);
         (*found) += rfound;
         if (res)
           break;
       }
     }
  }

  if (!*found && !and)
    return TRUE;			/* Default is to allow */

  return res;
}

/* Check limit directives.
 */
static int _check_limits(xaset_t *set, char *cmd, int hidden) {
  int res = 1, ignore_hidden = -1;
  config_rec *lc = NULL;

  errno = 0;

  if (!set)
    return res;

  for (lc = (config_rec *) set->xas_list; lc && (res == 1); lc = lc->next) {

    if (lc->config_type == CONF_LIMIT) {
      register unsigned int i = 0;

      for (i = 0; i < lc->argc; i++) {
        if (strcasecmp(cmd, (char *) lc->argv[i]) == 0)
          break;
      }
	
      if (i == lc->argc)
        continue;

      /* Found a <Limit> directive associated with the current command.
       * ignore_hidden defaults to -1, if an explicit IgnoreHidden off is seen,
       * it is set to 0 and the check will not be done again up the chain.  If
       * an explicit "IgnoreHidden on" is seen, checking short-circuits and we
       * set ENOENT.
       */

      if (hidden && ignore_hidden == -1) {
        unsigned char *ignore = get_param_ptr(lc->subset, "IgnoreHidden",
          FALSE);

        if (ignore)
          ignore_hidden = *ignore;

        if (ignore_hidden == 1) {
          res = 0;
          errno = ENOENT;
          break;
        }
      }

      switch (_check_limit(lc)) {
        case 1:
          res++;
          break;
	
        case -1:
        case -2:
          res = 0;
          break;
	
        default:
          continue;
      }
    }
  }

  if (!res && !errno)
    errno = EACCES;

  return res;
}

int dir_check_limits(config_rec *c, char *cmd, int hidden) {
  int res = 1;

  for (; c && (res == 1); c = c->parent)
    res = _check_limits(c->subset, cmd, hidden);

  if (!c && (res == 1))
    /* vhost or main server has been reached without an explicit permit or deny,
     * so try the current server.
     */
    res = _check_limits(main_server->conf, cmd, hidden);

  return res;
}

void build_dyn_config(pool *p, char *_path, struct stat *stp,
    unsigned char recurse) {
  char *fullpath = NULL, *path = NULL, *dynpath = NULL, *cp = NULL;
  struct stat st;
  config_rec *d = NULL;
  xaset_t **set = NULL;
  int isfile, removed = 0;

  /* Switch through each directory, from "deepest" up looking for
   * new or updated .ftpaccess files
   */

  if (!_path)
    return;

  /* Check to see whether .ftpaccess files are allowed to be parsed. */
  if (!allow_dyn_config())
    return;

  path = pstrdup(p, _path);

  memcpy(&st, stp, sizeof(st));

  if (S_ISDIR(st.st_mode))
    dynpath = pdircat(p, path, "/.ftpaccess", NULL);
  else
    dynpath = NULL;

  while (path) {
    if (session.chroot_path) {
      fullpath = pdircat(p, session.chroot_path, path, NULL);

      if (strcmp(fullpath, "/") &&
          *(fullpath + strlen(fullpath) - 1) == '/')
        *(fullpath + strlen(fullpath) - 1) = '\0';

    } else
      fullpath = path;

    if (dynpath)
      isfile = pr_fsio_stat(dynpath, &st);

    else
      isfile = -1;

    d = dir_match_path(p, fullpath);

    if (!d && isfile != -1) {
      set = (session.anon_config ? &session.anon_config->subset :
             &main_server->conf);

      d = add_config_set(set, fullpath);
      d->config_type = CONF_DIR;
      d->argc = 1;
      d->argv = pcalloc(d->pool, 2 * sizeof (void *));

    } else if (d) {
      config_rec *newd,*dnext;

      if (isfile != -1 &&
          strcmp(d->name, fullpath) != 0) {
        set = &d->subset;
        newd = add_config_set(set, fullpath);
        newd->config_type = CONF_DIR;
        newd->argc = 1;
        newd->argv = pcalloc(newd->pool, 2 * sizeof(void *));
	newd->parent = d;

        d = newd;

      } else if (strcmp(d->name, fullpath) == 0 &&
          (isfile == -1 ||
           st.st_mtime > (d->argv[0] ? *((time_t *) d->argv[0]) : 0))) {

        set = (d->parent ? &d->parent->subset : &main_server->conf);

	if (d->subset && d->subset->xas_list) {

       	  /* Remove all old dynamic entries. */
          for (newd = (config_rec *)d->subset->xas_list; newd; newd = dnext) {
	    dnext = newd->next;

            if (newd->flags & CF_DYNAMIC) {
              xaset_remove(d->subset, (xasetmember_t *) newd);
              removed++;
            }
          }
	}

        if (d->subset && !d->subset->xas_list) {
          destroy_pool(d->subset->pool);
          d->subset = NULL;
          d->argv[0] = NULL;

	  /* If the file has been removed and no entries exist in this
           * dynamic entry, remove it completely.
           */
          if (isfile == -1)
            xaset_remove(*set, (xasetmember_t *) d);
        }
      }
    }

    if (isfile != -1 && d &&
        st.st_mtime > (d->argv[0] ? *((time_t *) d->argv[0]) : 0)) {

      /* File has been modified or not loaded yet */
      d->argv[0] = pcalloc(d->pool, sizeof(time_t));
      *((time_t *) d->argv[0]) = st.st_mtime;

      pr_parser_prepare(p, NULL);

      d->config_type = CONF_DYNDIR;

      if (pr_parser_parse_file(p, dynpath, d, PR_PARSER_FL_DYNAMIC_CONFIG) < 0)
        pr_log_debug(DEBUG0, "error parsing '%s': %s", dynpath,
          strerror(errno));

      d->config_type = CONF_DIR;
      pr_parser_cleanup();

      _mergedown(*set, TRUE);
    }

    if (isfile == -1 && removed && d && set) {
      pr_log_debug(DEBUG5, "dynamic configuration removed for %s", fullpath);
      _mergedown(*set, FALSE);
    }

    if (!recurse)
      break;

    cp = strrchr(path, '/');

    if (cp && strcmp(path, "/") != 0)
      *cp = '\0';
    else
      path = NULL;

    if (path) {
      if (*path && *(path + strlen(path) - 1) == '*')
        *(path +strlen(path) - 1) = '\0';

      dynpath = pdircat(p, path, "/.ftpaccess", NULL);
    }
  }
}

/* dir_check_full() fully recurses the path passed
 * returns 1 if operation is allowed on current path,
 * or 0 if not.
 */

/* dir_check_full() and dir_check() both take a `hidden' argument which is a
 * pointer to an integer. This is provided so that they can tell the calling
 * function if an entry should be hidden or not.  This is used by mod_ls to
 * determine if a file should be displayed.  Note that in this context, hidden
 * means "hidden by configuration" (HideUser, etc), NOT "hidden because it's a
 * .dotfile".
 */

int dir_check_full(pool *pp, char *cmd, char *group, char *path, int *hidden) {
  char *fullpath, *owner, *tmp = NULL;
  config_rec *c;
  struct stat st;
  pool *p;
  mode_t _umask = (mode_t) -1;
  int res = 1, isfile;
  int op_hidden = FALSE, regex_hidden = FALSE;

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

  p = make_sub_pool(pp);
  pr_pool_tag(p, "dir_check_full() subpool");

  fullpath = path;

  if (session.chroot_path)
    fullpath = pdircat(p, session.chroot_path, fullpath, NULL);

  pr_log_debug(DEBUG5, "in dir_check_full(): path = '%s', fullpath = '%s'.",
            path, fullpath);

  /* Check and build all appropriate dynamic configuration entries */
  pr_fs_clear_cache();
  isfile = pr_fsio_stat(path, &st);
  if (isfile == -1)
    memset(&st, '\0', sizeof(st));

  build_dyn_config(p, path, &st, TRUE);

  /* Check to see if this path is hidden by HideFiles. */
  tmp = strrchr(fullpath, '/');
  regex_hidden = tmp ? dir_hide_file(++tmp) : dir_hide_file(fullpath);

  /* Cache a pointer to the set of configuration data for this directory in
   * session.dir_config.
   */
  session.dir_config = c = dir_match_path(p, fullpath);

  if (!c && session.anon_config)
    c = session.anon_config;

  if (!_kludge_disable_umask) {
    /* Check for a directory Umask. */
    if (S_ISDIR(st.st_mode) ||
        strcmp(cmd, C_MKD) == 0 ||
        strcmp(cmd, C_XMKD) == 0) {
      mode_t *dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
      _umask = dir_umask ? *dir_umask : (mode_t) -1;
    }

    /* It's either a file, or we had no directory Umask. */
    if (_umask == (mode_t) -1) {
      mode_t *file_umask = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
      _umask = file_umask ? *file_umask : (mode_t) 0022;
    }
  }

  session.fsuid = (uid_t) -1;
  session.fsgid = (gid_t) -1;

  owner = get_param_ptr(CURRENT_CONF, "UserOwner", FALSE);
  if (owner != NULL) {
    /* Attempt chown() on all new files. */
    struct passwd *pw;

    pw = pr_auth_getpwnam(p, owner);
    if (pw != NULL)
      session.fsuid = pw->pw_uid;
  }

  owner = get_param_ptr(CURRENT_CONF, "GroupOwner", FALSE);
  if (owner != NULL) {
    /* Attempt chgrp() on all new files. */
    struct group *gr;

    gr = pr_auth_getgrnam(p, owner);
    if (gr != NULL)
      session.fsgid = gr->gr_gid;
  }

  if (isfile != -1) {
    /* Check to see if the current config "hides" the path or not. */
    op_hidden = !dir_check_op(p, CURRENT_CONF, OP_HIDE,
      session.chroot_path ? path : fullpath, st.st_uid, st.st_gid, st.st_mode);

    res = dir_check_op(p, CURRENT_CONF, OP_COMMAND,
      session.chroot_path ? path : fullpath, st.st_uid, st.st_gid, st.st_mode);
  }

  if (res) {
    /* Note that dir_check_limits() also handles IgnoreHidden.  If it is set,
     * these return 0 (no access), and also set errno to ENOENT so it looks
     * like the file doesn't exist.
     */
    res = dir_check_limits(c, cmd, op_hidden || regex_hidden);

    /* If specifically allowed, res will be > 1 and we don't want to
     * check the command group limit.
     */
    if (res == 1 && group)
      res = dir_check_limits(c, group, op_hidden || regex_hidden);

    /* If still == 1, no explicit allow so check lowest priority "ALL" group.
     * Note that certain commands are deliberately excluded from the
     * ALL group (i.e. EPRT, EPSV, PASV, and PORT).
     */
    if (res == 1 &&
        strcmp(cmd, C_EPRT) != 0 &&
        strcmp(cmd, C_EPSV) != 0 &&
        strcmp(cmd, C_PASV) != 0 &&
        strcmp(cmd, C_PORT) != 0)
      res = dir_check_limits(c, "ALL", op_hidden || regex_hidden);
  }

  if (res &&
      _umask != (mode_t) -1)
    pr_log_debug(DEBUG5,
      "in dir_check_full(): setting umask to %04o (was %04o)",
        (unsigned int) _umask, (unsigned int) umask(_umask));

  destroy_pool(p);

  if (hidden)
    *hidden = op_hidden || regex_hidden;

  return res;
}

/* dir_check() checks the current dir configuration against the path,
 * if it matches (partially), a search is done only in the subconfig,
 * otherwise handed off to dir_check_full
 */

int dir_check(pool *pp, char *cmd, char *group, char *path, int *hidden) {
  char *fullpath, *owner, *tmp = NULL;
  config_rec *c;
  struct stat st;
  pool *p;
  mode_t _umask = (mode_t) -1;
  int res = 1, isfile;
  int op_hidden = FALSE, regex_hidden = FALSE;

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

  p = make_sub_pool(pp);
  pr_pool_tag(p, "dir_check() subpool");

  fullpath = path;

  if (session.chroot_path)
    fullpath = pdircat(p, session.chroot_path, fullpath, NULL);

  c = (session.dir_config ? session.dir_config :
        (session.anon_config ? session.anon_config : NULL));

  if (!c || strncmp(c->name, fullpath, strlen(c->name)) != 0) {
    destroy_pool(p);
    return dir_check_full(pp, cmd, group, path, hidden);
  }

  /* Check and build all appropriate dynamic configuration entries */
  pr_fs_clear_cache();
  isfile = pr_fsio_stat(path, &st);
  if (isfile == -1)
    memset(&st, 0, sizeof(st));

  build_dyn_config(p, path, &st, FALSE);

  /* Check to see if this path is hidden by HideFiles. */
  tmp = strrchr(fullpath, '/');
  regex_hidden = tmp ? dir_hide_file(++tmp) : dir_hide_file(fullpath);

  /* Cache a pointer to the set of configuration data for this directory in
   * session.dir_config.
   */
  session.dir_config = c = dir_match_path(p, fullpath);

  if (!c && session.anon_config)
    c = session.anon_config;

  if (!_kludge_disable_umask) {
    /* Check for a directory Umask. */
    if (S_ISDIR(st.st_mode) ||
        strcmp(cmd, C_MKD) == 0 ||
        strcmp(cmd, C_XMKD) == 0) {
      mode_t *dir_umask = get_param_ptr(CURRENT_CONF, "DirUmask", FALSE);
      _umask = dir_umask ? *dir_umask : (mode_t) -1;
    }

    /* It's either a file, or we had no directory Umask. */
    if (_umask == (mode_t) -1) {
      mode_t *file_umask = get_param_ptr(CURRENT_CONF, "Umask", FALSE);
      _umask = file_umask ? *file_umask : (mode_t) 0022;
    }
  }

  session.fsuid = (uid_t) -1;
  session.fsgid = (gid_t) -1;

  owner = get_param_ptr(CURRENT_CONF, "UserOwner", FALSE);
  if (owner != NULL) {
    /* Attempt chown() on all new files. */
    struct passwd *pw;

    pw = pr_auth_getpwnam(p, owner);
    if (pw != NULL)
      session.fsuid = pw->pw_uid;
  }

  owner = get_param_ptr(CURRENT_CONF, "GroupOwner", FALSE);
  if (owner != NULL) {
    /* Attempt chgrp() on all new files. */
    struct group *gr;

    gr = pr_auth_getgrnam(p, owner);
    if (gr != NULL)
      session.fsgid = gr->gr_gid;
  }

  if (isfile != -1) {
    /* If not already marked as hidden by its name, check to see if the path
     * is to be hidden by nature of its mode
     */
    op_hidden = !dir_check_op(p, CURRENT_CONF, OP_HIDE,
      session.chroot_path ? path : fullpath, st.st_uid, st.st_gid, st.st_mode);

    res = dir_check_op(p, CURRENT_CONF, OP_COMMAND,
      session.chroot_path ? path : fullpath, st.st_uid, st.st_gid, st.st_mode);
  }

  if (res) {
    res = dir_check_limits(c, cmd, op_hidden || regex_hidden);

    /* If specifically allowed, res will be > 1 and we don't want to
     * check the command group limit.
     */
    if (res == 1 && group)
      res = dir_check_limits(c, group, op_hidden || regex_hidden);

    /* If still == 1, no explicit allow so check lowest priority "ALL" group.
     * Note that certain commands are deliberately excluded from the
     * ALL group (i.e. EPRT, EPSV, PASV, and PORT).
     */
    if (res == 1 &&
        strcmp(cmd, C_EPRT) != 0 &&
        strcmp(cmd, C_EPSV) != 0 &&
        strcmp(cmd, C_PASV) != 0 &&
        strcmp(cmd, C_PORT) != 0)
      res = dir_check_limits(c, "ALL", op_hidden || regex_hidden);
  }

  if (res &&
      _umask != (mode_t) -1)
    pr_log_debug(DEBUG5, "in dir_check(): setting umask to %04o (was %04o)",
        (unsigned int) _umask, (unsigned int) umask(_umask));

  destroy_pool(p);

  if (hidden)
    *hidden = op_hidden || regex_hidden;

  return res;
}

/* dir_check_canon() canonocalizes as much of the path as possible (which may
 * not be all of it, as the target may not yet exist) then we hand off to
 * dir_check().
 */
int dir_check_canon(pool *pp, char *cmd, char *group, char *path, int *hidden) {
  return dir_check(pp, cmd, group, dir_best_path(pp, path), hidden);
}

/* Move all the members (i.e. a "branch") of one config set to a different
 * parent.
 */
static void _reparent_all(config_rec *newparent, xaset_t *set) {
  config_rec *c,*cnext;

  if (!newparent->subset)
    newparent->subset = xaset_create(newparent->pool,NULL);

  for (c = (config_rec*)set->xas_list; c; c = cnext) {
    cnext = c->next;
    xaset_remove(set, (xasetmember_t*)c);
    xaset_insert(newparent->subset, (xasetmember_t*)c);
    c->set = newparent->subset;
    c->parent = newparent;
  }
}

/* Recursively find the most appropriate place to move a CONF_DIR
 * directive to.
 */

static config_rec *_find_best_dir(xaset_t *set,char *path,int *matchlen)
{
  config_rec *c,*res = NULL,*rres;
  int len,imatchlen,tmatchlen;

  *matchlen = 0;

  if (!set || !set->xas_list)
    return NULL;

  for (c = (config_rec*)set->xas_list; c; c=c->next) {
    if (c->config_type == CONF_DIR) {
      if (!strcmp(c->name,path))
        continue;				/* Don't examine the current */
      len = strlen(c->name);
      while (len > 0 &&
             (*(c->name+len-1) == '*' || *(c->name+len-1) == '/'))
        len--;

      /*
       * Just a partial match on the pathname does not mean that the longer
       * path is the subdirectory of the other -- they might just be sharing
       * the last path component!
       * /var/www/.1
       * /var/www/.14
       *            ^ -- not /, not subdir
       * /var/www/.1
       * /var/www/.1/images
       *            ^ -- /, is subdir
       */
      if (strlen(path) > len && path[len] != '/')
          continue;

      if (!strncmp(c->name,path,len) &&
         len < strlen(path)) {
           rres = _find_best_dir(c->subset,path,&imatchlen);
           tmatchlen = _strmatch(path,c->name);
           if (!rres && tmatchlen > *matchlen) {
             res = c;
             *matchlen = tmatchlen;
           } else if (imatchlen > *matchlen) {
             res = rres;
             *matchlen = imatchlen;
           }
         }
    }
  }

  return res;
}

/* Reorder all the CONF_DIR configuration sections, so that they are
 * in directory tree order
 */

static void _reorder_dirs(xaset_t *set, int flags) {
  config_rec *c = NULL, *cnext = NULL, *newparent = NULL;
  int tmp, defer = 0;

  if (!set || !set->xas_list)
    return;

  if (!(flags & CF_DEFER))
    defer = 1;

  for (c = (config_rec *) set->xas_list; c; c = cnext) {
    cnext = c->next;

    if (c->config_type == CONF_DIR) {
      if (flags && !(c->flags & flags))
        continue;

      if (defer && (c->flags & CF_DEFER))
        continue;

      /* If <Directory *> is used inside <Anonymous>, move all
       * the directives from '*' into the higher level.
       */
      if (strcmp(c->name, "*") == 0 &&
          c->parent &&
          c->parent->config_type == CONF_ANON) {

        if (c->subset)
          _reparent_all(c->parent, c->subset);

        xaset_remove(c->parent->subset, (xasetmember_t*) c);

      } else {
        newparent = _find_best_dir(set, c->name, &tmp);
        if (newparent) {
          if (!newparent->subset)
            newparent->subset = xaset_create(newparent->pool, NULL);

          xaset_remove(c->set, (xasetmember_t *) c);
          xaset_insert(newparent->subset, (xasetmember_t *) c);
          c->set = newparent->subset;
          c->parent = newparent;
        }
      }
    }
  }

  /* Top level is now sorted, now we recursively sort all the sublevels. */
  for (c = (config_rec *) set->xas_list; c; c = c->next)
    if (c->config_type == CONF_DIR || c->config_type == CONF_ANON)
      _reorder_dirs(c->subset, flags);
}

static void config_dumpf(const char *fmt, ...) {
  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
  va_list msg;

  va_start(msg, fmt);
  vsnprintf(buf, sizeof(buf), fmt, msg);
  va_end(msg);

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

  pr_log_debug(DEBUG5, "%s", buf);
}

void pr_config_dump(void (*dumpf)(const char *, ...), xaset_t *s,
    char *indent) {
  config_rec *c = NULL;

  if (!s)
    return;

  if (!indent)
    indent = "";

  for (c = (config_rec *) s->xas_list; c; c = c->next) {

    /* Don't display directives whose name starts with an underscore. */
    if (*(c->name) != '_')
      dumpf("%s%s", indent, c->name);

    if (c->subset)
      pr_config_dump(dumpf, c->subset,
        pstrcat(c->pool, indent, " ", NULL));
  }
}

static void _mergedown(xaset_t *s,int dynamic)
{
  config_rec *c,*dest,*newconf;
  int argc;
  void **argv,**sargv;

  if (!s || !s->xas_list)
    return;

  for (c = (config_rec*)s->xas_list; c; c=c->next) {
    if ((c->flags & CF_MERGEDOWN) ||
        (c->flags & CF_MERGEDOWN_MULTI))
      for (dest = (config_rec*)s->xas_list; dest; dest=dest->next) {
        if (dest->config_type == CONF_ANON ||
           dest->config_type == CONF_DIR) {

          /* If an option of the same name/type is found in the
           * next level down, it overrides, so we don't merge.
           */
          if ((c->flags & CF_MERGEDOWN) &&
              find_config(dest->subset, c->config_type, c->name, FALSE))
            continue;

          if (!dest->subset)
            dest->subset = xaset_create(dest->pool,NULL);

          newconf = add_config_set(&dest->subset,c->name);
          newconf->config_type = c->config_type;
          newconf->flags = c->flags | (dynamic ? CF_DYNAMIC : 0);
          newconf->argc = c->argc;
          newconf->argv = pcalloc(newconf->pool, (c->argc+1)*sizeof(void*));
          argv = newconf->argv; sargv = c->argv;
          argc = newconf->argc;
          while (argc--)
            *argv++ = *sargv++;
          *argv++ = NULL;
        }
      }
  }

  /* Top level merged, recursively merge lower levels */
  for (c = (config_rec*)s->xas_list; c; c=c->next)
    if (c->subset && (c->config_type == CONF_ANON ||
                     c->config_type == CONF_DIR))
      _mergedown(c->subset,dynamic);
}

/* iterate through <Directory> blocks inside of anonymous and
 * resolve each one.
 */

void resolve_anonymous_dirs(xaset_t *clist)
{
  config_rec *c;
  char *realdir;

  if (!clist)
    return;

  for (c = (config_rec*)clist->xas_list; c; c=c->next) {
    if (c->config_type == CONF_DIR) {
      if (c->argv[1]) {
        realdir = dir_best_path(c->pool,c->argv[1]);
        if (realdir)
          c->argv[1] = realdir;
        else {
          realdir = dir_canonical_path(c->pool,c->argv[1]);
          if (realdir)
            c->argv[1] = realdir;
        }
      }

      if (c->subset)
        resolve_anonymous_dirs(c->subset);
    }
  }
}

/* Iterate through directory configuration items and resolve ~ references. */
void resolve_deferred_dirs(server_rec *s) {
  config_rec *c;
  char *realdir;

  if (!s || !s->conf)
    return;

  for (c = (config_rec*)s->conf->xas_list; c; c=c->next) {
    if (c->config_type == CONF_DIR && (c->flags & CF_DEFER)) {

      /* Check for any expandable variables. */
      c->name = path_subst_uservar(c->pool, &c->name);

      realdir = dir_best_path(c->pool,c->name);

      if (realdir)
        c->name = realdir;

      else {
        realdir = dir_canonical_path(c->pool,c->name);
        if (realdir)
          c->name = realdir;
      }
    }
  }
}

static void copy_recur(xaset_t **set, pool *p, config_rec *c,
    config_rec *new_parent) {
  config_rec *newconf;
  int argc;
  void **argv, **sargv;

  if (!*set)
    *set = xaset_create(p, NULL);

  newconf = add_config_set(set, c->name);
  newconf->config_type = c->config_type;
  newconf->flags = c->flags;
  newconf->parent = new_parent;
  newconf->argc = c->argc;

  if (c->argc) {
    newconf->argv = pcalloc(newconf->pool, (c->argc+1) * sizeof(void *));
    argv = newconf->argv;
    sargv = c->argv;
    argc = newconf->argc;

    while (argc--)
      *argv++ = *sargv++;

    if (argv)
      *argv++ = NULL;
  }

  if (c->subset)
    for (c = (config_rec *) c->subset->xas_list; c; c = c->next)
      copy_recur(&newconf->subset, p, c, newconf);
}

static void copy_global_to_all(xaset_t *set) {
  server_rec *s;
  config_rec *c;

  if (!set || !set->xas_list)
    return;

  for (c = (config_rec *) set->xas_list; c; c = c->next)
    for (s = (server_rec *) server_list->xas_list; s; s = s->next)
      copy_recur(&s->conf, s->pool, c, NULL);
}

static void fixup_globals(void) {
  server_rec *s = NULL, *smain = NULL;
  config_rec *c = NULL, *cnext = NULL;

  smain = (server_rec *) server_list->xas_list;
  for (s = smain; s; s = s->next) {
    /* Loop through each top level directive looking for a CONF_GLOBAL
     * context.
     */
    if (!s->conf || !s->conf->xas_list)
      continue;

    for (c = (config_rec * )s->conf->xas_list; c; c = cnext) {
      cnext = c->next;

      if (c->config_type == CONF_GLOBAL &&
          strcmp(c->name, "<Global>") == 0) {
        /* Copy the contents of the block to all other servers
         * (including this one), then pull the block "out of play".
         */
        if (c->subset && c->subset->xas_list)
          copy_global_to_all(c->subset);

        xaset_remove(s->conf, (xasetmember_t *) c);

        if (!s->conf->xas_list) {
          destroy_pool(s->conf->pool);
          s->conf = NULL;
        }
      }
    }
  }
}

void fixup_dirs(server_rec *s, int flags) {
  if (!s)
    return;

  if (!s->conf) {
    if (!(flags & CF_SILENT)) {
      pr_log_debug(DEBUG5, "%s", "");
      pr_log_debug(DEBUG5, "Config for %s:", s->ServerName);
    }

    return;
  }
 
  _reorder_dirs(s->conf, flags);

  /* Merge mergeable configuration items down. */
  _mergedown(s->conf, FALSE);

  if (!(flags & CF_SILENT)) {
    pr_log_debug(DEBUG5, "%s", "");
    pr_log_debug(DEBUG5, "Config for %s:", s->ServerName);
    pr_config_dump(config_dumpf, s->conf, NULL);
  }

  return;
}

config_rec *find_config_next(config_rec *prev, config_rec *c, int type,
                             const char *name, int recurse)
{
  config_rec *top = c;

  /* We do two searches (if recursing) so that we find the "deepest"
   * level first.
   */

  if (!c && !prev)
    return NULL;

  if (!prev)
    prev = top;

  if (recurse) {
    do {
      config_rec *res = NULL;

      for (c = top; c; c=c->next) {
        if (c->subset && c->subset->xas_list) {
          config_rec *subc = NULL;

          for (subc = (config_rec *) c->subset->xas_list; subc;
              subc = subc->next) {
            if ((res = find_config_next(NULL, subc, type, name, recurse+1)))
              return res;
          }
        }
      }

      /* If deep recursion yielded no match try the current subset */
      /* NOTE: the string comparison here is specifically case sensitive.
       * The config_rec names are supplied by the modules and intentionally
       * case sensitive (they shouldn't be verbatim from the config file)
       * Do NOT change this to strcasecmp(), no matter how tempted you are
       * to do so, it will break stuff. ;)
       */
      for (c = top; c; c=c->next) {
        if ((type == -1 || type == c->config_type) &&
            (!name || !strcmp(name,c->name)))
          return c;
      }

      /* Restart the search at the previous level if required */
      if (prev->parent && recurse == 1 &&
         prev->parent->next &&
         prev->parent->set != find_config_top) {
        prev = top = prev->parent->next; c = top;
        continue;
      }

      break;
    } while (TRUE);

  } else {
    for (c = top; c; c=c->next) {
      if ((type == -1 || type == c->config_type) &&
         (!name || !strcmp(name, c->name)))
        return c;
    }
  }

  return NULL;
}

void find_config_set_top(config_rec *c)
{
  if (c && c->parent)
    find_config_top = c->parent->set;
  else
    find_config_top = NULL;
}


config_rec *find_config(xaset_t *set, int type, const char *name, int recurse)
{
  if (!set || !set->xas_list)
    return NULL;

  find_config_set_top((config_rec*)set->xas_list);

  return find_config_next(NULL, (config_rec*)set->xas_list,type,name,recurse);
}

/* These next two functions return the first argument in a
 * CONF_PARAM configuration entry.  If more than one or all
 * parameters are needed, the caller will need to use find_config,
 * and iterate through the argv themselves.
 * _int returns -1 if the config name is not found, _ptr returns
 * NULL.
 */

long get_param_int(xaset_t *set,const char *name,int recurse)
{
  config_rec *c;

  if (!set) {
    _last_param_int = NULL;
    return -1;
  }

  c = find_config(set,CONF_PARAM,name,recurse);

  if (c && c->argc) {
    _last_param_int = c;
    return (long)c->argv[0];
  }

  _last_param_int = NULL;
  return -1;  /* Parameters aren't allowed to contain neg. integers anyway */
}

long get_param_int_next(const char *name,int recurse)
{
  config_rec *c;

  if (!_last_param_int || !_last_param_int->next) {
    _last_param_int = NULL;
    return -1;
  }

  c = find_config_next(_last_param_int,_last_param_int->next,
                       CONF_PARAM,name,recurse);

  if (c && c->argc) {
    _last_param_int = c;
    return (long)c->argv[0];
  }

  _last_param_int = NULL;
  return -1;
}

void *get_param_ptr(xaset_t *set,const char *name,int recurse)
{
  config_rec *c;

  if (!set) {
    _last_param_ptr = NULL;
    return NULL;
  }

  c = find_config(set,CONF_PARAM,name,recurse);

  if (c && c->argc) {
    _last_param_ptr = c;
    return c->argv[0];
  }

  _last_param_ptr = NULL;
  return NULL;
}

void *get_param_ptr_next(const char *name,int recurse) {
  config_rec *c;

  if (!_last_param_ptr || !_last_param_ptr->next) {
    _last_param_ptr = NULL;
    return NULL;
  }

  c = find_config_next(_last_param_ptr,_last_param_ptr->next,
                       CONF_PARAM,name,recurse);

  if (c && c->argv) {
    _last_param_ptr = c;
    return c->argv[0];
  }

  _last_param_ptr = NULL;
  return NULL;
}

int remove_config(xaset_t *set, const char *name, int recurse) {
  server_rec *s = pr_parser_server_ctxt_get();
  config_rec *c;
  int found = 0;
  xaset_t *fset;

  if (!s)
    s = main_server;

  while ((c = find_config(set, -1, name, recurse)) != NULL) {
    found++;

    fset = c->set;
    xaset_remove(fset, (xasetmember_t *) c);

    /* If the set is empty, and has no more contained members in the xas_list,
     * destroy the set.
     */
    if (!fset->xas_list) {

      /* First, set any pointers to the container of the set to NULL. */
      if (c->parent && c->parent->subset == fset)
        c->parent->subset = NULL;

      else if (s->conf == fset)
        s->conf = NULL;

      /* Next, destroy the set's pool, which destroys the set as well. */
      destroy_pool(fset->pool);

    } else {

      /* If the set was not empty, destroy only the requested config_rec. */
      destroy_pool(c->pool);
    }
  }

  return found;
}

config_rec *add_config_param_set(xaset_t **set, const char *name,
    int num, ...) {
  config_rec *c = add_config_set(set, name);
  void **argv;
  va_list ap;

  if (c) {
    c->config_type = CONF_PARAM;
    c->argc = num;
    c->argv = pcalloc(c->pool, (num+1) * sizeof(void *));

    argv = c->argv;
    va_start(ap,num);

    while (num-- > 0)
      *argv++ = va_arg(ap, void *);

    va_end(ap);
  }

  return c;
}

config_rec *add_config_param_str(const char *name, int num, ...) {
  config_rec *c = add_config(NULL, name);
  char *arg = NULL;
  void **argv = NULL;
  va_list ap;

  if (c) {
    c->config_type = CONF_PARAM;
    c->argc = num;
    c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));

    argv = c->argv;
    va_start(ap, num);

    while (num-- > 0) {
      arg = va_arg(ap, char *);
      if (arg)
        *argv++ = pstrdup(c->pool, arg);
      else
        *argv++ = NULL;
    }

    va_end(ap);
  }

  return c;
}

config_rec *pr_conf_add_server_config_param_str(server_rec *s, const char *name,
    int num, ...) {
  config_rec *c = add_config(s, name);
  char *arg = NULL;
  void **argv = NULL;
  va_list ap;

  if (c) {
    c->config_type = CONF_PARAM;
    c->argc = num;
    c->argv = pcalloc(c->pool, (num+1) * sizeof(char *));

    argv = c->argv;
    va_start(ap, num);

    while (num-- > 0) {
      arg = va_arg(ap, char *);
      if (arg)
        *argv++ = pstrdup(c->pool, arg);
      else
        *argv++ = NULL;
    }

    va_end(ap);
  }

  return c;
}

config_rec *add_config_param(const char *name, int num, ...) {
  config_rec *c = add_config(NULL, name);
  void **argv;
  va_list ap;

  if (c) {
    c->config_type = CONF_PARAM;
    c->argc = num;
    c->argv = pcalloc(c->pool, (num+1) * sizeof(void*));

    argv = c->argv;
    va_start(ap, num);

    while (num-- > 0)
      *argv++ = va_arg(ap, void *);

    va_end(ap);
  }

  return c;
}

static int config_cmp(const void *a, const void *b) {
  return strcmp(*((char **) a), *((char **) b));
}

int parse_config_path(pool *p, const char *path) {
  struct stat st;
  int have_glob;
  
  if (!path) {
    errno = EINVAL;
    return -1;
  }

  have_glob = is_fnmatch(path); 

  if (!have_glob && pr_fsio_lstat(path, &st) < 0)
    return -1;

  if (have_glob ||
      (!S_ISLNK(st.st_mode) && S_ISDIR(st.st_mode))) {
    void *dirh;
    struct dirent *dent;
    array_header *file_list;
    char *dup_path = pstrdup(p, path);
    char *tmp = strrchr(dup_path, '/');

    if (have_glob && tmp) {
      *tmp++ = '\0';

      if (is_fnmatch(dup_path)) {
        pr_log_pri(PR_LOG_ERR, "error: wildcard patterns not allowed in "
          "configuration directory name '%s'", dup_path);
        errno = EINVAL;
        return -1;
      }

      /* Check the directory component. */
      pr_fsio_lstat(dup_path, &st);

      if (S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) {
        pr_log_pri(PR_LOG_ERR, "error: cannot read configuration path '%s'",
          dup_path);
        errno = EINVAL;
        return -1;
      }

      if (!is_fnmatch(tmp)) {
        pr_log_pri(PR_LOG_ERR, "error: wildcard pattern required for file '%s'",
          tmp);
        errno = EINVAL;
        return -1;
      }
    }

    pr_log_pri(PR_LOG_INFO, "processing configuration directory '%s'",
      dup_path);

    dirh = pr_fsio_opendir(dup_path);
    if (!dirh) {
      pr_log_pri(PR_LOG_ERR,
        "error: unable to open configuration directory '%s': %s", dup_path,
        strerror(errno));
      errno = EINVAL;
      return -1;
    }

    file_list = make_array(p, 0, sizeof(char *));

    while ((dent = pr_fsio_readdir(dirh)) != NULL) {
      if (strcmp(dent->d_name, ".") != 0 &&
          strcmp(dent->d_name, "..") != 0 &&
          (!have_glob ||
           pr_fnmatch(tmp, dent->d_name, PR_FNM_PERIOD) == 0))
        *((char **) push_array(file_list)) = pdircat(p, dup_path,
          dent->d_name, NULL);
    }

    pr_fsio_closedir(dirh);

    if (file_list->nelts) {
      register unsigned int i;

      qsort((void *) file_list->elts, file_list->nelts, sizeof(char *),
        config_cmp);

      for (i = 0; i < file_list->nelts; i++) {
        char *file = ((char **) file_list->elts)[i];
        pr_parser_parse_file(p, file, NULL, 0);
      }
    }

    return 0;
  }

  return pr_parser_parse_file(p, path, NULL, 0);
}

/* Go through each server configuration and complain if important information
 * is missing (post reading configuration files).  Otherwise, fill in defaults
 * where applicable.
 */
int fixup_servers(xaset_t *list) {
  config_rec *c = NULL;
  server_rec *s = NULL, *next_s = NULL;

  fixup_globals();

  s = (server_rec *) list->xas_list;
  if (s && !s->ServerName)
    s->ServerName = pstrdup(s->pool, "ProFTPD");

  for (; s; s = next_s) {
    unsigned char *default_server = NULL;

    next_s = s->next;
    if (!s->ServerAddress) {
      array_header *addrs = NULL;

      s->ServerAddress = pr_netaddr_get_localaddr_str(s->pool);

      s->addr = pr_netaddr_get_addr(s->pool, s->ServerAddress, &addrs);
     
      if (addrs) {
        register unsigned int i;
        pr_netaddr_t **elts = addrs->elts;

        /* For every additional address, implicitly add a bind record. */
        for (i = 0; i < addrs->nelts; i++) {
          const char *ipstr = pr_netaddr_get_ipstr(elts[i]);

#ifdef PR_USE_IPV6
          char ipbuf[INET6_ADDRSTRLEN];
          if (pr_netaddr_get_family(elts[i]) == AF_INET) {

            /* Create the bind record using the IPv4-mapped IPv6 version of
             * this address.
             */
            snprintf(ipbuf, sizeof(ipbuf), "::ffff:%s", ipstr);
            ipstr = ipbuf;
          }
#endif /* PR_USE_IPV6 */

          pr_conf_add_server_config_param_str(s, "_bind", 1, ipstr);
        }
      }
 
    } else 
      s->addr = pr_netaddr_get_addr(s->pool, s->ServerAddress, NULL);

    if (s->addr == NULL) {
      pr_log_pri(PR_LOG_WARNING,
        "warning: unable to determine IP address of '%s'", s->ServerAddress);
      xaset_remove(list, (xasetmember_t *) s);
      destroy_pool(s->pool);
      continue;
    }

    s->ServerFQDN = pr_netaddr_get_dnsstr(s->addr);

    if (!s->ServerFQDN)
      s->ServerFQDN = s->ServerAddress;

    if (!s->ServerAdmin)
      s->ServerAdmin = pstrcat(s->pool, "root@", s->ServerFQDN, NULL);

    if (!s->ServerName) {
      server_rec *m = (server_rec *) list->xas_list;
      s->ServerName = pstrdup(s->pool, m->ServerName);
    }

    if (!s->tcp_rcvbuf_len)
      s->tcp_rcvbuf_len = PR_TUNABLE_RCVBUFSZ;

    if (!s->tcp_sndbuf_len)
      s->tcp_sndbuf_len = PR_TUNABLE_SNDBUFSZ;

    if ((c = find_config(s->conf, CONF_PARAM, "MasqueradeAddress",
        FALSE)) != NULL) {
      pr_log_pri(PR_LOG_INFO, "%s:%d masquerading as %s",
        pr_netaddr_get_ipstr(s->addr), s->ServerPort,
        pr_netaddr_get_ipstr((pr_netaddr_t *) c->argv[0]));
    }

    /* Honor the DefaultServer directive only if SocketBindTight is not
     * in effect.
     */
    default_server= get_param_ptr(s->conf, "DefaultServer", FALSE);

    if (default_server && *default_server == TRUE) {
      if (!SocketBindTight)
        pr_netaddr_set_sockaddr_any(s->addr);
      else
        pr_log_pri(PR_LOG_NOTICE,
          "SocketBindTight in effect, ignoring DefaultServer");
    }

    fixup_dirs(s, 0);
  }

  /* Make sure there actually are server_recs remaining in the list
   * before continuing.  Badly configured/resolved vhosts are rejected, and
   * it's possible to have all vhosts (even the default) rejected.
   */
  if (list->xas_list == NULL) {
    pr_log_pri(PR_LOG_NOTICE, "error: no valid servers configured");
    return -1;
  }

  pr_inet_clear();
  return 0;
}

void init_config(void) {
  pool *conf_pool = make_sub_pool(permanent_pool);
  pr_pool_tag(conf_pool, "Config Pool");

  /* Make sure global_config_pool is destroyed */
  if (global_config_pool) {
    destroy_pool(global_config_pool);
    global_config_pool = NULL;
  }

  if (server_list) {
    server_rec *s, *s_next;

    /* Free the old configuration completely */
    for (s = (server_rec *) server_list->xas_list; s; s = s_next) {
      s_next = s->next;
      destroy_pool(s->pool);
    }
    destroy_pool(server_list->pool);
  }

  server_list = xaset_create(conf_pool, NULL);

  conf_pool = make_sub_pool(permanent_pool);
  pr_pool_tag(conf_pool, "main_server pool");

  main_server = (server_rec *) pcalloc(conf_pool, sizeof(server_rec));
  xaset_insert(server_list, (xasetmember_t *) main_server);

  main_server->pool = conf_pool;
  main_server->set = server_list;
  main_server->sid = 1;

  /* Default server port */
  main_server->ServerPort = pr_inet_getservport(main_server->pool,
    "ftp", "tcp");

  return;
}

/* These functions are used by modules to help parse configuration.
 */

unsigned char check_context(cmd_rec *cmd, int allowed) {
  int ctxt = (cmd->config && cmd->config->config_type != CONF_PARAM ?
     cmd->config->config_type : cmd->server->config_type ?
     cmd->server->config_type : CONF_ROOT);

  if (ctxt & allowed)
    return TRUE;

  /* default */
  return FALSE;
}

char *get_context_name(cmd_rec *cmd) {
  static char cbuf[20];

  if (!cmd->config || cmd->config->config_type == CONF_PARAM) {
    if (cmd->server->config_type == CONF_VIRTUAL)
      return "<VirtualHost>";
    else
      return "server config";
  }

  switch (cmd->config->config_type) {
    case CONF_DIR:
      return "<Directory>";

    case CONF_ANON:
      return "<Anonymous>";

    case CONF_CLASS:
      return "<Class>";

    case CONF_LIMIT:
      return "<Limit>";

    case CONF_DYNDIR:
      return ".ftpaccess";

    case CONF_GLOBAL:
      return "<Global>";

    case CONF_USERDATA:
      return "user data";

    default:
      /* XXX should dispatch to modules here, to allow them to create and
       * handle their own arbitrary configuration contexts.
       */
      memset(cbuf, '\0', sizeof(cbuf));
      snprintf(cbuf, sizeof(cbuf), "%d", cmd->config->config_type);
      return cbuf;
  }
}

/* Boolean string can be "on", "off", "yes", "no", "true", "false",
 * "1" or "0."
 */
int pr_is_boolean(const char *str) {
  if (strcasecmp(str, "on") == 0)
    return 1;

  if (strcasecmp(str, "off") == 0)
    return 0;

  if (strcasecmp(str, "yes") == 0)
    return 1;

  if (strcasecmp(str, "no") == 0)
    return 0;

  if (strcasecmp(str, "true") == 0)
    return 1;

  if (strcasecmp(str, "false") == 0)
    return 0;

  if (strcasecmp(str, "1") == 0)
    return 1;

  if (strcasecmp(str, "0") == 0)
    return 0;

  return -1;
}

int get_boolean(cmd_rec *cmd, int av) {
  char *cp = cmd->argv[av];

  return pr_is_boolean(cp);
}

char *get_full_cmd(cmd_rec *cmd) {
  pool *p = cmd->tmp_pool;
  char *res = "";

  if (cmd->arg && *cmd->arg)
    res = pstrcat(p, cmd->argv[0], " ", cmd->arg, NULL);

  else if (cmd->argc > 1) {
    register unsigned int i = 0;
    res = cmd->argv[0];

    for (i = 1; i < cmd->argc; i++)
      res = pstrcat(p, res, cmd->argv[i], " ", NULL);

    while (res[strlen(res)-1] == ' ' && *res)
      res[strlen(res)-1] = '\0';

  } else
    res = pstrdup(p, cmd->argv[0]);

  return res;
}
