Files
Cloud-CMS/docker/mod_xsendfile.c
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

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
};