aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md27
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md4
-rw-r--r--rebar.config2
-rw-r--r--src/ejabberd_auth_jwt.erl5
-rw-r--r--src/ejabberd_mnesia.erl8
-rw-r--r--src/ejabberd_oauth.erl22
-rw-r--r--src/ejabberd_s2s.erl16
-rw-r--r--src/ejabberd_s2s_in.erl4
-rw-r--r--src/ejabberd_s2s_out.erl4
-rw-r--r--src/ejabberd_sql.erl26
-rw-r--r--src/ejabberd_web_admin.erl34
-rw-r--r--src/mod_bosh_mnesia.erl145
-rw-r--r--src/mod_jidprep.erl148
-rw-r--r--src/mod_muc_admin.erl10
-rw-r--r--src/mod_muc_room.erl3
-rw-r--r--src/mod_offline.erl5
-rw-r--r--src/mod_ping.erl2
-rw-r--r--src/mod_roster.erl4
-rw-r--r--src/mod_shared_roster.erl29
-rw-r--r--test/ejabberd_SUITE.erl1
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.yml1
-rw-r--r--test/jidprep_tests.erl62
22 files changed, 474 insertions, 88 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index bb5a6e196..647cf9245 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,21 +1,30 @@
-Environment
------------
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: Kind:Bug
+assignees: ''
+
+---
+
+## Environment
+
- ejabberd version: 18.09
- Erlang version: `erl +V`
- OS: Linux (Debian)
- Installed from: source | distro package | official deb/rpm | official binary installer | other
-Configuration (only if needed): grep -Ev '^$|^\s*#' ejabberd.yml
----------------------------------------------------------------------------
+## Configuration (only if needed): grep -Ev '^$|^\s*#' ejabberd.yml
+
```yaml
loglevel: 4
...
```
-Errors from error.log/crash.log
--------------------------------
+## Errors from error.log/crash.log
+
No errors
-Bug description
----------------
-Nothing works, plz halp :(
+## Bug description
+
+Please, give us a precise description (what does not work, what is expected, etc.)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 751cb5de0..da5b6ff25 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
-labels:
+title: ''
+labels: Kind:Feature
+assignees: ''
---
diff --git a/rebar.config b/rebar.config
index e05fe84e6..ec068db17 100644
--- a/rebar.config
+++ b/rebar.config
@@ -24,7 +24,7 @@
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.2"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.17"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.37"}}},
- {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.4.0"}}},
+ {xmpp, ".*", {git, "https://github.com/processone/xmpp", "e3181a5"}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.20"}}},
{yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.0"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}},
diff --git a/src/ejabberd_auth_jwt.erl b/src/ejabberd_auth_jwt.erl
index d80945628..3b3698d1c 100644
--- a/src/ejabberd_auth_jwt.erl
+++ b/src/ejabberd_auth_jwt.erl
@@ -30,7 +30,8 @@
-behaviour(ejabberd_auth).
-export([start/1, stop/1, check_password/4,
- store_type/1, plain_password_required/1
+ store_type/1, plain_password_required/1,
+ user_exists/2
]).
-include("xmpp.hrl").
@@ -67,6 +68,8 @@ check_password(User, AuthzId, Server, Token) ->
end
end.
+user_exists(_User, _Host) -> {nocache, false}.
+
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl
index 3cf2e460a..32fa158c2 100644
--- a/src/ejabberd_mnesia.erl
+++ b/src/ejabberd_mnesia.erl
@@ -258,7 +258,13 @@ validator() ->
[unique]).
create(Name, TabDef) ->
- ?INFO_MSG("Creating Mnesia table '~s'", [Name]),
+ Type = lists:foldl(
+ fun({ram_copies, _}, _) -> " ram ";
+ ({disc_copies, _}, _) -> " disc ";
+ ({disc_only_copies, _}, _) -> " disc_only ";
+ (_, Acc) -> Acc
+ end, " ", TabDef),
+ ?INFO_MSG("Creating Mnesia~stable '~s'", [Type, Name]),
case mnesia_op(create_table, [Name, TabDef]) of
{atomic, ok} ->
add_table_copy(Name);
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 31826fa53..d9b16c70e 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -45,10 +45,11 @@
check_token/2,
scope_in_scope_list/2,
process/2,
- config_reloaded/0]).
+ config_reloaded/0,
+ verify_resowner_scope/3]).
-export([get_commands_spec/0,
- oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1]).
+ oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1]).
-include("xmpp.hrl").
-include("logger.hrl").
@@ -211,6 +212,21 @@ authenticate_user({User, Server}, Ctx) ->
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
+-spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) ->
+ {ok, any(), [binary()]} | {error, any()}.
+verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
+ Cmds = [atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands()],
+ AllowedScopes = [<<"ejabberd:user">>, <<"ejabberd:admin">>, <<"sasl_auth">>] ++ Cmds,
+ case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
+ oauth2_priv_set:new(AllowedScopes)) of
+ true ->
+ {ok, {Ctx, Scope}};
+ false ->
+ {error, badscope}
+ end;
+verify_resowner_scope(_, _, _) ->
+ {error, badscope}.
+
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
@@ -398,7 +414,7 @@ process(_Handlers,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
[?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)]),
- ?INPUTID(<<"text">>, <<"username">>, <<"">>),
+ ?INPUTID(<<"email">>, <<"username">>, <<"">>),
?BR,
?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 2796a43fd..231622551 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -42,7 +42,7 @@
list_temporarily_blocked_hosts/0,
external_host_overloaded/1, is_temporarly_blocked/1,
get_commands_spec/0, zlib_enabled/1, get_idle_timeout/1,
- tls_required/1, tls_enabled/1, tls_options/2,
+ tls_required/1, tls_enabled/1, tls_options/3,
host_up/1, host_down/1, queue_type/1]).
%% gen_server callbacks
@@ -177,34 +177,34 @@ try_register({From, To} = FromTo) ->
dirty_get_connections() ->
mnesia:dirty_all_keys(s2s).
--spec tls_options(binary(), [proplists:property()]) -> [proplists:property()].
-tls_options(LServer, DefaultOpts) ->
+-spec tls_options(binary(), binary(), [proplists:property()]) -> [proplists:property()].
+tls_options(LServer, ServerHost, DefaultOpts) ->
TLSOpts1 = case ejabberd_pkix:get_certfile(LServer) of
error -> DefaultOpts;
{ok, CertFile} ->
lists:keystore(certfile, 1, DefaultOpts,
{certfile, CertFile})
end,
- TLSOpts2 = case ejabberd_option:s2s_ciphers(LServer) of
+ TLSOpts2 = case ejabberd_option:s2s_ciphers(ServerHost) of
undefined -> TLSOpts1;
Ciphers -> lists:keystore(ciphers, 1, TLSOpts1,
{ciphers, Ciphers})
end,
- TLSOpts3 = case ejabberd_option:s2s_protocol_options(LServer) of
+ TLSOpts3 = case ejabberd_option:s2s_protocol_options(ServerHost) of
undefined -> TLSOpts2;
ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2,
{protocol_options, ProtoOpts})
end,
- TLSOpts4 = case ejabberd_option:s2s_dhfile(LServer) of
+ TLSOpts4 = case ejabberd_option:s2s_dhfile(ServerHost) of
undefined -> TLSOpts3;
DHFile -> lists:keystore(dhfile, 1, TLSOpts3,
{dhfile, DHFile})
end,
TLSOpts5 = case lists:keymember(cafile, 1, TLSOpts4) of
true -> TLSOpts4;
- false -> [{cafile, get_cafile(LServer)}|TLSOpts4]
+ false -> [{cafile, get_cafile(ServerHost)}|TLSOpts4]
end,
- case ejabberd_option:s2s_tls_compression(LServer) of
+ case ejabberd_option:s2s_tls_compression(ServerHost) of
undefined -> TLSOpts5;
false -> [compression_none | TLSOpts5];
true -> lists:delete(compression_none, TLSOpts5)
diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl
index 4b6f70ea5..7e3bd6a89 100644
--- a/src/ejabberd_s2s_in.erl
+++ b/src/ejabberd_s2s_in.erl
@@ -135,8 +135,8 @@ process_closed(#{server := LServer} = State, Reason) ->
%%%===================================================================
%%% xmpp_stream_in callbacks
%%%===================================================================
-tls_options(#{tls_options := TLSOpts, server_host := ServerHost}) ->
- ejabberd_s2s:tls_options(ServerHost, TLSOpts).
+tls_options(#{tls_options := TLSOpts, lserver := LServer, server_host := ServerHost}) ->
+ ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts).
tls_required(#{server_host := ServerHost}) ->
ejabberd_s2s:tls_required(ServerHost).
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index 40ece0f7b..7bbc5eeb3 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -175,8 +175,8 @@ process_downgraded(State, _StreamStart) ->
%%%===================================================================
%%% xmpp_stream_out callbacks
%%%===================================================================
-tls_options(#{server_host := ServerHost}) ->
- ejabberd_s2s:tls_options(ServerHost, []).
+tls_options(#{server := LServer, server_host := ServerHost}) ->
+ ejabberd_s2s:tls_options(LServer, ServerHost, []).
tls_required(#{server_host := ServerHost}) ->
ejabberd_s2s:tls_required(ServerHost).
diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl
index 92642e7b7..42254d28d 100644
--- a/src/ejabberd_sql.erl
+++ b/src/ejabberd_sql.erl
@@ -523,15 +523,15 @@ outer_transaction(F, NRestarts, _Reason) ->
[T]),
erlang:exit(implementation_faulty)
end,
- sql_query_internal([<<"begin;">>]),
+ sql_begin(),
put(?NESTING_KEY, PreviousNestingLevel + 1),
try F() of
Res ->
- sql_query_internal([<<"commit;">>]),
+ sql_commit(),
{atomic, Res}
catch
?EX_RULE(throw, {aborted, Reason}, _) when NRestarts > 0 ->
- sql_query_internal([<<"rollback;">>]),
+ sql_rollback(),
put(?NESTING_KEY, ?TOP_LEVEL_TXN),
outer_transaction(F, NRestarts - 1, Reason);
?EX_RULE(throw, {aborted, Reason}, Stack) when NRestarts =:= 0 ->
@@ -542,10 +542,10 @@ outer_transaction(F, NRestarts, _Reason) ->
"== ~p",
[?MAX_TRANSACTION_RESTARTS, Reason,
StackTrace, get(?STATE_KEY)]),
- sql_query_internal([<<"rollback;">>]),
+ sql_rollback(),
{aborted, Reason};
?EX_RULE(exit, Reason, _) ->
- sql_query_internal([<<"rollback;">>]),
+ sql_rollback(),
{aborted, Reason}
end.
@@ -772,6 +772,22 @@ sql_query_format_res(Res, _SQLQuery) ->
sql_query_to_iolist(SQLQuery) ->
generic_sql_query_format(SQLQuery).
+sql_begin() ->
+ sql_query_internal(
+ [{mssql, [<<"begin transaction;">>]},
+ {any, [<<"begin;">>]}]).
+
+sql_commit() ->
+ sql_query_internal(
+ [{mssql, [<<"commit transaction;">>]},
+ {any, [<<"commit;">>]}]).
+
+sql_rollback() ->
+ sql_query_internal(
+ [{mssql, [<<"rollback transaction;">>]},
+ {any, [<<"rollback;">>]}]).
+
+
%% Generate the OTP callback return tuple depending on the driver result.
abort_on_driver_error({error, <<"query timed out">>} = Reply, From, Timestamp) ->
reply(From, Reply, Timestamp),
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 9bd278889..c168cae19 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -265,6 +265,13 @@ get_auth_admin(Auth, HostHTTP, RPath, Method) ->
get_auth_account(HostOfRule, AccessRule, User, Server,
Pass) ->
+ case lists:member(Server, ejabberd_config:get_option(hosts)) of
+ true -> get_auth_account2(HostOfRule, AccessRule, User, Server, Pass);
+ false -> {unauthorized, <<"inexistent-host">>}
+ end.
+
+get_auth_account2(HostOfRule, AccessRule, User, Server,
+ Pass) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true ->
case any_rules_allowed(HostOfRule, AccessRule,
@@ -443,7 +450,7 @@ process_admin(_Host, #request{path = [<<"additions.js">>]}, _) ->
process_admin(global, #request{path = [<<"vhosts">>], lang = Lang}, AJID) ->
Res = list_vhosts(Lang, AJID),
make_xhtml((?H1GL((translate:translate(Lang, ?T("Virtual Hosts"))),
- <<"virtualhosting">>, ?T("Virtual Hosting")))
+ <<"virtual-hosting">>, ?T("Virtual Hosting")))
++ Res,
global, Lang, AJID);
process_admin(Host, #request{path = [<<"users">>], q = Query,
@@ -478,8 +485,8 @@ process_admin(Host, #request{path = [<<"last-activity">>],
list_last_activity(Host, Lang, false, Month);
_ -> list_last_activity(Host, Lang, true, Month)
end,
- make_xhtml([?XCT(<<"h1">>, ?T("Users Last Activity"))]
- ++
+ PageH1 = ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), <<"mod-last">>, <<"mod_last">>),
+ make_xhtml(PageH1 ++
[?XAE(<<"form">>,
[{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
[?CT(?T("Period: ")),
@@ -509,8 +516,8 @@ process_admin(Host, #request{path = [<<"last-activity">>],
Host, Lang, AJID);
process_admin(Host, #request{path = [<<"stats">>], lang = Lang}, AJID) ->
Res = get_stats(Host, Lang),
- make_xhtml([?XCT(<<"h1">>, ?T("Statistics"))] ++ Res,
- Host, Lang, AJID);
+ PageH1 = ?H1GL(translate:translate(Lang, ?T("Statistics")), <<"mod-stats">>, <<"mod_stats">>),
+ make_xhtml(PageH1 ++ Res, Host, Lang, AJID);
process_admin(Host, #request{path = [<<"user">>, U],
q = Query, lang = Lang}, AJID) ->
case ejabberd_auth:user_exists(U, Host) of
@@ -1285,9 +1292,7 @@ get_node(global, Node, [<<"backup">>], Query, Lang) ->
[?CT(?T("Export data of users in a host to PIEFXIS "
"files (XEP-0227):")),
?C(<<" ">>),
- ?INPUT(<<"text">>,
- <<"export_piefxis_host_dirhost">>,
- (ejabberd_config:get_myname()))]),
+ make_select_host(Lang, <<"export_piefxis_host_dirhost">>)]),
?XE(<<"td">>,
[?INPUT(<<"text">>,
<<"export_piefxis_host_dirpath">>,
@@ -1301,9 +1306,7 @@ get_node(global, Node, [<<"backup">>], Query, Lang) ->
[?CT(?T("Export all tables as SQL queries "
"to a file:")),
?C(<<" ">>),
- ?INPUT(<<"text">>,
- <<"export_sql_filehost">>,
- (ejabberd_config:get_myname()))]),
+ make_select_host(Lang, <<"export_sql_filehost">>)]),
?XE(<<"td">>,
[?INPUT(<<"text">>,
<<"export_sql_filepath">>,
@@ -1473,6 +1476,15 @@ node_parse_query(Node, Query) ->
end
end.
+make_select_host(Lang, Name) ->
+ ?XAE(<<"select">>,
+ [{<<"name">>, Name}],
+ (lists:map(fun (Host) ->
+ ?XACT(<<"option">>,
+ ([{<<"value">>, Host}]), Host)
+ end,
+ ejabberd_config:get_option(hosts)))).
+
db_storage_select(ID, Opt, Lang) ->
?XAE(<<"select">>,
[{<<"name">>, <<"table", ID/binary>>}],
diff --git a/src/mod_bosh_mnesia.erl b/src/mod_bosh_mnesia.erl
index fd3135c31..c84b01704 100644
--- a/src/mod_bosh_mnesia.erl
+++ b/src/mod_bosh_mnesia.erl
@@ -33,12 +33,16 @@
terminate/2, code_change/3, start_link/0]).
-include("logger.hrl").
+-include_lib("stdlib/include/ms_transform.hrl").
--record(bosh, {sid = <<"">> :: binary() | '_',
- timestamp = erlang:timestamp() :: erlang:timestamp() | '_',
- pid = self() :: pid() | '$1'}).
+-define(CALL_TIMEOUT, timer:minutes(10)).
--record(state, {}).
+-record(bosh, {sid = <<"">> :: binary(),
+ timestamp = erlang:timestamp() :: erlang:timestamp(),
+ pid = self() :: pid()}).
+
+-record(state, {nodes = #{} :: #{node() => {pid(), reference()}}}).
+-type state() :: #state{}.
%%%===================================================================
%%% API
@@ -49,6 +53,7 @@ init() ->
transient, 5000, worker, [?MODULE]},
case supervisor:start_child(ejabberd_backend_sup, Spec) of
{ok, _Pid} -> ok;
+ {error, {already_started, _}} -> ok;
Err -> Err
end.
@@ -59,28 +64,21 @@ start_link() ->
use_cache() ->
false.
+-spec open_session(binary(), pid()) -> ok.
open_session(SID, Pid) ->
Session = #bosh{sid = SID, timestamp = erlang:timestamp(), pid = Pid},
- lists:foreach(
- fun(Node) when Node == node() ->
- gen_server:call(?MODULE, {write, Session});
- (Node) ->
- cluster_send({?MODULE, Node}, {write, Session})
- end, ejabberd_cluster:get_nodes()).
+ gen_server:call(?MODULE, {write, Session}, ?CALL_TIMEOUT).
+-spec close_session(binary()) -> ok.
close_session(SID) ->
case mnesia:dirty_read(bosh, SID) of
[Session] ->
- lists:foreach(
- fun(Node) when Node == node() ->
- gen_server:call(?MODULE, {delete, Session});
- (Node) ->
- cluster_send({?MODULE, Node}, {delete, Session})
- end, ejabberd_cluster:get_nodes());
+ gen_server:call(?MODULE, {delete, Session}, ?CALL_TIMEOUT);
[] ->
ok
end.
+-spec find_session(binary()) -> {ok, pid()} | {error, notfound}.
find_session(SID) ->
case mnesia:dirty_read(bosh, SID) of
[#bosh{pid = Pid}] ->
@@ -92,30 +90,90 @@ find_session(SID) ->
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
+-spec init([]) -> {ok, state()}.
init([]) ->
setup_database(),
+ multicast({join, node(), self()}),
+ mnesia:subscribe(system),
{ok, #state{}}.
-handle_call({write, Session}, _From, State) ->
- Res = write_session(Session),
- {reply, Res, State};
-handle_call({delete, Session}, _From, State) ->
- Res = delete_session(Session),
- {reply, Res, State};
+-spec handle_call(_, _, state()) -> {reply, ok, state()} | {noreply, state()}.
+handle_call({write, Session} = Msg, _From, State) ->
+ write_session(Session),
+ multicast(Msg),
+ {reply, ok, State};
+handle_call({delete, Session} = Msg, _From, State) ->
+ delete_session(Session),
+ multicast(Msg),
+ {reply, ok, State};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
+-spec handle_cast(_, state()) -> {noreply, state()}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
+-spec handle_info(_, state()) -> {noreply, state()}.
handle_info({write, Session}, State) ->
write_session(Session),
{noreply, State};
handle_info({delete, Session}, State) ->
delete_session(Session),
{noreply, State};
+handle_info({join, Node, Pid}, State) ->
+ ejabberd_cluster:send(Pid, {joined, node(), self()}),
+ case maps:find(Node, State#state.nodes) of
+ {ok, {Pid, _}} ->
+ ok;
+ _ ->
+ ejabberd_cluster:send(Pid, {join, node(), self()})
+ end,
+ {noreply, State};
+handle_info({joined, Node, Pid}, State) ->
+ case maps:find(Node, State#state.nodes) of
+ {ok, {Pid, _}} ->
+ {noreply, State};
+ Ret ->
+ MRef = erlang:monitor(process, {?MODULE, Node}),
+ Nodes = maps:put(Node, {Pid, MRef}, State#state.nodes),
+ case Ret of
+ error -> ejabberd_cluster:send(Pid, {first, self()});
+ _ -> ok
+ end,
+ {noreply, State#state{nodes = Nodes}}
+ end;
+handle_info({first, From}, State) ->
+ ejabberd_cluster:send(From, {replica, node(), first_session()}),
+ {noreply, State};
+handle_info({next, From, Key}, State) ->
+ ejabberd_cluster:send(From, {replica, node(), next_session(Key)}),
+ {noreply, State};
+handle_info({replica, _From, '$end_of_table'}, State) ->
+ {noreply, State};
+handle_info({replica, From, Session}, State) ->
+ write_session(Session),
+ ejabberd_cluster:send(From, {next, self(), Session#bosh.sid}),
+ {noreply, State};
+handle_info({'DOWN', _, process, {?MODULE, _}, _Info}, State) ->
+ {noreply, State};
+handle_info({mnesia_system_event, {mnesia_down, Node}}, State) ->
+ Sessions =
+ ets:select(
+ bosh,
+ ets:fun2ms(
+ fun(#bosh{pid = Pid} = S) when node(Pid) == Node ->
+ S
+ end)),
+ lists:foreach(
+ fun(S) ->
+ mnesia:dirty_delete_object(S)
+ end, Sessions),
+ Nodes = maps:remove(Node, State#state.nodes),
+ {noreply, State#state{nodes = Nodes}};
+handle_info({mnesia_system_event, _}, State) ->
+ {noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
@@ -129,22 +187,24 @@ code_change(_OldVsn, State, _Extra) ->
%%%===================================================================
%%% Internal functions
%%%===================================================================
+-spec write_session(#bosh{}) -> ok.
write_session(#bosh{pid = Pid1, sid = SID, timestamp = T1} = S1) ->
case mnesia:dirty_read(bosh, SID) of
[#bosh{pid = Pid2, timestamp = T2} = S2] ->
if Pid1 == Pid2 ->
mnesia:dirty_write(S1);
T1 < T2 ->
- cluster_send(Pid2, replaced),
+ ejabberd_cluster:send(Pid2, replaced),
mnesia:dirty_write(S1);
true ->
- cluster_send(Pid1, replaced),
+ ejabberd_cluster:send(Pid1, replaced),
mnesia:dirty_write(S2)
end;
[] ->
mnesia:dirty_write(S1)
end.
+-spec delete_session(#bosh{}) -> ok.
delete_session(#bosh{sid = SID, pid = Pid1}) ->
case mnesia:dirty_read(bosh, SID) of
[#bosh{pid = Pid2}] ->
@@ -157,8 +217,14 @@ delete_session(#bosh{sid = SID, pid = Pid1}) ->
ok
end.
-cluster_send(NodePid, Msg) ->
- erlang:send(NodePid, Msg, [noconnect, nosuspend]).
+-spec multicast(_) -> ok.
+multicast(Msg) ->
+ lists:foreach(
+ fun(Node) when Node /= node() ->
+ ejabberd_cluster:send({?MODULE, Node}, Msg);
+ (_) ->
+ ok
+ end, ejabberd_cluster:get_nodes()).
setup_database() ->
case catch mnesia:table_info(bosh, attributes) of
@@ -170,3 +236,30 @@ setup_database() ->
ejabberd_mnesia:create(?MODULE, bosh,
[{ram_copies, [node()]}, {local_content, true},
{attributes, record_info(fields, bosh)}]).
+
+-spec first_session() -> #bosh{} | '$end_of_table'.
+first_session() ->
+ case mnesia:dirty_first(bosh) of
+ '$end_of_table' ->
+ '$end_of_table';
+ First ->
+ read_session(First)
+ end.
+
+-spec next_session(binary()) -> #bosh{} | '$end_of_table'.
+next_session(Prev) ->
+ case mnesia:dirty_next(bosh, Prev) of
+ '$end_of_table' ->
+ '$end_of_table';
+ Next ->
+ read_session(Next)
+ end.
+
+-spec read_session(binary()) -> #bosh{} | '$end_of_table'.
+read_session(Key) ->
+ case mnesia:dirty_read(bosh, Key) of
+ [#bosh{pid = Pid} = Session] when node(Pid) == node() ->
+ Session;
+ _ ->
+ next_session(Key)
+ end.
diff --git a/src/mod_jidprep.erl b/src/mod_jidprep.erl
new file mode 100644
index 000000000..d530b5069
--- /dev/null
+++ b/src/mod_jidprep.erl
@@ -0,0 +1,148 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_jidprep.erl
+%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
+%%% Purpose : JID Prep (XEP-0328)
+%%% Created : 11 Sep 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2019 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_jidprep).
+-author('holger@zedat.fu-berlin.de').
+-protocol({xep, 328, '0.1'}).
+
+-behaviour(gen_mod).
+
+%% gen_mod callbacks.
+-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]).
+
+%% ejabberd_hooks callbacks.
+-export([disco_local_features/5]).
+
+%% gen_iq_handler callback.
+-export([process_iq/1]).
+
+-include("logger.hrl").
+-include("translate.hrl").
+-include("xmpp.hrl").
+
+%%--------------------------------------------------------------------
+%% gen_mod callbacks.
+%%--------------------------------------------------------------------
+-spec start(binary(), gen_mod:opts()) -> ok.
+start(Host, _Opts) ->
+ register_iq_handlers(Host),
+ register_hooks(Host).
+
+-spec stop(binary()) -> ok.
+stop(Host) ->
+ unregister_hooks(Host),
+ unregister_iq_handlers(Host).
+
+-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok.
+reload(_Host, _NewOpts, _OldOpts) ->
+ ok.
+
+-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
+depends(_Host, _Opts) ->
+ [].
+
+-spec mod_opt_type(atom()) -> econf:validator().
+mod_opt_type(access) ->
+ econf:acl().
+
+-spec mod_options(binary()) -> [{atom(), any()}].
+mod_options(_Host) ->
+ [{access, local}].
+
+%%--------------------------------------------------------------------
+%% Register/unregister hooks.
+%%--------------------------------------------------------------------
+-spec register_hooks(binary()) -> ok.
+register_hooks(Host) ->
+ ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
+ disco_local_features, 50).
+
+-spec unregister_hooks(binary()) -> ok.
+unregister_hooks(Host) ->
+ ejabberd_hooks:delete(disco_local_features, Host, ?MODULE,
+ disco_local_features, 50).
+
+%%--------------------------------------------------------------------
+%% Service discovery.
+%%--------------------------------------------------------------------
+-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(),
+ binary()) -> mod_disco:features_acc().
+disco_local_features(empty, From, To, Node, Lang) ->
+ disco_local_features({result, []}, From, To, Node, Lang);
+disco_local_features({result, OtherFeatures} = Acc, From,
+ #jid{lserver = LServer}, <<"">>, _Lang) ->
+ Access = gen_mod:get_module_opt(LServer, ?MODULE, access),
+ case acl:match_rule(LServer, Access, From) of
+ allow ->
+ {result, [?NS_JIDPREP_0 | OtherFeatures]};
+ deny ->
+ Acc
+ end;
+disco_local_features(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+%%--------------------------------------------------------------------
+%% IQ handlers.
+%%--------------------------------------------------------------------
+-spec register_iq_handlers(binary()) -> ok.
+register_iq_handlers(Host) ->
+ gen_iq_handler:add_iq_handler(ejabberd_local, Host,
+ ?NS_JIDPREP_0, ?MODULE, process_iq).
+
+-spec unregister_iq_handlers(binary()) -> ok.
+unregister_iq_handlers(Host) ->
+ gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_JIDPREP_0).
+
+-spec process_iq(iq()) -> iq().
+process_iq(#iq{type = set, lang = Lang} = IQ) ->
+ Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
+ xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
+process_iq(#iq{from = From, to = #jid{lserver = LServer}, lang = Lang,
+ sub_els = [#jidprep{jid = #jid{luser = U,
+ lserver = S,
+ lresource = R} = JID}]} = IQ) ->
+ Access = gen_mod:get_module_opt(LServer, ?MODULE, access),
+ case acl:match_rule(LServer, Access, From) of
+ allow ->
+ case jid:make(U, S, R) of
+ #jid{} = Normalized ->
+ ?DEBUG("Normalized JID for ~s: ~s",
+ [jid:encode(From), jid:encode(JID)]),
+ xmpp:make_iq_result(IQ, #jidprep{jid = Normalized});
+ error -> % Cannot happen.
+ ?DEBUG("Normalizing JID failed for ~s: ~s",
+ [jid:encode(From), jid:encode(JID)]),
+ Txt = ?T("JID normalization failed"),
+ xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang))
+ end;
+ deny ->
+ ?DEBUG("Won't return normalized JID to ~s: ~s",
+ [jid:encode(From), jid:encode(JID)]),
+ Txt = ?T("JID normalization denied by service policy"),
+ xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
+ end;
+process_iq(#iq{lang = Lang} = IQ) ->
+ Txt = ?T("No module is handling this query"),
+ xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 805e72481..4de0b2205 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -458,8 +458,9 @@ web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) ->
fun(Host, Acc) ->
Acc + mod_muc:count_online_rooms(Host)
end, 0, find_hosts(global)),
- Res = [?XCT(<<"h1">>, ?T("Multi-User Chat")),
- ?XCT(<<"h3">>, ?T("Statistics")),
+ PageTitle = translate:translate(Lang, ?T("Multi-User Chat")),
+ Res = ?H1GL(PageTitle, <<"mod-muc">>, <<"mod_muc">>) ++
+ [?XCT(<<"h3">>, ?T("Statistics")),
?XAE(<<"table">>, [],
[?XE(<<"tbody">>, [?TDTD(?T("Total rooms"), OnlineRoomsNumber)
])
@@ -531,8 +532,9 @@ make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) ->
end,
1,
Titles),
- [?XCT(<<"h1">>, ?T("Multi-User Chat")),
- ?XCT(<<"h2">>, ?T("Chatrooms")),
+ PageTitle = translate:translate(Lang, ?T("Multi-User Chat")),
+ ?H1GL(PageTitle, <<"mod-muc">>, <<"mod_muc">>) ++
+ [?XCT(<<"h2">>, ?T("Chatrooms")),
?XE(<<"table">>,
[?XE(<<"thead">>,
[?XE(<<"tr">>, Titles_TR)]
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 0dfd078a5..808b8a246 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -2418,7 +2418,8 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
last_presence = Presence0} = UserInfo =
maps:get(jid:tolower(LJID), StateData#state.users),
{Role1, Presence1} =
- case presence_broadcast_allowed(NJID, StateData) of
+ case (presence_broadcast_allowed(NJID, StateData) orelse
+ presence_broadcast_allowed(NJID, OldStateData)) of
true -> {Role0, Presence0};
false -> {none, #presence{type = unavailable}}
end,
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 07d71bfdc..496e15ea0 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -994,9 +994,8 @@ user_queue(User, Server, Query, Lang) ->
end,
Hdrs = get_messages_subset(User, Server, HdrsAll),
FMsgs = format_user_queue(Hdrs),
- [?XC(<<"h1">>,
- (str:format(translate:translate(Lang, ?T("~s's Offline Messages Queue")),
- [us_to_list(US)])))]
+ PageTitle = str:format(translate:translate(Lang, ?T("~s's Offline Messages Queue")), [us_to_list(US)]),
+ (?H1GL(PageTitle, <<"mod-offline">>, <<"mod_offline">>))
++ [?XREST(?T("Submitted"))] ++
[?XAE(<<"form">>,
[{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
diff --git a/src/mod_ping.erl b/src/mod_ping.erl
index d4c226e56..8e6827247 100644
--- a/src/mod_ping.erl
+++ b/src/mod_ping.erl
@@ -132,8 +132,6 @@ handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
-handle_info({iq_reply, #iq{type = error}, JID}, State) ->
- handle_info({iq_reply, timeout, JID}, State);
handle_info({iq_reply, #iq{}, _JID}, State) ->
{noreply, State};
handle_info({iq_reply, timeout, JID}, State) ->
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index 426589319..6446a368d 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -1004,8 +1004,8 @@ user_roster(User, Server, Query, Lang) ->
end,
SItems)))])]
end,
- [?XC(<<"h1">>,
- (<<(translate:translate(Lang, ?T("Roster of ")))/binary, (us_to_list(US))/binary>>))]
+ PageTitle = str:format(translate:translate(Lang, ?T("Roster of ~s")), [us_to_list(US)]),
+ (?H1GL(PageTitle, <<"mod-roster">>, <<"mod_roster">>))
++
case Res of
ok -> [?XREST(?T("Submitted"))];
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 47dffca80..afa0a49bf 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -542,8 +542,16 @@ is_user_in_group(US, Group, Host) ->
true
end.
-%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok}
+%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} | error
add_user_to_group(Host, US, Group) ->
+ {_LUser, LServer} = US,
+ case lists:member(LServer, ejabberd_config:get_option(hosts)) of
+ true -> add_user_to_group2(Host, US, Group);
+ false ->
+ ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~s", [LServer]),
+ error
+ end.
+add_user_to_group2(Host, US, Group) ->
{LUser, LServer} = US,
case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of
match ->
@@ -772,7 +780,7 @@ list_shared_roster_groups(Host, Query, Lang) ->
[?INPUTT(<<"submit">>, <<"addnew">>,
?T("Add New"))])])]))])),
(?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))),
- <<"mod_shared_roster">>, <<"mod_shared_roster">>))
+ <<"mod-shared-roster">>, <<"mod_shared_roster">>))
++
case Res of
ok -> [?XREST(?T("Submitted"))];
@@ -866,11 +874,17 @@ shared_roster_group(Host, Group, Query, Lang) ->
<<"20">>,
list_to_binary(FDisplayedGroups))])])])])),
(?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))),
- <<"mod_shared_roster">>, <<"mod_shared_roster">>))
+ <<"mod-shared-roster">>, <<"mod_shared_roster">>))
++
[?XC(<<"h2">>, <<(translate:translate(Lang, ?T("Group ")))/binary, Group/binary>>)] ++
case Res of
ok -> [?XREST(?T("Submitted"))];
+ {error_jids, NonAddedList1} ->
+ NonAddedList2 = [jid:encode({U,S,<<>>}) || {U,S} <- NonAddedList1],
+ NonAddedList3 = str:join(NonAddedList2, <<", ">>),
+ NonAddedText1 = translate:translate(Lang, ?T("Members not added (inexistent vhost): ")),
+ NonAddedText2 = str:concat(NonAddedText1, NonAddedList3),
+ [?XRES(NonAddedText2)];
error -> [?XREST(?T("Bad format"))];
nothing -> []
end
@@ -951,12 +965,15 @@ shared_roster_group_parse_query(Host, Group, Query) ->
Group)
end,
RemovedMembers),
- lists:foreach(fun (US) ->
- mod_shared_roster:add_user_to_group(Host, US,
+ NonAddedMembers = lists:filter(fun (US) ->
+ error == mod_shared_roster:add_user_to_group(Host, US,
Group)
end,
AddedMembers),
- ok
+ case NonAddedMembers of
+ [] -> ok;
+ _ -> {error_jids, NonAddedMembers}
+ end
end;
_ -> nothing
end.
diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl
index 77d767a84..e67c6ca40 100644
--- a/test/ejabberd_SUITE.erl
+++ b/test/ejabberd_SUITE.erl
@@ -352,6 +352,7 @@ no_db_tests() ->
auth_external_wrong_jid,
auth_external_wrong_server,
auth_external_invalid_cert,
+ jidprep_tests:single_cases(),
sm_tests:single_cases(),
sm_tests:master_slave_cases(),
muc_tests:single_cases(),
diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml
index 93c540b83..2bf090d5c 100644
--- a/test/ejabberd_SUITE_data/ejabberd.yml
+++ b/test/ejabberd_SUITE_data/ejabberd.yml
@@ -106,6 +106,7 @@ modules:
vcard: VCARD
mod_muc_admin: []
mod_carboncopy: []
+ mod_jidprep: []
mod_mam: []
mod_last: []
mod_register:
diff --git a/test/jidprep_tests.erl b/test/jidprep_tests.erl
new file mode 100644
index 000000000..046f17b3c
--- /dev/null
+++ b/test/jidprep_tests.erl
@@ -0,0 +1,62 @@
+%%%-------------------------------------------------------------------
+%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
+%%% Created : 11 Sep 2019 by Holger Weiss <holger@zedat.fu-berlin.de>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2019 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(jidprep_tests).
+
+%% API
+-compile(export_all).
+-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2,
+ server_jid/1]).
+
+-include("suite.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+%%%===================================================================
+%%% Single user tests
+%%%===================================================================
+single_cases() ->
+ {jidprep_single, [sequence],
+ [single_test(feature_enabled),
+ single_test(normalize_jid)]}.
+
+feature_enabled(Config) ->
+ true = is_feature_advertised(Config, ?NS_JIDPREP_0),
+ disconnect(Config).
+
+normalize_jid(Config) ->
+ ServerJID = server_jid(Config),
+ OrigJID = jid:decode(<<"Romeo@Example.COM/Orchard">>),
+ NormJID = jid:decode(<<"romeo@example.com/Orchard">>),
+ Request = #jidprep{jid = OrigJID},
+ #iq{type = result, sub_els = [#jidprep{jid = NormJID}]} =
+ send_recv(Config, #iq{type = get, to = ServerJID,
+ sub_els = [Request]}),
+ disconnect(Config).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+single_test(T) ->
+ list_to_atom("jidprep_" ++ atom_to_list(T)).