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