/* ==================================================================== * Copyright (c) 1999, MeepZor Consulting. * All rights reserved. * * The use and distribution of this code or document is * governed by version 1.0.1 of the MeepZor Consulting Public * Licence (MCPL), which may be found on the Internet at * . * * * $Id: mod_access_identd.c,v 1.2 1999/07/12 15:14:11 coar Exp $ * * Abstract: *+ * A security module for the Apache Web server, supplying mandatory * access control based upon the client username and host. The * credentials are obtained using the identd (RFC1413) mechanism, * so this is of limited usefulness if document access is through a * proxy or by clients not running an RFC1413 server daemon. As a * result, this module is best suited for intranets. *- * * Package name: mod_access_identd * Package version: 1.0.1 * Package files: * INSTALL.mod_access_identd * README.mod_access_identd * mod_access_identd.c * mod_access_identd.cfgpatch * mod_access_identd.docpatch * mod_access_identd.html * */ /* * Apache identd-based (RFC1413) mandatory access control module. * Access is checked and a decision is made based upon the client's * host name (or address) and username; this takes place before * the point at which any user-supplied credentials are checked * (or even requested, if not already available). */ #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "http_main.h" #include "http_protocol.h" #include "http_request.h" #include "util_script.h" #include "fnmatch.h" #include /*--------------------------------------------------------------------------*/ /* */ /* Private data declarations. */ /* */ /*--------------------------------------------------------------------------*/ /* * Per-directory configuration record identifying the settings of * allowed clients. Note that this layout is currently tightly * coupled to the method bit-vector model of Apache 1.3; if and * when Apache deals with extension methods in some other way, * this will need to be reworked. * */ typedef struct mai_conf_t { int checking[METHODS]; /* IdentCheck {On|Off} */ #define CHECK_UNSET 0 /* Used to indicated inherited value */ #define CHECK_ON 1 #define CHECK_OFF 2 int order[METHODS]; /* IdentOrder {allow,deny|deny,allow} */ #define ORDER_UNSET 0 /* Inherited value rather than locally set */ #define ORDER_ALLOW_DENY 1 #define ORDER_DENY_ALLOW 2 array_header *allowed; /* The only reason for wildecard */ array_header *allowed_wild; /* patterns to be listed separately */ array_header *denied; /* is to allow matching non-wildcards */ array_header *denied_wild; /* to be tested first (less processing) */ } mai_conf_t; /* * Structure for an individual pattern in an Ident directive line. */ typedef struct mai_irec_t { char *pattern; /* "user@host", "user@address", or "all" */ char *user; char *host; int match; /* Notes about wildcards */ #define MATCH_EXACT 0; #define MATCH_ALL 1 /* "*", "*@*", or "all" */ #define MATCH_ALL_USERS 2 /* "*@host" or "*@address" */ #define MATCH_ALL_HOSTS 3 /* "user@*" */ } mai_irec_t; /* * Declare ourselves so the configuration routines can find and know us. * We'll fill it in at the end of the module. */ module access_identd_module; /* * Command handler for the "IdentCheck {On|Off}" directive. */ static const char *cmd_enable(cmd_parms *cmd, void *mconfig, int boolval) { mai_conf_t *dconf; int i; int setting; dconf = (mai_conf_t *) mconfig; setting = boolval ? CHECK_ON : CHECK_OFF; /* * Record the setting only for the methods to which the current * directive applies (such as if it appears inside a * container). */ for (i = 0; i < METHODS; ++i) { if (cmd->limited & (1 << i)) { dconf->checking[i] = setting; } } return NULL; } /* * Command handler for the "IdentOrder {allow,deny|deny,allow}" directive. */ static const char *cmd_order(cmd_parms *cmd, void *mconfig, char *word1) { mai_conf_t *dconf; int i; int order; dconf = (mai_conf_t *) mconfig; if (strcasecmp(word1, "allow,deny") == 0) { order = ORDER_ALLOW_DENY; } else if (strcasecmp(word1, "deny,allow") == 0) { order = ORDER_DENY_ALLOW; } else { return "unrecognised order"; } /* * Note the order for all methods being limited. */ for (i = 0; i < METHODS; ++i) { if (cmd->limited & (1 << i)) { dconf->order[i] = order; } } return NULL; } /* * Command handler for the "Ident {allow|deny} user@host ..." directive. */ static const char *cmd_idaccess(cmd_parms *cmd, void *mconfig, char *word1, char *word2) { mai_conf_t *dconf; mai_irec_t item; mai_irec_t *irec; char *lhs; char *rhs; array_header *list; int wildcard = 0; dconf = (mai_conf_t *) mconfig; item.match = MATCH_EXACT; item.pattern = ap_pstrdup(cmd->pool, word2); /* * Pick the pattern apart into its components, if any, and store them * separately. */ lhs = word2; rhs = strchr(word2, '@'); if (rhs == NULL) { item.user = ap_pstrdup(cmd->pool, lhs); item.host = NULL; } else { item.user = ap_pstrndup(cmd->pool, lhs, (rhs - lhs)); item.host = ap_pstrdup(cmd->pool, ++rhs); } /* * Find out if the pattern includes any wildcards. If it does, we can * short-circuit some runtime processing. */ wildcard = (strchr(word2, '*') != NULL) || (strchr(word2, '?') != NULL) || (strchr(word2, '[') != NULL) || (strchr(word2, ']') != NULL) || (strcasecmp(word2, "all") == 0); if (wildcard) { if ((strcmp(word2, "*") == 0) || (strcmp(word2, "*@*") == 0) || (strcasecmp(word2, "all") == 0)) { item.match = MATCH_ALL; } else if (strncmp(word2, "*@", 2) == 0) { item.match = MATCH_ALL_USERS; } else if (strstr(word2, "@*") != NULL) { item.match = MATCH_ALL_HOSTS; } } /* * Now that it's parsed, a final check for validity.. */ if ((item.match != MATCH_ALL) && ((item.user == NULL) || (item.host == NULL))) { return ap_psprintf(cmd->pool, "%s (line %d): malformed identity pattern: '%s'", cmd->cmd->name, cmd->config_file->line_number, word2); } /* * Figure out to which of the lists this pattern should be added. */ if (strcasecmp(word1, "allow") == 0) { list = wildcard ? dconf->allowed_wild : dconf->allowed; } else if (strcasecmp(word1, "deny") == 0) { list = wildcard ? dconf->denied_wild : dconf->denied; } else { return "first keyword must be either 'allow' or 'deny'"; } /* * All set; add the information to the config record and depart. */ irec = (mai_irec_t *) ap_push_array(list); irec->pattern = item.pattern; irec->user = item.user; irec->host = item.host; irec->match = item.match; return NULL; } /*--------------------------------------------------------------------------*/ /* */ /* These routines are strictly internal to this module, and support its */ /* operation. They are not referenced by any external portion of the */ /* server. */ /* */ /*--------------------------------------------------------------------------*/ /* * Locate our directory configuration record for the current request. */ static mai_conf_t *our_dconfig(request_rec *r) { return (mai_conf_t *) ap_get_module_config(r->per_dir_config, &access_identd_module); } /* * This function gets called to create up a per-directory configuration * record. This will be called for the "default" server environment, and for * each directory for which the parser finds any of our directives applicable. * If a directory doesn't have any of our directives involved (i.e., they * aren't in the .htaccess file, or a , , or related * block), this routine will *not* be called - the configuration for the * closest ancestor is used. * * The return value is a pointer to the created module-specific * structure. */ static void *mai_create_dconf(pool *p, char *dirspec) { mai_conf_t *dconf; int i; /* * Allocate the space for our record from the pool supplied. */ dconf = (mai_conf_t *) ap_pcalloc(p, sizeof(mai_conf_t)); /* * Now fill in the defaults. If there are any `parent' configuration * records, they'll get merged as part of a separate callback. */ for (i = 0; i < METHODS; ++i) { dconf->order[i] = ORDER_UNSET; dconf->checking[i] = CHECK_UNSET; } dconf->allowed = ap_make_array(p, 2, sizeof(mai_irec_t)); dconf->allowed_wild = ap_make_array(p, 2, sizeof(mai_irec_t)); dconf->denied = ap_make_array(p, 2, sizeof(mai_irec_t)); dconf->denied_wild = ap_make_array(p, 2, sizeof(mai_irec_t)); return (void *) dconf; } /* * This function gets called to merge two per-directory configuration * records. This is typically done to cope with things like .htaccess files * or directives for directories that are beneath one for which a * configuration record was already created. The routine has the * responsibility of creating a new record and merging the contents of the * other two into it appropriately. If the module doesn't declare a merge * routine, the record for the closest ancestor location (that has one) is * used exclusively. * * The routine MUST NOT modify any of its arguments! * * The return value is a pointer to the created module-specific structure * containing the merged values. */ static void *mai_merge_dconf(pool *p, void *parent_conf, void *newloc_conf) { mai_conf_t *merged_config; mai_conf_t *pconf = (mai_conf_t *) parent_conf; mai_conf_t *nconf = (mai_conf_t *) newloc_conf; int i; merged_config = (mai_conf_t *) ap_pcalloc(p, sizeof(mai_conf_t)); /* * Inherit the order and activation setting from the closest config, * or from the parent if the closest one doesn't have an explicit * setting. */ for (i = 0; i < METHODS; ++i) { merged_config->order[i] = (nconf->order[i] == ORDER_UNSET) ? pconf->order[i] : nconf->order[i]; merged_config->checking[i] = (nconf->checking[i] == CHECK_UNSET) ? pconf->checking[i] : nconf->checking[i]; } /* * Merge the patterns and allow/deny settings, giving the * closer ones (nconf) preference by putting them first in * the final arrays. */ merged_config->allowed = ap_append_arrays(p, nconf->allowed, pconf->allowed); merged_config->allowed_wild = ap_append_arrays(p, nconf->allowed_wild, pconf->allowed_wild); merged_config->denied = ap_append_arrays(p, nconf->denied, pconf->denied); merged_config->denied_wild = ap_append_arrays(p, nconf->denied_wild, pconf->denied_wild); return (void *) merged_config; } #define USERFLAG (0) #define HOSTFLAG (FNM_CASE_BLIND) /* * Check to see if the credentials match any of the items in a list. * If so, return the matching entry (so it can be used to construct * a log message if necessary); otherwise return NULL. */ static mai_irec_t *mai_idmatch(array_header *list, const char *ruser, const char *rhostname, char *rhostaddr) { int i; mai_irec_t *irec; mai_irec_t *items; items = (mai_irec_t *) list->elts; for (i = 0; i < list->nelts; ++i) { irec = &items[i]; if ((irec->match == MATCH_ALL) || ((irec->match == MATCH_ALL_HOSTS) && (ap_fnmatch(irec->user, ruser, USERFLAG) == 0)) || ((irec->match == MATCH_ALL_USERS) && ((ap_fnmatch(irec->host, rhostname, HOSTFLAG) == 0) || (ap_fnmatch(irec->host, rhostaddr, HOSTFLAG) == 0))) || ((ap_fnmatch(irec->user, ruser, USERFLAG) == 0) && ((ap_fnmatch(irec->host, rhostname, HOSTFLAG) == 0) || (ap_fnmatch(irec->host, rhostaddr, HOSTFLAG) == 0)))) { return irec; } } return NULL; } /* * Check the client's RFC1413 identity against our access lists. * The return value is OK, DECLINED, or HTTP_mumble. */ static int mai_idcheck(request_rec *r) { mai_conf_t *dconf; mai_irec_t *match; int result; const char *ruser; const char *rhostname; char *rhostaddr; char *errmsg = "unknown reason"; dconf = our_dconfig(r); /* * If our checking is turned off in this scope, don't do anything. */ if (dconf->checking[r->method_number] != CHECK_ON) { return DECLINED; } ruser = ap_get_remote_logname(r); /* * If we're requiring an RFC1413 identity and we can't get one, * deny access with no questions. */ if (((ruser == NULL) || (strcmp(ruser, "unknown") == 0)) && ((ap_satisfies(r) != SATISFY_ANY) || (!ap_some_auth_required(r)))) { ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r, "access denied to %s : identd response required " "from client and not available", r->uri); return HTTP_FORBIDDEN; } rhostname = ap_get_remote_host(r->connection, r->per_dir_config, REMOTE_DOUBLE_REV); rhostaddr = r->connection->remote_ip; /* * We use "deny,allow" if it was explicitly specified or as a * default if no order has been chosen. */ result = (dconf->order[r->method_number] == ORDER_ALLOW_DENY) ? HTTP_FORBIDDEN : OK; /* * We have all our information, so now let's walk through the lists * and do the actual evaluation. */ if (dconf->order[r->method_number] == ORDER_ALLOW_DENY) { match = mai_idmatch(dconf->allowed, ruser, rhostname, rhostaddr); if (match == NULL) { /* * Not found, check the wildcard list to see if one of those * catches it. */ match = mai_idmatch(dconf->allowed_wild, ruser, rhostname, rhostaddr); } /* * We have to have at least one matched 'allow' clause, or * it's no use going on. */ if (match == NULL) { errmsg = ap_psprintf(r->pool, "no 'allow' rule permitting ident=%s@%s", ruser, rhostname); result = HTTP_FORBIDDEN; } else { /* * We're allowed so far; let's see if something explicitly * shuts us out. */ result = OK; match = mai_idmatch(dconf->denied, ruser, rhostname, rhostaddr); if (match == NULL) { match = mai_idmatch(dconf->denied_wild, ruser, rhostname, rhostaddr); } if (match != NULL) { result = HTTP_FORBIDDEN; errmsg = ap_psprintf(r->pool, "denied by pattern '%s'", match->pattern); } } } else { /* * Check the 'deny' rules first. */ match = mai_idmatch(dconf->denied, ruser, rhostname, rhostaddr); if (match == NULL) { match = mai_idmatch(dconf->denied_wild, ruser, rhostname, rhostaddr); } if (match != NULL) { /* * Something has blocked us out; assume it won't be overridden. */ result = HTTP_FORBIDDEN; errmsg = ap_psprintf(r->pool, "denied by pattern '%s'", match->pattern); /* * Only bother checking for 'allow' rules if we haven't been * blocked yet, */ match = mai_idmatch(dconf->allowed, ruser, rhostname, rhostaddr); if (match == NULL) { match = mai_idmatch(dconf->allowed_wild, ruser, rhostname, rhostaddr); } if (match != NULL) { /* * Found one cancelling the blockage. Issue a special * log message for this condition, since we're allowing * access in a primarily disallowed environment; this may be * useful in case of an unintended pattern match. The * message is INFO rather than ERROR severity. */ result = OK; ap_log_rerror(APLOG_MARK, APLOG_INFO|APLOG_NOERRNO, r, "access to %s granted to %s@%s by pattern '%s'", r->uri, ruser, (rhostname != NULL) ? rhostname : rhostaddr, match->pattern); } } } /* * If our decision is final (that is, it can't be overridden by * some discretionary access checking), say so. We really ought to * point out here that *we* said no but someone else might say yes, * if that's the case.. unfortunately, since modules typically * don't log a message when they *allow* access, we'd probably only * be confusing matters. I don't think the discretionary checks * can tell what result the mandatory ones return, so they couldn't * even be retrofitted to play with us on this. */ if ((result == HTTP_FORBIDDEN) && ((ap_satisfies(r) != SATISFY_ANY) || (!ap_some_auth_required(r)))) { ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r, "access denied to %s : %s", r->uri, errmsg); } return result; } /*--------------------------------------------------------------------------*/ /* */ /* All of the routines have been declared now. Here's the list of */ /* directives specific to our module, and information about where they */ /* may appear and how the command parser should pass them to us for */ /* processing. Note that care must be taken to ensure that there are NO */ /* collisions of directive names between modules. */ /* */ /*--------------------------------------------------------------------------*/ /* * List of directives specific to our module. */ static const command_rec mai_cmds[] = { { "IdentCheck", /* directive name */ cmd_enable, /* config action routine */ NULL, /* argument to include in call */ OR_AUTHCFG, /* where available */ FLAG, /* arguments */ "'On' or 'Off' to control identd access checking" /* directive argument description */ }, { "IdentOrder", /* directive name */ cmd_order, /* config action routine */ NULL, /* argument to include in call */ OR_AUTHCFG, /* where available */ TAKE1, /* arguments */ "'allow,deny' or 'deny,allow'" /* directive argument description */ }, { "Ident", /* directive name */ cmd_idaccess, /* config action routine */ NULL, /* argument to include in call */ OR_AUTHCFG, /* where available */ ITERATE2, /* arguments */ "{allow|deny} followed by list of host@ident pairs" /* directive argument description */ }, { NULL } }; /*--------------------------------------------------------------------------*/ /* */ /* Finally, the list of callback routines and data structures that */ /* provide the hooks into our module from the other parts of the server. */ /* */ /*--------------------------------------------------------------------------*/ /* * Module definition for configuration. If a particular callback is not * needed, its slot contains NULL. * */ module access_identd_module = { STANDARD_MODULE_STUFF, NULL, /* module initializer */ mai_create_dconf, /* per-directory config creator */ mai_merge_dconf, /* dir config merger */ NULL, /* server config creator */ NULL, /* server config merger */ mai_cmds, /* command table */ NULL, /* [7] list of handlers */ NULL, /* [2] filename-to-URI translation */ NULL, /* [5] check/validate user_id */ NULL, /* [6] check user_id is valid *here* */ mai_idcheck, /* [4] check access by host address */ NULL, /* [7] MIME type checker/setter */ NULL, /* [8] fixups */ NULL, /* [10] logger */ #if MODULE_MAGIC_NUMBER >= 19970103 NULL, /* [3] header parser */ #endif #if MODULE_MAGIC_NUMBER >= 19970719 NULL, /* process initializer */ #endif #if MODULE_MAGIC_NUMBER >= 19970728 NULL, /* process exit/cleanup */ #endif #if MODULE_MAGIC_NUMBER >= 19970902 NULL /* [1] post read_request handling */ #endif };