7 #include "http_header.h"
15 #if defined(HAVE_GDBM_H)
19 #if defined(HAVE_PCRE_H)
23 #if defined(USE_MEMCACHED)
24 # include <libmemcached/memcached.h>
28 * this is a trigger_b4_dl for a lighttpd plugin
32 /* plugin config for all request/connections */
43 #if defined(HAVE_PCRE_H)
47 #if defined(HAVE_GDBM_H)
51 #if defined(USE_MEMCACHED)
55 unsigned short trigger_timeout
;
64 plugin_config
**config_storage
;
69 /* init the plugin data */
70 INIT_FUNC(mod_trigger_b4_dl_init
) {
73 p
= calloc(1, sizeof(*p
));
75 p
->tmp_buf
= buffer_init();
80 /* detroy the plugin data */
81 FREE_FUNC(mod_trigger_b4_dl_free
) {
86 if (!p
) return HANDLER_GO_ON
;
88 if (p
->config_storage
) {
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
);
107 #if defined(HAVE_GDBM_H)
108 if (s
->db
) gdbm_close(s
->db
);
110 #if defined(USE_MEMCACHED)
111 if (s
->memc
) memcached_free(s
->memc
);
116 free(p
->config_storage
);
119 buffer_free(p
->tmp_buf
);
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
;
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
];
152 #if defined(HAVE_PCRE_H)
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",
184 return HANDLER_ERROR
;
186 fdevent_setfd_cloexec(gdbm_fdesc(s
->db
));
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
;
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();
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:",
246 buffer_free(option_string
);
248 if (NULL
== s
->memc
) return HANDLER_ERROR
;
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
;
257 return HANDLER_GO_ON
;
260 #if defined(HAVE_PCRE_H)
264 static int mod_trigger_b4_dl_patch_connection(server
*srv
, connection
*con
, plugin_data
*p
) {
266 plugin_config
*s
= p
->config_storage
[0];
268 #if defined(HAVE_GDBM)
271 #if defined(HAVE_PCRE_H)
272 PATCH(download_regex
);
273 PATCH(trigger_regex
);
275 PATCH(trigger_timeout
);
279 #if defined(USE_MEMCACHED)
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;
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
);
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
);
303 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) {
304 #if defined(HAVE_GDBM_H)
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"))) {
311 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("trigger-before-download.deny-url"))) {
313 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("trigger-before-download.memcache-namespace"))) {
315 } else if (buffer_is_equal_string(du
->key
, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) {
316 #if defined(USE_MEMCACHED)
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
;
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
;
359 if (!p
->conf
.memc
) return HANDLER_GO_ON
;
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 */
367 /* memcache can't handle spaces */
369 remote_ip
= con
->dst_addr_buf
->ptr
;
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
;
385 # if defined(HAVE_GDBM_H)
387 /* the trigger matched */
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",
402 # if defined(USE_MEMCACHED)
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
] = '-';
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",
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
;
436 /* the download uri matched */
437 # if defined(HAVE_GDBM_H)
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));
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;
469 if (0 != gdbm_delete(p
->conf
.db
, key
)) {
470 log_error_write(srv
, __FILE__
, __LINE__
, "s",
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",
488 # if defined(USE_MEMCACHED)
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
] = '-';
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",
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
;
545 /* check DB each minute */
546 if (srv
->cur_ts
% 60 != 0) return HANDLER_GO_ON
;
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;
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
)) {
568 val
= gdbm_fetch(s
->db
, key
);
570 memcpy(&last_hit
, val
.dptr
, sizeof(time_t));
574 if (srv
->cur_ts
- last_hit
> s
->trigger_timeout
) {
575 gdbm_delete(s
->db
, 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
;
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
;
602 p
->cleanup
= mod_trigger_b4_dl_free
;