hi all this is something I've been meaning to do for a while. as mod_include demonstrates, output filters will sometimes be required to remove the ETag header generated by content handlers, depending on how much they alter the content. the current methodology is to set a "no-etag" note, which is recognized by the header filter.
what I wanted to do was expand this a bit and offer an ETag weakening API - it should be easy to see that while some (most) output filters alter content dramatically, others might only alter content bitwise but not semantically. in these cases a weak etag is acceptable (and probably preferred to no etag at all, from my reading of the specs). under the current state of affairs, there is no way for an output filter to intercept ETag generation other than to specify its removal. the attached patch (etag_api.patch) offers two new functions. ap_weaken_etag() guarantees a weak ETag header will be generated if one is generated at all. the decision to generate one is still given to ap_set_etag(). ap_supress_etag() is just a formalization of the "no-etag" idiom into a real API. fwiw, I've also included patches against mod_case_filter and the perl-framework, which is what I used for my testing (so you can try it yourself). --Geoff
Index: include/http_protocol.h =================================================================== RCS file: /home/cvspublic/httpd-2.0/include/http_protocol.h,v retrieving revision 1.84 diff -u -r1.84 http_protocol.h --- include/http_protocol.h 3 Feb 2003 17:52:53 -0000 1.84 +++ include/http_protocol.h 12 Dec 2003 16:57:17 -0000 @@ -196,11 +196,25 @@ AP_DECLARE(char *) ap_make_etag(request_rec *r, int force_weak); /** - * Set the E-tag outgoing header + * Set the ETag outgoing header * @param The current request * @deffunc void ap_set_etag(request_rec *r) */ AP_DECLARE(void) ap_set_etag(request_rec *r); + +/** + * Supress the ETag outgoing header + * @param The current request + * @deffunc void ap_supress_etag(request_rec *r) + */ +AP_DECLARE(void) ap_supress_etag(request_rec *r); + +/** + * Force generation of a weak ETag header + * @param The current request + * @deffunc void ap_weaken_etag(request_rec *r) + */ +AP_DECLARE(void) ap_weaken_etag(request_rec *r); /** * Set the last modified time for the file being sent Index: modules/filters/mod_include.c =================================================================== RCS file: /home/cvspublic/httpd-2.0/modules/filters/mod_include.c,v retrieving revision 1.292 diff -u -r1.292 mod_include.c --- modules/filters/mod_include.c 10 Dec 2003 02:30:20 -0000 1.292 +++ modules/filters/mod_include.c 12 Dec 2003 16:57:33 -0000 @@ -3584,7 +3584,7 @@ * We don't know if we are going to be including a file or executing * a program - in either case a strong ETag header will likely be invalid. */ - apr_table_setn(f->r->notes, "no-etag", ""); + ap_supress_etag(f->r); return OK; } Index: modules/http/http_protocol.c =================================================================== RCS file: /home/cvspublic/httpd-2.0/modules/http/http_protocol.c,v retrieving revision 1.473 diff -u -r1.473 http_protocol.c --- modules/http/http_protocol.c 16 Nov 2003 02:09:13 -0000 1.473 +++ modules/http/http_protocol.c 12 Dec 2003 16:57:37 -0000 @@ -2693,7 +2693,7 @@ * note it for the header-sender to ignore. */ if (etag_bits & ETAG_NONE) { - apr_table_setn(r->notes, "no-etag", "omit"); + ap_supress_etag(r); return ""; } @@ -2711,7 +2711,13 @@ * we send a weak tag instead of a strong one, since it could * be modified again later in the second, and the validation * would be incorrect. + * + * additional criteria for a weak tag is if force_weak is true + * or ap_weaken_etag() was called */ + + force_weak = (force_weak || apr_table_get(r->notes, "weak-etag")); + if ((r->request_time - r->mtime > (1 * APR_USEC_PER_SEC)) && !force_weak) { weak = NULL; @@ -2831,6 +2837,37 @@ } apr_table_setn(r->headers_out, "ETag", etag); +} + +/* + * Take steps to ensure that current and future-generated ETag + * headers are marked as weak. + */ +AP_DECLARE(void) ap_weaken_etag(request_rec *r) +{ + const char *etag = apr_table_get(r->headers_out, "ETag"); + + /* if the ETag header exists and is not already marked + * as weak, mark it now + */ + if ((etag = apr_table_get(r->headers_out, "ETag")) && + strncmp(etag, ETAG_WEAK, strlen(ETAG_WEAK))) { + + etag = apr_pstrcat(r->pool, ETAG_WEAK, etag, NULL); + + apr_table_setn(r->headers_out, "ETag", etag); + } + + /* make sure future calls to ap_make_etag() produce weak tags */ + apr_table_setn(r->notes, "weak-etag", "yes"); +} + +/* + * Formalize ETag header supression. + */ +AP_DECLARE(void) ap_supress_etag(request_rec *r) +{ + apr_table_setn(r->notes, "no-etag", "omit"); } static int parse_byterange(char *range, apr_off_t clength,
Index: modules/experimental/mod_case_filter.c =================================================================== RCS file: /home/cvspublic/httpd-2.0/modules/experimental/mod_case_filter.c,v retrieving revision 1.16 diff -u -r1.16 mod_case_filter.c --- modules/experimental/mod_case_filter.c 16 Nov 2003 02:09:13 -0000 1.16 +++ modules/experimental/mod_case_filter.c 12 Dec 2003 16:57:28 -0000 @@ -59,6 +59,7 @@ #include "apr_lib.h" #include "util_filter.h" #include "http_request.h" +#include "http_protocol.h" #include <ctype.h> @@ -98,6 +99,19 @@ apr_bucket *pbktIn; apr_bucket_brigade *pbbOut; + if(f->r->args && strcmp(f->r->args, "weaken") == 0) { + ap_weaken_etag(f->r); + } + if(f->r->args && strcmp(f->r->args, "weaken-multi") == 0) { + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + } + if(f->r->args && strcmp(f->r->args, "supress") == 0) { + ap_supress_etag(f->r); + } + pbbOut=apr_brigade_create(r->pool, c->bucket_alloc); for (pbktIn = APR_BRIGADE_FIRST(pbbIn); pbktIn != APR_BRIGADE_SENTINEL(pbbIn); @@ -133,6 +147,24 @@ return ap_pass_brigade(f->next,pbbOut); } +static apr_status_t CaseFilterInit(ap_filter_t *f) +{ + if(f->r->args && strcmp(f->r->args, "weaken-init") == 0) { + ap_weaken_etag(f->r); + } + if(f->r->args && strcmp(f->r->args, "weaken-init-multi") == 0) { + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + ap_weaken_etag(f->r); + } + if(f->r->args && strcmp(f->r->args, "supress-init") == 0) { + ap_supress_etag(f->r); + } + + return OK; +} + static const char *CaseFilterEnable(cmd_parms *cmd, void *dummy, int arg) { CaseFilterConfig *pConfig=ap_get_module_config(cmd->server->module_config, @@ -152,8 +184,8 @@ static void CaseFilterRegisterHooks(apr_pool_t *p) { ap_hook_insert_filter(CaseFilterInsertFilter,NULL,NULL,APR_HOOK_MIDDLE); - ap_register_output_filter(s_szCaseFilterName,CaseFilterOutFilter,NULL, - AP_FTYPE_RESOURCE); + ap_register_output_filter(s_szCaseFilterName,CaseFilterOutFilter, + CaseFilterInit,AP_FTYPE_RESOURCE); } module AP_MODULE_DECLARE_DATA case_filter_module =
Index: t/conf/extra.conf.in =================================================================== RCS file: /home/cvspublic/httpd-test/perl-framework/t/conf/extra.conf.in,v retrieving revision 1.43 diff -u -r1.43 extra.conf.in --- t/conf/extra.conf.in 13 Aug 2003 01:28:47 -0000 1.43 +++ t/conf/extra.conf.in 12 Dec 2003 17:08:07 -0000 @@ -318,6 +318,11 @@ Options +Indexes AllowOverride All </Directory> + <Directory @SERVERROOT@/htdocs/etags> + Options +Indexes + IndexOptions +TrackModified + AllowOverride All + </Directory> </IfModule> ## --- /dev/null 2003-01-30 05:24:37.000000000 -0500 +++ t/apache/etagapi.t 2003-12-12 12:03:50.000000000 -0500 @@ -0,0 +1,55 @@ +use strict; +use warnings FATAL => 'all'; + +use Apache::Test; +use Apache::TestRequest; +use Apache::TestUtil; + +my %tests = ( + '/index.html' => qr!^"!, + '/index.html?weaken' => qr!^W/"!, + '/index.html?weaken-multi' => qr!^W/"!, + '/index.html?weaken-init' => qr!^W/"!, + '/index.html?weaken-init-multi' => qr!^W/"!, + '/index.html?supress' => undef, + '/index.html?supress-init' => undef, + '/etags/' => qr!^"!, + '/etags/?weaken' => qr!^W/"!, + '/etags/?weaken-multi' => qr!^W/"!, + '/etags/?weaken-init' => qr!^W/"!, + '/etags/?weaken-init-multi' => qr!^W/"!, + '/etags/?supress' => undef, + '/etags/?supress-init' => undef, + '/modules/cgi/perl.pl' => undef, + '/modules/cgi/perl.pl' => undef, + '/modules/cgi/perl.pl?weaken' => undef, + '/modules/cgi/perl.pl?weaken-multi' => undef, + '/modules/cgi/perl.pl?weaken-init' => undef, + '/modules/cgi/perl.pl?weaken-init-multi' => undef, + '/modules/cgi/perl.pl?supress' => undef, + '/modules/cgi/perl.pl?supress-init' => undef, +); + +plan tests => (scalar keys %tests) * 3, (have_min_apache_version(2.1) && + have_module('case_filter') && + have_module('autoindex') && + have_cgi); + +foreach my $uri (sort keys %tests) { + + my $response = GET $uri, 'X-AddOutputFilter' => 'CaseFilter'; + + ok t_cmp(200, + $response->code, + "GET $uri status code"); + + ok t_cmp(qr/[^a-z]/, + $response->content, + "GET $uri body all uppercase"); + + my $etag = $response->header('ETag'); + + ok t_cmp($tests{$uri}, + $etag, + "GET $uri ETag header"); +}