[mod_accesslog] %{ratio}n logs compression ratio (fixes #2133)
[lighttpd.git] / src / mod_trigger_b4_dl.c
blobffdb6dc8486b013d7c8c3dfe92a46cfc29e68c34
1 #include "first.h"
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
7 #include "plugin.h"
8 #include "response.h"
9 #include "inet_ntop_cache.h"
11 #include <ctype.h>
12 #include <stdlib.h>
13 #include <fcntl.h>
14 #include <string.h>
16 #if defined(HAVE_GDBM_H)
17 # include <gdbm.h>
18 #endif
20 #if defined(HAVE_PCRE_H)
21 # include <pcre.h>
22 #endif
24 #if defined(USE_MEMCACHED)
25 # include <libmemcached/memcached.h>
26 #endif
28 /**
29 * this is a trigger_b4_dl for a lighttpd plugin
33 /* plugin config for all request/connections */
35 typedef struct {
36 buffer *db_filename;
38 buffer *trigger_url;
39 buffer *download_url;
40 buffer *deny_url;
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
52 #if defined(USE_MEMCACHED)
53 memcached_st *memc;
54 #endif
56 unsigned short trigger_timeout;
57 unsigned short debug;
58 } plugin_config;
60 typedef struct {
61 PLUGIN_DATA;
63 buffer *tmp_buf;
65 plugin_config **config_storage;
67 plugin_config conf;
68 } plugin_data;
70 /* init the plugin data */
71 INIT_FUNC(mod_trigger_b4_dl_init) {
72 plugin_data *p;
74 p = calloc(1, sizeof(*p));
76 p->tmp_buf = buffer_init();
78 return p;
81 /* detroy the plugin data */
82 FREE_FUNC(mod_trigger_b4_dl_free) {
83 plugin_data *p = p_d;
85 UNUSED(srv);
87 if (!p) return HANDLER_GO_ON;
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];
94 if (NULL == s) continue;
96 buffer_free(s->db_filename);
97 buffer_free(s->download_url);
98 buffer_free(s->trigger_url);
99 buffer_free(s->deny_url);
101 buffer_free(s->mc_namespace);
102 array_free(s->mc_hosts);
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(USE_MEMCACHED)
112 if (s->memc) memcached_free(s->memc);
113 #endif
115 free(s);
117 free(p->config_storage);
120 buffer_free(p->tmp_buf);
122 free(p);
124 return HANDLER_GO_ON;
127 /* handle plugin config and check values */
129 SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) {
130 plugin_data *p = p_d;
131 size_t i = 0;
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 }
146 if (!p) return HANDLER_ERROR;
148 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
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
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();
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);
175 p->config_storage[i] = s;
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;
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;
187 fd_close_on_exec(gdbm_fdesc(s->db));
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))) {
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;
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))) {
206 log_error_write(srv, __FILE__, __LINE__, "sbss",
207 "compiling regex for trigger-url failed:",
208 s->trigger_url, "pos:", erroff);
210 return HANDLER_ERROR;
213 #endif
215 if (s->mc_hosts->used) {
216 #if defined(USE_MEMCACHED)
217 buffer *option_string = buffer_init();
218 size_t k;
221 data_string *ds = (data_string *)s->mc_hosts->data[0];
223 buffer_append_string_len(option_string, CONST_STR_LEN("--SERVER="));
224 buffer_append_string_buffer(option_string, ds->value);
227 for (k = 1; k < s->mc_hosts->used; k++) {
228 data_string *ds = (data_string *)s->mc_hosts->data[k];
230 buffer_append_string_len(option_string, CONST_STR_LEN(" --SERVER="));
231 buffer_append_string_buffer(option_string, ds->value);
234 s->memc = memcached(CONST_BUF_LEN(option_string));
236 if (NULL == s->memc) {
237 log_error_write(srv, __FILE__, __LINE__, "sb",
238 "configuring memcached failed for option string:",
239 option_string);
241 buffer_free(option_string);
243 if (NULL == s->memc) return HANDLER_ERROR;
244 #else
245 log_error_write(srv, __FILE__, __LINE__, "s",
246 "memcache support is not compiled in but trigger-before-download.memcache-hosts is set, aborting");
247 return HANDLER_ERROR;
248 #endif
252 return HANDLER_GO_ON;
255 #define PATCH(x) \
256 p->conf.x = s->x;
257 static int mod_trigger_b4_dl_patch_connection(server *srv, connection *con, plugin_data *p) {
258 size_t i, j;
259 plugin_config *s = p->config_storage[0];
261 #if defined(HAVE_GDBM)
262 PATCH(db);
263 #endif
264 #if defined(HAVE_PCRE_H)
265 PATCH(download_regex);
266 PATCH(trigger_regex);
267 #endif
268 PATCH(trigger_timeout);
269 PATCH(deny_url);
270 PATCH(mc_namespace);
271 PATCH(debug);
272 #if defined(USE_MEMCACHED)
273 PATCH(memc);
274 #endif
276 /* skip the first, the global context */
277 for (i = 1; i < srv->config_context->used; i++) {
278 data_config *dc = (data_config *)srv->config_context->data[i];
279 s = p->config_storage[i];
281 /* condition didn't match */
282 if (!config_check_cond(srv, con, dc)) continue;
284 /* merge config */
285 for (j = 0; j < dc->value->used; j++) {
286 data_unset *du = dc->value->data[j];
288 if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.download-url"))) {
289 #if defined(HAVE_PCRE_H)
290 PATCH(download_regex);
291 #endif
292 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-url"))) {
293 # if defined(HAVE_PCRE_H)
294 PATCH(trigger_regex);
295 # endif
296 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) {
297 #if defined(HAVE_GDBM_H)
298 PATCH(db);
299 #endif
300 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-timeout"))) {
301 PATCH(trigger_timeout);
302 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.debug"))) {
303 PATCH(debug);
304 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.deny-url"))) {
305 PATCH(deny_url);
306 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-namespace"))) {
307 PATCH(mc_namespace);
308 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) {
309 #if defined(USE_MEMCACHED)
310 PATCH(memc);
311 #endif
316 return 0;
318 #undef PATCH
320 URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) {
321 plugin_data *p = p_d;
322 const char *remote_ip;
323 data_string *ds;
325 #if defined(HAVE_PCRE_H)
326 int n;
327 # define N 10
328 int ovec[N * 3];
330 if (con->mode != DIRECT) return HANDLER_GO_ON;
332 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
334 mod_trigger_b4_dl_patch_connection(srv, con, p);
336 if (!p->conf.trigger_regex || !p->conf.download_regex) return HANDLER_GO_ON;
338 # if !defined(HAVE_GDBM_H) && !defined(USE_MEMCACHED)
339 return HANDLER_GO_ON;
340 # elif defined(HAVE_GDBM_H) && defined(USE_MEMCACHED)
341 if (!p->conf.db && !p->conf.memc) return HANDLER_GO_ON;
342 if (p->conf.db && p->conf.memc) {
343 /* can't decide which one */
345 return HANDLER_GO_ON;
347 # elif defined(HAVE_GDBM_H)
348 if (!p->conf.db) return HANDLER_GO_ON;
349 # else
350 if (!p->conf.memc) return HANDLER_GO_ON;
351 # endif
353 if (NULL != (ds = (data_string *)array_get_element(con->request.headers, "X-Forwarded-For"))) {
354 /* X-Forwarded-For contains the ip behind the proxy */
356 remote_ip = ds->value->ptr;
358 /* memcache can't handle spaces */
359 } else {
360 remote_ip = inet_ntop_cache_get_ip(srv, &(con->dst_addr));
363 if (p->conf.debug) {
364 log_error_write(srv, __FILE__, __LINE__, "ss", "(debug) remote-ip:", remote_ip);
367 /* check if URL is a trigger -> insert IP into DB */
368 if ((n = pcre_exec(p->conf.trigger_regex, NULL, CONST_BUF_LEN(con->uri.path), 0, 0, ovec, 3 * N)) < 0) {
369 if (n != PCRE_ERROR_NOMATCH) {
370 log_error_write(srv, __FILE__, __LINE__, "sd",
371 "execution error while matching:", n);
373 return HANDLER_ERROR;
375 } else {
376 # if defined(HAVE_GDBM_H)
377 if (p->conf.db) {
378 /* the trigger matched */
379 datum key, val;
381 key.dptr = (char *)remote_ip;
382 key.dsize = strlen(remote_ip);
384 val.dptr = (char *)&(srv->cur_ts);
385 val.dsize = sizeof(srv->cur_ts);
387 if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
388 log_error_write(srv, __FILE__, __LINE__, "s",
389 "insert failed");
392 # endif
393 # if defined(USE_MEMCACHED)
394 if (p->conf.memc) {
395 size_t i, len;
396 buffer_copy_buffer(p->tmp_buf, p->conf.mc_namespace);
397 buffer_append_string(p->tmp_buf, remote_ip);
399 len = buffer_string_length(p->tmp_buf);
400 for (i = 0; i < len; i++) {
401 if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
404 if (p->conf.debug) {
405 log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) triggered IP:", p->tmp_buf);
408 if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc,
409 CONST_BUF_LEN(p->tmp_buf),
410 (const char *)&(srv->cur_ts), sizeof(srv->cur_ts),
411 p->conf.trigger_timeout, 0)) {
412 log_error_write(srv, __FILE__, __LINE__, "s",
413 "insert failed");
416 # endif
419 /* check if URL is a download -> check IP in DB, update timestamp */
420 if ((n = pcre_exec(p->conf.download_regex, NULL, CONST_BUF_LEN(con->uri.path), 0, 0, ovec, 3 * N)) < 0) {
421 if (n != PCRE_ERROR_NOMATCH) {
422 log_error_write(srv, __FILE__, __LINE__, "sd",
423 "execution error while matching: ", n);
424 return HANDLER_ERROR;
426 } else {
427 /* the download uri matched */
428 # if defined(HAVE_GDBM_H)
429 if (p->conf.db) {
430 datum key, val;
431 time_t last_hit;
433 key.dptr = (char *)remote_ip;
434 key.dsize = strlen(remote_ip);
436 val = gdbm_fetch(p->conf.db, key);
438 if (val.dptr == NULL) {
439 /* not found, redirect */
441 response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
442 con->http_status = 307;
443 con->file_finished = 1;
445 return HANDLER_FINISHED;
448 memcpy(&last_hit, val.dptr, sizeof(time_t));
450 free(val.dptr);
452 if (srv->cur_ts - last_hit > p->conf.trigger_timeout) {
453 /* found, but timeout, redirect */
455 response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
456 con->http_status = 307;
457 con->file_finished = 1;
459 if (p->conf.db) {
460 if (0 != gdbm_delete(p->conf.db, key)) {
461 log_error_write(srv, __FILE__, __LINE__, "s",
462 "delete failed");
466 return HANDLER_FINISHED;
469 val.dptr = (char *)&(srv->cur_ts);
470 val.dsize = sizeof(srv->cur_ts);
472 if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
473 log_error_write(srv, __FILE__, __LINE__, "s",
474 "insert failed");
477 # endif
479 # if defined(USE_MEMCACHED)
480 if (p->conf.memc) {
481 size_t i, len;
483 buffer_copy_buffer(p->tmp_buf, p->conf.mc_namespace);
484 buffer_append_string(p->tmp_buf, remote_ip);
486 len = buffer_string_length(p->tmp_buf);
487 for (i = 0; i < len; i++) {
488 if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
491 if (p->conf.debug) {
492 log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) checking IP:", p->tmp_buf);
497 * memcached is do expiration for us, as long as we can fetch it every thing is ok
498 * and the timestamp is updated
501 if (MEMCACHED_SUCCESS != memcached_exist(p->conf.memc, CONST_BUF_LEN(p->tmp_buf))) {
502 response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
504 con->http_status = 307;
505 con->file_finished = 1;
507 return HANDLER_FINISHED;
510 /* set a new timeout */
511 if (MEMCACHED_SUCCESS != memcached_set(p->conf.memc,
512 CONST_BUF_LEN(p->tmp_buf),
513 (const 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");
519 # endif
522 #else
523 UNUSED(srv);
524 UNUSED(con);
525 UNUSED(p_d);
526 #endif
528 return HANDLER_GO_ON;
531 #if defined(HAVE_GDBM_H)
532 TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) {
533 plugin_data *p = p_d;
534 size_t i;
536 /* check DB each minute */
537 if (srv->cur_ts % 60 != 0) return HANDLER_GO_ON;
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;
544 if (!s->db) continue;
546 okey.dptr = NULL;
548 /* according to the manual this loop + delete does delete all entries on its way
550 * we don't care as the next round will remove them. We don't have to perfect here.
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;
559 val = gdbm_fetch(s->db, key);
561 memcpy(&last_hit, val.dptr, sizeof(time_t));
563 free(val.dptr);
565 if (srv->cur_ts - last_hit > s->trigger_timeout) {
566 gdbm_delete(s->db, key);
569 okey = key;
571 if (okey.dptr) free(okey.dptr);
573 /* reorg once a day */
574 if ((srv->cur_ts % (60 * 60 * 24) != 0)) gdbm_reorganize(s->db);
576 return HANDLER_GO_ON;
578 #endif
580 /* this function is called at dlopen() time and inits the callbacks */
582 int mod_trigger_b4_dl_plugin_init(plugin *p);
583 int mod_trigger_b4_dl_plugin_init(plugin *p) {
584 p->version = LIGHTTPD_VERSION_ID;
585 p->name = buffer_init_string("trigger_b4_dl");
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;
595 p->data = NULL;
597 return 0;