9 #include "stat_cache.h"
17 * this is a expire module for a lighttpd
19 * set 'Expires:' HTTP Headers on demand
24 /* plugin config for all request/connections */
28 array
*expire_mimetypes
;
36 plugin_config
**config_storage
;
41 /* init the plugin data */
42 INIT_FUNC(mod_expire_init
) {
45 p
= calloc(1, sizeof(*p
));
47 p
->expire_tstmp
= buffer_init();
49 buffer_string_prepare_copy(p
->expire_tstmp
, 255);
54 /* detroy the plugin data */
55 FREE_FUNC(mod_expire_free
) {
60 if (!p
) return HANDLER_GO_ON
;
62 buffer_free(p
->expire_tstmp
);
64 if (p
->config_storage
) {
66 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
67 plugin_config
*s
= p
->config_storage
[i
];
69 if (NULL
== s
) continue;
71 array_free(s
->expire_url
);
72 array_free(s
->expire_mimetypes
);
75 free(p
->config_storage
);
83 static int mod_expire_get_offset(server
*srv
, plugin_data
*p
, buffer
*expire
, time_t *offset
) {
93 * '(access|now|modification) [plus] {<num> <type>}*'
95 * e.g. 'access 1 years'
98 if (buffer_string_is_empty(expire
)) {
99 log_error_write(srv
, __FILE__
, __LINE__
, "s",
106 if (0 == strncmp(ts
, "access ", 7)) {
109 } else if (0 == strncmp(ts
, "now ", 4)) {
112 } else if (0 == strncmp(ts
, "modification ", 13)) {
116 /* invalid type-prefix */
117 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
118 "invalid <base>:", ts
);
122 if (0 == strncmp(ts
, "plus ", 5)) {
123 /* skip the optional plus */
127 /* the rest is just <number> (years|months|weeks|days|hours|minutes|seconds) */
132 if (NULL
== (space
= strchr(ts
, ' '))) {
133 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
134 "missing space after <num>:", ts
);
138 num
= strtol(ts
, &err
, 10);
140 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
141 "missing <type> after <num>:", ts
);
147 if (NULL
!= (space
= strchr(ts
, ' '))) {
154 0 == strncmp(ts
, "years", slen
)) {
155 num
*= 60 * 60 * 24 * 30 * 12;
156 } else if (slen
== 6 &&
157 0 == strncmp(ts
, "months", slen
)) {
158 num
*= 60 * 60 * 24 * 30;
159 } else if (slen
== 5 &&
160 0 == strncmp(ts
, "weeks", slen
)) {
161 num
*= 60 * 60 * 24 * 7;
162 } else if (slen
== 4 &&
163 0 == strncmp(ts
, "days", slen
)) {
165 } else if (slen
== 5 &&
166 0 == strncmp(ts
, "hours", slen
)) {
168 } else if (slen
== 7 &&
169 0 == strncmp(ts
, "minutes", slen
)) {
171 } else if (slen
== 7 &&
172 0 == strncmp(ts
, "seconds", slen
)) {
175 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
176 "unknown type:", ts
);
184 if (0 == strcmp(ts
, "years")) {
185 num
*= 60 * 60 * 24 * 30 * 12;
186 } else if (0 == strcmp(ts
, "months")) {
187 num
*= 60 * 60 * 24 * 30;
188 } else if (0 == strcmp(ts
, "weeks")) {
189 num
*= 60 * 60 * 24 * 7;
190 } else if (0 == strcmp(ts
, "days")) {
192 } else if (0 == strcmp(ts
, "hours")) {
194 } else if (0 == strcmp(ts
, "minutes")) {
196 } else if (0 == strcmp(ts
, "seconds")) {
199 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
200 "unknown type:", ts
);
210 if (offset
!= NULL
) *offset
= retts
;
216 /* handle plugin config and check values */
218 SETDEFAULTS_FUNC(mod_expire_set_defaults
) {
219 plugin_data
*p
= p_d
;
222 config_values_t cv
[] = {
223 { "expire.url", NULL
, T_CONFIG_ARRAY
, T_CONFIG_SCOPE_CONNECTION
}, /* 0 */
224 { "expire.mimetypes", NULL
, T_CONFIG_ARRAY
, T_CONFIG_SCOPE_CONNECTION
}, /* 1 */
225 { NULL
, NULL
, T_CONFIG_UNSET
, T_CONFIG_SCOPE_UNSET
}
228 if (!p
) return HANDLER_ERROR
;
230 p
->config_storage
= calloc(1, srv
->config_context
->used
* sizeof(plugin_config
*));
232 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
233 data_config
const* config
= (data_config
const*)srv
->config_context
->data
[i
];
236 s
= calloc(1, sizeof(plugin_config
));
237 s
->expire_url
= array_init();
238 s
->expire_mimetypes
= array_init();
240 cv
[0].destination
= s
->expire_url
;
241 cv
[1].destination
= s
->expire_mimetypes
;
243 p
->config_storage
[i
] = s
;
245 if (0 != config_insert_values_global(srv
, config
->value
, cv
, i
== 0 ? T_CONFIG_SCOPE_SERVER
: T_CONFIG_SCOPE_CONNECTION
)) {
246 return HANDLER_ERROR
;
249 for (k
= 0; k
< s
->expire_url
->used
; k
++) {
250 data_string
*ds
= (data_string
*)s
->expire_url
->data
[k
];
253 if (-1 == mod_expire_get_offset(srv
, p
, ds
->value
, NULL
)) {
254 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
255 "parsing expire.url failed:", ds
->value
);
256 return HANDLER_ERROR
;
260 for (k
= 0; k
< s
->expire_mimetypes
->used
; k
++) {
261 data_string
*ds
= (data_string
*)s
->expire_mimetypes
->data
[k
];
264 if (-1 == mod_expire_get_offset(srv
, p
, ds
->value
, NULL
)) {
265 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
266 "parsing expire.mimetypes failed:", ds
->value
);
267 return HANDLER_ERROR
;
273 return HANDLER_GO_ON
;
278 static int mod_expire_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
280 plugin_config
*s
= p
->config_storage
[0];
283 PATCH(expire_mimetypes
);
285 /* skip the first, the global context */
286 for (i
= 1; i
< srv
->config_context
->used
; i
++) {
287 data_config
*dc
= (data_config
*)srv
->config_context
->data
[i
];
288 s
= p
->config_storage
[i
];
290 /* condition didn't match */
291 if (!config_check_cond(srv
, con
, dc
)) continue;
294 for (j
= 0; j
< dc
->value
->used
; j
++) {
295 data_unset
*du
= dc
->value
->data
[j
];
297 if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("expire.url"))) {
299 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("expire.mimetypes"))) {
300 PATCH(expire_mimetypes
);
309 CONNECTION_FUNC(mod_expire_handler
) {
310 plugin_data
*p
= p_d
;
311 data_string
*ds
= NULL
;
315 /* Add caching headers only to http_status 200 OK or 206 Partial Content */
316 if (con
->http_status
!= 200 && con
->http_status
!= 206) return HANDLER_GO_ON
;
317 /* Add caching headers only to GET or HEAD requests */
318 if ( con
->request
.http_method
!= HTTP_METHOD_GET
319 && con
->request
.http_method
!= HTTP_METHOD_HEAD
) return HANDLER_GO_ON
;
320 /* Add caching headers only if not already present */
321 ds
= (data_string
*)array_get_element(con
->response
.headers
, "Cache-Control");
322 if (NULL
!= ds
&& !buffer_string_is_empty(ds
->value
)) return HANDLER_GO_ON
;
324 if (buffer_is_empty(con
->uri
.path
)) return HANDLER_GO_ON
;
326 mod_expire_patch_connection(srv
, con
, p
);
328 s_len
= buffer_string_length(con
->uri
.path
);
330 /* check expire.url */
331 for (k
= 0; k
< p
->conf
.expire_url
->used
; k
++) {
333 ds
= (data_string
*)p
->conf
.expire_url
->data
[k
];
334 ct_len
= buffer_string_length(ds
->key
);
336 if (ct_len
> s_len
) continue;
337 if (buffer_is_empty(ds
->key
)) continue;
339 if (0 == strncmp(con
->uri
.path
->ptr
, ds
->key
->ptr
, ct_len
)) {
343 /* check expire.mimetypes (if no match with expire.url) */
344 if (k
== p
->conf
.expire_url
->used
) {
345 const char *mimetype
;
346 ds
= (data_string
*)array_get_element(con
->response
.headers
, "Content-Type");
347 if (NULL
!= ds
&& !buffer_string_is_empty(ds
->value
)) {
348 mimetype
= ds
->value
->ptr
;
349 s_len
= buffer_string_length(ds
->value
);
354 for (k
= 0; k
< p
->conf
.expire_mimetypes
->used
; k
++) {
356 ds
= (data_string
*)p
->conf
.expire_mimetypes
->data
[k
];
357 ct_len
= buffer_string_length(ds
->key
);
359 if (ct_len
> s_len
) continue;
360 if (buffer_is_empty(ds
->key
)) continue;
362 /*(omit trailing '*', if present, from prefix match)*/
363 if (ds
->key
->ptr
[ct_len
-1] == '*') --ct_len
;
365 if (0 == strncmp(mimetype
, ds
->key
->ptr
, ct_len
)) {
369 if (k
== p
->conf
.expire_mimetypes
->used
) {
376 stat_cache_entry
*sce
= NULL
;
378 /* if stat fails => sce == NULL, ignore return value */
379 (void) stat_cache_get_entry(srv
, con
, con
->physical
.path
, &sce
);
381 switch(mod_expire_get_offset(srv
, p
, ds
->value
, &ts
)) {
384 expires
= (ts
+ srv
->cur_ts
);
389 /* can't set modification based expire header if
390 * mtime is not available
392 if (NULL
== sce
) return HANDLER_GO_ON
;
394 expires
= (ts
+ sce
->st
.st_mtime
);
397 /* -1 is handled at parse-time */
398 return HANDLER_ERROR
;
401 /* expires should be at least srv->cur_ts */
402 if (expires
< srv
->cur_ts
) expires
= srv
->cur_ts
;
404 buffer_string_prepare_copy(p
->expire_tstmp
, 255);
405 buffer_append_strftime(p
->expire_tstmp
, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(expires
)));
408 response_header_overwrite(srv
, con
, CONST_STR_LEN("Expires"), CONST_BUF_LEN(p
->expire_tstmp
));
411 buffer_copy_string_len(p
->expire_tstmp
, CONST_STR_LEN("max-age="));
412 buffer_append_int(p
->expire_tstmp
, expires
- srv
->cur_ts
); /* as expires >= srv->cur_ts the difference is >= 0 */
414 response_header_append(srv
, con
, CONST_STR_LEN("Cache-Control"), CONST_BUF_LEN(p
->expire_tstmp
));
416 return HANDLER_GO_ON
;
420 return HANDLER_GO_ON
;
423 /* this function is called at dlopen() time and inits the callbacks */
425 int mod_expire_plugin_init(plugin
*p
);
426 int mod_expire_plugin_init(plugin
*p
) {
427 p
->version
= LIGHTTPD_VERSION_ID
;
428 p
->name
= buffer_init_string("expire");
430 p
->init
= mod_expire_init
;
431 p
->handle_response_start
= mod_expire_handler
;
432 p
->set_defaults
= mod_expire_set_defaults
;
433 p
->cleanup
= mod_expire_free
;