6 #include "http_header.h"
9 #include "stat_cache.h"
16 * this is a expire module for a lighttpd
18 * set 'Expires:' HTTP Headers on demand
23 /* plugin config for all request/connections */
27 array
*expire_mimetypes
;
35 plugin_config
**config_storage
;
40 /* init the plugin data */
41 INIT_FUNC(mod_expire_init
) {
44 p
= calloc(1, sizeof(*p
));
46 p
->expire_tstmp
= buffer_init();
48 buffer_string_prepare_copy(p
->expire_tstmp
, 255);
53 /* detroy the plugin data */
54 FREE_FUNC(mod_expire_free
) {
59 if (!p
) return HANDLER_GO_ON
;
61 buffer_free(p
->expire_tstmp
);
63 if (p
->config_storage
) {
65 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
66 plugin_config
*s
= p
->config_storage
[i
];
68 if (NULL
== s
) continue;
70 array_free(s
->expire_url
);
71 array_free(s
->expire_mimetypes
);
74 free(p
->config_storage
);
82 static int mod_expire_get_offset(server
*srv
, plugin_data
*p
, buffer
*expire
, time_t *offset
) {
92 * '(access|now|modification) [plus] {<num> <type>}*'
94 * e.g. 'access 1 years'
97 if (buffer_string_is_empty(expire
)) {
98 log_error_write(srv
, __FILE__
, __LINE__
, "s",
105 if (0 == strncmp(ts
, "access ", 7)) {
108 } else if (0 == strncmp(ts
, "now ", 4)) {
111 } else if (0 == strncmp(ts
, "modification ", 13)) {
115 /* invalid type-prefix */
116 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
117 "invalid <base>:", ts
);
121 if (0 == strncmp(ts
, "plus ", 5)) {
122 /* skip the optional plus */
126 /* the rest is just <number> (years|months|weeks|days|hours|minutes|seconds) */
131 if (NULL
== (space
= strchr(ts
, ' '))) {
132 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
133 "missing space after <num>:", ts
);
137 num
= strtol(ts
, &err
, 10);
139 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
140 "missing <type> after <num>:", ts
);
146 if (NULL
!= (space
= strchr(ts
, ' '))) {
153 0 == strncmp(ts
, "years", slen
)) {
154 num
*= 60 * 60 * 24 * 30 * 12;
155 } else if (slen
== 6 &&
156 0 == strncmp(ts
, "months", slen
)) {
157 num
*= 60 * 60 * 24 * 30;
158 } else if (slen
== 5 &&
159 0 == strncmp(ts
, "weeks", slen
)) {
160 num
*= 60 * 60 * 24 * 7;
161 } else if (slen
== 4 &&
162 0 == strncmp(ts
, "days", slen
)) {
164 } else if (slen
== 5 &&
165 0 == strncmp(ts
, "hours", slen
)) {
167 } else if (slen
== 7 &&
168 0 == strncmp(ts
, "minutes", slen
)) {
170 } else if (slen
== 7 &&
171 0 == strncmp(ts
, "seconds", slen
)) {
174 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
175 "unknown type:", ts
);
183 if (0 == strcmp(ts
, "years")) {
184 num
*= 60 * 60 * 24 * 30 * 12;
185 } else if (0 == strcmp(ts
, "months")) {
186 num
*= 60 * 60 * 24 * 30;
187 } else if (0 == strcmp(ts
, "weeks")) {
188 num
*= 60 * 60 * 24 * 7;
189 } else if (0 == strcmp(ts
, "days")) {
191 } else if (0 == strcmp(ts
, "hours")) {
193 } else if (0 == strcmp(ts
, "minutes")) {
195 } else if (0 == strcmp(ts
, "seconds")) {
198 log_error_write(srv
, __FILE__
, __LINE__
, "ss",
199 "unknown type:", ts
);
209 if (offset
!= NULL
) *offset
= retts
;
215 /* handle plugin config and check values */
217 SETDEFAULTS_FUNC(mod_expire_set_defaults
) {
218 plugin_data
*p
= p_d
;
221 config_values_t cv
[] = {
222 { "expire.url", NULL
, T_CONFIG_ARRAY
, T_CONFIG_SCOPE_CONNECTION
}, /* 0 */
223 { "expire.mimetypes", NULL
, T_CONFIG_ARRAY
, T_CONFIG_SCOPE_CONNECTION
}, /* 1 */
224 { NULL
, NULL
, T_CONFIG_UNSET
, T_CONFIG_SCOPE_UNSET
}
227 if (!p
) return HANDLER_ERROR
;
229 p
->config_storage
= calloc(srv
->config_context
->used
, sizeof(plugin_config
*));
231 for (i
= 0; i
< srv
->config_context
->used
; i
++) {
232 data_config
const* config
= (data_config
const*)srv
->config_context
->data
[i
];
235 s
= calloc(1, sizeof(plugin_config
));
236 s
->expire_url
= array_init();
237 s
->expire_mimetypes
= array_init();
239 cv
[0].destination
= s
->expire_url
;
240 cv
[1].destination
= s
->expire_mimetypes
;
242 p
->config_storage
[i
] = s
;
244 if (0 != config_insert_values_global(srv
, config
->value
, cv
, i
== 0 ? T_CONFIG_SCOPE_SERVER
: T_CONFIG_SCOPE_CONNECTION
)) {
245 return HANDLER_ERROR
;
248 if (!array_is_kvstring(s
->expire_url
)) {
249 log_error_write(srv
, __FILE__
, __LINE__
, "s",
250 "unexpected value for expire.url; expected list of \"urlpath\" => \"expiration\"");
251 return HANDLER_ERROR
;
254 for (k
= 0; k
< s
->expire_url
->used
; k
++) {
255 data_string
*ds
= (data_string
*)s
->expire_url
->data
[k
];
258 if (-1 == mod_expire_get_offset(srv
, p
, ds
->value
, NULL
)) {
259 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
260 "parsing expire.url failed:", ds
->value
);
261 return HANDLER_ERROR
;
265 if (!array_is_kvstring(s
->expire_mimetypes
)) {
266 log_error_write(srv
, __FILE__
, __LINE__
, "s",
267 "unexpected value for expire.mimetypes; expected list of \"mimetype\" => \"expiration\"");
268 return HANDLER_ERROR
;
271 for (k
= 0; k
< s
->expire_mimetypes
->used
; k
++) {
272 data_string
*ds
= (data_string
*)s
->expire_mimetypes
->data
[k
];
273 size_t klen
= buffer_string_length(ds
->key
);
275 /*(omit trailing '*', if present, from prefix match)*/
276 /*(not usually a good idea to modify array keys
277 * since doing so might break array_get_element_klen() search,
278 * but array use in this module only walks array)*/
279 if (klen
&& ds
->key
->ptr
[klen
-1] == '*') buffer_string_set_length(ds
->key
, klen
-1);
282 if (-1 == mod_expire_get_offset(srv
, p
, ds
->value
, NULL
)) {
283 log_error_write(srv
, __FILE__
, __LINE__
, "sb",
284 "parsing expire.mimetypes failed:", ds
->value
);
285 return HANDLER_ERROR
;
291 return HANDLER_GO_ON
;
296 static int mod_expire_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
298 plugin_config
*s
= p
->config_storage
[0];
301 PATCH(expire_mimetypes
);
303 /* skip the first, the global context */
304 for (i
= 1; i
< srv
->config_context
->used
; i
++) {
305 data_config
*dc
= (data_config
*)srv
->config_context
->data
[i
];
306 s
= p
->config_storage
[i
];
308 /* condition didn't match */
309 if (!config_check_cond(srv
, con
, dc
)) continue;
312 for (j
= 0; j
< dc
->value
->used
; j
++) {
313 data_unset
*du
= dc
->value
->data
[j
];
315 if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("expire.url"))) {
317 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("expire.mimetypes"))) {
318 PATCH(expire_mimetypes
);
327 CONNECTION_FUNC(mod_expire_handler
) {
328 plugin_data
*p
= p_d
;
332 /* Add caching headers only to http_status 200 OK or 206 Partial Content */
333 if (con
->http_status
!= 200 && con
->http_status
!= 206) return HANDLER_GO_ON
;
334 /* Add caching headers only to GET or HEAD requests */
335 if ( con
->request
.http_method
!= HTTP_METHOD_GET
336 && con
->request
.http_method
!= HTTP_METHOD_HEAD
) return HANDLER_GO_ON
;
337 /* Add caching headers only if not already present */
338 vb
= http_header_response_get(con
, HTTP_HEADER_CACHE_CONTROL
, CONST_STR_LEN("Cache-Control"));
339 if (NULL
!= vb
) return HANDLER_GO_ON
;
341 if (buffer_is_empty(con
->uri
.path
)) return HANDLER_GO_ON
;
343 mod_expire_patch_connection(srv
, con
, p
);
345 /* check expire.url */
346 ds
= (data_string
*)array_match_key_prefix(p
->conf
.expire_url
, con
->uri
.path
);
351 /* check expire.mimetypes (if no match with expire.url) */
352 vb
= http_header_response_get(con
, HTTP_HEADER_CONTENT_TYPE
, CONST_STR_LEN("Content-Type"));
354 ? (data_string
*)array_match_key_prefix(p
->conf
.expire_mimetypes
, vb
)
355 : (data_string
*)array_get_element_klen(p
->conf
.expire_mimetypes
, CONST_STR_LEN(""));
356 if (NULL
== ds
) return HANDLER_GO_ON
;
362 stat_cache_entry
*sce
= NULL
;
364 switch(mod_expire_get_offset(srv
, p
, vb
, &ts
)) {
367 expires
= (ts
+ srv
->cur_ts
);
372 /* if stat fails => sce == NULL, ignore return value */
373 (void) stat_cache_get_entry(srv
, con
, con
->physical
.path
, &sce
);
375 /* can't set modification based expire header if
376 * mtime is not available
378 if (NULL
== sce
) return HANDLER_GO_ON
;
380 expires
= (ts
+ sce
->st
.st_mtime
);
383 /* -1 is handled at parse-time */
384 return HANDLER_ERROR
;
387 /* expires should be at least srv->cur_ts */
388 if (expires
< srv
->cur_ts
) expires
= srv
->cur_ts
;
390 buffer_clear(p
->expire_tstmp
);
391 buffer_append_strftime(p
->expire_tstmp
, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(expires
)));
394 http_header_response_set(con
, HTTP_HEADER_OTHER
, CONST_STR_LEN("Expires"), CONST_BUF_LEN(p
->expire_tstmp
));
397 buffer_copy_string_len(p
->expire_tstmp
, CONST_STR_LEN("max-age="));
398 buffer_append_int(p
->expire_tstmp
, expires
- srv
->cur_ts
); /* as expires >= srv->cur_ts the difference is >= 0 */
400 http_header_response_set(con
, HTTP_HEADER_CACHE_CONTROL
, CONST_STR_LEN("Cache-Control"), CONST_BUF_LEN(p
->expire_tstmp
));
402 return HANDLER_GO_ON
;
406 return HANDLER_GO_ON
;
409 /* this function is called at dlopen() time and inits the callbacks */
411 int mod_expire_plugin_init(plugin
*p
);
412 int mod_expire_plugin_init(plugin
*p
) {
413 p
->version
= LIGHTTPD_VERSION_ID
;
414 p
->name
= buffer_init_string("expire");
416 p
->init
= mod_expire_init
;
417 p
->handle_response_start
= mod_expire_handler
;
418 p
->set_defaults
= mod_expire_set_defaults
;
419 p
->cleanup
= mod_expire_free
;