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