blob: 8beb8248230839ae883f4ec136ef27f2f96088d2 [file] [log] [blame]
/**
* Flash Socket Policy Apache Module.
*
* This module provides a flash socket policy file on the same port that
* serves HTTP on Apache. This can help simplify setting up a server that
* supports cross-domain communication with flash.
*
* Quick note about Apache memory handling: Data is allocated from pools and
* is not manually returned to those pools. The pools are typically considered
* short-lived and will be cleaned up automatically by Apache.
*
* @author Dave Longley
*
* Copyright (c) 2010 Digital Bazaar, Inc. All rights reserved.
*/
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "ap_compat.h"
#include <string.h>
// length of a policy file request
#define PFR_LENGTH 23
// declare main module
module AP_MODULE_DECLARE_DATA fsp_module;
// configuration for the module
typedef struct fsp_config
{
// the cross-domain policy to serve
char* policy;
apr_size_t policy_length;
} fsp_config;
// filter state for keeping track of detected policy file requests
typedef struct filter_state
{
fsp_config* cfg;
int checked;
int found;
} filter_state;
// for registering hooks, filters, etc.
static void fsp_register_hooks(apr_pool_t *p);
static int fsp_pre_connection(conn_rec *c, void *csd);
// filter handler declarations
static apr_status_t fsp_input_filter(
ap_filter_t* f, apr_bucket_brigade* bb,
ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes);
static int fsp_output_filter(ap_filter_t* f, apr_bucket_brigade* bb);
/**
* Registers the hooks for this module.
*
* @param p the pool to allocate from, if necessary.
*/
static void fsp_register_hooks(apr_pool_t* p)
{
// registers the pre-connection hook to handle adding filters
ap_hook_pre_connection(
fsp_pre_connection, NULL, NULL, APR_HOOK_MIDDLE);
// will parse a policy file request, to be added in pre_connection
ap_register_input_filter(
"fsp_request", fsp_input_filter,
NULL, AP_FTYPE_CONNECTION);
// will emit a cross-domain policy response to be added in pre_connection
ap_register_output_filter(
"fsp_response", fsp_output_filter,
NULL, AP_FTYPE_CONNECTION);
}
/**
* A hook that is called before a connection is handled. This function will
* get the module configuration and add the flash socket policy filters if
* a cross-domain policy has been specified in the configuration.
*
* @param c the connection.
* @param csd the connection socket descriptor.
*
* @return OK on success.
*/
static int fsp_pre_connection(conn_rec* c, void* csd)
{
// only install filters if a policy was specified in the module config
fsp_config* cfg = ap_get_module_config(
c->base_server->module_config, &fsp_module);
if(cfg->policy != NULL)
{
// allocate filter state
filter_state* state = apr_palloc(c->pool, sizeof(filter_state));
if(state != NULL)
{
// initialize state
state->cfg = cfg;
state->checked = state->found = 0;
// add filters
ap_add_input_filter("fsp_request", state, NULL, c);
ap_add_output_filter("fsp_response", state, NULL, c);
}
}
return OK;
}
/**
* Searches the input request for a flash socket policy request. This request,
* unfortunately, does not follow the HTTP protocol and cannot be handled
* via a special HTTP handler. Instead, it is a short xml string followed by
* a null character:
*
* '<policy-file-request/>\0'
*
* A peek into the incoming data checks the first character of the stream to
* see if it is '<' (as opposed to typically something else for HTTP). If it
* is not, then this function returns and HTTP input is read normally. If it
* is, then the remaining bytes in the policy-file-request are read and
* checked. If a match is found, then the filter state will be updated to
* inform the output filter to send a cross-domain policy as a response. If
* no match is found, HTTP traffic will proceed as usual.
*
* @param f the input filter.
* @param state the filter state.
*
* @return APR_SUCCESS on success, some other status on failure.
*/
static apr_status_t find_policy_file_request(
ap_filter_t* f, filter_state* state)
{
apr_status_t rval = APR_SUCCESS;
// create a temp buffer for speculative reads
apr_bucket_brigade* tmp = apr_brigade_create(f->c->pool, f->c->bucket_alloc);
// FIXME: not sure how blocking mode works ... can it return fewer than
// the number of specified bytes?
// peek at the first PFR_LENGTH bytes
rval = ap_get_brigade(
f->next, tmp, AP_MODE_SPECULATIVE, APR_BLOCK_READ, PFR_LENGTH);
if(rval == APR_SUCCESS)
{
// quickly check the first bucket for the beginning of a pfr
const char* data;
apr_size_t length;
apr_bucket* b = APR_BRIGADE_FIRST(tmp);
rval = apr_bucket_read(b, &data, &length, APR_BLOCK_READ);
if(rval == APR_SUCCESS && length > 0 && data[0] == '<')
{
// possible policy file request, fill local buffer
char pfr[PFR_LENGTH];
char* ptr = pfr;
memcpy(ptr, data, length);
ptr += length;
memset(ptr, '\0', PFR_LENGTH - length);
b = APR_BUCKET_NEXT(b);
while(rval == APR_SUCCESS && b != APR_BRIGADE_SENTINEL(tmp))
{
rval = apr_bucket_read(b, &data, &length, APR_BLOCK_READ);
if(rval == APR_SUCCESS)
{
memcpy(ptr, data, length);
ptr += length;
b = APR_BUCKET_NEXT(b);
}
}
if(rval == APR_SUCCESS)
{
// see if pfr is a policy file request: '<policy-file-request/>\0'
if((ptr - pfr == PFR_LENGTH) && (pfr[PFR_LENGTH - 1] == '\0') &&
(strncmp(pfr, "<policy-file-request/>", PFR_LENGTH -1) == 0))
{
// pfr found
state->found = 1;
}
}
}
}
return rval;
}
/**
* Handles incoming data. If an attempt has not yet been made to look for
* a policy request (it is the beginning of the connection), then one is
* made. Otherwise this filter does nothing.
*
* If an attempt is made to find a policy request and one is not found, then
* reads proceed as normal. If one is found, then the filter state is modified
* to inform the output filter to send a policy request and the return value
* of this filter is EOF indicating that the connection should close after
* sending the cross-domain policy.
*
* @param f the input filter.
* @param bb the brigate to fill with input from the next filters in the chain
* and then process (look for a policy file request).
* @param mode the type of read requested (ie: AP_MODE_GETLINE means read until
* a CRLF is found, AP_MODE_GETBYTES means 'nbytes' of data, etc).
* @param block APR_BLOCK_READ or APR_NONBLOCK_READ, indicates the type of
* blocking to do when trying to read.
* @param nbytes used if the read mode is appropriate to specify the number of
* bytes to read (set to 0 for AP_MODE_GETLINE).
*
* @return the status of the input (ie: APR_SUCCESS for read success, APR_EOF
* for end of stream, APR_EAGAIN to read again when non-blocking).
*/
static apr_status_t fsp_input_filter(
ap_filter_t* f, apr_bucket_brigade* bb,
ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes)
{
apr_status_t rval = APR_SUCCESS;
filter_state* state = f->ctx;
if(state->checked == 1)
{
// already checked for policy file request, just read from other filters
rval = ap_get_brigade(f->next, bb, mode, block, nbytes);
}
else
{
// try to find a policy file request
rval = find_policy_file_request(f, state);
state->checked = 1;
if(rval == APR_SUCCESS)
{
if(state->found)
{
// do read of PFR_LENGTH bytes, consider end of stream
rval = ap_get_brigade(
f->next, bb, AP_MODE_READBYTES, APR_BLOCK_READ, PFR_LENGTH);
rval = APR_EOF;
}
else
{
// do normal read
rval = ap_get_brigade(f->next, bb, mode, block, nbytes);
}
}
}
return rval;
}
/**
* Handles outgoing data. If the filter state indicates that a cross-domain
* policy should be sent then it is added to the outgoing brigade of data. If
* a policy request was not detected, then this filter makes no changes to
* the outgoing data.
*
* @param f the output filter.
* @param bb the outgoing brigade of data.
*
* @return APR_SUCCESS on success, some other status on error.
*/
static int fsp_output_filter(ap_filter_t* f, apr_bucket_brigade* bb)
{
apr_status_t rval = APR_SUCCESS;
filter_state* state = f->ctx;
if(state->found)
{
// found policy-file-request, add response bucket
// bucket is immortal because the data is stored in the configuration
// and doesn't need to be copied
apr_bucket* head = apr_bucket_immortal_create(
state->cfg->policy, state->cfg->policy_length, bb->bucket_alloc);
APR_BRIGADE_INSERT_HEAD(bb, head);
}
if(rval == APR_SUCCESS)
{
// pass brigade to next filter
rval = ap_pass_brigade(f->next, bb);
}
return rval;
}
/**
* Creates the configuration for this module.
*
* @param p the pool to allocate from.
* @param s the server the configuration is for.
*
* @return the configuration data.
*/
static void* fsp_create_config(apr_pool_t* p, server_rec* s)
{
// allocate config
fsp_config* cfg = apr_palloc(p, sizeof(fsp_config));
// no default policy
cfg->policy = NULL;
cfg->policy_length = 0;
return cfg;
}
/**
* Sets the policy file to use from the configuration.
*
* @param parms the command directive parameters.
* @param userdata NULL, not used.
* @param arg the string argument to the command directive (the file with
* the cross-domain policy to serve as content).
*
* @return NULL on success, otherwise an error string to display.
*/
static const char* fsp_set_policy_file(
cmd_parms* parms, void* userdata, const char* arg)
{
const char* rval = NULL;
apr_pool_t* pool = (apr_pool_t*)parms->pool;
fsp_config* cfg = ap_get_module_config(
parms->server->module_config, &fsp_module);
// ensure command is in the correct context
rval = ap_check_cmd_context(parms, NOT_IN_DIR_LOC_FILE|NOT_IN_LIMIT);
if(rval == NULL)
{
// get canonical file name
char* fname = ap_server_root_relative(pool, arg);
if(fname == NULL)
{
rval = (const char*)apr_psprintf(
pool, "%s: Invalid policy file '%s'",
parms->cmd->name, arg);
}
else
{
// try to open the file
apr_status_t rv;
apr_file_t* fd;
apr_finfo_t finfo;
rv = apr_file_open(&fd, fname, APR_READ, APR_OS_DEFAULT, pool);
if(rv == APR_SUCCESS)
{
// stat file
rv = apr_file_info_get(&finfo, APR_FINFO_NORM, fd);
if(rv == APR_SUCCESS)
{
// ensure file is not empty
apr_size_t length = (apr_size_t)finfo.size;
if(length <= 0)
{
rval = (const char*)apr_psprintf(
pool, "%s: policy file '%s' is empty",
parms->cmd->name, fname);
}
// read file
else
{
char* buf = (char*)apr_palloc(pool, length + 1);
buf[length] = '\0';
rv = apr_file_read_full(fd, buf, length, NULL);
if(rv == APR_SUCCESS)
{
// TODO: validate file
// save policy string
cfg->policy = buf;
cfg->policy_length = length + 1;
}
}
// close the file
apr_file_close(fd);
}
}
// handle error case
if(rv != APR_SUCCESS)
{
char errmsg[120];
rval = (const char*)apr_psprintf(
pool, "%s: Invalid policy file '%s' (%s)",
parms->cmd->name, fname,
apr_strerror(rv, errmsg, sizeof(errmsg)));
}
}
}
return rval;
}
// table of configuration directives
static const command_rec fsp_cmds[] =
{
AP_INIT_TAKE1(
"FSPPolicyFile", /* the directive */
fsp_set_policy_file, /* function to call when directive is found */
NULL, /* user data to pass to function, not used */
RSRC_CONF, /* indicates the directive appears outside of <Location> */
"FSPPolicyFile (string) The cross-domain policy file to use"), /* docs */
{NULL}
};
// module setup
module AP_MODULE_DECLARE_DATA fsp_module =
{
STANDARD20_MODULE_STUFF, /* stuff declared in every 2.0 mod */
NULL, /* create per-directory config structure */
NULL, /* merge per-directory config structures */
fsp_create_config, /* create per-server config structure */
NULL, /* merge per-server config structures */
fsp_cmds, /* command apr_table_t */
fsp_register_hooks /* register hooks */
};