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