1 #include "base.h"
2 #include "log.h"
3 #include "buffer.h"
4 
5 #include "plugin.h"
6 #include "response.h"
7 #include "inet_ntop_cache.h"
8 
9 #include <ctype.h>
10 #include <stdlib.h>
11 #include <fcntl.h>
12 #include <string.h>
13 
14 #if defined(HAVE_GDBM_H)
15 # include <gdbm.h>
16 #endif
17 
18 #if defined(HAVE_PCRE_H)
19 # include <pcre.h>
20 #endif
21 
22 #if defined(HAVE_MEMCACHE_H)
23 # include <memcache.h>
24 #endif
25 
26 /**
27  * this is a trigger_b4_dl for a lighttpd plugin
28  *
29  */
30 
31 /* plugin config for all request/connections */
32 
33 typedef struct {
34 	buffer *db_filename;
35 
36 	buffer *trigger_url;
37 	buffer *download_url;
38 	buffer *deny_url;
39 
40 	array  *mc_hosts;
41 	buffer *mc_namespace;
42 #if defined(HAVE_PCRE_H)
43 	pcre *trigger_regex;
44 	pcre *download_regex;
45 #endif
46 #if defined(HAVE_GDBM_H)
47 	GDBM_FILE db;
48 #endif
49 
50 #if defined(HAVE_MEMCACHE_H)
51 	struct memcache *mc;
52 #endif
53 
54 	unsigned short trigger_timeout;
55 	unsigned short debug;
56 } plugin_config;
57 
58 typedef struct {
59 	PLUGIN_DATA;
60 
61 	buffer *tmp_buf;
62 
63 	plugin_config **config_storage;
64 
65 	plugin_config conf;
66 } plugin_data;
67 
68 /* init the plugin data */
INIT_FUNC(mod_trigger_b4_dl_init)69 INIT_FUNC(mod_trigger_b4_dl_init) {
70 	plugin_data *p;
71 
72 	p = calloc(1, sizeof(*p));
73 
74 	p->tmp_buf = buffer_init();
75 
76 	return p;
77 }
78 
79 /* detroy the plugin data */
FREE_FUNC(mod_trigger_b4_dl_free)80 FREE_FUNC(mod_trigger_b4_dl_free) {
81 	plugin_data *p = p_d;
82 
83 	UNUSED(srv);
84 
85 	if (!p) return HANDLER_GO_ON;
86 
87 	if (p->config_storage) {
88 		size_t i;
89 		for (i = 0; i < srv->config_context->used; i++) {
90 			plugin_config *s = p->config_storage[i];
91 
92 			if (!s) continue;
93 
94 			buffer_free(s->db_filename);
95 			buffer_free(s->download_url);
96 			buffer_free(s->trigger_url);
97 			buffer_free(s->deny_url);
98 
99 			buffer_free(s->mc_namespace);
100 			array_free(s->mc_hosts);
101 
102 #if defined(HAVE_PCRE_H)
103 			if (s->trigger_regex) pcre_free(s->trigger_regex);
104 			if (s->download_regex) pcre_free(s->download_regex);
105 #endif
106 #if defined(HAVE_GDBM_H)
107 			if (s->db) gdbm_close(s->db);
108 #endif
109 #if defined(HAVE_MEMCACHE_H)
110 			if (s->mc) mc_free(s->mc);
111 #endif
112 
113 			free(s);
114 		}
115 		free(p->config_storage);
116 	}
117 
118 	buffer_free(p->tmp_buf);
119 
120 	free(p);
121 
122 	return HANDLER_GO_ON;
123 }
124 
125 /* handle plugin config and check values */
126 
SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults)127 SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) {
128 	plugin_data *p = p_d;
129 	size_t i = 0;
130 
131 
132 	config_values_t cv[] = {
133 		{ "trigger-before-download.gdbm-filename",   NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },       /* 0 */
134 		{ "trigger-before-download.trigger-url",     NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },       /* 1 */
135 		{ "trigger-before-download.download-url",    NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },       /* 2 */
136 		{ "trigger-before-download.deny-url",        NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },       /* 3 */
137 		{ "trigger-before-download.trigger-timeout", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION },        /* 4 */
138 		{ "trigger-before-download.memcache-hosts",  NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },        /* 5 */
139 		{ "trigger-before-download.memcache-namespace", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },    /* 6 */
140 		{ "trigger-before-download.debug",           NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION },      /* 7 */
141 		{ NULL,                        NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
142 	};
143 
144 	if (!p) return HANDLER_ERROR;
145 
146 	p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));
147 
148 	for (i = 0; i < srv->config_context->used; i++) {
149 		plugin_config *s;
150 #if defined(HAVE_PCRE_H)
151 		const char *errptr;
152 		int erroff;
153 #endif
154 
155 		s = calloc(1, sizeof(plugin_config));
156 		s->db_filename    = buffer_init();
157 		s->download_url   = buffer_init();
158 		s->trigger_url    = buffer_init();
159 		s->deny_url       = buffer_init();
160 		s->mc_hosts       = array_init();
161 		s->mc_namespace   = buffer_init();
162 
163 		cv[0].destination = s->db_filename;
164 		cv[1].destination = s->trigger_url;
165 		cv[2].destination = s->download_url;
166 		cv[3].destination = s->deny_url;
167 		cv[4].destination = &(s->trigger_timeout);
168 		cv[5].destination = s->mc_hosts;
169 		cv[6].destination = s->mc_namespace;
170 		cv[7].destination = &(s->debug);
171 
172 		p->config_storage[i] = s;
173 
174 		if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) {
175 			return HANDLER_ERROR;
176 		}
177 #if defined(HAVE_GDBM_H)
178 		if (!buffer_is_empty(s->db_filename)) {
179 			if (NULL == (s->db = gdbm_open(s->db_filename->ptr, 4096, GDBM_WRCREAT | GDBM_NOLOCK, S_IRUSR | S_IWUSR, 0))) {
180 				log_error_write(srv, __FILE__, __LINE__, "s",
181 						"gdbm-open failed");
182 				return HANDLER_ERROR;
183 			}
184 #ifdef FD_CLOEXEC
185 			fcntl(gdbm_fdesc(s->db), F_SETFD, FD_CLOEXEC);
186 #endif
187 		}
188 #endif
189 #if defined(HAVE_PCRE_H)
190 		if (!buffer_is_empty(s->download_url)) {
191 			if (NULL == (s->download_regex = pcre_compile(s->download_url->ptr,
192 								      0, &errptr, &erroff, NULL))) {
193 
194 				log_error_write(srv, __FILE__, __LINE__, "sbss",
195 						"compiling regex for download-url failed:",
196 						s->download_url, "pos:", erroff);
197 				return HANDLER_ERROR;
198 			}
199 		}
200 
201 		if (!buffer_is_empty(s->trigger_url)) {
202 			if (NULL == (s->trigger_regex = pcre_compile(s->trigger_url->ptr,
203 								     0, &errptr, &erroff, NULL))) {
204 
205 				log_error_write(srv, __FILE__, __LINE__, "sbss",
206 						"compiling regex for trigger-url failed:",
207 						s->trigger_url, "pos:", erroff);
208 
209 				return HANDLER_ERROR;
210 			}
211 		}
212 #endif
213 
214 		if (s->mc_hosts->used) {
215 #if defined(HAVE_MEMCACHE_H)
216 			size_t k;
217 			s->mc = mc_new();
218 
219 			for (k = 0; k < s->mc_hosts->used; k++) {
220 				data_string *ds = (data_string *)s->mc_hosts->data[k];
221 
222 				if (0 != mc_server_add4(s->mc, ds->value->ptr)) {
223 					log_error_write(srv, __FILE__, __LINE__, "sb",
224 							"connection to host failed:",
225 							ds->value);
226 
227 					return HANDLER_ERROR;
228 				}
229 			}
230 #else
231 			log_error_write(srv, __FILE__, __LINE__, "s",
232 					"memcache support is not compiled in but trigger-before-download.memcache-hosts is set, aborting");
233 			return HANDLER_ERROR;
234 #endif
235 		}
236 
237 
238 #if (!defined(HAVE_GDBM_H) && !defined(HAVE_MEMCACHE_H)) || !defined(HAVE_PCRE_H)
239 		log_error_write(srv, __FILE__, __LINE__, "s",
240 				"(either gdbm or libmemcache) and pcre are require, but were not found, aborting");
241 		return HANDLER_ERROR;
242 #endif
243 	}
244 
245 	return HANDLER_GO_ON;
246 }
247 
248 #define PATCH(x) \
249 	p->conf.x = s->x;
mod_trigger_b4_dl_patch_connection(server * srv,connection * con,plugin_data * p)250 static int mod_trigger_b4_dl_patch_connection(server *srv, connection *con, plugin_data *p) {
251 	size_t i, j;
252 	plugin_config *s = p->config_storage[0];
253 
254 #if defined(HAVE_GDBM)
255 	PATCH(db);
256 #endif
257 #if defined(HAVE_PCRE_H)
258 	PATCH(download_regex);
259 	PATCH(trigger_regex);
260 #endif
261 	PATCH(trigger_timeout);
262 	PATCH(deny_url);
263 	PATCH(mc_namespace);
264 	PATCH(debug);
265 #if defined(HAVE_MEMCACHE_H)
266 	PATCH(mc);
267 #endif
268 
269 	/* skip the first, the global context */
270 	for (i = 1; i < srv->config_context->used; i++) {
271 		data_config *dc = (data_config *)srv->config_context->data[i];
272 		s = p->config_storage[i];
273 
274 		/* condition didn't match */
275 		if (!config_check_cond(srv, con, dc)) continue;
276 
277 		/* merge config */
278 		for (j = 0; j < dc->value->used; j++) {
279 			data_unset *du = dc->value->data[j];
280 
281 			if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.download-url"))) {
282 #if defined(HAVE_PCRE_H)
283 				PATCH(download_regex);
284 #endif
285 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-url"))) {
286 # if defined(HAVE_PCRE_H)
287 				PATCH(trigger_regex);
288 # endif
289 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) {
290 #if defined(HAVE_GDBM_H)
291 				PATCH(db);
292 #endif
293 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-timeout"))) {
294 				PATCH(trigger_timeout);
295 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.debug"))) {
296 				PATCH(debug);
297 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.deny-url"))) {
298 				PATCH(deny_url);
299 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-namespace"))) {
300 				PATCH(mc_namespace);
301 			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) {
302 #if defined(HAVE_MEMCACHE_H)
303 				PATCH(mc);
304 #endif
305 			}
306 		}
307 	}
308 
309 	return 0;
310 }
311 #undef PATCH
312 
URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler)313 URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) {
314 	plugin_data *p = p_d;
315 	const char *remote_ip;
316 	data_string *ds;
317 
318 #if defined(HAVE_PCRE_H)
319 	int n;
320 # define N 10
321 	int ovec[N * 3];
322 
323 	if (con->mode != DIRECT) return HANDLER_GO_ON;
324 
325 	if (con->uri.path->used == 0) return HANDLER_GO_ON;
326 
327 	mod_trigger_b4_dl_patch_connection(srv, con, p);
328 
329 	if (!p->conf.trigger_regex || !p->conf.download_regex) return HANDLER_GO_ON;
330 
331 # if !defined(HAVE_GDBM_H) && !defined(HAVE_MEMCACHE_H)
332 	return HANDLER_GO_ON;
333 # elif defined(HAVE_GDBM_H) && defined(HAVE_MEMCACHE_H)
334 	if (!p->conf.db && !p->conf.mc) return HANDLER_GO_ON;
335 	if (p->conf.db && p->conf.mc) {
336 		/* can't decide which one */
337 
338 		return HANDLER_GO_ON;
339 	}
340 # elif defined(HAVE_GDBM_H)
341 	if (!p->conf.db) return HANDLER_GO_ON;
342 # else
343 	if (!p->conf.mc) return HANDLER_GO_ON;
344 # endif
345 
346 	if (NULL != (ds = (data_string *)array_get_element(con->request.headers, "X-Forwarded-For"))) {
347 		/* X-Forwarded-For contains the ip behind the proxy */
348 
349 		remote_ip = ds->value->ptr;
350 
351 		/* memcache can't handle spaces */
352 	} else {
353 		remote_ip = inet_ntop_cache_get_ip(srv, &(con->dst_addr));
354 	}
355 
356 	if (p->conf.debug) {
357 		log_error_write(srv, __FILE__, __LINE__, "ss", "(debug) remote-ip:", remote_ip);
358 	}
359 
360 	/* check if URL is a trigger -> insert IP into DB */
361 	if ((n = pcre_exec(p->conf.trigger_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) {
362 		if (n != PCRE_ERROR_NOMATCH) {
363 			log_error_write(srv, __FILE__, __LINE__, "sd",
364 					"execution error while matching:", n);
365 
366 			return HANDLER_ERROR;
367 		}
368 	} else {
369 # if defined(HAVE_GDBM_H)
370 		if (p->conf.db) {
371 			/* the trigger matched */
372 			datum key, val;
373 
374 			key.dptr = (char *)remote_ip;
375 			key.dsize = strlen(remote_ip);
376 
377 			val.dptr = (char *)&(srv->cur_ts);
378 			val.dsize = sizeof(srv->cur_ts);
379 
380 			if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
381 				log_error_write(srv, __FILE__, __LINE__, "s",
382 						"insert failed");
383 			}
384 		}
385 # endif
386 # if defined(HAVE_MEMCACHE_H)
387 		if (p->conf.mc) {
388 			size_t i;
389 			buffer_copy_string_buffer(p->tmp_buf, p->conf.mc_namespace);
390 			buffer_append_string(p->tmp_buf, remote_ip);
391 
392 			for (i = 0; i < p->tmp_buf->used - 1; i++) {
393 				if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
394 			}
395 
396 			if (p->conf.debug) {
397 				log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) triggered IP:", p->tmp_buf);
398 			}
399 
400 			if (0 != mc_set(p->conf.mc,
401 					CONST_BUF_LEN(p->tmp_buf),
402 					(char *)&(srv->cur_ts), sizeof(srv->cur_ts),
403 					p->conf.trigger_timeout, 0)) {
404 				log_error_write(srv, __FILE__, __LINE__, "s",
405 						"insert failed");
406 			}
407 		}
408 # endif
409 	}
410 
411 	/* check if URL is a download -> check IP in DB, update timestamp */
412 	if ((n = pcre_exec(p->conf.download_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) {
413 		if (n != PCRE_ERROR_NOMATCH) {
414 			log_error_write(srv, __FILE__, __LINE__, "sd",
415 					"execution error while matching: ", n);
416 			return HANDLER_ERROR;
417 		}
418 	} else {
419 		/* the download uri matched */
420 # if defined(HAVE_GDBM_H)
421 		if (p->conf.db) {
422 			datum key, val;
423 			time_t last_hit;
424 
425 			key.dptr = (char *)remote_ip;
426 			key.dsize = strlen(remote_ip);
427 
428 			val = gdbm_fetch(p->conf.db, key);
429 
430 			if (val.dptr == NULL) {
431 				/* not found, redirect */
432 
433 				response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
434 				con->http_status = 307;
435 				con->file_finished = 1;
436 
437 				return HANDLER_FINISHED;
438 			}
439 
440 			last_hit = *(time_t *)(val.dptr);
441 
442 			free(val.dptr);
443 
444 			if (srv->cur_ts - last_hit > p->conf.trigger_timeout) {
445 				/* found, but timeout, redirect */
446 
447 				response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
448 				con->http_status = 307;
449 				con->file_finished = 1;
450 
451 				if (p->conf.db) {
452 					if (0 != gdbm_delete(p->conf.db, key)) {
453 						log_error_write(srv, __FILE__, __LINE__, "s",
454 								"delete failed");
455 					}
456 				}
457 
458 				return HANDLER_FINISHED;
459 			}
460 
461 			val.dptr = (char *)&(srv->cur_ts);
462 			val.dsize = sizeof(srv->cur_ts);
463 
464 			if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
465 				log_error_write(srv, __FILE__, __LINE__, "s",
466 						"insert failed");
467 			}
468 		}
469 # endif
470 
471 # if defined(HAVE_MEMCACHE_H)
472 		if (p->conf.mc) {
473 			void *r;
474 			size_t i;
475 
476 			buffer_copy_string_buffer(p->tmp_buf, p->conf.mc_namespace);
477 			buffer_append_string(p->tmp_buf, remote_ip);
478 
479 			for (i = 0; i < p->tmp_buf->used - 1; i++) {
480 				if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
481 			}
482 
483 			if (p->conf.debug) {
484 				log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) checking IP:", p->tmp_buf);
485 			}
486 
487 			/**
488 			 *
489 			 * memcached is do expiration for us, as long as we can fetch it every thing is ok
490 			 * and the timestamp is updated
491 			 *
492 			 */
493 			if (NULL == (r = mc_aget(p->conf.mc,
494 						 CONST_BUF_LEN(p->tmp_buf)
495 						 ))) {
496 
497 				response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
498 
499 				con->http_status = 307;
500 				con->file_finished = 1;
501 
502 				return HANDLER_FINISHED;
503 			}
504 
505 			free(r);
506 
507 			/* set a new timeout */
508 			if (0 != mc_set(p->conf.mc,
509 					CONST_BUF_LEN(p->tmp_buf),
510 					(char *)&(srv->cur_ts), sizeof(srv->cur_ts),
511 					p->conf.trigger_timeout, 0)) {
512 				log_error_write(srv, __FILE__, __LINE__, "s",
513 						"insert failed");
514 			}
515 		}
516 # endif
517 	}
518 
519 #else
520 	UNUSED(srv);
521 	UNUSED(con);
522 	UNUSED(p_d);
523 #endif
524 
525 	return HANDLER_GO_ON;
526 }
527 
528 #if defined(HAVE_GDBM_H)
TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger)529 TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) {
530 	plugin_data *p = p_d;
531 	size_t i;
532 
533 	/* check DB each minute */
534 	if (srv->cur_ts % 60 != 0) return HANDLER_GO_ON;
535 
536 	/* cleanup */
537 	for (i = 0; i < srv->config_context->used; i++) {
538 		plugin_config *s = p->config_storage[i];
539 		datum key, val, okey;
540 
541 		if (!s->db) continue;
542 
543 		okey.dptr = NULL;
544 
545 		/* according to the manual this loop + delete does delete all entries on its way
546 		 *
547 		 * we don't care as the next round will remove them. We don't have to perfect here.
548 		 */
549 		for (key = gdbm_firstkey(s->db); key.dptr; key = gdbm_nextkey(s->db, okey)) {
550 			time_t last_hit;
551 			if (okey.dptr) {
552 				free(okey.dptr);
553 				okey.dptr = NULL;
554 			}
555 
556 			val = gdbm_fetch(s->db, key);
557 
558 			last_hit = *(time_t *)(val.dptr);
559 
560 			free(val.dptr);
561 
562 			if (srv->cur_ts - last_hit > s->trigger_timeout) {
563 				gdbm_delete(s->db, key);
564 			}
565 
566 			okey = key;
567 		}
568 		if (okey.dptr) free(okey.dptr);
569 
570 		/* reorg once a day */
571 		if ((srv->cur_ts % (60 * 60 * 24) != 0)) gdbm_reorganize(s->db);
572 	}
573 	return HANDLER_GO_ON;
574 }
575 #endif
576 
577 /* this function is called at dlopen() time and inits the callbacks */
578 
579 int mod_trigger_b4_dl_plugin_init(plugin *p);
mod_trigger_b4_dl_plugin_init(plugin * p)580 int mod_trigger_b4_dl_plugin_init(plugin *p) {
581 	p->version     = LIGHTTPD_VERSION_ID;
582 	p->name        = buffer_init_string("trigger_b4_dl");
583 
584 	p->init        = mod_trigger_b4_dl_init;
585 	p->handle_uri_clean  = mod_trigger_b4_dl_uri_handler;
586 	p->set_defaults  = mod_trigger_b4_dl_set_defaults;
587 #if defined(HAVE_GDBM_H)
588 	p->handle_trigger  = mod_trigger_b4_dl_handle_trigger;
589 #endif
590 	p->cleanup     = mod_trigger_b4_dl_free;
591 
592 	p->data        = NULL;
593 
594 	return 0;
595 }
596