/* Copyright 2006-2010 by Nils Maier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * mod_xsendfile.c: Process X-SENDFILE header cgi/scripts may set * Written by Nils Maier, testnutzer123 at google mail, March 2006 * * Whenever an X-SENDFILE header occures in the response headers drop * the body and send the replacement file idenfitied by this header instead. * * Method inspired by lighttpd * Code inspired by mod_headers, mod_rewrite and such * * Installation: * apxs2 -cia mod_xsendfile.c */ /* * v0.12 (peer-review still required) * * $Id$ */ #include "apr.h" #include "apr_lib.h" #include "apr_strings.h" #include "apr_buckets.h" #include "apr_file_io.h" #include "apr_hash.h" #define APR_WANT_IOVEC #define APR_WANT_STRFUNC #include "apr_want.h" #include "httpd.h" #include "http_log.h" #include "http_config.h" #include "http_log.h" #define CORE_PRIVATE #include "http_request.h" #include "http_core.h" /* needed for per-directory core-config */ #include "util_filter.h" #include "http_protocol.h" /* ap_hook_insert_error_filter */ #define AP_XSENDFILE_HEADER "X-SENDFILE" module AP_MODULE_DECLARE_DATA xsendfile_module; typedef enum { XSENDFILE_UNSET = 0, XSENDFILE_ENABLED = 1<<0, XSENDFILE_DISABLED = 1<<1 } xsendfile_conf_active_t; typedef struct xsendfile_conf_t { xsendfile_conf_active_t enabled; xsendfile_conf_active_t ignoreETag; xsendfile_conf_active_t ignoreLM; apr_array_header_t *paths; } xsendfile_conf_t; static xsendfile_conf_t *xsendfile_config_create(apr_pool_t *p) { xsendfile_conf_t *conf; conf = (xsendfile_conf_t *) apr_pcalloc(p, sizeof(xsendfile_conf_t)); conf->ignoreETag = conf->ignoreLM = conf->enabled = XSENDFILE_UNSET; conf->paths = apr_array_make(p, 1, sizeof(char*)); return conf; } static void *xsendfile_config_server_create(apr_pool_t *p, server_rec *s) { return (void*)xsendfile_config_create(p); } #define XSENDFILE_CFLAG(x) conf->x = overrides->x != XSENDFILE_UNSET ? overrides->x : base->x static void *xsendfile_config_merge(apr_pool_t *p, void *basev, void *overridesv) { xsendfile_conf_t *base = (xsendfile_conf_t *)basev; xsendfile_conf_t *overrides = (xsendfile_conf_t *)overridesv; xsendfile_conf_t *conf; conf = (xsendfile_conf_t *) apr_pcalloc(p, sizeof(xsendfile_conf_t)); XSENDFILE_CFLAG(enabled); XSENDFILE_CFLAG(ignoreETag); XSENDFILE_CFLAG(ignoreLM); conf->paths = apr_array_append(p, overrides->paths, base->paths); return (void*)conf; } static void *xsendfile_config_perdir_create(apr_pool_t *p, char *path) { return (void*)xsendfile_config_create(p); } #undef XSENDFILE_CFLAG static const char *xsendfile_cmd_flag(cmd_parms *cmd, void *perdir_confv, int flag) { xsendfile_conf_t *conf = (xsendfile_conf_t *)perdir_confv; if (cmd->path == NULL) { conf = (xsendfile_conf_t*)ap_get_module_config( cmd->server->module_config, &xsendfile_module ); } if (!conf) { return "Cannot get configuration object"; } if (!strcasecmp(cmd->cmd->name, "xsendfile")) { conf->enabled = flag ? XSENDFILE_ENABLED : XSENDFILE_DISABLED; } else if (!strcasecmp(cmd->cmd->name, "xsendfileignoreetag")) { conf->ignoreETag = flag ? XSENDFILE_ENABLED: XSENDFILE_DISABLED; } else if (!strcasecmp(cmd->cmd->name, "xsendfileignorelastmodified")) { conf->ignoreLM = flag ? XSENDFILE_ENABLED: XSENDFILE_DISABLED; } else { return apr_psprintf(cmd->pool, "Not a valid command in this context: %s %s", cmd->cmd->name, flag ? "On": "Off"); } return NULL; } static const char *xsendfile_cmd_path(cmd_parms *cmd, void *pdc, const char *arg) { xsendfile_conf_t *conf = (xsendfile_conf_t*)ap_get_module_config( cmd->server->module_config, &xsendfile_module ); char **newpath = (char**)apr_array_push(conf->paths); *newpath = apr_pstrdup(cmd->pool, arg); return NULL; } /* little helper function to get the original request path code borrowed from request.c and util_script.c */ static const char *ap_xsendfile_get_orginal_path(request_rec *rec) { const char *rv = rec->the_request, *last; int dir = 0; size_t uri_len; /* skip method && spaces */ while (*rv && !apr_isspace(*rv)) { ++rv; } while (apr_isspace(*rv)) { ++rv; } /* first space is the request end */ last = rv; while (*last && !apr_isspace(*last)) { ++last; } uri_len = last - rv; if (!uri_len) { return NULL; } /* alright, lets see if the request_uri changed! */ if (strncmp(rv, rec->uri, uri_len) == 0) { rv = apr_pstrdup(rec->pool, rec->filename); dir = rec->finfo.filetype == APR_DIR; } else { /* need to lookup the url again as it changed */ request_rec *sr = ap_sub_req_lookup_uri( apr_pstrmemdup(rec->pool, rv, uri_len), rec, NULL ); if (!sr) { return NULL; } rv = apr_pstrdup(rec->pool, sr->filename); dir = rec->finfo.filetype == APR_DIR; ap_destroy_sub_req(sr); } /* now we need to truncate so we only have the directory */ if (!dir && (last = ap_strrchr(rv, '/')) != NULL) { *((char*)last + 1) = '\0'; } return rv; } /* little helper function to build the file path if available */ static apr_status_t ap_xsendfile_get_filepath(request_rec *r, xsendfile_conf_t *conf, const char *file, /* out */ char **path) { const char *root = ap_xsendfile_get_orginal_path(r); apr_status_t rv; apr_array_header_t *patharr; const char **paths; int i; #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: path is %s", root); #endif /* merge the array */ if (root) { patharr = apr_array_make(r->pool, conf->paths->nelts + 1, sizeof(char*)); *(const char**)(apr_array_push(patharr)) = root; apr_array_cat(patharr, conf->paths); } else { patharr = conf->paths; } if (patharr->nelts == 0) { return APR_EBADPATH; } paths = (const char**)patharr->elts; for (i = 0; i < patharr->nelts; ++i) { if ((rv = apr_filepath_merge( path, paths[i], file, APR_FILEPATH_TRUENAME | APR_FILEPATH_NOTABOVEROOT, r->pool )) == OK) { break; } } if (rv != OK) { *path = NULL; } return rv; } static apr_status_t ap_xsendfile_output_filter(ap_filter_t *f, apr_bucket_brigade *in) { request_rec *r = f->r, *sr = NULL; xsendfile_conf_t *dconf = (xsendfile_conf_t *)ap_get_module_config(r->per_dir_config, &xsendfile_module), *sconf = (xsendfile_conf_t *)ap_get_module_config(r->server->module_config, &xsendfile_module), *conf = xsendfile_config_merge(r->pool, sconf, dconf); core_dir_config *coreconf = (core_dir_config *)ap_get_module_config(r->per_dir_config, &core_module); apr_status_t rv; apr_bucket *e; apr_file_t *fd = NULL; apr_finfo_t finfo; const char *file = NULL; char *translated = NULL; int errcode; #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: output_filter for %s", r->the_request); #endif /* should we proceed with this request? * sub-requests suck * furthermore default-handled requests suck, as they actually shouldn't be able to set headers */ if ( r->status != HTTP_OK || r->main || (r->handler && strcmp(r->handler, "default-handler") == 0) /* those table-keys are lower-case, right? */ ) { #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: not met [%d]", r->status); #endif ap_remove_output_filter(f); return ap_pass_brigade(f->next, in); } /* alright, look for x-sendfile */ file = apr_table_get(r->headers_out, AP_XSENDFILE_HEADER); apr_table_unset(r->headers_out, AP_XSENDFILE_HEADER); /* cgi/fastcgi will put the stuff into err_headers_out */ if (!file || !*file) { file = apr_table_get(r->err_headers_out, AP_XSENDFILE_HEADER); apr_table_unset(r->err_headers_out, AP_XSENDFILE_HEADER); } /* nothing there :p */ if (!file || !*file) { #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: nothing found"); #endif ap_remove_output_filter(f); return ap_pass_brigade(f->next, in); } /* drop *everything* might be pretty expensive to generate content first that goes straight to the bitbucket, but actually the scripts that might set this flag won't output too much anyway */ while (!APR_BRIGADE_EMPTY(in)) { e = APR_BRIGADE_FIRST(in); apr_bucket_delete(e); } r->eos_sent = 0; /* as we dropped all the content this field is not valid anymore! */ apr_table_unset(r->headers_out, "Content-Length"); apr_table_unset(r->err_headers_out, "Content-Length"); apr_table_unset(r->headers_out, "Content-Encoding"); apr_table_unset(r->err_headers_out, "Content-Encoding"); rv = ap_xsendfile_get_filepath(r, conf, file, &translated); if (rv != OK) { ap_log_rerror( APLOG_MARK, APLOG_ERR, rv, r, "xsendfile: unable to find file: %s", file ); ap_remove_output_filter(f); ap_die(HTTP_NOT_FOUND, r); return HTTP_NOT_FOUND; } #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: found %s", translated); #endif /* try open the file */ if ((rv = apr_file_open( &fd, translated, APR_READ | APR_BINARY #if APR_HAS_SENDFILE | (coreconf->enable_sendfile != ENABLE_SENDFILE_OFF ? APR_SENDFILE_ENABLED : 0) #endif , 0, r->pool )) != APR_SUCCESS) { ap_log_rerror( APLOG_MARK, APLOG_ERR, rv, r, "xsendfile: cannot open file: %s", translated ); ap_remove_output_filter(f); ap_die(HTTP_NOT_FOUND, r); return HTTP_NOT_FOUND; } #if APR_HAS_SENDFILE && defined(_DEBUG) if (coreconf->enable_sendfile == ENABLE_SENDFILE_OFF) { ap_log_error( APLOG_MARK, APLOG_WARNING, 0, r->server, "xsendfile: sendfile configured, but not active %d", coreconf->enable_sendfile ); } #endif /* stat (for etag/cache/content-length stuff) */ if ((rv = apr_file_info_get(&finfo, APR_FINFO_NORM, fd)) != APR_SUCCESS) { ap_log_rerror( APLOG_MARK, APLOG_ERR, rv, r, "xsendfile: unable to stat file: %s", translated ); apr_file_close(fd); ap_remove_output_filter(f); ap_die(HTTP_FORBIDDEN, r); return HTTP_FORBIDDEN; } /* no inclusion of directories! we're serving files! */ if (finfo.filetype != APR_REG) { ap_log_rerror( APLOG_MARK, APLOG_ERR, APR_EBADPATH, r, "xsendfile: not a file %s", translated ); apr_file_close(fd); ap_remove_output_filter(f); ap_die(HTTP_NOT_FOUND, r); return HTTP_NOT_FOUND; } /* need to cheat here a bit as etag generator will use those ;) and we want local_copy and cache */ r->finfo.inode = finfo.inode; r->finfo.size = finfo.size; /* caching? why not :p */ r->no_cache = r->no_local_copy = 0; /* some script (f?cgi) place stuff in err_headers_out */ if ( conf->ignoreLM == XSENDFILE_ENABLED || ( !apr_table_get(r->headers_out, "last-modified") && !apr_table_get(r->headers_out, "last-modified") ) ) { apr_table_unset(r->err_headers_out, "last-modified"); ap_update_mtime(r, finfo.mtime); ap_set_last_modified(r); } if ( conf->ignoreETag == XSENDFILE_ENABLED || ( !apr_table_get(r->headers_out, "etag") && !apr_table_get(r->err_headers_out, "etag") ) ) { apr_table_unset(r->err_headers_out, "etag"); ap_set_etag(r); } ap_set_content_length(r, finfo.size); /* cache or something? */ if ((errcode = ap_meets_conditions(r)) != OK) { #ifdef _DEBUG ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: met condition %d for %s", errcode, file ); #endif apr_file_close(fd); r->status = errcode; } else { /* For platforms where the size of the file may be larger than * that which can be stored in a single bucket (where the * length field is an apr_size_t), split it into several * buckets: */ if (sizeof(apr_off_t) > sizeof(apr_size_t) && finfo.size > AP_MAX_SENDFILE) { apr_off_t fsize = finfo.size; e = apr_bucket_file_create(fd, 0, AP_MAX_SENDFILE, r->pool, in->bucket_alloc); while (fsize > AP_MAX_SENDFILE) { apr_bucket *ce; apr_bucket_copy(e, &ce); APR_BRIGADE_INSERT_TAIL(in, ce); e->start += AP_MAX_SENDFILE; fsize -= AP_MAX_SENDFILE; } e->length = (apr_size_t)fsize; /* Resize just the last bucket */ } else { e = apr_bucket_file_create(fd, 0, (apr_size_t)finfo.size, r->pool, in->bucket_alloc); } #if APR_HAS_MMAP if (coreconf->enable_mmap == ENABLE_MMAP_ON) { apr_bucket_file_enable_mmap(e, 0); } #if defined(_DEBUG) else { ap_log_error( APLOG_MARK, APLOG_WARNING, 0, r->server, "xsendfile: mmap configured, but not active %d", coreconf->enable_mmap ); } #endif /* _DEBUG */ #endif /* APR_HAS_MMAP */ APR_BRIGADE_INSERT_TAIL(in, e); } e = apr_bucket_eos_create(in->bucket_alloc); APR_BRIGADE_INSERT_TAIL(in, e); /* remove ourselves from the filter chain */ ap_remove_output_filter(f); #ifdef _DEBUG ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: sending %d bytes", (int)finfo.size); #endif /* send the data up the stack */ return ap_pass_brigade(f->next, in); } static void ap_xsendfile_insert_output_filter(request_rec *r) { xsendfile_conf_active_t enabled = ((xsendfile_conf_t *)ap_get_module_config(r->per_dir_config, &xsendfile_module))->enabled; if (XSENDFILE_UNSET == enabled) { enabled = ((xsendfile_conf_t*)ap_get_module_config(r->server->module_config, &xsendfile_module))->enabled; } if (XSENDFILE_ENABLED != enabled) { return; } ap_add_output_filter( "XSENDFILE", NULL, r, r->connection ); } static const command_rec xsendfile_command_table[] = { AP_INIT_FLAG( "XSendFile", xsendfile_cmd_flag, NULL, OR_FILEINFO, "On|Off - Enable/disable(default) processing" ), AP_INIT_FLAG( "XSendFileIgnoreEtag", xsendfile_cmd_flag, NULL, OR_FILEINFO, "On|Off - Ignore script provided Etag headers (default: Off)" ), AP_INIT_FLAG( "XSendFileIgnoreLastModified", xsendfile_cmd_flag, NULL, OR_FILEINFO, "On|Off - Ignore script provided Last-Modified headers (default: Off)" ), AP_INIT_TAKE1( "XSendFilePath", xsendfile_cmd_path, NULL, RSRC_CONF|ACCESS_CONF, "Allow to serve files from that Path. Must be absolute" ), { NULL } }; static void xsendfile_register_hooks(apr_pool_t *p) { ap_register_output_filter( "XSENDFILE", ap_xsendfile_output_filter, NULL, AP_FTYPE_CONTENT_SET ); ap_hook_insert_filter( ap_xsendfile_insert_output_filter, NULL, NULL, APR_HOOK_LAST + 1 ); } module AP_MODULE_DECLARE_DATA xsendfile_module = { STANDARD20_MODULE_STUFF, xsendfile_config_perdir_create, xsendfile_config_merge, xsendfile_config_server_create, xsendfile_config_merge, xsendfile_command_table, xsendfile_register_hooks };