/* * * auth_mellon_util.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * 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 * */ #include #include #include #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This function is used to get the url of the current request. * * Parameters: * request_rec *r The current request. * * Returns: * A string containing the full url of the current request. * The string is allocated from r->pool. */ char *am_reconstruct_url(request_rec *r) { char *url; /* This function will construct an full url for a given path relative to * the root of the web site. To configure what hostname and port this * function will use, see the UseCanonicalName configuration directive. */ url = ap_construct_url(r->pool, r->unparsed_uri, r); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "reconstruct_url: url==\"%s\", unparsed_uri==\"%s\"", url, r->unparsed_uri); return url; } /* Get the hostname of the current request. * * Parameters: * request_rec *r The current request. * * Returns: * The hostname of the current request. */ static const char *am_request_hostname(request_rec *r) { const char *url; apr_uri_t uri; int ret; url = am_reconstruct_url(r); ret = apr_uri_parse(r->pool, url, &uri); if (ret != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to parse request URL: %s", url); return NULL; } if (uri.hostname == NULL) { /* This shouldn't happen, since the request URL is built with a hostname, * but log a message to make any debuggin around this code easier. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No hostname in request URL: %s", url); return NULL; } return uri.hostname; } /* Validate the redirect URL. * * Checks that the redirect URL is to a trusted domain & scheme. * * Parameters: * request_rec *r The current request. * const char *url The redirect URL to validate. * * Returns: * OK if the URL is valid, HTTP_BAD_REQUEST if not. */ int am_validate_redirect_url(request_rec *r, const char *url) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); apr_uri_t uri; int ret; ret = apr_uri_parse(r->pool, url, &uri); if (ret != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid redirect URL: %s", url); return HTTP_BAD_REQUEST; } /* Sanity check of the scheme of the domain. We only allow http and https. */ if (uri.scheme) { if (strcasecmp(uri.scheme, "http") && strcasecmp(uri.scheme, "https")) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Only http or https scheme allowed in redirect URL: %s (%s)", url, uri.scheme); return HTTP_BAD_REQUEST; } } if (!uri.hostname) { return OK; /* No hostname to check. */ } for (int i = 0; cfg->redirect_domains[i] != NULL; i++) { const char *redirect_domain = cfg->redirect_domains[i]; if (!strcasecmp(redirect_domain, "[self]")) { if (!strcasecmp(uri.hostname, am_request_hostname(r))) { return OK; } } else if (apr_fnmatch(redirect_domain, uri.hostname, APR_FNM_PERIOD | APR_FNM_CASE_BLIND) == APR_SUCCESS) { return OK; } } AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Untrusted hostname (%s) in redirect URL: %s", uri.hostname, url); return HTTP_BAD_REQUEST; } /* This function builds an array of regexp backreferences * * Parameters: * request_rec *r The current request. * const am_cond_t *ce The condition * const char *value Attribute value * const ap_regmatch_t *regmatch regmatch_t from ap_regexec() * * Returns: * An array of collected backreference strings */ const apr_array_header_t *am_cond_backrefs(request_rec *r, const am_cond_t *ce, const char *value, const ap_regmatch_t *regmatch) { apr_array_header_t *backrefs; const char **ref; int nsub; int i; nsub = ce->regex->re_nsub + 1; /* +1 for %0 */ backrefs = apr_array_make(r->pool, nsub, sizeof(const char *)); backrefs->nelts = nsub; ref = (const char **)(backrefs->elts); for (i = 0; i < nsub; i++) { if ((regmatch[i].rm_so == -1) || (regmatch[i].rm_eo == -1)) { ref[i] = ""; } else { int len = regmatch[i].rm_eo - regmatch[i].rm_so; int off = regmatch[i].rm_so; ref[i] = apr_pstrndup(r->pool, value + off, len); } } return (const apr_array_header_t *)backrefs; } /* This function clones an am_cond_t and substitute value to * match (both regexp and string) with backreferences from * a previous regex match. * * Parameters: * request_rec *r The current request. * const am_cond_t *cond The am_cond_t to clone and substiture * const apr_array_header_t *backrefs Collected backreferences * * Returns: * The cloned am_cond_t */ const am_cond_t *am_cond_substitue(request_rec *r, const am_cond_t *ce, const apr_array_header_t *backrefs) { am_cond_t *c; const char *instr = ce->str; apr_size_t inlen = strlen(instr); const char *outstr = ""; size_t last; size_t i; c = (am_cond_t *)apr_pmemdup(r->pool, ce, sizeof(*ce)); last = 0; for (i = strcspn(instr, "%"); i < inlen; i += strcspn(instr + i, "%")) { const char *fstr; const char *ns; const char *name; const char *value; apr_size_t flen; apr_size_t pad; apr_size_t nslen; /* * Make sure we got a % */ assert(instr[i] == '%'); /* * Copy the format string in fstr. It can be a single * digit (e.g.: %1) , or a curly-brace enclosed text * (e.g.: %{12}) */ fstr = instr + i + 1; if (*fstr == '{') { /* Curly-brace enclosed text */ pad = 3; /* 3 for %{} */ fstr++; flen = strcspn(fstr, "}"); /* If there is no closing }, we do not substitute */ if (fstr[flen] == '\0') { pad = 2; /* 2 for %{ */ i += flen + pad; break; } } else if (*fstr == '\0') { /* String ending by a % */ break; } else { /* Single digit */ pad = 1; /* 1 for % */ flen = 1; } /* * Try to extract a namespace (ns) and a name, e.g: %{ENV:foo} */ fstr = apr_pstrndup(r->pool, fstr, flen); if ((nslen = strcspn(fstr, ":")) != flen) { ns = apr_pstrndup(r->pool, fstr, nslen); name = fstr + nslen + 1; /* +1 for : */ } else { nslen = 0; ns = ""; name = fstr; } value = NULL; if ((*ns == '\0') && (strspn(fstr, "0123456789") == flen) && (backrefs != NULL)) { /* * If fstr has only digits, this is a regexp backreference */ int d = (int)apr_atoi64(fstr); if ((d >= 0) && (d < backrefs->nelts)) value = ((const char **)(backrefs->elts))[d]; } else if ((*ns == '\0') && (strcmp(fstr, "%") == 0)) { /* * %-escape */ value = fstr; } else if (strcmp(ns, "ENV") == 0) { /* * ENV namespace. Get value from apache environment. * This is akin to how Apache itself does it during expression evaluation. */ value = apr_table_get(r->subprocess_env, name); if (value == NULL) { value = apr_table_get(r->notes, name); } if (value == NULL) { value = getenv(name); } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Resolving \"%s\" from ENV to \"%s\"", name, value == NULL ? "(nothing)" : value); } /* * If we did not find a value, substitue the * format string with an empty string. */ if (value == NULL) value = ""; /* * Concatenate the value with leading text, and * keep track * of the last location we copied in source string */ outstr = apr_pstrcat(r->pool, outstr, apr_pstrndup(r->pool, instr + last, i - last), value, NULL); last = i + flen + pad; /* * Move index to the end of the format string */ i += flen + pad; } /* * Copy text remaining after the last format string. */ outstr = apr_pstrcat(r->pool, outstr, apr_pstrndup(r->pool, instr + last, i - last), NULL); c->str = outstr; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Directive %s, \"%s\" substituted into \"%s\"", ce->directive, instr, outstr); /* * If this was a regexp, recompile it. */ if (ce->flags & AM_COND_FLAG_REG) { int regex_flags = AP_REG_EXTENDED|AP_REG_NOSUB; if (ce->flags & AM_COND_FLAG_NC) regex_flags |= AP_REG_ICASE; c->regex = ap_pregcomp(r->pool, outstr, regex_flags); if (c->regex == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Invalid regular expression \"%s\"", outstr); return ce; } } return (const am_cond_t *)c; } /* This function checks if the user has access according * to the MellonRequire and MellonCond directives. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The current session. * * Returns: * OK if the user has access and HTTP_FORBIDDEN if he doesn't. */ int am_check_permissions(request_rec *r, am_cache_entry_t *session) { am_dir_cfg_rec *dir_cfg; int i, j; int skip_or = 0; const apr_array_header_t *backrefs = NULL; dir_cfg = am_get_dir_cfg(r); /* Iterate over all cond-directives */ for (i = 0; i < dir_cfg->cond->nelts; i++) { const am_cond_t *ce; const char *value = NULL; int match = 0; ce = &((am_cond_t *)(dir_cfg->cond->elts))[i]; am_diag_printf(r, "%s processing condition %d of %d: %s ", __func__, i, dir_cfg->cond->nelts, am_diag_cond_str(r, ce)); /* * Rule with ignore flog? */ if (ce->flags & AM_COND_FLAG_IGN) continue; /* * We matched a [OR] rule, skip the next rules * until we have one without [OR]. */ if (skip_or) { if (!(ce->flags & AM_COND_FLAG_OR)) skip_or = 0; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Skip %s, [OR] rule matched previously", ce->directive); am_diag_printf(r, "Skip, [OR] rule matched previously\n"); continue; } /* * look for a match on each value for this attribute, * stop on first match. */ for (j = 0; (j < session->size) && !match; j++) { const char *varname = NULL; am_envattr_conf_t *envattr_conf = NULL; /* * if MAP flag is set, check for remapped * attribute name with mellonSetEnv */ if (ce->flags & AM_COND_FLAG_MAP) { envattr_conf = (am_envattr_conf_t *)apr_hash_get(dir_cfg->envattr, am_cache_entry_get_string(session,&session->env[j].varname), APR_HASH_KEY_STRING); if (envattr_conf != NULL) varname = envattr_conf->name; } /* * Otherwise or if not found, use the attribute name * sent by the IdP. */ if (varname == NULL) varname = am_cache_entry_get_string(session, &session->env[j].varname); if (strcmp(varname, ce->varname) != 0) continue; value = am_cache_entry_get_string(session, &session->env[j].value); /* * Substiture backrefs if available */ if (ce->flags & AM_COND_FLAG_FSTR) ce = am_cond_substitue(r, ce, backrefs); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Evaluate %s vs \"%s\"", ce->directive, value); am_diag_printf(r, "evaluate value \"%s\" ", value); if (value == NULL) { match = 0; /* can not happen */ } else if (ce->flags & (AM_COND_FLAG_REG|AM_COND_FLAG_REF)) { int nsub = ce->regex->re_nsub + 1; ap_regmatch_t *regmatch; regmatch = (ap_regmatch_t *)apr_palloc(r->pool, nsub * sizeof(*regmatch)); match = !ap_regexec(ce->regex, value, nsub, regmatch, 0); if (match) backrefs = am_cond_backrefs(r, ce, value, regmatch); } else if (ce->flags & AM_COND_FLAG_REG) { match = !ap_regexec(ce->regex, value, 0, NULL, 0); } else if (ce->flags & (AM_COND_FLAG_SUB|AM_COND_FLAG_NC)) { match = (ap_strcasestr(ce->str, value) != NULL); } else if (ce->flags & AM_COND_FLAG_SUB) { match = (strstr(ce->str, value) != NULL); } else if (ce->flags & AM_COND_FLAG_NC) { match = !strcasecmp(ce->str, value); } else { match = !strcmp(ce->str, value); } am_diag_printf(r, "match=%s, ", match ? "yes" : "no"); } if (ce->flags & AM_COND_FLAG_NOT) { match = !match; am_diag_printf(r, "negating now match=%s ", match ? "yes" : "no"); } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "%s: %smatch", ce->directive, (match == 0) ? "no ": ""); /* * If no match, we stop here, except if it is an [OR] condition */ if (!match & !(ce->flags & AM_COND_FLAG_OR)) { ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "Client failed to match %s", ce->directive); am_diag_printf(r, "failed (no OR condition)" " returning HTTP_FORBIDDEN\n"); return HTTP_FORBIDDEN; } /* * Match on [OR] condition means we skip until a rule * without [OR], */ if (match && (ce->flags & AM_COND_FLAG_OR)) skip_or = 1; am_diag_printf(r, "\n"); } am_diag_printf(r, "%s succeeds\n", __func__); return OK; } /* This function sets default Cache-Control headers. * * Parameters: * request_rec *r The request we are handling. * * Returns: * Nothing. */ void am_set_cache_control_headers(request_rec *r) { /* Send Cache-Control header to ensure that: * - no proxy in the path caches content inside this location (private), * - user agent have to revalidate content on server (must-revalidate). * - content is always stale as the session login status can change at any * time synchronously (Redirect logout, session cookie is removed) or * asynchronously (SOAP logout, session cookie still exists but is * invalid), * * But never prohibit specifically any user agent to cache or store content * * Setting the headers in err_headers_out ensures that they will be * sent for all responses. */ apr_table_setn(r->err_headers_out, "Cache-Control", "private, max-age=0, must-revalidate"); } /* This function reads the post data for a request. * * The data is stored in a buffer allocated from the request pool. * After successful operation *data contains a pointer to the data and * *length contains the length of the data. * The data will always be null-terminated. * * Parameters: * request_rec *r The request we read the form data from. * char **data Pointer to where we will store the pointer * to the data we read. * apr_size_t *length Pointer to where we will store the length * of the data we read. Pass NULL if you don't * need to know the length of the data. * * Returns: * OK if we successfully read the POST data. * An error if we fail to read the data. */ int am_read_post_data(request_rec *r, char **data, apr_size_t *length) { apr_size_t bytes_read; apr_size_t bytes_left; apr_size_t len; long read_length; int rc; /* Prepare to receive data from the client. We request that apache * dechunks data if it is chunked. */ rc = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK); if (rc != OK) { return rc; } /* This function will send a 100 Continue response if the client is * waiting for that. If the client isn't going to send data, then this * function will return 0. */ if (!ap_should_client_block(r)) { len = 0; } else { len = r->remaining; } if (len >= 1024*1024) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Too large POST data payload (%lu bytes).", (unsigned long)len); return HTTP_BAD_REQUEST; } if (length != NULL) { *length = len; } *data = (char *)apr_palloc(r->pool, len + 1); if (*data == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to allocate memory for %lu bytes of POST data.", (unsigned long)len); return HTTP_INTERNAL_SERVER_ERROR; } /* Make sure that the data is null-terminated. */ (*data)[len] = '\0'; bytes_read = 0; bytes_left = len; while (bytes_left > 0) { /* Read data from the client. Returns 0 on EOF and -1 on * error, the number of bytes otherwise. */ read_length = ap_get_client_block(r, &(*data)[bytes_read], bytes_left); if (read_length == 0) { /* got the EOF */ (*data)[bytes_read] = '\0'; if (length != NULL) { *length = bytes_read; } break; } else if (read_length < 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to read POST data from client."); return HTTP_INTERNAL_SERVER_ERROR; } bytes_read += read_length; bytes_left -= read_length; } am_diag_printf(r, "POST data: %s\n", *data); return OK; } /* extract_query_parameter is a function which extracts the value of * a given parameter in a query string. The query string can be the * query_string parameter of a GET request, or it can be the data * passed to the web server in a POST request. * * Parameters: * apr_pool_t *pool The memory pool which the memory for * the value will be allocated from. * const char *query_string Either the query_string from a GET * request, or the data from a POST * request. * const char *name The name of the parameter to extract. * Note that the search for this name is * case sensitive. * * Returns: * The value of the parameter or NULL if we don't find the parameter. */ char *am_extract_query_parameter(apr_pool_t *pool, const char *query_string, const char *name) { const char *ip; const char *value_end; apr_size_t namelen; if (query_string == NULL) { return NULL; } ip = query_string; namelen = strlen(name); /* Find parameter. Searches for /[^&][&=$]/. * Moves ip to the first character after the name (either '&', '=' * or '\0'). */ for (;;) { /* First we find the name of the parameter. */ ip = strstr(ip, name); if (ip == NULL) { /* Parameter not found. */ return NULL; } /* Then we check what is before the parameter name. */ if (ip != query_string && ip[-1] != '&') { /* Name not preceded by [^&]. */ ip++; continue; } /* And last we check what follows the parameter name. */ if (ip[namelen] != '=' && ip[namelen] != '&' && ip[namelen] != '\0') { /* Name not followed by [&=$]. */ ip++; continue; } /* We have found the pattern. */ ip += namelen; break; } /* Now ip points to the first character after the name. If this * character is '&' or '\0', then this field doesn't have a value. * If this character is '=', then this field has a value. */ if (ip[0] == '=') { ip += 1; } /* The value is from ip to '&' or to the end of the string, whichever * comes first. */ value_end = strchr(ip, '&'); if (value_end != NULL) { /* '&' comes first. */ return apr_pstrndup(pool, ip, value_end - ip); } else { /* Value continues until the end of the string. */ return apr_pstrdup(pool, ip); } } /* Convert a hexadecimal digit to an integer. * * Parameters: * char c The digit we should convert. * * Returns: * The digit as an integer, or -1 if it isn't a hex digit. */ static int am_unhex_digit(char c) { if (c >= '0' && c <= '9') { return c - '0'; } else if (c >= 'a' && c <= 'f') { return c - 'a' + 0xa; } else if (c >= 'A' && c <= 'F') { return c - 'A' + 0xa; } else { return -1; } } /* This function urldecodes a string in-place. * * Parameters: * char *data The string to urldecode. * * Returns: * OK if successful or HTTP_BAD_REQUEST if any escape sequence decodes to a * null-byte ('\0'), or if an invalid escape sequence is found. */ int am_urldecode(char *data) { char *ip; char *op; int c1, c2; if (data == NULL) { return HTTP_BAD_REQUEST; } ip = data; op = data; while (*ip) { switch (*ip) { case '+': *op = ' '; ip++; op++; break; case '%': /* Decode the hex digits. Note that we need to check the * result of the first conversion before attempting the * second conversion -- otherwise we may read past the end * of the string. */ c1 = am_unhex_digit(ip[1]); if (c1 < 0) { return HTTP_BAD_REQUEST; } c2 = am_unhex_digit(ip[2]); if (c2 < 0) { return HTTP_BAD_REQUEST; } *op = (c1 << 4) | c2; if (*op == '\0') { /* null-byte. */ return HTTP_BAD_REQUEST; } ip += 3; op++; break; default: *op = *ip; ip++; op++; } } *op = '\0'; return OK; } /* This function urlencodes a string. It will escape all characters * except a-z, A-Z, 0-9, '_' and '.'. * * Parameters: * apr_pool_t *pool The pool we should allocate memory from. * const char *str The string we should urlencode. * * Returns: * The urlencoded string, or NULL if str == NULL. */ char *am_urlencode(apr_pool_t *pool, const char *str) { const char *ip; apr_size_t length; char *ret; char *op; int hi, low; /* Return NULL if str is NULL. */ if(str == NULL) { return NULL; } /* Find the length of the output string. */ length = 0; for(ip = str; *ip; ip++) { if(*ip >= 'a' && *ip <= 'z') { length++; } else if(*ip >= 'A' && *ip <= 'Z') { length++; } else if(*ip >= '0' && *ip <= '9') { length++; } else if(*ip == '_' || *ip == '.') { length++; } else { length += 3; } } /* Add space for null-terminator. */ length++; /* Allocate memory for string. */ ret = (char *)apr_palloc(pool, length); /* Encode string. */ for(ip = str, op = ret; *ip; ip++, op++) { if(*ip >= 'a' && *ip <= 'z') { *op = *ip; } else if(*ip >= 'A' && *ip <= 'Z') { *op = *ip; } else if(*ip >= '0' && *ip <= '9') { *op = *ip; } else if(*ip == '_' || *ip == '.') { *op = *ip; } else { *op = '%'; op++; hi = (*ip & 0xf0) >> 4; if(hi < 0xa) { *op = '0' + hi; } else { *op = 'A' + hi - 0xa; } op++; low = *ip & 0x0f; if(low < 0xa) { *op = '0' + low; } else { *op = 'A' + low - 0xa; } } } /* Make output string null-terminated. */ *op = '\0'; return ret; } /* * Check that a URL is safe for redirect. * * Parameters: * request_rec *r The request we are processing. * const char *url The URL we should check. * * Returns: * OK on success, HTTP_BAD_REQUEST otherwise. */ int am_check_url(request_rec *r, const char *url) { const char *i; for (i = url; *i; i++) { if (*i >= 0 && *i < ' ') { /* Deny all control-characters. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r, "Control character detected in URL."); return HTTP_BAD_REQUEST; } if (*i == '\\') { /* Reject backslash character, as it can be used to bypass * redirect URL validation. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r, "Backslash character detected in URL."); return HTTP_BAD_REQUEST; } } return OK; } /* This function generates a given number of (pseudo)random bytes. * The current implementation uses OpenSSL's RAND_*-functions. * * Parameters: * request_rec *r The request we are generating random bytes for. * The request is used for configuration and * error/warning reporting. * void *dest The address if the buffer we should fill with data. * apr_size_t count The number of random bytes to create. * * Returns: * OK on success, or HTTP_INTERNAL_SERVER on failure. */ int am_generate_random_bytes(request_rec *r, void *dest, apr_size_t count) { int rc; rc = RAND_bytes((unsigned char *)dest, (int)count); if(rc != 1) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error generating random data: %lu", ERR_get_error()); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* This function generates an id which is AM_ID_LENGTH characters long. * The id will consist of hexadecimal characters. * * Parameters: * request_rec *r The request we associate allocated memory with. * * Returns: * The session id, made up of AM_ID_LENGTH hexadecimal characters, * terminated by a null-byte. */ char *am_generate_id(request_rec *r) { int rc; char *ret; int rand_data_len; unsigned char *rand_data; int i; unsigned char b; int hi, low; ret = (char *)apr_palloc(r->pool, AM_ID_LENGTH + 1); /* We need to round the length of the random data _up_, in case the * length of the session id isn't even. */ rand_data_len = (AM_ID_LENGTH + 1) / 2; /* Fill the last rand_data_len bytes of the string with * random bytes. This allows us to overwrite from the beginning of * the string. */ rand_data = (unsigned char *)&ret[AM_ID_LENGTH - rand_data_len]; /* Generate random numbers. */ rc = am_generate_random_bytes(r, rand_data, rand_data_len); if(rc != OK) { return NULL; } /* Convert the random bytes to hexadecimal. Note that we will write * AM_ID_LENGTH+1 characters if we have a non-even length of the * session id. This is OK - we will simply overwrite the last character * with the null-terminator afterwards. */ for(i = 0; i < AM_ID_LENGTH; i += 2) { b = rand_data[i / 2]; hi = (b >> 4) & 0xf; low = b & 0xf; if(hi >= 0xa) { ret[i] = 'a' + hi - 0xa; } else { ret[i] = '0' + hi; } if(low >= 0xa) { ret[i+1] = 'a' + low - 0xa; } else { ret[i+1] = '0' + low; } } /* Add null-terminator- */ ret[AM_ID_LENGTH] = '\0'; return ret; } /* This returns the directroy part of a path, a la dirname(3) * * Parameters: * apr_pool_t p Pool to allocate memory from * const char *path Path to extract directory from * * Returns: * The directory part of path */ const char *am_filepath_dirname(apr_pool_t *p, const char *path) { char *cp; /* * Try Unix and then Windows style. Borrowed from * apr_match_glob(), it seems it cannot be made more * portable. */ if (((cp = strrchr(path, (int)'/')) == NULL) && ((cp = strrchr(path, (int)'\\')) == NULL)) return "."; return apr_pstrndup(p, path, cp - path); } /* * Allocate and initialize a am_file_data_t * * Parameters: * apr_pool_t *pool Allocation pool. * const char *path If non-NULL initialize file_data->path to copy of path * * Returns: * Newly allocated & initialized file_data_t */ am_file_data_t *am_file_data_new(apr_pool_t *pool, const char *path) { am_file_data_t *file_data = NULL; if ((file_data = apr_pcalloc(pool, sizeof(am_file_data_t))) == NULL) { return NULL; } file_data->pool = pool; file_data->rv = APR_EINIT; if (path) { file_data->path = apr_pstrdup(file_data->pool, path); } return file_data; } /* * Allocate a new am_file_data_t and copy * * Parameters: * apr_pool_t *pool Allocation pool. * am_file_data_t *src_file_data The src being copied. * * Returns: * Newly allocated & initialized from src_file_data */ am_file_data_t *am_file_data_copy(apr_pool_t *pool, am_file_data_t *src_file_data) { am_file_data_t *dst_file_data = NULL; if ((dst_file_data = am_file_data_new(pool, src_file_data->path)) == NULL) { return NULL; } dst_file_data->path = apr_pstrdup(pool, src_file_data->path); dst_file_data->stat_time = src_file_data->stat_time; dst_file_data->finfo = src_file_data->finfo; dst_file_data->contents = apr_pstrdup(pool, src_file_data->contents); dst_file_data->read_time = src_file_data->read_time; dst_file_data->rv = src_file_data->rv; dst_file_data->strerror = apr_pstrdup(pool, src_file_data->strerror); dst_file_data->generated = src_file_data->generated; return dst_file_data; } /* * Peform a stat on a file to get it's properties * * A stat is performed on the file. If there was an error the * result value is left in file_data->rv and an error description * string is formatted and left in file_data->strerror and function * returns the rv value. If the stat was successful the stat * information is left in file_data->finfo and APR_SUCCESS * set set as file_data->rv and returned as the function result. * * The file_data->stat_time indicates if and when the stat was * performed, a zero time value indicates the operation has not yet * been performed. * * Parameters: * am_file_data_t *file_data Struct containing file information * * Returns: * APR status code, same value as file_data->rv */ apr_status_t am_file_stat(am_file_data_t *file_data) { char buffer[512]; if (file_data == NULL) { return APR_EINVAL; } file_data->strerror = NULL; file_data->stat_time = apr_time_now(); file_data->rv = apr_stat(&file_data->finfo, file_data->path, APR_FINFO_SIZE, file_data->pool); if (file_data->rv != APR_SUCCESS) { file_data->strerror = apr_psprintf(file_data->pool, "apr_stat: Error opening \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); } return file_data->rv; } /* * Read file into dynamically allocated buffer * * First a stat is performed on the file. If there was an error the * result value is left in file_data->rv and an error description * string is formatted and left in file_data->strerror and function * returns the rv value. If the stat was successful the stat * information is left in file_data->finfo. * * A buffer is dynamically allocated and the contents of the file is * read into file_data->contents. If there was an error the result * value is left in file_data->rv and an error description string is * formatted and left in file_data->strerror and the function returns * the rv value. * * The file_data->stat_time and file_data->read_time indicate if and * when those operations were performed, a zero time value indicates * the operation has not yet been performed. * * Parameters: * am_file_data_t *file_data Struct containing file information * * Returns: * APR status code, same value as file_data->rv */ apr_status_t am_file_read(am_file_data_t *file_data) { char buffer[512]; apr_file_t *fd; apr_size_t nbytes; if (file_data == NULL) { return APR_EINVAL; } file_data->rv = APR_SUCCESS; file_data->strerror = NULL; am_file_stat(file_data); if (file_data->rv != APR_SUCCESS) { return file_data->rv; } if ((file_data->rv = apr_file_open(&fd, file_data->path, APR_READ, 0, file_data->pool)) != 0) { file_data->strerror = apr_psprintf(file_data->pool, "apr_file_open: Error opening \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); return file_data->rv; } file_data->read_time = apr_time_now(); nbytes = file_data->finfo.size; file_data->contents = (char *)apr_palloc(file_data->pool, nbytes + 1); file_data->rv = apr_file_read_full(fd, file_data->contents, nbytes, NULL); if (file_data->rv != 0) { file_data->strerror = apr_psprintf(file_data->pool, "apr_file_read_full: Error reading \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); (void)apr_file_close(fd); return file_data->rv; } file_data->contents[nbytes] = '\0'; (void)apr_file_close(fd); return file_data->rv; } /* * Purge outdated saved POST requests. * * Parameters: * request_rec *r The current request * * Returns: * OK on success, or HTTP_INTERNAL_SERVER on failure. */ int am_postdir_cleanup(request_rec *r) { am_mod_cfg_rec *mod_cfg; apr_dir_t *postdir; apr_status_t rv; char error_buffer[64]; apr_finfo_t afi; char *fname; int count; apr_time_t expire_before; mod_cfg = am_get_mod_cfg(r->server); /* The oldes file we should keep. Delete files that are older. */ expire_before = apr_time_now() - mod_cfg->post_ttl * APR_USEC_PER_SEC; /* * Open our POST directory or create it. */ rv = apr_dir_open(&postdir, mod_cfg->post_dir, r->pool); if (rv != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to open MellonPostDirectory \"%s\": %s", mod_cfg->post_dir, apr_strerror(rv, error_buffer, sizeof(error_buffer))); return HTTP_INTERNAL_SERVER_ERROR; } /* * Purge outdated items */ count = 0; do { rv = apr_dir_read(&afi, APR_FINFO_NAME|APR_FINFO_CTIME, postdir); if (rv != OK) break; /* Skip dot_files */ if (afi.name[0] == '.') continue; if (afi.ctime < expire_before) { fname = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, afi.name); (void)apr_file_remove(fname , r->pool); } else { count++; } } while (1 /* CONSTCOND */); (void)apr_dir_close(postdir); if (count >= mod_cfg->post_count) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Too many saved POST sessions. " "Increase MellonPostCount directive."); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* * HTML-encode a string * * Parameters: * request_rec *r The current request * const char *str The string to encode * * Returns: * The encoded string */ char *am_htmlencode(request_rec *r, const char *str) { const char *cp; char *output; apr_size_t outputlen; int i; outputlen = 0; for (cp = str; *cp; cp++) { switch (*cp) { case '&': outputlen += 5; break; case '"': outputlen += 6; break; default: outputlen += 1; break; } } i = 0; output = apr_palloc(r->pool, outputlen + 1); for (cp = str; *cp; cp++) { switch (*cp) { case '&': (void)strcpy(&output[i], "&"); i += 5; break; case '"': (void)strcpy(&output[i], """); i += 6; break; default: output[i] = *cp; i += 1; break; } } output[i] = '\0'; return output; } /* This function produces the endpoint URL * * Parameters: * request_rec *r The request we received. * * Returns: * the endpoint URL */ char *am_get_endpoint_url(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); return ap_construct_url(r->pool, cfg->endpoint_path, r); } /* * This function saves a POST request for later replay and updates * the return URL. * * Parameters: * request_rec *r The current request. * const char **relay_state The returl URL * * Returns: * OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise */ int am_save_post(request_rec *r, const char **relay_state) { am_mod_cfg_rec *mod_cfg; const char *content_type; const char *charset; const char *psf_id; char *psf_name; char *post_data; apr_size_t post_data_len; apr_size_t written; apr_file_t *psf; mod_cfg = am_get_mod_cfg(r->server); if (mod_cfg->post_dir == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "MellonPostReplay enabled but MellonPostDirectory not set " "-- cannot save post data"); return HTTP_INTERNAL_SERVER_ERROR; } if (am_postdir_cleanup(r) != OK) return HTTP_INTERNAL_SERVER_ERROR; /* Check Content-Type */ content_type = apr_table_get(r->headers_in, "Content-Type"); if (content_type == NULL) { content_type = "urlencoded"; charset = NULL; } else { if (am_has_header(r, content_type, "application/x-www-form-urlencoded")) { content_type = "urlencoded"; } else if (am_has_header(r, content_type, "multipart/form-data")) { content_type = "multipart"; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unknown POST Content-Type \"%s\"", content_type); return HTTP_INTERNAL_SERVER_ERROR; } charset = am_get_header_attr(r, content_type, NULL, "charset"); } if ((psf_id = am_generate_id(r)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot generate id"); return HTTP_INTERNAL_SERVER_ERROR; } psf_name = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, psf_id); if (apr_file_open(&psf, psf_name, APR_WRITE|APR_CREATE|APR_BINARY, APR_FPROT_UREAD|APR_FPROT_UWRITE, r->pool) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot create POST session file"); return HTTP_INTERNAL_SERVER_ERROR; } if (am_read_post_data(r, &post_data, &post_data_len) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot read POST data"); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } if (post_data_len > mod_cfg->post_size) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "POST data size %" APR_SIZE_T_FMT " exceeds maximum %" APR_SIZE_T_FMT ". " "Increase MellonPostSize directive.", post_data_len, mod_cfg->post_size); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } written = post_data_len; if ((apr_file_write(psf, post_data, &written) != OK) || (written != post_data_len)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot write to POST session file"); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } if (apr_file_close(psf) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot close POST session file"); return HTTP_INTERNAL_SERVER_ERROR; } if (charset != NULL) charset = apr_psprintf(r->pool, "&charset=%s", am_urlencode(r->pool, charset)); else charset = ""; *relay_state = apr_psprintf(r->pool, "%srepost?id=%s&ReturnTo=%s&enctype=%s%s", am_get_endpoint_url(r), psf_id, am_urlencode(r->pool, *relay_state), content_type, charset); return OK; } /* * This function replaces CRLF by LF in a string * * Parameters: * request_rec *r The current request * const char *str The string * * Returns: * Output string */ const char *am_strip_cr(request_rec *r, const char *str) { char *output; const char *cp; apr_size_t i; output = apr_palloc(r->pool, strlen(str) + 1); i = 0; for (cp = str; *cp; cp++) { if ((*cp == '\r') && (*(cp + 1) == '\n')) continue; output[i++] = *cp; } output[i++] = '\0'; return (const char *)output; } /* * This function replaces LF by CRLF in a string * * Parameters: * request_rec *r The current request * const char *str The string * * Returns: * Output string */ const char *am_add_cr(request_rec *r, const char *str) { char *output; const char *cp; apr_size_t xlen; apr_size_t i; xlen = 0; for (cp = str; *cp; cp++) if (*cp == '\n') xlen++; output = apr_palloc(r->pool, strlen(str) + xlen + 1); i = 0; for (cp = str; *cp; cp++) { if (*cp == '\n') output[i++] = '\r'; output[i++] = *cp; } output[i++] = '\0'; return (const char *)output; } /* * This function tokenize a string, just like strtok_r, except that * the separator is a string instead of a character set. * * Parameters: * const char *str The string to tokenize * const char *sep The separator string * char **last Pointer to state (char *) * * Returns: * OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise */ const char *am_xstrtok(request_rec *r, const char *str, const char *sep, char **last) { char *s; char *np; /* Resume */ if (str != NULL) s = apr_pstrdup(r->pool, str); else s = *last; /* End of string */ if (*s == '\0') return NULL; /* Next sep exists? */ if ((np = strstr(s, sep)) == NULL) { *last = s + strlen(s); } else { *last = np + strlen(sep); memset(np, 0, strlen(sep)); } return s; } /* This function strips leading spaces and tabs from a string * * Parameters: * const char **s Pointer to the string * */ void am_strip_blank(const char **s) { while ((**s == ' ') || (**s == '\t')) (*s)++; return; } /* This function extracts a MIME header from a MIME section * * Parameters: * request_rec *r The request * const char *m The MIME section * const char *h The header to extract (case insensitive) * * Returns: * The header value, or NULL on failure. */ const char *am_get_mime_header(request_rec *r, const char *m, const char *h) { const char *line; char *l1; const char *value; char *l2; for (line = am_xstrtok(r, m, "\n", &l1); line && *line; line = am_xstrtok(r, NULL, "\n", &l1)) { am_strip_blank(&line); if (((value = am_xstrtok(r, line, ":", &l2)) != NULL) && (strcasecmp(value, h) == 0)) { if ((value = am_xstrtok(r, NULL, ":", &l2)) != NULL) am_strip_blank(&value); return value; } } return NULL; } /* This function extracts an attribute from a header * * Parameters: * request_rec *r The request * const char *h The header * const char *v Optional header value to check (case insensitive) * const char *a Optional attribute to extract (case insensitive) * * Returns: * if i was provided, item value, or NULL on failure. * if i is NULL, the whole header, or NULL on failure. This is * useful for testing v. */ const char *am_get_header_attr(request_rec *r, const char *h, const char *v, const char *a) { const char *value; const char *attr; char *l1; const char *attr_value = NULL; /* Looking for * header-value; item_name="item_value"\n */ if ((value = am_xstrtok(r, h, ";", &l1)) == NULL) return NULL; am_strip_blank(&value); /* If a header value was provided, check it */ if ((v != NULL) && (strcasecmp(value, v) != 0)) return NULL; /* If no attribute name is provided, return everything */ if (a == NULL) return h; while ((attr = am_xstrtok(r, NULL, ";", &l1)) != NULL) { const char *attr_name = NULL; char *l2; am_strip_blank(&attr); attr_name = am_xstrtok(r, attr, "=", &l2); if ((attr_name != NULL) && (strcasecmp(attr_name, a) == 0)) { if ((attr_value = am_xstrtok(r, NULL, "=", &l2)) != NULL) am_strip_blank(&attr_value); break; } } /* Remove leading and trailing quotes */ if (attr_value != NULL) { apr_size_t len; len = strlen(attr_value); if ((len > 1) && (attr_value[len - 1] == '\"')) attr_value = apr_pstrndup(r->pool, attr_value, len - 1); if (attr_value[0] == '\"') attr_value++; } return attr_value; } /* This function checks for a header name/value existence * * Parameters: * request_rec *r The request * const char *h The header (case insensitive) * const char *v Optional header value to check (case insensitive) * * Returns: * 0 if header does not exists or does not has the value, 1 otherwise */ int am_has_header(request_rec *r, const char *h, const char *v) { return (am_get_header_attr(r, h, v, NULL) != NULL); } /* This function extracts the body from a MIME section * * Parameters: * request_rec *r The request * const char *mime The MIME section * * Returns: * The MIME section body, or NULL on failure. */ const char *am_get_mime_body(request_rec *r, const char *mime) { const char lflf[] = "\n\n"; const char *body; apr_size_t body_len; if ((body = strstr(mime, lflf)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No MIME body"); return NULL; } body += strlen(lflf); /* Strip tralling \n */ if ((body_len = strlen(body)) >= 1) { if (body[body_len - 1] == '\n') body = apr_pstrmemdup(r->pool, body, body_len - 1); } /* Turn back LF into CRLF */ return am_add_cr(r, body); } /* This function returns the URL for a given provider service (type + method) * * Parameters: * request_rec *r The request * LassoProfile *profile Login profile * char *endpoint_name Service and method as specified in metadata * e.g.: "SingleSignOnService HTTP-Redirect" * Returns: * The endpoint URL that must be freed by caller, or NULL on failure. */ char * am_get_service_url(request_rec *r, LassoProfile *profile, char *service_name) { LassoProvider *provider; gchar *url; provider = lasso_server_get_provider(profile->server, profile->remote_providerID); if (LASSO_IS_PROVIDER(provider) == FALSE) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Cannot find provider service %s, no provider.", service_name); return NULL; } url = lasso_provider_get_metadata_one(provider, service_name); if (url == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Cannot find provider service %s from metadata.", service_name); return NULL; } return url; } /*------------------------ Begin Token Parsing Code --------------------------*/ typedef enum { TOKEN_WHITESPACE = 1, TOKEN_SEMICOLON, TOKEN_COMMA, TOKEN_EQUAL, TOKEN_IDENTIFIER, TOKEN_DBL_QUOTE_STRING, } TokenType; typedef struct { TokenType type; /* The type of this token */ char *str; /* The string value of the token */ apr_size_t len; /* The number of characters in the token */ apr_size_t offset; /* The offset from the beginning of the string to the start of the token */ } Token; #ifdef DEBUG /* Return string representation of TokenType enumeration * * Parameters: * token_type A TokenType enumeration * Returns: String name of token_type */ static const char * token_type_str(TokenType token_type) { switch(token_type) { case TOKEN_WHITESPACE: return "WHITESPACE"; case TOKEN_SEMICOLON: return "SEMICOLON"; case TOKEN_COMMA: return "COMMA"; case TOKEN_EQUAL: return "EQUAL"; case TOKEN_IDENTIFIER: return "IDENTIFIER"; case TOKEN_DBL_QUOTE_STRING: return "DBL_QUOTE_STRING"; default: return "unknown"; } } static void dump_tokens(request_rec *r, apr_array_header_t *tokens) { apr_size_t i; for (i = 0; i < tokens->nelts; i++) { Token token = APR_ARRAY_IDX(tokens, i, Token); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "token[%2zd] %s \"%s\" offset=%lu len=%lu ", i, token_type_str(token.type), token.str, token.offset, token.len); } } #endif /* Initialize token and add to list of tokens * * Utility to assist tokenize function. * * A token object is created and added to the end of the list of * tokens. It is initialized with the type of token, a copy of the * string, it's length, and it's offset from the beginning of the * string where it was found. * * Tokens with special processing needs are also handled here. * * A double quoted string will: * * * Have it's delimiting quotes removed. * * Will unescape escaped characters. * * Parameters: * tokens Array of Token objects. * type The type of the token (e.g. TokenType). * str The string the token was parsed from, used to compute * the position of the token in the original string. * start The first character in the token. * end the last character in the token. */ static inline void push_token(apr_array_header_t *tokens, TokenType type, const char *str, const char *start, const char *end) { apr_size_t offset = start - str; Token *token = apr_array_push(tokens); if (type == TOKEN_DBL_QUOTE_STRING) { /* do not include quotes in token value */ start++; end--; } token->type = type; token->len = end - start; token->offset = offset; token->str = apr_pstrmemdup(tokens->pool, start, token->len); if (type == TOKEN_DBL_QUOTE_STRING) { /* * The original HTTP 1.1 spec was ambiguous with respect to * backslash quoting inside double quoted strings. This has since * been resolved in this errata: * * http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-16.html#rfc.section.3.2.3 * * Which states: * * Recipients that process the value of the quoted-string MUST * handle a quoted-pair as if it were replaced by the octet * following the backslash. * * Senders SHOULD NOT escape octets in quoted-strings that do not * require escaping (i.e., other than DQUOTE and the backslash * octet). */ char *p, *t; for (p = token->str; *p; p++) { if (p[0] == '\\' && p[1]) { /* * Found backslash with following character. * Move rest of string down 1 character. */ for (t = p; *t; t++) { t[0] = t[1]; } token->len--; } } } } /* Break a string into a series of tokens * * Given a string return an array of tokens. If the string cannot be * successfully parsed an error string is returned at the location * specified by the error parameter, if error is NULL then the parsing * was successful. If an error occured the returned array of tokens * will include all tokens parsed up until where the unrecognized * input occurred. The input str is never modified. * * Parameters: * pool memory allocation pool * str input string to be parsed. * ignore_whitespace if True whitespace tokens are not returned * error location where error string is returned * if NULL no error occurred * Returns: array of Token objects */ static apr_array_header_t * tokenize(apr_pool_t *pool, const char *str, bool ignore_whitespace, char **error) { apr_array_header_t *tokens = apr_array_make(pool, 10, sizeof(Token)); const char *p, *start; *error = NULL; p = start = str; while(*p) { if (apr_isspace(*p)) { /* whitespace */ p++; while(*p && apr_isspace(*p)) p++; if (!ignore_whitespace) { push_token(tokens, TOKEN_WHITESPACE, str, start, p); } start = p; } else if (apr_isalpha(*p)) { /* identifier: must begin with alpha then any alphanumeric or underscore */ p++; while(*p && (apr_isalnum(*p) || *p == '_')) p++; push_token(tokens, TOKEN_IDENTIFIER, str, start, p); start = p; } else if (*p == '"') { /* double quoted string */ p++; /* step over double quote */ while(*p) { if (*p == '\\') { /* backslash escape */ p++; /* step over backslash */ if (*p) { p++; /* step over escaped character */ } else { break; /* backslash at end of string, stop */ } } if (*p == '\"') break; /* terminating quote delimiter */ p++; /* keep scanning */ } if (*p != '\"') { *error = apr_psprintf(pool, "unterminated string beginning at " "position %" APR_SIZE_T_FMT " in \"%s\"", start-str, str); break; } p++; push_token(tokens, TOKEN_DBL_QUOTE_STRING, str, start, p); start = p; } else if (*p == '=') { /* equals */ p++; push_token(tokens, TOKEN_EQUAL, str, start, p); start = p; } else if (*p == ',') { /* comma */ p++; push_token(tokens, TOKEN_COMMA, str, start, p); start = p; } else if (*p == ';') { /* semicolon */ p++; push_token(tokens, TOKEN_SEMICOLON, str, start, p); start = p; } else { /* unrecognized token */ *error = apr_psprintf(pool, "unknown token at " "position %" APR_SIZE_T_FMT " in string \"%s\"", p-str, str); break; } } return tokens; } /* Test if the token is what we're looking for * * Given an index into the tokens array determine if the token type * matches. If the value parameter is non-NULL then the token's value * must also match. If the array index is beyond the last array item * false is returned. * * Parameters: * tokens Array of Token objects * index Index used to select the Token object from the Tokens array. * If the index is beyond the last array item False is returned. * type The token type which must match * value If non-NULL then the token string value must be equal to this. * Returns: True if the token matches, False otherwise. */ static bool is_token(apr_array_header_t *tokens, apr_size_t index, TokenType type, const char *value) { if (index >= tokens->nelts) { return false; } Token token = APR_ARRAY_IDX(tokens, index, Token); if (token.type != type) { return false; } if (value) { if (!g_str_equal(token.str, value)) { return false; } } return true; } /*------------------------- End Token Parsing Code ---------------------------*/ /* Return message describing position an error when parsing. * * When parsing we expect tokens to appear in a certain sequence. We * report the contents of the unexpected token and it's position in * the string. However if the parsing error is due to the fact we've * exhausted all tokens but are still expecting another token then our * error message indicates we reached the end of the string. * * Parameters: * tokens Array of Token objects. * index Index in tokens array where bad token was found */ static inline const char * parse_error_msg(apr_array_header_t *tokens, apr_size_t index) { if (index >= tokens->nelts) { return "end of string"; } return apr_psprintf(tokens->pool, "\"%s\" at position %" APR_SIZE_T_FMT, APR_ARRAY_IDX(tokens, index, Token).str, APR_ARRAY_IDX(tokens, index, Token).offset); } /* This function checks if an HTTP PAOS header is valid and * returns any service options which may have been specified. * * A PAOS header is composed of a mandatory PAOS version and service * values. A semicolon separates the version from the service values. * * Service values are delimited by semicolons, and options are * comma-delimited from the service value and each other. * * The PAOS version must be in the form ver="xxx" (note the version * string must be in double quotes). * * The ECP service must be specified, it MAY be followed by optional * comma seperated options, all values must be in double quotes. * * ECP Service * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" * * Recognized Options: * * Support for channel bindings * urn:oasis:names:tc:SAML:protocol:ext:channel-binding * * Support for Holder-of-Key subject confirmation * urn:oasis:names:tc:SAML:2.0:cm:holder-of-key * * Request for signed SAML request * urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp:2.0:WantAuthnRequestsSigned * * Request to delegate credentials to the service provider * urn:oasis:names:tc:SAML:2.0:conditions:delegation * * * Example PAOS HTTP header:: * * PAOS: ver="urn:liberty:paos:2003-08"; * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", * "urn:oasis:names:tc:SAML:protocol:ext:channel-binding", * "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key" * * Parameters: * request_rec *r The request * const char *header The PAOS header value * ECPServiceOptions *options_return * Pointer to location to receive options, * may be NULL. Bitmask of option flags. * * Returns: * true if the PAOS header is valid, false otherwise. If options is non-NULL * then the set of option flags is returned there. * */ bool am_parse_paos_header(request_rec *r, const char *header, ECPServiceOptions *options_return) { bool result = false; ECPServiceOptions options = 0; apr_array_header_t *tokens; apr_size_t i; char *error; AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "PAOS header: \"%s\"", header); tokens = tokenize(r->pool, header, true, &error); #ifdef DEBUG dump_tokens(r, tokens); #endif if (error) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "%s", error); goto cleanup; } /* Header must begin with "ver=xxx" where xxx is paos version */ if (!is_token(tokens, 0, TOKEN_IDENTIFIER, "ver") || !is_token(tokens, 1, TOKEN_EQUAL, NULL) || !is_token(tokens, 2, TOKEN_DBL_QUOTE_STRING, LASSO_PAOS_HREF)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected header to begin with ver=\"%s\", " "actual header=\"%s\"", LASSO_PAOS_HREF, header); goto cleanup; } /* Next is the service value, separated from the version by a semicolon */ if (!is_token(tokens, 3, TOKEN_SEMICOLON, NULL)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected semicolon after PAOS version " "but found %s in header=\"%s\"", parse_error_msg(tokens, 3), header); goto cleanup; } if (!is_token(tokens, 4, TOKEN_DBL_QUOTE_STRING, LASSO_ECP_HREF)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected service token to be \"%s\", " "but found %s in header=\"%s\"", LASSO_ECP_HREF, parse_error_msg(tokens, 4), header); goto cleanup; } /* After the service value there may be optional flags separated by commas */ if (tokens->nelts == 5) { /* no options */ result = true; goto cleanup; } /* More tokens after the service value, must be options, iterate over them */ for (i = 5; i < tokens->nelts; i++) { if (!is_token(tokens, i, TOKEN_COMMA, NULL)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected comma after PAOS service " "but found %s in header=\"%s\"", parse_error_msg(tokens, i), header); goto cleanup; } if (++i > tokens->nelts) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected option after comma " "in header=\"%s\"", header); goto cleanup; } Token token = APR_ARRAY_IDX(tokens, i, Token); if (token.type != TOKEN_DBL_QUOTE_STRING) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected quoted string after comma " "but found %s in header=\"%s\"", parse_error_msg(tokens, i), header); goto cleanup; } /* Have an option string, convert it to a bit flag */ const char *value = token.str; if (g_str_equal(value, LASSO_SAML_EXT_CHANNEL_BINDING)) { options |= ECP_SERVICE_OPTION_CHANNEL_BINDING; } else if (g_str_equal(value, LASSO_SAML2_CONFIRMATION_METHOD_HOLDER_OF_KEY)) { options |= ECP_SERVICE_OPTION_HOLDER_OF_KEY; } else if (g_str_equal(value, LASSO_SAML2_ECP_PROFILE_WANT_AUTHN_SIGNED)) { options |= ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED; } else if (g_str_equal(value, LASSO_SAML2_CONDITIONS_DELEGATION)) { options |= ECP_SERVICE_OPTION_DELEGATION; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Unknown PAOS service option = \"%s\"", value); goto cleanup; } } result = true; cleanup: if (options_return) { *options_return = options; } return result; } /* This function checks if Accept header has a media type * * Given an Accept header value like this: * * "text/html,application/xhtml+xml,application/xml;q=0.9" * * Parse the string and find name of each media type, ignore any parameters * bound to the name. Test to see if the name matches the input media_type. * * Parameters: * request_rec *r The request * const char *header The header value * const char *media_type media type header value to check (case insensitive) * * Returns: * true if media type is in header, false otherwise */ bool am_header_has_media_type(request_rec *r, const char *header, const char *media_type) { bool result = false; char **comma_tokens = NULL; char **media_ranges = NULL; char *media_range = NULL; if (header == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid Accept header, NULL"); goto cleanup; } /* * Split the header into a list of media_range tokens separated by * a comma and iterate over the list. */ comma_tokens = g_strsplit(header, ",", 0); for (media_ranges = comma_tokens, media_range = *media_ranges; media_range; media_range = *(++media_ranges)) { char **semicolon_tokens = NULL; char *name = NULL; /* * Split the media_range into a name and parameters, each * separated by a semicolon. The first element in the list is * the media_type name, subsequent params are optional and ignored. */ media_range = g_strstrip(media_range); semicolon_tokens = g_strsplit(media_range, ";", 0); /* * Does the media_type match our required media_type? * If so clean up and return success. */ name = g_strstrip(semicolon_tokens[0]); if (name && g_str_equal(name, media_type)) { result = true; g_strfreev(semicolon_tokens); goto cleanup; } g_strfreev(semicolon_tokens); } cleanup: g_strfreev(comma_tokens); return result; } /* * Lookup a config string in a specific language. If lang is NULL and * the config string had been defined without a language qualifier * return the unqualified value. If not found NULL is returned. */ const char *am_get_config_langstring(apr_hash_t *h, const char *lang) { char *string; if (lang == NULL) { lang = ""; } string = (char *)apr_hash_get(h, lang, APR_HASH_KEY_STRING); return string; } /* * Get the value of boolean query parameter. * * Parameters: * request_rec *r The request * const char *name The name of the query parameter * int *return_value The address of the variable to receive * the boolean value * int default_value The value returned if parameter is absent or * in event of an error * * Returns: * OK on success, HTTP error otherwise * * Looks for the named parameter in the query parameters, if found * parses the value which must be one of: * * * true * * false * * If value cannot be parsed HTTP_BAD_REQUEST is returned. * * If not found, or if there is an error, the returned value is set to * default_value. */ int am_get_boolean_query_parameter(request_rec *r, const char *name, int *return_value, int default_value) { char *value_str; int ret = OK; *return_value = default_value; value_str = am_extract_query_parameter(r->pool, r->args, name); if (value_str != NULL) { ret = am_urldecode(value_str); if (ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding \"%s\" boolean query parameter, " "value=\"%s\"", name, value_str); return ret; } if(!strcmp(value_str, "true")) { *return_value = TRUE; } else if(!strcmp(value_str, "false")) { *return_value = FALSE; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid value for \"%s\" boolean query parameter, " "value=\"%s\"", name, value_str); ret = HTTP_BAD_REQUEST; } } return ret; } /* * Get the URL of the AssertionConsumerServer having specific protocol * binding. * * Parameters: * LassoProvider *provider The provider whose endpoints will be scanned. * const char *binding The required binding short name. * * Returns: * The endpoint URL or NULL if not found. Must be freed with g_free(). * * Lasso does not provide a public API to select a provider endpoint * by binding. The best we can do is iterate over a list of endpoint * descriptors and select a matching descriptor. * * Lasso does not document the format of these descriptor names but * essentially a descriptor is a space separated concatenation of the * endpoint properties. For SAML2 one can assume it is the endpoint * type, optionally followed by the protocol binding name, optionally * followd by the index (if the endpoint type is indexed). If the * endpoint is a response location then "ResponseLocation" will be * appended as the final token. For example here is a list of * descriptors returned for a service provider (note they are * unordered). * * "AssertionConsumerService HTTP-POST 0" * "AuthnRequestsSigned" * "AssertionConsumerService PAOS 2" * "SingleLogoutService HTTP-Redirect" * "SingleLogoutService SOAP" * "AssertionConsumerService HTTP-Artifact 1" * "NameIDFormat" * "SingleLogoutService HTTP-POST ResponseLocation" * * The possible binding names are: * * "SOAP" * "HTTP-Redirect" * "HTTP-POST" * "HTTP-Artifact" * "PAOS" * "URI" * * We know the AssertionConsumerService is indexed. If there is more * than one endpoint with the required binding we select the one with * the lowest index assuming it is preferred. */ char *am_get_assertion_consumer_service_by_binding(LassoProvider *provider, const char *binding) { GList *descriptors; char *url; char *selected_descriptor; char *descriptor; char **tokens; guint n_tokens; GList *i; char *endptr; long descriptor_index, min_index; url = NULL; selected_descriptor = NULL; min_index = LONG_MAX; /* The descriptor list is unordered */ descriptors = lasso_provider_get_metadata_keys_for_role(provider, LASSO_PROVIDER_ROLE_SP); for (i = g_list_first(descriptors), tokens=NULL; i; i = g_list_next(i), g_strfreev(tokens)) { descriptor = i->data; descriptor_index = LONG_MAX; /* * Split the descriptor into tokens, only consider descriptors * which have at least 3 tokens and whose first token is * AssertionConsumerService */ tokens = g_strsplit(descriptor, " ", 0); n_tokens = g_strv_length(tokens); if (n_tokens < 3) continue; if (!g_str_equal(tokens[0], "AssertionConsumerService")) continue; if (!g_str_equal(tokens[1], binding)) continue; descriptor_index = strtol(tokens[2], &endptr, 10); if (tokens[2] == endptr) continue; /* could not parse int */ if (descriptor_index < min_index) { selected_descriptor = descriptor; min_index = descriptor_index; } } if (selected_descriptor) { url = lasso_provider_get_metadata_one_for_role(provider, LASSO_PROVIDER_ROLE_SP, selected_descriptor); } lasso_release_list_of_strings(descriptors); return url; } #ifdef HAVE_ECP /* String representation of ECPServiceOptions bitmask * * ECPServiceOptions is a bitmask of flags. Return a comma separated string * of all the flags. If any bit in the bitmask is unaccounted for an * extra string will be appended of the form "(unknown bits = x)". * * Parameters: * pool memory allocation pool * options bitmask of PAOS options */ char *am_ecp_service_options_str(apr_pool_t *pool, ECPServiceOptions options) { apr_array_header_t *names = apr_array_make(pool, 4, sizeof(const char *)); if (options & ECP_SERVICE_OPTION_CHANNEL_BINDING) { APR_ARRAY_PUSH(names, const char *) = "channel-binding"; options &= ~ECP_SERVICE_OPTION_CHANNEL_BINDING; } if (options & ECP_SERVICE_OPTION_HOLDER_OF_KEY) { APR_ARRAY_PUSH(names, const char *) = "holder-of-key"; options &= ~ECP_SERVICE_OPTION_HOLDER_OF_KEY; } if (options & ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED) { APR_ARRAY_PUSH(names, const char *) = "want-authn-signed"; options &= ~ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED; } if (options & ECP_SERVICE_OPTION_DELEGATION) { APR_ARRAY_PUSH(names, const char *) = "delegation"; options &= ~ECP_SERVICE_OPTION_DELEGATION; } if (options) { APR_ARRAY_PUSH(names, const char *) = apr_psprintf(pool, "(unknown bits = %#x)", options); } return apr_array_pstrcat(pool, names, ','); } /* Determine if request is compatible with PAOS, decode headers * * To indicate support for the ECP profile, and the PAOS binding, the * request MUST include the following HTTP header fields: * * 1. An Accept header indicating acceptance of the MIME type * "application/vnd.paos+xml" * * 2. A PAOS header specifying the PAOS version with a value, at minimum, of * "urn:liberty:paos:2003-08" and a supported service value of * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp". The service value MAY * contain option values. * * This function validates the Accept header the the PAOS header, if * all condidtions are met it returns true, false otherwise. If the * validation succeeds any ECP options specified along with the * ECP service are parsed and stored in req_cfg->ecp_service_options * * Any error discovered during processing are returned in the * error_code parameter, zero indicates success. This function never * returns true if an error occurred. * * Parameters: * request_rec *r The current request. * int * error_code Return error code here * */ bool am_is_paos_request(request_rec *r, int *error_code) { const char *accept_header = NULL; const char *paos_header = NULL; bool have_paos_media_type = false; bool valid_paos_header = false; bool is_paos = false; ECPServiceOptions ecp_service_options = 0; *error_code = 0; accept_header = apr_table_get(r->headers_in, "Accept"); paos_header = apr_table_get(r->headers_in, "PAOS"); if (accept_header) { if (am_header_has_media_type(r, accept_header, MEDIA_TYPE_PAOS)) { have_paos_media_type = true; } } if (paos_header) { if (am_parse_paos_header(r, paos_header, &ecp_service_options)) { valid_paos_header = true; } else { if (*error_code == 0) *error_code = AM_ERROR_INVALID_PAOS_HEADER; } } if (have_paos_media_type) { if (valid_paos_header) { is_paos = true; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "request supplied PAOS media type in Accept header " "but omitted valid PAOS header"); if (*error_code == 0) *error_code = AM_ERROR_MISSING_PAOS_HEADER; } } else { if (valid_paos_header) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "request supplied valid PAOS header " "but omitted PAOS media type in Accept header"); if (*error_code == 0) *error_code = AM_ERROR_MISSING_PAOS_MEDIA_TYPE; } } AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "have_paos_media_type=%s valid_paos_header=%s is_paos=%s " "error_code=%d ecp options=[%s]", have_paos_media_type ? "True" : "False", valid_paos_header ? "True" : "False", is_paos ? "True" : "False", *error_code, am_ecp_service_options_str(r->pool, ecp_service_options)); if (is_paos) { am_req_cfg_rec *req_cfg; req_cfg = am_get_req_cfg(r); req_cfg->ecp_service_options = ecp_service_options; } return is_paos; } #endif /* HAVE_ECP */ char * am_saml_response_status_str(request_rec *r, LassoNode *node) { LassoSamlp2StatusResponse *response = (LassoSamlp2StatusResponse*)node; LassoSamlp2Status *status = NULL; const char *status_code1 = NULL; const char *status_code2 = NULL; if (!LASSO_IS_SAMLP2_STATUS_RESPONSE(response)) { return apr_psprintf(r->pool, "error, expected LassoSamlp2StatusResponse " "but got %s", lasso_node_get_name((LassoNode*)response)); } status = response->Status; if (status == NULL || !LASSO_IS_SAMLP2_STATUS(status) || status->StatusCode == NULL || status->StatusCode->Value == NULL) { return apr_psprintf(r->pool, "Status missing"); } status_code1 = status->StatusCode->Value; if (status->StatusCode->StatusCode) { status_code2 = status->StatusCode->StatusCode->Value; } return apr_psprintf(r->pool, "StatusCode1=\"%s\", StatusCode2=\"%s\", " "StatusMessage=\"%s\"", status_code1, status_code2, status->StatusMessage); }