Packages
ejabberd
20.2.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/ejabberd_shaper.erl
%%%----------------------------------------------------------------------
%%% ejabberd, Copyright (C) 2002-2020 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(ejabberd_shaper).
-behaviour(gen_server).
-export([start_link/0, new/1, update/2, match/3, get_max_rate/1]).
-export([reload_from_config/0]).
-export([validator/1, shaper_rules_validator/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("logger.hrl").
-type state() :: #{hosts := [binary()]}.
-type shaper() :: none | p1_shaper:state().
-type shaper_rate() :: {pos_integer(), pos_integer()} | pos_integer() | infinity.
-type shaper_rule() :: {atom() | pos_integer(), [acl:access_rule()]}.
-type shaper_rate_rule() :: {shaper_rate(), [acl:access_rule()]}.
-export_type([shaper/0, shaper_rule/0, shaper_rate/0]).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec match(global | binary(), atom() | [shaper_rule()],
jid:jid() | jid:ljid() | inet:ip_address() | acl:match()) -> none | shaper_rate().
match(_, none, _) -> none;
match(_, infinity, _) -> infinity;
match(Host, Shaper, Match) when is_map(Match) ->
Rules = if is_atom(Shaper) -> read_shaper_rules(Shaper, Host);
true -> Shaper
end,
Rate = acl:match_rules(Host, Rules, Match, none),
read_shaper(Rate);
match(Host, Shaper, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 ->
match(Host, Shaper, #{ip => IP});
match(Host, Shaper, JID) ->
match(Host, Shaper, #{usr => jid:tolower(JID)}).
-spec get_max_rate(none | shaper_rate()) -> none | pos_integer().
get_max_rate({Rate, _}) -> Rate;
get_max_rate(Rate) when is_integer(Rate), Rate > 0 -> Rate;
get_max_rate(_) -> none.
-spec new(none | shaper_rate()) -> shaper().
new({Rate, Burst}) -> p1_shaper:new(Rate, Burst);
new(Rate) when is_integer(Rate), Rate > 0 -> p1_shaper:new(Rate);
new(_) -> none.
-spec update(shaper(), non_neg_integer()) -> {shaper(), non_neg_integer()}.
update(none, _Size) -> {none, 0};
update(Shaper1, Size) ->
Shaper2 = p1_shaper:update(Shaper1, Size),
?DEBUG("Shaper update:~n~ts =>~n~ts",
[p1_shaper:pp(Shaper1), p1_shaper:pp(Shaper2)]),
Shaper2.
-spec validator(shaper | shaper_rules) -> econf:validator().
validator(shaper) ->
econf:options(
#{'_' => shaper_validator()},
[{disallowed, reserved()}, {return, map}, unique]);
validator(shaper_rules) ->
econf:options(
#{'_' => shaper_rules_validator()},
[{disallowed, reserved()}, unique]).
-spec shaper_rules_validator() -> econf:validator().
shaper_rules_validator() ->
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) ->
{(shaper_name())(K), (acl:access_validator())(V)};
(N) ->
{(shaper_name())(N), [{acl, all}]}
end, lists:flatten(L));
(N) ->
[{(shaper_name())(N), [{acl, all}]}]
end.
-spec reload_from_config() -> ok.
reload_from_config() ->
gen_server:call(?MODULE, reload_from_config, timer:minutes(1)).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
create_tabs(),
Hosts = ejabberd_option:hosts(),
load_from_config([], Hosts),
ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20),
{ok, #{hosts => Hosts}}.
-spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}.
handle_call(reload_from_config, _, #{hosts := OldHosts} = State) ->
NewHosts = ejabberd_option:hosts(),
load_from_config(OldHosts, NewHosts),
{reply, ok, State#{hosts => NewHosts}};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
-spec handle_info(term(), state()) -> {noreply, state()}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(any(), state()) -> ok.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20).
-spec code_change(term(), state(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%%===================================================================
%%% Table management
%%%===================================================================
-spec load_from_config([binary()], [binary()]) -> ok.
load_from_config(OldHosts, NewHosts) ->
?DEBUG("Loading shaper rules from config", []),
Shapers = ejabberd_option:shaper(),
ets:insert(shaper, maps:to_list(Shapers)),
ets:insert(
shaper_rules,
lists:flatmap(
fun(Host) ->
lists:flatmap(
fun({Name, List}) ->
case resolve_shapers(Name, List, Shapers) of
[] -> [];
List1 ->
[{{Name, Host}, List1}]
end
end, ejabberd_option:shaper_rules(Host))
end, [global|NewHosts])),
lists:foreach(
fun(Host) ->
ets:match_delete(shaper_rules, {{'_', Host}, '_'})
end, OldHosts -- NewHosts),
?DEBUG("Shaper rules loaded successfully", []).
-spec create_tabs() -> ok.
create_tabs() ->
_ = mnesia:delete_table(shaper),
_ = ets:new(shaper, [named_table, {read_concurrency, true}]),
_ = ets:new(shaper_rules, [named_table, {read_concurrency, true}]),
ok.
-spec read_shaper_rules(atom(), global | binary()) -> [shaper_rate_rule()].
read_shaper_rules(Name, Host) ->
case ets:lookup(shaper_rules, {Name, Host}) of
[{_, Rule}] -> Rule;
[] -> []
end.
-spec read_shaper(atom() | shaper_rate()) -> none | shaper_rate().
read_shaper(Name) when is_atom(Name), Name /= none, Name /= infinity ->
case ets:lookup(shaper, Name) of
[{_, Rate}] -> Rate;
[] -> none
end;
read_shaper(Rate) ->
Rate.
%%%===================================================================
%%% Validators
%%%===================================================================
shaper_name() ->
econf:either(
econf:and_then(
econf:atom(),
fun(infinite) -> infinity;
(unlimited) -> infinity;
(A) -> A
end),
econf:pos_int()).
shaper_validator() ->
econf:either(
econf:and_then(
econf:options(
#{rate => econf:pos_int(),
burst_size => econf:pos_int()},
[unique, {required, [rate]}, {return, map}]),
fun(#{rate := Rate} = Map) ->
{Rate, maps:get(burst_size, Map, Rate)}
end),
econf:pos_int(infinity)).
%%%===================================================================
%%% Aux
%%%===================================================================
reserved() ->
[none, infinite, unlimited, infinity].
-spec resolve_shapers(atom(), [shaper_rule()], #{atom() => shaper_rate()}) -> [shaper_rate_rule()].
resolve_shapers(ShaperRule, Rules, Shapers) ->
lists:filtermap(
fun({Name, Rule}) when is_atom(Name), Name /= none, Name /= infinity ->
try {true, {maps:get(Name, Shapers), Rule}}
catch _:{badkey, _} ->
?WARNING_MSG(
"Shaper rule '~ts' refers to unknown shaper: ~ts",
[ShaperRule, Name]),
false
end;
(_) ->
true
end, Rules).