| /* Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You 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. | |
| */ | |
| /* | |
| * Author: mod_file_cache by Bill Stoddard <stoddard apache.org> | |
| * Based on mod_mmap_static by Dean Gaudet <dgaudet arctic.org> | |
| * | |
| * v0.01: initial implementation | |
| */ | |
| /* | |
| Documentation: | |
| Some sites have a set of static files that are really busy, and | |
| change infrequently (or even on a regular schedule). Save time | |
| by caching open handles to these files. This module, unlike | |
| mod_mmap_static, caches open file handles, not file content. | |
| On systems (like Windows) with heavy system call overhead and | |
| that have an efficient sendfile implementation, caching file handles | |
| offers several advantages over caching content. First, the file system | |
| can manage the memory, allowing infrequently hit cached files to | |
| be paged out. Second, since caching open handles does not consume | |
| significant resources, it will be possible to enable an AutoLoadCache | |
| feature where static files are dynamically loaded in the cache | |
| as the server runs. On systems that have file change notification, | |
| this module can be enhanced to automatically garbage collect | |
| cached files that change on disk. | |
| This module should work on Unix systems that have sendfile. Place | |
| cachefile directives into your configuration to direct files to | |
| be cached. | |
| cachefile /path/to/file1 | |
| cachefile /path/to/file2 | |
| ... | |
| These files are only cached when the server is restarted, so if you | |
| change the list, or if the files are changed, then you'll need to | |
| restart the server. | |
| To reiterate that point: if the files are modified *in place* | |
| without restarting the server you may end up serving requests that | |
| are completely bogus. You should update files by unlinking the old | |
| copy and putting a new copy in place. | |
| There's no such thing as inheriting these files across vhosts or | |
| whatever... place the directives in the main server only. | |
| Known problems: | |
| Don't use Alias or RewriteRule to move these files around... unless | |
| you feel like paying for an extra stat() on each request. This is | |
| a deficiency in the Apache API that will hopefully be solved some day. | |
| The file will be served out of the file handle cache, but there will be | |
| an extra stat() that's a waste. | |
| */ | |
| #include "apr.h" | |
| #if !(APR_HAS_SENDFILE || APR_HAS_MMAP) | |
| #error mod_file_cache only works on systems with APR_HAS_SENDFILE or APR_HAS_MMAP | |
| #endif | |
| #include "apr_mmap.h" | |
| #include "apr_strings.h" | |
| #include "apr_hash.h" | |
| #include "apr_buckets.h" | |
| #define APR_WANT_STRFUNC | |
| #include "apr_want.h" | |
| #if APR_HAVE_SYS_TYPES_H | |
| #include <sys/types.h> | |
| #endif | |
| #include "httpd.h" | |
| #include "http_config.h" | |
| #include "http_log.h" | |
| #include "http_protocol.h" | |
| #include "http_request.h" | |
| #include "http_core.h" | |
| module AP_MODULE_DECLARE_DATA file_cache_module; | |
| typedef struct { | |
| #if APR_HAS_SENDFILE | |
| apr_file_t *file; | |
| #endif | |
| const char *filename; | |
| apr_finfo_t finfo; | |
| int is_mmapped; | |
| #if APR_HAS_MMAP | |
| apr_mmap_t *mm; | |
| #endif | |
| char mtimestr[APR_RFC822_DATE_LEN]; | |
| char sizestr[21]; /* big enough to hold any 64-bit file size + null */ | |
| } a_file; | |
| typedef struct { | |
| apr_hash_t *fileht; | |
| } a_server_config; | |
| static void *create_server_config(apr_pool_t *p, server_rec *s) | |
| { | |
| a_server_config *sconf = apr_palloc(p, sizeof(*sconf)); | |
| sconf->fileht = apr_hash_make(p); | |
| return sconf; | |
| } | |
| static void cache_the_file(cmd_parms *cmd, const char *filename, int mmap) | |
| { | |
| a_server_config *sconf; | |
| a_file *new_file; | |
| a_file tmp; | |
| apr_file_t *fd = NULL; | |
| apr_status_t rc; | |
| const char *fspec; | |
| fspec = ap_server_root_relative(cmd->pool, filename); | |
| if (!fspec) { | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, APR_EBADPATH, cmd->server, APLOGNO(00794) | |
| "invalid file path " | |
| "%s, skipping", filename); | |
| return; | |
| } | |
| if ((rc = apr_stat(&tmp.finfo, fspec, APR_FINFO_MIN, | |
| cmd->temp_pool)) != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, rc, cmd->server, APLOGNO(00795) | |
| "unable to stat(%s), skipping", fspec); | |
| return; | |
| } | |
| if (tmp.finfo.filetype != APR_REG) { | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server, APLOGNO(00796) | |
| "%s isn't a regular file, skipping", fspec); | |
| return; | |
| } | |
| if (tmp.finfo.size > AP_MAX_SENDFILE) { | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server, APLOGNO(00797) | |
| "%s is too large to cache, skipping", fspec); | |
| return; | |
| } | |
| rc = apr_file_open(&fd, fspec, APR_READ | APR_BINARY | APR_XTHREAD, | |
| APR_OS_DEFAULT, cmd->pool); | |
| if (rc != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, rc, cmd->server, APLOGNO(00798) | |
| "unable to open(%s, O_RDONLY), skipping", fspec); | |
| return; | |
| } | |
| apr_file_inherit_set(fd); | |
| /* WooHoo, we have a file to put in the cache */ | |
| new_file = apr_pcalloc(cmd->pool, sizeof(a_file)); | |
| new_file->finfo = tmp.finfo; | |
| #if APR_HAS_MMAP | |
| if (mmap) { | |
| /* MMAPFile directive. MMAP'ing the file | |
| * XXX: APR_HAS_LARGE_FILES issue; need to reject this request if | |
| * size is greater than MAX(apr_size_t) (perhaps greater than 1M?). | |
| */ | |
| if ((rc = apr_mmap_create(&new_file->mm, fd, 0, | |
| (apr_size_t)new_file->finfo.size, | |
| APR_MMAP_READ, cmd->pool)) != APR_SUCCESS) { | |
| apr_file_close(fd); | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, rc, cmd->server, APLOGNO(00799) | |
| "unable to mmap %s, skipping", filename); | |
| return; | |
| } | |
| apr_file_close(fd); | |
| new_file->is_mmapped = TRUE; | |
| } | |
| #endif | |
| #if APR_HAS_SENDFILE | |
| if (!mmap) { | |
| /* CacheFile directive. Caching the file handle */ | |
| new_file->is_mmapped = FALSE; | |
| new_file->file = fd; | |
| } | |
| #endif | |
| new_file->filename = fspec; | |
| apr_rfc822_date(new_file->mtimestr, new_file->finfo.mtime); | |
| apr_snprintf(new_file->sizestr, sizeof new_file->sizestr, "%" APR_OFF_T_FMT, new_file->finfo.size); | |
| sconf = ap_get_module_config(cmd->server->module_config, &file_cache_module); | |
| apr_hash_set(sconf->fileht, new_file->filename, strlen(new_file->filename), new_file); | |
| } | |
| static const char *cachefilehandle(cmd_parms *cmd, void *dummy, const char *filename) | |
| { | |
| #if APR_HAS_SENDFILE | |
| cache_the_file(cmd, filename, 0); | |
| #else | |
| /* Sendfile not supported by this OS */ | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server, APLOGNO(00800) | |
| "unable to cache file: %s. Sendfile is not supported on this OS", filename); | |
| #endif | |
| return NULL; | |
| } | |
| static const char *cachefilemmap(cmd_parms *cmd, void *dummy, const char *filename) | |
| { | |
| #if APR_HAS_MMAP | |
| cache_the_file(cmd, filename, 1); | |
| #else | |
| /* MMAP not supported by this OS */ | |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server, APLOGNO(00801) | |
| "unable to cache file: %s. MMAP is not supported by this OS", filename); | |
| #endif | |
| return NULL; | |
| } | |
| static int file_cache_post_config(apr_pool_t *p, apr_pool_t *plog, | |
| apr_pool_t *ptemp, server_rec *s) | |
| { | |
| /* Hummm, anything to do here? */ | |
| return OK; | |
| } | |
| /* If it's one of ours, fill in r->finfo now to avoid extra stat()... this is a | |
| * bit of a kludge, because we really want to run after core_translate runs. | |
| */ | |
| static int file_cache_xlat(request_rec *r) | |
| { | |
| a_server_config *sconf; | |
| a_file *match; | |
| int res; | |
| sconf = ap_get_module_config(r->server->module_config, &file_cache_module); | |
| /* we only operate when at least one cachefile directive was used */ | |
| if (!apr_hash_count(sconf->fileht)) { | |
| return DECLINED; | |
| } | |
| res = ap_core_translate(r); | |
| if (res != OK || !r->filename) { | |
| return res; | |
| } | |
| /* search the cache */ | |
| match = (a_file *) apr_hash_get(sconf->fileht, r->filename, APR_HASH_KEY_STRING); | |
| if (match == NULL) | |
| return DECLINED; | |
| /* pass search results to handler */ | |
| ap_set_module_config(r->request_config, &file_cache_module, match); | |
| /* shortcircuit the get_path_info() stat() calls and stuff */ | |
| r->finfo = match->finfo; | |
| return OK; | |
| } | |
| static int mmap_handler(request_rec *r, a_file *file) | |
| { | |
| #if APR_HAS_MMAP | |
| conn_rec *c = r->connection; | |
| apr_bucket *b; | |
| apr_bucket_brigade *bb = apr_brigade_create(r->pool, c->bucket_alloc); | |
| b = apr_bucket_immortal_create((const char *)file->mm->mm, | |
| (apr_size_t)file->finfo.size, c->bucket_alloc); | |
| APR_BRIGADE_INSERT_TAIL(bb, b); | |
| b = apr_bucket_eos_create(c->bucket_alloc); | |
| APR_BRIGADE_INSERT_TAIL(bb, b); | |
| if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS) | |
| return AP_FILTER_ERROR; | |
| #endif | |
| return OK; | |
| } | |
| static int sendfile_handler(request_rec *r, a_file *file) | |
| { | |
| #if APR_HAS_SENDFILE | |
| conn_rec *c = r->connection; | |
| apr_bucket *b; | |
| apr_bucket_brigade *bb = apr_brigade_create(r->pool, c->bucket_alloc); | |
| apr_brigade_insert_file(bb, file->file, 0, file->finfo.size, r->pool); | |
| b = apr_bucket_eos_create(c->bucket_alloc); | |
| APR_BRIGADE_INSERT_TAIL(bb, b); | |
| if (ap_pass_brigade(r->output_filters, bb) != APR_SUCCESS) | |
| return AP_FILTER_ERROR; | |
| #endif | |
| return OK; | |
| } | |
| static int file_cache_handler(request_rec *r) | |
| { | |
| a_file *match; | |
| int errstatus; | |
| int rc = OK; | |
| /* Bail out if r->handler isn't the default value, and doesn't look like a Content-Type | |
| * XXX: Even though we made the user explicitly list each path to cache? | |
| */ | |
| if (ap_strcmp_match(r->handler, "*/*") && !AP_IS_DEFAULT_HANDLER_NAME(r->handler)) { | |
| return DECLINED; | |
| } | |
| /* we don't handle anything but GET */ | |
| if (r->method_number != M_GET) return DECLINED; | |
| /* did xlat phase find the file? */ | |
| match = ap_get_module_config(r->request_config, &file_cache_module); | |
| if (match == NULL) { | |
| return DECLINED; | |
| } | |
| /* note that we would handle GET on this resource */ | |
| r->allowed |= (AP_METHOD_BIT << M_GET); | |
| /* This handler has no use for a request body (yet), but we still | |
| * need to read and discard it if the client sent one. | |
| */ | |
| if ((errstatus = ap_discard_request_body(r)) != OK) | |
| return errstatus; | |
| ap_update_mtime(r, match->finfo.mtime); | |
| /* ap_set_last_modified() always converts the file mtime to a string | |
| * which is slow. Accelerate the common case. | |
| * ap_set_last_modified(r); | |
| */ | |
| { | |
| apr_time_t mod_time; | |
| char *datestr; | |
| mod_time = ap_rationalize_mtime(r, r->mtime); | |
| if (mod_time == match->finfo.mtime) | |
| datestr = match->mtimestr; | |
| else { | |
| datestr = apr_palloc(r->pool, APR_RFC822_DATE_LEN); | |
| apr_rfc822_date(datestr, mod_time); | |
| } | |
| apr_table_setn(r->headers_out, "Last-Modified", datestr); | |
| } | |
| /* ap_set_content_length() always converts the same number and never | |
| * returns an error. Accelerate it. | |
| */ | |
| r->clength = match->finfo.size; | |
| apr_table_setn(r->headers_out, "Content-Length", match->sizestr); | |
| ap_set_etag(r); | |
| if ((errstatus = ap_meets_conditions(r)) != OK) { | |
| return errstatus; | |
| } | |
| /* Call appropriate handler */ | |
| if (!r->header_only) { | |
| if (match->is_mmapped == TRUE) | |
| rc = mmap_handler(r, match); | |
| else | |
| rc = sendfile_handler(r, match); | |
| } | |
| return rc; | |
| } | |
| static const command_rec file_cache_cmds[] = | |
| { | |
| AP_INIT_ITERATE("cachefile", cachefilehandle, NULL, RSRC_CONF, | |
| "A space separated list of files to add to the file handle cache at config time"), | |
| AP_INIT_ITERATE("mmapfile", cachefilemmap, NULL, RSRC_CONF, | |
| "A space separated list of files to mmap at config time"), | |
| {NULL} | |
| }; | |
| static void register_hooks(apr_pool_t *p) | |
| { | |
| ap_hook_handler(file_cache_handler, NULL, NULL, APR_HOOK_LAST); | |
| ap_hook_post_config(file_cache_post_config, NULL, NULL, APR_HOOK_MIDDLE); | |
| ap_hook_translate_name(file_cache_xlat, NULL, NULL, APR_HOOK_MIDDLE); | |
| /* This trick doesn't work apparently because the translate hooks | |
| are single shot. If the core_hook returns OK, then our hook is | |
| not called. | |
| ap_hook_translate_name(file_cache_xlat, aszPre, NULL, APR_HOOK_MIDDLE); | |
| */ | |
| } | |
| AP_DECLARE_MODULE(file_cache) = | |
| { | |
| STANDARD20_MODULE_STUFF, | |
| NULL, /* create per-directory config structure */ | |
| NULL, /* merge per-directory config structures */ | |
| create_server_config, /* create per-server config structure */ | |
| NULL, /* merge per-server config structures */ | |
| file_cache_cmds, /* command handlers */ | |
| register_hooks /* register hooks */ | |
| }; |