[stat_cache] FAM: ignore event with no valid match
[lighttpd.git] / src / mod_trigger_b4_dl.c
blobc78d5c8687a8cabd600babb58b63a6ab1c5ca55f
1 #include "first.h"
3 #include "base.h"
4 #include "fdevent.h"
5 #include "log.h"
6 #include "buffer.h"
7 #include "http_header.h"
9 #include "plugin.h"
11 #include <sys/stat.h>
12 #include <stdlib.h>
13 #include <string.h>
15 #if defined(HAVE_GDBM_H)
16 # include <gdbm.h>
17 #endif
19 #if defined(HAVE_PCRE_H)
20 # include <pcre.h>
21 #endif
23 #if defined(USE_MEMCACHED)
24 # include <libmemcached/memcached.h>
25 #endif
27 /**
28 * this is a trigger_b4_dl for a lighttpd plugin
32 /* plugin config for all request/connections */
34 typedef struct {
35 buffer *db_filename;
37 buffer *trigger_url;
38 buffer *download_url;
39 buffer *deny_url;
41 array *mc_hosts;
42 buffer *mc_namespace;
43 #if defined(HAVE_PCRE_H)
44 pcre *trigger_regex;
45 pcre *download_regex;
46 #endif
47 #if defined(HAVE_GDBM_H)
48 GDBM_FILE db;
49 #endif
51 #if defined(USE_MEMCACHED)
52 memcached_st *memc;
53 #endif
55 unsigned short trigger_timeout;
56 unsigned short debug;
57 } plugin_config;
59 typedef struct {
60 PLUGIN_DATA;
62 buffer *tmp_buf;
64 plugin_config **config_storage;
66 plugin_config conf;
67 } plugin_data;
69 /* init the plugin data */
70 INIT_FUNC(mod_trigger_b4_dl_init) {
71 plugin_data *p;
73 p = calloc(1, sizeof(*p));
75 p->tmp_buf = buffer_init();
77 return p;
80 /* detroy the plugin data */
81 FREE_FUNC(mod_trigger_b4_dl_free) {
82 plugin_data *p = p_d;
84 UNUSED(srv);
86 if (!p) return HANDLER_GO_ON;
88 if (p->config_storage) {
89 size_t i;
90 for (i = 0; i < srv->config_context->used; i++) {
91 plugin_config *s = p->config_storage[i];
93 if (NULL == s) continue;
95 buffer_free(s->db_filename);
96 buffer_free(s->download_url);
97 buffer_free(s->trigger_url);
98 buffer_free(s->deny_url);
100 buffer_free(s->mc_namespace);
101 array_free(s->mc_hosts);
103 #if defined(HAVE_PCRE_H)
104 if (s->trigger_regex) pcre_free(s->trigger_regex);
105 if (s->download_regex) pcre_free(s->download_regex);
106 #endif
107 #if defined(HAVE_GDBM_H)
108 if (s->db) gdbm_close(s->db);
109 #endif
110 #if defined(USE_MEMCACHED)
111 if (s->memc) memcached_free(s->memc);
112 #endif
114 free(s);
116 free(p->config_storage);
119 buffer_free(p->tmp_buf);
121 free(p);
123 return HANDLER_GO_ON;
126 /* handle plugin config and check values */
128 SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) {
129 plugin_data *p = p_d;
130 size_t i = 0;
133 config_values_t cv[] = {
134 { "trigger-before-download.gdbm-filename", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
135 { "trigger-before-download.trigger-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
136 { "trigger-before-download.download-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
137 { "trigger-before-download.deny-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
138 { "trigger-before-download.trigger-timeout", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
139 { "trigger-before-download.memcache-hosts", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
140 { "trigger-before-download.memcache-namespace", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 6 */
141 { "trigger-before-download.debug", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 7 */
142 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
145 if (!p) return HANDLER_ERROR;
147 p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *));
149 for (i = 0; i < srv->config_context->used; i++) {
150 data_config const* config = (data_config const*)srv->config_context->data[i];
151 plugin_config *s;
152 #if defined(HAVE_PCRE_H)
153 const char *errptr;
154 int erroff;
155 #endif
157 s = calloc(1, sizeof(plugin_config));
158 s->db_filename = buffer_init();
159 s->download_url = buffer_init();
160 s->trigger_url = buffer_init();
161 s->deny_url = buffer_init();
162 s->mc_hosts = array_init();
163 s->mc_namespace = buffer_init();
165 cv[0].destination = s->db_filename;
166 cv[1].destination = s->trigger_url;
167 cv[2].destination = s->download_url;
168 cv[3].destination = s->deny_url;
169 cv[4].destination = &(s->trigger_timeout);
170 cv[5].destination = s->mc_hosts;
171 cv[6].destination = s->mc_namespace;
172 cv[7].destination = &(s->debug);
174 p->config_storage[i] = s;
176 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
177 return HANDLER_ERROR;
179 #if defined(HAVE_GDBM_H)
180 if (!buffer_string_is_empty(s->db_filename)) {
181 if (NULL == (s->db = gdbm_open(s->db_filename->ptr, 4096, GDBM_WRCREAT | GDBM_NOLOCK, S_IRUSR | S_IWUSR, 0))) {
182 log_error_write(srv, __FILE__, __LINE__, "s",
183 "gdbm-open failed");
184 return HANDLER_ERROR;
186 fdevent_setfd_cloexec(gdbm_fdesc(s->db));
188 #endif
189 #if defined(HAVE_PCRE_H)
190 if (!buffer_string_is_empty(s->download_url)) {
191 if (NULL == (s->download_regex = pcre_compile(s->download_url->ptr,
192 0, &errptr, &erroff, NULL))) {
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;
201 if (!buffer_string_is_empty(s->trigger_url)) {
202 if (NULL == (s->trigger_regex = pcre_compile(s->trigger_url->ptr,
203 0, &errptr, &erroff, NULL))) {
205 log_error_write(srv, __FILE__, __LINE__, "sbss",
206 "compiling regex for trigger-url failed:",
207 s->trigger_url, "pos:", erroff);
209 return HANDLER_ERROR;
212 #endif
214 if (!array_is_vlist(s->mc_hosts)) {
215 log_error_write(srv, __FILE__, __LINE__, "s",
216 "unexpected value for trigger-before-download.memcache-hosts; expected list of \"host\"");
217 return HANDLER_ERROR;
220 if (s->mc_hosts->used) {
221 #if defined(USE_MEMCACHED)
222 buffer *option_string = buffer_init();
223 size_t k;
226 data_string *ds = (data_string *)s->mc_hosts->data[0];
228 buffer_append_string_len(option_string, CONST_STR_LEN("--SERVER="));
229 buffer_append_string_buffer(option_string, ds->value);
232 for (k = 1; k < s->mc_hosts->used; k++) {
233 data_string *ds = (data_string *)s->mc_hosts->data[k];
235 buffer_append_string_len(option_string, CONST_STR_LEN(" --SERVER="));
236 buffer_append_string_buffer(option_string, ds->value);
239 s->memc = memcached(CONST_BUF_LEN(option_string));
241 if (NULL == s->memc) {
242 log_error_write(srv, __FILE__, __LINE__, "sb",
243 "configuring memcached failed for option string:",
244 option_string);
246 buffer_free(option_string);
248 if (NULL == s->memc) return HANDLER_ERROR;
249 #else
250 log_error_write(srv, __FILE__, __LINE__, "s",
251 "memcache support is not compiled in but trigger-before-download.memcache-hosts is set, aborting");
252 return HANDLER_ERROR;
253 #endif
257 return HANDLER_GO_ON;
260 #if defined(HAVE_PCRE_H)
262 #define PATCH(x) \
263 p->conf.x = s->x;
264 static int mod_trigger_b4_dl_patch_connection(server *srv, connection *con, plugin_data *p) {
265 size_t i, j;
266 plugin_config *s = p->config_storage[0];
268 #if defined(HAVE_GDBM)
269 PATCH(db);
270 #endif
271 #if defined(HAVE_PCRE_H)
272 PATCH(download_regex);
273 PATCH(trigger_regex);
274 #endif
275 PATCH(trigger_timeout);
276 PATCH(deny_url);
277 PATCH(mc_namespace);
278 PATCH(debug);
279 #if defined(USE_MEMCACHED)
280 PATCH(memc);
281 #endif
283 /* skip the first, the global context */
284 for (i = 1; i < srv->config_context->used; i++) {
285 data_config *dc = (data_config *)srv->config_context->data[i];
286 s = p->config_storage[i];
288 /* condition didn't match */
289 if (!config_check_cond(srv, con, dc)) continue;
291 /* merge config */
292 for (j = 0; j < dc->value->used; j++) {
293 data_unset *du = dc->value->data[j];
295 if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.download-url"))) {
296 #if defined(HAVE_PCRE_H)
297 PATCH(download_regex);
298 #endif
299 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-url"))) {
300 # if defined(HAVE_PCRE_H)
301 PATCH(trigger_regex);
302 # endif
303 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) {
304 #if defined(HAVE_GDBM_H)
305 PATCH(db);
306 #endif
307 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-timeout"))) {
308 PATCH(trigger_timeout);
309 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.debug"))) {
310 PATCH(debug);
311 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.deny-url"))) {
312 PATCH(deny_url);
313 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-namespace"))) {
314 PATCH(mc_namespace);
315 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) {
316 #if defined(USE_MEMCACHED)
317 PATCH(memc);
318 #endif
323 return 0;
325 #undef PATCH
327 #endif
329 URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) {
330 #if defined(HAVE_PCRE_H)
331 plugin_data *p = p_d;
332 const char *remote_ip;
333 buffer *vb;
335 int n;
336 # define N 10
337 int ovec[N * 3];
339 if (con->mode != DIRECT) return HANDLER_GO_ON;
341 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
343 mod_trigger_b4_dl_patch_connection(srv, con, p);
345 if (!p->conf.trigger_regex || !p->conf.download_regex) return HANDLER_GO_ON;
347 # if !defined(HAVE_GDBM_H) && !defined(USE_MEMCACHED)
348 return HANDLER_GO_ON;
349 # elif defined(HAVE_GDBM_H) && defined(USE_MEMCACHED)
350 if (!p->conf.db && !p->conf.memc) return HANDLER_GO_ON;
351 if (p->conf.db && p->conf.memc) {
352 /* can't decide which one */
354 return HANDLER_GO_ON;
356 # elif defined(HAVE_GDBM_H)
357 if (!p->conf.db) return HANDLER_GO_ON;
358 # else
359 if (!p->conf.memc) return HANDLER_GO_ON;
360 # endif
362 if (NULL != (vb = http_header_request_get(con, HTTP_HEADER_X_FORWARDED_FOR, CONST_STR_LEN("X-Forwarded-For")))) {
363 /* X-Forwarded-For contains the ip behind the proxy */
365 remote_ip = vb->ptr;
367 /* memcache can't handle spaces */
368 } else {
369 remote_ip = con->dst_addr_buf->ptr;
372 if (p->conf.debug) {
373 log_error_write(srv, __FILE__, __LINE__, "ss", "(debug) remote-ip:", remote_ip);
376 /* check if URL is a trigger -> insert IP into DB */
377 if ((n = pcre_exec(p->conf.trigger_regex, NULL, CONST_BUF_LEN(con->uri.path), 0, 0, ovec, 3 * N)) < 0) {
378 if (n != PCRE_ERROR_NOMATCH) {
379 log_error_write(srv, __FILE__, __LINE__, "sd",
380 "execution error while matching:", n);
382 return HANDLER_ERROR;
384 } else {
385 # if defined(HAVE_GDBM_H)
386 if (p->conf.db) {
387 /* the trigger matched */
388 datum key, val;
390 key.dptr = (char *)remote_ip;
391 key.dsize = strlen(remote_ip);
393 val.dptr = (char *)&(srv->cur_ts);
394 val.dsize = sizeof(srv->cur_ts);
396 if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
397 log_error_write(srv, __FILE__, __LINE__, "s",
398 "insert failed");
401 # endif
402 # if defined(USE_MEMCACHED)
403 if (p->conf.memc) {
404 size_t i, len;
405 buffer_copy_buffer(p->tmp_buf, p->conf.mc_namespace);
406 buffer_append_string(p->tmp_buf, remote_ip);
408 len = buffer_string_length(p->tmp_buf);
409 for (i = 0; i < len; i++) {
410 if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
413 if (p->conf.debug) {
414 log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) triggered IP:", p->tmp_buf);
417 if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc,
418 CONST_BUF_LEN(p->tmp_buf),
419 (const char *)&(srv->cur_ts), sizeof(srv->cur_ts),
420 p->conf.trigger_timeout, 0)) {
421 log_error_write(srv, __FILE__, __LINE__, "s",
422 "insert failed");
425 # endif
428 /* check if URL is a download -> check IP in DB, update timestamp */
429 if ((n = pcre_exec(p->conf.download_regex, NULL, CONST_BUF_LEN(con->uri.path), 0, 0, ovec, 3 * N)) < 0) {
430 if (n != PCRE_ERROR_NOMATCH) {
431 log_error_write(srv, __FILE__, __LINE__, "sd",
432 "execution error while matching: ", n);
433 return HANDLER_ERROR;
435 } else {
436 /* the download uri matched */
437 # if defined(HAVE_GDBM_H)
438 if (p->conf.db) {
439 datum key, val;
440 time_t last_hit;
442 key.dptr = (char *)remote_ip;
443 key.dsize = strlen(remote_ip);
445 val = gdbm_fetch(p->conf.db, key);
447 if (val.dptr == NULL) {
448 /* not found, redirect */
450 http_header_response_set(con, HTTP_HEADER_LOCATION, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
451 con->http_status = 307;
452 con->file_finished = 1;
454 return HANDLER_FINISHED;
457 memcpy(&last_hit, val.dptr, sizeof(time_t));
459 free(val.dptr);
461 if (srv->cur_ts - last_hit > p->conf.trigger_timeout) {
462 /* found, but timeout, redirect */
464 http_header_response_set(con, HTTP_HEADER_LOCATION, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
465 con->http_status = 307;
466 con->file_finished = 1;
468 if (p->conf.db) {
469 if (0 != gdbm_delete(p->conf.db, key)) {
470 log_error_write(srv, __FILE__, __LINE__, "s",
471 "delete failed");
475 return HANDLER_FINISHED;
478 val.dptr = (char *)&(srv->cur_ts);
479 val.dsize = sizeof(srv->cur_ts);
481 if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
482 log_error_write(srv, __FILE__, __LINE__, "s",
483 "insert failed");
486 # endif
488 # if defined(USE_MEMCACHED)
489 if (p->conf.memc) {
490 size_t i, len;
492 buffer_copy_buffer(p->tmp_buf, p->conf.mc_namespace);
493 buffer_append_string(p->tmp_buf, remote_ip);
495 len = buffer_string_length(p->tmp_buf);
496 for (i = 0; i < len; i++) {
497 if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
500 if (p->conf.debug) {
501 log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) checking IP:", p->tmp_buf);
506 * memcached is do expiration for us, as long as we can fetch it every thing is ok
507 * and the timestamp is updated
510 if (MEMCACHED_SUCCESS != memcached_exist(p->conf.memc, CONST_BUF_LEN(p->tmp_buf))) {
511 http_header_response_set(con, HTTP_HEADER_LOCATION, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
513 con->http_status = 307;
514 con->file_finished = 1;
516 return HANDLER_FINISHED;
519 /* set a new timeout */
520 if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc,
521 CONST_BUF_LEN(p->tmp_buf),
522 (const char *)&(srv->cur_ts), sizeof(srv->cur_ts),
523 p->conf.trigger_timeout, 0)) {
524 log_error_write(srv, __FILE__, __LINE__, "s",
525 "insert failed");
528 # endif
531 #else
532 UNUSED(srv);
533 UNUSED(con);
534 UNUSED(p_d);
535 #endif
537 return HANDLER_GO_ON;
540 #if defined(HAVE_GDBM_H)
541 TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) {
542 plugin_data *p = p_d;
543 size_t i;
545 /* check DB each minute */
546 if (srv->cur_ts % 60 != 0) return HANDLER_GO_ON;
548 /* cleanup */
549 for (i = 0; i < srv->config_context->used; i++) {
550 plugin_config *s = p->config_storage[i];
551 datum key, val, okey;
553 if (!s->db) continue;
555 okey.dptr = NULL;
557 /* according to the manual this loop + delete does delete all entries on its way
559 * we don't care as the next round will remove them. We don't have to perfect here.
561 for (key = gdbm_firstkey(s->db); key.dptr; key = gdbm_nextkey(s->db, okey)) {
562 time_t last_hit;
563 if (okey.dptr) {
564 free(okey.dptr);
565 okey.dptr = NULL;
568 val = gdbm_fetch(s->db, key);
570 memcpy(&last_hit, val.dptr, sizeof(time_t));
572 free(val.dptr);
574 if (srv->cur_ts - last_hit > s->trigger_timeout) {
575 gdbm_delete(s->db, key);
578 okey = key;
580 if (okey.dptr) free(okey.dptr);
582 /* reorg once a day */
583 if ((srv->cur_ts % (60 * 60 * 24) != 0)) gdbm_reorganize(s->db);
585 return HANDLER_GO_ON;
587 #endif
589 /* this function is called at dlopen() time and inits the callbacks */
591 int mod_trigger_b4_dl_plugin_init(plugin *p);
592 int mod_trigger_b4_dl_plugin_init(plugin *p) {
593 p->version = LIGHTTPD_VERSION_ID;
594 p->name = buffer_init_string("trigger_b4_dl");
596 p->init = mod_trigger_b4_dl_init;
597 p->handle_uri_clean = mod_trigger_b4_dl_uri_handler;
598 p->set_defaults = mod_trigger_b4_dl_set_defaults;
599 #if defined(HAVE_GDBM_H)
600 p->handle_trigger = mod_trigger_b4_dl_handle_trigger;
601 #endif
602 p->cleanup = mod_trigger_b4_dl_free;
604 p->data = NULL;
606 return 0;