Replay POST requets after been sent to the IdP

git-svn-id: https://modmellon.googlecode.com/svn/trunk@67 a716ebb1-153a-0410-b759-cfb97c6a1b53
This commit is contained in:
manu@netbsd.org 2009-11-09 13:46:28 +00:00
parent 727a602582
commit 24d4e22219
7 changed files with 614 additions and 65 deletions

5
NEWS
View File

@ -1,3 +1,8 @@
Version 0.2.5
---------------------------------------------------------------------------
* Replay POST requests after been sent to the IdP
Version 0.2.4
---------------------------------------------------------------------------

19
README
View File

@ -104,6 +104,25 @@ MellonCacheSize 100
# Default: MellonLockFile "/tmp/mellonLock"
MellonLockFile "/tmp/mellonLock"
# MellonPostDir is the full path of a directory where POST requests are
# saved during authentication. This directory must be owned by the Apache
# user and be mode 700. We will attempt to create it if it does not exist.
# Default: MellonPostDir "/var/tmp/mellonpost"
MellonPostDir "/var/tmp/mellonpost"
# MellonPostTTL is the delay in seconds before a saved POST request can
# be flushed.
# Default: MellonPostTTL 900 (15 mn)
MellonPostTTL 900
# MellonPostSize is the maximum size for saved POST requests
# Default: MellonPostSize 1073741824 (1 MB)
MellonPostSize 1073741824
# MellonPostCount is the maxmimum amount of saved POST requests
# Default: MellonPostCount 100
MellonPostCount 100
###########################################################################
# End of global configuration for mod_auth_mellon.
###########################################################################

View File

@ -89,6 +89,10 @@
typedef struct am_mod_cfg_rec {
int cache_size;
const char *lock_file;
const char *post_dir;
apr_time_t post_ttl;
int post_count;
apr_size_t post_size;
/* These variables can't be allowed to change after the session store
* has been initialized. Therefore we copy them before initializing
@ -257,11 +261,15 @@ char *am_urlencode(apr_pool_t *pool, const char *str);
int am_urldecode(char *data);
char *am_generate_session_id(request_rec *r);
char *am_getfile(apr_pool_t *conf, server_rec *s, const char *file);
char *am_get_endpoint_url(request_rec *r);
int am_postdir_cleanup(request_rec *s);
char *am_htmlencode(request_rec *r, const char *str);
int am_save_post(request_rec *r, const char **relay_state);
int am_auth_mellon_user(request_rec *r);
int am_check_uid(request_rec *r);
int am_handle_metadata(request_rec *r);
int am_handler(request_rec *r);
int am_httpclient_get(request_rec *r, const char *uri,

View File

@ -56,6 +56,25 @@ static const int default_dump_saml_response = 0;
*/
static const char *default_login_path = "/";
/* This is the directory for storing saved POST sessions
* the MellonPostDirectory configuration directive if you change this.
*/
static const char *post_dir = "/var/tmp/mellonpost";
/* saved POST session time to live
* the MellonPostTTL configuration directive if you change this.
*/
static const apr_time_t post_ttl = 15 * 60;
/* saved POST session maximum size
* the MellonPostSize configuration directive if you change this.
*/
static const apr_size_t post_size = 1024 * 1024 * 1024;
/* maximum saved POST sessions
* the MellonPostCount configuration directive if you change this.
*/
static const int post_count = 100;
/* This function handles configuration directives which set a file
* slot in the module configuration. If lasso is recent enough, it
@ -454,6 +473,38 @@ const command_rec auth_mellon_commands[] = {
"The lock file for session synchronization."
" Default value is \"/tmp/mellonLock\"."
),
AP_INIT_TAKE1(
"MellonPostDirectory",
am_set_module_config_string_slot,
(void *)APR_OFFSETOF(am_mod_cfg_rec, post_dir),
RSRC_CONF,
"The directory for saving POST requests."
" Default value is \"/var/tmp/mellonpost\"."
),
AP_INIT_TAKE1(
"MellonPostTTL",
am_set_module_config_int_slot,
(void *)APR_OFFSETOF(am_mod_cfg_rec, post_ttl),
RSRC_CONF,
"The time to live for saved POST requests in seconds."
" Default value is 15 mn."
),
AP_INIT_TAKE1(
"MellonPostCount",
am_set_module_config_int_slot,
(void *)APR_OFFSETOF(am_mod_cfg_rec, post_count),
RSRC_CONF,
"The maximum saved POST sessions at once."
" Default value is 100."
),
AP_INIT_TAKE1(
"MellonPostSize",
am_set_module_config_int_slot,
(void *)APR_OFFSETOF(am_mod_cfg_rec, post_size),
RSRC_CONF,
"The maximum size of a saved POST, in bytes."
" Default value is 1 MB."
),
/* Per-location configuration directives. */
@ -870,6 +921,10 @@ void *auth_mellon_server_config(apr_pool_t *p, server_rec *s)
mod->cache_size = 100; /* ought to be enough for everybody */
mod->lock_file = "/tmp/mellonLock";
mod->post_dir = post_dir;
mod->post_ttl = post_ttl;
mod->post_count = post_count;
mod->post_size = post_size;
mod->init_cache_size = 0;
mod->init_lock_file = NULL;

View File

@ -30,43 +30,6 @@
#endif /* HAVE_lasso_server_new_from_buffers */
/* This function produces the endpoint URL
*
* Parameters:
* request_rec *r The request we received.
*
* Returns:
* the endpoint URL
*/
static char *am_get_endpoint_url(request_rec *r)
{
static APR_OPTIONAL_FN_TYPE(ssl_is_https) *am_is_https = NULL;
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
apr_pool_t *p = r->pool;
server_rec *s = r->server;
apr_port_t default_port;
char *port;
char *scheme;
am_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https);
if (am_is_https && am_is_https(r->connection)) {
scheme = "https://";
default_port = DEFAULT_HTTPS_PORT;
} else {
scheme = "http://";
default_port = DEFAULT_HTTP_PORT;
}
if (s->addrs->host_port != default_port)
port = apr_psprintf(p, ":%d", s->addrs->host_port);
else
port = "";
return apr_psprintf(p, "%s%s%s%s", scheme,
s->server_hostname,
port, cfg->endpoint_path);
}
#ifdef HAVE_lasso_server_new_from_buffers
/* This function generates optional metadata for a given element
@ -1730,6 +1693,129 @@ static int am_handle_artifact_reply(request_rec *r)
return am_handle_reply_common(r, login, relay_state, "");
}
/* This function handles responses to repost request
*
* Parameters:
* request_rec *r The request we received.
*
* Returns:
* OK on success, or an error on failure.
*/
static int am_handle_repost(request_rec *r)
{
am_mod_cfg_rec *mod_cfg;
const char *query;
char *cp;
char *psf_id;
char *psf_filename;
char *post_data;
char *post_form;
char *output;
char *last;
char *return_url;
mod_cfg = am_get_mod_cfg(r->server);
query = r->parsed_uri.query;
psf_id = am_extract_query_parameter(r->pool, query, "id");
if (psf_id == NULL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Bad repost query: missing id");
return HTTP_BAD_REQUEST;
}
/* Check that Id is sane */
for (cp = psf_id; *cp; cp++) {
if (!apr_isalnum(*cp)) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Bad repost query: invalid id \"%s\"", psf_id);
return HTTP_BAD_REQUEST;
}
}
return_url = am_extract_query_parameter(r->pool, query, "ReturnTo");
if (return_url == NULL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Invalid or missing query ReturnTo parameter.");
return HTTP_BAD_REQUEST;
}
if (am_urldecode(return_url) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: return");
return HTTP_BAD_REQUEST;
}
psf_filename = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, psf_id);
if ((post_data = am_getfile(r->pool, r->server, psf_filename)) == NULL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Bad repost query: cannot find \"%s\"", psf_filename);
return HTTP_BAD_REQUEST;
}
post_form = "";
for (cp = apr_strtok(post_data, "&", &last); cp;
cp = apr_strtok(NULL, "&", &last)) {
char *item;
char *last2;
char *name;
char *value;
char *input_item;
item = apr_pstrdup(r->pool, cp);
name = apr_strtok(item, "=", &last2);
value = apr_strtok(NULL, "=", &last2);
if (name == NULL)
continue;
if (value == NULL)
value = (char *)"";
if (am_urldecode(name) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"urldecode(\"%s\") failed", name);
return HTTP_INTERNAL_SERVER_ERROR;
}
if (am_urldecode(value) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"urldecode(\"%s\") failed", value);
return HTTP_INTERNAL_SERVER_ERROR;
}
input_item = apr_psprintf(r->pool,
" <input type=\"hidden\" name=\"%s\" value=\"%s\">\n",
am_htmlencode(r, name), am_htmlencode(r, value));
post_form = apr_pstrcat(r->pool, post_form, input_item, NULL);
}
r->content_type = "text/html";
output = apr_psprintf(r->pool,
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"
"<html>\n"
" <head>\n"
" <title>SAML rePOST request</title>\n"
" </head>\n"
" <body onload=\"document.getElementById('form').submit();\">\n"
" <noscript>\n"
" Your browser does not support Javascript, \n"
" you must click the button below to proceed.\n"
" </noscript>\n"
" <form id=\"form\" method=\"POST\" action=\"%s\">\n%s"
" <noscript>\n"
" <input type=\"submit\">\n"
" </noscript>\n"
" </form>\n"
" </body>\n"
"</html>\n",
return_url, post_form);
ap_rputs(output, r);
return OK;
}
/* This function handles responses to metadata request
*
@ -1739,14 +1825,46 @@ static int am_handle_artifact_reply(request_rec *r)
* Returns:
* OK on success, or an error on failure.
*/
int am_handle_metadata(request_rec *r)
static int am_handle_metadata(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
const char *endpoint;
#ifdef HAVE_lasso_server_new_from_buffers
LassoServer *server;
const char *data;
server = am_get_lasso_server(r);
if(server == NULL)
return HTTP_INTERNAL_SERVER_ERROR;
data = cfg->sp_metadata_file;
if (data == NULL)
return HTTP_INTERNAL_SERVER_ERROR;
r->content_type = "application/samlmetadata+xml";
ap_rputs(data, r);
return OK;
#else /* ! HAVE_lasso_server_new_from_buffers */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"metadata publishing require lasso 2.2.2 or higher");
return HTTP_NOT_FOUND;
#endif
}
/* This function handles responses to request on our endpoint
*
* Parameters:
* request_rec *r The request we received.
*
* Returns:
* OK on success, or an error on failure.
*/
int am_handler(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
const char *endpoint;
/* Check if this is a request for one of our endpoints. We check if
* the uri starts with the path set with the MellonEndpointPath
@ -1755,11 +1873,6 @@ int am_handle_metadata(request_rec *r)
if(strstr(r->uri, cfg->endpoint_path) != r->uri)
return DECLINED;
endpoint = &r->uri[strlen(cfg->endpoint_path)];
if (strcmp(endpoint, "metadata") != 0)
return DECLINED;
#ifdef HAVE_lasso_server_new_from_buffers
/* Make sure that this is a GET request. */
if(r->method_number != M_GET) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
@ -1779,27 +1892,17 @@ int am_handle_metadata(request_rec *r)
return DECLINED;
}
server = am_get_lasso_server(r);
if(server == NULL)
return HTTP_INTERNAL_SERVER_ERROR;
data = cfg->sp_metadata_file;
if (data == NULL)
return HTTP_INTERNAL_SERVER_ERROR;
r->content_type = "application/samlmetadata+xml";
ap_rputs(data, r);
return OK;
#else /* ! HAVE_lasso_server_new_from_buffers */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"metadata publishing require lasso 2.2.2 or higher");
return HTTP_NOT_FOUND;
#endif
endpoint = &r->uri[strlen(cfg->endpoint_path)];
if (strcmp(endpoint, "metadata") == 0)
return am_handle_metadata(r);
else if (strcmp(endpoint, "repost") == 0)
return am_handle_repost(r);
else
return DECLINED;
}
static int am_auth_new_ticket(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
@ -1812,6 +1915,12 @@ static int am_auth_new_ticket(request_rec *r)
relay_state = am_reconstruct_url(r);
/* If this is a POST request, attempt to save it */
if (r->method_number == M_POST) {
if (am_save_post(r, &relay_state) != OK)
return HTTP_INTERNAL_SERVER_ERROR;
}
/* Check if IdP discovery is in use and no IdP was selected yet */
if ((cfg->discovery_url != NULL) &&
(am_builtin_discovery_timeout(r) == -1) && /* no built-in discovery */
@ -1965,6 +2074,8 @@ static int am_endpoint_handler(request_rec *r)
return am_auth_new_ticket(r);
} else if(!strcmp(endpoint, "metadata")) {
return OK;
} else if(!strcmp(endpoint, "repost")) {
return OK;
} else if(!strcmp(endpoint, "logout")
|| !strcmp(endpoint, "logoutRequest")) {
/* logoutRequest is included for backwards-compatibility

View File

@ -587,3 +587,355 @@ char *am_getfile(apr_pool_t *conf, server_rec *s, const char *file)
return data;
}
/*
* Create a directory for saved POST sessions, check for proper permissions
*
* Parameters:
* request_rec *r The current request
*
* Returns:
* OK on success, or HTTP_INTERNAL_SERVER on failure.
*/
static int am_postdir_mkdir(request_rec *r)
{
apr_int32_t wanted;
apr_finfo_t afi;
apr_status_t rv;
char buffer[512];
am_mod_cfg_rec *mod_cfg;
apr_fileperms_t mode;
apr_uid_t user;
apr_uid_t group;
apr_fileperms_t prot;
mod_cfg = am_get_mod_cfg(r->server);
mode = APR_FPROT_UREAD|APR_FPROT_UWRITE|APR_FPROT_UEXECUTE;
if ((rv = apr_dir_make_recursive(mod_cfg->post_dir, mode, r->pool)) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"cannot create POST directory \"%s\": %s",
mod_cfg->post_dir,
apr_strerror(rv, buffer, sizeof(buffer)));
return HTTP_INTERNAL_SERVER_ERROR;
}
/*
* The directory may have already existed. Check we really own it
*/
wanted = APR_FINFO_USER|APR_FINFO_UPROT|APR_FINFO_GPROT|APR_FINFO_WPROT;
if (apr_stat(&afi, mod_cfg->post_dir, wanted, r->pool) == OK) {
if (apr_uid_current(&user, &group, r->pool) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"apr_uid_current failed");
return HTTP_INTERNAL_SERVER_ERROR;
}
if (afi.user != user) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"POST directory \"%s\" must be owned by the same "
"user as the web server is running as.",
mod_cfg->post_dir);
return HTTP_INTERNAL_SERVER_ERROR;
}
prot = APR_FPROT_UREAD|APR_FPROT_UWRITE|APR_FPROT_UEXECUTE;
if (afi.protection != prot) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Premissions on POST directory \"%s\" must be 0700.",
mod_cfg->post_dir);
return HTTP_INTERNAL_SERVER_ERROR;
}
}
return OK;
}
/*
* Purge outdated saved POST requests. If the MellonPostDir directory
* does not exist, create it first.
*
* 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;
apr_finfo_t afi;
char *fname;
int count;
mod_cfg = am_get_mod_cfg(r->server);
/*
* Open our POST directory or create it.
*/
if (apr_dir_open(&postdir, mod_cfg->post_dir, r->pool) != OK)
return am_postdir_mkdir(r);
/*
* 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 + mod_cfg->post_ttl > apr_time_sec(apr_time_now())) {
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) {
ap_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], "&amp;");
i += 5;
break;
case '"':
(void)strcpy(&output[i], "&quot;");
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)
{
static APR_OPTIONAL_FN_TYPE(ssl_is_https) *am_is_https = NULL;
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
apr_pool_t *p = r->pool;
server_rec *s = r->server;
apr_port_t default_port;
char *port;
char *scheme;
am_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https);
if (am_is_https && am_is_https(r->connection)) {
scheme = "https://";
default_port = DEFAULT_HTTPS_PORT;
} else {
scheme = "http://";
default_port = DEFAULT_HTTP_PORT;
}
if (s->addrs->host_port != default_port)
port = apr_psprintf(p, ":%d", s->addrs->host_port);
else
port = "";
return apr_psprintf(p, "%s%s%s%s", scheme,
s->server_hostname,
port, cfg->endpoint_path);
}
/*
* The two functions below extract an HTTP header from the request.
*
* Parameters:
* request_rec *r The current request.
*
* Returns:
* 1 if multipart/form-data, 0 otherwise.
*/
struct am_get_header_state {
request_rec *req;
const char *header;
const char *value;
};
static int am_get_header_callback(void *s, const char *key, const char *val)
{
struct am_get_header_state *state;
state = (struct am_get_header_state *)s;
if (strcmp(key, state->header) != 0)
return 1;
state->value = val;
return 0;
}
static const char *am_get_header(request_rec *r, const char *header)
{
struct am_get_header_state state;
state.req = r;
state.header = header;
state.value = NULL;
(void)apr_table_do(am_get_header_callback, &state, r->headers_in, NULL);
return state.value;
}
/*
* 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 *psf_id;
char *psf_name;
char *post_data;
apr_size_t post_data_len;
apr_size_t written;
apr_file_t *psf;
if (am_postdir_cleanup(r) != OK)
return HTTP_INTERNAL_SERVER_ERROR;
/* Check Content-Type */
content_type = am_get_header(r, "Content-Type");
if ((content_type != NULL) &&
(strcmp(content_type, "application/x-www-form-urlencoded") != 0)) {
/*
* This is probably "multipart/form-data; boundary=XXXXXXXXXX"
* The POST request then contains MIME data. We are not yet
* able to manage that, so issue an error 500.
*/
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Unsupported POST Content-Type \"%s\"", content_type);
return HTTP_INTERNAL_SERVER_ERROR;
}
mod_cfg = am_get_mod_cfg(r->server);
if ((psf_id = am_generate_session_id(r)) == NULL) {
ap_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) {
ap_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) {
ap_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) {
ap_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)) {
ap_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) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"cannot close POST session file");
return HTTP_INTERNAL_SERVER_ERROR;
}
*relay_state = apr_psprintf(r->pool, "%srepost?id=%s&ReturnTo=%s",
am_get_endpoint_url(r), psf_id,
am_urlencode(r->pool, *relay_state));
return OK;
}

View File

@ -94,7 +94,6 @@ static int am_global_init(apr_pool_t *conf, apr_pool_t *log,
mod->init_cache_size = mod->cache_size;
mod->init_lock_file = apr_pstrdup(conf, mod->lock_file);
/* find out the memory size of the cache */
mem_size = sizeof(am_cache_entry_t) * mod->init_cache_size;
@ -195,7 +194,7 @@ static void register_hooks(apr_pool_t *p)
ap_hook_check_user_id(am_check_uid, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_post_config(am_global_init, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_child_init(am_child_init, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_handler(am_handle_metadata, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_handler(am_handler, NULL, NULL, APR_HOOK_MIDDLE);
return;
}