mod_expires.c 18 KB

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