500 lines
14 KiB
C
500 lines
14 KiB
C
|
|
/*
|
|
* mod_limit - limit concurrent connections or requests per second
|
|
*
|
|
* Description:
|
|
* mod_limit lets you limit the number of concurrent connections or requests per second.
|
|
* Both limits can be "in total" or per IP.
|
|
*
|
|
* Setups:
|
|
* none
|
|
*
|
|
* Options:
|
|
* none
|
|
|
|
* Actions:
|
|
* limit.con <limit> [=> action];
|
|
* - <limit> is the number of concurrent connections
|
|
* - [action] is an action to be executed if the limit is reached
|
|
* limit.con_ip <limit> [=> action];
|
|
* - <limit> is the number of concurrent connections per IP
|
|
* - [action] is an action to be executed if the limit is reached
|
|
* limit.req <limit> [=> action];
|
|
* - <limit> is the number of requests per second
|
|
* - [action] is an action to be executed if the limit is reached
|
|
* limit.req_ip <limit> [=> action];
|
|
* - <limit> is the number of requests per second per IP
|
|
* - [action] is an action to be executed if the limit is reached
|
|
*
|
|
* Example config:
|
|
* if req.path =^ "/downloads/" {
|
|
* limit.con 10;
|
|
* limit.con_ip 1;
|
|
* }
|
|
*
|
|
* This config snippet will allow only 10 active downloads overall and 1 per IP.
|
|
*
|
|
* if req.path == "/login" {
|
|
* limit.req_ip 1 => ${ log.write "Possible bruteforce from %{req.remoteip}"; };
|
|
* }
|
|
*
|
|
* This config snippet will write a message to the log containing the clien IP address if the /login page is hit more than once in a second.
|
|
*
|
|
* Todo:
|
|
* -
|
|
*
|
|
* Author:
|
|
* Copyright (c) 2010 Thomas Porzelt
|
|
* License:
|
|
* MIT, see COPYING file in the lighttpd 2 tree
|
|
*/
|
|
|
|
#include <lighttpd/base.h>
|
|
#include <lighttpd/radix.h>
|
|
|
|
LI_API gboolean mod_limit_init(liModules *mods, liModule *mod);
|
|
LI_API gboolean mod_limit_free(liModules *mods, liModule *mod);
|
|
|
|
typedef enum {
|
|
ML_TYPE_CON,
|
|
ML_TYPE_CON_IP,
|
|
ML_TYPE_REQ,
|
|
ML_TYPE_REQ_IP
|
|
} mod_limit_context_type;
|
|
|
|
struct mod_limit_context {
|
|
mod_limit_context_type type;
|
|
gint limit;
|
|
gint refcount;
|
|
GMutex *mutex; /* used when type != ML_TYPE_CON */
|
|
liPlugin *plugin;
|
|
liAction *action_limit_reached;
|
|
|
|
union {
|
|
gint con; /* decreased on vr_close */
|
|
liRadixTree *con_ip; /* radix tree contains gint, removed on vr_close */
|
|
struct {
|
|
gint num;
|
|
time_t ts;
|
|
} req; /* reset when now - ts > 1 */
|
|
liRadixTree *req_ip; /* radix tree contains (mod_limit_req_ip_data*), removed via waitqueue timer */
|
|
} pool;
|
|
};
|
|
typedef struct mod_limit_context mod_limit_context;
|
|
|
|
struct mod_limit_req_ip_data {
|
|
gint requests;
|
|
liWaitQueueElem timeout_elem;
|
|
liSocketAddress ip;
|
|
mod_limit_context *ctx;
|
|
};
|
|
typedef struct mod_limit_req_ip_data mod_limit_req_ip_data;
|
|
|
|
struct mod_limit_data {
|
|
liWaitQueue *timeout_queues; /* each worker has its own timeout queue */
|
|
};
|
|
typedef struct mod_limit_data mod_limit_data;
|
|
|
|
|
|
static mod_limit_context* mod_limit_context_new(mod_limit_context_type type, gint limit, liAction *action_limit_reached, liPlugin *plugin) {
|
|
mod_limit_context *ctx = g_slice_new0(mod_limit_context);
|
|
ctx->type = type;
|
|
ctx->limit = limit;
|
|
ctx->action_limit_reached = action_limit_reached;
|
|
ctx->plugin = plugin;
|
|
ctx->refcount = 1;
|
|
|
|
switch (type) {
|
|
case ML_TYPE_CON:
|
|
ctx->pool.con = 0;
|
|
break;
|
|
case ML_TYPE_CON_IP:
|
|
ctx->pool.con_ip = li_radixtree_new();
|
|
ctx->mutex = g_mutex_new();
|
|
break;
|
|
case ML_TYPE_REQ:
|
|
ctx->pool.req.num = 0;
|
|
ctx->pool.req.ts = 0;
|
|
break;
|
|
case ML_TYPE_REQ_IP:
|
|
ctx->pool.req_ip = li_radixtree_new();
|
|
ctx->mutex = g_mutex_new();
|
|
break;
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
static void mod_limit_context_free(liServer *srv, mod_limit_context *ctx) {
|
|
if (ctx->mutex)
|
|
g_mutex_free(ctx->mutex);
|
|
|
|
if (ctx->action_limit_reached) {
|
|
li_action_release(srv, ctx->action_limit_reached);
|
|
}
|
|
|
|
switch (ctx->type) {
|
|
case ML_TYPE_CON:
|
|
break;
|
|
case ML_TYPE_CON_IP:
|
|
li_radixtree_free(ctx->pool.con_ip, NULL, NULL);
|
|
break;
|
|
case ML_TYPE_REQ:
|
|
break;
|
|
case ML_TYPE_REQ_IP:
|
|
li_radixtree_free(ctx->pool.req_ip, NULL, NULL);
|
|
break;
|
|
}
|
|
|
|
g_slice_free(mod_limit_context, ctx);
|
|
}
|
|
|
|
static void mod_limit_timeout_callback(liWaitQueue *wq, gpointer data) {
|
|
liWaitQueueElem *wqe;
|
|
mod_limit_req_ip_data *rid;
|
|
gpointer addr;
|
|
guint32 bits;
|
|
|
|
UNUSED(data);
|
|
|
|
while ((wqe = li_waitqueue_pop(wq)) != NULL) {
|
|
rid = wqe->data;
|
|
|
|
/* IPv4 or IPv6? */
|
|
if (rid->ip.addr->plain.sa_family == AF_INET) {
|
|
addr = &rid->ip.addr->ipv4.sin_addr.s_addr;
|
|
bits = 32;
|
|
} else {
|
|
addr = &rid->ip.addr->ipv6.sin6_addr.s6_addr;
|
|
bits = 128;
|
|
}
|
|
|
|
g_mutex_lock(rid->ctx->mutex);
|
|
li_radixtree_remove(rid->ctx->pool.req_ip, addr, bits);
|
|
g_mutex_unlock(rid->ctx->mutex);
|
|
li_sockaddr_clear(&rid->ip);
|
|
g_slice_free(mod_limit_req_ip_data, rid);
|
|
}
|
|
|
|
li_waitqueue_update(wq);
|
|
}
|
|
|
|
static void mod_limit_vrclose(liVRequest *vr, liPlugin *p) {
|
|
GPtrArray *arr = g_ptr_array_index(vr->plugin_ctx, p->id);
|
|
mod_limit_context *ctx;
|
|
guint i;
|
|
gint cons;
|
|
liSocketAddress *remote_addr = &vr->coninfo->remote_addr;
|
|
gpointer addr;
|
|
guint32 bits;
|
|
|
|
if (!arr)
|
|
return;
|
|
|
|
for (i = 0; i < arr->len; i++) {
|
|
ctx = g_ptr_array_index(arr, i);
|
|
|
|
switch (ctx->type) {
|
|
case ML_TYPE_CON:
|
|
g_atomic_int_add(&ctx->pool.con, -1);
|
|
break;
|
|
case ML_TYPE_CON_IP:
|
|
/* IPv4 or IPv6? */
|
|
if (remote_addr->addr->plain.sa_family == AF_INET) {
|
|
addr = &remote_addr->addr->ipv4.sin_addr.s_addr;
|
|
bits = 32;
|
|
} else {
|
|
addr = &remote_addr->addr->ipv6.sin6_addr.s6_addr;
|
|
bits = 128;
|
|
}
|
|
|
|
g_mutex_lock(ctx->mutex);
|
|
cons = GPOINTER_TO_INT(li_radixtree_lookup_exact(ctx->pool.con_ip, addr, bits));
|
|
cons--;
|
|
if (!cons) {
|
|
li_radixtree_remove(ctx->pool.con_ip, addr, bits);
|
|
} else {
|
|
li_radixtree_insert(ctx->pool.con_ip, addr, bits, GINT_TO_POINTER(cons));
|
|
}
|
|
g_mutex_unlock(ctx->mutex);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (g_atomic_int_dec_and_test(&ctx->refcount)) {
|
|
mod_limit_context_free(vr->wrk->srv, ctx);
|
|
}
|
|
}
|
|
|
|
g_ptr_array_free(arr, TRUE);
|
|
}
|
|
|
|
static liHandlerResult mod_limit_action_handle(liVRequest *vr, gpointer param, gpointer *context) {
|
|
gboolean limit_reached = FALSE;
|
|
mod_limit_context *ctx = (mod_limit_context*) param;
|
|
GPtrArray *arr = g_ptr_array_index(vr->plugin_ctx, ctx->plugin->id);
|
|
gint cons;
|
|
mod_limit_req_ip_data *rid;
|
|
liSocketAddress *remote_addr = &vr->coninfo->remote_addr;
|
|
gpointer addr;
|
|
guint32 bits;
|
|
|
|
UNUSED(context);
|
|
|
|
if (li_vrequest_is_handled(vr)) {
|
|
VR_DEBUG(vr, "%s", "mod_limit: already have a content handler - ignoring limits. Put limit.* before content handlers such as 'static', 'fastcgi' or 'proxy'");
|
|
return LI_HANDLER_GO_ON;
|
|
}
|
|
|
|
/* IPv4 or IPv6? */
|
|
switch (remote_addr->addr->plain.sa_family) {
|
|
case AF_INET:
|
|
addr = &remote_addr->addr->ipv4.sin_addr.s_addr;
|
|
bits = 32;
|
|
break;
|
|
case AF_INET6:
|
|
addr = &remote_addr->addr->ipv6.sin6_addr.s6_addr;
|
|
bits = 128;
|
|
break;
|
|
default:
|
|
if (ctx->type == ML_TYPE_CON_IP || ctx->type == ML_TYPE_REQ_IP) {
|
|
VR_DEBUG(vr, "%s", "mod_limit only supports ipv4 or ipv6 clients");
|
|
return LI_HANDLER_ERROR;
|
|
}
|
|
addr = NULL;
|
|
bits = 0;
|
|
}
|
|
|
|
if (!arr) {
|
|
/* request is not in any context yet, create new array */
|
|
arr = g_ptr_array_sized_new(2);
|
|
g_ptr_array_index(vr->plugin_ctx, ctx->plugin->id) = arr;
|
|
}
|
|
|
|
switch (ctx->type) {
|
|
case ML_TYPE_CON:
|
|
if (g_atomic_int_exchange_and_add(&ctx->pool.con, 1) > ctx->limit) {
|
|
g_atomic_int_add(&ctx->pool.con, -1);
|
|
limit_reached = TRUE;
|
|
VR_DEBUG(vr, "limit.con: limit reached (%d active connections)", ctx->limit);
|
|
}
|
|
break;
|
|
case ML_TYPE_CON_IP:
|
|
g_mutex_lock(ctx->mutex);
|
|
cons = GPOINTER_TO_INT(li_radixtree_lookup_exact(ctx->pool.con_ip, addr, bits));
|
|
if (cons < ctx->limit) {
|
|
li_radixtree_insert(ctx->pool.con_ip, addr, bits, GINT_TO_POINTER(cons+1));
|
|
} else {
|
|
limit_reached = TRUE;
|
|
VR_DEBUG(vr, "limit.con_ip: limit reached (%d active connections)", ctx->limit);
|
|
}
|
|
g_mutex_unlock(ctx->mutex);
|
|
break;
|
|
case ML_TYPE_REQ:
|
|
g_mutex_lock(ctx->mutex);
|
|
if (CUR_TS(vr->wrk) - ctx->pool.req.ts > 1.0) {
|
|
/* reset pool */
|
|
ctx->pool.req.ts = CUR_TS(vr->wrk);
|
|
ctx->pool.req.num = 1;
|
|
} else {
|
|
ctx->pool.req.num++;
|
|
if (ctx->pool.req.num > ctx->limit) {
|
|
limit_reached = TRUE;
|
|
VR_DEBUG(vr, "limit.req: limit reached (%d req/s)", ctx->limit);
|
|
}
|
|
}
|
|
g_mutex_unlock(ctx->mutex);
|
|
break;
|
|
case ML_TYPE_REQ_IP:
|
|
g_mutex_lock(ctx->mutex);
|
|
rid = li_radixtree_lookup_exact(ctx->pool.req_ip, addr, bits);
|
|
if (!rid) {
|
|
/* IP not known */
|
|
rid = g_slice_new0(mod_limit_req_ip_data);
|
|
rid->requests = 1;
|
|
rid->ip = li_sockaddr_dup(*remote_addr);
|
|
rid->ctx = ctx;
|
|
rid->timeout_elem.data = rid;
|
|
li_radixtree_insert(ctx->pool.req_ip, addr, bits, rid);
|
|
li_waitqueue_push(&(((mod_limit_data*)ctx->plugin->data)->timeout_queues[vr->wrk->ndx]), &rid->timeout_elem);
|
|
} else if (rid->requests < ctx->limit) {
|
|
rid->requests++;
|
|
} else {
|
|
limit_reached = TRUE;
|
|
VR_DEBUG(vr, "limit.req_ip: limit reached (%d req/s)", ctx->limit);
|
|
}
|
|
g_mutex_unlock(ctx->mutex);
|
|
break;
|
|
}
|
|
|
|
if (limit_reached) {
|
|
/* limit reached, we either execute the defined action or return a 503 error page */
|
|
if (ctx->action_limit_reached) {
|
|
/* execute action */
|
|
li_action_enter(vr, ctx->action_limit_reached);
|
|
} else {
|
|
/* return 503 error page */
|
|
if (!li_vrequest_handle_direct(vr)) {
|
|
return LI_HANDLER_ERROR;
|
|
}
|
|
|
|
vr->response.http_status = 503;
|
|
}
|
|
} else {
|
|
g_ptr_array_add(arr, ctx);
|
|
g_atomic_int_inc(&ctx->refcount);
|
|
}
|
|
|
|
return LI_HANDLER_GO_ON;
|
|
}
|
|
|
|
static void mod_limit_action_free(liServer *srv, gpointer param) {
|
|
mod_limit_context *ctx = param;
|
|
|
|
if (g_atomic_int_dec_and_test(&ctx->refcount)) {
|
|
mod_limit_context_free(srv, ctx);
|
|
}
|
|
}
|
|
|
|
static liAction* mod_limit_action_create(liServer *srv, liPlugin *p, mod_limit_context_type type, liValue *val) {
|
|
const char* act_names[] = { "limit.con", "limit.con_ip", "limit.req", "limit.req_ip" };
|
|
mod_limit_context *ctx;
|
|
gint limit = 0;
|
|
liAction *action_limit_reached = NULL;
|
|
|
|
if (!val || (val->type != LI_VALUE_NUMBER && val->type != LI_VALUE_LIST)) {
|
|
ERROR(srv, "%s expects either an integer > 0 as parameter, or a list of (int,action)", act_names[type]);
|
|
return NULL;
|
|
} else if (val->type == LI_VALUE_NUMBER) {
|
|
/* limit.* N; */
|
|
if (val->data.number < 1) {
|
|
ERROR(srv, "%s expects either an integer > 0 as parameter, or a list of (int,action)", act_names[type]);
|
|
return NULL;
|
|
}
|
|
|
|
limit = val->data.number;
|
|
action_limit_reached = NULL;
|
|
} else if (val->type == LI_VALUE_LIST) {
|
|
if (val->data.list->len != 2
|
|
|| g_array_index(val->data.list, liValue*, 0)->type != LI_VALUE_NUMBER
|
|
|| g_array_index(val->data.list, liValue*, 1)->type != LI_VALUE_ACTION) {
|
|
ERROR(srv, "%s expects either an integer > 0 as parameter, or a list of (int,action)", act_names[type]);
|
|
return NULL;
|
|
}
|
|
|
|
limit = g_array_index(val->data.list, liValue*, 0)->data.number;
|
|
action_limit_reached = li_value_extract_action(g_array_index(val->data.list, liValue*, 1));
|
|
}
|
|
|
|
ctx = mod_limit_context_new(type, limit, action_limit_reached, p);
|
|
|
|
return li_action_new_function(mod_limit_action_handle, NULL, mod_limit_action_free, ctx);
|
|
}
|
|
|
|
static liAction* mod_limit_action_con_create(liServer *srv, liWorker *wrk, liPlugin *p, liValue *val, gpointer userdata) {
|
|
UNUSED(wrk); UNUSED(userdata);
|
|
|
|
return mod_limit_action_create(srv, p, ML_TYPE_CON, val);
|
|
}
|
|
|
|
static liAction* mod_limit_action_con_ip_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
|
|
UNUSED(wrk); UNUSED(userdata);
|
|
|
|
return mod_limit_action_create(srv, p, ML_TYPE_CON_IP, val);
|
|
}
|
|
|
|
static liAction* mod_limit_action_req_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
|
|
UNUSED(wrk); UNUSED(userdata);
|
|
|
|
return mod_limit_action_create(srv, p, ML_TYPE_REQ, val);
|
|
}
|
|
|
|
static liAction* mod_limit_action_req_ip_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
|
|
UNUSED(wrk); UNUSED(userdata);
|
|
|
|
return mod_limit_action_create(srv, p, ML_TYPE_REQ_IP, val);
|
|
}
|
|
|
|
|
|
static const liPluginOption options[] = {
|
|
{ NULL, 0, 0, NULL }
|
|
};
|
|
|
|
static const liPluginOptionPtr optionptrs[] = {
|
|
{ NULL, 0, NULL, NULL, NULL }
|
|
};
|
|
|
|
static const liPluginAction actions[] = {
|
|
{ "limit.con", mod_limit_action_con_create, NULL },
|
|
{ "limit.con_ip", mod_limit_action_con_ip_create, NULL },
|
|
{ "limit.req", mod_limit_action_req_create, NULL },
|
|
{ "limit.req_ip", mod_limit_action_req_ip_create, NULL },
|
|
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
static const liPluginSetup setups[] = {
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
static void mod_limit_prepare_worker(liServer *srv, liPlugin *p, liWorker *wrk) {
|
|
static gint once = 0;
|
|
mod_limit_data *mld;
|
|
|
|
/* initialize once */
|
|
if (g_atomic_int_compare_and_exchange(&once, 0, 1)) {
|
|
mld = g_slice_new(mod_limit_data);
|
|
p->data = mld;
|
|
mld->timeout_queues = g_new0(liWaitQueue, srv->worker_count);
|
|
g_atomic_int_set(&once, 2);
|
|
} else {
|
|
while (g_atomic_int_get(&once) != 2) { }
|
|
mld = p->data;
|
|
}
|
|
|
|
li_waitqueue_init(&(mld->timeout_queues[wrk->ndx]), wrk->loop, mod_limit_timeout_callback, 1.0, NULL);
|
|
}
|
|
|
|
static void plugin_limit_free(liServer *srv, liPlugin *p) {
|
|
mod_limit_data *mld = p->data;
|
|
|
|
UNUSED(srv); UNUSED(p);
|
|
|
|
if (mld) {
|
|
g_free(mld->timeout_queues);
|
|
g_slice_free(mod_limit_data, mld);
|
|
}
|
|
}
|
|
|
|
static void plugin_limit_init(liServer *srv, liPlugin *p, gpointer userdata) {
|
|
UNUSED(srv); UNUSED(userdata);
|
|
|
|
p->options = options;
|
|
p->optionptrs = optionptrs;
|
|
p->actions = actions;
|
|
p->setups = setups;
|
|
|
|
p->free = plugin_limit_free;
|
|
p->handle_vrclose = mod_limit_vrclose;
|
|
p->handle_prepare_worker = mod_limit_prepare_worker;
|
|
}
|
|
|
|
|
|
gboolean mod_limit_init(liModules *mods, liModule *mod) {
|
|
UNUSED(mod);
|
|
|
|
MODULE_VERSION_CHECK(mods);
|
|
|
|
mod->config = li_plugin_register(mods->main, "mod_limit", plugin_limit_init, NULL);
|
|
|
|
return mod->config != NULL;
|
|
}
|
|
|
|
gboolean mod_limit_free(liModules *mods, liModule *mod) {
|
|
if (mod->config)
|
|
li_plugin_free(mods->main, mod->config);
|
|
|
|
return TRUE;
|
|
}
|