609 lines
16 KiB
C
609 lines
16 KiB
C
/* 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 <http://lighttpd.net/>
|
|
* 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
|
|
};
|