/* * ProFTPD - FTP server daemon * Copyright (c) 1997, 1998 Public Flood Software * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu * 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.171 2005/09/28 02:06:26 castaglia Exp $ */ #include "conf.h" #ifdef HAVE_ARPA_INET_H # include #endif #ifdef HAVE_REGEX_H # include #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 . 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 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 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_size > 0 && 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 is used inside , 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 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; 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)) { char *realdir; /* 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, "") == 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 = NULL; } /* Note: xaset_create() assigns the given pool to the 'pool' member * of the created list, i.e. server_list->pool == conf_pool. Hence * why we create yet another subpool, reusing the conf_pool pointer. * The pool creation below is not redundant. */ 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 ""; else return "server config"; } switch (cmd->config->config_type) { case CONF_DIR: return ""; case CONF_ANON: return ""; case CONF_CLASS: return ""; case CONF_LIMIT: return ""; case CONF_DYNDIR: return ".ftpaccess"; case CONF_GLOBAL: return ""; 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; errno = EINVAL; 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; }