/*
* 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 ;
}