From a3f7300ec9e4c28254096dd6338dc8e25c0bc15b Mon Sep 17 00:00:00 2001 From: Jan Kneschke Date: Thu, 7 Jul 2005 22:48:42 +0000 Subject: [PATCH] added trigger-b4-dl, upped version number to .15 git-svn-id: svn://svn.lighttpd.net/lighttpd/branches/lighttpd-1.3.x@429 152afb58-edef-0310-8abb-c4023f1b3aa9 --- configure.in | 30 ++- doc/Makefile.am | 6 +- doc/trigger_b4_dl.txt | 58 +++++ src/Makefile.am | 5 + src/mod_trigger_b4_dl.c | 541 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 637 insertions(+), 3 deletions(-) create mode 100644 doc/trigger_b4_dl.txt create mode 100644 src/mod_trigger_b4_dl.c diff --git a/configure.in b/configure.in index dc67e718..397798de 100644 --- a/configure.in +++ b/configure.in @@ -1,7 +1,7 @@ # -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ(2.57) -AC_INIT(lighttpd, 1.3.14, jan@kneschke.de) +AC_INIT(lighttpd, 1.3.15, jan@kneschke.de) AC_CONFIG_SRCDIR([src/server.c]) AC_CANONICAL_TARGET @@ -245,6 +245,34 @@ AC_CHECK_LIB(bz2, BZ2_bzCompress, [ ]) AC_SUBST(BZ_LIB) + +AC_MSG_CHECKING(for gdbm) +AC_ARG_WITH(gdbm, AC_HELP_STRING([--with-gdbm],[gdbm storage for mod_trigger_b4_dl]), +[AC_MSG_RESULT(yes) + AC_CHECK_LIB(gdbm, gdbm_open, [ + AC_CHECK_HEADERS([gdbm.h],[ + GDBM_LIB=-lgdbm + AC_DEFINE([HAVE_GDBM], [1], [libgdbm]) + AC_DEFINE([HAVE_GDBM_H], [1]) + ]) + ]) +],[AC_MSG_RESULT(no)]) +AC_SUBST(GDBM_LIB) + +AC_MSG_CHECKING(for libmemcache) +AC_ARG_WITH(gdbm, AC_HELP_STRING([--with-libmemcache],[memcached storage for mod_trigger_b4_dl]), +[AC_MSG_RESULT(yes) + AC_CHECK_LIB(memcache, mc_new, [ + AC_CHECK_HEADERS([memcache.h],[ + MEMCACHE_LIB=-lmemcache + AC_DEFINE([HAVE_MEMCACHE], [1], [libmemcache]) + AC_DEFINE([HAVE_MEMCACHE_H], [1]) + ]) + ]) +],[AC_MSG_RESULT(no)]) +AC_SUBST(MEMCACHE_LIB) + + AC_SEARCH_LIBS(socket,socket) AC_SEARCH_LIBS(gethostbyname,nsl socket) AC_SEARCH_LIBS(hstrerror,resolv) diff --git a/doc/Makefile.am b/doc/Makefile.am index f61ff796..ae77c1a1 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -30,7 +30,8 @@ traffic-shaping.txt \ setenv.txt \ status.txt \ scgi.txt \ -cml.txt +cml.txt \ +trigger_b4_dl.txt HTMLDOCS=accesslog.html \ authentication.html \ @@ -61,7 +62,8 @@ HTMLDOCS=accesslog.html \ setenv.html \ status.html \ scgi.html \ - cml.html + cml.html \ + trigger_b4_dl.html EXTRA_DIST=lighttpd.conf lighttpd.user \ rc.lighttpd rc.lighttpd.redhat sysconfig.lighttpd \ diff --git a/doc/trigger_b4_dl.txt b/doc/trigger_b4_dl.txt new file mode 100644 index 00000000..f7e4875c --- /dev/null +++ b/doc/trigger_b4_dl.txt @@ -0,0 +1,58 @@ +======================= +Trigger before Download +======================= + +------------------------- +Module: mod_trigger_b4_dl +------------------------- + +:Author: Jan Kneschke +:Date: $Date: 2004/11/03 22:26:05 $ +:Revision: $Revision: 1.2 $ + +:abstract: + another anti hot-linking module + +.. meta:: + :keywords: lighttpd, hot-linking, deep-linking + +.. contents:: Table of Contents + +Description +=========== + +Anti Hotlinking: + + * if user requests ''download-url'' directly the request is denied and he is redirected to ''deny-url' + * if user visits ''trigger-url'' before requesting ''download-url'' access is granted + * if user visits ''download-url'' again after ''trigger-timeout'' has run down to the request is denied and he is redirected to ''deny-url'' + +The storage for the trigger information is either stored locally in a gdbm file or remotly in memcached. + +Requirements +------------ + + * libpcre + * libgdbm + * libmemcache + +Options +======= + +:: + + trigger-before-download.gdbm-filename = "/home/weigon/testbase/trigger.db" + trigger-before-download.memcache-hosts = ( "127.0.0.1:11211" ) + trigger-before-download.trigger-url = "^/trigger/" + trigger-before-download.download-url = "^/download/" + trigger-before-download.deny-url = "http://192.168.1.5:1025/index.html" + trigger-before-download.trigger-timeout = 10 + +If both trigger-before-download.gdbm-filename and +trigger-before-download.memcache-hosts is set gdbm will be prefered. + +Installation +============ + +memcached should be started with the option -M as we don't want to remove entry if the memory is full. + diff --git a/src/Makefile.am b/src/Makefile.am index 71412e1f..7d01d0be 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -73,6 +73,11 @@ mod_cml_la_SOURCES = mod_cml.c mod_cml_funcs.c mod_cml_logic.c mod_cml_la_LDFLAGS = -module -export-dynamic -avoid-version -no-undefined mod_cml_la_LIBADD = $(MYSQL_LIBS) $(common_libadd) +lib_LTLIBRARIES += mod_trigger_b4_dl.la +mod_trigger_b4_dl_la_SOURCES = mod_trigger_b4_dl.c +mod_trigger_b4_dl_la_LDFLAGS = -module -export-dynamic -avoid-version -no-undefined +mod_trigger_b4_dl_la_LIBADD = $(GDBM_LIB) $(MEMCACHE_LIB) $(common_libadd) + lib_LTLIBRARIES += mod_mysql_vhost.la mod_mysql_vhost_la_SOURCES = mod_mysql_vhost.c mod_mysql_vhost_la_LDFLAGS = -module -export-dynamic -avoid-version -no-undefined diff --git a/src/mod_trigger_b4_dl.c b/src/mod_trigger_b4_dl.c new file mode 100644 index 00000000..cb195310 --- /dev/null +++ b/src/mod_trigger_b4_dl.c @@ -0,0 +1,541 @@ +#include +#include +#include + + + +#include "base.h" +#include "log.h" +#include "buffer.h" + +#include "plugin.h" +#include "response.h" +#include "inet_ntop_cache.h" + +#include "config.h" + +#if defined(HAVE_GDBM_H) +#include +#endif + +#if defined(HAVE_PCRE_H) +#include +#endif + +#if defined(HAVE_MEMCACHE_H) +#include +#endif + +/** + * this is a trigger_b4_dl for a lighttpd plugin + * + */ + +/* plugin config for all request/connections */ + +typedef struct { + buffer *db_filename; + + buffer *trigger_url; + buffer *download_url; + buffer *deny_url; + + array *mc_hosts; +#if defined(HAVE_PCRE_H) + pcre *trigger_regex; + pcre *download_regex; +#endif +#if defined(HAVE_GDBM_H) + GDBM_FILE db; +#endif + +#if defined(HAVE_MEMCACHE_H) + struct memcache *mc; +#endif + + unsigned short trigger_timeout; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + + plugin_config **config_storage; + + plugin_config conf; +} plugin_data; + +/* init the plugin data */ +INIT_FUNC(mod_trigger_b4_dl_init) { + plugin_data *p; + + p = calloc(1, sizeof(*p)); + + return p; +} + +/* detroy the plugin data */ +FREE_FUNC(mod_trigger_b4_dl_free) { + plugin_data *p = p_d; + + UNUSED(srv); + + if (!p) return HANDLER_GO_ON; + + if (p->config_storage) { + size_t i; + for (i = 0; i < srv->config_context->used; i++) { + plugin_config *s = p->config_storage[i]; + + buffer_free(s->db_filename); + buffer_free(s->download_url); + buffer_free(s->trigger_url); + buffer_free(s->deny_url); + +#if defined(HAVE_PCRE_H) + if (s->trigger_regex) pcre_free(s->trigger_regex); + if (s->download_regex) pcre_free(s->download_regex); +#endif +#if defined(HAVE_GDBM_H) + if (s->db) { + gdbm_close(s->db); + } +#endif +#if defined(HAVE_MEMCACHE_H) + if (s->mc) { + mc_free(s->mc); + } +#endif + + free(s); + } + free(p->config_storage); + } + + free(p); + + return HANDLER_GO_ON; +} + +/* handle plugin config and check values */ + +SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) { + plugin_data *p = p_d; + size_t i = 0; + + + config_values_t cv[] = { + { "trigger-before-download.gdbm-filename", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ + { "trigger-before-download.trigger-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ + { "trigger-before-download.download-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */ + { "trigger-before-download.deny-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 3 */ + { "trigger-before-download.trigger-timeout", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 4 */ + { "trigger-before-download.memcache-hosts", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 5 */ + { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } + }; + + if (!p) return HANDLER_ERROR; + + p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *)); + + for (i = 0; i < srv->config_context->used; i++) { + plugin_config *s; + size_t k; +#if defined(HAVE_PCRE_H) + const char *errptr; + int erroff; +#endif + + s = calloc(1, sizeof(plugin_config)); + s->db_filename = buffer_init(); + s->download_url = buffer_init(); + s->trigger_url = buffer_init(); + s->deny_url = buffer_init(); + s->mc_hosts = array_init(); + + cv[0].destination = s->db_filename; + cv[1].destination = s->trigger_url; + cv[2].destination = s->download_url; + cv[3].destination = s->deny_url; + cv[4].destination = &(s->trigger_timeout); + cv[5].destination = s->mc_hosts; + + p->config_storage[i] = s; + + if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) { + return HANDLER_ERROR; + } +#if defined(HAVE_GDBM_H) + if (!buffer_is_empty(s->db_filename)) { + if (NULL == (s->db = gdbm_open(s->db_filename->ptr, 4096, GDBM_WRCREAT | GDBM_NOLOCK, S_IRUSR | S_IWUSR, 0))) { + log_error_write(srv, __FILE__, __LINE__, "s", + "gdbm-open failed"); + return HANDLER_ERROR; + } + } +#endif +#if defined(HAVE_PCRE_H) + if (!buffer_is_empty(s->download_url)) { + if (NULL == (s->download_regex = pcre_compile(s->download_url->ptr, + 0, &errptr, &erroff, NULL))) { + + log_error_write(srv, __FILE__, __LINE__, "sbss", + "compiling regex for download-url failed:", + s->download_url, "pos:", erroff); + return HANDLER_ERROR; + } + } + + if (!buffer_is_empty(s->trigger_url)) { + if (NULL == (s->trigger_regex = pcre_compile(s->trigger_url->ptr, + 0, &errptr, &erroff, NULL))) { + + log_error_write(srv, __FILE__, __LINE__, "sbss", + "compiling regex for trigger-url failed:", + s->trigger_url, "pos:", erroff); + + return HANDLER_ERROR; + } + } +#endif + + if (s->mc_hosts->used) { +#if defined(HAVE_MEMCACHE_H) + s->mc = mc_new(); + + for (k = 0; k < s->mc_hosts->used; k++) { + data_string *ds = (data_string *)s->mc_hosts->data[k]; + + if (0 != mc_server_add4(s->mc, ds->value->ptr)) { + log_error_write(srv, __FILE__, __LINE__, "sb", + "connection to host failed:", + ds->value); + + return HANDLER_ERROR; + } + } +#else + log_error_write(srv, __FILE__, __LINE__, "s", + "memcache support is not compiled in but trigger-before-download.memcache-hosts is set, aborting"); + return HANDLER_ERROR; +#endif + } + + +#if (!defined(HAVE_GDBM_H) && !defined(HAVE_MEMCACHE_H)) || !defined(HAVE_PCRE_H) + log_error_write(srv, __FILE__, __LINE__, "s", + "(either gdbm or libmemcache) and pcre are require, but were not found, aborting"); + return HANDLER_ERROR; +#endif + } + + return HANDLER_GO_ON; +} + +#define PATCH(x) \ + p->conf.x = s->x; +static int mod_trigger_b4_dl_patch_connection(server *srv, connection *con, plugin_data *p, const char *stage, size_t stage_len) { + size_t i, j; + + /* skip the first, the global context */ + for (i = 1; i < srv->config_context->used; i++) { + data_config *dc = (data_config *)srv->config_context->data[i]; + plugin_config *s = p->config_storage[i]; + + /* not our stage */ + if (!buffer_is_equal_string(dc->comp_key, stage, stage_len)) continue; + + /* condition didn't match */ + if (!config_check_cond(srv, con, dc)) continue; + + /* merge config */ + for (j = 0; j < dc->value->used; j++) { + data_unset *du = dc->value->data[j]; + + if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.download-url"))) { +#if defined(HAVE_PCRE_H) + PATCH(download_regex); +#endif + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-url"))) { +# if defined(HAVE_PCRE_H) + PATCH(trigger_regex); +# endif + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) { +#if defined(HAVE_GDBM_H) + PATCH(db); +#endif + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-timeout"))) { + PATCH(trigger_timeout); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.deny-url"))) { + PATCH(deny_url); + } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) { +#if defined(HAVE_MEMCACHE_H) + PATCH(mc); +#endif + } + } + } + + return 0; +} + +static int mod_trigger_b4_dl_setup_connection(server *srv, connection *con, plugin_data *p) { + plugin_config *s = p->config_storage[0]; + UNUSED(srv); + UNUSED(con); +#if defined(HAVE_GDBM_H) + PATCH(db); +#endif +#if defined(HAVE_PCRE_H) + PATCH(download_regex); + PATCH(trigger_regex); +#endif + PATCH(trigger_timeout); + PATCH(deny_url); +#if defined(HAVE_MEMCACHE_H) + PATCH(mc); +#endif + return 0; +} +#undef PATCH + +URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) { + plugin_data *p = p_d; + size_t i; + const char *remote_ip; + data_string *ds; + +#if defined(HAVE_PCRE_H) + int n; +# define N 10 + int ovec[N * 3]; + + if (con->uri.path->used == 0) return HANDLER_GO_ON; + + mod_trigger_b4_dl_setup_connection(srv, con, p); + for (i = 0; i < srv->config_patches->used; i++) { + buffer *patch = srv->config_patches->ptr[i]; + + mod_trigger_b4_dl_patch_connection(srv, con, p, CONST_BUF_LEN(patch)); + } + + if (!p->conf.trigger_regex || !p->conf.download_regex || (!p->conf.db && !p->conf.mc)) return HANDLER_GO_ON; + + if (NULL != (ds = (data_string *)array_get_element(con->request.headers, "X-Forwarded-For"))) { + /* X-Forwarded-For contains the ip behind the proxy */ + + remote_ip = ds->value->ptr; + } else { + remote_ip = inet_ntop_cache_get_ip(srv, &(con->dst_addr)); + } + + /* check if URL is a trigger -> insert IP into DB */ + if ((n = pcre_exec(p->conf.trigger_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) { + if (n != PCRE_ERROR_NOMATCH) { + log_error_write(srv, __FILE__, __LINE__, "sd", + "execution error while matching:", n); + + return HANDLER_ERROR; + } + } else { + if (p->conf.db) { +# if defined(HAVE_GDBM_H) + /* the trigger matched */ + datum key, val; + + key.dptr = (char *)remote_ip; + key.dsize = strlen(remote_ip); + + val.dptr = (char *)&(srv->cur_ts); + val.dsize = sizeof(srv->cur_ts); + + if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "insert failed"); + } +# endif + } else if (p->conf.mc) { +# if defined(HAVE_MEMCACHE_H) + if (0 != mc_set(p->conf.mc, + (char *)remote_ip, strlen(remote_ip), + (char *)&(srv->cur_ts), sizeof(srv->cur_ts), + p->conf.trigger_timeout, 0)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "insert failed"); + } +# endif + } + } + + /* check if URL is a download -> check IP in DB, update timestamp */ + if ((n = pcre_exec(p->conf.download_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) { + if (n != PCRE_ERROR_NOMATCH) { + log_error_write(srv, __FILE__, __LINE__, "sd", + "execution error while matching: ", n); + return HANDLER_ERROR; + } + } else { + /* the download uri matched */ + time_t last_hit; + + if (p->conf.db) { +# if defined(HAVE_GDBM_H) + datum key, val; + + key.dptr = (char *)remote_ip; + key.dsize = strlen(remote_ip); + + val = gdbm_fetch(p->conf.db, key); + + if (val.dptr == NULL) { + /* not found, redirect */ + + response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url)); + + con->http_status = 307; + + return HANDLER_FINISHED; + } + + last_hit = *(time_t *)(val.dptr); + + free(val.dptr); + + if (srv->cur_ts - last_hit > p->conf.trigger_timeout) { + /* found, but timeout, redirect */ + + response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url)); + con->http_status = 307; + + if (p->conf.db) { + if (0 != gdbm_delete(p->conf.db, key)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "delete failed"); + } + } else if (p->conf.mc) { + if (0 != mc_delete(p->conf.mc, + (char *)remote_ip, strlen(remote_ip), + 0)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "insert failed"); + } + } + + return HANDLER_FINISHED; + } + + val.dptr = (char *)&(srv->cur_ts); + val.dsize = sizeof(srv->cur_ts); + + if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "insert failed"); + } +# endif + } else if (p->conf.mc) { +# if defined(HAVE_MEMCACHE_H) + void *r; + + /** + * + * memcached is do expiration for us, as long as we can fetch it every thing is ok + * and the timestamp is updated + * + */ + if (NULL == (r = mc_aget(p->conf.mc, + (char *)remote_ip, strlen(remote_ip)))) { + + response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url)); + + con->http_status = 307; + + return HANDLER_FINISHED; + } + + free(r); + + /* set a new timeout */ + if (0 != mc_set(p->conf.mc, + (char *)remote_ip, strlen(remote_ip), + (char *)&(srv->cur_ts), sizeof(srv->cur_ts), + p->conf.trigger_timeout, 0)) { + log_error_write(srv, __FILE__, __LINE__, "s", + "insert failed"); + } +# endif + } + } + +#else + UNUSED(srv); + UNUSED(con); + UNUSED(p_d); +#endif + + return HANDLER_GO_ON; +} + +TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) { +#if defined(HAVE_GDBM_H) + plugin_data *p = p_d; + size_t i; + + /* check DB each minute */ + if (srv->cur_ts % 60 != 0) return HANDLER_GO_ON; + + /* cleanup */ + for (i = 0; i < srv->config_context->used; i++) { + plugin_config *s = p->config_storage[i]; + datum key, val, okey; + + if (!s->db) continue; + + okey.dptr = NULL; + + /* according to the manual this loop + delete does delete all entries on its way + * + * we don't care as the next round will remove them. We don't have to perfect here. + */ + for (key = gdbm_firstkey(s->db); key.dptr; key = gdbm_nextkey(s->db, okey)) { + time_t last_hit; + if (okey.dptr) { + free(okey.dptr); + okey.dptr = NULL; + } + + val = gdbm_fetch(s->db, key); + + last_hit = *(time_t *)(val.dptr); + + free(val.dptr); + + if (srv->cur_ts - last_hit > s->trigger_timeout) { + gdbm_delete(s->db, key); + } + + okey = key; + } + if (okey.dptr) free(okey.dptr); + + /* reorg once a day */ + if ((srv->cur_ts % (60 * 60 * 24) != 0)) gdbm_reorganize(s->db); + } +#endif + return HANDLER_GO_ON; +} + +/* this function is called at dlopen() time and inits the callbacks */ + +int mod_trigger_b4_dl_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = buffer_init_string("trigger_b4_dl"); + + p->init = mod_trigger_b4_dl_init; + p->handle_uri_clean = mod_trigger_b4_dl_uri_handler; + p->set_defaults = mod_trigger_b4_dl_set_defaults; + p->handle_trigger = mod_trigger_b4_dl_handle_trigger; + p->cleanup = mod_trigger_b4_dl_free; + + p->data = NULL; + + return 0; +}