xref: /lighttpd1.4/src/mod_expire.c (revision 49e9f0ac)
1 #include "first.h"
2 
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
6 #include "http_header.h"
7 
8 #include "plugin.h"
9 #include "stat_cache.h"
10 
11 #include <stdlib.h>
12 #include <string.h>
13 #include <time.h>
14 
15 /**
16  * this is a expire module for a lighttpd
17  *
18  * set 'Expires:' HTTP Headers on demand
19  */
20 
21 
22 
23 /* plugin config for all request/connections */
24 
25 typedef struct {
26 	array *expire_url;
27 	array *expire_mimetypes;
28 } plugin_config;
29 
30 typedef struct {
31 	PLUGIN_DATA;
32 
33 	buffer *expire_tstmp;
34 
35 	plugin_config **config_storage;
36 
37 	plugin_config conf;
38 } plugin_data;
39 
40 /* init the plugin data */
41 INIT_FUNC(mod_expire_init) {
42 	plugin_data *p;
43 
44 	p = calloc(1, sizeof(*p));
45 
46 	p->expire_tstmp = buffer_init();
47 
48 	buffer_string_prepare_copy(p->expire_tstmp, 255);
49 
50 	return p;
51 }
52 
53 /* detroy the plugin data */
54 FREE_FUNC(mod_expire_free) {
55 	plugin_data *p = p_d;
56 
57 	UNUSED(srv);
58 
59 	if (!p) return HANDLER_GO_ON;
60 
61 	buffer_free(p->expire_tstmp);
62 
63 	if (p->config_storage) {
64 		size_t i;
65 		for (i = 0; i < srv->config_context->used; i++) {
66 			plugin_config *s = p->config_storage[i];
67 
68 			if (NULL == s) continue;
69 
70 			array_free(s->expire_url);
71 			array_free(s->expire_mimetypes);
72 			free(s);
73 		}
74 		free(p->config_storage);
75 	}
76 
77 	free(p);
78 
79 	return HANDLER_GO_ON;
80 }
81 
82 static int mod_expire_get_offset(server *srv, plugin_data *p, buffer *expire, time_t *offset) {
83 	char *ts;
84 	int type = -1;
85 	time_t retts = 0;
86 
87 	UNUSED(p);
88 
89 	/*
90 	 * parse
91 	 *
92 	 * '(access|now|modification) [plus] {<num> <type>}*'
93 	 *
94 	 * e.g. 'access 1 years'
95 	 */
96 
97 	if (buffer_string_is_empty(expire)) {
98 		log_error_write(srv, __FILE__, __LINE__, "s",
99 				"empty:");
100 		return -1;
101 	}
102 
103 	ts = expire->ptr;
104 
105 	if (0 == strncmp(ts, "access ", 7)) {
106 		type  = 0;
107 		ts   += 7;
108 	} else if (0 == strncmp(ts, "now ", 4)) {
109 		type  = 0;
110 		ts   += 4;
111 	} else if (0 == strncmp(ts, "modification ", 13)) {
112 		type  = 1;
113 		ts   += 13;
114 	} else {
115 		/* invalid type-prefix */
116 		log_error_write(srv, __FILE__, __LINE__, "ss",
117 				"invalid <base>:", ts);
118 		return -1;
119 	}
120 
121 	if (0 == strncmp(ts, "plus ", 5)) {
122 		/* skip the optional plus */
123 		ts   += 5;
124 	}
125 
126 	/* the rest is just <number> (years|months|weeks|days|hours|minutes|seconds) */
127 	while (1) {
128 		char *space, *err;
129 		int num;
130 
131 		if (NULL == (space = strchr(ts, ' '))) {
132 			log_error_write(srv, __FILE__, __LINE__, "ss",
133 					"missing space after <num>:", ts);
134 			return -1;
135 		}
136 
137 		num = strtol(ts, &err, 10);
138 		if (*err != ' ') {
139 			log_error_write(srv, __FILE__, __LINE__, "ss",
140 					"missing <type> after <num>:", ts);
141 			return -1;
142 		}
143 
144 		ts = space + 1;
145 
146 		if (NULL != (space = strchr(ts, ' '))) {
147 			int slen;
148 			/* */
149 
150 			slen = space - ts;
151 
152 			if (slen == 5 &&
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)) {
163 				num *= 60 * 60 * 24;
164 			} else if (slen == 5 &&
165 				   0 == strncmp(ts, "hours", slen)) {
166 				num *= 60 * 60;
167 			} else if (slen == 7 &&
168 				   0 == strncmp(ts, "minutes", slen)) {
169 				num *= 60;
170 			} else if (slen == 7 &&
171 				   0 == strncmp(ts, "seconds", slen)) {
172 				num *= 1;
173 			} else {
174 				log_error_write(srv, __FILE__, __LINE__, "ss",
175 						"unknown type:", ts);
176 				return -1;
177 			}
178 
179 			retts += num;
180 
181 			ts = space + 1;
182 		} else {
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")) {
190 				num *= 60 * 60 * 24;
191 			} else if (0 == strcmp(ts, "hours")) {
192 				num *= 60 * 60;
193 			} else if (0 == strcmp(ts, "minutes")) {
194 				num *= 60;
195 			} else if (0 == strcmp(ts, "seconds")) {
196 				num *= 1;
197 			} else {
198 				log_error_write(srv, __FILE__, __LINE__, "ss",
199 						"unknown type:", ts);
200 				return -1;
201 			}
202 
203 			retts += num;
204 
205 			break;
206 		}
207 	}
208 
209 	if (offset != NULL) *offset = retts;
210 
211 	return type;
212 }
213 
214 
215 /* handle plugin config and check values */
216 
217 SETDEFAULTS_FUNC(mod_expire_set_defaults) {
218 	plugin_data *p = p_d;
219 	size_t i = 0, k;
220 
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 }
225 	};
226 
227 	if (!p) return HANDLER_ERROR;
228 
229 	p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *));
230 
231 	for (i = 0; i < srv->config_context->used; i++) {
232 		data_config const* config = (data_config const*)srv->config_context->data[i];
233 		plugin_config *s;
234 
235 		s = calloc(1, sizeof(plugin_config));
236 		s->expire_url    = array_init();
237 		s->expire_mimetypes = array_init();
238 
239 		cv[0].destination = s->expire_url;
240 		cv[1].destination = s->expire_mimetypes;
241 
242 		p->config_storage[i] = s;
243 
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;
246 		}
247 
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;
252 		}
253 
254 		for (k = 0; k < s->expire_url->used; k++) {
255 			data_string *ds = (data_string *)s->expire_url->data[k];
256 
257 			/* parse lines */
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;
262 			}
263 		}
264 
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;
269 		}
270 
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);
274 
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);
280 
281 			/* parse lines */
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;
286 			}
287 		}
288 	}
289 
290 
291 	return HANDLER_GO_ON;
292 }
293 
294 #define PATCH(x) \
295 	p->conf.x = s->x;
296 static int mod_expire_patch_connection(server *srv, connection *con, plugin_data *p) {
297 	size_t i, j;
298 	plugin_config *s = p->config_storage[0];
299 
300 	PATCH(expire_url);
301 	PATCH(expire_mimetypes);
302 
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];
307 
308 		/* condition didn't match */
309 		if (!config_check_cond(srv, con, dc)) continue;
310 
311 		/* merge config */
312 		for (j = 0; j < dc->value->used; j++) {
313 			data_unset *du = dc->value->data[j];
314 
315 			if (buffer_is_equal_string(du->key, CONST_STR_LEN("expire.url"))) {
316 				PATCH(expire_url);
317 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("expire.mimetypes"))) {
318 				PATCH(expire_mimetypes);
319 			}
320 		}
321 	}
322 
323 	return 0;
324 }
325 #undef PATCH
326 
327 CONNECTION_FUNC(mod_expire_handler) {
328 	plugin_data *p = p_d;
329 	buffer *vb;
330 	data_string *ds;
331 
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;
340 
341 	if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
342 
343 	mod_expire_patch_connection(srv, con, p);
344 
345 	/* check expire.url */
346 	ds = (data_string *)array_match_key_prefix(p->conf.expire_url, con->uri.path);
347 	if (NULL != ds) {
348 		vb = ds->value;
349 	}
350 	else {
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"));
353 		ds = (NULL != vb)
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;
357 		vb = ds->value;
358 	}
359 
360 	if (NULL != vb) {
361 			time_t ts, expires;
362 			stat_cache_entry *sce = NULL;
363 
364 			switch(mod_expire_get_offset(srv, p, vb, &ts)) {
365 			case 0:
366 				/* access */
367 				expires = (ts + srv->cur_ts);
368 				break;
369 			case 1:
370 				/* modification */
371 
372 				/* if stat fails => sce == NULL, ignore return value */
373 				(void) stat_cache_get_entry(srv, con, con->physical.path, &sce);
374 
375 				/* can't set modification based expire header if
376 				 * mtime is not available
377 				 */
378 				if (NULL == sce) return HANDLER_GO_ON;
379 
380 				expires = (ts + sce->st.st_mtime);
381 				break;
382 			default:
383 				/* -1 is handled at parse-time */
384 				return HANDLER_ERROR;
385 			}
386 
387 			/* expires should be at least srv->cur_ts */
388 			if (expires < srv->cur_ts) expires = srv->cur_ts;
389 
390 			buffer_clear(p->expire_tstmp);
391 			buffer_append_strftime(p->expire_tstmp, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(expires)));
392 
393 			/* HTTP/1.0 */
394 			http_header_response_set(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Expires"), CONST_BUF_LEN(p->expire_tstmp));
395 
396 			/* HTTP/1.1 */
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 */
399 
400 			http_header_response_set(con, HTTP_HEADER_CACHE_CONTROL, CONST_STR_LEN("Cache-Control"), CONST_BUF_LEN(p->expire_tstmp));
401 
402 			return HANDLER_GO_ON;
403 	}
404 
405 	/* not found */
406 	return HANDLER_GO_ON;
407 }
408 
409 /* this function is called at dlopen() time and inits the callbacks */
410 
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");
415 
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;
420 
421 	p->data        = NULL;
422 
423 	return 0;
424 }
425