diff --git a/include/mod_antispam.hrl b/include/mod_antispam.hrl index 7f681046e..c30f24620 100644 --- a/include/mod_antispam.hrl +++ b/include/mod_antispam.hrl @@ -24,3 +24,13 @@ -type filename() :: binary() | none | false. -type jid_set() :: sets:set(ljid()). -type url_set() :: sets:set(url()). + +-define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>). + +-record(rtbl_service, + {host = none :: binary() | none, + node = ?DEFAULT_RTBL_DOMAINS_NODE :: binary(), + subscribed = false :: boolean(), + retry_timer = undefined :: reference() | undefined}). + +-type rtbl_service() :: #rtbl_service{}. diff --git a/src/mod_antispam.erl b/src/mod_antispam.erl index 9baaeffc1..0e3a1b8a7 100644 --- a/src/mod_antispam.erl +++ b/src/mod_antispam.erl @@ -51,6 +51,8 @@ terminate/2, code_change/3]). +-export([get_rtbl_services_option/1]). + %% ejabberd_commands callbacks. -export([add_blocked_domain/2, add_to_spam_filter_cache/2, @@ -87,7 +89,6 @@ -type state() :: #state{}. -define(COMMAND_TIMEOUT, timer:seconds(30)). --define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>). -define(DEFAULT_CACHE_SIZE, 10000). %% @format-begin @@ -137,12 +138,14 @@ mod_opt_type(access_spam) -> econf:acl(); mod_opt_type(cache_size) -> econf:pos_int(unlimited); -mod_opt_type(rtbl_host) -> - econf:either( - econf:enum([none]), econf:host()); -mod_opt_type(rtbl_domains_node) -> - econf:non_empty( - econf:binary()); +mod_opt_type(rtbl_services) -> + econf:list( + econf:either( + econf:binary(), + econf:map( + econf:binary(), + econf:map( + econf:enum([spam_source_domains_node]), econf:binary())))); mod_opt_type(spam_domains_file) -> econf:either( econf:enum([none]), econf:file()); @@ -159,12 +162,11 @@ mod_opt_type(whitelist_domains_file) -> econf:either( econf:enum([none]), econf:file()). --spec mod_options(binary()) -> [{atom(), any()}]. +-spec mod_options(binary()) -> [{rtbl_services, [tuple()]} | {atom(), any()}]. mod_options(_Host) -> [{access_spam, none}, {cache_size, ?DEFAULT_CACHE_SIZE}, - {rtbl_domains_node, ?DEFAULT_RTBL_DOMAINS_NODE}, - {rtbl_host, none}, + {rtbl_services, []}, {spam_domains_file, none}, {spam_dump_file, false}, {spam_jids_file, none}, @@ -200,6 +202,21 @@ mod_doc() -> "Note that separate caches are used for each virtual host, " " and that the caches aren't distributed across cluster nodes. " "The default value is '10000'.")}}, + {rtbl_services, + #{value => ?T("[Service]"), + example => + ["rtbl_services:", + " - pubsub.server1.localhost:", + " spam_source_domains_node: actual_custom_pubsub_node"], + desc => + ?T("Query a RTBL service to get domains to block, as provided by " + "https://xmppbl.org/[xmppbl.org]. " + "Please note right now this option only supports one service in that list. " + "For blocking spam and abuse on MUC channels, please use _`mod_muc_rtbl`_ for now. " + "If only the host is provided, the default node names will be assumed. " + "If the node name is different than 'spam_source_domains', " + "you can setup the custom node name with the option 'spam_source_domains_node'. " + "The default value is an empty list of services.")}}, {spam_domains_file, #{value => ?T("none | Path"), desc => @@ -253,6 +270,8 @@ mod_doc() -> example => ["modules:", " mod_antispam:", + " rtbl_services:", + " - xmppbl.org", " spam_jids_file: \"@CONFIG_PATH@/spam_jids.txt\"", " spam_dump_file: \"@LOG_PATH@/spam/host-@HOST@.log\""]}. @@ -274,8 +293,7 @@ init([Host, Opts]) -> mod_antispam_rtbl, pubsub_event_handler, 50), - RTBLHost = gen_mod:get_opt(rtbl_host, Opts), - RTBLDomainsNode = gen_mod:get_opt(rtbl_domains_node, Opts), + [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] = get_rtbl_services_option(Opts), mod_antispam_filter:init_filtering(Host), InitState = #state{host = Host, @@ -384,8 +402,8 @@ handle_cast({reload, NewOpts, OldOpts}, end, ok = mod_antispam_rtbl:unsubscribe(OldRTBLHost, OldRTBLDomainsNode, Host), {_Result, State3} = reload_files(State2#state{blocked_domains = #{}}), - RTBLHost = gen_mod:get_opt(rtbl_host, NewOpts), - RTBLDomainsNode = gen_mod:get_opt(rtbl_domains_node, NewOpts), + [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}] = + get_rtbl_services_option(NewOpts), ok = mod_antispam_rtbl:request_blocked_domains(RTBLHost, RTBLDomainsNode, Host), {noreply, State3#state{rtbl_host = RTBLHost, rtbl_domains_node = RTBLDomainsNode}}; handle_cast({update_blocked_domains, NewItems}, @@ -565,6 +583,27 @@ read_files(Host) -> whitelist_domains => gen_mod:get_module_opt(Host, ?MODULE, whitelist_domains_file)}, ejabberd_hooks:run_fold(antispam_get_lists, Host, AccInitial, [Files]). +get_rtbl_services_option(Host) when is_binary(Host) -> + get_rtbl_services_option(gen_mod:get_module_opts(Host, ?MODULE)); +get_rtbl_services_option(Opts) when is_map(Opts) -> + Services = gen_mod:get_opt(rtbl_services, Opts), + case length(Services) =< 1 of + true -> + ok; + false -> + ?WARNING_MSG("Option rtbl_services only supports one service, but several " + "were configured. Will use only first one", + []) + end, + case Services of + [] -> + [#rtbl_service{}]; + [Host | _] when is_binary(Host) -> + [#rtbl_service{host = Host, node = ?DEFAULT_RTBL_DOMAINS_NODE}]; + [[{Host, [{spam_source_domains_node, Node}]}] | _] -> + [#rtbl_service{host = Host, node = Node}] + end. + -spec get_proc_name(binary()) -> atom(). get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). diff --git a/src/mod_antispam_files.erl b/src/mod_antispam_files.erl index 0981edbb2..0362a1b72 100644 --- a/src/mod_antispam_files.erl +++ b/src/mod_antispam_files.erl @@ -52,7 +52,6 @@ -define(COMMAND_TIMEOUT, timer:seconds(30)). -define(DEFAULT_CACHE_SIZE, 10000). --define(DEFAULT_RTBL_DOMAINS_NODE, <<"spam_source_domains">>). -define(HTTPC_TIMEOUT, timer:seconds(3)). %%-------------------------------------------------------------------- diff --git a/src/mod_antispam_rtbl.erl b/src/mod_antispam_rtbl.erl index 246b59dfa..df5bac591 100644 --- a/src/mod_antispam_rtbl.erl +++ b/src/mod_antispam_rtbl.erl @@ -78,7 +78,7 @@ request_blocked_domains(RTBLHost, RTBLDomainsNode, From) -> -spec parse_blocked_domains(stanza()) -> #{binary() => any()} | undefined. parse_blocked_domains(#iq{to = #jid{lserver = LServer}, type = result} = IQ) -> ?DEBUG("parsing iq-result items: ~p", [IQ]), - RTBLDomainsNode = gen_mod:get_module_opt(LServer, ?SERVICE_MODULE, rtbl_domains_node), + [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), case xmpp:get_subtag(IQ, #pubsub{}) of #pubsub{items = #ps_items{node = RTBLDomainsNode, items = Items}} -> ?DEBUG("Got items:~n~p", [Items]), @@ -89,7 +89,7 @@ parse_blocked_domains(#iq{to = #jid{lserver = LServer}, type = result} = IQ) -> -spec parse_pubsub_event(stanza()) -> #{binary() => any()}. parse_pubsub_event(#message{to = #jid{lserver = LServer}} = Msg) -> - RTBLDomainsNode = gen_mod:get_module_opt(LServer, ?SERVICE_MODULE, rtbl_domains_node), + [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), case xmpp:get_subtag(Msg, #ps_event{}) of #ps_event{items = #ps_items{node = RTBLDomainsNode, @@ -130,7 +130,8 @@ pubsub_event_handler(#message{from = FromJid, Msg) -> ?DEBUG("Got RTBL message:~n~p", [Msg]), From = jid:encode(FromJid), - case gen_mod:get_module_opt(LServer, ?SERVICE_MODULE, rtbl_host) of + [#rtbl_service{host = RTBLHost}] = mod_antispam:get_rtbl_services_option(LServer), + case RTBLHost of From -> ParsedItems = parse_pubsub_event(Msg), Proc = gen_mod:get_module_proc(LServer, ?SERVICE_MODULE), diff --git a/test/antispam_tests.erl b/test/antispam_tests.erl index f60872913..8d7bd2472 100644 --- a/test/antispam_tests.erl +++ b/test/antispam_tests.erl @@ -31,6 +31,7 @@ disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, my_muc_jid/1, get_features/2, set_opt/3]). -include("suite.hrl"). +-include("mod_antispam.hrl"). %% @format-begin @@ -166,7 +167,8 @@ rtbl_domains(Config) -> RTBLDomainsNode = <<"spam_source_domains">>, OldOpts = gen_mod:get_module_opts(Host, mod_antispam), NewOpts = - maps:merge(OldOpts, #{rtbl_host => RTBLHost, rtbl_domains_node => RTBLDomainsNode}), + maps:merge(OldOpts, + #{rtbl_services => [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}]}), Owner = jid:make(?config(user, Config), ?config(server, Config), <<>>), {result, _} = mod_pubsub:create_node(RTBLHost, @@ -210,7 +212,8 @@ rtbl_domains_whitelisted(Config) -> RTBLDomainsNode = <<"spam_source_domains">>, OldOpts = gen_mod:get_module_opts(Host, mod_antispam), NewOpts = - maps:merge(OldOpts, #{rtbl_host => RTBLHost, rtbl_domains_node => RTBLDomainsNode}), + maps:merge(OldOpts, + #{rtbl_services => [#rtbl_service{host = RTBLHost, node = RTBLDomainsNode}]}), Owner = jid:make(?config(user, Config), ?config(server, Config), <<>>), {result, _} = mod_pubsub:create_node(RTBLHost, diff --git a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml index ae78645ae..56fdf5e6e 100644 --- a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml +++ b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml @@ -7,7 +7,8 @@ define_macro: db_type: internal access: local mod_antispam: - rtbl_host: pubsub.mnesia.localhost + rtbl_services: + - "pubsub.mnesia.localhost" spam_jids_file: spam_jids.txt spam_domains_file: spam_domains.txt spam_urls_file: spam_urls.txt diff --git a/test/ejabberd_SUITE_data/ejabberd.redis.yml b/test/ejabberd_SUITE_data/ejabberd.redis.yml index 8ec927e86..fb1ba435f 100644 --- a/test/ejabberd_SUITE_data/ejabberd.redis.yml +++ b/test/ejabberd_SUITE_data/ejabberd.redis.yml @@ -8,7 +8,8 @@ define_macro: db_type: internal access: local mod_antispam: - rtbl_host: pubsub.redis.localhost + rtbl_services: + - "pubsub.redis.localhost" spam_jids_file: spam_jids.txt spam_domains_file: spam_domains.txt spam_urls_file: spam_urls.txt