-*- coding: utf-8 -*-
Changes with Apache 2.5.1
+ *) "[mod_dav_fs etag handling] should really honor the FileETag setting".
+ - It now does.
+ - Add "Digest" to FileETag directive, allowing a strong ETag to be
+ generated using a file digest.
+ - Add ap_make_etag_ex() and ap_set_etag_fd() to allow full control over
+ ETag generation.
+ - Add concept of "binary notes" to request_rec, allowing packed bit flags
+ to be added to a request.
+ - First binary note - AP_REQUEST_STRONG_ETAG - allows modules to force
+ the ETag to a strong ETag to comply with RFC requirements, such as those
+ mandated by various WebDAV extensions.
+ [Graham Leggett]
+
*) mod_ssl: Fix a race condition and possible crash when using a proxy client
certificate (SSLProxyMachineCertificateFile).
[Armin Abfalterer <a.abfalterer gmail.com>]
<highlight language="config">
FileETag INode MTime Size
</highlight></dd>
+ <dt><strong>Digest</strong></dt>
+ <dd>If a document is file-based, the <code>ETag</code> field will be
+ calculated by taking the digest over the file.</dd>
<dt><strong>None</strong></dt>
<dd>If a document is file-based, no <code>ETag</code> field will be
included in the response</dd>
</dl>
- <p>The <code>INode</code>, <code>MTime</code>, and <code>Size</code>
- keywords may be prefixed with either <code>+</code> or <code>-</code>,
- which allow changes to be made to the default setting inherited
- from a broader scope. Any keyword appearing without such a prefix
+ <p>The <code>INode</code>, <code>MTime</code>, <code>Size</code> and
+ <code>Digest</code> keywords may be prefixed with either <code>+</code>
+ or <code>-</code>, which allow changes to be made to the default setting
+ inherited from a broader scope. Any keyword appearing without such a prefix
immediately and completely cancels the inherited setting.</p>
<p>If a directory's configuration includes
the setting for that subdirectory (which will be inherited by
any sub-subdirectories that don't override it) will be equivalent to
<code>FileETag MTime Size</code>.</p>
- <note type="warning"><title>Warning</title>
- Do not change the default for directories or locations that have WebDAV
- enabled and use <module>mod_dav_fs</module> as a storage provider.
- <module>mod_dav_fs</module> uses <code>MTime Size</code>
- as a fixed format for <code>ETag</code> comparisons on conditional requests.
- These conditional requests will break if the <code>ETag</code> format is
- changed via <directive>FileETag</directive>.
- </note>
<note><title>Server Side Includes</title>
An ETag is not generated for responses parsed by <module>mod_include</module>
- since the response entity can change without a change of the INode, MTime, or Size
- of the static file with embedded SSI directives.
+ since the response entity can change without a change of the INode, MTime,
+ Size or Digest of the static file with embedded SSI directives.
</note>
</usage>
* 20200420.5 (2.5.1-dev) Add pre_translate_name hook
* 20200420.6 (2.5.1-dev) Add map_encoded_one and map_encoded_all bits to
* proxy_server_conf
+ * 20200420.7 (2.5.1-dev) Add struct etag_rec, ap_make_etag_ex(),
+ * ap_set_etag_fd(). Add typedef ap_request_bnotes_t,
+ * macros AP_REQUEST_GET_BNOTE, AP_REQUEST_SET_BNOTE,
+ * AP_REQUEST_STRONG_ETAG, AP_REQUEST_IS_STRONG_ETAG.
+ * Add bnotes to request_rec.
*/
#define MODULE_MAGIC_COOKIE 0x41503235UL /* "AP25" */
#ifndef MODULE_MAGIC_NUMBER_MAJOR
#define MODULE_MAGIC_NUMBER_MAJOR 20200420
#endif
-#define MODULE_MAGIC_NUMBER_MINOR 6 /* 0...n */
+#define MODULE_MAGIC_NUMBER_MINOR 7 /* 0...n */
/**
* Determine if the server's current MODULE_MAGIC_NUMBER is at least a
*/
typedef unsigned long etag_components_t;
-#define ETAG_UNSET 0
-#define ETAG_NONE (1 << 0)
-#define ETAG_MTIME (1 << 1)
-#define ETAG_INODE (1 << 2)
-#define ETAG_SIZE (1 << 3)
-#define ETAG_ALL (ETAG_MTIME | ETAG_INODE | ETAG_SIZE)
+#define ETAG_UNSET 0
+#define ETAG_NONE (1 << 0)
+#define ETAG_MTIME (1 << 1)
+#define ETAG_INODE (1 << 2)
+#define ETAG_SIZE (1 << 3)
+#define ETAG_DIGEST (1 << 4)
+#define ETAG_ALL (ETAG_MTIME | ETAG_INODE | ETAG_SIZE)
/* This is the default value used */
#define ETAG_BACKWARD (ETAG_MTIME | ETAG_SIZE)
*/
AP_DECLARE(void) ap_setup_make_content_type(apr_pool_t *pool);
+/** A structure with the ingredients for a file based etag */
+typedef struct etag_rec etag_rec;
+
+/**
+ * @brief A structure with the ingredients for a file based etag
+ */
+struct etag_rec {
+ /** Optional vary list validator */
+ const char *vlist_validator;
+ /** Time when the request started */
+ apr_time_t request_time;
+ /** finfo.protection (st_mode) set to zero if no such file */
+ apr_finfo_t *finfo;
+ /** File pathname used when generating a digest */
+ const char *pathname;
+ /** File descriptor used when generating a digest */
+ apr_file_t *fd;
+ /** Force a non-digest etag to be weak */
+ int force_weak;
+};
+
/**
* Construct an entity tag from the resource information. If it's a real
* file, build in some of the file characteristics.
*/
AP_DECLARE(char *) ap_make_etag(request_rec *r, int force_weak);
+/**
+ * Construct an entity tag from information provided in the etag_rec
+ * structure.
+ * @param r The current request
+ * @param er The etag record, containing ingredients for the etag.
+ */
+AP_DECLARE(char *) ap_make_etag_ex(request_rec *r, etag_rec *er);
+
/**
* Set the E-tag outgoing header
* @param r The current request
*/
AP_DECLARE(void) ap_set_etag(request_rec *r);
+/**
+ * Set the E-tag outgoing header, with the option of forcing a strong ETag.
+ * @param r The current request
+ * @param fd The file descriptor
+ */
+AP_DECLARE(void) ap_set_etag_fd(request_rec *r, apr_file_t *fd);
+
/**
* Set the last modified time for the file being sent
* @param r The current request
AP_DECLARE_HOOK(apr_port_t,default_port,(const request_rec *r))
-#define AP_PROTOCOL_HTTP1 "http/1.1"
+#define AP_PROTOCOL_HTTP1 "http/1.1"
/**
* Determine the list of protocols available for a connection/request. This may
*/
AP_DECLARE(void) ap_send_interim_response(request_rec *r, int send_headers);
+
#ifdef __cplusplus
}
#endif
* The method mask bit to shift for anding with a bitmask.
*/
#define AP_METHOD_BIT ((ap_method_mask_t)1)
-/** @} */
-
/** @see ap_method_list_t */
typedef struct ap_method_list_t ap_method_list_t;
/** the array used for extension methods */
apr_array_header_t *method_list;
};
+/** @} */
+
+/**
+ * @defgroup bnotes Binary notes recognized by the server
+ * @ingroup APACHE_CORE_DAEMON
+ * @{
+ *
+ * @brief Binary notes recognized by the server.
+ */
+
+/**
+ * The type used for request binary notes.
+ */
+typedef apr_uint64_t ap_request_bnotes_t;
+
+/**
+ * These constants represent bitmasks for notes associated with this
+ * request. There are space for 64 bits in the apr_uint64_t.
+ *
+ */
+#define AP_REQUEST_STRONG_ETAG 1 >> 0
+
+/**
+ * This is a convenience macro to ease with getting specific request
+ * binary notes.
+ */
+#define AP_REQUEST_GET_BNOTE(r, mask) \
+ ((mask) & ((r)->bnotes))
+
+/**
+ * This is a convenience macro to ease with setting specific request
+ * binary notes.
+ */
+#define AP_REQUEST_SET_BNOTE(r, mask, val) \
+ (r)->bnotes = (((r)->bnotes & ~(mask)) | (val))
+
+/**
+ * Returns true if the strong etag flag is set for this request.
+ */
+#define AP_REQUEST_IS_STRONG_ETAG(r) \
+ AP_REQUEST_GET_BNOTE((r), AP_REQUEST_STRONG_ETAG)
+/** @} */
+
/**
* @defgroup module_magic Module Magic mime types
* TODO: compact elsewhere
*/
unsigned int flushed:1;
+ /** Request flags associated with this request. Use
+ * AP_REQUEST_GET_FLAGS() and AP_REQUEST_SET_FLAGS() to access
+ * the elements of this field.
+ */
+ ap_request_bnotes_t bnotes;
};
/**
return dav_fs_internal_walk(params, depth, 0, NULL, response);
}
-/* dav_fs_etag: Stolen from ap_make_etag. Creates a strong etag
- * for file path.
- * ### do we need to return weak tags sometimes?
+/* dav_fs_etag: Creates an etag for the file path.
*/
static const char *dav_fs_getetag(const dav_resource *resource)
{
- dav_resource_private *ctx = resource->info;
- /* XXX: This should really honor the FileETag setting */
+ etag_rec er;
- if (!resource->exists)
- return apr_pstrdup(ctx->pool, "");
+ dav_resource_private *ctx = resource->info;
- if (ctx->finfo.filetype != APR_NOFILE) {
- return apr_psprintf(ctx->pool, "\"%" APR_UINT64_T_HEX_FMT "-%"
- APR_UINT64_T_HEX_FMT "\"",
- (apr_uint64_t) ctx->finfo.size,
- (apr_uint64_t) ctx->finfo.mtime);
+ if (!resource->exists) {
+ return "";
}
- return apr_psprintf(ctx->pool, "\"%" APR_UINT64_T_HEX_FMT "\"",
- (apr_uint64_t) ctx->finfo.mtime);
+ er.vlist_validator = NULL;
+ er.request_time = ctx->r->request_time;
+ er.finfo = &ctx->finfo;
+ er.pathname = ctx->pathname;
+ er.fd = NULL;
+ er.force_weak = 0;
+
+ return ap_make_etag_ex(ctx->r, &er);
}
static const dav_hooks_repository dav_hooks_repository_fs =
/* copied from default handler: */
ap_update_mtime(r, r->finfo.mtime);
ap_set_last_modified(r);
- ap_set_etag(r);
+ ap_set_etag_fd(r, fd);
ap_set_accept_ranges(r);
ap_set_content_length(r, r->finfo.size);
else if (ap_cstr_casecmp(token, "INode") == 0) {
bit = ETAG_INODE;
}
+ else if (ap_cstr_casecmp(token, "Digest") == 0) {
+ bit = ETAG_DIGEST;
+ }
else {
return apr_pstrcat(cmd->pool, "Unknown keyword '",
token, "' for ", cmd->cmd->name,
ap_update_mtime(r, r->finfo.mtime);
ap_set_last_modified(r);
- ap_set_etag(r);
+ ap_set_etag_fd(r, fd);
ap_set_accept_ranges(r);
ap_set_content_length(r, r->finfo.size);
if (bld_content_md5) {
#include "apr_strings.h"
#include "apr_thread_proc.h" /* for RLIMIT stuff */
+#include "apr_sha1.h"
+#include "apr_base64.h"
#define APR_WANT_STRFUNC
#include "apr_want.h"
#define ETAG_WEAK "W/"
#define CHARS_PER_UINT64 (sizeof(apr_uint64_t) * 2)
+
+static void etag_start(char *etag, const char *weak, char **next)
+{
+ if (weak) {
+ while (*weak) {
+ *etag++ = *weak++;
+ }
+ }
+ *etag++ = '"';
+
+ *next = etag;
+}
+
+static void etag_end(char *next, const char *vlv, apr_size_t vlv_len)
+{
+ if (vlv) {
+ *next++ = ';';
+ apr_cpystrn(next, vlv, vlv_len);
+ }
+ else {
+ *next++ = '"';
+ *next = '\0';
+ }
+}
+
/*
* Construct an entity tag (ETag) from resource information. If it's a real
* file, build in some of the file characteristics. If the modification time
* is newer than (request-time minus 1 second), mark the ETag as weak - it
- * could be modified again in as short an interval. We rationalize the
- * modification time we're given to keep it from being in the future.
+ * could be modified again in as short an interval.
*/
-AP_DECLARE(char *) ap_make_etag(request_rec *r, int force_weak)
+AP_DECLARE(char *) ap_make_etag_ex(request_rec *r, etag_rec *er)
{
- char *weak;
- apr_size_t weak_len;
- char *etag;
+ char *weak = NULL;
+ apr_size_t weak_len = 0, vlv_len = 0;
+ char *etag, *vlv;
char *next;
core_dir_config *cfg;
etag_components_t etag_bits;
cfg = (core_dir_config *)ap_get_core_module_config(r->per_dir_config);
etag_bits = (cfg->etag_bits & (~ cfg->etag_remove)) | cfg->etag_add;
+ if (er->force_weak) {
+ weak = ETAG_WEAK;
+ weak_len = sizeof(ETAG_WEAK);
+ }
+
+ if (r->vlist_validator) {
+
+ /* If we have a variant list validator (vlv) due to the
+ * response being negotiated, then we create a structured
+ * entity tag which merges the variant etag with the variant
+ * list validator (vlv). This merging makes revalidation
+ * somewhat safer, ensures that caches which can deal with
+ * Vary will (eventually) be updated if the set of variants is
+ * changed, and is also a protocol requirement for transparent
+ * content negotiation.
+ */
+
+ /* if the variant list validator is weak, we make the whole
+ * structured etag weak. If we would not, then clients could
+ * have problems merging range responses if we have different
+ * variants with the same non-globally-unique strong etag.
+ */
+
+ vlv = r->vlist_validator;
+ if (vlv[0] == 'W') {
+ vlv += 3;
+ weak = ETAG_WEAK;
+ weak_len = sizeof(ETAG_WEAK);
+ }
+ else {
+ vlv++;
+ }
+ vlv_len = strlen(vlv);
+
+ }
+ else {
+ vlv = NULL;
+ vlv_len = 0;
+ }
+
+ /*
+ * Did a module flag the need for a strong etag, or did the
+ * configuration tell us to generate a digest?
+ */
+ if (er->finfo->filetype == APR_REG &&
+ (AP_REQUEST_IS_STRONG_ETAG(r) || (etag_bits & ETAG_DIGEST))) {
+
+ apr_sha1_ctx_t context;
+ unsigned char buf[4096]; /* keep this a multiple of 64 */
+ unsigned char digest[APR_SHA1_DIGESTSIZE];
+ apr_file_t *fd = NULL;
+
+ apr_size_t nbytes;
+ apr_off_t offset = 0L;
+
+ if (er->fd) {
+ fd = er->fd;
+ }
+ else if (er->pathname) {
+ apr_file_open(&fd, er->pathname, APR_READ | APR_BINARY, 0, r->pool);
+ }
+ if (!fd) {
+ return "";
+ }
+
+ etag = apr_palloc(r->pool, weak_len + sizeof("\"\"") +
+ 4*(APR_SHA1_DIGESTSIZE/3) + vlv_len + 4);
+
+ apr_sha1_init(&context);
+ nbytes = sizeof(buf);
+ while (apr_file_read(fd, buf, &nbytes) == APR_SUCCESS) {
+ apr_sha1_update_binary(&context, buf, nbytes);
+ nbytes = sizeof(buf);
+ }
+ apr_file_seek(fd, APR_SET, &offset);
+ apr_sha1_final(digest, &context);
+
+ etag_start(etag, weak, &next);
+ next += apr_base64_encode_binary(next, digest, APR_SHA1_DIGESTSIZE) - 1;
+ etag_end(next, vlv, vlv_len);
+
+ return etag;
+ }
+
/*
* If it's a file (or we wouldn't be here) and no ETags
* should be set for files, return an empty string and
* note it for the header-sender to ignore.
*/
if (etag_bits & ETAG_NONE) {
- apr_table_setn(r->notes, "no-etag", "omit");
return "";
}
* be modified again later in the second, and the validation
* would be incorrect.
*/
- if ((r->request_time - r->mtime > (1 * APR_USEC_PER_SEC)) &&
- !force_weak) {
- weak = NULL;
- weak_len = 0;
- }
- else {
+ if ((er->request_time - er->finfo->mtime < (1 * APR_USEC_PER_SEC))) {
weak = ETAG_WEAK;
weak_len = sizeof(ETAG_WEAK);
}
- if (r->finfo.filetype != APR_NOFILE) {
+ if (er->finfo->filetype != APR_NOFILE) {
/*
* ETag gets set to [W/]"inode-size-mtime", modulo any
* FileETag keywords.
*/
etag = apr_palloc(r->pool, weak_len + sizeof("\"--\"") +
- 3 * CHARS_PER_UINT64 + 1);
- next = etag;
- if (weak) {
- while (*weak) {
- *next++ = *weak++;
- }
- }
- *next++ = '"';
+ 3 * CHARS_PER_UINT64 + vlv_len + 2);
+
+ etag_start(etag, weak, &next);
+
bits_added = 0;
if (etag_bits & ETAG_INODE) {
- next = etag_uint64_to_hex(next, r->finfo.inode);
+ next = etag_uint64_to_hex(next, er->finfo->inode);
bits_added |= ETAG_INODE;
}
if (etag_bits & ETAG_SIZE) {
if (bits_added != 0) {
*next++ = '-';
}
- next = etag_uint64_to_hex(next, r->finfo.size);
+ next = etag_uint64_to_hex(next, er->finfo->size);
bits_added |= ETAG_SIZE;
}
if (etag_bits & ETAG_MTIME) {
if (bits_added != 0) {
*next++ = '-';
}
- next = etag_uint64_to_hex(next, r->mtime);
+ next = etag_uint64_to_hex(next, er->finfo->mtime);
}
- *next++ = '"';
- *next = '\0';
+
+ etag_end(next, vlv, vlv_len);
+
}
else {
/*
* Not a file document, so just use the mtime: [W/]"mtime"
*/
etag = apr_palloc(r->pool, weak_len + sizeof("\"\"") +
- CHARS_PER_UINT64 + 1);
- next = etag;
- if (weak) {
- while (*weak) {
- *next++ = *weak++;
- }
- }
- *next++ = '"';
- next = etag_uint64_to_hex(next, r->mtime);
- *next++ = '"';
- *next = '\0';
+ CHARS_PER_UINT64 + vlv_len + 2);
+
+ etag_start(etag, weak, &next);
+ next = etag_uint64_to_hex(next, er->finfo->mtime);
+ etag_end(next, vlv, vlv_len);
+
}
return etag;
}
+AP_DECLARE(char *) ap_make_etag(request_rec *r, int force_weak)
+{
+ etag_rec er;
+
+ er.vlist_validator = NULL;
+ er.request_time = r->request_time;
+ er.finfo = &r->finfo;
+ er.pathname = r->filename;
+ er.fd = NULL;
+ er.force_weak = force_weak;
+
+ return ap_make_etag_ex(r, &er);
+}
+
AP_DECLARE(void) ap_set_etag(request_rec *r)
{
char *etag;
- char *variant_etag, *vlv;
- int vlv_weak;
- if (!r->vlist_validator) {
- etag = ap_make_etag(r, 0);
+ etag_rec er;
- /* If we get a blank etag back, don't set the header. */
- if (!etag[0]) {
- return;
- }
+ er.vlist_validator = r->vlist_validator;
+ er.request_time = r->request_time;
+ er.finfo = &r->finfo;
+ er.pathname = r->filename;
+ er.fd = NULL;
+ er.force_weak = 0;
+
+ etag = ap_make_etag_ex(r, &er);
+
+ if (etag && etag[0]) {
+ apr_table_setn(r->headers_out, "ETag", etag);
}
else {
- /* If we have a variant list validator (vlv) due to the
- * response being negotiated, then we create a structured
- * entity tag which merges the variant etag with the variant
- * list validator (vlv). This merging makes revalidation
- * somewhat safer, ensures that caches which can deal with
- * Vary will (eventually) be updated if the set of variants is
- * changed, and is also a protocol requirement for transparent
- * content negotiation.
- */
+ apr_table_setn(r->notes, "no-etag", "omit");
+ }
- /* if the variant list validator is weak, we make the whole
- * structured etag weak. If we would not, then clients could
- * have problems merging range responses if we have different
- * variants with the same non-globally-unique strong etag.
- */
+}
- vlv = r->vlist_validator;
- vlv_weak = (vlv[0] == 'W');
+AP_DECLARE(void) ap_set_etag_fd(request_rec *r, apr_file_t *fd)
+{
+ char *etag;
- variant_etag = ap_make_etag(r, vlv_weak);
+ etag_rec er;
- /* If we get a blank etag back, don't append vlv and stop now. */
- if (!variant_etag[0]) {
- return;
- }
+ er.vlist_validator = r->vlist_validator;
+ er.request_time = r->request_time;
+ er.finfo = &r->finfo;
+ er.pathname = NULL;
+ er.fd = fd;
+ er.force_weak = 0;
- /* merge variant_etag and vlv into a structured etag */
- variant_etag[strlen(variant_etag) - 1] = '\0';
- if (vlv_weak) {
- vlv += 3;
- }
- else {
- vlv++;
- }
- etag = apr_pstrcat(r->pool, variant_etag, ";", vlv, NULL);
+ etag = ap_make_etag_ex(r, &er);
+
+ if (etag && etag[0]) {
+ apr_table_setn(r->headers_out, "ETag", etag);
+ }
+ else {
+ apr_table_setn(r->notes, "no-etag", "omit");
}
- apr_table_setn(r->headers_out, "ETag", etag);
}