]> granicus.if.org Git - apache/blob - modules/metadata/mod_expires.c
Fix alignment in a <highlight> block.
[apache] / modules / metadata / mod_expires.c
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * mod_expires.c
19  * version 0.0.11
20  * status beta
21  *
22  * Andrew Wilson <Andrew.Wilson@cm.cf.ac.uk> 26.Jan.96
23  *
24  * This module allows you to control the form of the Expires: header
25  * that Apache issues for each access.  Directives can appear in
26  * configuration files or in .htaccess files so expiry semantics can
27  * be defined on a per-directory basis.
28  *
29  * DIRECTIVE SYNTAX
30  *
31  * Valid directives are:
32  *
33  *     ExpiresActive on | off
34  *     ExpiresDefault <code><seconds>
35  *     ExpiresByType type/encoding <code><seconds>
36  *
37  * Valid values for <code> are:
38  *
39  *     'M'      expires header shows file modification date + <seconds>
40  *     'A'      expires header shows access time + <seconds>
41  *
42  *              [I'm not sure which of these is best under different
43  *              circumstances, I guess it's for other people to explore.
44  *              The effects may be indistinguishable for a number of cases]
45  *
46  * <seconds> should be an integer value [acceptable to atoi()]
47  *
48  * There is NO space between the <code> and <seconds>.
49  *
50  * For example, a directory which contains information which changes
51  * frequently might contain:
52  *
53  *     # reports generated by cron every hour.  don't let caches
54  *     # hold onto stale information
55  *     ExpiresDefault M3600
56  *
57  * Another example, our html pages can change all the time, the gifs
58  * tend not to change often:
59  *
60  *     # pages are hot (1 week), images are cold (1 month)
61  *     ExpiresByType text/html A604800
62  *     ExpiresByType image/gif A2592000
63  *
64  * Expires can be turned on for all URLs on the server by placing the
65  * following directive in a conf file:
66  *
67  *     ExpiresActive on
68  *
69  * ExpiresActive can also appear in .htaccess files, enabling the
70  * behaviour to be turned on or off for each chosen directory.
71  *
72  *     # turn off Expires behaviour in this directory
73  *     # and subdirectories
74  *     ExpiresActive off
75  *
76  * Directives defined for a directory are valid in subdirectories
77  * unless explicitly overridden by new directives in the subdirectory
78  * .htaccess files.
79  *
80  * ALTERNATIVE DIRECTIVE SYNTAX
81  *
82  * Directives can also be defined in a more readable syntax of the form:
83  *
84  *     ExpiresDefault "<base> [plus] {<num> <type>}*"
85  *     ExpiresByType type/encoding "<base> [plus] {<num> <type>}*"
86  *
87  * where <base> is one of:
88  *      access
89  *      now             equivalent to 'access'
90  *      modification
91  *
92  * where the 'plus' keyword is optional
93  *
94  * where <num> should be an integer value [acceptable to atoi()]
95  *
96  * where <type> is one of:
97  *      years
98  *      months
99  *      weeks
100  *      days
101  *      hours
102  *      minutes
103  *      seconds
104  *
105  * For example, any of the following directives can be used to make
106  * documents expire 1 month after being accessed, by default:
107  *
108  *      ExpiresDefault "access plus 1 month"
109  *      ExpiresDefault "access plus 4 weeks"
110  *      ExpiresDefault "access plus 30 days"
111  *
112  * The expiry time can be fine-tuned by adding several '<num> <type>'
113  * clauses:
114  *
115  *      ExpiresByType text/html "access plus 1 month 15 days 2 hours"
116  *      ExpiresByType image/gif "modification plus 5 hours 3 minutes"
117  *
118  * ---
119  *
120  * Change-log:
121  * 29.Jan.96    Hardened the add_* functions.  Server will now bail out
122  *              if bad directives are given in the conf files.
123  * 02.Feb.96    Returns DECLINED if not 'ExpiresActive on', giving other
124  *              expires-aware modules a chance to play with the same
125  *              directives. [Michael Rutman]
126  * 03.Feb.96    Call tzset() before localtime().  Trying to get the module
127  *              to work properly in non GMT timezones.
128  * 12.Feb.96    Modified directive syntax to allow more readable commands:
129  *                ExpiresDefault "now plus 10 days 20 seconds"
130  *                ExpiresDefault "access plus 30 days"
131  *                ExpiresDefault "modification plus 1 year 10 months 30 days"
132  * 13.Feb.96    Fix call to table_get() with NULL 2nd parameter [Rob Hartill]
133  * 19.Feb.96    Call gm_timestr_822() to get time formatted correctly, can't
134  *              rely on presence of HTTP_TIME_FORMAT in Apache 1.1+.
135  * 21.Feb.96    This version (0.0.9) reverses assumptions made in 0.0.8
136  *              about star/star handlers.  Reverting to 0.0.7 behaviour.
137  * 08.Jun.96    allows ExpiresDefault to be used with responses that use
138  *              the DefaultType by not DECLINING, but instead skipping
139  *              the table_get check and then looking for an ExpiresDefault.
140  *              [Rob Hartill]
141  * 04.Nov.96    'const' definitions added.
142  *
143  * TODO
144  * add support for Cache-Control: max-age=20 from the HTTP/1.1
145  * proposal (in this case, a ttl of 20 seconds) [ask roy]
146  * add per-file expiry and explicit expiry times - duplicates some
147  * of the mod_cern_meta.c functionality.  eg:
148  *              ExpiresExplicit index.html "modification plus 30 days"
149  *
150  * BUGS
151  * Hi, welcome to the internet.
152  */
153
154 #include "apr.h"
155 #include "apr_strings.h"
156 #include "apr_lib.h"
157
158 #define APR_WANT_STRFUNC
159 #include "apr_want.h"
160
161 #include "ap_config.h"
162 #include "httpd.h"
163 #include "http_config.h"
164 #include "http_log.h"
165 #include "http_request.h"
166 #include "http_protocol.h"
167
168 typedef struct {
169     int active;
170     int wildcards;
171     char *expiresdefault;
172     apr_table_t *expiresbytype;
173 } expires_dir_config;
174
175 /* from mod_dir, why is this alias used?
176  */
177 #define DIR_CMD_PERMS OR_INDEXES
178
179 #define ACTIVE_ON       1
180 #define ACTIVE_OFF      0
181 #define ACTIVE_DONTCARE 2
182
183 module AP_MODULE_DECLARE_DATA expires_module;
184
185 static void *create_dir_expires_config(apr_pool_t *p, char *dummy)
186 {
187     expires_dir_config *new =
188     (expires_dir_config *) apr_pcalloc(p, sizeof(expires_dir_config));
189     new->active = ACTIVE_DONTCARE;
190     new->wildcards = 0;
191     new->expiresdefault = NULL;
192     new->expiresbytype = apr_table_make(p, 4);
193     return (void *) new;
194 }
195
196 static const char *set_expiresactive(cmd_parms *cmd, void *in_dir_config, int arg)
197 {
198     expires_dir_config *dir_config = in_dir_config;
199
200     /* if we're here at all it's because someone explicitly
201      * set the active flag
202      */
203     dir_config->active = ACTIVE_ON;
204     if (arg == 0) {
205         dir_config->active = ACTIVE_OFF;
206     }
207     return NULL;
208 }
209
210 /* check_code() parse 'code' and return NULL or an error response
211  * string.  If we return NULL then real_code contains code converted
212  * to the cnnnn format.
213  */
214 static char *check_code(apr_pool_t *p, const char *code, char **real_code)
215 {
216     char *word;
217     char base = 'X';
218     int modifier = 0;
219     int num = 0;
220     int factor;
221
222     /* 0.0.4 compatibility?
223      */
224     if ((code[0] == 'A') || (code[0] == 'M')) {
225         *real_code = (char *)code;
226         return NULL;
227     }
228
229     /* <base> [plus] {<num> <type>}*
230      */
231
232     /* <base>
233      */
234     word = ap_getword_conf(p, &code);
235     if (!strncasecmp(word, "now", 1) ||
236         !strncasecmp(word, "access", 1)) {
237         base = 'A';
238     }
239     else if (!strncasecmp(word, "modification", 1)) {
240         base = 'M';
241     }
242     else {
243         return apr_pstrcat(p, "bad expires code, unrecognised <base> '",
244                        word, "'", NULL);
245     }
246
247     /* [plus]
248      */
249     word = ap_getword_conf(p, &code);
250     if (!strncasecmp(word, "plus", 1)) {
251         word = ap_getword_conf(p, &code);
252     }
253
254     /* {<num> <type>}*
255      */
256     while (word[0]) {
257         /* <num>
258          */
259         if (apr_isdigit(word[0])) {
260             num = atoi(word);
261         }
262         else {
263             return apr_pstrcat(p, "bad expires code, numeric value expected <num> '",
264                            word, "'", NULL);
265         }
266
267         /* <type>
268          */
269         word = ap_getword_conf(p, &code);
270         if (word[0] == '\0') {
271             return apr_pstrcat(p, "bad expires code, missing <type>", NULL);
272         }
273
274         if (!strncasecmp(word, "years", 1)) {
275             factor = 60 * 60 * 24 * 365;
276         }
277         else if (!strncasecmp(word, "months", 2)) {
278             factor = 60 * 60 * 24 * 30;
279         }
280         else if (!strncasecmp(word, "weeks", 1)) {
281             factor = 60 * 60 * 24 * 7;
282         }
283         else if (!strncasecmp(word, "days", 1)) {
284             factor = 60 * 60 * 24;
285         }
286         else if (!strncasecmp(word, "hours", 1)) {
287             factor = 60 * 60;
288         }
289         else if (!strncasecmp(word, "minutes", 2)) {
290             factor = 60;
291         }
292         else if (!strncasecmp(word, "seconds", 1)) {
293             factor = 1;
294         }
295         else {
296             return apr_pstrcat(p, "bad expires code, unrecognised <type>",
297                            "'", word, "'", NULL);
298         }
299
300         modifier = modifier + factor * num;
301
302         /* next <num>
303          */
304         word = ap_getword_conf(p, &code);
305     }
306
307     *real_code = apr_psprintf(p, "%c%d", base, modifier);
308
309     return NULL;
310 }
311
312 static const char *set_expiresbytype(cmd_parms *cmd, void *in_dir_config,
313                                      const char *mime, const char *code)
314 {
315     expires_dir_config *dir_config = in_dir_config;
316     char *response, *real_code;
317     const char *check;
318
319     check = ap_strrchr_c(mime, '/');
320     if (check == NULL) {
321         return "Invalid mimetype: should contain a slash";
322     }
323     if ((strlen(++check) == 1) && (*check == '*')) {
324         dir_config->wildcards = 1;
325     }
326
327     if ((response = check_code(cmd->pool, code, &real_code)) == NULL) {
328         apr_table_setn(dir_config->expiresbytype, mime, real_code);
329         return NULL;
330     }
331     return apr_pstrcat(cmd->pool,
332                  "'ExpiresByType ", mime, " ", code, "': ", response, NULL);
333 }
334
335 static const char *set_expiresdefault(cmd_parms *cmd, void *in_dir_config,
336                                       const char *code)
337 {
338     expires_dir_config * dir_config = in_dir_config;
339     char *response, *real_code;
340
341     if ((response = check_code(cmd->pool, code, &real_code)) == NULL) {
342         dir_config->expiresdefault = real_code;
343         return NULL;
344     }
345     return apr_pstrcat(cmd->pool,
346                    "'ExpiresDefault ", code, "': ", response, NULL);
347 }
348
349 static const command_rec expires_cmds[] =
350 {
351     AP_INIT_FLAG("ExpiresActive", set_expiresactive, NULL, DIR_CMD_PERMS,
352                  "Limited to 'on' or 'off'"),
353     AP_INIT_TAKE2("ExpiresByType", set_expiresbytype, NULL, DIR_CMD_PERMS,
354                   "a MIME type followed by an expiry date code"),
355     AP_INIT_TAKE1("ExpiresDefault", set_expiresdefault, NULL, DIR_CMD_PERMS,
356                   "an expiry date code"),
357     {NULL}
358 };
359
360 static void *merge_expires_dir_configs(apr_pool_t *p, void *basev, void *addv)
361 {
362     expires_dir_config *new = (expires_dir_config *) apr_pcalloc(p, sizeof(expires_dir_config));
363     expires_dir_config *base = (expires_dir_config *) basev;
364     expires_dir_config *add = (expires_dir_config *) addv;
365
366     if (add->active == ACTIVE_DONTCARE) {
367         new->active = base->active;
368     }
369     else {
370         new->active = add->active;
371     }
372
373     if (add->expiresdefault != NULL) {
374         new->expiresdefault = add->expiresdefault;
375     }
376     else {
377         new->expiresdefault = base->expiresdefault;
378     }
379     new->wildcards = add->wildcards;
380     new->expiresbytype = apr_table_overlay(p, add->expiresbytype,
381                                         base->expiresbytype);
382     return new;
383 }
384
385 /*
386  * Handle the setting of the expiration response header fields according
387  * to our criteria.
388  */
389
390 static int set_expiration_fields(request_rec *r, const char *code,
391                                  apr_table_t *t)
392 {
393     apr_time_t base;
394     apr_time_t additional;
395     apr_time_t expires;
396     int additional_sec;
397     char *timestr;
398
399     switch (code[0]) {
400     case 'M':
401         if (r->finfo.filetype == APR_NOFILE) {
402             /* file doesn't exist on disk, so we can't do anything based on
403              * modification time.  Note that this does _not_ log an error.
404              */
405             return DECLINED;
406         }
407         base = r->finfo.mtime;
408         additional_sec = atoi(&code[1]);
409         additional = apr_time_from_sec(additional_sec);
410         break;
411     case 'A':
412         /* there's been some discussion and it's possible that
413          * 'access time' will be stored in request structure
414          */
415         base = r->request_time;
416         additional_sec = atoi(&code[1]);
417         additional = apr_time_from_sec(additional_sec);
418         break;
419     default:
420         /* expecting the add_* routines to be case-hardened this
421          * is just a reminder that module is beta
422          */
423         ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01500)
424                     "internal error: bad expires code: %s", r->filename);
425         return HTTP_INTERNAL_SERVER_ERROR;
426     }
427
428     expires = base + additional;
429     if (expires < r->request_time) {
430         expires = r->request_time;
431     }
432     apr_table_mergen(t, "Cache-Control",
433                      apr_psprintf(r->pool, "max-age=%" APR_TIME_T_FMT,
434                                   apr_time_sec(expires - r->request_time)));
435     timestr = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
436     apr_rfc822_date(timestr, expires);
437     apr_table_setn(t, "Expires", timestr);
438     return OK;
439 }
440
441 /*
442  * Output filter to set the Expires response header field
443  * according to the content-type of the response -- if it hasn't
444  * already been set.
445  */
446 static apr_status_t expires_filter(ap_filter_t *f,
447                                    apr_bucket_brigade *b)
448 {
449     request_rec *r;
450     expires_dir_config *conf;
451     const char *expiry;
452     apr_table_t *t;
453
454     /* Don't add Expires headers to errors */
455     if (ap_is_HTTP_ERROR(f->r->status)) {
456         ap_remove_output_filter(f);
457         return ap_pass_brigade(f->next, b);
458     }
459
460     r = f->r;
461     conf = (expires_dir_config *) ap_get_module_config(r->per_dir_config,
462                                                        &expires_module);
463
464     /*
465      * Check to see which output header table we should use;
466      * mod_cgi loads script fields into r->err_headers_out,
467      * for instance.
468      */
469     expiry = apr_table_get(r->err_headers_out, "Expires");
470     if (expiry != NULL) {
471         t = r->err_headers_out;
472     }
473     else {
474         expiry = apr_table_get(r->headers_out, "Expires");
475         t = r->headers_out;
476     }
477     if (expiry == NULL) {
478         /*
479          * No expiration has been set, so we can apply any managed by
480          * this module.  First, check to see if there is an applicable
481          * ExpiresByType directive.
482          */
483         expiry = apr_table_get(conf->expiresbytype,
484                                ap_field_noparam(r->pool, r->content_type));
485         if (expiry == NULL) {
486             int usedefault = 1;
487             /*
488              * See if we have a wildcard entry for the major type.
489              */
490             if (conf->wildcards) {
491                 char *checkmime;
492                 char *spos;
493                 checkmime = apr_pstrdup(r->pool, r->content_type);
494                 spos = checkmime ? ap_strchr(checkmime, '/') : NULL;
495                 if (spos != NULL) {
496                     /*
497                      * Without a '/' character, nothing we have will match.
498                      * However, we have one.
499                      */
500                     if (strlen(++spos) > 0) {
501                         *spos++ = '*';
502                         *spos = '\0';
503                     }
504                     else {
505                         checkmime = apr_pstrcat(r->pool, checkmime, "*", NULL);
506                     }
507                     expiry = apr_table_get(conf->expiresbytype, checkmime);
508                     usedefault = (expiry == NULL);
509                 }
510             }
511             if (usedefault) {
512                 /*
513                  * Use the ExpiresDefault directive
514                  */
515                 expiry = conf->expiresdefault;
516             }
517         }
518         if (expiry != NULL) {
519             set_expiration_fields(r, expiry, t);
520         }
521     }
522     ap_remove_output_filter(f);
523     return ap_pass_brigade(f->next, b);
524 }
525
526 static void expires_insert_filter(request_rec *r)
527 {
528     expires_dir_config *conf;
529
530     /* Don't add Expires headers to errors */
531     if (ap_is_HTTP_ERROR(r->status)) {
532         return;
533     }
534     /* Say no to subrequests */
535     if (r->main != NULL) {
536         return;
537     }
538     conf = (expires_dir_config *) ap_get_module_config(r->per_dir_config,
539                                                        &expires_module);
540
541     /* Check to see if the filter is enabled and if there are any applicable
542      * config directives for this directory scope
543      */
544     if (conf->active != ACTIVE_ON ||
545         (apr_is_empty_table(conf->expiresbytype) && !conf->expiresdefault)) {
546         return;
547     }
548     ap_add_output_filter("MOD_EXPIRES", NULL, r, r->connection);
549 }
550
551 static void register_hooks(apr_pool_t *p)
552 {
553     /* mod_expires needs to run *before* the cache save filter which is
554      * AP_FTYPE_CONTENT_SET-1.  Otherwise, our expires won't be honored.
555      */
556     ap_register_output_filter("MOD_EXPIRES", expires_filter, NULL,
557                               AP_FTYPE_CONTENT_SET-2);
558     ap_hook_insert_error_filter(expires_insert_filter, NULL, NULL, APR_HOOK_MIDDLE);
559     ap_hook_insert_filter(expires_insert_filter, NULL, NULL, APR_HOOK_MIDDLE);
560 }
561
562 AP_DECLARE_MODULE(expires) =
563 {
564     STANDARD20_MODULE_STUFF,
565     create_dir_expires_config,  /* dir config creater */
566     merge_expires_dir_configs,  /* dir merger --- default is to override */
567     NULL,                       /* server config */
568     NULL,                       /* merge server configs */
569     expires_cmds,               /* command apr_table_t */
570     register_hooks              /* register hooks */
571 };