/** * 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 // 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: * * '\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: '\0' if((ptr - pfr == PFR_LENGTH) && (pfr[PFR_LENGTH - 1] == '\0') && (strncmp(pfr, "", 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 */ "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 */ };