1#include "base.h"
2#include "log.h"
3#include "buffer.h"
4#include "response.h"
5
6#include "plugin.h"
7#include "stat_cache.h"
8
9#include <ctype.h>
10#include <stdlib.h>
11#include <string.h>
12#include <time.h>
13
14/**
15 * this is a expire module for a lighttpd
16 *
17 * set 'Expires:' HTTP Headers on demand
18 */
19
20
21
22/* plugin config for all request/connections */
23
24typedef struct {
25	array *expire_url;
26} plugin_config;
27
28typedef struct {
29	PLUGIN_DATA;
30
31	buffer *expire_tstmp;
32
33	plugin_config **config_storage;
34
35	plugin_config conf;
36} plugin_data;
37
38/* init the plugin data */
39INIT_FUNC(mod_expire_init) {
40	plugin_data *p;
41
42	p = calloc(1, sizeof(*p));
43
44	p->expire_tstmp = buffer_init();
45
46	buffer_string_prepare_copy(p->expire_tstmp, 255);
47
48	return p;
49}
50
51/* detroy the plugin data */
52FREE_FUNC(mod_expire_free) {
53	plugin_data *p = p_d;
54
55	UNUSED(srv);
56
57	if (!p) return HANDLER_GO_ON;
58
59	buffer_free(p->expire_tstmp);
60
61	if (p->config_storage) {
62		size_t i;
63		for (i = 0; i < srv->config_context->used; i++) {
64			plugin_config *s = p->config_storage[i];
65
66			if (NULL == s) continue;
67
68			array_free(s->expire_url);
69			free(s);
70		}
71		free(p->config_storage);
72	}
73
74	free(p);
75
76	return HANDLER_GO_ON;
77}
78
79static int mod_expire_get_offset(server *srv, plugin_data *p, buffer *expire, time_t *offset) {
80	char *ts;
81	int type = -1;
82	time_t retts = 0;
83
84	UNUSED(p);
85
86	/*
87	 * parse
88	 *
89	 * '(access|now|modification) [plus] {<num> <type>}*'
90	 *
91	 * e.g. 'access 1 years'
92	 */
93
94	if (buffer_string_is_empty(expire)) {
95		log_error_write(srv, __FILE__, __LINE__, "s",
96				"empty:");
97		return -1;
98	}
99
100	ts = expire->ptr;
101
102	if (0 == strncmp(ts, "access ", 7)) {
103		type  = 0;
104		ts   += 7;
105	} else if (0 == strncmp(ts, "now ", 4)) {
106		type  = 0;
107		ts   += 4;
108	} else if (0 == strncmp(ts, "modification ", 13)) {
109		type  = 1;
110		ts   += 13;
111	} else {
112		/* invalid type-prefix */
113		log_error_write(srv, __FILE__, __LINE__, "ss",
114				"invalid <base>:", ts);
115		return -1;
116	}
117
118	if (0 == strncmp(ts, "plus ", 5)) {
119		/* skip the optional plus */
120		ts   += 5;
121	}
122
123	/* the rest is just <number> (years|months|weeks|days|hours|minutes|seconds) */
124	while (1) {
125		char *space, *err;
126		int num;
127
128		if (NULL == (space = strchr(ts, ' '))) {
129			log_error_write(srv, __FILE__, __LINE__, "ss",
130					"missing space after <num>:", ts);
131			return -1;
132		}
133
134		num = strtol(ts, &err, 10);
135		if (*err != ' ') {
136			log_error_write(srv, __FILE__, __LINE__, "ss",
137					"missing <type> after <num>:", ts);
138			return -1;
139		}
140
141		ts = space + 1;
142
143		if (NULL != (space = strchr(ts, ' '))) {
144			int slen;
145			/* */
146
147			slen = space - ts;
148
149			if (slen == 5 &&
150			    0 == strncmp(ts, "years", slen)) {
151				num *= 60 * 60 * 24 * 30 * 12;
152			} else if (slen == 6 &&
153				   0 == strncmp(ts, "months", slen)) {
154				num *= 60 * 60 * 24 * 30;
155			} else if (slen == 5 &&
156				   0 == strncmp(ts, "weeks", slen)) {
157				num *= 60 * 60 * 24 * 7;
158			} else if (slen == 4 &&
159				   0 == strncmp(ts, "days", slen)) {
160				num *= 60 * 60 * 24;
161			} else if (slen == 5 &&
162				   0 == strncmp(ts, "hours", slen)) {
163				num *= 60 * 60;
164			} else if (slen == 7 &&
165				   0 == strncmp(ts, "minutes", slen)) {
166				num *= 60;
167			} else if (slen == 7 &&
168				   0 == strncmp(ts, "seconds", slen)) {
169				num *= 1;
170			} else {
171				log_error_write(srv, __FILE__, __LINE__, "ss",
172						"unknown type:", ts);
173				return -1;
174			}
175
176			retts += num;
177
178			ts = space + 1;
179		} else {
180			if (0 == strcmp(ts, "years")) {
181				num *= 60 * 60 * 24 * 30 * 12;
182			} else if (0 == strcmp(ts, "months")) {
183				num *= 60 * 60 * 24 * 30;
184			} else if (0 == strcmp(ts, "weeks")) {
185				num *= 60 * 60 * 24 * 7;
186			} else if (0 == strcmp(ts, "days")) {
187				num *= 60 * 60 * 24;
188			} else if (0 == strcmp(ts, "hours")) {
189				num *= 60 * 60;
190			} else if (0 == strcmp(ts, "minutes")) {
191				num *= 60;
192			} else if (0 == strcmp(ts, "seconds")) {
193				num *= 1;
194			} else {
195				log_error_write(srv, __FILE__, __LINE__, "ss",
196						"unknown type:", ts);
197				return -1;
198			}
199
200			retts += num;
201
202			break;
203		}
204	}
205
206	if (offset != NULL) *offset = retts;
207
208	return type;
209}
210
211
212/* handle plugin config and check values */
213
214SETDEFAULTS_FUNC(mod_expire_set_defaults) {
215	plugin_data *p = p_d;
216	size_t i = 0, k;
217
218	config_values_t cv[] = {
219		{ "expire.url",                 NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },       /* 0 */
220		{ NULL,                         NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
221	};
222
223	if (!p) return HANDLER_ERROR;
224
225	p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
226
227	for (i = 0; i < srv->config_context->used; i++) {
228		data_config const* config = (data_config const*)srv->config_context->data[i];
229		plugin_config *s;
230
231		s = calloc(1, sizeof(plugin_config));
232		s->expire_url    = array_init();
233
234		cv[0].destination = s->expire_url;
235
236		p->config_storage[i] = s;
237
238		if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
239			return HANDLER_ERROR;
240		}
241
242		for (k = 0; k < s->expire_url->used; k++) {
243			data_string *ds = (data_string *)s->expire_url->data[k];
244
245			/* parse lines */
246			if (-1 == mod_expire_get_offset(srv, p, ds->value, NULL)) {
247				log_error_write(srv, __FILE__, __LINE__, "sb",
248						"parsing expire.url failed:", ds->value);
249				return HANDLER_ERROR;
250			}
251		}
252	}
253
254
255	return HANDLER_GO_ON;
256}
257
258#define PATCH(x) \
259	p->conf.x = s->x;
260static int mod_expire_patch_connection(server *srv, connection *con, plugin_data *p) {
261	size_t i, j;
262	plugin_config *s = p->config_storage[0];
263
264	PATCH(expire_url);
265
266	/* skip the first, the global context */
267	for (i = 1; i < srv->config_context->used; i++) {
268		data_config *dc = (data_config *)srv->config_context->data[i];
269		s = p->config_storage[i];
270
271		/* condition didn't match */
272		if (!config_check_cond(srv, con, dc)) continue;
273
274		/* merge config */
275		for (j = 0; j < dc->value->used; j++) {
276			data_unset *du = dc->value->data[j];
277
278			if (buffer_is_equal_string(du->key, CONST_STR_LEN("expire.url"))) {
279				PATCH(expire_url);
280			}
281		}
282	}
283
284	return 0;
285}
286#undef PATCH
287
288URIHANDLER_FUNC(mod_expire_path_handler) {
289	plugin_data *p = p_d;
290	int s_len;
291	size_t k;
292
293	if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
294
295	mod_expire_patch_connection(srv, con, p);
296
297	s_len = buffer_string_length(con->uri.path);
298
299	for (k = 0; k < p->conf.expire_url->used; k++) {
300		data_string *ds = (data_string *)p->conf.expire_url->data[k];
301		int ct_len = buffer_string_length(ds->key);
302
303		if (ct_len > s_len) continue;
304		if (buffer_is_empty(ds->key)) continue;
305
306		if (0 == strncmp(con->uri.path->ptr, ds->key->ptr, ct_len)) {
307			time_t ts, expires;
308			stat_cache_entry *sce = NULL;
309
310			/* if stat fails => sce == NULL, ignore return value */
311			(void) stat_cache_get_entry(srv, con, con->physical.path, &sce);
312
313			switch(mod_expire_get_offset(srv, p, ds->value, &ts)) {
314			case 0:
315				/* access */
316				expires = (ts + srv->cur_ts);
317				break;
318			case 1:
319				/* modification */
320
321				/* can't set modification based expire header if
322				 * mtime is not available
323				 */
324				if (NULL == sce) return HANDLER_GO_ON;
325
326				expires = (ts + sce->st.st_mtime);
327				break;
328			default:
329				/* -1 is handled at parse-time */
330				return HANDLER_ERROR;
331			}
332
333			/* expires should be at least srv->cur_ts */
334			if (expires < srv->cur_ts) expires = srv->cur_ts;
335
336			buffer_string_prepare_copy(p->expire_tstmp, 255);
337			buffer_append_strftime(p->expire_tstmp, "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(expires)));
338
339			/* HTTP/1.0 */
340			response_header_overwrite(srv, con, CONST_STR_LEN("Expires"), CONST_BUF_LEN(p->expire_tstmp));
341
342			/* HTTP/1.1 */
343			buffer_copy_string_len(p->expire_tstmp, CONST_STR_LEN("max-age="));
344			buffer_append_int(p->expire_tstmp, expires - srv->cur_ts); /* as expires >= srv->cur_ts the difference is >= 0 */
345
346			response_header_append(srv, con, CONST_STR_LEN("Cache-Control"), CONST_BUF_LEN(p->expire_tstmp));
347
348			return HANDLER_GO_ON;
349		}
350	}
351
352	/* not found */
353	return HANDLER_GO_ON;
354}
355
356/* this function is called at dlopen() time and inits the callbacks */
357
358int mod_expire_plugin_init(plugin *p);
359int mod_expire_plugin_init(plugin *p) {
360	p->version     = LIGHTTPD_VERSION_ID;
361	p->name        = buffer_init_string("expire");
362
363	p->init        = mod_expire_init;
364	p->handle_subrequest_start = mod_expire_path_handler;
365	p->set_defaults  = mod_expire_set_defaults;
366	p->cleanup     = mod_expire_free;
367
368	p->data        = NULL;
369
370	return 0;
371}
372