diff options
-rw-r--r-- | .github/ISSUE_TEMPLATE/bug_report.md | 27 | ||||
-rw-r--r-- | .github/ISSUE_TEMPLATE/feature_request.md | 4 | ||||
-rw-r--r-- | rebar.config | 2 | ||||
-rw-r--r-- | src/ejabberd_auth_jwt.erl | 5 | ||||
-rw-r--r-- | src/ejabberd_mnesia.erl | 8 | ||||
-rw-r--r-- | src/ejabberd_oauth.erl | 22 | ||||
-rw-r--r-- | src/ejabberd_s2s.erl | 16 | ||||
-rw-r--r-- | src/ejabberd_s2s_in.erl | 4 | ||||
-rw-r--r-- | src/ejabberd_s2s_out.erl | 4 | ||||
-rw-r--r-- | src/ejabberd_sql.erl | 26 | ||||
-rw-r--r-- | src/ejabberd_web_admin.erl | 34 | ||||
-rw-r--r-- | src/mod_bosh_mnesia.erl | 145 | ||||
-rw-r--r-- | src/mod_jidprep.erl | 148 | ||||
-rw-r--r-- | src/mod_muc_admin.erl | 10 | ||||
-rw-r--r-- | src/mod_muc_room.erl | 3 | ||||
-rw-r--r-- | src/mod_offline.erl | 5 | ||||
-rw-r--r-- | src/mod_ping.erl | 2 | ||||
-rw-r--r-- | src/mod_roster.erl | 4 | ||||
-rw-r--r-- | src/mod_shared_roster.erl | 29 | ||||
-rw-r--r-- | test/ejabberd_SUITE.erl | 1 | ||||
-rw-r--r-- | test/ejabberd_SUITE_data/ejabberd.yml | 1 | ||||
-rw-r--r-- | test/jidprep_tests.erl | 62 |
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)). |