25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-24 16:23:40 +01:00
xmpp.chapril.org-ejabberd/src/mod_mix_pam.erl

520 lines
18 KiB
Erlang

%%%-------------------------------------------------------------------
%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% Created : 4 Dec 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2018 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_mix_pam).
-behaviour(gen_mod).
-protocol({xep, 405, '0.3.0'}).
%% gen_mod callbacks
-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]).
-export([mod_doc/0]).
%% Hooks and handlers
-export([bounce_sm_packet/1,
disco_sm_features/5,
remove_user/2,
process_iq/1,
get_mix_roster_items/2,
webadmin_user/4,
webadmin_page/3]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("mod_roster.hrl").
-include("translate.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-define(MIX_PAM_CACHE, mix_pam_cache).
-callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}.
-callback add_channel(jid(), jid(), binary()) -> ok | {error, db_failure}.
-callback del_channel(jid(), jid()) -> ok | {error, db_failure}.
-callback get_channel(jid(), jid()) -> {ok, binary()} | {error, notfound | db_failure}.
-callback get_channels(jid()) -> {ok, [{jid(), binary()}]} | {error, db_failure}.
-callback del_channels(jid()) -> ok | {error, db_failure}.
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].
-optional_callbacks([use_cache/1, cache_nodes/1]).
%%%===================================================================
%%% API
%%%===================================================================
start(Host, Opts) ->
Mod = gen_mod:db_mod(Opts, ?MODULE),
case Mod:init(Host, Opts) of
ok ->
init_cache(Mod, Host, Opts),
ejabberd_hooks:add(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50),
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50),
ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50),
ejabberd_hooks:add(roster_get, Host, ?MODULE, get_mix_roster_items, 50),
ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50),
ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, ?MODULE, process_iq),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2, ?MODULE, process_iq);
Err ->
Err
end.
stop(Host) ->
ejabberd_hooks:delete(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50),
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_sm_features, 50),
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_mix_roster_items, 50),
ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50),
ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2).
reload(Host, NewOpts, OldOpts) ->
NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
if NewMod /= OldMod ->
NewMod:init(Host, NewOpts);
true ->
ok
end,
init_cache(NewMod, Host, NewOpts).
depends(_Host, _Opts) ->
[].
mod_opt_type(db_type) ->
econf:db_type(?MODULE);
mod_opt_type(use_cache) ->
econf:bool();
mod_opt_type(cache_size) ->
econf:pos_int(infinity);
mod_opt_type(cache_missed) ->
econf:bool();
mod_opt_type(cache_life_time) ->
econf:timeout(second, infinity).
mod_options(Host) ->
[{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{use_cache, ejabberd_option:use_cache(Host)},
{cache_size, ejabberd_option:cache_size(Host)},
{cache_missed, ejabberd_option:cache_missed(Host)},
{cache_life_time, ejabberd_option:cache_life_time(Host)}].
mod_doc() ->
#{desc =>
[?T("This module implements "
"https://xmpp.org/extensions/xep-0405.html"
"[XEP-0405: Mediated Information eXchange (MIX): "
"Participant Server Requirements]. "
"The module is needed if MIX compatible clients "
"on your server are going to join MIX channels "
"(either on your server or on any remote servers)."), "",
?T("NOTE: 'mod_mix' is not required for this module "
"to work, however, without 'mod_mix_pam' the MIX "
"functionality of your local XMPP clients will be impaired.")],
opts =>
[{db_type,
#{value => "mnesia | sql",
desc =>
?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
-spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}.
bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To,
from = From,
type = groupchat} = Msg} = Acc) ->
case xmpp:has_subtag(Msg, #mix{}) of
true ->
{LUser, LServer, _} = jid:tolower(To),
case get_channel(To, From) of
{ok, _} ->
lists:foreach(
fun(R) ->
To1 = jid:replace_resource(To, R),
ejabberd_router:route(xmpp:set_to(Msg, To1))
end, ejabberd_sm:get_user_resources(LUser, LServer)),
{pass, Msg};
_ ->
Acc
end;
false ->
Acc
end;
bounce_sm_packet(Acc) ->
Acc.
-spec disco_sm_features({error, stanza_error()} | empty | {result, [binary()]},
jid(), jid(), binary(), binary()) ->
{error, stanza_error()} | empty | {result, [binary()]}.
disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
Acc;
disco_sm_features(Acc, _From, _To, <<"">>, _Lang) ->
{result, [?NS_MIX_PAM_0, ?NS_MIX_PAM_2 |
case Acc of
{result, Features} -> Features;
empty -> []
end]};
disco_sm_features(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec process_iq(iq()) -> iq() | ignore.
process_iq(#iq{from = #jid{luser = U1, lserver = S1},
to = #jid{luser = U2, lserver = S2}} = IQ)
when {U1, S1} /= {U2, S2} ->
xmpp:make_error(IQ, forbidden_query_error(IQ));
process_iq(#iq{type = set,
sub_els = [#mix_client_join{} = Join]} = IQ) ->
case Join#mix_client_join.channel of
undefined ->
xmpp:make_error(IQ, missing_channel_error(IQ));
_ ->
process_join(IQ)
end;
process_iq(#iq{type = set,
sub_els = [#mix_client_leave{} = Leave]} = IQ) ->
case Leave#mix_client_leave.channel of
undefined ->
xmpp:make_error(IQ, missing_channel_error(IQ));
_ ->
process_leave(IQ)
end;
process_iq(IQ) ->
xmpp:make_error(IQ, unsupported_query_error(IQ)).
-spec get_mix_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}].
get_mix_roster_items(Acc, {LUser, LServer}) ->
JID = jid:make(LUser, LServer),
case get_channels(JID) of
{ok, Channels} ->
lists:map(
fun({ItemJID, Id}) ->
#roster_item{
jid = ItemJID,
name = <<>>,
subscription = both,
ask = undefined,
groups = [<<"Channels">>],
mix_channel = #mix_roster_channel{participant_id = Id}
}
end, Channels);
_ ->
[]
end ++ Acc.
-spec remove_user(binary(), binary()) -> ok | {error, db_failure}.
remove_user(LUser, LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
JID = jid:make(LUser, LServer),
Chans = case Mod:get_channels(JID) of
{ok, Channels} ->
lists:map(
fun({Channel, _}) ->
ejabberd_router:route(
#iq{from = JID,
to = Channel,
id = p1_rand:get_string(),
type = set,
sub_els = [#mix_leave{}]}),
Channel
end, Channels);
_ ->
[]
end,
Mod:del_channels(jid:make(LUser, LServer)),
lists:foreach(
fun(Chan) ->
delete_cache(Mod, JID, Chan)
end, Chans).
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec process_join(iq()) -> ignore.
process_join(#iq{from = From, lang = Lang,
sub_els = [#mix_client_join{channel = Channel,
join = Join}]} = IQ) ->
ejabberd_router:route_iq(
#iq{from = jid:remove_resource(From),
to = Channel, type = set, sub_els = [Join]},
fun(#iq{sub_els = [El]} = ResIQ) ->
try xmpp:decode(El) of
MixJoin ->
process_join_result(ResIQ#iq {
sub_els = [MixJoin]
}, IQ)
catch
_:{xmpp_codec, Reason} ->
Txt = xmpp:io_format_error(Reason),
Err = xmpp:err_bad_request(Txt, Lang),
ejabberd_router:route_error(IQ, Err)
end
end),
ignore.
-spec process_leave(iq()) -> iq() | error.
process_leave(#iq{from = From,
sub_els = [#mix_client_leave{channel = Channel,
leave = Leave}]} = IQ) ->
case del_channel(From, Channel) of
ok ->
ejabberd_router:route_iq(
#iq{from = jid:remove_resource(From),
to = Channel, type = set, sub_els = [Leave]},
fun(ResIQ) -> process_leave_result(ResIQ, IQ) end),
ignore;
{error, db_failure} ->
xmpp:make_error(IQ, db_error(IQ))
end.
-spec process_join_result(iq(), iq()) -> ok.
process_join_result(#iq{from = #jid{} = Channel,
type = result, sub_els = [#mix_join{id = ID, xmlns = XmlNs} = Join]},
#iq{to = To} = IQ) ->
case add_channel(To, Channel, ID) of
ok ->
% Do roster push
mod_roster:push_item(To, #roster_item{jid = #jid{}}, #roster_item{
jid = Channel,
name = <<>>,
subscription = none,
ask = undefined,
groups = [],
mix_channel = #mix_roster_channel{participant_id = ID}
}),
% send IQ result
ChanID = make_channel_id(Channel, ID),
Join1 = Join#mix_join{id = <<"">>, jid = ChanID},
ResIQ = xmpp:make_iq_result(IQ, #mix_client_join{join = Join1, xmlns = XmlNs}),
ejabberd_router:route(ResIQ);
{error, db_failure} ->
ejabberd_router:route_error(IQ, db_error(IQ))
end;
process_join_result(#iq{type = error} = Err, IQ) ->
process_iq_error(Err, IQ).
-spec process_leave_result(iq(), iq()) -> ok.
process_leave_result(#iq{from = Channel, type = result, sub_els = [#mix_leave{xmlns = XmlNs} = Leave]},
#iq{to = User} = IQ) ->
% Do roster push
mod_roster:push_item(User,
#roster_item{jid = Channel, subscription = none},
#roster_item{jid = Channel, subscription = remove}),
% send iq result
ResIQ = xmpp:make_iq_result(IQ, #mix_client_leave{leave = Leave, xmlns = XmlNs}),
ejabberd_router:route(ResIQ);
process_leave_result(Err, IQ) ->
process_iq_error(Err, IQ).
-spec process_iq_error(iq(), iq()) -> ok.
process_iq_error(#iq{type = error} = ErrIQ, #iq{sub_els = [El]} = IQ) ->
case xmpp:get_error(ErrIQ) of
undefined ->
%% Not sure if this stuff is correct because
%% RFC6120 section 8.3.1 bullet 4 states that
%% an error stanza MUST contain an <error/> child element
IQ1 = xmpp:make_iq_result(IQ, El),
ejabberd_router:route(IQ1#iq{type = error});
Err ->
ejabberd_router:route_error(IQ, Err)
end;
process_iq_error(timeout, IQ) ->
Txt = ?T("Request has timed out"),
Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang),
ejabberd_router:route_error(IQ, Err).
-spec make_channel_id(jid(), binary()) -> jid().
make_channel_id(JID, ID) ->
{U, S, R} = jid:split(JID),
jid:make(<<ID/binary, $#, U/binary>>, S, R).
%%%===================================================================
%%% Error generators
%%%===================================================================
-spec missing_channel_error(stanza()) -> stanza_error().
missing_channel_error(Pkt) ->
Txt = ?T("Attribute 'channel' is required for this request"),
xmpp:err_bad_request(Txt, xmpp:get_lang(Pkt)).
-spec forbidden_query_error(stanza()) -> stanza_error().
forbidden_query_error(Pkt) ->
Txt = ?T("Query to another users is forbidden"),
xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)).
-spec unsupported_query_error(stanza()) -> stanza_error().
unsupported_query_error(Pkt) ->
Txt = ?T("No module is handling this query"),
xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)).
-spec db_error(stanza()) -> stanza_error().
db_error(Pkt) ->
Txt = ?T("Database failure"),
xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)).
%%%===================================================================
%%% Database queries
%%%===================================================================
get_channel(JID, Channel) ->
{LUser, LServer, _} = jid:tolower(JID),
{Chan, Service, _} = jid:tolower(Channel),
Mod = gen_mod:db_mod(LServer, ?MODULE),
case use_cache(Mod, LServer) of
false -> Mod:get_channel(JID, Channel);
true ->
case ets_cache:lookup(
?MIX_PAM_CACHE, {LUser, LServer, Chan, Service},
fun() -> Mod:get_channel(JID, Channel) end) of
error -> {error, notfound};
Ret -> Ret
end
end.
get_channels(JID) ->
{_, LServer, _} = jid:tolower(JID),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_channels(JID).
add_channel(JID, Channel, ID) ->
Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE),
case Mod:add_channel(JID, Channel, ID) of
ok -> delete_cache(Mod, JID, Channel);
Err -> Err
end.
del_channel(JID, Channel) ->
Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE),
case Mod:del_channel(JID, Channel) of
ok -> delete_cache(Mod, JID, Channel);
Err -> Err
end.
%%%===================================================================
%%% Cache management
%%%===================================================================
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
init_cache(Mod, Host, Opts) ->
case use_cache(Mod, Host) of
true ->
CacheOpts = cache_opts(Opts),
ets_cache:new(?MIX_PAM_CACHE, CacheOpts);
false ->
ets_cache:delete(?MIX_PAM_CACHE)
end.
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
cache_opts(Opts) ->
MaxSize = mod_mix_pam_opt:cache_size(Opts),
CacheMissed = mod_mix_pam_opt:cache_missed(Opts),
LifeTime = mod_mix_pam_opt:cache_life_time(Opts),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec use_cache(module(), binary()) -> boolean().
use_cache(Mod, Host) ->
case erlang:function_exported(Mod, use_cache, 1) of
true -> Mod:use_cache(Host);
false -> mod_mix_pam_opt:use_cache(Host)
end.
-spec cache_nodes(module(), binary()) -> [node()].
cache_nodes(Mod, Host) ->
case erlang:function_exported(Mod, cache_nodes, 1) of
true -> Mod:cache_nodes(Host);
false -> ejabberd_cluster:get_nodes()
end.
-spec delete_cache(module(), jid(), jid()) -> ok.
delete_cache(Mod, JID, Channel) ->
{LUser, LServer, _} = jid:tolower(JID),
{Chan, Service, _} = jid:tolower(Channel),
case use_cache(Mod, LServer) of
true ->
ets_cache:delete(?MIX_PAM_CACHE,
{LUser, LServer, Chan, Service},
cache_nodes(Mod, LServer));
false ->
ok
end.
%%%===================================================================
%%% Webadmin interface
%%%===================================================================
webadmin_user(Acc, User, Server, Lang) ->
QueueLen = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of
{ok, Channels} -> length(Channels);
error -> -1
end,
FQueueLen = ?C(integer_to_binary(QueueLen)),
FQueueView = ?AC(<<"mix_channels/">>, ?T("View joined MIX channels")),
Acc ++
[?XCT(<<"h3">>, ?T("Joined MIX channels:")),
FQueueLen,
?C(<<" | ">>),
FQueueView].
webadmin_page(_, Host,
#request{us = _US, path = [<<"user">>, U, <<"mix_channels">>],
lang = Lang} = _Request) ->
Res = web_mix_channels(U, Host, Lang),
{stop, Res};
webadmin_page(Acc, _, _) -> Acc.
web_mix_channels(User, Server, Lang) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
US = {LUser, LServer},
Items = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of
{ok, Channels} -> Channels;
error -> []
end,
SItems = lists:sort(Items),
FItems = case SItems of
[] -> [?CT(?T("None"))];
_ ->
THead = ?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Channel JID")),
?XCT(<<"td">>, ?T("Participant ID"))])]),
Entries = lists:map(fun ({JID, ID}) ->
?XE(<<"tr">>, [
?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], jid:encode(JID)),
?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], ID)
])
end, SItems),
[?XE(<<"table">>, [THead, ?XE(<<"tbody">>, Entries)])]
end,
PageTitle = str:translate_and_format(Lang, ?T("Joined MIX channels of ~ts"), [us_to_list(US)]),
(?H1GL(PageTitle, <<"modules/#mod-mix-pam">>, <<"mod_mix_pam">>))
++ FItems.
us_to_list({User, Server}) ->
jid:encode({User, Server, <<"">>}).