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