xmpp.chapril.org-ejabberd/src/mod_pres_counter.erl

162 lines
5.0 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_pres_counter.erl
%%% Author : Ahmed Omar
%%% Purpose : Presence subscription flood prevention
%%% Created : 23 Sep 2010 by Ahmed Omar
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2021 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(mod_pres_counter).
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, check_packet/4,
mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
-include("logger.hrl").
-include("translate.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-record(pres_counter,
{dir, start, count, logged = false}).
start(Host, _Opts) ->
ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE,
check_packet, 25),
ok.
stop(Host) ->
ejabberd_hooks:delete(privacy_check_packet, Host,
?MODULE, check_packet, 25),
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
depends(_Host, _Opts) ->
[].
-spec check_packet(allow | deny, ejabberd_c2s:state() | jid(),
stanza(), in | out) -> allow | deny.
check_packet(Acc, #{jid := JID}, Packet, Dir) ->
check_packet(Acc, JID, Packet, Dir);
check_packet(_, #jid{lserver = LServer},
#presence{from = From, to = To, type = Type}, Dir) ->
IsSubscription = case Type of
subscribe -> true;
subscribed -> true;
unsubscribe -> true;
unsubscribed -> true;
_ -> false
end,
if IsSubscription ->
JID = case Dir of
in -> To;
out -> From
end,
update(LServer, JID, Dir);
true -> allow
end;
check_packet(Acc, _, _, _) ->
Acc.
update(Server, JID, Dir) ->
StormCount = mod_pres_counter_opt:count(Server),
TimeInterval = mod_pres_counter_opt:interval(Server),
TimeStamp = erlang:system_time(millisecond),
case read(Dir) of
undefined ->
write(Dir,
#pres_counter{dir = Dir, start = TimeStamp, count = 1}),
allow;
#pres_counter{start = TimeStart, count = Count,
logged = Logged} =
R ->
if TimeStamp - TimeStart > TimeInterval ->
write(Dir,
R#pres_counter{start = TimeStamp, count = 1}),
allow;
(Count =:= StormCount) and Logged -> {stop, deny};
Count =:= StormCount ->
write(Dir, R#pres_counter{logged = true}),
case Dir of
in ->
?WARNING_MSG("User ~ts is being flooded, ignoring received "
"presence subscriptions",
[jid:encode(JID)]);
out ->
IP = ejabberd_sm:get_user_ip(JID#jid.luser,
JID#jid.lserver,
JID#jid.lresource),
?WARNING_MSG("Flooder detected: ~ts, on IP: ~ts ignoring "
"sent presence subscriptions~n",
[jid:encode(JID),
misc:ip_to_list(IP)])
end,
{stop, deny};
true ->
write(Dir,
R#pres_counter{start = TimeStamp, count = Count + 1}),
allow
end
end.
read(K) -> get({pres_counter, K}).
write(K, V) -> put({pres_counter, K}, V).
mod_opt_type(count) ->
econf:pos_int();
mod_opt_type(interval) ->
econf:timeout(second).
mod_options(_) ->
[{count, 5}, {interval, timer:seconds(60)}].
mod_doc() ->
#{desc =>
?T("This module detects flood/spam in presence "
"subscriptions traffic. If a user sends or receives "
"more of those stanzas in a given time interval, "
"the exceeding stanzas are silently dropped, and a "
"warning is logged."),
opts =>
[{count,
#{value => ?T("Number"),
desc =>
?T("The number of subscription presence stanzas "
"(subscribe, unsubscribe, subscribed, unsubscribed) "
"allowed for any direction (input or output) per time "
"defined in 'interval' option. Please note that two "
"users subscribing to each other usually generate 4 "
"stanzas, so the recommended value is '4' or more. "
"The default value is '5'.")}},
{interval,
#{value => "timeout()",
desc =>
?T("The time interval. The default value is '1' minute.")}}],
example =>
["modules:",
" ...",
" mod_pres_counter:",
" count: 5",
" interval: 30 secs",
" ..."]}.