From 2e28d06744ae0bd2bc5807becc17931520e15b2e Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Tue, 28 Jun 2016 17:02:41 -0400 Subject: [PATCH 001/179] Harden the systemd unit Restrict capabilities, have a private tmp directory, private /dev, and don't accessing file system locations that really shouldn't be accessed. --- ejabberd.service.template | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ejabberd.service.template b/ejabberd.service.template index 80b15adbd..49ba14737 100644 --- a/ejabberd.service.template +++ b/ejabberd.service.template @@ -12,6 +12,13 @@ ExecStop=@ctlscriptpath@/ejabberdctl stop ExecReload=@ctlscriptpath@/ejabberdctl reload_config Type=oneshot RemainAfterExit=yes +# The CAP_DAC_OVERRIDE capability is required for pam authentication to work +CapabilityBoundingSet=CAP_DAC_OVERRIDE +PrivateTmp=true +PrivateDevices=true +ProtectHome=true +ProtectSystem=full +NoNewPrivileges=true [Install] WantedBy=multi-user.target From b31c0d9e2eb0bb07241929e1766923d50e563dac Mon Sep 17 00:00:00 2001 From: gabrielgatu Date: Tue, 5 Jul 2016 12:36:49 +0200 Subject: [PATCH 002/179] Support elixir module installer --- src/ext_mod.erl | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/ext_mod.erl b/src/ext_mod.erl index 332d2c5e2..842bb09fc 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -484,17 +484,28 @@ compile_deps(_Module, _Spec, DestDir) -> filelib:ensure_dir(filename:join(Ebin, ".")), Result = lists:foldl(fun(Dep, Acc) -> Inc = filename:join(Dep, "include"), + Lib = filename:join(Dep, "lib"), Src = filename:join(Dep, "src"), Options = [{outdir, Ebin}, {i, Inc}], [file:copy(App, Ebin) || App <- filelib:wildcard(Src++"/*.app")], - Acc++[case compile:file(File, Options) of + + %% Compile erlang files + Acc1 = Acc ++ [case compile:file(File, Options) of {ok, _} -> ok; {ok, _, _} -> ok; {ok, _, _, _} -> ok; error -> {error, {compilation_failed, File}}; Error -> Error end - || File <- filelib:wildcard(Src++"/*.erl")] + || File <- filelib:wildcard(Src++"/*.erl")], + + %% Compile elixir files + Acc1 ++ [case compile_elixir_file(Ebin, File) of + {ok, _} -> ok; + {error, File} -> {error, {compilation_failed, File}} + end + || File <- filelib:wildcard(Lib ++ "/*.ex")] + end, [], filelib:wildcard("deps/*")), case lists:dropwhile( fun(ok) -> true; @@ -515,6 +526,8 @@ compile(_Module, _Spec, DestDir) -> verbose, report_errors, report_warnings] ++ ExtLib, [file:copy(App, Ebin) || App <- filelib:wildcard("src/*.app")], + + %% Compile erlang files Result = [case compile:file(File, Options) of {ok, _} -> ok; {ok, _, _} -> ok; @@ -523,14 +536,32 @@ compile(_Module, _Spec, DestDir) -> Error -> Error end || File <- filelib:wildcard("src/*.erl")], + + %% Compile elixir files + Result1 = Result ++ [case compile_elixir_file(Ebin, File) of + {ok, _} -> ok; + {error, File} -> {error, {compilation_failed, File}} + end + || File <- filelib:wildcard("lib/*.ex")], + case lists:dropwhile( fun(ok) -> true; (_) -> false - end, Result) of + end, Result1) of [] -> ok; [Error|_] -> Error end. +compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) -> + compile_elixir_file(list_to_binary(Dest), list_to_binary(File)); + +compile_elixir_file(Dest, File) -> + try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of + [Module] -> {ok, Module} + catch + _ -> {error, File} + end. + install(Module, Spec, DestDir) -> Errors = lists:dropwhile(fun({_, {ok, _}}) -> true; (_) -> false From 8bc3dc9c495ddfea5b2a8c20c2b0ab1af2c09de5 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 18 Jul 2016 22:31:08 +0200 Subject: [PATCH 003/179] jlib: Don't try to keep just one tag It seems unclear whether XEP-0203 really mandates that stanzas may not have multiple tags. Editing/removing existing tags doesn't seem worth the effort, especially as we'd have to take more care which tag to keep if the stanza already has more than one. --- src/jlib.erl | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/jlib.erl b/src/jlib.erl index 532a74610..4bc9b0055 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -577,33 +577,8 @@ add_delay_info(El, From, Time) -> binary()) -> xmlel(). add_delay_info(El, From, Time, Desc) -> - case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of - false -> - %% Add new tag - DelayTag = create_delay_tag(Time, From, Desc), - fxml:append_subtags(El, [DelayTag]); - DelayTag -> - %% Update existing tag - NewDelayTag = - case {fxml:get_tag_cdata(DelayTag), Desc} of - {<<"">>, <<"">>} -> - DelayTag; - {OldDesc, <<"">>} -> - DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}; - {<<"">>, NewDesc} -> - DelayTag#xmlel{children = [{xmlcdata, NewDesc}]}; - {OldDesc, NewDesc} -> - case binary:match(OldDesc, NewDesc) of - nomatch -> - FinalDesc = <>, - DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]}; - _ -> - DelayTag#xmlel{children = [{xmlcdata, OldDesc}]} - end - end, - NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}), - fxml:append_subtags(NewEl, [NewDelayTag]) - end. + DelayTag = create_delay_tag(Time, From, Desc), + fxml:append_subtags(El, [DelayTag]). -spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary()) -> xmlel() | error. From fca2f24231928593234630f255753c3b98341c54 Mon Sep 17 00:00:00 2001 From: xmppjingle Date: Mon, 18 Jul 2016 17:55:10 -0300 Subject: [PATCH 004/179] External Component Connection Hooks --- src/ejabberd_service.erl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 465fb587a..360475565 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -224,8 +224,10 @@ wait_for_handshake({xmlstreamelement, El}, StateData) -> fun (H) -> ejabberd_router:register_route(H, ?MYNAME), ?INFO_MSG("Route registered for service ~p~n", - [H]) - end, dict:fetch_keys(StateData#state.host_opts)), + [H]), + ejabberd_hooks:run(component_connected, + [H]) + end, dict:fetch_keys(StateData#state.host_opts)), {next_state, stream_established, StateData}; _ -> send_text(StateData, ?INVALID_HANDSHAKE_ERR), @@ -288,13 +290,19 @@ stream_established({xmlstreamelement, El}, StateData) -> end, {next_state, stream_established, StateData}; stream_established({xmlstreamend, _Name}, StateData) -> + ejabberd_hooks:run(component_disconnected, + [StateData#state.host]), {stop, normal, StateData}; stream_established({xmlstreamerror, _}, StateData) -> + ejabberd_hooks:run(component_disconnected, + [StateData#state.host]), send_text(StateData, <<(?INVALID_XML_ERR)/binary, (?STREAM_TRAILER)/binary>>), {stop, normal, StateData}; stream_established(closed, StateData) -> + ejabberd_hooks:run(component_disconnected, + [StateData#state.host]), {stop, normal, StateData}. %%---------------------------------------------------------------------- From 673a654c4715ca26fc97f8d1335cceaebdc61df1 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Mon, 18 Jul 2016 20:25:23 -0300 Subject: [PATCH 005/179] Fix ce0d1704c6cc167c8bc891587952f78c55f979ad Original request was to allow ejabberd sysadmin to generate tokens for specific users. JIDs must not be passed as argument when requesting the tokens. --- src/ejabberd_oauth.erl | 74 ++++++++++++++++++++++-------------------- src/mod_http_api.erl | 2 -- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 57c1baab2..246bac127 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -48,7 +48,7 @@ process/2, opt_type/1]). --export([oauth_issue_token/1, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]). +-export([oauth_issue_token/2, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]). -include("jlib.hrl"). @@ -67,7 +67,7 @@ %% (as it has access to ejabberd command line). -record(oauth_token, { token = {<<"">>, <<"">>} :: {binary(), binary()}, - us = {<<"">>, <<"">>} :: {binary(), binary()} | server_admin, + us = {<<"">>, <<"">>} :: {binary(), binary()}, scope = [] :: [binary()], expire :: integer() }). @@ -92,18 +92,18 @@ get_commands_spec() -> #ejabberd_commands{name = oauth_issue_token, tags = [oauth], desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins", module = ?MODULE, function = oauth_issue_token, - args = [{scopes, string}], + args = [{jid, string},{scopes, string}], policy = restricted, - args_example = ["connected_users_number;muc_online_rooms"], + args_example = ["user@server.com", "connected_users_number;muc_online_rooms"], args_desc = ["List of scopes to allow, separated by ';'"], result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}} }, #ejabberd_commands{name = oauth_list_tokens, tags = [oauth], - desc = "List oauth tokens, their scope, and how many seconds remain until expirity", + desc = "List oauth tokens, their user and scope, and how many seconds remain until expirity", module = ?MODULE, function = oauth_list_tokens, args = [], policy = restricted, - result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}} + result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} }, #ejabberd_commands{name = oauth_list_scopes, tags = [oauth], desc = "List scopes that can be granted to tokens generated through the command line", @@ -122,25 +122,30 @@ get_commands_spec() -> } ]. -oauth_issue_token(ScopesString) -> +oauth_issue_token(Jid, ScopesString) -> Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")], - case oauth2:authorize_client_credentials(ejabberd_ctl, Scopes, none) of - {ok, {_AppCtx, Authorization}} -> - {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, none), - {ok, AccessToken} = oauth2_response:access_token(Response), - {ok, Expires} = oauth2_response:expires_in(Response), - {ok, VerifiedScope} = oauth2_response:scope(Response), - {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"}; - {error, Error} -> - {error, Error} + case jid:from_string(list_to_binary(Jid)) of + #jid{luser =Username, lserver = Server} -> + case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of + {ok, {Ctx,Authorization}} -> + {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, Ctx), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Expires} = oauth2_response:expires_in(Response), + {ok, VerifiedScope} = oauth2_response:scope(Response), + {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"}; + {error, Error} -> + {error, Error} + end; + error -> + {error, "Invalid JID: " ++ Jid} end. oauth_list_tokens() -> - Tokens = mnesia:dirty_match_object(#oauth_token{us = server_admin, _ = '_'}), + Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}), {MegaSecs, Secs, _MiniSecs} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - [{Token, Scope, integer_to_list(Expires - TS) ++ " seconds"} || - #oauth_token{token=Token, scope=Scope, expire=Expires} <- Tokens]. + [{Token, jid:to_string(jid:make(U,S,<<>>)), Scope, integer_to_list(Expires - TS) ++ " seconds"} || + #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens]. oauth_revoke_token(Token) -> @@ -203,7 +208,7 @@ get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}. -authenticate_user({User, Server}, {password, Password} = Ctx) -> +authenticate_user({User, Server}, Ctx) -> case jid:make(User, Server, <<"">>) of #jid{} = JID -> Access = @@ -213,11 +218,16 @@ authenticate_user({User, Server}, {password, Password} = Ctx) -> none), case acl:match_rule(JID#jid.lserver, Access, JID) of allow -> - case ejabberd_auth:check_password(User, <<"">>, Server, Password) of - true -> - {ok, {Ctx, {user, User, Server}}}; - false -> - {error, badpass} + case Ctx of + {password, Password} -> + case ejabberd_auth:check_password(User, <<"">>, Server, Password) of + true -> + {ok, {Ctx, {user, User, Server}}}; + false -> + {error, badpass} + end; + admin_generated -> + {ok, {Ctx, {user, User, Server}}} end; deny -> {error, badpass} @@ -272,17 +282,12 @@ associate_access_code(_AccessCode, _Context, AppContext) -> {ok, AppContext}. associate_access_token(AccessToken, Context, AppContext) -> - %% Tokens generated using the API/WEB belongs to users and always include the user, server pair. - %% Tokens generated form command line aren't tied to an user, and instead belongs to the ejabberd sysadmin - US = case proplists:get_value(<<"resource_owner">>, Context, <<"">>) of - {user, User, Server} -> {jid:nodeprep(User), jid:nodeprep(Server)}; - undefined -> server_admin - end, + {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), Scope = proplists:get_value(<<"scope">>, Context, []), Expire = proplists:get_value(<<"expiry_time">>, Context, 0), R = #oauth_token{ token = AccessToken, - us = US, + us = {jid:nodeprep(User), jid:nodeprep(Server)}, scope = Scope, expire = Expire }, @@ -320,10 +325,7 @@ check_token(Scope, Token) -> case oauth2_priv_set:is_member( Scope, oauth2_priv_set:new(TokenScope)) andalso Expire > TS of - true -> case US of - {LUser, LServer} -> {ok, user, {LUser, LServer}}; - server_admin -> {ok, server_admin} - end; + true -> {ok, user, US}; false -> false end; _ -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 815ed3ab6..1b4aa502b 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -162,8 +162,6 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _) case oauth_check_token(Call, Token) of {ok, user, {User, Server}} -> {ok, {User, Server, {oauth, Token}, Admin}}; - {ok, server_admin} -> %% token whas generated using issue_token command line - {ok, admin}; false -> false end; From 33e0283f0d4f851d2414a8b42434641ca432031d Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Mon, 18 Jul 2016 23:27:49 -0300 Subject: [PATCH 006/179] Add 'ejabberd:user' and 'ejabberd:admin' oauth scopes 'ejabberd:user' includes all commands defined with policy "user". 'ejabberd:admin' includes commands defined with policy "admin". --- include/ejabberd_commands.hrl | 2 ++ src/ejabberd_commands.erl | 20 ++++++++----- src/ejabberd_oauth.erl | 52 ++++++++++++++++++++------------- src/mod_http_api.erl | 20 ++++++------- test/mod_http_api_mock_test.exs | 27 ++++++++++++----- 5 files changed, 76 insertions(+), 45 deletions(-) diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 81be06dc3..2b4eca581 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -26,6 +26,8 @@ {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. +-type oauth_scope() :: atom(). + -record(ejabberd_commands, {name :: atom(), tags = [] :: [atom()] | '_' | '$2', diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 9d41f50c2..075ff35cf 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -218,7 +218,7 @@ get_command_format/1, get_command_format/2, get_command_format/3, - get_command_policy/1, + get_command_policy_and_scope/1, get_command_definition/1, get_command_definition/2, get_tags_commands/0, @@ -366,17 +366,23 @@ get_command_format(Name, Auth, Version) -> {Args, Result} end. --spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}. +-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}. %% @doc return command policy. -get_command_policy(Name) -> +get_command_policy_and_scope(Name) -> case get_command_definition(Name) of - #ejabberd_commands{policy = Policy} -> - {ok, Policy}; + #ejabberd_commands{policy = Policy} = Cmd -> + {ok, Policy, cmd_scope(Cmd)}; command_not_found -> {error, command_not_found} end. +%% The oauth scopes for a command are the command name itself, +%% also might include either 'ejabberd:user' or 'ejabberd:admin' +cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) -> + [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin]. + + -spec get_command_definition(atom()) -> ejabberd_commands(). %% @doc Get the definition record of a command. @@ -627,8 +633,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI check_auth(_Command, noauth) -> no_auth_provided; check_auth(Command, {User, Server, {oauth, Token}, _}) -> - Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8), - case ejabberd_oauth:check_token(User, Server, Scope, Token) of + ScopeList = cmd_scope(Command), + case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of true -> {ok, User, Server}; false -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 246bac127..0397571b2 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -90,7 +90,7 @@ start() -> get_commands_spec() -> [ #ejabberd_commands{name = oauth_issue_token, tags = [oauth], - desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins", + desc = "Issue an oauth token for the given jid", module = ?MODULE, function = oauth_issue_token, args = [{jid, string},{scopes, string}], policy = restricted, @@ -106,11 +106,11 @@ get_commands_spec() -> result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} }, #ejabberd_commands{name = oauth_list_scopes, tags = [oauth], - desc = "List scopes that can be granted to tokens generated through the command line", + desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow", module = ?MODULE, function = oauth_list_scopes, args = [], policy = restricted, - result = {scopes, {list, {scope, string}}} + result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}} }, #ejabberd_commands{name = oauth_revoke_token, tags = [oauth], desc = "Revoke authorization for a token", @@ -153,7 +153,7 @@ oauth_revoke_token(Token) -> oauth_list_tokens(). oauth_list_scopes() -> - get_cmd_scopes(). + [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())]. @@ -240,7 +240,7 @@ authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> Cmds = ejabberd_commands:get_commands(), - Cmds1 = [sasl_auth | Cmds], + Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds], RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1], case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), oauth2_priv_set:new(RegisteredScope)) of @@ -254,17 +254,27 @@ verify_resowner_scope(_, _, _) -> get_cmd_scopes() -> - Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of - {ok, Policy} when Policy =/= restricted -> true; - _ -> false - end end, - ejabberd_commands:get_commands()), - [atom_to_binary(C, utf8) || C <- Cmds]. + ScopeMap = lists:foldl(fun(Cmd, Accum) -> + case ejabberd_commands:get_command_policy_and_scope(Cmd) of + {ok, Policy, Scopes} when Policy =/= restricted -> + lists:foldl(fun(Scope, Accum2) -> + dict:append(Scope, Cmd, Accum2) + end, Accum, Scopes); + _ -> Accum + end end, dict:new(), ejabberd_commands:get_commands()), + ScopeMap. + + %Scps = lists:flatmap(fun(Cmd) -> case ejabberd_commands:get_command_policy_and_scope(Cmd) of + % {ok, Policy, Scopes} when Policy =/= restricted -> Scopes; + % _ -> [] + % end end, + % ejabberd_commands:get_commands()), + %lists:usort(Scps). %% 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) -> - RegisteredScope = get_cmd_scopes(), + RegisteredScope = dict:fetch_keys(get_cmd_scopes()), case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), oauth2_priv_set:new(RegisteredScope)) of true -> @@ -299,7 +309,7 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) -> {ok, AppContext}. -check_token(User, Server, Scope, Token) -> +check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), case catch mnesia:dirty_read(oauth_token, Token) of @@ -308,23 +318,25 @@ check_token(User, Server, Scope, Token) -> expire = Expire}] -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - oauth2_priv_set:is_member( - Scope, oauth2_priv_set:new(TokenScope)) andalso - Expire > TS; + TokenScopeSet = oauth2_priv_set:new(TokenScope), + lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList) andalso Expire > TS; _ -> false end. -check_token(Scope, Token) -> +check_token(ScopeList, Token) -> case catch mnesia:dirty_read(oauth_token, Token) of [#oauth_token{us = US, scope = TokenScope, expire = Expire}] -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - case oauth2_priv_set:is_member( - Scope, oauth2_priv_set:new(TokenScope)) andalso - Expire > TS of + TokenScopeSet = oauth2_priv_set:new(TokenScope), + case lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList) andalso Expire > TS of true -> {ok, user, US}; false -> false end; diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 1b4aa502b..f6621c09f 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -133,13 +133,13 @@ depends(_Host, _Opts) -> check_permissions(Request, Command) -> case catch binary_to_existing_atom(Command, utf8) of Call when is_atom(Call) -> - {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call), - check_permissions2(Request, Call, CommandPolicy); + {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), + check_permissions2(Request, Call, CommandPolicy, Scope); _ -> unauthorized_response() end. -check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _) +check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList) when HTTPAuth /= undefined -> Admin = case lists:keysearch(<<"X-Admin">>, 1, Headers) of @@ -159,7 +159,7 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _) false end; {oauth, Token, _} -> - case oauth_check_token(Call, Token) of + case oauth_check_token(ScopeList, Token) of {ok, user, {User, Server}} -> {ok, {User, Server, {oauth, Token}, Admin}}; false -> @@ -172,9 +172,9 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _) {ok, A} -> {allowed, Call, A}; _ -> unauthorized_response() end; -check_permissions2(_Request, Call, open) -> +check_permissions2(_Request, Call, open, _Scope) -> {allowed, Call, noauth}; -check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) -> +check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) -> Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, fun(V) -> V end, none), @@ -194,13 +194,11 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) -> _E -> {allowed, Call, noauth} end; -check_permissions2(_Request, _Call, _Policy) -> +check_permissions2(_Request, _Call, _Policy, _Scope) -> unauthorized_response(). -oauth_check_token(Scope, Token) when is_atom(Scope) -> - oauth_check_token(atom_to_binary(Scope, utf8), Token); -oauth_check_token(Scope, Token) -> - ejabberd_oauth:check_token(Scope, Token). +oauth_check_token(ScopeList, Token) when is_list(ScopeList) -> + ejabberd_oauth:check_token(ScopeList, Token). %% ------------------ %% command processing diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index 47b1fe94a..db8761887 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -70,8 +70,8 @@ defmodule ModHttpApiMockTest do fn (@acommand, {@user, @domain, @userpass, false}, @version) -> {[], {:res, :rescode}} end) - :meck.expect(:ejabberd_commands, :get_command_policy, - fn (@acommand) -> {:ok, :user} end) + :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, + fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end) :meck.expect(:ejabberd_commands, :get_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, @@ -123,8 +123,8 @@ defmodule ModHttpApiMockTest do fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> {[], {:res, :rescode}} end) - :meck.expect(:ejabberd_commands, :get_command_policy, - fn (@acommand) -> {:ok, :user} end) + :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, + fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) :meck.expect(:ejabberd_commands, :get_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, @@ -134,7 +134,7 @@ defmodule ModHttpApiMockTest do end) - # Correct OAuth call + # Correct OAuth call using specific scope token = EjabberdOauthMock.get_token @user, @domain, @command req = request(method: :GET, path: ["api", @command], @@ -147,6 +147,19 @@ defmodule ModHttpApiMockTest do assert 200 == elem(result, 0) # HTTP code assert "0" == elem(result, 2) # command result + # Correct OAuth call using specific ejabberd:user scope + token = EjabberdOauthMock.get_token @user, @domain, "ejabberd:user" + req = request(method: :GET, + path: ["api", @command], + q: [nokey: ""], + # OAuth + auth: {:oauth, token, []}, + ip: {{127,0,0,1},60000}, + host: @domain) + result = :mod_http_api.process([@command], req) + assert 200 == elem(result, 0) # HTTP code + assert "0" == elem(result, 2) # command result + # Wrong OAuth token req = request(method: :GET, path: ["api", @command], @@ -184,8 +197,8 @@ defmodule ModHttpApiMockTest do result = :mod_http_api.process([@command], req) assert 401 == elem(result, 0) # HTTP code - # Check that the command was executed only once - assert 1 == + # Check that the command was executed twice + assert 2 == :meck.num_calls(:ejabberd_commands, :execute_command, :_) assert :meck.validate :ejabberd_auth From 483ef09263264184fa08a0ed75cb33d90c33c46d Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Tue, 19 Jul 2016 00:51:04 -0300 Subject: [PATCH 007/179] Fix command argument formatting --- src/ejabberd_oauth.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 0397571b2..bf85d6ec2 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -117,7 +117,7 @@ get_commands_spec() -> module = ?MODULE, function = oauth_revoke_token, args = [{token, string}], policy = restricted, - result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}}, + result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}, result_desc = "List of remaining tokens" } ]. From 655cbf6055c00769108d5c810afbef14c2962b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Tue, 19 Jul 2016 11:26:33 +0200 Subject: [PATCH 008/179] Make access rules in ejabberd_web_admin configurable --- src/ejabberd_web_admin.erl | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 3281f6430..62f2eb7fa 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -74,14 +74,27 @@ get_acl_rule([<<"vhosts">>], _) -> %% The pages of a vhost are only accesible if the user is admin of that vhost: get_acl_rule([<<"server">>, VHost | _RPath], Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> - {VHost, [configure, webadmin_view]}; + AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin, + access, fun(A) -> A end, configure), + ACR = gen_mod:get_module_opt(VHost, ejabberd_web_admin, + access_readonly, fun(A) -> A end, webadmin_view), + {VHost, [AC, ACR]}; get_acl_rule([<<"server">>, VHost | _RPath], 'POST') -> - {VHost, [configure]}; + AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin, + access, fun(A) -> A end, configure), + {VHost, [AC]}; %% Default rule: only global admins can access any other random page get_acl_rule(_RPath, Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> - {global, [configure, webadmin_view]}; -get_acl_rule(_RPath, 'POST') -> {global, [configure]}. + AC = gen_mod:get_module_opt(global, ejabberd_web_admin, + access, fun(A) -> A end, configure), + ACR = gen_mod:get_module_opt(global, ejabberd_web_admin, + access_readonly, fun(A) -> A end, webadmin_view), + {global, [AC, ACR]}; +get_acl_rule(_RPath, 'POST') -> + AC = gen_mod:get_module_opt(global, ejabberd_web_admin, + access, fun(A) -> A end, configure), + {global, [AC]}. is_acl_match(Host, Rules, Jid) -> lists:any(fun (Rule) -> @@ -2965,7 +2978,8 @@ make_menu_item(item, 3, URI, Name, Lang) -> %%%================================== -opt_type(access) -> fun (V) -> V end; -opt_type(_) -> [access]. +opt_type(access) -> fun acl:access_rules_validator/1; +opt_type(access_readonly) -> fun acl:access_rules_validator/1; +opt_type(_) -> [access, access_readonly]. %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: From f79ac6874e731ac0eb16c40e62d93e6315db6501 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 19 Jul 2016 12:35:45 +0200 Subject: [PATCH 009/179] Lock relx version as newer version does not compile fine --- mix.exs | 5 ++++- mix.lock | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/mix.exs b/mix.exs index 0806e1210..03ce64da2 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,10 @@ defmodule Ejabberd.Mixfile do {:ezlib, "~> 1.0"}, {:iconv, "~> 1.0"}, {:eredis, "~> 1.0"}, - {:exrm, "~> 1.0.0-rc7", only: :dev}] + {:exrm, "~> 1.0.0", only: :dev}, + # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with + # version 3.20: + {:relx, "~> 3.19.0", only: :dev}] end defp package do diff --git a/mix.lock b/mix.lock index d576c518f..eda31f4a9 100644 --- a/mix.lock +++ b/mix.lock @@ -2,16 +2,16 @@ "cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, - "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, - "esip": {:hex, :esip, "1.0.6", "cb1ced88fae4c4a4888d9023c2c13b2239e14f8e360aee134c964b4a36dcc34d", [:rebar3], [{:stun, "1.0.5", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}, - "exrm": {:hex, :exrm, "1.0.6", "f708fc091dcacb93c1da58254a1ab34166d5ac3dca162877e878fe5d7a9e9dce", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, + "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, + "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:stun, "1.0.6", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}, + "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, - "fast_tls": {:hex, :fast_tls, "1.0.5", "8b970a91d4131fe5b9d47ffaccc2466944293c88dc5cc75a25548d73d57f7b77", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "fast_xml": {:hex, :fast_xml, "1.1.13", "85eca0a003598dbb0644320bd9bdc5fef30ad6285ab2aa80e2b5b82e65b79aa8", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "fast_yaml": {:hex, :fast_yaml, "1.0.4", "075ffb55f6ff3aa2f0461b8bfd1218e2f91e632c1675fc535963b9de7834800e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "fast_xml": {:hex, :fast_xml, "1.1.14", "23d4de66e645bca1d8a557444e83062cc42f235d7a7e2d4072d525bac3986f04", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "fast_yaml": {:hex, :fast_yaml, "1.0.5", "a67772c75abb84181c6c9899e1f988b08ac214ea0d764ff1f9889bb7e27f74d4", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, "goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []}, - "iconv": {:hex, :iconv, "1.0.0", "5ff1c54e5b3b9a8235de872632e9612c7952acdf89bc21db2f2efae0e72647be", [:rebar3], []}, + "iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, @@ -20,7 +20,7 @@ "p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []}, "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]}, + "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, - "stringprep": {:hex, :stringprep, "1.0.4", "f8f94d838ed202787699ff71d67b65481d350bda32b232ba1db52faca8eaed39", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "stun": {:hex, :stun, "1.0.5", "ec1d9928f25451d6fd2d2ade58c46b58b8d2c8326ddea3a667e926d04792f82c", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}} + "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}} From 490a758050fdc3f39cf2bfe1a770454f2fc45957 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 19 Jul 2016 13:05:01 +0200 Subject: [PATCH 010/179] Upgrade Elixir version to 1.2 in rebar config This matches the version used in mix.exs --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 9ef8bc0fc..7b65ead2c 100644 --- a/rebar.config +++ b/rebar.config @@ -39,7 +39,7 @@ "6e7fc924506e2dc166a6170e580ce1d95ebbd5bd"}}}, % for riak_pb-2.1.0.7 with correct meck dependency %% Elixir support, needed to run tests {if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", - {tag, "v1.1.1"}}}}, + {tag, "v1.2.6"}}}}, %% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin {if_var_true, elixir, {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}, From 9fcb81dea99dd7f97ed0c970ed44b23a09153be9 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 19 Jul 2016 21:08:13 +0200 Subject: [PATCH 011/179] mod_mam: Always limit result set page size Limit the number of messages returned for a given MAM request even if the client didn't specify an RSM set (not just if the client specified an RSM set without a limit). This is still not done for MAM v0.2 requests though, as that version of the XEP doesn't require clients to support RSM. --- src/mod_mam.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 10eb098da..f5dc67abc 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -1032,12 +1032,12 @@ filter_by_max(_Msgs, _Junk) -> limit_max(RSM, ?NS_MAM_TMP) -> RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. +limit_max(none, _NS) -> + #rsm_in{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_in{max = Max} = RSM, _NS) when not is_integer(Max) -> RSM#rsm_in{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_in{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> - RSM#rsm_in{max = ?MAX_PAGE_SIZE}; -limit_max(RSM, _NS) -> - RSM. + RSM#rsm_in{max = ?MAX_PAGE_SIZE}. match_interval(Now, Start, End) -> (Now >= Start) and (Now =< End). From 8f8c499cfa78075b8edb41ac3683ce47b54f5f22 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 19 Jul 2016 21:23:30 +0200 Subject: [PATCH 012/179] mod_mam: Fix handling of result set page limit Restore function clause for handling a client-specified result set page limit that doesn't exceed mod_mam's upper threshold. --- src/mod_mam.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index f5dc67abc..7e1460695 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -1037,7 +1037,9 @@ limit_max(none, _NS) -> limit_max(#rsm_in{max = Max} = RSM, _NS) when not is_integer(Max) -> RSM#rsm_in{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_in{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> - RSM#rsm_in{max = ?MAX_PAGE_SIZE}. + RSM#rsm_in{max = ?MAX_PAGE_SIZE}; +limit_max(RSM, _NS) -> + RSM. match_interval(Now, Start, End) -> (Now >= Start) and (Now =< End). From 1d317e8068b1691da76cec60497264efdd4dea45 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Tue, 19 Jul 2016 20:18:07 -0300 Subject: [PATCH 013/179] Let user choose the desired oauth token TTL --- src/ejabberd_oauth.erl | 85 ++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index bf85d6ec2..042099d63 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -39,7 +39,6 @@ authenticate_user/2, authenticate_client/2, verify_resowner_scope/3, - verify_client_scope/3, associate_access_code/3, associate_access_token/3, associate_refresh_token/3, @@ -48,7 +47,7 @@ process/2, opt_type/1]). --export([oauth_issue_token/2, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]). +-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]). -include("jlib.hrl"). @@ -92,7 +91,7 @@ get_commands_spec() -> #ejabberd_commands{name = oauth_issue_token, tags = [oauth], desc = "Issue an oauth token for the given jid", module = ?MODULE, function = oauth_issue_token, - args = [{jid, string},{scopes, string}], + args = [{jid, string},{ttl, integer}, {scopes, string}], policy = restricted, args_example = ["user@server.com", "connected_users_number;muc_online_rooms"], args_desc = ["List of scopes to allow, separated by ';'"], @@ -122,17 +121,16 @@ get_commands_spec() -> } ]. -oauth_issue_token(Jid, ScopesString) -> +oauth_issue_token(Jid, TTLSeconds, ScopesString) -> Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")], case jid:from_string(list_to_binary(Jid)) of #jid{luser =Username, lserver = Server} -> case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of - {ok, {Ctx,Authorization}} -> - {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, Ctx), + {ok, {_Ctx,Authorization}} -> + {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, seconds_since_epoch(TTLSeconds)}]), {ok, AccessToken} = oauth2_response:access_token(Response), - {ok, Expires} = oauth2_response:expires_in(Response), {ok, VerifiedScope} = oauth2_response:scope(Response), - {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"}; + {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"}; {error, Error} -> {error, Error} end; @@ -158,7 +156,6 @@ oauth_list_scopes() -> - start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -264,37 +261,44 @@ get_cmd_scopes() -> end end, dict:new(), ejabberd_commands:get_commands()), ScopeMap. - %Scps = lists:flatmap(fun(Cmd) -> case ejabberd_commands:get_command_policy_and_scope(Cmd) of - % {ok, Policy, Scopes} when Policy =/= restricted -> Scopes; - % _ -> [] - % end end, - % ejabberd_commands:get_commands()), - %lists:usort(Scps). - %% 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) -> - RegisteredScope = dict:fetch_keys(get_cmd_scopes()), - case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), - oauth2_priv_set:new(RegisteredScope)) of - true -> - {ok, {Ctx, Scope}}; - false -> - {error, badscope} - end. +%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) -> +% RegisteredScope = dict:fetch_keys(get_cmd_scopes()), +% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), +% oauth2_priv_set:new(RegisteredScope)) of +% true -> +% {ok, {Ctx, Scope}}; +% false -> +% {error, badscope} +% end. +-spec seconds_since_epoch(integer()) -> non_neg_integer(). +seconds_since_epoch(Diff) -> + {Mega, Secs, _} = os:timestamp(), + Mega * 1000000 + Secs + Diff. + associate_access_code(_AccessCode, _Context, AppContext) -> %put(?ACCESS_CODE_TABLE, AccessCode, Context), {ok, AppContext}. associate_access_token(AccessToken, Context, AppContext) -> + {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), + Expire = case proplists:get_value(expiry_time, AppContext, undefined) of + undefined -> + proplists:get_value(<<"expiry_time">>, Context, 0); + E -> + %% There is no clean way in oauth2 lib to actually override the TTL of the generated token. + %% It always pass the global configured value. Here we use the app context to pass the per-case + %% ttl if we want to override it. + E + end, {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), Scope = proplists:get_value(<<"scope">>, Context, []), - Expire = proplists:get_value(<<"expiry_time">>, Context, 0), R = #oauth_token{ token = AccessToken, us = {jid:nodeprep(User), jid:nodeprep(Server)}, @@ -308,7 +312,6 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) -> %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), {ok, AppContext}. - check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), @@ -386,6 +389,17 @@ process(_Handlers, ?INPUT(<<"hidden">>, <<"scope">>, Scope), ?INPUT(<<"hidden">>, <<"state">>, State), ?BR, + ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]), + ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], + [ + ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>}, + {<<"value">>, jlib:integer_to_binary(expire())}],<<"Default (", (integer_to_binary(expire()))/binary, " seconds)">>), + ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), + ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), + ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), + ?XAC(<<"option">>, [{<<"value">>, <<"31536000">>}],<<"1 Year">>), + ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), + ?BR, ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) ]), Top = @@ -434,6 +448,11 @@ process(_Handlers, Password = proplists:get_value(<<"password">>, Q, <<"">>), State = proplists:get_value(<<"state">>, Q, <<"">>), Scope = str:tokens(SScope, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> seconds_since_epoch(jlib:binary_to_integer(TTL)) + end, case oauth2:authorize_password({Username, Server}, ClientId, RedirectURI, @@ -441,10 +460,18 @@ process(_Handlers, {password, Password}) of {ok, {_AppContext, Authorization}} -> {ok, {_AppContext2, Response}} = - oauth2:issue_token(Authorization, none), + oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), {ok, AccessToken} = oauth2_response:access_token(Response), {ok, Type} = oauth2_response:token_type(Response), - {ok, Expires} = oauth2_response:expires_in(Response), + %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have + %%per-case expirity time. + Expires = case ExpiresIn of + undefined -> + {ok, Ex} = oauth2_response:expires_in(Response), + Ex; + _ -> + ExpiresIn + end, {ok, VerifiedScope} = oauth2_response:scope(Response), %oauth2_wrq:redirected_access_token_response(ReqData, % RedirectURI, From 0c0c6465ba2e6f3c4349dd516e59a4266ab24c37 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Tue, 19 Jul 2016 20:36:02 -0300 Subject: [PATCH 014/179] Fix test for changes in oauth expiry --- test/ejabberd_oauth_mock.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs index 81cfdc038..e6a34f65e 100644 --- a/test/ejabberd_oauth_mock.exs +++ b/test/ejabberd_oauth_mock.exs @@ -40,7 +40,7 @@ defmodule EjabberdOauthMock do {:user, user, domain}}, {"scope", [to_string command]}, {"expiry_time", expire}], - :undefined) + []) token end From 7748dd4e5d249016cdb4edf326c6d73f81f4a3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 20 Jul 2016 10:11:08 +0200 Subject: [PATCH 015/179] Make processing of if_* clauses in rebar.config recursive --- rebar.config.script | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rebar.config.script b/rebar.config.script index 166f1cbec..1e40dea49 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -19,7 +19,7 @@ ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) -> [{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg] end end, -ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end. +ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end, Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config")) of {ok, Terms} -> @@ -28,6 +28,13 @@ Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config")) [] end, +ProcessSingleVar = fun(F, Var, Tail) -> + case F(F, [Var], []) of + [] -> Tail; + [Val] -> [Val | Tail] + end + end, + ProcessVars = fun(_F, [], Acc) -> lists:reverse(Acc); (F, [{Type, Ver, Value} | Tail], Acc) when @@ -40,7 +47,7 @@ ProcessVars = fun(_F, [], Acc) -> SysVer < Ver end, if Include -> - F(F, Tail, [Value | Acc]); + F(F, Tail, ProcessSingleVar(F, Value, Acc)); true -> F(F, Tail, Acc) end; @@ -50,7 +57,7 @@ ProcessVars = fun(_F, [], Acc) -> Flag = Type == if_var_true, case proplists:get_bool(Var, Cfg) of V when V == Flag -> - F(F, Tail, [Value | Acc]); + F(F, Tail, ProcessSingleVar(F, Value, Acc)); _ -> F(F, Tail, Acc) end; @@ -59,7 +66,7 @@ ProcessVars = fun(_F, [], Acc) -> Type == if_var_no_match -> case proplists:get_value(Var, Cfg) of V when V == Match -> - F(F, Tail, [Value | Acc]); + F(F, Tail, ProcessSingleVar(F, Value, Acc)); _ -> F(F, Tail, Acc) end; From 5458d8bfcba74ff688a388db379ecb0d2e8842b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 20 Jul 2016 10:11:34 +0200 Subject: [PATCH 016/179] Add else branch to if_version_{above,below} --- rebar.config.script | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rebar.config.script b/rebar.config.script index 1e40dea49..ccafba7ec 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -51,6 +51,20 @@ ProcessVars = fun(_F, [], Acc) -> true -> F(F, Tail, Acc) end; + (F, [{Type, Ver, Value, ElseValue} | Tail], Acc) when + Type == if_version_above orelse + Type == if_version_below -> + SysVer = erlang:system_info(otp_release), + Include = if Type == if_version_above -> + SysVer > Ver; + true -> + SysVer < Ver + end, + if Include -> + F(F, Tail, ProcessSingleVar(F, Value, Acc)); + true -> + F(F, Tail, ProcessSingleVar(F, ElseValue, Acc)) + end; (F, [{Type, Var, Value} | Tail], Acc) when Type == if_var_true orelse Type == if_var_false -> @@ -153,7 +167,7 @@ Conf6 = case {lists:keyfind(cover_enabled, 1, Conf5), os:getenv("TRAVIS")} of Conf5 end, -%io:format("ejabberd configuration:~n ~p~n", [Conf5]), +%io:format("ejabberd configuration:~n ~p~n", [Conf6]), Conf6. From c2753cd51c8cae703126abcff71f70d8ac11d6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 20 Jul 2016 10:12:00 +0200 Subject: [PATCH 017/179] Use different version of elixir depending on erlang version --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 7b65ead2c..693f4f58b 100644 --- a/rebar.config +++ b/rebar.config @@ -39,7 +39,7 @@ "6e7fc924506e2dc166a6170e580ce1d95ebbd5bd"}}}, % for riak_pb-2.1.0.7 with correct meck dependency %% Elixir support, needed to run tests {if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", - {tag, "v1.2.6"}}}}, + {tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}}, %% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin {if_var_true, elixir, {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}, From cbfab687e870cefe6875aa8ba94d5776fd4798b8 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Wed, 20 Jul 2016 14:47:11 -0300 Subject: [PATCH 018/179] Oauth callback must pass expires_in as ttl instead of epoch --- src/ejabberd_oauth.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 042099d63..2d3cb03f3 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -127,7 +127,7 @@ oauth_issue_token(Jid, TTLSeconds, ScopesString) -> #jid{luser =Username, lserver = Server} -> case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of {ok, {_Ctx,Authorization}} -> - {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, seconds_since_epoch(TTLSeconds)}]), + {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]), {ok, AccessToken} = oauth2_response:access_token(Response), {ok, VerifiedScope} = oauth2_response:scope(Response), {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"}; @@ -291,11 +291,11 @@ associate_access_token(AccessToken, Context, AppContext) -> Expire = case proplists:get_value(expiry_time, AppContext, undefined) of undefined -> proplists:get_value(<<"expiry_time">>, Context, 0); - E -> + ExpiresIn -> %% There is no clean way in oauth2 lib to actually override the TTL of the generated token. %% It always pass the global configured value. Here we use the app context to pass the per-case %% ttl if we want to override it. - E + seconds_since_epoch(ExpiresIn) end, {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), Scope = proplists:get_value(<<"scope">>, Context, []), @@ -451,7 +451,7 @@ process(_Handlers, TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), ExpiresIn = case TTL of <<>> -> undefined; - _ -> seconds_since_epoch(jlib:binary_to_integer(TTL)) + _ -> jlib:binary_to_integer(TTL) end, case oauth2:authorize_password({Username, Server}, ClientId, From 6983dfa21f2117ebb3d6c2921297708211fc2439 Mon Sep 17 00:00:00 2001 From: xmppjingle Date: Thu, 21 Jul 2016 14:03:01 -0300 Subject: [PATCH 019/179] External Component Hook Changed Hook Trigger Event and included a Reason upon component_disconnected/2 Hook --- src/ejabberd_service.erl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 360475565..9d72b17b4 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -290,19 +290,13 @@ stream_established({xmlstreamelement, El}, StateData) -> end, {next_state, stream_established, StateData}; stream_established({xmlstreamend, _Name}, StateData) -> - ejabberd_hooks:run(component_disconnected, - [StateData#state.host]), {stop, normal, StateData}; stream_established({xmlstreamerror, _}, StateData) -> - ejabberd_hooks:run(component_disconnected, - [StateData#state.host]), send_text(StateData, <<(?INVALID_XML_ERR)/binary, (?STREAM_TRAILER)/binary>>), {stop, normal, StateData}; stream_established(closed, StateData) -> - ejabberd_hooks:run(component_disconnected, - [StateData#state.host]), {stop, normal, StateData}. %%---------------------------------------------------------------------- @@ -390,7 +384,9 @@ terminate(Reason, StateName, StateData) -> case StateName of stream_established -> lists:foreach(fun (H) -> - ejabberd_router:unregister_route(H) + ejabberd_router:unregister_route(H), + ejabberd_hooks:run(component_disconnected, + [StateData#state.host, Reason]) end, dict:fetch_keys(StateData#state.host_opts)); _ -> ok From bdfef09c0f5a0fcff9a593742ad830c38e0c45db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Fri, 15 Jul 2016 16:42:13 +0200 Subject: [PATCH 020/179] Fix handling of complex values as arguments in http_api --- src/mod_http_api.erl | 45 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index f6621c09f..b8aed94c2 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -366,28 +366,33 @@ format_args(Args, ArgsFormat) -> L when is_list(L) -> exit({additional_unused_args, L}) end. -format_arg({array, Elements}, - {list, {ElementDefName, ElementDefFormat}}) +format_arg(Elements, + {list, {_ElementDefName, ElementDefFormat}}) when is_list(Elements) -> - lists:map(fun ({struct, [{ElementName, ElementValue}]}) when - ElementDefName == ElementName -> - format_arg(ElementValue, ElementDefFormat) - end, - Elements); -format_arg({array, [{struct, Elements}]}, - {list, {ElementDefName, ElementDefFormat}}) - when is_list(Elements) -> - lists:map(fun ({ElementName, ElementValue}) -> - true = ElementDefName == ElementName, - format_arg(ElementValue, ElementDefFormat) - end, - Elements); -format_arg({array, [{struct, Elements}]}, + [format_arg(Element, ElementDefFormat) + || Element <- Elements]; +format_arg({[{Name, Value}]}, + {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}) + when Tuple1S == binary; + Tuple1S == string -> + {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)}; +format_arg({Elements}, {tuple, ElementsDef}) when is_list(Elements) -> - FormattedList = format_args(Elements, ElementsDef), - list_to_tuple(FormattedList); -format_arg({array, Elements}, {list, ElementsDef}) + F = lists:map(fun({TElName, TElDef}) -> + case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of + {_, Value} -> + format_arg(Value, TElDef); + _ when TElDef == binary; TElDef == string -> + <<"">>; + _ -> + ?ERROR_MSG("missing field ~p in tuple ~p", [TElName, Elements]), + throw({invalid_parameter, + io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])}) + end + end, ElementsDef), + list_to_tuple(F); +format_arg(Elements, {list, ElementsDef}) when is_list(Elements) and is_atom(ElementsDef) -> [format_arg(Element, ElementsDef) || Element <- Elements]; @@ -401,7 +406,7 @@ format_arg(undefined, string) -> <<>>; format_arg(Arg, Format) -> ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), throw({invalid_parameter, - io_lib:format("Arg ~p is not in format ~p", + io_lib:format("Arg ~w is not in format ~w", [Arg, Format])}). process_unicode_codepoints(Str) -> From ffbe97d9881e075ba6fd5f5862200a6e6c06bd08 Mon Sep 17 00:00:00 2001 From: Jerome Sautret Date: Fri, 22 Jul 2016 16:33:40 +0200 Subject: [PATCH 021/179] Quote postgresql database name (#1136) --- src/ejabberd_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index a480a1bd3..b19f16414 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -790,7 +790,7 @@ pgsql_connect(Server, Port, DB, Username, Password) -> {port, Port}, {as_binary, true}]) of {ok, Ref} -> - pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>, + pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>, <<"standard_conforming_strings='off';">>]), pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]), {ok, Ref}; From c5d9d35e7bf39d2446101edb3da881a543e55037 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 22 Jul 2016 16:51:48 +0200 Subject: [PATCH 022/179] Convert password provided by web form to UTF8 before passing it (#375) --- src/ejabberd_http.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 6b53f46c6..a79f26305 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -763,7 +763,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) -> undefined; Pos -> {User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1), - {User, Pass} + PassUtf8 = unicode:characters_to_binary(binary_to_list(Pass), utf8), + {User, PassUtf8} end; parse_auth(<<"Bearer ", SToken/binary>>) -> Token = str:strip(SToken), From caf7b5430578df75a2cef8117d72555d767a5712 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Fri, 22 Jul 2016 15:37:48 -0300 Subject: [PATCH 023/179] oauth: single jid field instead of username/password fields --- src/ejabberd_oauth.erl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 2d3cb03f3..b2192a781 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -375,12 +375,9 @@ process(_Handlers, ?XAE(<<"form">>, [{<<"action">>, <<"authorization_token">>}, {<<"method">>, <<"post">>}], - [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]), + [?LABEL(<<"username">>, [?CT(<<"User (jid)">>), ?C(<<": ">>)]), ?INPUTID(<<"text">>, <<"username">>, <<"">>), ?BR, - ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]), - ?INPUTID(<<"text">>, <<"server">>, <<"">>), - ?BR, ?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]), ?INPUTID(<<"password">>, <<"password">>, <<"">>), ?INPUT(<<"hidden">>, <<"response_type">>, ResponseType), @@ -443,8 +440,8 @@ process(_Handlers, ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), SScope = proplists:get_value(<<"scope">>, Q, <<"">>), - Username = proplists:get_value(<<"username">>, Q, <<"">>), - Server = proplists:get_value(<<"server">>, Q, <<"">>), + StringJID = proplists:get_value(<<"username">>, Q, <<"">>), + #jid{user = Username, server = Server} = jid:from_string(StringJID), Password = proplists:get_value(<<"password">>, Q, <<"">>), State = proplists:get_value(<<"state">>, Q, <<"">>), Scope = str:tokens(SScope, <<" ">>), From 12b58b98705ff0e03d295ede33fae6019fc369e0 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Fri, 22 Jul 2016 16:25:54 -0300 Subject: [PATCH 024/179] Fix elixir test case: stringprep was required --- test/acl_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/acl_test.exs b/test/acl_test.exs index 551c74ae0..c5482727f 100644 --- a/test/acl_test.exs +++ b/test/acl_test.exs @@ -26,6 +26,7 @@ defmodule ACLTest do setup_all do :ok = :mnesia.start :ok = :jid.start + :ok = :stringprep.start :ok = :ejabberd_config.start(["domain1", "domain2"], []) :ok = :acl.start end From 57aeef74d5f572438d60a0c8e7a4f9c9933d3852 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Fri, 22 Jul 2016 19:15:56 -0300 Subject: [PATCH 025/179] stringprep might already be started Depending on the way the test us ran (full test suite or the elixir quicktest one) the stringprep might already be loaded. --- test/acl_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acl_test.exs b/test/acl_test.exs index c5482727f..4bd8e6989 100644 --- a/test/acl_test.exs +++ b/test/acl_test.exs @@ -26,7 +26,7 @@ defmodule ACLTest do setup_all do :ok = :mnesia.start :ok = :jid.start - :ok = :stringprep.start + :stringprep.start :ok = :ejabberd_config.start(["domain1", "domain2"], []) :ok = :acl.start end From 4332dddbc47ff60c8fc4454c4fe8bfa9f0d9f0b9 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Fri, 22 Jul 2016 19:17:12 -0300 Subject: [PATCH 026/179] Support oauth password grant type As in https://tools.ietf.org/html/rfc6749#section-4.3 --- src/ejabberd_oauth.erl | 55 ++++++++++++++++++++++++++++ test/mod_http_api_mock_test.exs | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index b2192a781..81b5f4156 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -497,10 +497,65 @@ process(_Handlers, }], ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])} end; +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + path = [_, <<"token">>]}) -> + case proplists:get_value(<<"grant_type">>, Q, <<"">>) of + <<"password">> -> + SScope = proplists:get_value(<<"scope">>, Q, <<"">>), + StringJID = proplists:get_value(<<"username">>, Q, <<"">>), + #jid{user = Username, server = Server} = jid:from_string(StringJID), + Password = proplists:get_value(<<"password">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> jlib:binary_to_integer(TTL) + end, + case oauth2:authorize_password({Username, Server}, + Scope, + {password, Password}) of + {ok, {_AppContext, Authorization}} -> + {ok, {_AppContext2, Response}} = + oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Type} = oauth2_response:token_type(Response), + %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have + %%per-case expirity time. + Expires = case ExpiresIn of + undefined -> + {ok, Ex} = oauth2_response:expires_in(Response), + Ex; + _ -> + ExpiresIn + end, + {ok, VerifiedScope} = oauth2_response:scope(Response), + json_response(200, {[ + {<<"access_token">>, AccessToken}, + {<<"token_type">>, Type}, + {<<"scope">>, str:join(VerifiedScope, <<" ">>)}, + {<<"expires_in">>, Expires}]}); + {error, Error} when is_atom(Error) -> + json_response(400, {[ + {<<"error">>, <<"invalid_grant">>}, + {<<"error_description">>, Error}]}) + end; + _OtherGrantType -> + json_response(400, {[ + {<<"error">>, <<"unsupported_grant_type">>}]}) + end; + process(_Handlers, _Request) -> ejabberd_web:error(not_found). +%% Headers as per RFC 6749 +json_response(Code, Body) -> + {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, + {<<"Cache-Control">>, <<"no-store">>}, + {<<"Pragma">>, <<"no-cache">>}], + jiffy:encode(Body)}. + web_head() -> diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index db8761887..fcfdfee13 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -58,6 +58,7 @@ defmodule ModHttpApiMockTest do setup do :meck.unload :meck.new :ejabberd_commands + :meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth EjabberdAuthMock.init :ok end @@ -206,5 +207,69 @@ defmodule ModHttpApiMockTest do #assert :ok = :meck.history(:ejabberd_commands) end + test "Request oauth token, resource owner password credentials" do + EjabberdAuthMock.create_user @user, @domain, @userpass + :application.set_env(:oauth2, :backend, :ejabberd_oauth) + :application.start(:oauth2) + + # Mock a simple command() -> :ok + :meck.expect(:ejabberd_commands, :get_command_format, + fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> + {[], {:res, :rescode}} + end) + :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, + fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) + :meck.expect(:ejabberd_commands, :get_commands, + fn () -> [@acommand] end) + :meck.expect(:ejabberd_commands, :execute_command, + fn (:undefined, {@user, @domain, {:oauth, _token}, false}, + @acommand, [], @version, _) -> + :ok + end) + + #Mock acl to allow oauth authorizations + :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end) + + + # Correct password + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"ttl", "4000"}, {"password", @userpass}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 200 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "bearer"} = List.keyfind(kv, "token_type", 0) + assert {_, @command} = List.keyfind(kv, "scope", 0) + assert {_, 4000} = List.keyfind(kv, "expires_in", 0) + {"access_token", _token} = List.keyfind(kv, "access_token", 0) + + #missing grant_type + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 400 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0) + + + # incorrect user/pass + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass<>"aa"}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 400 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0) + + assert :meck.validate :ejabberd_auth + assert :meck.validate :ejabberd_commands + end end From 814b80c644fa425990dc63875e1da841d698b0fa Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sat, 23 Jul 2016 01:08:05 +0200 Subject: [PATCH 027/179] Preserve PID for offline sessions Don't set the PID to 'undefined' when a session goes offline, as this looses the information which node created the session table entry. Fixes #1196. --- include/ejabberd_sm.hrl | 4 ++-- src/ejabberd_sm.erl | 42 +++++++++++++++++++++++------------------ src/mod_admin_extra.erl | 19 +++++++++++-------- src/mod_configure.erl | 36 +++++++++++++++++++++-------------- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl index 38298d66a..f86ab1c15 100644 --- a/include/ejabberd_sm.hrl +++ b/include/ejabberd_sm.hrl @@ -1,9 +1,9 @@ -ifndef(EJABBERD_SM_HRL). -define(EJABBERD_SM_HRL, true). --record(session, {sid, usr, us, priority, info}). +-record(session, {sid, usr, us, priority, info = []}). -record(session_counter, {vhost, count}). --type sid() :: {erlang:timestamp(), pid()} | {erlang:timestamp(), undefined}. +-type sid() :: {erlang:timestamp(), pid()}. -type ip() :: {inet:ip_address(), inet:port_number()} | undefined. -type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()} | {oor, boolean()} | {auth_module, atom()} diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 8d94bc6aa..16e0f9114 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -270,25 +270,28 @@ get_session_pid(User, Server, Resource) -> -spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok. -set_offline_info({Time, _Pid}, User, Server, Resource, Info) -> - SID = {Time, undefined}, +set_offline_info(SID, User, Server, Resource, Info) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - set_session(SID, LUser, LServer, LResource, undefined, Info). + set_session(SID, LUser, LServer, LResource, undefined, [offline | Info]). -spec get_offline_info(erlang:timestamp(), binary(), binary(), binary()) -> none | info(). get_offline_info(Time, User, Server, Resource) -> - SID = {Time, undefined}, LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of - [#session{sid = SID, info = Info}] -> - Info; + [#session{sid = {Time, _}, info = Info}] -> + case proplists:get_bool(offline, Info) of + true -> + Info; + false -> + none + end; _ -> none end. @@ -425,11 +428,12 @@ set_session(SID, User, Server, Resource, Priority, Info) -> -spec online([#session{}]) -> [#session{}]. online(Sessions) -> - lists:filter(fun(#session{sid = {_, undefined}}) -> - false; - (_) -> - true - end, Sessions). + lists:filter(fun is_online/1, Sessions). + +-spec is_online(#session{}) -> boolean(). + +is_online(#session{info = Info}) -> + not proplists:get_bool(offline, Info). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -678,15 +682,17 @@ check_for_sessions_to_replace(User, Server, Resource) -> check_max_sessions(LUser, LServer). check_existing_resources(LUser, LServer, LResource) -> - SIDs = get_resource_sessions(LUser, LServer, LResource), - if SIDs == [] -> ok; + Mod = get_sm_backend(LServer), + Ss = Mod:get_sessions(LUser, LServer, LResource), + {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss), + lists:foreach(fun(#session{sid = S}) -> + Mod:delete_session(LUser, LServer, LResource, S) + end, OfflineSs), + if OnlineSs == [] -> ok; true -> + SIDs = [SID || #session{sid = SID} <- OnlineSs], MaxSID = lists:max(SIDs), - lists:foreach(fun ({_, undefined} = S) -> - Mod = get_sm_backend(LServer), - Mod:delete_session(LUser, LServer, LResource, - S); - ({_, Pid} = S) when S /= MaxSID -> + lists:foreach(fun ({_, Pid} = S) when S /= MaxSID -> Pid ! replaced; (_) -> ok end, diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 8f6724281..2ad1cc28e 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -863,12 +863,15 @@ connected_users_vhost(Host) -> %% Code copied from ejabberd_sm.erl and customized dirty_get_sessions_list2() -> - mnesia:dirty_select( - session, - [{#session{usr = '$1', sid = {'$2', '$3'}, priority = '$4', info = '$5', - _ = '_'}, - [{is_pid, '$3'}], - [['$1', {{'$2', '$3'}}, '$4', '$5']]}]). + Ss = mnesia:dirty_select( + session, + [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4', + _ = '_'}, + [], + [['$1', '$2', '$3', '$4']]}]), + lists:filter(fun([_USR, _SID, _Priority, Info]) -> + not proplists:get_bool(offline, Info) + end, Ss). %% Make string more print-friendly stringize(String) -> @@ -903,8 +906,8 @@ user_sessions_info(User, Host) -> {'EXIT', _Reason} -> []; Ss -> - lists:filter(fun(#session{sid = {_, Pid}}) -> - is_pid(Pid) + lists:filter(fun(#session{info = Info}) -> + not proplists:get_bool(offline, Info) end, Ss) end, lists:map( diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 97c944842..d0e0166a4 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -1917,21 +1917,29 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>), case JID#jid.lresource of <<>> -> - SIDs = mnesia:dirty_select(session, - [{#session{sid = {'$1', '$2'}, - usr = {LUser, LServer, '_'}, - _ = '_'}, - [{is_pid, '$2'}], - [{{'$1', '$2'}}]}]), - [Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs]; + SIs = mnesia:dirty_select(session, + [{#session{usr = {LUser, LServer, '_'}, + sid = '$1', + info = '$2', + _ = '_'}, + [], [{{'$1', '$2'}}]}]), + Pids = [P || {{_, P}, Info} <- SIs, + not proplists:get_bool(offline, Info)], + lists:foreach(fun(Pid) -> + Pid ! {kick, kicked_by_admin, Xmlelement} + end, Pids); R -> - [{_, Pid}] = mnesia:dirty_select(session, - [{#session{sid = {'$1', '$2'}, - usr = {LUser, LServer, R}, - _ = '_'}, - [{is_pid, '$2'}], - [{{'$1', '$2'}}]}]), - Pid ! {kick, kicked_by_admin, Xmlelement} + [{{_, Pid}, Info}] = mnesia:dirty_select( + session, + [{#session{usr = {LUser, LServer, R}, + sid = '$1', + info = '$2', + _ = '_'}, + [], [{{'$1', '$2'}}]}]), + case proplists:get_bool(offline, Info) of + true -> ok; + false -> Pid ! {kick, kicked_by_admin, Xmlelement} + end end, {result, []}; set_form(From, Host, From d4d1941133e801562bd5ca73cc00d85b5f2be95f Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sat, 23 Jul 2016 01:23:24 +0200 Subject: [PATCH 028/179] XEP-0198: Log debug message when dropping stanza Log a debug message when an unacknowledged message is neither resent nor bounced because it's archived. --- src/ejabberd_c2s.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 9ddb8511d..74ce74d62 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2951,6 +2951,9 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData) [StateData, From, StateData#state.jid, El]) of true -> + ?DEBUG("Dropping archived message stanza from ~s", + [fxml:get_attr_s(<<"from">>, + El#xmlel.attrs)]), ok; false -> ReRoute(From, To, El, Time) From 2c70c572c83c331e93d46a4fdc83cdc4df5a5c55 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 20 Jul 2016 20:50:59 +0200 Subject: [PATCH 029/179] Clean-up of error codes and format json structure --- src/mod_http_api.erl | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index b8aed94c2..07a1574e9 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -220,8 +220,12 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = log(Call, Args, IPPort), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args, Version, IP), - json_response(Code, jiffy:encode(Result)); + case handle(Cmd, Auth, Args, Version, IP) of + {Code, Result} -> + json_response(Code, jiffy:encode(Result)); + {HTMLCode, JSONErrorCode, Message} -> + json_error(HTMLCode, JSONErrorCode, Message) + end; %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse @@ -278,6 +282,8 @@ get_api_version([]) -> %% command handlers %% ---------------- +%% TODO Check accept types of request before decided format of reply. + % generic ejabberd command handler handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> case ejabberd_commands:get_command_format(Call, Auth, Version) of @@ -309,8 +315,8 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> {401, jlib:atom_to_binary(Why)}; throw:{not_allowed, Msg} -> {401, iolist_to_binary(Msg)}; - throw:{error, account_unprivileged} -> - {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)}; + throw:{error, account_unprivileged} -> + {403, 31, <<"Command need to be run with admin priviledge.">>}; throw:{invalid_parameter, Msg} -> {400, iolist_to_binary(Msg)}; throw:{error, Why} when is_atom(Why) -> @@ -490,9 +496,7 @@ format_result(404, {_Name, _}) -> "not_found". unauthorized_response() -> - unauthorized_response(<<"401 Unauthorized">>). -unauthorized_response(Body) -> - json_response(401, jiffy:encode(Body)). + json_error(401, 10, <<"Oauth Token is invalid or expired.">>). badrequest_response() -> badrequest_response(<<"400 Bad Request">>). @@ -502,6 +506,15 @@ badrequest_response(Body) -> json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. +%% HTTPCode, JSONCode = integers +%% message is binary +json_error(HTTPCode, JSONCode, Message) -> + {HTTPCode, ?HEADER(?CT_JSON), + jiffy:encode({[{<<"status">>, <<"error">>}, + {<<"code">>, JSONCode}, + {<<"message">>, Message}]}) + }. + log(Call, Args, {Addr, Port}) -> AddrS = jlib:ip_to_list({Addr, Port}), ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]); From 1485b5621151c37948ebd6c68232c3ffd23984f1 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 23 Jul 2016 16:21:00 +0200 Subject: [PATCH 030/179] Move any access rules check to ACL module --- src/acl.erl | 11 ++++++++++- src/ejabberd_web_admin.erl | 14 ++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/acl.erl b/src/acl.erl index 31a7547dd..834b85d97 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -31,7 +31,7 @@ -export([add_access/3, clear/0]). -export([start/0, add/3, add_list/3, add_local/3, add_list_local/3, - load_from_config/0, match_rule/3, + load_from_config/0, match_rule/3, any_rules_allowed/3, transform_options/1, opt_type/1, acl_rule_matches/3, acl_rule_verify/1, access_matches/3, transform_access_rules_config/1, @@ -274,6 +274,15 @@ normalize_spec(Spec) -> end end. +-spec any_rules_allowed(global | binary(), access_name(), + jid() | ljid() | inet:ip_address()) -> boolean(). + +any_rules_allowed(Host, Access, Entity) -> + lists:any(fun (Rule) -> + allow == acl:match_rule(Host, Rule, Entity) + end, + Access). + -spec match_rule(global | binary(), access_name(), jid() | ljid() | inet:ip_address()) -> any(). diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 62f2eb7fa..6583fb445 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -96,12 +96,6 @@ get_acl_rule(_RPath, 'POST') -> access, fun(A) -> A end, configure), {global, [AC]}. -is_acl_match(Host, Rules, Jid) -> - lists:any(fun (Rule) -> - allow == acl:match_rule(Host, Rule, Jid) - end, - Rules). - %%%================================== %%%% Menu Items Access @@ -151,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) -> is_allowed_path(Path, JID); is_allowed_path(Path, JID) -> {HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'), - is_acl_match(HostOfRule, AccessRule, JID). + acl:any_rules_allowed(HostOfRule, AccessRule, JID). %% @spec(Path) -> URL %% where Path = [string()] @@ -279,8 +273,8 @@ get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> - case is_acl_match(HostOfRule, AccessRule, - jid:make(User, Server, <<"">>)) + case acl:any_rules_allowed(HostOfRule, AccessRule, + jid:make(User, Server, <<"">>)) of false -> {unauthorized, <<"unprivileged-account">>}; true -> {ok, {User, Server}} @@ -1346,7 +1340,7 @@ parse_access_rule(Text) -> list_vhosts(Lang, JID) -> Hosts = (?MYHOSTS), HostsAllowed = lists:filter(fun (Host) -> - is_acl_match(Host, + acl:any_rules_allowed(Host, [configure, webadmin_view], JID) end, From caf2c202108791a2e89061259ddfbe27c0244e3e Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 23 Jul 2016 17:56:15 +0200 Subject: [PATCH 031/179] Error when not authorized should be 403 --- test/mod_http_api_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index 99b8d9b28..b440f3eb8 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -54,20 +54,20 @@ defmodule ModHttpApiTest do end # This related to the commands config file option - test "Attempting to access a command that is not exposed as HTTP API returns 401" do + test "Attempting to access a command that is not exposed as HTTP API returns 403" do setup_mocks() :ejabberd_config.add_local_option(:commands, []) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {401, _, _} = :mod_http_api.process(["open_cmd"], request) + {403, _, _} = :mod_http_api.process(["open_cmd"], request) end test "Call to user, admin or restricted commands without authentication are rejected" do setup_mocks() :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {401, _, _} = :mod_http_api.process(["user_cmd"], request) - {401, _, _} = :mod_http_api.process(["admin_cmd"], request) - {401, _, _} = :mod_http_api.process(["restricted_cmd"], request) + {403, _, _} = :mod_http_api.process(["user_cmd"], request) + {403, _, _} = :mod_http_api.process(["admin_cmd"], request) + {403, _, _} = :mod_http_api.process(["restricted_cmd"], request) end @tag pending: true From 68555ff4666588bba68e372b66d5bdbef3849838 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 23 Jul 2016 17:57:44 +0200 Subject: [PATCH 032/179] Add support for checking access rules conformance for commands --- include/ejabberd_commands.hrl | 5 +++ src/ejabberd_admin.erl | 6 ++-- src/ejabberd_commands.erl | 62 +++++++++++++++++++++++------------ src/mod_http_api.erl | 1 + 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 2b4eca581..bafd93a4d 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -38,19 +38,24 @@ function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, + access_rules = [] :: [atom()], result = {res, rescode} :: rterm() | '_' | '$2', args_desc = none :: none | [string()] | '_', result_desc = none :: none | string() | '_', args_example = none :: none | [any()] | '_', result_example = none :: any()}). +%% TODO Fix me: Type is not up to date -type ejabberd_commands() :: #ejabberd_commands{name :: atom(), tags :: [atom()], desc :: string(), longdesc :: string(), + version :: integer(), module :: atom(), function :: atom(), args :: [aterm()], + policy :: open | restricted | admin | user, + access_rules :: [atom()], result :: rterm()}. %% @type ejabberd_commands() = #ejabberd_commands{ diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 87ac76875..68aaf9be6 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -129,6 +129,8 @@ get_commands_spec() -> #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", + policy = admin, + access_rules = [configure], module = ?MODULE, function = register, args = [{user, binary}, {host, binary}, {password, binary}], result = {res, restuple}}, @@ -166,7 +168,7 @@ get_commands_spec() -> #ejabberd_commands{name = list_cluster, tags = [cluster], desc = "List nodes that are part of the cluster handled by Node", module = ?MODULE, function = list_cluster, - args = [], + args = [], result = {nodes, {list, {node, atom}}}}, #ejabberd_commands{name = import_file, tags = [mnesia], @@ -220,7 +222,7 @@ get_commands_spec() -> desc = "Delete offline messages older than DAYS", module = ?MODULE, function = delete_old_messages, args = [{days, integer}], result = {res, rescode}}, - + #ejabberd_commands{name = export2sql, tags = [mnesia], desc = "Export virtual host information from Mnesia tables to SQL files", module = ejd2sql, function = export, diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 075ff35cf..0f8cd2e0a 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -494,7 +494,7 @@ execute_command(AccessCommands, Auth, Name, Arguments) -> %% %% @doc Execute a command in a given API version %% Can return the following exceptions: -%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). @@ -503,32 +503,45 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> true -> admin; false -> Auth1 end, + TokenJID = oauth_token_user(Auth1), Command = get_command_definition(Name, Version), AccessCommands = get_access_commands(AccessCommands1, Version), case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of - ok -> execute_command2(Auth, Command, Arguments) + ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) end. -execute_command2( - _Auth, #ejabberd_commands{policy = open} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - admin, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - noauth, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, Arguments); -execute_command2( - {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) -> - execute_command2(Command, [User, Server | Arguments]). -execute_command2(Command, Arguments) -> +execute_check_policy( + _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) -> + execute_check_access(JID, Command, Arguments); +execute_check_policy( + admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_check_access(JID, Command, Arguments); +execute_check_policy( + noauth, _JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_policy( + {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_check_access(JID, Command, [User, Server | Arguments]). + +execute_check_access(_FromJID, #ejabberd_commands{access_rules = []} = Command, Arguments) -> + do_execute_command(Command, Arguments); +execute_check_access(FromJID, #ejabberd_commands{access_rules = Rules} = Command, Arguments) -> + %% TODO Review: Do we have smarter / better way to check rule on other Host than global ? + case acl:any_rules_allowed(global, Rules, FromJID) of + true -> + do_execute_command(Command, Arguments); + false -> + {error, access_rules_unauthorized} + end. + +do_execute_command(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), @@ -754,6 +767,13 @@ get_commands(Version) -> end, AdminCmds ++ UserCmds, Opts), Cmds. +oauth_token_user(noauth) -> + undefined; +oauth_token_user(admin) -> + undefined; +oauth_token_user({User, Server, _, _}) -> + jid:make(User, Server, <<>>). + is_admin(_Name, admin, _Extra) -> true; is_admin(_Name, {_User, _Server, _, false}, _Extra) -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 07a1574e9..bc30ee090 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -136,6 +136,7 @@ check_permissions(Request, Command) -> {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), check_permissions2(Request, Call, CommandPolicy, Scope); _ -> + %% TODO Should this be a 404 or 400 instead of 401 ? unauthorized_response() end. From bfa61eaa466ca8407812b928c119ed1606ebdd1f Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 23 Jul 2016 18:57:57 +0200 Subject: [PATCH 033/179] Make default OAuth token TTL values more user friendly --- src/ejabberd_oauth.erl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 81b5f4156..a909fe893 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -71,7 +71,7 @@ expire :: integer() }). --define(EXPIRE, 3600). +-define(EXPIRE, 31536000). start() -> init_db(mnesia, ?MYNAME), @@ -389,12 +389,10 @@ process(_Handlers, ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]), ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], [ - ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>}, - {<<"value">>, jlib:integer_to_binary(expire())}],<<"Default (", (integer_to_binary(expire()))/binary, " seconds)">>), ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), - ?XAC(<<"option">>, [{<<"value">>, <<"31536000">>}],<<"1 Year">>), + ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>), ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), ?BR, ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) @@ -500,7 +498,7 @@ process(_Handlers, process(_Handlers, #request{method = 'POST', q = Q, lang = _Lang, path = [_, <<"token">>]}) -> - case proplists:get_value(<<"grant_type">>, Q, <<"">>) of + case proplists:get_value(<<"grant_type">>, Q, <<"">>) of <<"password">> -> SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), @@ -549,11 +547,11 @@ process(_Handlers, _Request) -> ejabberd_web:error(not_found). -%% Headers as per RFC 6749 +%% Headers as per RFC 6749 json_response(Code, Body) -> {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, - {<<"Cache-Control">>, <<"no-store">>}, - {<<"Pragma">>, <<"no-cache">>}], + {<<"Cache-Control">>, <<"no-store">>}, + {<<"Pragma">>, <<"no-cache">>}], jiffy:encode(Body)}. @@ -670,7 +668,7 @@ css() -> text-decoration: underline; } - .container > .section { + .container > .section { background: #424A55; } From b4a430541dd98807b9183d7ba65252cd1373feab Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 24 Jul 2016 14:10:12 +0200 Subject: [PATCH 034/179] Return more user friendly, human readable error description --- src/ejabberd_oauth.erl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index a909fe893..86444e51a 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -534,13 +534,10 @@ process(_Handlers, {<<"scope">>, str:join(VerifiedScope, <<" ">>)}, {<<"expires_in">>, Expires}]}); {error, Error} when is_atom(Error) -> - json_response(400, {[ - {<<"error">>, <<"invalid_grant">>}, - {<<"error_description">>, Error}]}) + json_error(400, <<"invalid_grant">>, Error) end; - _OtherGrantType -> - json_response(400, {[ - {<<"error">>, <<"unsupported_grant_type">>}]}) + _OtherGrantType -> + json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type) end; process(_Handlers, _Request) -> @@ -554,7 +551,17 @@ json_response(Code, Body) -> {<<"Pragma">>, <<"no-cache">>}], jiffy:encode(Body)}. +%% OAauth error are defined in: +%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2 +json_error(Code, Error, Reason) -> + Desc = json_error_desc(Reason), + Body = {[{<<"error">>, Error}, + {<<"error_description">>, Desc}]}, + json_response(Code, Body). +json_error_desc(access_denied) -> <<"Access denied">>; +json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>; +json_error_desc(invalid_scope) -> <<"Invalid scope">>. web_head() -> [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, From 4b0d71d402baffd44f6dc523218ba836abd8458f Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sun, 24 Jul 2016 20:55:11 +0200 Subject: [PATCH 035/179] Don't return error for blocked MUC PMs If a message stanza is blocked as per XEP-0016 or XEP-0191 and the stanza is marked as a private MUC message, don't return an error. This makes sure users won't be kicked from MUC rooms when blocking other participants. --- src/ejabberd_c2s.erl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 74ce74d62..0ffca7179 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1632,11 +1632,18 @@ handle_info({route, From, To, <<"groupchat">> -> ok; <<"headline">> -> ok; _ -> - Err = - jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, - Err) + case fxml:get_subtag_with_xmlns(Packet, + <<"x">>, + ?NS_MUC_USER) + of + false -> + Err = + jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, + Err); + _ -> ok + end end, {false, Attrs, StateData} end; From d7ad99f14763ed07f51872a2d6e2c9711bf442da Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 25 Jul 2016 11:43:49 +0200 Subject: [PATCH 036/179] Initial attempt on access on commands May change and will require more work / test / refactor --- include/ejabberd_commands.hrl | 22 +++- src/ejabberd_admin.erl | 2 +- src/ejabberd_commands.erl | 183 ++++++++++++++------------- src/mod_http_api.erl | 13 +- test/ejabberd_commands_mock_test.exs | 42 +++++- 5 files changed, 166 insertions(+), 96 deletions(-) diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index bafd93a4d..c5c34b743 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -28,6 +28,23 @@ -type oauth_scope() :: atom(). +%% ejabberd_commands OAuth ReST ACL definition: +%% Two fields exist that are used to control access on a command from ReST API: +%% 1. Policy +%% If policy is: +%% - restricted: command is not exposed as OAuth Rest API. +%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access +%% - user: Command might be called by any server user. +%% - open: Command can be called by anyone. +%% +%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as +%% defined by access option. +%% Access option can be a list of: +%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command. +%% - AccessRule name: direct name of the access rule to check in config file. +%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter +%% to command, so that the command can perform additional check. + -record(ejabberd_commands, {name :: atom(), tags = [] :: [atom()] | '_' | '$2', @@ -38,7 +55,8 @@ function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, - access_rules = [] :: [atom()], + %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}] + access = [] :: [{atom(),atom(),atom()}|atom()], result = {res, rescode} :: rterm() | '_' | '$2', args_desc = none :: none | [string()] | '_', result_desc = none :: none | string() | '_', @@ -55,7 +73,7 @@ function :: atom(), args :: [aterm()], policy :: open | restricted | admin | user, - access_rules :: [atom()], + access :: [{atom(),atom(),atom()}|atom()], result :: rterm()}. %% @type ejabberd_commands() = #ejabberd_commands{ diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 68aaf9be6..f20aeebf0 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -130,7 +130,7 @@ get_commands_spec() -> #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", policy = admin, - access_rules = [configure], + access = [{mod_register, access, configure}], module = ?MODULE, function = register, args = [{user, binary}, {host, binary}, {password, binary}], result = {res, restuple}}, diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 0f8cd2e0a..7bfabf661 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -319,8 +319,8 @@ list_commands() -> list_commands(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc} || #ejabberd_commands{name = Name, - args = Args, - desc = Desc} <- Commands]. + args = Args, + desc = Desc} <- Commands]. -spec list_commands_policy(integer()) -> @@ -331,10 +331,10 @@ list_commands(Version) -> list_commands_policy(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc, Policy} || - #ejabberd_commands{name = Name, - args = Args, - desc = Desc, - policy = Policy} <- Commands]. + #ejabberd_commands{name = Name, + args = Args, + desc = Desc, + policy = Policy} <- Commands]. -spec get_command_format(atom()) -> {[aterm()], rterm()}. @@ -356,14 +356,14 @@ get_command_format(Name, Auth, Version) -> Admin = is_admin(Name, Auth, #{}), #ejabberd_commands{args = Args, result = Result, - policy = Policy} = - get_command_definition(Name, Version), + policy = Policy} = + get_command_definition(Name, Version), case Policy of - user when Admin; - Auth == noauth -> - {[{user, binary}, {server, binary} | Args], Result}; - _ -> - {Args, Result} + user when Admin; + Auth == noauth -> + {[{user, binary}, {server, binary} | Args], Result}; + _ -> + {Args, Result} end. -spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}. @@ -394,16 +394,16 @@ get_command_definition(Name) -> %% @doc Get the definition record of a command in a given API version. get_command_definition(Name, Version) -> case lists:reverse( - lists:sort( - mnesia:dirty_select( - ejabberd_commands, - ets:fun2ms( - fun(#ejabberd_commands{name = N, version = V} = C) - when N == Name, V =< Version -> - {V, C} - end)))) of - [{_, Command} | _ ] -> Command; - _E -> throw(unknown_command) + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = N, version = V} = C) + when N == Name, V =< Version -> + {V, C} + end)))) of + [{_, Command} | _ ] -> Command; + _E -> throw(unknown_command) end. -spec get_commands_definition(integer()) -> [ejabberd_commands()]. @@ -411,20 +411,20 @@ get_command_definition(Name, Version) -> % @doc Returns all commands for a given API version get_commands_definition(Version) -> L = lists:reverse( - lists:sort( - mnesia:dirty_select( - ejabberd_commands, - ets:fun2ms( - fun(#ejabberd_commands{name = Name, version = V} = C) - when V =< Version -> - {Name, V, C} - end)))), + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = Name, version = V} = C) + when V =< Version -> + {Name, V, C} + end)))), F = fun({_Name, _V, Command}, []) -> - [Command]; - ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> - Acc; - ({_Name, _V, Command}, Acc) -> [Command | Acc] - end, + [Command]; + ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + Acc; + ({_Name, _V, Command}, Acc) -> [Command | Acc] + end, lists:foldl(F, [], L). %% @spec (Name::atom(), Arguments) -> ResultTerm @@ -433,7 +433,7 @@ get_commands_definition(Version) -> %% @doc Execute a command. %% Can return the following exceptions: %% command_unknown | account_unprivileged | invalid_account_data | -%% no_auth_provided +%% no_auth_provided | access_rules_unauthorized execute_command(Name, Arguments) -> execute_command(Name, Arguments, ?DEFAULT_VERSION). @@ -505,7 +505,7 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> end, TokenJID = oauth_token_user(Auth1), Command = get_command_definition(Name, Version), - AccessCommands = get_access_commands(AccessCommands1, Version), + AccessCommands = get_all_access_commands(AccessCommands1), case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) end. @@ -530,15 +530,22 @@ execute_check_policy( {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> execute_check_access(JID, Command, [User, Server | Arguments]). -execute_check_access(_FromJID, #ejabberd_commands{access_rules = []} = Command, Arguments) -> +execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) -> do_execute_command(Command, Arguments); -execute_check_access(FromJID, #ejabberd_commands{access_rules = Rules} = Command, Arguments) -> +execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) -> %% TODO Review: Do we have smarter / better way to check rule on other Host than global ? - case acl:any_rules_allowed(global, Rules, FromJID) of + Host = global, + Rules = lists:map(fun({Mod, AccessName, Default}) -> + gen_mod:get_module_opt(Host, Mod, + AccessName, fun(A) -> A end, Default); + (Default) -> + Default + end, AccessRefs), + case acl:any_rules_allowed(Host, Rules, FromJID) of true -> do_execute_command(Command, Arguments); false -> - {error, access_rules_unauthorized} + throw({error, access_rules_unauthorized}) end. do_execute_command(Command, Arguments) -> @@ -611,31 +618,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI Command1 end, AccessCommandsAllowed = - lists:filter( - fun({Access, Commands, ArgumentRestrictions}) -> - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end; - ({Access, Commands}) -> - ArgumentRestrictions = [], - case check_access(Command, Access, Auth, CallerInfo) of - true -> - check_access_command(Commands, Command, - ArgumentRestrictions, - Method, Arguments); - false -> - false - end - end, - AccessCommands), + lists:filter( + fun({Access, Commands, ArgumentRestrictions}) -> + case check_access(Command, Access, Auth, CallerInfo) of + true -> + check_access_command(Commands, Command, + ArgumentRestrictions, + Method, Arguments); + false -> + false + end; + ({Access, Commands}) -> + ArgumentRestrictions = [], + case check_access(Command, Access, Auth, CallerInfo) of + true -> + check_access_command(Commands, Command, + ArgumentRestrictions, + Method, Arguments); + false -> + false + end + end, + AccessCommands), case AccessCommandsAllowed of - [] -> throw({error, account_unprivileged}); - L when is_list(L) -> ok + [] -> throw({error, account_unprivileged}); + L when is_list(L) -> ok end. -spec check_auth(ejabberd_commands(), noauth) -> noauth_provided; @@ -699,9 +706,9 @@ check_access2(Access, AccessInfo, Server) -> check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, - Arguments); - false -> false + true -> check_access_arguments(Command, ArgumentRestrictions, + Arguments); + false -> false end. check_access_arguments(Command, ArgumentRestrictions, Arguments) -> @@ -724,6 +731,10 @@ tag_arguments(ArgsDefs, Args) -> Args). +%% Get commands for all version +get_all_access_commands(AccessCommands) -> + get_access_commands(AccessCommands, ?DEFAULT_VERSION). + get_access_commands(undefined, Version) -> Cmds = get_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; @@ -736,7 +747,7 @@ get_commands(Version) -> Opts0 = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, - []), + []), Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0), CommandsList = list_commands_policy(Version), OpenCmds = [N || {N, _, _, open} <- CommandsList], @@ -746,27 +757,29 @@ get_commands(Version) -> Cmds = lists:foldl( fun([{add_commands, L}], Acc) -> - Cmds = case L of - open -> OpenCmds; - restricted -> RestrictedCmds; - admin -> AdminCmds; - user -> UserCmds; - _ when is_list(L) -> L - end, + Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds), lists:usort(Cmds ++ Acc); ([{remove_commands, L}], Acc) -> - Cmds = case L of - open -> OpenCmds; - restricted -> RestrictedCmds; - admin -> AdminCmds; - user -> UserCmds; - _ when is_list(L) -> L - end, + Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds), Acc -- Cmds; (_, Acc) -> Acc - end, AdminCmds ++ UserCmds, Opts), + end, [], Opts), Cmds. +expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) -> + lists:foldl(fun(El, Acc) -> + expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) ++ Acc + end, [], L); +expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) -> + case El of + open -> OpenCmds; + restricted -> RestrictedCmds; + admin -> AdminCmds; + user -> UserCmds; + _ -> [El] + end. + + oauth_token_user(noauth) -> undefined; oauth_token_user(admin) -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index bc30ee090..ba3a14cf8 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -136,8 +136,7 @@ check_permissions(Request, Command) -> {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), check_permissions2(Request, Call, CommandPolicy, Scope); _ -> - %% TODO Should this be a 404 or 400 instead of 401 ? - unauthorized_response() + json_error(404, 40, <<"Endpoint not found.">>) end. check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList) @@ -269,10 +268,10 @@ get_api_version(#request{path = Path}) -> get_api_version(lists:reverse(Path)); get_api_version([<<"v", String/binary>> | Tail]) -> case catch jlib:binary_to_integer(String) of - N when is_integer(N) -> - N; - _ -> - get_api_version(Tail) + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail) end; get_api_version([_Head | Tail]) -> get_api_version(Tail); @@ -318,6 +317,8 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> {401, iolist_to_binary(Msg)}; throw:{error, account_unprivileged} -> {403, 31, <<"Command need to be run with admin priviledge.">>}; + throw:{error, access_rules_unauthorized} -> + {403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>}; throw:{invalid_parameter, Msg} -> {400, iolist_to_binary(Msg)}; throw:{error, Why} when is_atom(Why) -> diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 487cf6a4b..7c15b58b3 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -18,6 +18,8 @@ # # ---------------------------------------------------------------------- +## TODO Fix next test error: add admin user ACL + defmodule EjabberdCommandsMockTest do use ExUnit.Case, async: false @@ -44,6 +46,9 @@ defmodule EjabberdCommandsMockTest do _ -> :ok end :mnesia.start + :ok = :jid.start + :ok = :ejabberd_config.start(["domain1", "domain2"], []) + :ok = :acl.start EjabberdOauthMock.init :ok end @@ -313,7 +318,6 @@ defmodule EjabberdCommandsMockTest do end - test "API command with admin policy" do mock_commands_config @@ -393,6 +397,40 @@ defmodule EjabberdCommandsMockTest do assert :meck.validate @module end + test "Commands can perform extra check on access" do + mock_commands_config + + command_name = :test + function = :test_command + command = ejabberd_commands(name: command_name, + args: [{:user, :binary}, {:host, :binary}], + access: [:basic_rule_1], + module: @module, + function: function, + policy: :open) + :meck.expect(@module, function, + fn(user, domain) when is_binary(user) and is_binary(domain) -> + {user, domain} + end) + assert :ok == :ejabberd_commands.register_commands [command] + + :acl.add(:global, :basic_acl_1, {:user, @user}) + :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) + + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@user, @domain, + @userpass, false}, + command_name, + [@user, @domain]) + assert {@user, @domain} == + :ejabberd_commands.execute_command(:undefined, + {@admin, @domain, + @adminpass, false}, + command_name, + [@user, @domain]) + + end ########################################################## # Utils @@ -412,7 +450,7 @@ defmodule EjabberdCommandsMockTest do end) :meck.expect(:ejabberd_config, :get_myhosts, fn() -> [@domain] end) - :meck.new :acl + :meck.new :acl #, [:passthrough] :meck.expect(:acl, :access_matches, fn(:commands_admin_access, info, _scope) -> case info do From 5d4f8bcf0dd502068576eb87e1ceb2fd6997266d Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 25 Jul 2016 16:57:05 +0200 Subject: [PATCH 037/179] Export acl:parse_ip_netmask/1 for mod_rest (ejabberd-contrib#175) --- src/acl.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/acl.erl b/src/acl.erl index 31a7547dd..07f7e3c12 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -35,6 +35,7 @@ transform_options/1, opt_type/1, acl_rule_matches/3, acl_rule_verify/1, access_matches/3, transform_access_rules_config/1, + parse_ip_netmask/1, access_rules_validator/1, shaper_rules_validator/1]). -include("ejabberd.hrl"). From c183092aa4a382d6e3396c6df3f3c7ed56dfb453 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 25 Jul 2016 18:28:05 +0200 Subject: [PATCH 038/179] Simplify code for command policy group expansion --- src/ejabberd_commands.erl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 7bfabf661..9496fe09f 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -496,7 +496,7 @@ execute_command(AccessCommands, Auth, Name, Arguments) -> %% Can return the following exceptions: %% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> -execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). + execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> Auth = case is_admin(Name, Auth1, CallerInfo) of @@ -506,6 +506,7 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> TokenJID = oauth_token_user(Auth1), Command = get_command_definition(Name, Version), AccessCommands = get_all_access_commands(AccessCommands1), + case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) end. @@ -766,19 +767,15 @@ get_commands(Version) -> end, [], Opts), Cmds. +%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) -> - lists:foldl(fun(El, Acc) -> - expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) ++ Acc - end, [], L); -expand_commands(El, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) -> - case El of - open -> OpenCmds; - restricted -> RestrictedCmds; - admin -> AdminCmds; - user -> UserCmds; - _ -> [El] - end. - + lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc; + (user, Acc) -> UserCmds ++ Acc; + (admin, Acc) -> AdminCmds ++ Acc; + (restricted, Acc) -> RestrictedCmds ++ Acc; + (Command, Acc) when is_atom(Command) -> + [Command, Acc] + end, [], L). oauth_token_user(noauth) -> undefined; From dbc049827941f9d6f18fe3673141dabe41d118b4 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 25 Jul 2016 18:28:40 +0200 Subject: [PATCH 039/179] Fix tests, command need to be properly added to list of exposed commands --- test/ejabberd_commands_mock_test.exs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 7c15b58b3..72e7c2d3a 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -185,7 +185,7 @@ defmodule EjabberdCommandsMockTest do test "API command with user policy" do - mock_commands_config + mock_commands_config [:user, :admin] # Register a command test(user, domain) -> {:versionN, user, domain} # with policy=user and versions 1 & 3 @@ -319,7 +319,7 @@ defmodule EjabberdCommandsMockTest do test "API command with admin policy" do - mock_commands_config + mock_commands_config [:admin] # Register a command test(user, domain) -> {user, domain} # with policy=admin @@ -398,7 +398,7 @@ defmodule EjabberdCommandsMockTest do end test "Commands can perform extra check on access" do - mock_commands_config + mock_commands_config [:admin, :open] command_name = :test function = :test_command @@ -414,8 +414,8 @@ defmodule EjabberdCommandsMockTest do end) assert :ok == :ejabberd_commands.register_commands [command] - :acl.add(:global, :basic_acl_1, {:user, @user}) - :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) +# :acl.add(:global, :basic_acl_1, {:user, @user, @host}) +# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) assert {@user, @domain} == :ejabberd_commands.execute_command(:undefined, @@ -437,7 +437,7 @@ defmodule EjabberdCommandsMockTest do # Mock a config where only @admin user is allowed to call commands # as admin - def mock_commands_config do + def mock_commands_config(commands \\ []) do EjabberdAuthMock.init EjabberdAuthMock.create_user @user, @domain, @userpass EjabberdAuthMock.create_user @admin, @domain, @adminpass @@ -446,11 +446,13 @@ defmodule EjabberdCommandsMockTest do :meck.expect(:ejabberd_config, :get_option, fn(:commands_admin_access, _, _) -> :commands_admin_access (:oauth_access, _, _) -> :all + (:commands, _, _) -> [{:add_commands, commands}] (_, _, default) -> default end) :meck.expect(:ejabberd_config, :get_myhosts, fn() -> [@domain] end) - :meck.new :acl #, [:passthrough] + + :meck.new :acl :meck.expect(:acl, :access_matches, fn(:commands_admin_access, info, _scope) -> case info do From 839490b0d9a8cbdcf13f5eb08412fae3c11ffcf6 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Wed, 20 Jul 2016 16:55:45 +0300 Subject: [PATCH 040/179] Add DB backend support for ejabberd_oauth --- include/ejabberd_oauth.hrl | 26 ++++++++++++++ src/ejabberd_oauth.erl | 65 ++++++++++++++++------------------- src/ejabberd_oauth_mnesia.erl | 65 +++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 include/ejabberd_oauth.hrl create mode 100644 src/ejabberd_oauth_mnesia.erl diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl new file mode 100644 index 000000000..6b5a9bcc8 --- /dev/null +++ b/include/ejabberd_oauth.hrl @@ -0,0 +1,26 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2016 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. +%%% +%%%---------------------------------------------------------------------- + +-record(oauth_token, { + token = <<"">> :: binary() | '_', + us = {<<"">>, <<"">>} :: {binary(), binary()} | '_', + scope = [] :: [binary()] | '_', + expire :: integer() | '$1' + }). diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 81b5f4156..d4b1ff87e 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -56,6 +56,7 @@ -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). +-include("ejabberd_oauth.hrl"). -include("ejabberd_commands.hrl"). @@ -64,17 +65,12 @@ %% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass %% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin %% (as it has access to ejabberd command line). --record(oauth_token, { - token = {<<"">>, <<"">>} :: {binary(), binary()}, - us = {<<"">>, <<"">>} :: {binary(), binary()}, - scope = [] :: [binary()], - expire :: integer() - }). -define(EXPIRE, 3600). start() -> - init_db(mnesia, ?MYNAME), + DBMod = get_db_backend(), + DBMod:init(), Expire = expire(), application:set_env(oauth2, backend, ejabberd_oauth), application:set_env(oauth2, expiry_time, Expire), @@ -172,15 +168,8 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info(clean, State) -> {MegaSecs, Secs, MiniSecs} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - F = fun() -> - Ts = mnesia:select( - oauth_token, - [{#oauth_token{expire = '$1', _ = '_'}, - [{'<', '$1', TS}], - ['$_']}]), - lists:foreach(fun mnesia:delete_object/1, Ts) - end, - mnesia:async_dirty(F), + DBMod = get_db_backend(), + DBMod:clean(TS), erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)), self(), clean), {noreply, State}; @@ -191,16 +180,6 @@ terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -init_db(mnesia, _Host) -> - mnesia:create_table(oauth_token, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, oauth_token)}]), - mnesia:add_table_copy(oauth_token, node(), disc_copies); -init_db(_, _) -> - ok. - - get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}. @@ -305,7 +284,8 @@ associate_access_token(AccessToken, Context, AppContext) -> scope = Scope, expire = Expire }, - mnesia:dirty_write(R), + DBMod = get_db_backend(), + DBMod:store(R), {ok, AppContext}. associate_refresh_token(_RefreshToken, _Context, AppContext) -> @@ -315,10 +295,11 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) -> check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - case catch mnesia:dirty_read(oauth_token, Token) of - [#oauth_token{us = {LUser, LServer}, - scope = TokenScope, - expire = Expire}] -> + DBMod = get_db_backend(), + case DBMod:lookup(Token) of + #oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, TokenScopeSet = oauth2_priv_set:new(TokenScope), @@ -330,10 +311,11 @@ check_token(User, Server, ScopeList, Token) -> end. check_token(ScopeList, Token) -> - case catch mnesia:dirty_read(oauth_token, Token) of - [#oauth_token{us = US, - scope = TokenScope, - expire = Expire}] -> + DBMod = get_db_backend(), + case DBMod:lookup(Token) of + #oauth_token{us = US, + scope = TokenScope, + expire = Expire} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, TokenScopeSet = oauth2_priv_set:new(TokenScope), @@ -548,6 +530,15 @@ process(_Handlers, process(_Handlers, _Request) -> ejabberd_web:error(not_found). +-spec get_db_backend() -> module(). + +get_db_backend() -> + DBType = ejabberd_config:get_option( + oauth_db_type, + fun(T) -> ejabberd_config:v_db(?MODULE, T) end, + mnesia), + list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)). + %% Headers as per RFC 6749 json_response(Code, Body) -> @@ -688,4 +679,6 @@ opt_type(oauth_expire) -> fun(I) when is_integer(I), I >= 0 -> I end; opt_type(oauth_access) -> fun acl:access_rules_validator/1; -opt_type(_) -> [oauth_expire, oauth_access]. +opt_type(oauth_db_type) -> + fun(T) -> ejabberd_config:v_db(?MODULE, T) end; +opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type]. diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl new file mode 100644 index 000000000..a23f443ed --- /dev/null +++ b/src/ejabberd_oauth_mnesia.erl @@ -0,0 +1,65 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_mnesia.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 mnesia backend +%%% Created : 20 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_mnesia). + +-export([init/0, + store/1, + lookup/1, + clean/1]). + +-include("ejabberd_oauth.hrl"). + +init() -> + mnesia:create_table(oauth_token, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, oauth_token)}]), + mnesia:add_table_copy(oauth_token, node(), disc_copies), + ok. + +store(R) -> + mnesia:dirty_write(R). + +lookup(Token) -> + case catch mnesia:dirty_read(oauth_token, Token) of + [R] -> + R; + _ -> + false + end. + +clean(TS) -> + F = fun() -> + Ts = mnesia:select( + oauth_token, + [{#oauth_token{expire = '$1', _ = '_'}, + [{'<', '$1', TS}], + ['$_']}]), + lists:foreach(fun mnesia:delete_object/1, Ts) + end, + mnesia:async_dirty(F). + From fede85c9bd7c78204c988c30dfe846fd7d237f52 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 11:53:34 +0200 Subject: [PATCH 041/179] Remove unused import --- lib/ct_formatter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ct_formatter.ex b/lib/ct_formatter.ex index 47c487ac4..0c301353b 100644 --- a/lib/ct_formatter.ex +++ b/lib/ct_formatter.ex @@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do use GenEvent - import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5, + import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5, format_test_case_failure: 5] def init(opts) do From 12f74b4aa79054f186273bf80e8cb2a24f0000ef Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 11:57:38 +0200 Subject: [PATCH 042/179] Fix list appending bug --- src/ejabberd_commands.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 9496fe09f..ef25be2e5 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -774,7 +774,7 @@ expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L (admin, Acc) -> AdminCmds ++ Acc; (restricted, Acc) -> RestrictedCmds ++ Acc; (Command, Acc) when is_atom(Command) -> - [Command, Acc] + [Command|Acc] end, [], L). oauth_token_user(noauth) -> From 9ff7257287b6a7339cd3f5503f46e8b6b70308c7 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 11:58:14 +0200 Subject: [PATCH 043/179] Make jlib ETS table more resilient --- src/jid.erl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/jid.erl b/src/jid.erl index 0c3ac77c0..46830d031 100644 --- a/src/jid.erl +++ b/src/jid.erl @@ -50,11 +50,22 @@ -spec start() -> ok. start() -> + Owner = spawn(fun() -> ets_keepalive() end), SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]), - catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]), + %% Table is public to allow ETS insert to fix / update the table even if table already exist + %% with another owner. + catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]), ets:insert(jlib, {string_to_jid_pattern, SplitPattern}), ok. +%% Process used to keep jlib ETS table alive in case the original owner dies. +%% The table need to be public, otherwise subsequent inserts would fail. +ets_keepalive() -> + receive + _ -> + ets_keepalive() + end. + -spec make(binary(), binary(), binary()) -> jid() | error. make(User, Server, Resource) -> From e5fd1ee4f6d80c464297eb9c7ae1c87c8b992e41 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 12:12:48 +0200 Subject: [PATCH 044/179] Avoid starting several time the owner process --- src/jid.erl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/jid.erl b/src/jid.erl index 46830d031..a730bd949 100644 --- a/src/jid.erl +++ b/src/jid.erl @@ -50,7 +50,7 @@ -spec start() -> ok. start() -> - Owner = spawn(fun() -> ets_keepalive() end), + {ok, Owner} = ets_owner(), SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]), %% Table is public to allow ETS insert to fix / update the table even if table already exist %% with another owner. @@ -58,6 +58,19 @@ start() -> ets:insert(jlib, {string_to_jid_pattern, SplitPattern}), ok. +ets_owner() -> + case whereis(jlib_ets) of + undefined -> + Pid = spawn(fun() -> ets_keepalive() end), + case catch register(jlib_ets, Pid) of + true -> + {ok, Pid}; + Error -> Error + end; + Pid -> + {ok,Pid} + end. + %% Process used to keep jlib ETS table alive in case the original owner dies. %% The table need to be public, otherwise subsequent inserts would fail. ets_keepalive() -> From 7781f39b74c46bd6eb6db3cb80397678c7be9ebb Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 12:15:03 +0200 Subject: [PATCH 045/179] Clarify command module API --- src/ejabberd_commands.erl | 46 +++++++++++++++++++++++++++++---------- src/ejabberd_oauth.erl | 4 ++-- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index ef25be2e5..33edcb7c7 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -223,9 +223,10 @@ get_command_definition/2, get_tags_commands/0, get_tags_commands/1, - get_commands/0, + get_exposed_commands/0, register_commands/1, - unregister_commands/1, + unregister_commands/1, + expose_commands/1, execute_command/2, execute_command/3, execute_command/4, @@ -275,10 +276,10 @@ get_commands_spec() -> init() -> mnesia:delete_table(ejabberd_commands), mnesia:create_table(ejabberd_commands, - [{ram_copies, [node()]}, + [{ram_copies, [node()]}, {local_content, true}, - {attributes, record_info(fields, ejabberd_commands)}, - {type, bag}]), + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), mnesia:add_table_copy(ejabberd_commands, node(), ram_copies), register_commands(get_commands_spec()). @@ -287,12 +288,14 @@ init() -> %% @doc Register ejabberd commands. %% If a command is already registered, a warning is printed and the %% old command is preserved. +%% A registered command is not directly available to be called through +%% ejabberd ReST API. It need to be exposed to be available through API. register_commands(Commands) -> lists:foreach( fun(Command) -> - % XXX check if command exists - mnesia:dirty_write(Command) - % ?DEBUG("This command is already defined:~n~p", [Command]) + %% XXX check if command exists + mnesia:dirty_write(Command) + %% ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands). @@ -306,6 +309,25 @@ unregister_commands(Commands) -> end, Commands). +%% @doc Expose command through ejabberd ReST API. +%% Pass a list of command names or policy to expose. +-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}. + +expose_commands(Commands) -> + Names = lists:map(fun(#ejabberd_commands{name = Name}) -> + Name; + (Name) when is_atom(Name) -> + Name + end, + Commands), + + case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of + {aborted, Reason} -> + {error, Reason}; + {atomic, Result} -> + Result + end. + -spec list_commands() -> [{atom(), [aterm()], string()}]. %% @doc Get a list of all the available commands, arguments and description. @@ -737,14 +759,14 @@ get_all_access_commands(AccessCommands) -> get_access_commands(AccessCommands, ?DEFAULT_VERSION). get_access_commands(undefined, Version) -> - Cmds = get_commands(Version), + Cmds = get_exposed_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; get_access_commands(AccessCommands, _Version) -> AccessCommands. -get_commands() -> - get_commands(?DEFAULT_VERSION). -get_commands(Version) -> +get_exposed_commands() -> + get_exposed_commands(?DEFAULT_VERSION). +get_exposed_commands(Version) -> Opts0 = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 86444e51a..0ac18b7ef 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -236,7 +236,7 @@ authenticate_user({User, Server}, Ctx) -> authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> - Cmds = ejabberd_commands:get_commands(), + Cmds = ejabberd_commands:get_exposed_commands(), Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds], RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1], case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), @@ -258,7 +258,7 @@ get_cmd_scopes() -> dict:append(Scope, Cmd, Accum2) end, Accum, Scopes); _ -> Accum - end end, dict:new(), ejabberd_commands:get_commands()), + end end, dict:new(), ejabberd_commands:get_exposed_commands()), ScopeMap. %% This is callback for oauth tokens generated through the command line. Only open and admin commands are From 2a8005e47fdb7dbeff27dde41550221823bdb75d Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 26 Jul 2016 12:17:37 +0200 Subject: [PATCH 046/179] Add ability to run test with Elixir mix --- mix.exs | 6 +++++- mix.lock | 9 ++++++--- test/ejabberd_commands_mock_test.exs | 4 +++- test/ejabberd_commands_test.exs | 22 ++++++++++++++++++++++ test/ejabberd_cyrsasl_test.exs | 10 +++++----- test/mod_admin_extra_test.exs | 3 +++ test/mod_http_api_mock_test.exs | 6 +++--- test/mod_http_api_test.exs | 18 +++++++++--------- test/test_helper.exs | 7 +++++++ 9 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 test/test_helper.exs diff --git a/mix.exs b/mix.exs index 03ce64da2..7453ea473 100644 --- a/mix.exs +++ b/mix.exs @@ -11,6 +11,8 @@ defmodule Ejabberd.Mixfile do compilers: [:asn1] ++ Mix.compilers, erlc_options: erlc_options, erlc_paths: ["asn1", "src"], + # Elixir tests are starting the part of ejabberd they need + aliases: [test: "test --no-start"], package: package, deps: deps] end @@ -59,7 +61,9 @@ defmodule Ejabberd.Mixfile do {:exrm, "~> 1.0.0", only: :dev}, # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with # version 3.20: - {:relx, "~> 3.19.0", only: :dev}] + {:relx, "~> 3.19.0", only: :dev}, + {:meck, "~> 0.8.4", only: :test}, + {:moka, github: "processone/moka", tag: "1.0.5b", only: :test}] end defp package do diff --git a/mix.lock b/mix.lock index eda31f4a9..467f142ba 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,7 @@ "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, - "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:stun, "1.0.6", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}, + "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:stun, "1.0.6", [hex: :stun, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, "fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, @@ -14,13 +14,16 @@ "iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, + "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, + "moka": {:git, "https://github.com/processone/moka.git", "768efea96443c57125e6247dbebee687f17be149", [tag: "1.0.5b"]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []}, "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]}, + "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, + "samerlib": {:git, "https://github.com/processone/samerlib", "9158f65d18ec63f8b409543b6fb46dd5fce46160", [tag: "0.8.0b"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}} + "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}} diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 72e7c2d3a..439a3c1d3 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -23,6 +23,8 @@ defmodule EjabberdCommandsMockTest do use ExUnit.Case, async: false + require EjabberdOauthMock + @author "jsautret@process-one.net" # mocked callback module @@ -50,7 +52,7 @@ defmodule EjabberdCommandsMockTest do :ok = :ejabberd_config.start(["domain1", "domain2"], []) :ok = :acl.start EjabberdOauthMock.init - :ok + on_exit fn -> :meck.unload end end setup do diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs index 31d108214..10b656140 100644 --- a/test/ejabberd_commands_test.exs +++ b/test/ejabberd_commands_test.exs @@ -28,7 +28,11 @@ defmodule EjabberdCommandsTest do setup_all do :mnesia.start + :stringprep.start + :ok = :ejabberd_config.start(["localhost"], []) + :ejabberd_commands.init + :ok end test "Check that we can register a command" do @@ -37,6 +41,14 @@ defmodule EjabberdCommandsTest do assert Enum.member?(commands, {:test_user, [], "Test user"}) end + test "get_exposed_commands/0 returns registered commands" do + commands = [open_test_command] + :ok = :ejabberd_commands.register_commands(commands) + :ok = :ejabberd_commands.expose_commands(commands) + exposed_commands = :ejabberd_commands.get_exposed_commands + assert Enum.member?(exposed_commands, :test_open) + end + test "Check that admin commands are rejected with noauth credentials" do :ok = :ejabberd_commands.register_commands([admin_test_command]) @@ -70,6 +82,16 @@ defmodule EjabberdCommandsTest do ]}}}}) end + defp open_test_command do + ejabberd_commands(name: :test_open, tags: [:test], + desc: "Test open", + policy: :open, + module: __MODULE__, + function: :test_open, + args: [], + result: {:res, :rescode}) + end + defp admin_test_command do ejabberd_commands(name: :test_admin, tags: [:roster], desc: "Test admin", diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs index 0dc64ee44..d9b949294 100644 --- a/test/ejabberd_cyrsasl_test.exs +++ b/test/ejabberd_cyrsasl_test.exs @@ -71,8 +71,8 @@ defmodule EjabberdCyrsaslTest do response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <> "nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <> "charset=utf-8,algorithm=md5-sess" - assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response) - assert {:ok, list} = :cyrsasl.server_step(state3, "") + assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response) + assert {:ok, _list} = :cyrsasl.server_step(state3, "") end defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do @@ -94,7 +94,7 @@ defmodule EjabberdCyrsaslTest do defp setup_anonymous_mocks() do :meck.unload mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled, - fn (host) -> + fn (_host) -> true end) mock(:ejabberd_auth, :is_user_exists, @@ -119,7 +119,7 @@ defmodule EjabberdCyrsaslTest do end end - defp check_password(user, authzid, pass) do + defp check_password(_user, authzid, pass) do case get_password(authzid) do {^pass, mod} -> {true, mod} @@ -128,7 +128,7 @@ defmodule EjabberdCyrsaslTest do end end - defp check_password_digest(user, authzid, pass, digest, digest_gen) do + defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do case get_password(authzid) do {spass, mod} -> v = digest_gen.(spass) diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs index 761b07b7c..03422264f 100644 --- a/test/mod_admin_extra_test.exs +++ b/test/mod_admin_extra_test.exs @@ -22,6 +22,9 @@ defmodule EjabberdModAdminExtraTest do use ExUnit.Case, async: false require EjabberdAuthMock + require EjabberdSmMock + require ModLastMock + require ModRosterMock @author "jsautret@process-one.net" diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index fcfdfee13..9cba35365 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -73,7 +73,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) -> @@ -126,7 +126,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, {:oauth, _token}, false}, @@ -219,7 +219,7 @@ defmodule ModHttpApiMockTest do end) :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_commands, + :meck.expect(:ejabberd_commands, :get_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, {:oauth, _token}, false}, diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index b440f3eb8..e2ae3d784 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -31,24 +31,24 @@ defmodule ModHttpApiTest do :ok = :mnesia.start :stringprep.start :ok = :ejabberd_config.start(["localhost"], []) - :ok = :ejabberd_commands.init - :ok = :ejabberd_commands.register_commands(cmds) - on_exit fn -> unregister_commands(cmds) end + on_exit fn -> + :meck.unload + unregister_commands(cmds) end end test "We can expose several commands to API at a time" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]]) - commands = :ejabberd_commands.get_commands() + :ejabberd_commands.expose_commands([:open_cmd, :user_cmd]) + commands = :ejabberd_commands.get_exposed_commands() assert Enum.member?(commands, :open_cmd) assert Enum.member?(commands, :user_cmd) end test "We can call open commands without authentication" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]]) + :ejabberd_commands.expose_commands([:open_cmd]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {200, _, _} = :mod_http_api.process(["open_cmd"], request) end @@ -56,14 +56,14 @@ defmodule ModHttpApiTest do # This related to the commands config file option test "Attempting to access a command that is not exposed as HTTP API returns 403" do setup_mocks() - :ejabberd_config.add_local_option(:commands, []) + :ejabberd_commands.expose_commands([]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {403, _, _} = :mod_http_api.process(["open_cmd"], request) end test "Call to user, admin or restricted commands without authentication are rejected" do setup_mocks() - :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]]) + :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted]) request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") {403, _, _} = :mod_http_api.process(["user_cmd"], request) {403, _, _} = :mod_http_api.process(["admin_cmd"], request) @@ -98,7 +98,7 @@ defmodule ModHttpApiTest do defp setup_mocks() do :meck.unload mock(:gen_mod, :get_module_opt, - fn (_server, :mod_http_api, admin_ip_access, _, _) -> + fn (_server, :mod_http_api, _admin_ip_access, _, _) -> [{:allow, [{:ip, {{127,0,0,2}, 32}}]}] end) end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 000000000..454f2338a --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,7 @@ +Code.require_file "ejabberd_auth_mock.exs", __DIR__ +Code.require_file "ejabberd_oauth_mock.exs", __DIR__ +Code.require_file "ejabberd_sm_mock.exs", __DIR__ +Code.require_file "mod_last_mock.exs", __DIR__ +Code.require_file "mod_roster_mock.exs", __DIR__ + +ExUnit.start From 984c4cf6bd77b33a17649c10da3a15cc43567dfd Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 26 Jul 2016 14:37:28 +0300 Subject: [PATCH 047/179] Add 'allow_subscription' MUC configuration option --- src/mod_muc.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index a86f580d3..4c5b45ff5 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -225,6 +225,7 @@ init([Host, Opts]) -> public -> Bool; public_list -> Bool; mam -> Bool; + allow_subscription -> Bool; password -> fun iolist_to_binary/1; title -> fun iolist_to_binary/1; allow_private_messages_from_visitors -> From 72445bb374d36f6655d7a5b4d0ac813d9415924a Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 27 Jul 2016 00:28:47 +0200 Subject: [PATCH 048/179] mod_http_upload_quota: Apply cosmetic changes Use "fun f/1" syntax in place of "fun(X) -> f(X) end". --- src/mod_http_upload_quota.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index 35e266ddf..3051a4e87 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -251,7 +251,7 @@ terminate(Reason, #state{server_host = ServerHost, timers = Timers}) -> ?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]), ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, handle_slot_request, 50), - lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers). + lists:foreach(fun timer:cancel/1, Timers). -spec code_change({down, _} | _, state(), _) -> {ok, state()}. @@ -299,7 +299,7 @@ enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> {[Path | AccFiles], AccSize + Size, NewSize} end, {[], 0, 0}, Files), if OldSize + SlotSize > MaxSize -> - lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles), + lists:foreach(fun del_file_and_dir/1, DelFiles), file:del_dir(UserDir), % In case it's empty, now. NewSize + SlotSize; true -> @@ -314,7 +314,7 @@ delete_old_files(UserDir, CutOff) -> [] -> ok; OldFiles -> - lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles), + lists:foreach(fun del_file_and_dir/1, OldFiles), file:del_dir(UserDir) % In case it's empty, now. end. From 91865c66c0ce2339156f98db83740f65edc909e6 Mon Sep 17 00:00:00 2001 From: gabrielgatu Date: Thu, 28 Jul 2016 15:57:35 +0200 Subject: [PATCH 049/179] Start elixir application after ejabberd_app:start_apps() --- src/ejabberd_app.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 703614f63..bbeb510dc 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -45,6 +45,7 @@ start(normal, _Args) -> write_pid_file(), jid:start(), start_apps(), + start_elixir_application(), ejabberd:check_app(ejabberd), randoms:start(), db_init(), @@ -237,3 +238,9 @@ opt_type(modules) -> Mods) end; opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. + +start_elixir_application() -> + case application:ensure_started(elixir) of + ok -> ok; + {error, Msg} -> ?ERROR_MSG("Elixir application not started.", []) + end. From fd365b2893c65908837924c06d9d45da4302086b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 28 Jul 2016 16:20:13 +0200 Subject: [PATCH 050/179] Display data that is send to websocket connection in debug log level This should help with detecting problems like in #1097 --- src/ejabberd_c2s.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 0ffca7179..24a2af56d 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1862,6 +1862,7 @@ send_text(StateData, Text) -> send_element(StateData, El) when StateData#state.mgmt_state == pending -> ?DEBUG("Cannot send element while waiting for resumption: ~p", [El]); send_element(StateData, El) when StateData#state.xml_socket -> + ?DEBUG("Send XML on stream = ~p", [fxml:element_to_binary(El)]), (StateData#state.sockmod):send_xml(StateData#state.socket, {xmlstreamelement, El}); send_element(StateData, El) -> From a0803220554a6a44dd87a0452f424159e08157fe Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Fri, 29 Jul 2016 11:18:42 +0200 Subject: [PATCH 051/179] Switch workers from temporary to transient --- src/ejabberd_oauth.erl | 2 +- src/mod_echo.erl | 2 +- src/mod_irc.erl | 2 +- src/mod_muc.erl | 2 +- src/mod_muc_log.erl | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 0af158562..c45a69d17 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -76,7 +76,7 @@ start() -> application:set_env(oauth2, expiry_time, Expire), application:start(oauth2), ChildSpec = {?MODULE, {?MODULE, start_link, []}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec), ejabberd_commands:register_commands(get_commands_spec()), ok. diff --git a/src/mod_echo.erl b/src/mod_echo.erl index ee904d798..96651aebf 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -63,7 +63,7 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> diff --git a/src/mod_irc.erl b/src/mod_irc.erl index 2206028b7..e2203a306 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -90,7 +90,7 @@ start(Host, Opts) -> start_supervisor(Host), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 4c5b45ff5..571f85926 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -95,7 +95,7 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index ec4711b43..d5ced9116 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -81,7 +81,7 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 1000, worker, [?MODULE]}, + transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> From 42e6f72ee95cc69df956e4e8009d514ac1063baf Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Fri, 29 Jul 2016 20:38:05 +0200 Subject: [PATCH 052/179] Do not crash on check when we do not have JID --- src/ejabberd_commands.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 33edcb7c7..2c095440f 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -555,6 +555,8 @@ execute_check_policy( execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) -> do_execute_command(Command, Arguments); +execute_check_access(undefined, _Command, _Arguments) -> + throw({error, access_rules_unauthorized}); execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) -> %% TODO Review: Do we have smarter / better way to check rule on other Host than global ? Host = global, From fb2603d3cdef98542f479764567b15c9c7777411 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 11:50:04 +0200 Subject: [PATCH 053/179] Return 409 conflict error code on register if user already exists --- src/ejabberd_admin.erl | 2 +- src/mod_http_api.erl | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index f20aeebf0..f40b99ea5 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -382,7 +382,7 @@ register(User, Host, Password) -> {atomic, exists} -> String = io_lib:format("User ~s@~s already registered at node ~p", [User, Host, node()]), - {exists, String}; + {conflict, String}; {error, Reason} -> String = io_lib:format("Can't register user ~s@~s at node ~p: ~p", [User, Host, node(), Reason]), diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index ba3a14cf8..f56a47666 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -444,22 +444,24 @@ ejabberd_command(Auth, Cmd, Args, Version, IP) -> format_command_result(Cmd, Auth, Result, Version) -> {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of - {{_, rescode}, V} when V == true; V == ok -> - {200, 0}; - {{_, rescode}, _} -> - {200, 1}; - {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> - {200, iolist_to_binary(Text1)}; - {{_, restuple}, {_, Text2}} -> - {500, iolist_to_binary(Text2)}; - {{_, {list, _}}, _V} -> - {_, L} = format_result(Result, ResultFormat), - {200, L}; - {{_, {tuple, _}}, _V} -> - {_, T} = format_result(Result, ResultFormat), - {200, T}; - _ -> - {200, {[format_result(Result, ResultFormat)]}} + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; + {{_, restuple}, {V, Text}} when V == true; V == ok -> + {200, iolist_to_binary(Text)}; + {{_, restuple}, {V, Text}} when V == conflict -> + {409, iolist_to_binary(Text)}; + {{_, restuple}, {_, Text2}} -> + {500, iolist_to_binary(Text2)}; + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> + {200, {[format_result(Result, ResultFormat)]}} end. format_result(Atom, {Name, atom}) -> From 39640b67c7bc6c46312879beccc54fa5de4c4d95 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 13:08:30 +0200 Subject: [PATCH 054/179] Add support for rich error reporting for API --- src/ejabberd_admin.erl | 7 +++---- src/ejabberd_ctl.erl | 11 +++++++++-- src/mod_http_api.erl | 32 +++++++++++++++++++------------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index f40b99ea5..2c4d5ed6d 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -380,13 +380,12 @@ register(User, Host, Password) -> {atomic, ok} -> {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; {atomic, exists} -> - String = io_lib:format("User ~s@~s already registered at node ~p", - [User, Host, node()]), - {conflict, String}; + Msg = io_lib:format("User ~s@~s already registered", [User, Host]), + {error, conflict, 10090, Msg}; {error, Reason} -> String = io_lib:format("Can't register user ~s@~s at node ~p: ~p", [User, Host, node(), Reason]), - {cannot_register, String} + {error, cannot_register, 10001, String} end. unregister(User, Host) -> diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index d52d1c0a9..0652267ed 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -212,7 +212,7 @@ process(["help" | Mode], Version) -> end; process(["--version", Arg | Args], _) -> - Version = + Version = try list_to_integer(Arg) catch _:_ -> @@ -321,7 +321,7 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) -> {ArgsFormat, ResultFormat} -> case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, + Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, ArgsFormatted, Version), @@ -374,6 +374,12 @@ format_arg2(Arg, Parse)-> format_result({error, ErrorAtom}, _) -> {io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)}; +%% An error should always be allowed to return extended error to help with API. +%% Extended error is of the form: +%% {error, type :: atom(), code :: int(), Desc :: string()} +format_result({error, ErrorAtom, Code, _Msg}, _) -> + {io_lib:format("Error: ~p", [ErrorAtom]), make_status(Code)}; + format_result(Atom, {_Name, atom}) -> io_lib:format("~p", [Atom]); @@ -433,6 +439,7 @@ format_result(404, {_Name, _}) -> make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; +make_status(Code) when is_integer(Code) -> Code; make_status(_Error) -> ?STATUS_ERROR. get_list_commands(Version) -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index f56a47666..6f6d59cda 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -220,12 +220,8 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = log(Call, Args, IPPort), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - case handle(Cmd, Auth, Args, Version, IP) of - {Code, Result} -> - json_response(Code, jiffy:encode(Result)); - {HTMLCode, JSONErrorCode, Message} -> - json_error(HTMLCode, JSONErrorCode, Message) - end; + Result = handle(Cmd, Auth, Args, Version, IP), + json_format(Result); %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse @@ -247,8 +243,8 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args, Version, IP), - json_response(Code, jiffy:encode(Result)); + Result = handle(Cmd, Auth, Args, Version, IP), + json_format(Result); %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse @@ -302,7 +298,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> [{Key, undefined}|Acc] end, [], ArgsSpec), try - handle2(Call, Auth, match(Args2, Spec), Version, IP) + handle2(Call, Auth, match(Args2, Spec), Version, IP) catch throw:not_found -> {404, <<"not_found">>}; throw:{not_found, Why} when is_atom(Why) -> @@ -448,12 +444,12 @@ format_command_result(Cmd, Auth, Result, Version) -> {200, 0}; {{_, rescode}, _} -> {200, 1}; + {_, {error, ErrorAtom, Code, Msg}} -> + format_error_result(ErrorAtom, Code, Msg); {{_, restuple}, {V, Text}} when V == true; V == ok -> {200, iolist_to_binary(Text)}; - {{_, restuple}, {V, Text}} when V == conflict -> - {409, iolist_to_binary(Text)}; - {{_, restuple}, {_, Text2}} -> - {500, iolist_to_binary(Text2)}; + {{_, restuple}, {ErrorAtom, Msg}} -> + format_error_result(ErrorAtom, 0, Msg); {{_, {list, _}}, _V} -> {_, L} = format_result(Result, ResultFormat), {200, L}; @@ -499,6 +495,11 @@ format_result(Tuple, {Name, {tuple, Def}}) -> format_result(404, {_Name, _}) -> "not_found". +format_error_result(conflict, Code, Msg) -> + {409, Code, iolist_to_binary(Msg)}; +format_error_result(_ErrorAtom, Code, Msg) -> + {500, Code, iolist_to_binary(Msg)}. + unauthorized_response() -> json_error(401, 10, <<"Oauth Token is invalid or expired.">>). @@ -507,6 +508,11 @@ badrequest_response() -> badrequest_response(Body) -> json_response(400, jiffy:encode(Body)). +json_format({Code, Result}) -> + json_response(Code, jiffy:encode(Result)); +json_format({HTMLCode, JSONErrorCode, Message}) -> + json_error(HTMLCode, JSONErrorCode, Message). + json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. From 19ad6e6145db08f575871e6ee82f78d96f8e7091 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 13:18:39 +0200 Subject: [PATCH 055/179] Ensure ejabberdctl status result is in valid shell supported range --- src/ejabberd_ctl.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 0652267ed..d52b55cf9 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -439,7 +439,8 @@ format_result(404, {_Name, _}) -> make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; -make_status(Code) when is_integer(Code) -> Code; +make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR; +make_status(Code) when is_integer(Code), Code > 0 -> Code; make_status(_Error) -> ?STATUS_ERROR. get_list_commands(Version) -> From 4bf8ce76810e19864fec5bb659f1ecc803fcef61 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 18:50:58 +0200 Subject: [PATCH 056/179] Make s2s stats commands more robust --- src/ejabberd_s2s.erl | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 19de64adb..2a17c75cb 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -473,28 +473,34 @@ send_element(Pid, El) -> %%% ejabberd commands get_commands_spec() -> - [#ejabberd_commands{name = incoming_s2s_number, - tags = [stats, s2s], - desc = - "Number of incoming s2s connections on " - "the node", - policy = admin, - module = ?MODULE, function = incoming_s2s_number, - args = [], result = {s2s_incoming, integer}}, - #ejabberd_commands{name = outgoing_s2s_number, - tags = [stats, s2s], - desc = - "Number of outgoing s2s connections on " - "the node", - policy = admin, - module = ?MODULE, function = outgoing_s2s_number, - args = [], result = {s2s_outgoing, integer}}]. + [#ejabberd_commands{ + name = incoming_s2s_number, + tags = [stats, s2s], + desc = "Number of incoming s2s connections on the node", + policy = admin, + module = ?MODULE, function = incoming_s2s_number, + args = [], result = {s2s_incoming, integer}}, + #ejabberd_commands{ + name = outgoing_s2s_number, + tags = [stats, s2s], + desc = "Number of outgoing s2s connections on the node", + policy = admin, + module = ?MODULE, function = outgoing_s2s_number, + args = [], result = {s2s_outgoing, integer}}]. +%% TODO Move those stats commands to ejabberd stats command ? incoming_s2s_number() -> - length(supervisor:which_children(ejabberd_s2s_in_sup)). + supervisor_count(ejabberd_s2s_in_sup). outgoing_s2s_number() -> - length(supervisor:which_children(ejabberd_s2s_out_sup)). + supervisor_count(ejabberd_s2s_out_sup). + +supervisor_count(Supervisor) -> + case catch supervisor:which_children(Supervisor) of + {'EXIT', _} -> 0; + Result -> + length(Result) + end. %%%---------------------------------------------------------------------- %%% Update Mnesia tables From 674a8039ef0da080c9882bbe8ea3a476d78df0f5 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 18:51:54 +0200 Subject: [PATCH 057/179] Add support for sending back missing scope error to API ReST command calls --- src/cyrsasl_oauth.erl | 2 +- src/ejabberd_commands.erl | 2 +- src/ejabberd_oauth.erl | 34 ++++++++++++++++++++++------------ src/mod_http_api.erl | 11 ++++++++--- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/cyrsasl_oauth.erl b/src/cyrsasl_oauth.erl index 16f1e3dfb..80ba315ed 100644 --- a/src/cyrsasl_oauth.erl +++ b/src/cyrsasl_oauth.erl @@ -51,7 +51,7 @@ mech_step(State, ClientIn) -> {ok, [{username, User}, {authzid, AuthzId}, {auth_module, ejabberd_oauth}]}; - false -> + _ -> {error, <<"not-authorized">>, User} end; _ -> {error, <<"bad-protocol">>} diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 2c095440f..d9497322f 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -682,7 +682,7 @@ check_auth(Command, {User, Server, {oauth, Token}, _}) -> case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of true -> {ok, User, Server}; - false -> + _ -> throw({error, invalid_account_data}) end; check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 0ac18b7ef..531f27749 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -321,12 +321,17 @@ check_token(User, Server, ScopeList, Token) -> expire = Expire}] -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - TokenScopeSet = oauth2_priv_set:new(TokenScope), - lists:any(fun(Scope) -> - oauth2_priv_set:is_member(Scope, TokenScopeSet) end, - ScopeList) andalso Expire > TS; + if + Expire > TS -> + TokenScopeSet = oauth2_priv_set:new(TokenScope), + lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList); + true -> + {false, expired} + end; _ -> - false + {false, not_found} end. check_token(ScopeList, Token) -> @@ -336,15 +341,20 @@ check_token(ScopeList, Token) -> expire = Expire}] -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, - TokenScopeSet = oauth2_priv_set:new(TokenScope), - case lists:any(fun(Scope) -> - oauth2_priv_set:is_member(Scope, TokenScopeSet) end, - ScopeList) andalso Expire > TS of - true -> {ok, user, US}; - false -> false + if + Expire > TS -> + TokenScopeSet = oauth2_priv_set:new(TokenScope), + case lists:any(fun(Scope) -> + oauth2_priv_set:is_member(Scope, TokenScopeSet) end, + ScopeList) of + true -> {ok, user, US}; + false -> {false, no_matching_scope} + end; + true -> + {false, expired} end; _ -> - false + {false, not_found} end. diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 6f6d59cda..cda4d6059 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -162,14 +162,15 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeL case oauth_check_token(ScopeList, Token) of {ok, user, {User, Server}} -> {ok, {User, Server, {oauth, Token}, Admin}}; - false -> - false + {false, Reason} -> + {false, Reason} end; _ -> false end, case Auth of {ok, A} -> {allowed, Call, A}; + {false, no_matching_scope} -> outofscope_response(); _ -> unauthorized_response() end; check_permissions2(_Request, Call, open, _Scope) -> @@ -189,7 +190,7 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) -> Commands when is_list(Commands) -> case lists:member(Call, Commands) of true -> {allowed, Call, admin}; - _ -> unauthorized_response() + _ -> outofscope_response() end; _E -> {allowed, Call, noauth} @@ -495,6 +496,7 @@ format_result(Tuple, {Name, {tuple, Def}}) -> format_result(404, {_Name, _}) -> "not_found". + format_error_result(conflict, Code, Msg) -> {409, Code, iolist_to_binary(Msg)}; format_error_result(_ErrorAtom, Code, Msg) -> @@ -503,6 +505,9 @@ format_error_result(_ErrorAtom, Code, Msg) -> unauthorized_response() -> json_error(401, 10, <<"Oauth Token is invalid or expired.">>). +outofscope_response() -> + json_error(401, 11, <<"Token does not grant usage to command required scope.">>). + badrequest_response() -> badrequest_response(<<"400 Bad Request">>). badrequest_response(Body) -> From 2a49f8cae7e579abfdd3c988c6749087c06fdd86 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 30 Jul 2016 20:12:04 +0200 Subject: [PATCH 058/179] Change name of result key for offline count to value This is more user friendly and should be more consistent with other commands. --- src/mod_admin_extra.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 2ad1cc28e..c23fd7818 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -535,7 +535,7 @@ get_commands_spec() -> policy = user, module = mod_offline, function = count_offline_messages, args = [], - result = {res, integer}}, + result = {value, integer}}, #ejabberd_commands{name = send_message, tags = [stanza], desc = "Send a message to a local or remote bare of full JID", module = ?MODULE, function = send_message, From 6ea7153e31a8d36fbb34816b4c87ea3eca1ac8fc Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 31 Jul 2016 22:48:24 +0200 Subject: [PATCH 059/179] Improve error handling --- src/ejabberd_admin.erl | 1 + src/ejabberd_commands.erl | 2 +- src/mod_http_api.erl | 34 ++++++++++++++++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 2c4d5ed6d..615459f77 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -87,6 +87,7 @@ get_commands_spec() -> args = [], result = {res, rescode}}, #ejabberd_commands{name = reopen_log, tags = [logs, server], desc = "Reopen the log files", + policy = admin, module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, #ejabberd_commands{name = rotate_log, tags = [logs, server], diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index d9497322f..a8b3e25ab 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -425,7 +425,7 @@ get_command_definition(Name, Version) -> {V, C} end)))) of [{_, Command} | _ ] -> Command; - _E -> throw(unknown_command) + _E -> throw({error, unknown_command}) end. -spec get_commands_definition(integer()) -> [ejabberd_commands()]. diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index cda4d6059..d33fb7a7f 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -213,11 +213,7 @@ process(_, #request{method = 'POST', data = <<>>}) -> process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) -> Version = get_api_version(Req), try - Args = case jiffy:decode(Data) of - List when is_list(List) -> List; - {List} when is_list(List) -> List; - Other -> [Other] - end, + Args = extract_args(Data), log(Call, Args, IPPort), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> @@ -227,10 +223,14 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = ErrorResponse -> ErrorResponse end - catch _:{error,{_,invalid_json}} = _Err -> - ?DEBUG("Bad Request: ~p", [_Err]), - badrequest_response(<<"Invalid JSON input">>); - _:_Error -> + catch + %% TODO We need to refactor to remove redundant error return formatting + throw:{error, unknown_command} -> + {404, 40, <<"Command not found.">>}; + _:{error,{_,invalid_json}} = _Err -> + ?DEBUG("Bad Request: ~p", [_Err]), + badrequest_response(<<"Invalid JSON input">>); + _:_Error -> ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; @@ -250,7 +250,12 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> ErrorResponse -> ErrorResponse end - catch _:_Error -> + catch + %% TODO We need to refactor to remove redundant error return formatting + throw:{error, unknown_command} -> + {404, 40, <<"Command not found.">>}; + _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; @@ -260,6 +265,15 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), badrequest_response(). +%% Be tolerant to make API more easily usable from command-line pipe. +extract_args(<<"\n">>) -> []; +extract_args(Data) -> + case jiffy:decode(Data) of + List when is_list(List) -> List; + {List} when is_list(List) -> List; + Other -> [Other] + end. + % get API version N from last "vN" element in URL path get_api_version(#request{path = Path}) -> get_api_version(lists:reverse(Path)); From c5c394e929d508a0b04efcae6abc696c142d0802 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 1 Aug 2016 08:58:49 +0200 Subject: [PATCH 060/179] Fix HTTP process return formatting --- src/mod_http_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index d33fb7a7f..a91c3c1a7 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -253,7 +253,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> - {404, 40, <<"Command not found.">>}; + json_format({404, 40, <<"Command not found.">>}); _:_Error -> ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), From 4982639d054814270e566dbcef00247fbb933c12 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 1 Aug 2016 09:28:54 +0200 Subject: [PATCH 061/179] Fix error return expectation in command test --- test/ejabberd_commands_mock_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 439a3c1d3..9d33d7573 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -174,7 +174,7 @@ defmodule EjabberdCommandsMockTest do # default version is latest one assert :result3 == :ejabberd_commands.execute_command(command_name, []) # no such command in APIv0 - assert :unknown_command == + assert {:error, :unknown_command} == catch_throw :ejabberd_commands.execute_command(command_name, [], 0) assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1) assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2) From a9c6748ec7831ba5e0323186f148edb78ba9aa05 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Mon, 1 Aug 2016 10:55:02 +0200 Subject: [PATCH 062/179] Add missing comas in sql statement (#1219) --- src/nodetree_tree_sql.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl index edfdbc1d5..9f6b6d5a7 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -77,9 +77,9 @@ set_node(Record) when is_record(Record, pubsub_node) -> catch ejabberd_sql:sql_query_t( ?SQL("update pubsub_node set" - " host=%(H)s" - " node=%(Node)s" - " parent=%(Parent)s" + " host=%(H)s," + " node=%(Node)s," + " parent=%(Parent)s," " type=%(Type)s " "where nodeid=%(OldNidx)d")), OldNidx; From bf45c9eeeebd5dd04397e12dd3051a9a02e792ab Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Mon, 1 Aug 2016 14:09:16 +0200 Subject: [PATCH 063/179] Switch mix worker to transient --- src/mod_mix.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_mix.erl b/src/mod_mix.erl index b373ad13d..a81efd5ce 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -44,7 +44,7 @@ start_link(Host, Opts) -> start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - temporary, 5000, worker, [?MODULE]}, + transient, 5000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> From 90ea3ca3613d7e342ba64e45cefcbe1227ee88c7 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 1 Aug 2016 15:29:47 +0200 Subject: [PATCH 064/179] Improve error message when try to call api on api root --- src/mod_http_api.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index a91c3c1a7..73e6f7e4e 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -253,7 +253,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> - json_format({404, 40, <<"Command not found.">>}); + json_format({404, 44, <<"Command not found.">>}); _:_Error -> ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), @@ -263,7 +263,7 @@ process([], #request{method = 'OPTIONS', data = <<>>}) -> {200, ?OPTIONS_HEADER, []}; process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), - badrequest_response(). + json_error(400, 40, <<"Missing command name.">>). %% Be tolerant to make API more easily usable from command-line pipe. extract_args(<<"\n">>) -> []; From d02d7b2b6a9c83a47fd2722b9481d385590b8668 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 1 Aug 2016 15:35:54 +0200 Subject: [PATCH 065/179] Remove compile warning --- src/ejabberd_app.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index bbeb510dc..6f0b97fa3 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -242,5 +242,5 @@ opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. start_elixir_application() -> case application:ensure_started(elixir) of ok -> ok; - {error, Msg} -> ?ERROR_MSG("Elixir application not started.", []) + {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) end. From 76eba3647a9763e7e1e65759f13f61d3b9e73e87 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Mon, 1 Aug 2016 15:46:14 +0200 Subject: [PATCH 066/179] Implement gen_mod callback in ModPresenceDemo module --- lib/mod_presence_demo.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex index 89fc60d87..ba5abe90e 100644 --- a/lib/mod_presence_demo.ex +++ b/lib/mod_presence_demo.ex @@ -7,15 +7,20 @@ defmodule ModPresenceDemo do Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50) :ok end - + def stop(host) do info('Stopping ejabberd module Presence Demo') Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50) :ok end - + def on_presence(user, _server, _resource, _packet) do info('Receive presence for #{user}') :none - end + end + + # gen_mod callbacks + def depends(_host, _opts), do: [] + def mod_opt_type(_), do: [] + end From 4add262090653e3b2e026f9181247c31acdf6090 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Mon, 1 Aug 2016 16:53:25 +0300 Subject: [PATCH 067/179] Add OAUTH SQL backend --- sql/lite.sql | 7 ++++ sql/mssql.sql | 10 +++++ sql/mysql.sql | 7 ++++ sql/pg.sql | 9 +++++ src/ejabberd_oauth_sql.erl | 78 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 src/ejabberd_oauth_sql.erl diff --git a/sql/lite.sql b/sql/lite.sql index 1741ea950..aacea11e7 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -313,3 +313,10 @@ CREATE TABLE sm ( CREATE UNIQUE INDEX i_sm_sid ON sm(usec, pid); CREATE INDEX i_sm_node ON sm(node); CREATE INDEX i_sm_username ON sm(username); + +CREATE TABLE oauth_token ( + token text NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); diff --git a/sql/mssql.sql b/sql/mssql.sql index 45378d246..0dfaa7161 100644 --- a/sql/mssql.sql +++ b/sql/mssql.sql @@ -480,3 +480,13 @@ ON DELETE CASCADE; ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1]; +CREATE TABLE [dbo].[oauth_token] ( + [token] [varchar] (250) NOT NULL, + [jid] [text] NOT NULL, + [scope] [text] NOT NULL, + [expire] [bigint] NOT NULL, + CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED +( + [token] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) +) TEXTIMAGE_ON [PRIMARY]; diff --git a/sql/mysql.sql b/sql/mysql.sql index 5150fc45b..3d253c574 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -328,3 +328,10 @@ CREATE TABLE sm ( CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75)); CREATE INDEX i_node ON sm(node(75)); CREATE INDEX i_username ON sm(username); + +CREATE TABLE oauth_token ( + token varchar(191) NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/sql/pg.sql b/sql/pg.sql index 1bc4f397c..3d7de4285 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -330,3 +330,12 @@ CREATE TABLE sm ( CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid); CREATE INDEX i_sm_node ON sm USING btree (node); CREATE INDEX i_sm_username ON sm USING btree (username); + +CREATE TABLE oauth_token ( + token text NOT NULL, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl new file mode 100644 index 000000000..9253335ff --- /dev/null +++ b/src/ejabberd_oauth_sql.erl @@ -0,0 +1,78 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_sql.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 SQL backend +%%% Created : 27 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_sql). + +-compile([{parse_transform, ejabberd_sql_pt}]). + +-export([init/0, + store/1, + lookup/1, + clean/1]). + +-include("ejabberd_oauth.hrl"). +-include("ejabberd.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("jlib.hrl"). + +init() -> + ok. + +store(R) -> + Token = R#oauth_token.token, + {User, Server} = R#oauth_token.us, + SJID = jid:to_string({User, Server, <<"">>}), + Scope = str:join(R#oauth_token.scope, <<" ">>), + Expire = R#oauth_token.expire, + ?SQL_UPSERT( + ?MYNAME, + "oauth_token", + ["!token=%(Token)s", + "jid=%(SJID)s", + "scope=%(Scope)s", + "expire=%(Expire)d"]). + +lookup(Token) -> + case ejabberd_sql:sql_query( + ?MYNAME, + ?SQL("select @(jid)s, @(scope)s, @(expire)d" + " from oauth_token where token=%(Token)s")) of + {selected, [{SJID, Scope, Expire}]} -> + JID = jid:from_string(SJID), + US = {JID#jid.luser, JID#jid.lserver}, + #oauth_token{token = Token, + us = US, + scope = str:tokens(Scope, <<" ">>), + expire = Expire}; + _ -> + false + end. + +clean(TS) -> + ejabberd_sql:sql_query( + ?MYNAME, + ?SQL("delete from oauth_token where expire < %(TS)d")). + From 3c1e4f0dfd75d0e51fa0ec2bc42c3dbb5530e121 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 3 Aug 2016 02:15:15 +0200 Subject: [PATCH 068/179] XEP-0198: Increase timeout for stream resumption During stream resumption, the #state is transferred from the old c2s process to the new one. This is usually very fast, but under certain conditions, it can take longer than five seconds. --- src/ejabberd_c2s.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 24a2af56d..d69599485 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -3059,7 +3059,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> end. resume_session({Time, PID}) -> - (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 5000). + (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 15000). make_resume_id(StateData) -> {Time, _} = StateData#state.sid, From 78fa9e08a5a809c0c6b3c838459077b5d44c8e01 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 3 Aug 2016 02:28:46 +0200 Subject: [PATCH 069/179] XEP-0198: Handle timeouts during stream resumption If session resumption failed because requesting the #state from the old c2s process took too long, the new c2s process will usually receive the response. Let the new process handle that case gracefully. --- src/ejabberd_c2s.erl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index d69599485..74b78512a 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1318,7 +1318,7 @@ handle_sync_event({resume_session, Time}, _From, _StateName, StateData#state.user, StateData#state.server, StateData#state.resource), - {stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}}; + {stop, normal, {resume, StateData}, StateData#state{mgmt_state = resumed}}; handle_sync_event({resume_session, _Time}, _From, StateName, StateData) -> {reply, {error, <<"Previous session not found">>}, StateName, StateData}; @@ -1745,6 +1745,13 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> fsm_next_state(StateName, StateData); handle_info(dont_ask_offline, StateName, StateData) -> fsm_next_state(StateName, StateData#state{ask_offline = false}); +handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) -> + %% This happens if the resume_session/1 request timed out; the new session + %% now receives the late response. + ?DEBUG("Received old session state for ~s after failed resumption", + [jid:to_string(OldStateData#state.jid)]), + handle_unacked_stanzas(OldStateData#state{mgmt_resend = false}), + fsm_next_state(StateName, StateData); handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), fsm_next_state(StateName, StateData). @@ -3017,7 +3024,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> OldPID -> OldSID = {Time, OldPID}, case catch resume_session(OldSID) of - {ok, OldStateData} -> + {resume, OldStateData} -> NewSID = {Time, self()}, % Old time, new PID Priority = case OldStateData#state.pres_last of undefined -> From 111aa83f5e3269fc20ccf0f9d56c6d24da98e19c Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Thu, 4 Aug 2016 01:58:56 +0300 Subject: [PATCH 070/179] Add tokens cache to ejabberd_oauth --- src/ejabberd_oauth.erl | 57 +++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index e4396260e..ca0693645 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -71,6 +71,18 @@ start() -> DBMod = get_db_backend(), DBMod:init(), + MaxSize = + ejabberd_config:get_option( + oauth_cache_size, + fun(I) when is_integer(I), I>0 -> I end, + 1000), + LifeTime = + ejabberd_config:get_option( + oauth_cache_life_time, + fun(I) when is_integer(I), I>0 -> I end, + timer:hours(1) div 1000), + cache_tab:new(oauth_token, + [{max_size, MaxSize}, {life_time, LifeTime}]), Expire = expire(), application:set_env(oauth2, backend, ejabberd_oauth), application:set_env(oauth2, expiry_time, Expire), @@ -284,8 +296,7 @@ associate_access_token(AccessToken, Context, AppContext) -> scope = Scope, expire = Expire }, - DBMod = get_db_backend(), - DBMod:store(R), + store(R), {ok, AppContext}. associate_refresh_token(_RefreshToken, _Context, AppContext) -> @@ -295,11 +306,10 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) -> check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - DBMod = get_db_backend(), - case DBMod:lookup(Token) of - #oauth_token{us = {LUser, LServer}, - scope = TokenScope, - expire = Expire} -> + case lookup(Token) of + {ok, #oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, if @@ -316,11 +326,10 @@ check_token(User, Server, ScopeList, Token) -> end. check_token(ScopeList, Token) -> - DBMod = get_db_backend(), - case DBMod:lookup(Token) of - #oauth_token{us = US, - scope = TokenScope, - expire = Expire} -> + case lookup(Token) of + {ok, #oauth_token{us = US, + scope = TokenScope, + expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, if @@ -340,6 +349,26 @@ check_token(ScopeList, Token) -> end. +store(R) -> + cache_tab:insert( + oauth_token, R#oauth_token.token, R, + fun() -> + DBMod = get_db_backend(), + DBMod:store(R) + end). + +lookup(Token) -> + cache_tab:lookup( + oauth_token, Token, + fun() -> + DBMod = get_db_backend(), + case DBMod:lookup(Token) of + #oauth_token{} = R -> {ok, R}; + _ -> error + end + end). + + expire() -> ejabberd_config:get_option( oauth_expire, @@ -696,4 +725,8 @@ opt_type(oauth_access) -> fun acl:access_rules_validator/1; opt_type(oauth_db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; +opt_type(oauth_cache_life_time) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(oauth_cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type]. From 72b0fb49e89e811492a1bc0a299d540a25d4eef6 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 4 Aug 2016 09:49:23 +0200 Subject: [PATCH 071/179] Fix type convertion bug injected by 4ccc40b (#1229) --- src/mod_admin_extra.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index c23fd7818..8e9ea007f 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -1171,8 +1171,8 @@ subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) - subscribe_roster({Name, Server, Group, Nick}, Roster); %% Subscribe Name2 to Name1 subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> - subscribe(Name1, Server1, list_to_binary(Name2), list_to_binary(Server2), - list_to_binary(Nick2), list_to_binary(Group2), <<"both">>, []), + subscribe(Name1, Server1, iolist_to_binary(Name2), iolist_to_binary(Server2), + iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []), subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). push_alltoall(S, G) -> From 9a5f0751be9d012a6f16119556497456ab47da4f Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 5 Aug 2016 01:57:01 +0200 Subject: [PATCH 072/179] mod_mam: Simplify "assume_mam_usage" option The "assume_mam_usage" option now takes a boolean value. Setting it to "true" has the same effect as "if_enabled" had before. The "on_request" behavior is no longer offered, as it made the option (and its documentation) overly complex. --- src/mod_mam.erl | 62 ++++++++++++++----------------------------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 7e1460695..f9a77eff2 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -111,15 +111,12 @@ start(Host, Opts) -> ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE, remove_user, 50), case gen_mod:get_opt(assume_mam_usage, Opts, - fun(if_enabled) -> if_enabled; - (on_request) -> on_request; - (never) -> never - end, never) of - never -> - ok; - _ -> + fun(B) when is_boolean(B) -> B end, false) of + true -> ejabberd_hooks:add(message_is_archived, Host, ?MODULE, - message_is_archived, 50) + message_is_archived, 50); + false -> + ok end, ejabberd_commands:register_commands(get_commands_spec()), ok. @@ -164,15 +161,12 @@ stop(Host) -> ejabberd_hooks:delete(anonymous_purge_hook, Host, ?MODULE, remove_user, 50), case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage, - fun(if_enabled) -> if_enabled; - (on_request) -> on_request; - (never) -> never - end, never) of - never -> - ok; - _ -> + fun(B) when is_boolean(B) -> B end, false) of + true -> ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, - message_is_archived, 50) + message_is_archived, 50); + false -> + ok end, ejabberd_commands:unregister_commands(get_commands_spec()), ok. @@ -381,32 +375,13 @@ message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) -> true; message_is_archived(false, C2SState, Peer, #jid{luser = LUser, lserver = LServer}, Pkt) -> - Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage, - fun(if_enabled) -> if_enabled; - (on_request) -> on_request; - (never) -> never - end, never) of - if_enabled -> - case get_prefs(LUser, LServer) of - #archive_prefs{} = P -> - {ok, P}; - error -> - error - end; - on_request -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - cache_tab:lookup(archive_prefs, {LUser, LServer}, - fun() -> - Mod:get_prefs(LUser, LServer) - end); - never -> - error - end, - case Res of - {ok, Prefs} -> + case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage, + fun(B) when is_boolean(B) -> B end, false) of + true -> should_archive(strip_my_archived_tag(Pkt, LServer), LServer) - andalso should_archive_peer(C2SState, Prefs, Peer); - error -> + andalso should_archive_peer(C2SState, get_prefs(LUser, LServer), + Peer); + false -> false end. @@ -1088,10 +1063,7 @@ get_commands_spec() -> result = {res, rescode}}]. mod_opt_type(assume_mam_usage) -> - fun(if_enabled) -> if_enabled; - (on_request) -> on_request; - (never) -> never - end; + fun (B) when is_boolean(B) -> B end; mod_opt_type(cache_life_time) -> fun (I) when is_integer(I), I > 0 -> I end; mod_opt_type(cache_size) -> From d969e917c6b5dec8f0da300439d6f4153ccb3ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Fri, 5 Aug 2016 13:03:22 +0200 Subject: [PATCH 073/179] Use newer samerlib/moka --- mix.lock | 4 ++-- rebar.config | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index 467f142ba..8da9063f8 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, - "moka": {:git, "https://github.com/processone/moka.git", "768efea96443c57125e6247dbebee687f17be149", [tag: "1.0.5b"]}, + "moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, @@ -23,7 +23,7 @@ "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, - "samerlib": {:git, "https://github.com/processone/samerlib", "9158f65d18ec63f8b409543b6fb46dd5fce46160", [tag: "0.8.0b"]}, + "samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}} diff --git a/rebar.config b/rebar.config index 693f4f58b..31745ec4c 100644 --- a/rebar.config +++ b/rebar.config @@ -48,7 +48,7 @@ {if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck", {tag, "0.8.4"}}}}, {if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git", - {tag, "1.0.5b"}}}}, + {tag, "1.0.5c"}}}}, {if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.0.8"}}}}]}. From c2ef55a0757cdaefdf60312f2c6f501b60d22d70 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 5 Aug 2016 20:09:52 +0200 Subject: [PATCH 074/179] Cosmetic change: Set CSI state 'active' on resume The CSI state is always set to 'active' when a stream management session is resumed; so there's no need to apply the CSI state of the old c2s process, first. --- src/ejabberd_c2s.erl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 74b78512a..ddbe56023 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2800,8 +2800,7 @@ handle_resume(StateData, Attrs) -> #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, AttrXmlns}], children = []}), - FlushedState = csi_flush_queue(NewState), - NewStateData = FlushedState#state{csi_state = active}, + NewStateData = csi_flush_queue(NewState), ?INFO_MSG("Resumed session for ~s", [jid:to_string(NewStateData#state.jid)]), {ok, NewStateData}; @@ -3048,13 +3047,13 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> pres_timestamp = OldStateData#state.pres_timestamp, privacy_list = OldStateData#state.privacy_list, aux_fields = OldStateData#state.aux_fields, - csi_state = OldStateData#state.csi_state, mgmt_xmlns = OldStateData#state.mgmt_xmlns, mgmt_queue = OldStateData#state.mgmt_queue, mgmt_timeout = OldStateData#state.mgmt_timeout, mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in, mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out, - mgmt_state = active}}; + mgmt_state = active, + csi_state = active}}; {error, Msg} -> {error, Msg}; _ -> From 91e26fbf7a14d85105f44d93874f20b170f11bfe Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 5 Aug 2016 23:47:18 +0200 Subject: [PATCH 075/179] Add user's JID to CSI hook arguments Add the JID of the CSI user to the arguments of the 'csi_filter_stanza' and 'csi_flush_queue' hooks. --- src/ejabberd_c2s.erl | 12 ++++++----- src/mod_client_state.erl | 45 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index ddbe56023..270ef1dc5 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -3080,20 +3080,22 @@ add_resent_delay_info(#state{server = From}, El, Time) -> %%% XEP-0352 %%%---------------------------------------------------------------------- -csi_filter_stanza(#state{csi_state = CsiState, server = Server} = StateData, - Stanza) -> +csi_filter_stanza(#state{csi_state = CsiState, jid = JID, server = Server} = + StateData, Stanza) -> {StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_filter_stanza, Server, {StateData, [Stanza]}, - [Server, Stanza]), + [Server, JID, Stanza]), StateData2 = lists:foldl(fun(CurStanza, AccState) -> send_stanza(AccState, CurStanza) end, StateData1#state{csi_state = active}, Stanzas), StateData2#state{csi_state = CsiState}. -csi_flush_queue(#state{csi_state = CsiState, server = Server} = StateData) -> +csi_flush_queue(#state{csi_state = CsiState, jid = JID, server = Server} = + StateData) -> {StateData1, Stanzas} = ejabberd_hooks:run_fold(csi_flush_queue, Server, - {StateData, []}, [Server]), + {StateData, []}, + [Server, JID]), StateData2 = lists:foldl(fun(CurStanza, AccState) -> send_stanza(AccState, CurStanza) end, StateData1#state{csi_state = active}, diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index 036175cce..da870f8ef 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -34,8 +34,8 @@ -export([start/2, stop/1, mod_opt_type/1, depends/2]). %% ejabberd_hooks callbacks. --export([filter_presence/3, filter_chat_states/3, filter_pep/3, filter_other/3, - flush_queue/2, add_stream_feature/2]). +-export([filter_presence/4, filter_chat_states/4, filter_pep/4, filter_other/4, + flush_queue/3, add_stream_feature/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -151,69 +151,70 @@ depends(_Host, _Opts) -> %% ejabberd_hooks callbacks. %%-------------------------------------------------------------------- --spec filter_presence({term(), [xmlel()]}, binary(), xmlel()) +-spec filter_presence({term(), [xmlel()]}, binary(), jid(), xmlel()) -> {term(), [xmlel()]} | {stop, {term(), [xmlel()]}}. -filter_presence({C2SState, _OutStanzas} = Acc, Host, +filter_presence({C2SState, _OutStanzas} = Acc, Host, To, #xmlel{name = <<"presence">>, attrs = Attrs} = Stanza) -> case fxml:get_attr(<<"type">>, Attrs) of {value, Type} when Type /= <<"unavailable">> -> Acc; _ -> - ?DEBUG("Got availability presence stanza", []), + ?DEBUG("Got availability presence stanza for ~s", + [jid:to_string(To)]), queue_add(presence, Stanza, Host, C2SState) end; -filter_presence(Acc, _Host, _Stanza) -> Acc. +filter_presence(Acc, _Host, _To, _Stanza) -> Acc. --spec filter_chat_states({term(), [xmlel()]}, binary(), xmlel()) +-spec filter_chat_states({term(), [xmlel()]}, binary(), jid(), xmlel()) -> {term(), [xmlel()]} | {stop, {term(), [xmlel()]}}. -filter_chat_states({C2SState, _OutStanzas} = Acc, Host, +filter_chat_states({C2SState, _OutStanzas} = Acc, Host, To, #xmlel{name = <<"message">>} = Stanza) -> case jlib:is_standalone_chat_state(Stanza) of true -> From = fxml:get_tag_attr_s(<<"from">>, Stanza), - To = fxml:get_tag_attr_s(<<"to">>, Stanza), - case {jid:from_string(From), jid:from_string(To)} of + case {jid:from_string(From), To} of {#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}} -> %% Don't queue (carbon copies of) chat states from other %% resources, as they might be used to sync the state of %% conversations across clients. Acc; _ -> - ?DEBUG("Got standalone chat state notification", []), + ?DEBUG("Got standalone chat state notification for ~s", + [jid:to_string(To)]), queue_add(chatstate, Stanza, Host, C2SState) end; false -> Acc end; -filter_chat_states(Acc, _Host, _Stanza) -> Acc. +filter_chat_states(Acc, _Host, _To, _Stanza) -> Acc. --spec filter_pep({term(), [xmlel()]}, binary(), xmlel()) +-spec filter_pep({term(), [xmlel()]}, binary(), jid(), xmlel()) -> {term(), [xmlel()]} | {stop, {term(), [xmlel()]}}. -filter_pep({C2SState, _OutStanzas} = Acc, Host, +filter_pep({C2SState, _OutStanzas} = Acc, Host, To, #xmlel{name = <<"message">>} = Stanza) -> case get_pep_node(Stanza) of {value, Node} -> - ?DEBUG("Got PEP notification", []), + ?DEBUG("Got PEP notification for ~s", [jid:to_string(To)]), queue_add({pep, Node}, Stanza, Host, C2SState); false -> Acc end; -filter_pep(Acc, _Host, _Stanza) -> Acc. +filter_pep(Acc, _Host, _To, _Stanza) -> Acc. --spec filter_other({term(), [xmlel()]}, binary(), xmlel()) +-spec filter_other({term(), [xmlel()]}, binary(), jid(), xmlel()) -> {stop, {term(), [xmlel()]}}. -filter_other({C2SState, _OutStanzas}, Host, Stanza) -> - ?DEBUG("Won't add stanza to CSI queue", []), +filter_other({C2SState, _OutStanzas}, Host, To, Stanza) -> + ?DEBUG("Won't add stanza for ~s to CSI queue", [jid:to_string(To)]), queue_take(Stanza, Host, C2SState). --spec flush_queue({term(), [xmlel()]}, binary()) -> {term(), [xmlel()]}. +-spec flush_queue({term(), [xmlel()]}, binary(), jid()) -> {term(), [xmlel()]}. -flush_queue({C2SState, _OutStanzas}, Host) -> - ?DEBUG("Going to flush CSI queue", []), +flush_queue({C2SState, _OutStanzas}, Host, JID) -> + ?DEBUG("Going to flush CSI queue of ~s", [jid:to_string(JID)]), Queue = get_queue(C2SState), NewState = set_queue([], C2SState), {NewState, get_stanzas(Queue, Host)}. From b62aa3d2dcf3298f221f01b8ffb02f839aabfb8b Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sat, 6 Aug 2016 13:36:27 +0200 Subject: [PATCH 076/179] mod_client_state: Let other modules filter stanzas Don't stop execution of the 'csi_filter_stanza' hook if mod_client_state won't queue the stanza. --- src/mod_client_state.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index da870f8ef..9d37d4f5b 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -205,7 +205,7 @@ filter_pep({C2SState, _OutStanzas} = Acc, Host, To, filter_pep(Acc, _Host, _To, _Stanza) -> Acc. -spec filter_other({term(), [xmlel()]}, binary(), jid(), xmlel()) - -> {stop, {term(), [xmlel()]}}. + -> {term(), [xmlel()]}. filter_other({C2SState, _OutStanzas}, Host, To, Stanza) -> ?DEBUG("Won't add stanza for ~s to CSI queue", [jid:to_string(To)]), @@ -250,7 +250,7 @@ queue_add(Type, Stanza, Host, C2SState) -> {stop, {NewState, []}} end. --spec queue_take(xmlel(), binary(), term()) -> {stop, {term(), [xmlel()]}}. +-spec queue_take(xmlel(), binary(), term()) -> {term(), [xmlel()]}. queue_take(Stanza, Host, C2SState) -> From = fxml:get_tag_attr_s(<<"from">>, Stanza), @@ -260,7 +260,7 @@ queue_take(Stanza, Host, C2SState) -> U == LUser andalso S == LServer end, get_queue(C2SState)), NewState = set_queue(Rest, C2SState), - {stop, {NewState, get_stanzas(Selected, Host) ++ [Stanza]}}. + {NewState, get_stanzas(Selected, Host) ++ [Stanza]}. -spec set_queue(csi_queue(), term()) -> term(). From a0c8012c66cce3f01562edc4d5708fdbb6a7f76f Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 7 Aug 2016 18:24:08 +0200 Subject: [PATCH 077/179] Do not force command line to pass a credentials --- src/ejabberd_commands.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index a8b3e25ab..7110410f1 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -537,6 +537,9 @@ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> execute_check_policy( _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) -> do_execute_command(Command, Arguments); +execute_check_policy( + noauth, _JID, Command, Arguments) -> + do_execute_command(Command, Arguments); execute_check_policy( _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) -> do_execute_command(Command, Arguments); @@ -546,9 +549,6 @@ execute_check_policy( execute_check_policy( admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> execute_check_access(JID, Command, Arguments); -execute_check_policy( - noauth, _JID, #ejabberd_commands{policy = user} = Command, Arguments) -> - do_execute_command(Command, Arguments); execute_check_policy( {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) -> execute_check_access(JID, Command, [User, Server | Arguments]). From efc744092b08c38aaa9952d7763f15979f436837 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 7 Aug 2016 18:27:16 +0200 Subject: [PATCH 078/179] We do not force yet access rules check on register command --- src/ejabberd_admin.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 615459f77..ae2fac3e1 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -131,7 +131,6 @@ get_commands_spec() -> #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", policy = admin, - access = [{mod_register, access, configure}], module = ?MODULE, function = register, args = [{user, binary}, {host, binary}, {password, binary}], result = {res, restuple}}, From 9c6ee60f1a08be64d25497fd79e688e46395fe2c Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 7 Aug 2016 18:28:14 +0200 Subject: [PATCH 079/179] Update moka dependency --- mix.exs | 2 +- mix.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mix.exs b/mix.exs index 7453ea473..2c83a319b 100644 --- a/mix.exs +++ b/mix.exs @@ -63,7 +63,7 @@ defmodule Ejabberd.Mixfile do # version 3.20: {:relx, "~> 3.19.0", only: :dev}, {:meck, "~> 0.8.4", only: :test}, - {:moka, github: "processone/moka", tag: "1.0.5b", only: :test}] + {:moka, github: "processone/moka", tag: "1.0.5c", only: :test}] end defp package do diff --git a/mix.lock b/mix.lock index 8da9063f8..17be772ea 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,17 @@ %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, - "cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, - "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:stun, "1.0.6", [hex: :stun, optional: false]}]}, + "esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, - "fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "fast_xml": {:hex, :fast_xml, "1.1.14", "23d4de66e645bca1d8a557444e83062cc42f235d7a7e2d4072d525bac3986f04", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "fast_yaml": {:hex, :fast_yaml, "1.0.5", "a67772c75abb84181c6c9899e1f988b08ac214ea0d764ff1f9889bb7e27f74d4", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, + "fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, + "fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, "goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []}, - "iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, + "iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, @@ -19,11 +19,12 @@ "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, - "p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []}, + "p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []}, "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, + "pc": {:hex, :pc, "1.2.0", "5e07731d1f8bf997a8d0271510983e570f910b42cd59bf612e664ad6dc35742e", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, "samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, - "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}, - "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}} + "stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, + "stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}} From c4b14d045a5e0dc88738351cd38954607af7ff62 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 7 Aug 2016 18:31:55 +0200 Subject: [PATCH 080/179] Update to released version --- rebar.config | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rebar.config b/rebar.config index 31745ec4c..ab5562858 100644 --- a/rebar.config +++ b/rebar.config @@ -9,13 +9,13 @@ {deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.2.1"}}}, {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.5"}}}, - {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.3"}}}, - {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.6"}}}, - {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.5"}}}, - {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.14"}}}, - {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.6"}}}, - {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.7"}}}, - {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.5"}}}, + {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.4"}}}, + {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.7"}}}, + {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.6"}}}, + {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.15"}}}, + {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.7"}}}, + {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.8"}}}, + {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}}, {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}}, {p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}}, @@ -44,7 +44,7 @@ {if_var_true, elixir, {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}, {if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv", - {tag, "1.0.1"}}}}, + {tag, "1.0.2"}}}}, {if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck", {tag, "0.8.4"}}}}, {if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git", From 1fc58ace2f139ea01c13bac0a8f30735d9f8af29 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 9 Aug 2016 13:36:43 +0300 Subject: [PATCH 081/179] Add commands for MUC subscriptions management --- src/mod_muc_admin.erl | 71 ++++++++++++++++++++++++++++++++++++++++- src/mod_muc_room.erl | 74 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 0b5e79f60..a07ac0bc9 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -20,6 +20,7 @@ change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, web_menu_main/2, web_page_main/2, web_menu_host/3, + subscribe_room/4, unsubscribe_room/2, web_page_host/3, mod_opt_type/1, get_commands_spec/0]). -include("ejabberd.hrl"). @@ -151,7 +152,17 @@ get_commands_spec() -> {value, string} ]}} }}}, - + #ejabberd_commands{name = subscribe_room, tags = [muc_room], + desc = "Subscribe to a MUC conference", + module = ?MODULE, function = subscribe_room, + args = [{user, binary}, {nick, binary}, {room, binary}, + {nodes, binary}], + result = {list, {node, string}}}, + #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], + desc = "Unsubscribe from a MUC conference", + module = ?MODULE, function = unsubscribe_room, + args = [{user, binary}, {room, binary}], + result = {res, rescode}}, #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], desc = "Change an affiliation in a MUC room", module = ?MODULE, function = set_room_affiliation, @@ -884,6 +895,64 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> error end. +%%% +%%% MUC Subscription +%%% + +subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> -> + throw({error, "Nickname must be set"}); +subscribe_room(User, Nick, Room, Nodes) -> + NodeList = re:split(Nodes, "\\h*,\\h*"), + case jid:from_string(Room) of + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + case jid:from_string(User) of + error -> + throw({error, "Malformed user JID"}); + JID -> + UserJID = jid:replace_resource(JID, Nick), + case get_room_pid(Name, Host) of + Pid when is_pid(Pid) -> + case gen_fsm:sync_send_all_state_event( + Pid, + {muc_subscribe, UserJID, Nick, NodeList}) of + {ok, SubscribedNodes} -> + SubscribedNodes; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + _ -> + throw({error, "The room does not exist"}) + end + end; + _ -> + throw({error, "Malformed room JID"}) + end. + +unsubscribe_room(User, Room) -> + case jid:from_string(Room) of + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + case jid:from_string(User) of + error -> + throw({error, "Malformed user JID"}); + UserJID -> + case get_room_pid(Name, Host) of + Pid when is_pid(Pid) -> + case gen_fsm:sync_send_all_state_event( + Pid, + {muc_unsubscribe, UserJID}) of + ok -> + ok; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + _ -> + throw({error, "The room does not exist"}) + end + end; + _ -> + throw({error, "Malformed room JID"}) + end. + make_opts(StateData) -> Config = StateData#state.config, [ diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 773953c4a..e5ed4cc68 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -749,6 +749,60 @@ handle_sync_event({change_state, NewStateData}, _From, handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> NSD = process_item_change(Item, StateData, UJID), {reply, {ok, NSD}, StateName, NSD}; +handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, + StateName, StateData) -> + SubEl = #xmlel{name = <<"subscribe">>, + attrs = [{<<"xmlns">>, ?NS_MUCSUB}, {<<"nick">>, Nick}], + children = [#xmlel{name = <<"event">>, + attrs = [{<<"node">>, Node}]} + || Node <- Nodes]}, + IQ = #iq{type = set, id = randoms:get_string(), + xmlns = ?NS_MUCSUB, sub_el = SubEl}, + Packet = jlib:iq_to_xml(IQ#iq{sub_el = [SubEl]}), + Config = StateData#state.config, + CaptchaRequired = Config#config.captcha_protected, + PasswordProtected = Config#config.password_protected, + TmpConfig = Config#config{captcha_protected = false, + password_protected = false}, + TmpState = StateData#state{config = TmpConfig}, + case process_iq_mucsub(From, Packet, IQ, TmpState) of + {result, _, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected}, + {reply, {ok, get_subscription_nodes(Packet)}, StateName, + NewState#state{config = NewConfig}}; + {ignore, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected}, + {reply, {error, <<"Requrest is ignored">>}, + NewState#state{config = NewConfig}}; + {error, Err, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected}, + {reply, {error, get_error_text(Err)}, StateName, + NewState#state{config = NewConfig}}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} + end; +handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) -> + SubEl = #xmlel{name = <<"unsubscribe">>, + attrs = [{<<"xmlns">>, ?NS_MUCSUB}]}, + IQ = #iq{type = set, id = randoms:get_string(), + xmlns = ?NS_MUCSUB, sub_el = SubEl}, + Packet = jlib:iq_to_xml(IQ), + case process_iq_mucsub(From, Packet, IQ, StateData) of + {result, _, NewState} -> + {reply, ok, StateName, NewState}; + {ignore, NewState} -> + {reply, {error, <<"Requrest is ignored">>}, NewState}; + {error, Err, NewState} -> + {reply, {error, get_error_text(Err)}, StateName, NewState}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} + end; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. @@ -1346,6 +1400,14 @@ get_error_condition2(Packet) -> <- EEls], {condition, Condition}. +get_error_text(Error) -> + case fxml:get_subtag_with_xmlns(Error, <<"text">>, ?NS_STANZAS) of + #xmlel{} = Tag -> + fxml:get_tag_cdata(Tag); + false -> + <<"">> + end. + make_reason(Packet, From, StateData, Reason1) -> {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users), Condition = get_error_condition(Packet), @@ -4608,6 +4670,18 @@ process_iq_mucsub(From, _Packet, NewStateData = remove_subscription(From, User, StateData), store_room(NewStateData), {result, [], NewStateData}; + error when From#jid.lresource == <<"">> -> + {LUser, LServer, _} = LJID, + NewStateData = + dict:fold( + fun({U, S, _}, #user{jid = J, is_subscriber = true} = User, + AccState) when U == LUser, S == LServer -> + remove_subscription(J, User, AccState); + (_, _, AccState) -> + AccState + end, StateData, StateData#state.users), + store_room(NewStateData), + {result, [], NewStateData}; _ -> {result, [], StateData} end; From e63fe5c2160088cf1585fc14e32fc13853888f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 10 Aug 2016 11:16:20 +0200 Subject: [PATCH 082/179] Fix result type of subscribe_room command --- src/mod_muc_admin.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index a07ac0bc9..692dedf5f 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -157,7 +157,7 @@ get_commands_spec() -> module = ?MODULE, function = subscribe_room, args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], - result = {list, {node, string}}}, + result = {nodes, {list, {node, string}}}}, #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], desc = "Unsubscribe from a MUC conference", module = ?MODULE, function = unsubscribe_room, From 50596dc4d347fc76f01f7b063b42c85296fad55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Tue, 9 Aug 2016 19:22:51 +0200 Subject: [PATCH 083/179] Provide proper args_desc in oauth_issue_token command --- src/ejabberd_oauth.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index ca0693645..36c679cb6 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -102,7 +102,9 @@ get_commands_spec() -> args = [{jid, string},{ttl, integer}, {scopes, string}], policy = restricted, args_example = ["user@server.com", "connected_users_number;muc_online_rooms"], - args_desc = ["List of scopes to allow, separated by ';'"], + args_desc = ["Jid for which issue token", + "Time to live of generated token in seconds", + "List of scopes to allow, separated by ';'"], result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}} }, #ejabberd_commands{name = oauth_list_tokens, tags = [oauth], From ffba664f2cfa0965c05d572e14c3524550bd6742 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Thu, 11 Aug 2016 17:13:20 +0300 Subject: [PATCH 084/179] Add a requirement for full JID in subscribe_room command --- src/mod_muc_admin.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 692dedf5f..e1d48cdab 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -908,8 +908,9 @@ subscribe_room(User, Nick, Room, Nodes) -> case jid:from_string(User) of error -> throw({error, "Malformed user JID"}); - JID -> - UserJID = jid:replace_resource(JID, Nick), + #jid{lresource = <<"">>} -> + throw({error, "User's JID should have a resource"}); + UserJID -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> case gen_fsm:sync_send_all_state_event( From 28dde294e50ab99c8a0350bbf1073fb7bb96d4d1 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 12 Aug 2016 20:38:17 +0200 Subject: [PATCH 085/179] mod_mam: Don't store messages of type "headline" XEP-0313 says: "a server SHOULD include in a user archive all of the messages a user sends or receives of type 'normal' or 'chat' that contain a element." --- src/mod_mam.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index f9a77eff2..4ee998793 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -560,6 +560,8 @@ should_archive(#xmlel{name = <<"message">>} = Pkt, LServer) -> false; <<"groupchat">> -> false; + <<"headline">> -> + false; _ -> case is_resent(Pkt, LServer) of true -> From bf9d6b55342dbb013bba612d4855271b582ae0ff Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 12 Aug 2016 21:13:10 +0200 Subject: [PATCH 086/179] Honor hint for any non-"error" message XEP-0334 says: "A message containing the hint that is not of type 'error' SHOULD be stored by the entity." --- src/mod_mam.erl | 42 +++++++++++++++++----------------- src/mod_offline.erl | 55 +++++++++++++++++++++++---------------------- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 4ee998793..6ea757223 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -555,31 +555,29 @@ parse_query_v0_2(Query) -> end, Query#xmlel.children). should_archive(#xmlel{name = <<"message">>} = Pkt, LServer) -> - case fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of - <<"error">> -> + case is_resent(Pkt, LServer) of + true -> false; - <<"groupchat">> -> - false; - <<"headline">> -> - false; - _ -> - case is_resent(Pkt, LServer) of - true -> + false -> + case {check_store_hint(Pkt), + fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs)} of + {_Hint, <<"error">>} -> false; - false -> - case check_store_hint(Pkt) of - store -> - true; - no_store -> + {store, _Type} -> + true; + {no_store, _Type} -> + false; + {none, <<"groupchat">>} -> + false; + {none, <<"headline">>} -> + false; + {none, _Type} -> + case fxml:get_subtag_cdata(Pkt, <<"body">>) of + <<>> -> + %% Empty body false; - none -> - case fxml:get_subtag_cdata(Pkt, <<"body">>) of - <<>> -> - %% Empty body - false; - _ -> - true - end + _ -> + true end end end; diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 799605c69..87a136853 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -437,35 +437,36 @@ remove_msg_by_node(To, Seq) -> end. need_to_store(LServer, Packet) -> - Type = fxml:get_tag_attr_s(<<"type">>, Packet), - if (Type /= <<"error">>) and (Type /= <<"groupchat">>) - and (Type /= <<"headline">>) -> - case has_offline_tag(Packet) of - false -> - case check_store_hint(Packet) of - store -> + case has_offline_tag(Packet) of + false -> + case {check_store_hint(Packet), + fxml:get_tag_attr_s(<<"type">>, Packet)} of + {_Hint, <<"error">>} -> + false; + {store, _Type} -> + true; + {no_store, _Type} -> + false; + {none, <<"groupchat">>} -> + false; + {none, <<"headline">>} -> + false; + {none, _Type} -> + case gen_mod:get_module_opt( + LServer, ?MODULE, store_empty_body, + fun(V) when is_boolean(V) -> V; + (unless_chat_state) -> unless_chat_state + end, + unless_chat_state) of + true -> true; - no_store -> - false; - none -> - case gen_mod:get_module_opt( - LServer, ?MODULE, store_empty_body, - fun(V) when is_boolean(V) -> V; - (unless_chat_state) -> unless_chat_state - end, - unless_chat_state) of - false -> - fxml:get_subtag(Packet, <<"body">>) /= false; - unless_chat_state -> - not jlib:is_standalone_chat_state(Packet); - true -> - true - end - end; - true -> - false + false -> + fxml:get_subtag(Packet, <<"body">>) /= false; + unless_chat_state -> + not jlib:is_standalone_chat_state(Packet) + end end; - true -> + true -> false end. From 4ee8af633bc1d14059ae22a591e7c7db7098c6e4 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sat, 13 Aug 2016 00:07:27 +0200 Subject: [PATCH 087/179] Store announcements for offline users Add a hint to announcements (unless they are explicitly sent to online users). Without that hint, announcements weren't delivered to offline users, since they are sent as messages of type "headline". --- src/mod_announce.erl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 52ff2de92..d74c46bf9 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -696,7 +696,7 @@ announce_all(From, To, Packet) -> lists:foreach( fun({User, Server}) -> Dest = jid:make(User, Server, <<>>), - ejabberd_router:route(Local, Dest, Packet) + ejabberd_router:route(Local, Dest, add_store_hint(Packet)) end, ejabberd_auth:get_vh_registered_users(Host)) end. @@ -713,7 +713,7 @@ announce_all_hosts_all(From, To, Packet) -> lists:foreach( fun({User, Server}) -> Dest = jid:make(User, Server, <<>>), - ejabberd_router:route(Local, Dest, Packet) + ejabberd_router:route(Local, Dest, add_store_hint(Packet)) end, ejabberd_auth:dirty_get_registered_users()) end. @@ -899,7 +899,7 @@ send_announcement_to_all(Host, SubjectS, BodyS) -> lists:foreach( fun({U, S, R}) -> Dest = jid:make(U, S, R), - ejabberd_router:route(Local, Dest, Packet) + ejabberd_router:route(Local, Dest, add_store_hint(Packet)) end, Sessions). -spec get_access(global | binary()) -> atom(). @@ -909,6 +909,12 @@ get_access(Host) -> fun(A) -> A end, none). +-spec add_store_hint(xmlel()) -> xmlel(). + +add_store_hint(El) -> + Hint = #xmlel{name = <<"store">>, attrs = [{<<"xmlns">>, ?NS_HINTS}]}, + fxml:append_subtags(El, [Hint]). + %%------------------------------------------------------------------------- export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), From 0ab08f4eeb8407e5d97c86f1a0b2da083764e2b2 Mon Sep 17 00:00:00 2001 From: Badlop Date: Sun, 14 Aug 2016 20:35:25 +0200 Subject: [PATCH 088/179] Don't delete ejabberd_commands table, it's problematic in cluster (#1210) --- src/ejabberd_commands.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 7110410f1..d5649b2d7 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -274,7 +274,6 @@ get_commands_spec() -> args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], result_example = ok}]. init() -> - mnesia:delete_table(ejabberd_commands), mnesia:create_table(ejabberd_commands, [{ram_copies, [node()]}, {local_content, true}, From 877d0752e279bdfcc7458636f6a3b0263efc317a Mon Sep 17 00:00:00 2001 From: HAMANO Tsukasa Date: Mon, 15 Aug 2016 17:51:06 +0900 Subject: [PATCH 089/179] fix clean redis table --- src/ejabberd_sm_redis.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl index d25f777e3..2bfd2d8d1 100644 --- a/src/ejabberd_sm_redis.erl +++ b/src/ejabberd_sm_redis.erl @@ -145,7 +145,10 @@ clean_table() -> {_, SID} = binary_to_term(USSIDKey), node(element(2, SID)) == node() end, Vals), - Q1 = ["HDEL", ServKey | Vals1], + Q1 = case Vals1 of + [] -> []; + _ -> ["HDEL", ServKey | Vals1] + end, Q2 = lists:map( fun(USSIDKey) -> {US, SID} = binary_to_term(USSIDKey), @@ -153,7 +156,7 @@ clean_table() -> SIDKey = sid_to_key(SID), ["HDEL", USKey, SIDKey] end, Vals1), - Res = ejabberd_redis:qp([Q1|Q2]), + Res = ejabberd_redis:qp(lists:delete([], [Q1|Q2])), case lists:filter( fun({ok, _}) -> false; (_) -> true From 5b4f347da8d3931d62ce4f6d94b4d71e62f1ab34 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 15 Aug 2016 15:53:35 +0200 Subject: [PATCH 090/179] Support sql backend in mod_shared_roster commands (#1244) --- src/mod_admin_extra.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 8e9ea007f..2bb436f31 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -1315,11 +1315,11 @@ srg_create(Group, Host, Name, Description, Display) -> Opts = [{name, Name}, {displayed_groups, DisplayList}, {description, Description}], - {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts), + {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts), ok. srg_delete(Group, Host) -> - {atomic, ok} = mod_shared_roster:delete_group(Host, Group), + {atomic, _} = mod_shared_roster:delete_group(Host, Group), ok. srg_list(Host) -> @@ -1342,11 +1342,11 @@ srg_get_members(Group, Host) -> || {MUser, MServer} <- Members]. srg_user_add(User, Host, Group, GroupHost) -> - {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + {atomic, _} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), ok. srg_user_del(User, Host, Group, GroupHost) -> - {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + {atomic, _} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), ok. From e7217e63207ebf311042dceed6e1a1be97b46ea5 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 15 Aug 2016 20:24:43 +0200 Subject: [PATCH 091/179] Add functions to get/set some c2s state elements --- src/ejabberd_c2s.erl | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 270ef1dc5..02e873be4 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -52,6 +52,11 @@ set_aux_field/3, del_aux_field/2, get_subscription/2, + get_queued_stanzas/1, + get_csi_state/1, + set_csi_state/2, + get_resume_timeout/1, + set_resume_timeout/2, send_filtered/5, broadcast/4, get_subscribed/1, @@ -244,6 +249,27 @@ get_subscription(LFrom, StateData) -> true -> none end. +get_queued_stanzas(#state{mgmt_queue = Queue} = StateData) -> + lists:map(fun({_N, Time, El}) -> + add_resent_delay_info(StateData, El, Time) + end, queue:to_list(Queue)). + +get_csi_state(#state{csi_state = CsiState}) -> + CsiState. + +set_csi_state(#state{} = StateData, CsiState) -> + StateData#state{csi_state = CsiState}; +set_csi_state(FsmRef, CsiState) -> + FsmRef ! {set_csi_state, CsiState}. + +get_resume_timeout(#state{mgmt_timeout = Timeout}) -> + Timeout. + +set_resume_timeout(#state{} = StateData, Timeout) -> + StateData#state{mgmt_timeout = Timeout}; +set_resume_timeout(FsmRef, Timeout) -> + FsmRef ! {set_resume_timeout, Timeout}. + send_filtered(FsmRef, Feature, From, To, Packet) -> FsmRef ! {send_filtered, Feature, From, To, Packet}. @@ -1743,6 +1769,10 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> From, jid:make(USR), Packet) end, lists:usort(Recipients)), fsm_next_state(StateName, StateData); +handle_info({set_csi_state, CsiState}, StateName, StateData) -> + fsm_next_state(StateName, StateData#state{csi_state = CsiState}); +handle_info({set_resume_timeout, Timeout}, StateName, StateData) -> + fsm_next_state(StateName, StateData#state{mgmt_timeout = Timeout}); handle_info(dont_ask_offline, StateName, StateData) -> fsm_next_state(StateName, StateData#state{ask_offline = false}); handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) -> From 4d19fb518f830a8c65ecd6aca0ec9c75a5d66535 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 15 Aug 2016 21:49:58 +0200 Subject: [PATCH 092/179] ejabberd_c2s: Add XEP-0198 resumption hooks The new 'c2s_session_pending' and 'c2s_session_resumed' hooks are invoked when a stream management session is pending and resumed, respectively. --- src/ejabberd_c2s.erl | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 02e873be4..73cc57247 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2495,13 +2495,19 @@ fsm_next_state(session_established, StateData) -> ?C2S_HIBERNATE_TIMEOUT}; fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) -> {stop, normal, StateData}; -fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} = - StateData) -> +fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined, + sid = SID, jid = JID, ip = IP, + conn = Conn, auth_module = AuthModule, + server = Host} = StateData) -> ?INFO_MSG("Waiting for resumption of stream for ~s", - [jid:to_string(StateData#state.jid)]), + [jid:to_string(JID)]), + Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], + NewStateData = ejabberd_hooks:run_fold(c2s_session_pending, Host, StateData, + [SID, JID, Info]), {next_state, wait_for_resume, - StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()}, - StateData#state.mgmt_timeout}; + NewStateData#state{mgmt_state = pending, + mgmt_pending_since = os:timestamp()}, + NewStateData#state.mgmt_timeout}; fsm_next_state(wait_for_resume, StateData) -> Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since), Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1), @@ -2790,8 +2796,8 @@ handle_resume(StateData, Attrs) -> of {{value, PrevID}, H} when is_integer(H), H >= 0 -> case inherit_session_state(StateData, PrevID) of - {ok, InheritedState} -> - {ok, InheritedState, H}; + {ok, InheritedState, Info} -> + {ok, InheritedState, Info, H}; {error, Err, InH} -> {error, ?MGMT_ITEM_NOT_FOUND_H(Xmlns, InH), Err}; {error, Err} -> @@ -2810,7 +2816,7 @@ handle_resume(StateData, Attrs) -> <<"Invalid XMLNS">>} end, case R of - {ok, ResumedState, NumHandled} -> + {ok, ResumedState, ResumedInfo, NumHandled} -> NewState = check_h_attribute(ResumedState, NumHandled), AttrXmlns = NewState#state.mgmt_xmlns, AttrId = make_resume_id(NewState), @@ -2830,10 +2836,16 @@ handle_resume(StateData, Attrs) -> #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, AttrXmlns}], children = []}), - NewStateData = csi_flush_queue(NewState), + NewState1 = csi_flush_queue(NewState), + NewState2 = ejabberd_hooks:run_fold(c2s_session_resumed, + StateData#state.server, + NewState1, + [NewState1#state.sid, + NewState1#state.jid, + ResumedInfo]), ?INFO_MSG("Resumed session for ~s", - [jid:to_string(NewStateData#state.jid)]), - {ok, NewStateData}; + [jid:to_string(NewState2#state.jid)]), + {ok, NewState2}; {error, El, Msg} -> send_element(StateData, El), ?INFO_MSG("Cannot resume session for ~s@~s: ~s", @@ -3083,7 +3095,7 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in, mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out, mgmt_state = active, - csi_state = active}}; + csi_state = active}, Info}; {error, Msg} -> {error, Msg}; _ -> From 8821cf8b2785b8cd8df3fb32dde5dbfee26b53f4 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 15 Aug 2016 22:30:08 +0200 Subject: [PATCH 093/179] mod_offline: Add 'store_offline_message' hook The new 'store_offline_message' hook is invoked whenever an offline message is stored. --- src/mod_offline.erl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 87a136853..1d9417117 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -476,14 +476,22 @@ store_packet(From, To, Packet) -> case check_event(From, To, Packet) of true -> #jid{luser = LUser, lserver = LServer} = To, - TimeStamp = p1_time_compat:timestamp(), - #xmlel{children = Els} = Packet, - Expire = find_x_expire(TimeStamp, Els), - gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) ! - #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, expire = Expire, - from = From, to = To, packet = Packet}, - stop; + case ejabberd_hooks:run_fold(store_offline_message, LServer, + Packet, [From, To]) of + drop -> + ok; + NewPacket -> + TimeStamp = p1_time_compat:timestamp(), + #xmlel{children = Els} = NewPacket, + Expire = find_x_expire(TimeStamp, Els), + gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) ! + #offline_msg{us = {LUser, LServer}, + timestamp = TimeStamp, + expire = Expire, + from = From, to = To, + packet = NewPacket}, + stop + end; _ -> ok end; false -> ok From 20a510d87786155e9bfce068b99660d103dc3494 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 15 Aug 2016 23:28:36 +0200 Subject: [PATCH 094/179] mod_mam: Add 'store_mam_message' hook The new 'store_mam_message' hook is invoked whenever a MAM message is stored. --- src/mod_mam.erl | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 6ea757223..f6d3c8f1f 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -718,8 +718,14 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) -> case should_archive_peer(C2SState, Prefs, Peer) of true -> US = {LUser, LServer}, - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:store(Pkt, LServer, US, chat, Peer, <<"">>, Dir); + case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, + [LUser, LServer, Peer, chat, Dir]) of + drop -> + pass; + NewPkt -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store(NewPkt, LServer, US, chat, Peer, <<"">>, Dir) + end; false -> pass end. @@ -727,10 +733,16 @@ store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) -> store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> case should_archive_muc(Pkt) of true -> - LServer = MUCState#state.server_host, {U, S, _} = jid:tolower(RoomJID), - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:store(Pkt, LServer, {U, S}, groupchat, Peer, Nick, recv); + LServer = MUCState#state.server_host, + case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, + [U, S, Peer, groupchat, recv]) of + drop -> + pass; + NewPkt -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store(NewPkt, LServer, {U, S}, groupchat, Peer, Nick, recv) + end; false -> pass end. From 8af85d913fb1345d8130344405bfa6eefd2a8a0b Mon Sep 17 00:00:00 2001 From: Badlop Date: Tue, 16 Aug 2016 18:32:06 +0200 Subject: [PATCH 095/179] Erlang R17 has a Time limit in erlang:send_after (#1246) --- src/ejabberd_oauth.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 36c679cb6..4541190ad 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -66,7 +66,7 @@ %% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin %% (as it has access to ejabberd command line). --define(EXPIRE, 31536000). +-define(EXPIRE, 4294967). start() -> DBMod = get_db_backend(), From e13edff6aec93527539b084d1c744f87613ba6c1 Mon Sep 17 00:00:00 2001 From: Peter Lemenkov Date: Wed, 17 Aug 2016 18:29:24 +0300 Subject: [PATCH 096/179] Remove no longer necessary p1_xmlrpc Ths module was superceded by fast_xml in commit processone/ejabberd@dfc29ea03ca91e1eb5387d93612e2ac4b4b496da. So let's remove it entirely to avoid any confusion. Signed-off-by: Peter Lemenkov --- mix.exs | 3 +-- mix.lock | 1 - rebar.config | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 2c83a319b..8a5a2b88b 100644 --- a/mix.exs +++ b/mix.exs @@ -29,7 +29,7 @@ defmodule Ejabberd.Mixfile do included_applications: [:lager, :mnesia, :p1_utils, :cache_tab, :fast_tls, :stringprep, :fast_xml, :stun, :fast_yaml, :ezlib, :iconv, - :esip, :jiffy, :p1_oauth2, :p1_xmlrpc, :eredis, + :esip, :jiffy, :p1_oauth2, :eredis, :p1_mysql, :p1_pgsql, :sqlite3]] end @@ -51,7 +51,6 @@ defmodule Ejabberd.Mixfile do {:esip, "~> 1.0"}, {:jiffy, "~> 0.14.7"}, {:p1_oauth2, "~> 0.6.1"}, - {:p1_xmlrpc, "~> 1.15"}, {:p1_mysql, "~> 1.0"}, {:p1_pgsql, "~> 1.1"}, {:sqlite3, "~> 1.1"}, diff --git a/mix.lock b/mix.lock index 17be772ea..b2e2be3a0 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,6 @@ "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []}, - "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []}, "pc": {:hex, :pc, "1.2.0", "5e07731d1f8bf997a8d0271510983e570f910b42cd59bf612e664ad6dc35742e", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, diff --git a/rebar.config b/rebar.config index ab5562858..06a7d350a 100644 --- a/rebar.config +++ b/rebar.config @@ -18,7 +18,6 @@ {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}}, {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}}, - {p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}}, {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}}, {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.1"}}}}, From 23d9fb05923ed752147b53be48fffa6de8cf1b6d Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 22 Aug 2016 01:17:32 +0200 Subject: [PATCH 097/179] mod_muc_admin: Accept 'allow_subscription' option Allow for setting the new 'allow_subscription' option using the 'change_room_option' command. --- src/mod_muc_admin.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index e1d48cdab..76e90957e 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -800,6 +800,7 @@ change_option(Option, Value, Config) -> allow_private_messages -> Config#config{allow_private_messages = Value}; allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; allow_query_users -> Config#config{allow_query_users = Value}; + allow_subscription -> Config#config{allow_subscription = Value}; allow_user_invites -> Config#config{allow_user_invites = Value}; allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; allow_visitor_status -> Config#config{allow_visitor_status = Value}; From d5659735b301d1945705e44a0716d5a50cf6fffd Mon Sep 17 00:00:00 2001 From: Igor Manturov Jr Date: Tue, 23 Aug 2016 00:59:39 +0600 Subject: [PATCH 098/179] Fixed #1260 Stream Management feature for the websocket connections --- src/ejabberd_http_ws.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index e66cf33a5..24554a8cc 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -112,7 +112,7 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) -> %%% Internal init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> - SOpts = lists:filtermap(fun({stream_managment, _}) -> true; + SOpts = lists:filtermap(fun({stream_management, _}) -> true; ({max_ack_queue, _}) -> true; ({resume_timeout, _}) -> true; ({max_resume_timeout, _}) -> true; From f1afea223bbfde471cab67158d76d023eb1db887 Mon Sep 17 00:00:00 2001 From: Igor Manturov Jr Date: Tue, 23 Aug 2016 02:21:09 +0600 Subject: [PATCH 099/179] Fixed typo in Stream Management option name --- src/ejabberd_http_bind.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl index ea8cd792f..758c1cee5 100644 --- a/src/ejabberd_http_bind.erl +++ b/src/ejabberd_http_bind.erl @@ -338,7 +338,7 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, init([Sid, Key, IP, HOpts]) -> ?DEBUG("started: ~p", [{Sid, Key, IP}]), Opts1 = ejabberd_c2s_config:get_c2s_limits(), - SOpts = lists:filtermap(fun({stream_managment, _}) -> true; + SOpts = lists:filtermap(fun({stream_management, _}) -> true; ({max_ack_queue, _}) -> true; ({resume_timeout, _}) -> true; ({max_resume_timeout, _}) -> true; From 96a748d34f05f31c3b045a54f9d4ab0555e0f9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Mon, 5 Sep 2016 16:42:05 +0200 Subject: [PATCH 100/179] ejabberd_oauth requiere working cache_tab, initialize it before tests --- test/ejabberd_oauth_mock.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs index e6a34f65e..965bff1e6 100644 --- a/test/ejabberd_oauth_mock.exs +++ b/test/ejabberd_oauth_mock.exs @@ -26,7 +26,10 @@ defmodule EjabberdOauthMock do :mnesia.start :mnesia.create_table(:oauth_token, [ram_copies: [node], - attributes: [:oauth_token, :us, :scope, :expire]]) + attributes: [:oauth_token, :us, :scope, :expire]]) + :application.start(:cache_tab) + :cache_tab.new(:oauth_token, + [{:max_size, 1000}, {:life_time, 3600}]) end def get_token(user, domain, command, expiration \\ 3600) do From c770a54aac579d9a4fa39873a90cee775368a62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Mon, 5 Sep 2016 17:18:27 +0200 Subject: [PATCH 101/179] Clean ejabberd_commands before tests --- test/ejabberd_commands_mock_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 9d33d7573..785e74cd7 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -58,6 +58,7 @@ defmodule EjabberdCommandsMockTest do setup do :meck.unload :meck.new(@module, [:non_strict]) + :mnesia.delete_table(:ejabberd_commands) :ejabberd_commands.init end From e29f47893f0ae7035c6a53cd1f6c9c87a0654639 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 6 Sep 2016 00:05:54 +0200 Subject: [PATCH 102/179] mod_pubsub: Remove outdated comment --- src/mod_pubsub.erl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 81afc9a06..d64d05737 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -363,8 +363,6 @@ depends(ServerHost, Opts) -> %% The default plugin module is implicit. %%

The Erlang code for the plugin is located in a module called %% node_plugin. The 'node_' prefix is mandatory.

-%%

The modules are initialized in alphetical order and the list is checked -%% and sorted to ensure that each module is initialized only once.

%%

See {@link node_hometree:init/1} for an example implementation.

init_plugins(Host, ServerHost, Opts) -> TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts, From 48ce34987d211208a794844e9a47489588c9ba75 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 6 Sep 2016 00:08:43 +0200 Subject: [PATCH 103/179] node_mb: Fix configuration documentation A node plugin used in a 'pep_mapping' must explicitly be added to the list of 'plugins'. --- src/node_mb.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node_mb.erl b/src/node_mb.erl index 6c7f09780..ce264a9e4 100644 --- a/src/node_mb.erl +++ b/src/node_mb.erl @@ -38,6 +38,7 @@ %%% plugins: %%% - "flat" %%% - "pep" # Requires mod_caps. +%%% - "mb" %%% pep_mapping: %%% "urn:xmpp:microblog:0": "mb" %%%

From af2999a7833936b225a1625453e4a8da8efd43ba Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 6 Sep 2016 00:30:46 +0200 Subject: [PATCH 104/179] node_mb: Call node_pep instead of node_hometree --- src/node_mb.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_mb.erl b/src/node_mb.erl index ce264a9e4..3399422e5 100644 --- a/src/node_mb.erl +++ b/src/node_mb.erl @@ -155,7 +155,7 @@ set_subscriptions(Nidx, Owner, Subscription, SubId) -> node_pep:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_pep:get_pending_nodes(Host, Owner). get_states(Nidx) -> node_pep:get_states(Nidx). From 417284a9210cb8f6a3bcf9df41232e17c374da03 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 6 Sep 2016 17:55:18 +0300 Subject: [PATCH 105/179] Add get_subscribers command to list MUC subscribers --- src/mod_muc_admin.erl | 16 +++++++++++++++- src/mod_muc_room.erl | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 76e90957e..9ceec7d5b 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -20,7 +20,7 @@ change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, web_menu_main/2, web_page_main/2, web_menu_host/3, - subscribe_room/4, unsubscribe_room/2, + subscribe_room/4, unsubscribe_room/2, get_subscribers/2, web_page_host/3, mod_opt_type/1, get_commands_spec/0]). -include("ejabberd.hrl"). @@ -163,6 +163,11 @@ get_commands_spec() -> module = ?MODULE, function = unsubscribe_room, args = [{user, binary}, {room, binary}], result = {res, rescode}}, + #ejabberd_commands{name = get_subscribers, tags = [muc_room], + desc = "List subscribers of a MUC conference", + module = ?MODULE, function = get_subscribers, + args = [{name, binary}, {service, binary}], + result = {subscribers, {list, {jid, string}}}}, #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], desc = "Change an affiliation in a MUC room", module = ?MODULE, function = set_room_affiliation, @@ -955,6 +960,15 @@ unsubscribe_room(User, Room) -> throw({error, "Malformed room JID"}) end. +get_subscribers(Name, Host) -> + case get_room_pid(Name, Host) of + Pid when is_pid(Pid) -> + {ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers), + [jid:to_string(jid:remove_resource(J)) || J <- JIDList]; + _ -> + throw({error, "The room does not exist"}) + end. + make_opts(StateData) -> Config = StateData#state.config, [ diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index e5ed4cc68..bdcb808a1 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -749,6 +749,14 @@ handle_sync_event({change_state, NewStateData}, _From, handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> NSD = process_item_change(Item, StateData, UJID), {reply, {ok, NSD}, StateName, NSD}; +handle_sync_event(get_subscribers, _From, StateName, StateData) -> + JIDs = dict:fold( + fun(_, #user{is_subscriber = true, jid = J}, Acc) -> + [J|Acc]; + (_, _, Acc) -> + Acc + end, [], StateData#state.users), + {reply, {ok, JIDs}, StateName, StateData}; handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, StateName, StateData) -> SubEl = #xmlel{name = <<"subscribe">>, From 5ec2874a963a1559e5e1d76762fa0204efc6021f Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 6 Sep 2016 18:17:30 +0300 Subject: [PATCH 106/179] Do not update muc_online_users table on MUC/Sub operations --- src/mod_muc_room.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index bdcb808a1..6280d75ad 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -1217,6 +1217,7 @@ subscriber_becomes_available(From, Nick, Packet, StateData) -> Role = get_default_role(Aff, State1), State2 = set_role(From, Role, State1), State3 = set_nick(From, Nick, State2), + tab_add_online_user(From, StateData), send_existing_presences(From, State3), send_initial_presence(From, State3, StateData), State3. @@ -1752,7 +1753,6 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, NewStateData. add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> - tab_add_online_user(JID, StateData), User = #user{jid = JID, nick = Nick, role = Role, is_subscriber = IsSubscriber, subscriptions = Nodes}, StateData1 = update_online_user(JID, User, StateData), @@ -1775,6 +1775,7 @@ remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) -> error -> StateData#state.users end, + tab_remove_online_user(JID, StateData), StateData#state{users = Users}; remove_online_user(JID, StateData, _IsSubscriber, Reason) -> LJID = jid:tolower(JID), @@ -2033,6 +2034,7 @@ add_new_user(From, Nick, add_online_user(From, Nick, Role, IsSubscribeRequest, Nodes, StateData)), + tab_add_online_user(From, NewState), send_existing_presences(From, NewState), send_initial_presence(From, NewState, StateData), Shift = count_stanza_shift(Nick, Els, NewState), From c6afb9731b6d2e0de33eba4f239e27c236477039 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 6 Sep 2016 19:06:02 +0300 Subject: [PATCH 107/179] Handle request to list MUC subscribers --- src/mod_muc_room.erl | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 6280d75ad..f86b990d3 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -4695,11 +4695,28 @@ process_iq_mucsub(From, _Packet, _ -> {result, [], StateData} end; -process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) -> +process_iq_mucsub(From, _Packet, + #iq{type = get, lang = Lang, + sub_el = #xmlel{name = <<"subscriptions">>}}, + StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + Subs = dict:fold( + fun(_, #user{is_subscriber = true, jid = J}, Acc) -> + SJID = jid:to_string(jid:remove_resource(J)), + [#xmlel{name = <<"subscription">>, + attrs = [{<<"jid">>, SJID}]}|Acc]; + (_, _, Acc) -> + Acc + end, [], StateData#state.users), + {result, Subs, StateData}; + true -> + Txt = <<"Moderator privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, Txt)} + end; +process_iq_mucsub(_From, _Packet, #iq{lang = Lang}, _StateData) -> Txt = <<"Unrecognized subscription command">>, - {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; -process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) -> - Txt = <<"Value 'get' of 'type' attribute is not allowed">>, {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> From 1edca899ff7d3fb6df0d679dfe88b704d1670b80 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 7 Sep 2016 07:15:12 +0200 Subject: [PATCH 108/179] Add SQL support for microblogging node plugin --- src/node_mb_sql.erl | 151 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/node_mb_sql.erl diff --git a/src/node_mb_sql.erl b/src/node_mb_sql.erl new file mode 100644 index 000000000..a3fdf3aed --- /dev/null +++ b/src/node_mb_sql.erl @@ -0,0 +1,151 @@ +%%%---------------------------------------------------------------------- +%%% File : node_mb_sql.erl +%%% Author : Holger Weiss +%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend +%%% Created : 6 Sep 2016 by Holger Weiss +%%% +%%% +%%% ejabberd, Copyright (C) 2016 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(node_mb_sql). +-behaviour(gen_pubsub_node). +-author('holger@zedat.fu-berlin.de'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/7, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1]). + +init(Host, ServerHost, Opts) -> + node_pep_sql:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_pep_sql:terminate(Host, ServerHost), ok. + +options() -> + [{sql, true}, {rsm, true} | node_mb:options()]. + +features() -> + [<<"rsm">> | node_mb:features()]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_pep_sql:create_node_permission(Host, ServerHost, Node, ParentNode, + Owner, Access). + +create_node(Nidx, Owner) -> + node_pep_sql:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_pep_sql:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options) -> + node_pep_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_pep_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> + node_pep_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, + Payload, PubOpts). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_pep_sql:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_pep_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_pep_sql:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_pep_sql:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_pep_sql:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_pep_sql:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_pep_sql:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_pep_sql:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_pep_sql:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_pep_sql:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_pep_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_pep_sql:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_pep_sql:get_states(Nidx). + +get_state(Nidx, JID) -> + node_pep_sql:get_state(Nidx, JID). + +set_state(State) -> + node_pep_sql:set_state(State). + +get_items(Nidx, From, RSM) -> + node_pep_sql:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, + RSM) -> + node_pep_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription, + RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_pep_sql:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, + SubId) -> + node_pep_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, + RosterGroup, SubId). + +set_item(Item) -> + node_pep_sql:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_pep_sql:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_pep_sql:node_to_path(Node). + +path_to_node(Path) -> + node_pep_sql:path_to_node(Path). From 3803a8de3c2ad70ef967bb6d7a47f20fcf82f859 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Wed, 7 Sep 2016 10:33:37 +0300 Subject: [PATCH 109/179] Link MUC subscription to bare JID --- include/mod_muc_room.hrl | 9 +- src/mod_muc_room.erl | 337 ++++++++++++++++----------------------- 2 files changed, 148 insertions(+), 198 deletions(-) diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index d985f3f3b..47489f3d0 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -77,11 +77,15 @@ jid :: jid(), nick :: binary(), role :: role(), - is_subscriber = false :: boolean(), - subscriptions = [] :: [binary()], + %%is_subscriber = false :: boolean(), + %%subscriptions = [] :: [binary()], last_presence :: xmlel() }). +-record(subscriber, {jid :: jid(), + nick = <<>> :: binary(), + nodes = [] :: [binary()]}). + -record(activity, { message_time = 0 :: integer(), @@ -101,6 +105,7 @@ jid = #jid{} :: jid(), config = #config{} :: config(), users = (?DICT):new() :: ?TDICT, + subscribers = (?DICT):new() :: ?TDICT, last_voice_request_time = treap:empty() :: treap:treap(), robots = (?DICT):new() :: ?TDICT, nicks = (?DICT):new() :: ?TDICT, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index f86b990d3..11cc1d5f5 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -678,7 +678,7 @@ handle_event({service_message, Msg}, _StateName, children = [{xmlcdata, Msg}]}]}, send_wrapped_multiple( StateData#state.jid, - StateData#state.users, + get_users_and_subscribers(StateData), MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), @@ -750,12 +750,8 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData NSD = process_item_change(Item, StateData, UJID), {reply, {ok, NSD}, StateName, NSD}; handle_sync_event(get_subscribers, _From, StateName, StateData) -> - JIDs = dict:fold( - fun(_, #user{is_subscriber = true, jid = J}, Acc) -> - [J|Acc]; - (_, _, Acc) -> - Acc - end, [], StateData#state.users), + JIDs = lists:map(fun jid:make/1, + ?DICT:fetch_keys(StateData#state.subscribers)), {reply, {ok, JIDs}, StateName, StateData}; handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, StateName, StateData) -> @@ -936,7 +932,7 @@ terminate(Reason, _StateName, StateData) -> end, tab_remove_online_user(LJID, StateData) end, - [], StateData#state.users), + [], get_users_and_subscribers(StateData)), add_to_log(room_existence, stopped, StateData), mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(), StateData#state.server_host), @@ -953,11 +949,12 @@ process_groupchat_message(From, #xmlel{name = <<"message">>, attrs = Attrs} = Packet, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), - case is_user_online(From, StateData) orelse + IsSubscriber = is_subscriber(From, StateData), + case is_user_online(From, StateData) orelse IsSubscriber orelse is_user_allowed_message_nonparticipant(From, StateData) of true -> - {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData), + {FromNick, Role} = get_participant_data(From, StateData), if (Role == moderator) or (Role == participant) or IsSubscriber or ((StateData#state.config)#config.moderated == false) -> Subject = check_subject(Packet), @@ -1000,7 +997,7 @@ process_groupchat_message(From, end, send_wrapped_multiple( jid:replace_resource(StateData#state.jid, FromNick), - StateData#state.users, + get_users_and_subscribers(StateData), NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> @@ -1068,9 +1065,9 @@ get_participant_data(From, StateData) -> case (?DICT):find(jid:tolower(From), StateData#state.users) of - {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} -> - {FromNick, Role, IsSubscriber}; - error -> {<<"">>, moderator, false} + {ok, #user{nick = FromNick, role = Role}} -> + {FromNick, Role}; + error -> {<<"">>, moderator} end. process_presence(From, Nick, @@ -1078,7 +1075,6 @@ process_presence(From, Nick, StateData) -> Type0 = fxml:get_attr_s(<<"type">>, Attrs0), IsOnline = is_user_online(From, StateData), - IsSubscriber = is_subscriber(From, StateData), if Type0 == <<"">>; IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> case ejabberd_hooks:run_fold(muc_filter_presence, @@ -1115,7 +1111,7 @@ process_presence(From, Nick, Status_el -> fxml:get_tag_cdata(Status_el) end, - remove_online_user(From, NewState, IsSubscriber, Reason); + remove_online_user(From, NewState, Reason); <<"error">> -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " @@ -1172,27 +1168,15 @@ process_presence(From, Nick, From, Err), StateData; _ -> - case is_initial_presence(From, StateData) of - true -> - subscriber_becomes_available( - From, Nick, Packet, StateData); - false -> - change_nick(From, Nick, StateData) - end + change_nick(From, Nick, StateData) end; _NotNickChange -> - case is_initial_presence(From, StateData) of - true -> - subscriber_becomes_available( - From, Nick, Packet, StateData); - false -> - Stanza = maybe_strip_status_from_presence( - From, Packet, StateData), - NewState = add_user_presence(From, Stanza, - StateData), - send_new_presence(From, NewState, StateData), - NewState - end + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, Stanza, + StateData), + send_new_presence(From, NewState, StateData), + NewState end end end, @@ -1210,22 +1194,10 @@ maybe_strip_status_from_presence(From, Packet, StateData) -> _Allowed -> Packet end. -subscriber_becomes_available(From, Nick, Packet, StateData) -> - Stanza = maybe_strip_status_from_presence(From, Packet, StateData), - State1 = add_user_presence(From, Stanza, StateData), - Aff = get_affiliation(From, State1), - Role = get_default_role(Aff, State1), - State2 = set_role(From, Role, State1), - State3 = set_nick(From, Nick, State2), - tab_add_online_user(From, StateData), - send_existing_presences(From, State3), - send_initial_presence(From, State3, StateData), - State3. - close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent - andalso (?DICT):to_list(StateData1#state.users) == [] - of + andalso (?DICT):size(StateData1#state.users) == 0 + andalso (?DICT):size(StateData1#state.subscribers) == 0 of true -> ?INFO_MSG("Destroyed MUC room ~s because it's temporary " "and empty", @@ -1235,18 +1207,39 @@ close_room_if_temporary_and_empty(StateData1) -> _ -> {next_state, normal_state, StateData1} end. +get_users_and_subscribers(StateData) -> + OnlineSubscribers = ?DICT:fold( + fun(LJID, _, Acc) -> + LBareJID = jid:remove_resource(LJID), + case is_subscriber(LBareJID, StateData) of + true -> + ?SETS:add_element(LBareJID, Acc); + false -> + Acc + end + end, ?SETS:new(), StateData#state.users), + ?DICT:fold( + fun(LBareJID, #subscriber{nick = Nick}, Acc) -> + case ?SETS:is_element(LBareJID, OnlineSubscribers) of + false -> + ?DICT:store(LBareJID, + #user{jid = jid:make(LBareJID), + nick = Nick, + role = none, + last_presence = undefined}, + Acc); + true -> + Acc + end + end, StateData#state.users, StateData#state.subscribers). + is_user_online(JID, StateData) -> LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). is_subscriber(JID, StateData) -> - LJID = jid:tolower(JID), - case (?DICT):find(LJID, StateData#state.users) of - {ok, #user{is_subscriber = IsSubscriber}} -> - IsSubscriber; - _ -> - false - end. + LJID = jid:tolower(jid:remove_resource(JID)), + (?DICT):is_key(LJID, StateData#state.subscribers). %% Check if the user is occupant of the room, or at least is an admin or owner. is_occupant_or_admin(JID, StateData) -> @@ -1423,7 +1416,6 @@ make_reason(Packet, From, StateData, Reason1) -> iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). expulse_participant(Packet, From, StateData, Reason1) -> - IsSubscriber = is_subscriber(From, StateData), Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #xmlel{name = <<"presence">>, @@ -1438,7 +1430,7 @@ expulse_participant(Packet, From, StateData, Reason1) -> Reason2}]}]}, StateData), send_new_presence(From, NewState, StateData), - remove_online_user(From, NewState, IsSubscriber). + remove_online_user(From, NewState). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). @@ -1718,8 +1710,7 @@ prepare_room_queue(StateData) -> {empty, _} -> StateData end. -update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, - is_subscriber = IsSubscriber} = User, StateData) -> +update_online_user(JID, #user{nick = Nick} = User, StateData) -> LJID = jid:tolower(JID), Nicks1 = case (?DICT):find(LJID, StateData#state.users) of {ok, #user{nick = OldNick}} -> @@ -1738,9 +1729,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, [LJID], Nicks1), Users = (?DICT):update(LJID, fun(U) -> - U#user{nick = Nick, - subscriptions = Nodes, - is_subscriber = IsSubscriber} + U#user{nick = Nick} end, User, StateData#state.users), NewStateData = StateData#state{users = Users, nicks = Nicks}, case {?DICT:find(LJID, StateData#state.users), @@ -1752,32 +1741,26 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, end, NewStateData. -add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> - User = #user{jid = JID, nick = Nick, role = Role, - is_subscriber = IsSubscriber, subscriptions = Nodes}, - StateData1 = update_online_user(JID, User, StateData), - if IsSubscriber -> - store_room(StateData1); - true -> - ok - end, - StateData1. +set_subscriber(JID, Nick, Nodes, StateData) -> + BareJID = jid:remove_resource(JID), + Subscribers = ?DICT:store(jid:tolower(BareJID), + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + StateData#state.subscribers), + NewStateData = StateData#state{subscribers = Subscribers}, + store_room(NewStateData), + NewStateData. -remove_online_user(JID, StateData, IsSubscriber) -> - remove_online_user(JID, StateData, IsSubscriber, <<"">>). +add_online_user(JID, Nick, Role, StateData) -> + tab_add_online_user(JID, StateData), + User = #user{jid = JID, nick = Nick, role = Role}, + update_online_user(JID, User, StateData). -remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) -> - LJID = jid:tolower(JID), - Users = case (?DICT):find(LJID, StateData#state.users) of - {ok, U} -> - (?DICT):store(LJID, U#user{last_presence = undefined}, - StateData#state.users); - error -> - StateData#state.users - end, - tab_remove_online_user(JID, StateData), - StateData#state{users = Users}; -remove_online_user(JID, StateData, _IsSubscriber, Reason) -> +remove_online_user(JID, StateData) -> + remove_online_user(JID, StateData, <<"">>). + +remove_online_user(JID, StateData, Reason) -> LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), @@ -2032,9 +2015,7 @@ add_new_user(From, Nick, NewState = add_user_presence( From, Packet, add_online_user(From, Nick, Role, - IsSubscribeRequest, - Nodes, StateData)), - tab_add_online_user(From, NewState), + StateData)), send_existing_presences(From, NewState), send_initial_presence(From, NewState, StateData), Shift = count_stanza_shift(Nick, Els, NewState), @@ -2044,9 +2025,7 @@ add_new_user(From, Nick, end, NewState; true -> - add_online_user(From, Nick, none, - IsSubscribeRequest, - Nodes, StateData) + set_subscriber(From, Nick, Nodes, StateData) end, ResultState = case NewStateData#state.just_created of @@ -2286,15 +2265,6 @@ presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). -is_initial_presence(From, StateData) -> - LJID = jid:tolower(From), - case (?DICT):find(LJID, StateData#state.users) of - {ok, #user{last_presence = Pres}} when Pres /= undefined -> - false; - _ -> - true - end. - send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). @@ -2388,7 +2358,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> true -> [{LNJID, UserInfo}]; false -> - (?DICT):to_list(StateData#state.users) + (?DICT):to_list(get_users_and_subscribers(StateData)) end, lists:foreach( fun({LUJID, Info}) -> @@ -2444,7 +2414,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node1, StateData), Type = fxml:get_tag_attr_s(<<"type">>, Packet), - IsSubscriber = Info#user.is_subscriber, + IsSubscriber = is_subscriber(Info#user.jid, StateData), IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and (IsInitialPresence or (Type == <<"unavailable">>)) -> @@ -2668,19 +2638,20 @@ send_nick_changing(JID, OldNick, StateData, (_) -> ok end, - (?DICT):to_list(StateData#state.users)). + ?DICT:to_list(get_users_and_subscribers(StateData))). maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), + Users = get_users_and_subscribers(StateData), IsOccupant = case LJID of {LUser, LServer, <<"">>} -> not (?DICT):is_empty( (?DICT):filter(fun({U, S, _}, _) -> U == LUser andalso S == LServer - end, StateData#state.users)); + end, Users)); {_LUser, _LServer, _LResource} -> - (?DICT):is_key(LJID, StateData#state.users) + (?DICT):is_key(LJID, Users) end, case IsOccupant of true -> @@ -2701,19 +2672,19 @@ send_affiliation(LJID, Affiliation, StateData) -> children = [#xmlel{name = <<"item">>, attrs = ItemAttrs}]}]}, + Users = get_users_and_subscribers(StateData), Recipients = case (StateData#state.config)#config.anonymous of true -> (?DICT):filter(fun(_, #user{role = moderator}) -> true; (_, _) -> false - end, StateData#state.users); + end, Users); false -> - StateData#state.users + Users end, - send_multiple(StateData#state.jid, - StateData#state.server_host, - Recipients, Message). + send_wrapped_multiple(StateData#state.jid, Recipients, Message, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData). status_els(IsInitialPresence, JID, #user{jid = JID}, StateData) -> Status = case IsInitialPresence of @@ -3420,7 +3391,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData#state.jid, Nick), send_wrapped(RoomJIDNick, Info#user.jid, Packet, ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), - IsSubscriber = Info#user.is_subscriber, + IsSubscriber = is_subscriber(Info#user.jid, StateData), IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) -> send_wrapped(RoomJIDNick, Info#user.jid, Packet, @@ -3429,7 +3400,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, ok end end, - (?DICT):to_list(StateData#state.users)). + (?DICT):to_list(get_users_and_subscribers(StateData))). get_actor_nick(<<"">>, _StateData) -> <<"">>; @@ -4259,7 +4230,7 @@ send_config_change_info(New, #state{config = Old} = StateData) -> attrs = [{<<"xmlns">>, ?NS_MUC_USER}], children = StatusEls}]}, send_wrapped_multiple(StateData#state.jid, - StateData#state.users, + get_users_and_subscribers(StateData), Message, ?NS_MUCSUB_NODES_CONFIG, StateData). @@ -4275,7 +4246,7 @@ remove_nonmembers(StateData) -> _ -> SD end end, - StateData, (?DICT):to_list(StateData#state.users)). + StateData, (?DICT):to_list(get_users_and_subscribers(StateData))). set_opts([], StateData) -> StateData; set_opts([{Opt, Val} | Opts], StateData) -> @@ -4396,14 +4367,17 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{allow_subscription = Val}}; subscribers -> - lists:foldl( - fun({JID, Nick, Nodes}, State) -> - User = #user{jid = JID, nick = Nick, - subscriptions = Nodes, - is_subscriber = true, - role = none}, - update_online_user(JID, User, State) - end, StateData, Val); + Subscribers = lists:foldl( + fun({JID, Nick, Nodes}, Acc) -> + BareJID = jid:remove_resource(JID), + ?DICT:store( + jid:tolower(BareJID), + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + Acc) + end, ?DICT:new(), Val), + StateData#state{subscribers = Subscribers}; affiliations -> StateData#state{affiliations = (?DICT):from_list(Val)}; subject -> StateData#state{subject = Val}; @@ -4418,12 +4392,11 @@ set_opts([{Opt, Val} | Opts], StateData) -> make_opts(StateData) -> Config = StateData#state.config, Subscribers = (?DICT):fold( - fun(_LJID, #user{is_subscriber = true} = User, Acc) -> - [{User#user.jid, User#user.nick, - User#user.subscriptions}|Acc]; - (_, _, Acc) -> - Acc - end, [], StateData#state.users), + fun(_LJID, Sub, Acc) -> + [{Sub#subscriber.jid, + Sub#subscriber.nick, + Sub#subscriber.nodes}|Acc] + end, [], StateData#state.subscribers), [?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), @@ -4479,7 +4452,7 @@ destroy_room(DEl, StateData) -> Info#user.jid, Packet, ?NS_MUCSUB_NODES_CONFIG, StateData) end, - (?DICT):to_list(StateData#state.users)), + (?DICT):to_list(get_users_and_subscribers(StateData))), case (StateData#state.config)#config.persistent of true -> mod_muc:forget_room(StateData#state.server_host, @@ -4639,9 +4612,9 @@ process_iq_mucsub(From, Packet, Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>), {error, Err}; Nick when Config#config.allow_subscription -> - LJID = jid:tolower(From), - case (?DICT):find(LJID, StateData#state.users) of - {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick -> + LBareJID = jid:tolower(jid:remove_resource(From)), + case (?DICT):find(LBareJID, StateData#state.subscribers) of + {ok, #subscriber{nick = Nick1}} when Nick1 /= Nick -> Nodes = get_subscription_nodes(Packet), case {nick_collision(From, Nick, StateData), mod_muc:can_use_nick(StateData#state.server_host, @@ -4654,14 +4627,12 @@ process_iq_mucsub(From, Packet, ErrText = <<"That nickname is registered by another person">>, {error, ?ERRT_CONFLICT(Lang, ErrText)}; _ -> - NewStateData = add_online_user( - From, Nick, Role, true, Nodes, StateData), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), {result, subscription_nodes_to_events(Nodes), NewStateData} end; - {ok, #user{role = Role}} -> + {ok, #subscriber{}} -> Nodes = get_subscription_nodes(Packet), - NewStateData = add_online_user( - From, Nick, Role, true, Nodes, StateData), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), {result, subscription_nodes_to_events(Nodes), NewStateData}; error -> add_new_user(From, Nick, Packet, StateData) @@ -4674,27 +4645,11 @@ process_iq_mucsub(From, _Packet, #iq{type = set, sub_el = #xmlel{name = <<"unsubscribe">>}}, StateData) -> - LJID = jid:tolower(From), - case ?DICT:find(LJID, StateData#state.users) of - {ok, #user{is_subscriber = true} = User} -> - NewStateData = remove_subscription(From, User, StateData), - store_room(NewStateData), - {result, [], NewStateData}; - error when From#jid.lresource == <<"">> -> - {LUser, LServer, _} = LJID, - NewStateData = - dict:fold( - fun({U, S, _}, #user{jid = J, is_subscriber = true} = User, - AccState) when U == LUser, S == LServer -> - remove_subscription(J, User, AccState); - (_, _, AccState) -> - AccState - end, StateData, StateData#state.users), - store_room(NewStateData), - {result, [], NewStateData}; - _ -> - {result, [], StateData} - end; + LBareJID = jid:tolower(jid:remove_resource(From)), + Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers), + NewStateData = StateData#state{subscribers = Subscribers}, + store_room(NewStateData), + {result, [], NewStateData}; process_iq_mucsub(From, _Packet, #iq{type = get, lang = Lang, sub_el = #xmlel{name = <<"subscriptions">>}}, @@ -4703,13 +4658,11 @@ process_iq_mucsub(From, _Packet, FRole = get_role(From, StateData), if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> Subs = dict:fold( - fun(_, #user{is_subscriber = true, jid = J}, Acc) -> - SJID = jid:to_string(jid:remove_resource(J)), + fun(_, #subscriber{jid = J}, Acc) -> + SJID = jid:to_string(J), [#xmlel{name = <<"subscription">>, - attrs = [{<<"jid">>, SJID}]}|Acc]; - (_, _, Acc) -> - Acc - end, [], StateData#state.users), + attrs = [{<<"jid">>, SJID}]}|Acc] + end, [], StateData#state.subscribers), {result, Subs, StateData}; true -> Txt = <<"Moderator privileges required">>, @@ -4719,25 +4672,9 @@ process_iq_mucsub(_From, _Packet, #iq{lang = Lang}, _StateData) -> Txt = <<"Unrecognized subscription command">>, {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. -remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> - case User#user.last_presence of - undefined -> - remove_online_user(JID, StateData, false); - _ -> - LJID = jid:tolower(JID), - Users = ?DICT:store(LJID, User#user{is_subscriber = false}, - StateData#state.users), - StateData#state{users = Users} - end; -remove_subscription(_JID, #user{}, StateData) -> - StateData. - remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> - dict:fold( - fun(_LJID, User, State) -> - remove_subscription(User#user.jid, User, State) - end, StateData, StateData#state.users); + StateData#state{subscribers = ?DICT:new()}; true -> StateData end. @@ -5198,18 +5135,26 @@ store_room(StateData) -> send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), - case ?DICT:find(LTo, State#state.users) of - {ok, #user{is_subscriber = true, - subscriptions = Nodes, - last_presence = undefined}} -> - case lists:member(Node, Nodes) of - true -> - NewPacket = wrap(From, To, Packet, Node), - ejabberd_router:route(State#state.jid, To, NewPacket); - false -> + LBareTo = jid:tolower(jid:remove_resource(To)), + IsOffline = case ?DICT:find(LTo, State#state.users) of + {ok, #user{last_presence = undefined}} -> true; + error -> true; + _ -> false + end, + if IsOffline -> + case ?DICT:find(LBareTo, State#state.subscribers) of + {ok, #subscriber{nodes = Nodes, jid = JID}} -> + case lists:member(Node, Nodes) of + true -> + NewPacket = wrap(From, JID, Packet, Node), + ejabberd_router:route(State#state.jid, JID, NewPacket); + false -> + ok + end; + _ -> ok end; - _ -> + true -> ejabberd_router:route(From, To, Packet) end. @@ -5230,9 +5175,9 @@ wrap(From, To, Packet, Node) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast -send_multiple(From, Server, Users, Packet) -> - JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). +%% send_multiple(From, Server, Users, Packet) -> +%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], +%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). send_wrapped_multiple(From, Users, Packet, Node, State) -> lists:foreach( From f3041496151e57eb8212ff1f29bf99ad42e4691a Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Wed, 7 Sep 2016 11:15:19 +0300 Subject: [PATCH 110/179] Create room on configuration request as per XEP-0045, 10.1.3 --- src/mod_muc.erl | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 571f85926..85c0914d1 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -488,9 +488,8 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, _ -> case mnesia:dirty_read(muc_online_room, {Room, Host}) of [] -> - Type = fxml:get_attr_s(<<"type">>, Attrs), - case {Name, Type} of - {<<"presence">>, <<"">>} -> + case is_create_request(Packet) of + true -> case check_user_can_create_room(ServerHost, AccessCreate, From, Room) and check_create_roomid(ServerHost, Room) of @@ -508,7 +507,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), ejabberd_router:route(To, From, Err) end; - _ -> + false -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"Conference room does not exist">>, Err = jlib:make_error_reply(Packet, @@ -523,6 +522,22 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, end end. +-spec is_create_request(xmlel()) -> boolean(). +is_create_request(#xmlel{name = <<"presence">>} = Packet) -> + <<"">> == fxml:get_tag_attr_s(<<"type">>, Packet); +is_create_request(#xmlel{name = <<"iq">>} = Packet) -> + case jlib:iq_query_info(Packet) of + #iq{type = set, xmlns = ?NS_MUCSUB, + sub_el = #xmlel{name = <<"subscribe">>}} -> + true; + #iq{type = get, xmlns = ?NS_MUC_OWNER, sub_el = SubEl} -> + [] == fxml:remove_cdata(SubEl#xmlel.children); + _ -> + false + end; +is_create_request(_) -> + false. + check_user_can_create_room(ServerHost, AccessCreate, From, _RoomID) -> case acl:match_rule(ServerHost, AccessCreate, From) of From af0a493c663ea591305b0562810f142803a03c32 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Wed, 7 Sep 2016 14:34:31 +0200 Subject: [PATCH 111/179] Merge pull request #1253 from Amuhar/xep0356 --- ejabberd.yml.example | 10 + include/ejabberd_service.hrl | 20 ++ include/ns.hrl | 2 + src/ejabberd_app.erl | 1 + src/ejabberd_c2s.erl | 14 + src/ejabberd_service.erl | 161 +++++++++-- src/jlib.erl | 9 +- src/mod_delegation.erl | 538 +++++++++++++++++++++++++++++++++++ src/mod_privilege.erl | 363 +++++++++++++++++++++++ 9 files changed, 1091 insertions(+), 27 deletions(-) create mode 100644 include/ejabberd_service.hrl create mode 100644 src/mod_delegation.erl create mode 100644 src/mod_privilege.erl diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 72439e5e1..dae839fdc 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -147,6 +147,15 @@ listen: ## access: all ## shaper_rule: fast ## ip: "127.0.0.1" + ## privilege_access: + ## roster: "both" + ## message: "outgoing" + ## presence: "roster" + ## delegations: + ## "urn:xmpp:mam:1": + ## filtering: ["node"] + ## "http://jabber.org/protocol/pubsub": + ## filtering: [] ## hosts: ## "icq.example.org": ## password: "secret" @@ -580,6 +589,7 @@ modules: mod_carboncopy: {} mod_client_state: {} mod_configure: {} # requires mod_adhoc + ##mod_delegation: {} # for xep0356 mod_disco: {} ## mod_echo: {} mod_irc: {} diff --git a/include/ejabberd_service.hrl b/include/ejabberd_service.hrl new file mode 100644 index 000000000..7cd3b6943 --- /dev/null +++ b/include/ejabberd_service.hrl @@ -0,0 +1,20 @@ +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). + +-type filter_attr() :: {binary(), [binary()]}. + +-record(state, + {socket :: ejabberd_socket:socket_state(), + sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket, + streamid = <<"">> :: binary(), + host_opts = dict:new() :: ?TDICT, + host = <<"">> :: binary(), + access :: atom(), + check_from = true :: boolean(), + server_hosts = ?MYHOSTS :: [binary()], + privilege_access :: [attr()], + delegations :: [filter_attr()], + last_pres = dict:new() :: ?TDICT}). + +-type(state() :: #state{} ). diff --git a/include/ns.hrl b/include/ns.hrl index a150746e7..3dbc765b0 100644 --- a/include/ns.hrl +++ b/include/ns.hrl @@ -164,6 +164,8 @@ -define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>). -define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>). -define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>). +-define(NS_PRIVILEGE, <<"urn:xmpp:privilege:1">>). +-define(NS_DELEGATION, <<"urn:xmpp:delegation:1">>). -define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>). -define(NS_MUCSUB_NODES_PRESENCE, <<"urn:xmpp:mucsub:nodes:presence">>). -define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>). diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 6f0b97fa3..3b333b3b5 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -75,6 +75,7 @@ start(normal, _Args) -> ejabberd_oauth:start(), gen_mod:start_modules(), ejabberd_listener:start_listeners(), + ejabberd_service:start(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), Sup; start(_, _) -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 270ef1dc5..cf7602441 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -32,6 +32,7 @@ -protocol({xep, 78, '2.5'}). -protocol({xep, 138, '2.0'}). -protocol({xep, 198, '1.3'}). +-protocol({xep, 356, '7.1'}). -update_info({update, 0}). @@ -48,6 +49,7 @@ send_element/2, socket_type/0, get_presence/1, + get_last_presence/1, get_aux_field/2, set_aux_field/3, del_aux_field/2, @@ -212,6 +214,9 @@ socket_type() -> xml_stream. get_presence(FsmRef) -> (?GEN_FSM):sync_send_all_state_event(FsmRef, {get_presence}, 1000). +get_last_presence(FsmRef) -> + (?GEN_FSM):sync_send_all_state_event(FsmRef, + {get_last_presence}, 1000). get_aux_field(Key, #state{aux_fields = Opts}) -> case lists:keysearch(Key, 1, Opts) of @@ -1306,6 +1311,15 @@ handle_sync_event({get_presence}, _From, StateName, Resource = StateData#state.resource, Reply = {User, Resource, Show, Status}, fsm_reply(Reply, StateName, StateData); +handle_sync_event({get_last_presence}, _From, StateName, + StateData) -> + User = StateData#state.user, + Server = StateData#state.server, + PresLast = StateData#state.pres_last, + Resource = StateData#state.resource, + Reply = {User, Server, Resource, PresLast}, + fsm_reply(Reply, StateName, StateData); + handle_sync_event(get_subscribed, _From, StateName, StateData) -> Subscribed = (?SETS):to_list(StateData#state.pres_f), diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 9d72b17b4..9dd7c831e 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -36,7 +36,7 @@ -behaviour(?GEN_FSM). %% External exports --export([start/2, start_link/2, send_text/2, +-export([start/0, start/2, start_link/2, send_text/2, send_element/2, socket_type/0, transform_listen_option/2]). -export([init/1, wait_for_stream/2, @@ -44,19 +44,10 @@ handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3, print_state/1, opt_type/1]). --include("ejabberd.hrl"). --include("logger.hrl"). +-include("ejabberd_service.hrl"). +-include("mod_privacy.hrl"). --include("jlib.hrl"). - --record(state, - {socket :: ejabberd_socket:socket_state(), - sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket, - streamid = <<"">> :: binary(), - host_opts = dict:new() :: ?TDICT, - host = <<"">> :: binary(), - access :: atom(), - check_from = true :: boolean()}). +-export([get_delegated_ns/1]). %-define(DBGFSM, true). @@ -99,6 +90,15 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- + +%% for xep-0355 +%% table contans records like {namespace, fitering attributes, pid(), +%% host, disco info for general case, bare jid disco info } + +start() -> + ets:new(delegated_namespaces, [named_table, public]), + ets:new(hooks_tmp, [named_table, public]). + start(SockData, Opts) -> supervisor:start_child(ejabberd_service_sup, [SockData, Opts]). @@ -109,6 +109,9 @@ start_link(SockData, Opts) -> socket_type() -> xml_stream. +get_delegated_ns(FsmRef) -> + (?GEN_FSM):sync_send_all_state_event(FsmRef, {get_delegated_ns}). + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- @@ -141,6 +144,21 @@ init([{SockMod, Socket}, Opts]) -> p1_sha:sha(crypto:rand_bytes(20))), dict:from_list([{global, Pass}]) end, + %% privilege access to entities data + PrivAccess = case lists:keysearch(privilege_access, 1, Opts) of + {value, {_, PrivAcc}} -> PrivAcc; + _ -> [] + end, + Delegations = case lists:keyfind(delegations, 1, Opts) of + {delegations, Del} -> + lists:foldl( + fun({Ns, FiltAttr}, D) when Ns /= ?NS_DELEGATION -> + Attr = proplists:get_value(filtering, FiltAttr, []), + D ++ [{Ns, Attr}]; + (_Deleg, D) -> D + end, [], Del); + false -> [] + end, Shaper = case lists:keysearch(shaper_rule, 1, Opts) of {value, {_, S}} -> S; _ -> none @@ -154,8 +172,9 @@ init([{SockMod, Socket}, Opts]) -> SockMod:change_shaper(Socket, Shaper), {ok, wait_for_stream, #state{socket = Socket, sockmod = SockMod, - streamid = new_id(), host_opts = HostOpts, - access = Access, check_from = CheckFrom}}. + streamid = new_id(), host_opts = HostOpts, access = Access, + check_from = CheckFrom, privilege_access = PrivAccess, + delegations = Delegations}}. %%---------------------------------------------------------------------- %% Func: StateName/2 @@ -227,8 +246,31 @@ wait_for_handshake({xmlstreamelement, El}, StateData) -> [H]), ejabberd_hooks:run(component_connected, [H]) - end, dict:fetch_keys(StateData#state.host_opts)), - {next_state, stream_established, StateData}; + end, dict:fetch_keys(StateData#state.host_opts)), + + mod_privilege:advertise_permissions(StateData), + DelegatedNs = mod_delegation:advertise_delegations(StateData), + + RosterAccess = proplists:get_value(roster, + StateData#state.privilege_access), + + case proplists:get_value(presence, + StateData#state.privilege_access) of + <<"managed_entity">> -> + mod_privilege:initial_presences(StateData), + Fun = mod_privilege:process_presence(self()), + add_hooks(user_send_packet, Fun); + <<"roster">> when (RosterAccess == <<"both">>) or + (RosterAccess == <<"get">>) -> + mod_privilege:initial_presences(StateData), + Fun = mod_privilege:process_presence(self()), + add_hooks(user_send_packet, Fun), + Fun2 = mod_privilege:process_roster_presence(self()), + add_hooks(s2s_receive_packet, Fun2); + _ -> ok + end, + {next_state, stream_established, + StateData#state{delegations = DelegatedNs}}; _ -> send_text(StateData, ?INVALID_HANDSHAKE_ERR), {stop, normal, StateData} @@ -276,11 +318,12 @@ stream_established({xmlstreamelement, El}, StateData) -> <<"">> -> error; _ -> jid:from_string(To) end, - if ((Name == <<"iq">>) or (Name == <<"message">>) or - (Name == <<"presence">>)) - and (ToJID /= error) - and (FromJID /= error) -> - ejabberd_router:route(FromJID, ToJID, NewEl); + if (Name == <<"iq">>) and (ToJID /= error) and (FromJID /= error) -> + mod_privilege:process_iq(StateData, FromJID, ToJID, NewEl); + (Name == <<"presence">>) and (ToJID /= error) and (FromJID /= error) -> + ejabberd_router:route(FromJID, ToJID, NewEl); + (Name == <<"message">>) and (ToJID /= error) and (FromJID /= error) -> + mod_privilege:process_message(StateData, FromJID, ToJID, NewEl); true -> Lang = fxml:get_tag_attr_s(<<"xml:lang">>, El), Txt = <<"Incorrect stanza name or from/to JID">>, @@ -330,8 +373,11 @@ handle_event(_Event, StateName, StateData) -> %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- -handle_sync_event(_Event, _From, StateName, - StateData) -> +handle_sync_event({get_delegated_ns}, _From, StateName, StateData) -> + Reply = {StateData#state.host, StateData#state.delegations}, + {reply, Reply, StateName, StateData}; + +handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> @@ -370,6 +416,36 @@ handle_info({route, From, To, Packet}, StateName, ejabberd_router:route_error(To, From, Err, Packet) end, {next_state, StateName, StateData}; + +handle_info({user_presence, Packet, From}, + stream_established, StateData) -> + To = jid:from_string(StateData#state.host), + PacketNew = jlib:replace_from_to(From, To, Packet), + send_element(StateData, PacketNew), + {next_state, stream_established, StateData}; + +handle_info({roster_presence, Packet, From}, + stream_established, StateData) -> + %% check that current presence stanza is equivalent to last + PresenceNew = jlib:remove_attr(<<"to">>, Packet), + Dict = StateData#state.last_pres, + LastPresence = + try dict:fetch(From, Dict) + catch _:_ -> + undefined + end, + case mod_privilege:compare_presences(LastPresence, PresenceNew) of + false -> + #xmlel{attrs = Attrs} = PresenceNew, + Presence = PresenceNew#xmlel{attrs = [{<<"to">>, StateData#state.host} | Attrs]}, + send_element(StateData, Presence), + DictNew = dict:store(From, PresenceNew, Dict), + StateDataNew = StateData#state{last_pres = DictNew}, + {next_state, stream_established, StateDataNew}; + _ -> + {next_state, stream_established, StateData} + end; + handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {next_state, StateName, StateData}. @@ -388,7 +464,26 @@ terminate(Reason, StateName, StateData) -> ejabberd_hooks:run(component_disconnected, [StateData#state.host, Reason]) end, - dict:fetch_keys(StateData#state.host_opts)); + dict:fetch_keys(StateData#state.host_opts)), + + lists:foreach(fun({Ns, _FilterAttr}) -> + ets:delete(delegated_namespaces, Ns), + remove_iq_handlers(Ns) + end, StateData#state.delegations), + + RosterAccess = proplists:get_value(roster, StateData#state.privilege_access), + case proplists:get_value(presence, StateData#state.privilege_access) of + <<"managed_entity">> -> + Fun = mod_privilege:process_presence(self()), + remove_hooks(user_send_packet, Fun); + <<"roster">> when (RosterAccess == <<"both">>) or + (RosterAccess == <<"get">>) -> + Fun = mod_privilege:process_presence(self()), + remove_hooks(user_send_packet, Fun), + Fun2 = mod_privilege:process_roster_presence(self()), + remove_hooks(s2s_receive_packet, Fun2); + _ -> ok + end; _ -> ok end, (StateData#state.sockmod):close(StateData#state.socket), @@ -448,3 +543,19 @@ fsm_limit_opts(Opts) -> opt_type(max_fsm_queue) -> fun (I) when is_integer(I), I > 0 -> I end; opt_type(_) -> [max_fsm_queue]. + +remove_iq_handlers(Ns) -> + lists:foreach(fun(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, Ns), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, Ns) + end, ?MYHOSTS). + +add_hooks(Hook, Fun) -> + lists:foreach(fun(Host) -> + ejabberd_hooks:add(Hook, Host,Fun, 100) + end, ?MYHOSTS). + +remove_hooks(Hook, Fun) -> + lists:foreach(fun(Host) -> + ejabberd_hooks:delete(Hook, Host, Fun, 100) + end, ?MYHOSTS). diff --git a/src/jlib.erl b/src/jlib.erl index 4bc9b0055..3384e670e 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -371,15 +371,20 @@ iq_type_to_string(error) -> <<"error">>. -spec iq_to_xml(IQ :: iq()) -> xmlel(). iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) -> + Children = + if + is_list(SubEl) -> SubEl; + true -> [SubEl] + end, if ID /= <<"">> -> #xmlel{name = <<"iq">>, attrs = [{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}], - children = SubEl}; + children = Children}; true -> #xmlel{name = <<"iq">>, attrs = [{<<"type">>, iq_type_to_string(Type)}], - children = SubEl} + children = Children} end. -spec parse_xdata_submit(El :: xmlel()) -> diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl new file mode 100644 index 000000000..f2d1a13b5 --- /dev/null +++ b/src/mod_delegation.erl @@ -0,0 +1,538 @@ +%%%-------------------------------------------------------------------------------------- +%%% File : mod_delegation.erl +%%% Author : Anna Mukharram +%%% Purpose : This module is an implementation for XEP-0355: Namespace Delegation +%%%-------------------------------------------------------------------------------------- + +-module(mod_delegation). + +-author('amuhar3@gmail.com'). + +-behaviour(gen_mod). + +-protocol({xep, 0355, '0.3'}). + +-export([start/2, stop/1, depends/2, mod_opt_type/1]). + +-export([advertise_delegations/1, process_iq/3, + disco_local_features/5, disco_sm_features/5, + disco_local_identity/5, disco_sm_identity/5, disco_info/5, clean/0]). + +-include_lib("stdlib/include/ms_transform.hrl"). + +-include("ejabberd_service.hrl"). + +-define(CLEAN_INTERVAL, timer:minutes(10)). + +%%%-------------------------------------------------------------------------------------- +%%% API +%%%-------------------------------------------------------------------------------------- + +start(Host, _Opts) -> + mod_disco:register_feature(Host, ?NS_DELEGATION), + %% start timer for hooks_tmp table cleaning + timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []), + + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, + disco_local_features, 500), %% This hook should be the last + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, + disco_local_identity, 500), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 500), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 500), + ejabberd_hooks:add(disco_info, Host, ?MODULE, + disco_info, 500). + + +stop(Host) -> + mod_disco:unregister_feature(Host, ?NS_DELEGATION), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, + disco_local_features, 500), + ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, + disco_local_identity, 500), + ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, + disco_sm_identity, 500), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 500), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, + disco_info, 500). + +depends(_Host, _Opts) -> []. + +mod_opt_type(_Opt) -> []. + +%%%-------------------------------------------------------------------------------------- +%%% 4.2 Functions to advertise service of delegated namespaces +%%%-------------------------------------------------------------------------------------- +attribute_tag(Attrs) -> + lists:map(fun(Attr) -> + #xmlel{name = <<"attribute">>, attrs = [{<<"name">> , Attr}]} + end, Attrs). + +delegations(From, To, Delegations) -> + {Elem0, DelegatedNs} = + lists:foldl(fun({Ns, FiltAttr}, {Acc, AccNs}) -> + case ets:insert_new(delegated_namespaces, + {Ns, FiltAttr, self(), To, {}, {}}) of + true -> + Attrs = + if + FiltAttr == [] -> + ?DEBUG("namespace ~s is delegated to ~s with" + " no filtering attributes ~n",[Ns, To]), + []; + true -> + ?DEBUG("namespace ~s is delegated to ~s with" + " ~p filtering attributes ~n",[Ns, To, FiltAttr]), + attribute_tag(FiltAttr) + end, + add_iq_handlers(Ns), + {[#xmlel{name = <<"delegated">>, + attrs = [{<<"namespace">>, Ns}], + children = Attrs}| Acc], [{Ns, FiltAttr}|AccNs]}; + false -> {Acc, AccNs} + end + end, {[], []}, Delegations), + case Elem0 of + [] -> {ignore, DelegatedNs}; + _ -> + Elem1 = #xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}], + children = Elem0}, + Id = randoms:get_string(), + {#xmlel{name = <<"message">>, + attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}], + children = [Elem1]}, DelegatedNs} + end. + +add_iq_handlers(Ns) -> + lists:foreach(fun(Host) -> + IQDisc = + gen_mod:get_module_opt(Host, ?MODULE, iqdisc, + fun gen_iq_handler:check_type/1, one_queue), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + Ns, ?MODULE, + process_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + Ns, ?MODULE, + process_iq, IQDisc) + end, ?MYHOSTS). + +advertise_delegations(#state{delegations = []}) -> []; +advertise_delegations(StateData) -> + {Delegated, DelegatedNs} = + delegations(?MYNAME, StateData#state.host, StateData#state.delegations), + if + Delegated /= ignore -> + ejabberd_service:send_element(StateData, Delegated), + % server asks available features for delegated namespaces + disco_info(StateData#state{delegations = DelegatedNs}); + true -> ok + end, + DelegatedNs. + +%%%-------------------------------------------------------------------------------------- +%%% Delegated namespaces hook +%%%-------------------------------------------------------------------------------------- + +check_filter_attr([], _Children) -> true; +check_filter_attr(_FilterAttr, []) -> false; +check_filter_attr(FilterAttr, [#xmlel{} = Stanza|_]) -> + Attrs = proplists:get_keys(Stanza#xmlel.attrs), + lists:all(fun(Attr) -> + lists:member(Attr, Attrs) + end, FilterAttr); +check_filter_attr(_FilterAttr, _Children) -> false. + +-spec get_client_server([attr()]) -> {jid(), jid()}. + +get_client_server(Attrs) -> + Client = fxml:get_attr_s(<<"from">>, Attrs), + ClientJID = jid:from_string(Client), + ServerJID = jid:from_string(ClientJID#jid.lserver), + {ClientJID, ServerJID}. + +decapsulate_result(#xmlel{children = []}) -> ok; +decapsulate_result(#xmlel{children = Children}) -> + decapsulate_result0(Children). + +decapsulate_result0([]) -> ok; +decapsulate_result0([#xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}]} = Packet]) -> + decapsulate_result1(Packet#xmlel.children); +decapsulate_result0(_Children) -> ok. + +decapsulate_result1([]) -> ok; +decapsulate_result1([#xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}]} = Packet]) -> + decapsulate_result2(Packet#xmlel.children); +decapsulate_result1(_Children) -> ok. + +decapsulate_result2([]) -> ok; +decapsulate_result2([#xmlel{name = <<"iq">>, attrs = Attrs} = Packet]) -> + Ns = fxml:get_attr_s(<<"xmlns">>, Attrs), + if + Ns /= <<"jabber:client">> -> + ok; + true -> Packet + end; +decapsulate_result2(_Children) -> ok. + +-spec check_iq(xmlel(), xmlel()) -> xmlel() | ignore. + +check_iq(#xmlel{attrs = Attrs} = Packet, + #xmlel{attrs = AttrsOrigin} = OriginPacket) -> + % Id attribute of OriginPacket Must be equil to Packet Id attribute + Id1 = fxml:get_attr_s(<<"id">>, Attrs), + Id2 = fxml:get_attr_s(<<"id">>, AttrsOrigin), + % From attribute of OriginPacket Must be equil to Packet To attribute + From = fxml:get_attr_s(<<"from">>, AttrsOrigin), + To = fxml:get_attr_s(<<"to">>, Attrs), + % Type attribute Must be error or result + Type = fxml:get_attr_s(<<"type">>, Attrs), + if + ((Type == <<"result">>) or (Type == <<"error">>)), + Id1 == Id2, To == From -> + NewPacket = jlib:remove_attr(<<"xmlns">>, Packet), + %% We can send the decapsulated stanza from Server to Client (To) + NewPacket; + true -> + %% service-unavailable + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + Err + end; +check_iq(_Packet, _OriginPacket) -> ignore. + +-spec manage_service_result(atom(), atom(), binary(), xmlel()) -> ok. + +manage_service_result(HookRes, HookErr, Service, OriginPacket) -> + fun(Packet) -> + {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs), + Server = ClientJID#jid.lserver, + + ets:delete(hooks_tmp, {HookRes, Server}), + ets:delete(hooks_tmp, {HookErr, Server}), + % Check Packet "from" attribute + % It Must be equil to current service host + From = fxml:get_attr_s(<<"from">> , Packet#xmlel.attrs), + if + From == Service -> + % decapsulate iq result + ResultIQ = decapsulate_result(Packet), + ServResponse = check_iq(ResultIQ, OriginPacket), + if + ServResponse /= ignore -> + ejabberd_router:route(ServerJID, ClientJID, ServResponse); + true -> ok + end; + true -> + % service unavailable + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(ServerJID, ClientJID, Err) + end + end. + +-spec manage_service_error(atom(), atom(), xmlel()) -> ok. + +manage_service_error(HookRes, HookErr, OriginPacket) -> + fun(_Packet) -> + {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs), + Server = ClientJID#jid.lserver, + ets:delete(hooks_tmp, {HookRes, Server}), + ets:delete(hooks_tmp, {HookErr, Server}), + Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(ServerJID, ClientJID, Err) + end. + + +-spec forward_iq(binary(), binary(), xmlel()) -> ok. + +forward_iq(Server, Service, Packet) -> + Elem0 = #xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}], children = [Packet]}, + Elem1 = #xmlel{name = <<"delegation">>, + attrs = [{<<"xmlns">>, ?NS_DELEGATION}], children = [Elem0]}, + Id = randoms:get_string(), + Elem2 = #xmlel{name = <<"iq">>, + attrs = [{<<"from">>, Server}, {<<"to">>, Service}, + {<<"type">>, <<"set">>}, {<<"id">>, Id}], + children = [Elem1]}, + + HookRes = {iq, result, Id}, + HookErr = {iq, error, Id}, + + FunRes = manage_service_result(HookRes, HookErr, Service, Packet), + FunErr = manage_service_error(HookRes, HookErr, Packet), + + Timestamp = p1_time_compat:system_time(seconds), + ets:insert(hooks_tmp, {{HookRes, Server}, FunRes, Timestamp}), + ets:insert(hooks_tmp, {{HookErr, Server}, FunErr, Timestamp}), + + From = jid:make(<<"">>, Server, <<"">>), + To = jid:make(<<"">>, Service, <<"">>), + ejabberd_router:route(From, To, Elem2). + +process_iq(From, #jid{lresource = <<"">>} = To, + #iq{type = Type, xmlns = XMLNS} = IQ) -> + %% check if stanza directed to server + %% or directed to the bare JID of the sender + case ((Type == get) or (Type == set)) of + true -> + Packet = jlib:iq_to_xml(IQ), + #xmlel{name = <<"iq">>, attrs = Attrs, children = Children} = Packet, + AttrsNew = [{<<"xmlns">>, <<"jabber:client">>} | Attrs], + AttrsNew2 = jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), AttrsNew), + case ets:lookup(delegated_namespaces, XMLNS) of + [{XMLNS, FiltAttr, _Pid, ServiceHost, _, _}] -> + case check_filter_attr(FiltAttr, Children) of + true -> + forward_iq(From#jid.server, ServiceHost, + Packet#xmlel{attrs = AttrsNew2}); + _ -> ok + end; + [] -> ok + end, + ignore; + _ -> + ignore + end; +process_iq(_From, _To, _IQ) -> ignore. + +%%%-------------------------------------------------------------------------------------- +%%% 7. Discovering Support +%%%-------------------------------------------------------------------------------------- + +decapsulate_features(#xmlel{attrs = Attrs} = Packet, Node) -> + case fxml:get_attr_s(<<"node">>, Attrs) of + Node -> + PREFIX = << ?NS_DELEGATION/binary, "::" >>, + Size = byte_size(PREFIX), + BARE_PREFIX = << ?NS_DELEGATION/binary, ":bare:" >>, + SizeBare = byte_size(BARE_PREFIX), + + Features = [Feat || #xmlel{attrs = [{<<"var">>, Feat}]} <- + fxml:get_subtags(Packet, <<"feature">>)], + + Identity = [I || I <- fxml:get_subtags(Packet, <<"identity">>)], + + Exten = [I || I <- fxml:get_subtags_with_xmlns(Packet, <<"x">>, ?NS_XDATA)], + + case Node of + << PREFIX:Size/binary, NS/binary >> -> + ets:update_element(delegated_namespaces, NS, + {5, {Features, Identity, Exten}}); + << BARE_PREFIX:SizeBare/binary, NS/binary >> -> + ets:update_element(delegated_namespaces, NS, + {6, {Features, Identity, Exten}}); + _ -> ok + end; + _ -> ok + end; +decapsulate_features(_Packet, _Node) -> ok. + +-spec disco_result(atom(), atom(), binary()) -> ok. + +disco_result(HookRes, HookErr, Node) -> + fun(Packet) -> + Tag = fxml:get_subtag_with_xmlns(Packet, <<"query">>, ?NS_DISCO_INFO), + decapsulate_features(Tag, Node), + + ets:delete(hooks_tmp, {HookRes, ?MYNAME}), + ets:delete(hooks_tmp, {HookErr, ?MYNAME}) + end. + +-spec disco_error(atom(), atom()) -> ok. + +disco_error(HookRes, HookErr) -> + fun(_Packet) -> + ets:delete(hooks_tmp, {HookRes, ?MYNAME}), + ets:delete(hooks_tmp, {HookErr, ?MYNAME}) + end. + +-spec disco_info(state()) -> ok. + +disco_info(StateData) -> + disco_info(StateData, <<"::">>), + disco_info(StateData, <<":bare:">>). + +-spec disco_info(state(), binary()) -> ok. + +disco_info(StateData, Sep) -> + lists:foreach(fun({Ns, _FilterAttr}) -> + Id = randoms:get_string(), + Node = << ?NS_DELEGATION/binary, Sep/binary, Ns/binary >>, + + HookRes = {iq, result, Id}, + HookErr = {iq, error, Id}, + + FunRes = disco_result(HookRes, HookErr, Node), + FunErr = disco_error(HookRes, HookErr), + + Timestamp = p1_time_compat:system_time(seconds), + ets:insert(hooks_tmp, {{HookRes, ?MYNAME}, FunRes, Timestamp}), + ets:insert(hooks_tmp, {{HookErr, ?MYNAME}, FunErr, Timestamp}), + + Tag = #xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}, + {<<"node">>, Node}], + children = []}, + DiscoReq = #xmlel{name = <<"iq">>, + attrs = [{<<"type">>, <<"get">>}, {<<"id">>, Id}, + {<<"from">>, ?MYNAME}, + {<<"to">>, StateData#state.host }], + children = [Tag]}, + ejabberd_service:send_element(StateData, DiscoReq) + + end, StateData#state.delegations). + + +disco_features(Acc, Bare) -> + Fun = fun(Feat) -> + ets:foldl(fun({Ns, _, _, _, _, _}, A) -> + A or str:prefix(Ns, Feat) + end, false, delegated_namespaces) + end, + % delete feature namespace which is delegated to service + Features = lists:filter(fun ({{Feature, _Host}}) -> + not Fun(Feature); + (Feature) when is_binary(Feature) -> + not Fun(Feature) + end, Acc), + % add service features + FeaturesList = + ets:foldl(fun({_, _, _, _, {Feats, _, _}, {FeatsBare, _, _}}, A) -> + if + Bare -> A ++ FeatsBare; + true -> A ++ Feats + end; + (_, A) -> A + end, Features, delegated_namespaces), + {result, FeaturesList}. + +disco_identity(Acc, Bare) -> + % filter delegated identites + Fun = fun(Ident) -> + ets:foldl(fun({_, _, _, _, {_ , I, _}, {_ , IBare, _}}, A) -> + Identity = + if + Bare -> IBare; + true -> I + end, + (fxml:get_attr_s(<<"category">> , Ident) == + fxml:get_attr_s(<<"category">>, Identity)) and + (fxml:get_attr_s(<<"type">> , Ident) == + fxml:get_attr_s(<<"type">>, Identity)) or A; + (_, A) -> A + end, false, delegated_namespaces) + end, + + Identities = + lists:filter(fun (#xmlel{attrs = Attrs}) -> + not Fun(Attrs) + end, Acc), + % add service features + ets:foldl(fun({_, _, _, _, {_, I, _}, {_, IBare, _}}, A) -> + if + Bare -> A ++ IBare; + true -> A ++ I + end; + (_, A) -> A + end, Identities, delegated_namespaces). + +%% xmlns from value element + +-spec get_field_value([xmlel()]) -> binary(). + +get_field_value([]) -> <<"">>; +get_field_value([Elem| Elems]) -> + case (fxml:get_attr_s(<<"var">>, Elem#xmlel.attrs) == <<"FORM_TYPE">>) and + (fxml:get_attr_s(<<"type">>, Elem#xmlel.attrs) == <<"hidden">>) of + true -> + Ns = fxml:get_subtag_cdata(Elem, <<"value">>), + if + Ns /= <<"">> -> Ns; + true -> get_field_value(Elems) + end; + _ -> get_field_value(Elems) + end. + +get_info(Acc, Bare) -> + Fun = fun(Feat) -> + ets:foldl(fun({Ns, _, _, _, _, _}, A) -> + (A or str:prefix(Ns, Feat)) + end, false, delegated_namespaces) + end, + Exten = lists:filter(fun(Xmlel) -> + Tags = fxml:get_subtags(Xmlel, <<"field">>), + case get_field_value(Tags) of + <<"">> -> true; + Value -> not Fun(Value) + end + end, Acc), + ets:foldl(fun({_, _, _, _, {_, _, Ext}, {_, _, ExtBare}}, A) -> + if + Bare -> A ++ ExtBare; + true -> A ++ Ext + end; + (_, A) -> A + end, Exten, delegated_namespaces). + +%% 7.2.1 General Case + +disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_local_features(Acc, _From, _To, <<>>, _Lang) -> + FeatsOld = case Acc of + {result, I} -> I; + _ -> [] + end, + disco_features(FeatsOld, false); +disco_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_local_identity(Acc, _From, _To, <<>>, _Lang) -> + disco_identity(Acc, false); +disco_local_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +%% 7.2.2 Rediction Of Bare JID Disco Info + +disco_sm_features({error, ?ERR_ITEM_NOT_FOUND}, _From, + #jid{lresource = <<"">>}, <<>>, _Lang) -> + disco_features([], true); +disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> + Acc; +disco_sm_features(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) -> + FeatsOld = case Acc of + {result, I} -> I; + _ -> [] + end, + disco_features(FeatsOld, true); +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_sm_identity(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) -> + disco_identity(Acc, true); +disco_sm_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +disco_info(Acc, #jid{}, #jid{lresource = <<"">>}, <<>>, _Lang) -> + get_info(Acc, true); +disco_info(Acc, _Host, _Mod, <<>>, _Lang) -> + get_info(Acc, false); +disco_info(Acc, _Host, _Mod, _Node, _Lang) -> + Acc. + +%% clean hooks_tmp table + +clean() -> + ?DEBUG("cleaning ~p ETS table~n", [hooks_tmp]), + Now = p1_time_compat:system_time(seconds), + catch ets:select_delete(hooks_tmp, + ets:fun2ms(fun({_, _, Timestamp}) -> + Now - 300 >= Timestamp + end)), + %% start timer for table cleaning + timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []). diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl new file mode 100644 index 000000000..af6dacec4 --- /dev/null +++ b/src/mod_privilege.erl @@ -0,0 +1,363 @@ +%%%-------------------------------------------------------------------------------------- +%%% File : mod_privilege.erl +%%% Author : Anna Mukharram +%%% Purpose : This module is an implementation for XEP-0356: Privileged Entity +%%%-------------------------------------------------------------------------------------- + +-module(mod_privilege). + +-author('amuhar3@gmail.com'). + +-protocol({xep, 0356, '0.2.1'}). + +-export([advertise_permissions/1, initial_presences/1, process_presence/1, + process_roster_presence/1, compare_presences/2, + process_message/4, process_iq/4]). + +-include("ejabberd_service.hrl"). + +-include("mod_privacy.hrl"). + +%%%-------------------------------------------------------------------------------------- +%%% Functions to advertise services of allowed permission +%%%-------------------------------------------------------------------------------------- + +-spec permissions(binary(), binary(), list()) -> xmlel(). + +permissions(From, To, PrivAccess) -> + Perms = lists:map(fun({Access, Type}) -> + ?DEBUG("Advertise service ~s of allowed permission: ~s = ~s~n", + [To, Access, Type]), + #xmlel{name = <<"perm">>, + attrs = [{<<"access">>, + atom_to_binary(Access,latin1)}, + {<<"type">>, Type}]} + end, PrivAccess), + Stanza = #xmlel{name = <<"privilege">>, + attrs = [{<<"xmlns">> ,?NS_PRIVILEGE}], + children = Perms}, + Id = randoms:get_string(), + #xmlel{name = <<"message">>, + attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}], + children = [Stanza]}. + +advertise_permissions(#state{privilege_access = []}) -> ok; +advertise_permissions(StateData) -> + Stanza = + permissions(?MYNAME, StateData#state.host, StateData#state.privilege_access), + ejabberd_service:send_element(StateData, Stanza). + +%%%-------------------------------------------------------------------------------------- +%%% Process presences +%%%-------------------------------------------------------------------------------------- + +initial_presences(StateData) -> + Pids = ejabberd_sm:get_all_pids(), + lists:foreach( + fun(Pid) -> + {User, Server, Resource, PresenceLast} = ejabberd_c2s:get_last_presence(Pid), + From = #jid{user = User, server = Server, resource = Resource}, + To = jid:from_string(StateData#state.host), + PacketNew = jlib:replace_from_to(From, To, PresenceLast), + ejabberd_service:send_element(StateData, PacketNew) + end, Pids). + +%% hook user_send_packet(Packet, C2SState, From, To) -> Packet +%% for Managed Entity Presence +process_presence(Pid) -> + fun(#xmlel{name = <<"presence">>} = Packet, _C2SState, From, _To) -> + case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of + T when (T == <<"">>) or (T == <<"unavailable">>) -> + Pid ! {user_presence, Packet, From}; + _ -> ok + end, + Packet; + (Packet, _C2SState, _From, _To) -> + Packet + end. +%% s2s_receive_packet(From, To, Packet) -> ok +%% for Roster Presence +%% From subscription "from" or "both" +process_roster_presence(Pid) -> + fun(From, To, #xmlel{name = <<"presence">>} = Packet) -> + case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of + T when (T == <<"">>) or (T == <<"unavailable">>) -> + Server = To#jid.server, + User = To#jid.user, + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, #userlist{}, [User, Server]), + case privacy_check_packet(Server, User, PrivList, From, To, Packet, in) of + allow -> + Pid ! {roster_presence, Packet, From}; + _ -> ok + end, + ok; + _ -> ok + end; + (_From, _To, _Packet) -> ok + end. + +%%%-------------------------------------------------------------------------------------- +%%% Manage Roster +%%%-------------------------------------------------------------------------------------- + +process_iq(StateData, FromJID, ToJID, Packet) -> + IQ = jlib:iq_query_or_response_info(Packet), + case IQ of + #iq{xmlns = ?NS_ROSTER} -> + case (ToJID#jid.luser /= <<"">>) and + (FromJID#jid.luser == <<"">>) and + lists:member(ToJID#jid.lserver, ?MYHOSTS) of + true -> + AccessType = + proplists:get_value(roster, StateData#state.privilege_access, none), + case IQ#iq.type of + get when (AccessType == <<"both">>) or (AccessType == <<"get">>) -> + RosterIQ = roster_management(ToJID, FromJID, IQ), + ejabberd_service:send_element(StateData, RosterIQ); + set when (AccessType == <<"both">>) or (AccessType == <<"set">>) -> + %% check if user ToJID exist + #jid{lserver = Server, luser = User} = ToJID, + case ejabberd_auth:is_user_exists(User,Server) of + true -> + ResIQ = roster_management(ToJID, FromJID, IQ), + ejabberd_service:send_element(StateData, ResIQ); + _ -> ok + end; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + #iq{type = Type, id = Id} when (Type == error) or (Type == result) -> % for XEP-0355 + Hook = {iq, Type, Id}, + Host = ToJID#jid.lserver, + case (ToJID#jid.luser == <<"">>) and + (FromJID#jid.luser == <<"">>) and + lists:member(ToJID#jid.lserver, ?MYHOSTS) of + true -> + case ets:lookup(hooks_tmp, {Hook, Host}) of + [{_, Function, _Timestamp}] -> + catch apply(Function, [Packet]); + [] -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end. + +roster_management(FromJID, ToJID, IQ) -> + ResIQ = mod_roster:process_iq(FromJID, FromJID, IQ), + ResXml = jlib:iq_to_xml(ResIQ), + jlib:replace_from_to(FromJID, ToJID, ResXml). + +%%%-------------------------------------------------------------------------------------- +%%% Message permission +%%%-------------------------------------------------------------------------------------- + +process_message(StateData, FromJID, ToJID, #xmlel{children = Children} = Packet) -> + %% if presence was send from service to server, + case lists:member(ToJID#jid.lserver, ?MYHOSTS) and + (ToJID#jid.luser == <<"">>) and + (FromJID#jid.luser == <<"">>) of %% service + true -> + %% if stanza contains privilege element + case Children of + [#xmlel{name = <<"privilege">>, + attrs = [{<<"xmlns">>, ?NS_PRIVILEGE}], + children = [#xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}], + children = Children2}]}] -> + %% 1 case : privilege service send subscription message + %% on behalf of the client + %% 2 case : privilege service send message on behalf + %% of the client + case Children2 of + %% it isn't case of 0356 extension + [#xmlel{name = <<"presence">>} = Child] -> + forward_subscribe(StateData, Child, Packet); + [#xmlel{name = <<"message">>} = Child] -> %% xep-0356 + forward_message(StateData, Child, Packet); + _ -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"invalid forwarded element">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end; + + _ -> + ejabberd_router:route(FromJID, ToJID, Packet) + end. + +forward_subscribe(StateData, Presence, Packet) -> + PrivAccess = StateData#state.privilege_access, + T = proplists:get_value(roster, PrivAccess, none), + Type = fxml:get_attr_s(<<"type">>, Presence#xmlel.attrs), + if + ((T == <<"both">>) or (T == <<"set">>)) and (Type == <<"subscribe">>) -> + From = fxml:get_attr_s(<<"from">>, Presence#xmlel.attrs), + FromJ = jid:from_string(From), + To = fxml:get_attr_s(<<"to">>, Presence#xmlel.attrs), + ToJ = case To of + <<"">> -> error; + _ -> jid:from_string(To) + end, + if + (ToJ /= error) and (FromJ /= error) -> + Server = FromJ#jid.lserver, + User = FromJ#jid.luser, + case (FromJ#jid.lresource == <<"">>) and + lists:member(Server, ?MYHOSTS) of + true -> + if + (Server /= ToJ#jid.lserver) or + (User /= ToJ#jid.luser) -> + %% 0356 server MUST NOT allow the privileged entity + %% to do anything that the managed entity could not do + try_roster_subscribe(Server,User, FromJ, ToJ, Presence); + true -> %% we don't want presence sent to self + ok + end; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Incorrect stanza from/to JID">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end. + +forward_message(StateData, Message, Packet) -> + PrivAccess = StateData#state.privilege_access, + T = proplists:get_value(message, PrivAccess, none), + if + (T == <<"outgoing">>) -> + From = fxml:get_attr_s(<<"from">>, Message#xmlel.attrs), + FromJ = jid:from_string(From), + To = fxml:get_attr_s(<<"to">>, Message#xmlel.attrs), + ToJ = case To of + <<"">> -> FromJ; + _ -> jid:from_string(To) + end, + if + (ToJ /= error) and (FromJ /= error) -> + Server = FromJ#jid.server, + User = FromJ#jid.user, + case (FromJ#jid.lresource == <<"">>) and + lists:member(Server, ?MYHOSTS) of + true -> + %% there are no restriction on to attribute + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, #userlist{}, + [User, Server]), + check_privacy_route(Server, User, PrivList, + FromJ, ToJ, Message); + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Incorrect stanza from/to JID">>, + Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), + ejabberd_service:send_element(StateData, Err) + end; + true -> + Err = jlib:make_error_reply(Packet,?ERR_FORBIDDEN), + ejabberd_service:send_element(StateData, Err) + end. + +%%%-------------------------------------------------------------------------------------- +%%% helper functions +%%%-------------------------------------------------------------------------------------- + +compare_presences(undefined, _Presence) -> false; +compare_presences(#xmlel{attrs = Attrs, children = Child}, + #xmlel{attrs = Attrs2, children = Child2}) -> + Id1 = fxml:get_attr_s(<<"id">>, Attrs), + Id2 = fxml:get_attr_s(<<"id">>, Attrs2), + if + (Id1 /= Id2) -> + false; + (Id1 /= <<"">>) and (Id1 == Id2) -> + true; + true -> + case not compare_attrs(Attrs, Attrs2) of + true -> false; + _ -> + compare_elements(Child, Child2) + end + end. + + +compare_elements([],[]) -> true; +compare_elements(Tags1, Tags2) when length(Tags1) == length(Tags2) -> + compare_tags(Tags1,Tags2); +compare_elements(_Tags1, _Tags2) -> false. + +compare_tags([],[]) -> true; +compare_tags([{xmlcdata, CData}|Tags1], [{xmlcdata, CData}|Tags2]) -> + compare_tags(Tags1, Tags2); +compare_tags([{xmlcdata, _CData1}|_Tags1], [{xmlcdata, _CData2}|_Tags2]) -> + false; +compare_tags([#xmlel{} = Stanza1|Tags1], [#xmlel{} = Stanza2|Tags2]) -> + case (Stanza1#xmlel.name == Stanza2#xmlel.name) and + compare_attrs(Stanza1#xmlel.attrs, Stanza2#xmlel.attrs) and + compare_tags(Stanza1#xmlel.children, Stanza2#xmlel.children) of + true -> + compare_tags(Tags1,Tags2); + false -> + false + end. + +%% attr() :: {Name, Value} +-spec compare_attrs([attr()], [attr()]) -> boolean(). +compare_attrs([],[]) -> true; +compare_attrs(Attrs1, Attrs2) when length(Attrs1) == length(Attrs2) -> + lists:foldl(fun(Attr,Acc) -> lists:member(Attr, Attrs2) and Acc end, true, Attrs1); +compare_attrs(_Attrs1, _Attrs2) -> false. + +%% Check if privacy rules allow this delivery +%% from ejabberd_c2s.erl +privacy_check_packet(Server, User, PrivList, From, To, Packet , Dir) -> + ejabberd_hooks:run_fold(privacy_check_packet, + Server, allow, [User, Server, PrivList, + {From, To, Packet}, Dir]). + +check_privacy_route(Server, User, PrivList, From, To, Packet) -> + case privacy_check_packet(Server, User, PrivList, From, To, Packet, out) of + allow -> + ejabberd_router:route(From, To, Packet); + _ -> ok %% who should receive error : service or user? + end. + +try_roster_subscribe(Server,User, From, To, Packet) -> + Access = + gen_mod:get_module_opt(Server, mod_roster, access, + fun(A) when is_atom(A) -> A end, all), + case acl:match_rule(Server, Access, From) of + deny -> + ok; + allow -> + ejabberd_hooks:run(roster_out_subscription, Server, + [User, Server, To, subscribe]), + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + Server, + #userlist{}, + [User, Server]), + check_privacy_route(Server, User, PrivList, From, To, Packet) + end. From 7a538bb88b8d07c6014db1f7d18bf54045826802 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Wed, 7 Sep 2016 17:38:04 +0200 Subject: [PATCH 112/179] Enforce pathtype use with config path (#1264) --- src/ejabberd_config.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 87a918704..b75883fb2 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -318,7 +318,9 @@ get_absolute_path(File) -> File; relative -> {ok, Dir} = file:get_cwd(), - filename:absname_join(Dir, File) + filename:absname_join(Dir, File); + volumerelative -> + filename:absname(File) end. From 621f0e2b7cbb6c431e2c26ad6561c6c0eb74c065 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 7 Sep 2016 23:16:54 +0200 Subject: [PATCH 113/179] New stream management option: ack_timeout Close the connection if a stream management client fails to respond to an acknowledgement request within 60 seconds. This number of seconds can be changed with the new "ack_timeout" option, and the mechanism can be disabled by specifying 'infinity'. As a side effect of this change, a new acknowledgement is no longer requested before the response to the previous request is received. --- src/ejabberd_c2s.erl | 70 +++++++++++++++++++++++++++++++------- src/ejabberd_http_bind.erl | 1 + src/ejabberd_http_ws.erl | 1 + 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index cf7602441..09df739b4 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -113,9 +113,12 @@ mgmt_pending_since, mgmt_timeout, mgmt_max_timeout, + mgmt_ack_timeout, + mgmt_ack_timer, mgmt_resend, mgmt_stanzas_in = 0, mgmt_stanzas_out = 0, + mgmt_stanzas_req = 0, ask_offline = true, lang = <<"">>}). @@ -308,13 +311,18 @@ init([{SockMod, Socket}, Opts]) -> _ -> 1000 end, ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of - Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout; + RTimeo when is_integer(RTimeo), RTimeo >= 0 -> RTimeo; _ -> 300 end, MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of Max when is_integer(Max), Max >= ResumeTimeout -> Max; _ -> ResumeTimeout end, + AckTimeout = case proplists:get_value(ack_timeout, Opts) of + ATimeo when is_integer(ATimeo), ATimeo > 0 -> ATimeo * 1000; + infinity -> undefined; + _ -> 60000 + end, ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of Resend when is_boolean(Resend) -> Resend; if_offline -> if_offline; @@ -338,6 +346,7 @@ init([{SockMod, Socket}, Opts]) -> mgmt_max_queue = MaxAckQueue, mgmt_timeout = ResumeTimeout, mgmt_max_timeout = MaxResumeTimeout, + mgmt_ack_timeout = AckTimeout, mgmt_resend = ResendOnTimeout}, {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}. @@ -1759,6 +1768,11 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> fsm_next_state(StateName, StateData); handle_info(dont_ask_offline, StateName, StateData) -> fsm_next_state(StateName, StateData#state{ask_offline = false}); +handle_info(close, StateName, StateData) -> + ?DEBUG("Timeout waiting for stream management acknowledgement of ~s", + [jid:to_string(StateData#state.jid)]), + close(self()), + fsm_next_state(StateName, StateData); handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) -> %% This happens if the resume_session/1 request timed out; the new session %% now receives the late response. @@ -1894,8 +1908,8 @@ send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive -> send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending -> mgmt_queue_add(StateData, Stanza); send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active -> - NewStateData = send_stanza_and_ack_req(StateData, Stanza), - mgmt_queue_add(NewStateData, Stanza); + NewStateData = mgmt_queue_add(StateData, Stanza), + mgmt_send_stanza(NewStateData, Stanza); send_stanza(StateData, Stanza) -> send_element(StateData, Stanza), StateData. @@ -2757,7 +2771,8 @@ handle_r(StateData) -> handle_a(StateData, Attrs) -> case catch jlib:binary_to_integer(fxml:get_attr_s(<<"h">>, Attrs)) of H when is_integer(H), H >= 0 -> - check_h_attribute(StateData, H); + NewStateData = check_h_attribute(StateData, H), + maybe_renew_ack_request(NewStateData); _ -> ?DEBUG("Ignoring invalid ACK element from ~s", [jid:to_string(StateData#state.jid)]), @@ -2850,18 +2865,47 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El) update_num_stanzas_in(StateData, _El) -> StateData. -send_stanza_and_ack_req(StateData, Stanza) -> - AckReq = #xmlel{name = <<"r">>, - attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], - children = []}, - case send_element(StateData, Stanza) == ok andalso - send_element(StateData, AckReq) == ok of - true -> - StateData; - false -> +mgmt_send_stanza(StateData, Stanza) -> + case send_element(StateData, Stanza) of + ok -> + maybe_request_ack(StateData); + _ -> StateData#state{mgmt_state = pending} end. +maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) -> + request_ack(StateData); +maybe_request_ack(StateData) -> + StateData. + +request_ack(#state{mgmt_xmlns = Xmlns, + mgmt_ack_timeout = AckTimeout} = StateData) -> + AckReq = #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, Xmlns}]}, + case {send_element(StateData, AckReq), AckTimeout} of + {ok, undefined} -> + ok; + {ok, Timeout} -> + Timer = erlang:send_after(Timeout, self(), close), + StateData#state{mgmt_ack_timer = Timer, + mgmt_stanzas_req = StateData#state.mgmt_stanzas_out}; + _ -> + StateData#state{mgmt_state = pending} + end. + +maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) -> + StateData; +maybe_renew_ack_request(#state{mgmt_ack_timer = Timer, + mgmt_queue = Queue, + mgmt_stanzas_out = NumStanzasOut, + mgmt_stanzas_req = NumStanzasReq} = StateData) -> + erlang:cancel_timer(Timer), + case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of + true -> + request_ack(StateData#state{mgmt_ack_timer = undefined}); + false -> + StateData#state{mgmt_ack_timer = undefined} + end. + mgmt_queue_add(StateData, El) -> NewNum = case StateData#state.mgmt_stanzas_out of 4294967295 -> diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl index 758c1cee5..628119e6f 100644 --- a/src/ejabberd_http_bind.erl +++ b/src/ejabberd_http_bind.erl @@ -340,6 +340,7 @@ init([Sid, Key, IP, HOpts]) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), SOpts = lists:filtermap(fun({stream_management, _}) -> true; ({max_ack_queue, _}) -> true; + ({ack_timeout, _}) -> true; ({resume_timeout, _}) -> true; ({max_resume_timeout, _}) -> true; ({resend_on_timeout, _}) -> true; diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index 24554a8cc..e76e8689a 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -114,6 +114,7 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) -> init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> SOpts = lists:filtermap(fun({stream_management, _}) -> true; ({max_ack_queue, _}) -> true; + ({ack_timeout, _}) -> true; ({resume_timeout, _}) -> true; ({max_resume_timeout, _}) -> true; ({resend_on_timeout, _}) -> true; From a8368278ecf40adc0c50fceeaf428cf753ab1a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 8 Sep 2016 08:44:49 +0200 Subject: [PATCH 114/179] Properly normalize resource_regexp acl rule This fixes issue #1288. --- src/acl.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/acl.erl b/src/acl.erl index 897996976..bf17e8a1a 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -262,6 +262,7 @@ normalize_spec(Spec) -> {server, S} -> {server, nameprep(S)}; {resource, R} -> {resource, resourceprep(R)}; {server_regexp, SR} -> {server_regexp, b(SR)}; + {resource_regexp, R} -> {resource_regexp, b(R)}; {server_glob, S} -> {server_glob, b(S)}; {resource_glob, R} -> {resource_glob, b(R)}; {ip, {Net, Mask}} -> {ip, {Net, Mask}}; @@ -687,6 +688,7 @@ transform_options({acl, Name, Type}, Opts) -> {server_glob, S} -> {server_glob, [b(S)]}; {ip, S} -> {ip, [b(S)]}; {resource_glob, R} -> {resource_glob, [b(R)]} + {resource_regexp, R} -> {resource_regexp, [b(R)]} end, [{acl, [{Name, [T]}]}|Opts]; transform_options({access, Name, Rules}, Opts) -> From d2621130a349573c1b54fb4842a478e1ae0750aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 8 Sep 2016 10:27:14 +0200 Subject: [PATCH 115/179] Typo --- src/acl.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/acl.erl b/src/acl.erl index bf17e8a1a..d3f9afe38 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -687,7 +687,7 @@ transform_options({acl, Name, Type}, Opts) -> {server_regexp, SR} -> {server_regexp, [b(SR)]}; {server_glob, S} -> {server_glob, [b(S)]}; {ip, S} -> {ip, [b(S)]}; - {resource_glob, R} -> {resource_glob, [b(R)]} + {resource_glob, R} -> {resource_glob, [b(R)]}; {resource_regexp, R} -> {resource_regexp, [b(R)]} end, [{acl, [{Name, [T]}]}|Opts]; From 58a72bd395fbe7dec95dea035c5a0185e7319a66 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 8 Sep 2016 10:29:44 +0200 Subject: [PATCH 116/179] Fix compilation with Elixir 1.3 / R19 --- mix.exs | 4 ++-- mix.lock | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mix.exs b/mix.exs index 8a5a2b88b..d414ea574 100644 --- a/mix.exs +++ b/mix.exs @@ -40,7 +40,7 @@ defmodule Ejabberd.Mixfile do end defp deps do - [{:lager, "~> 3.0.0"}, + [{:lager, "~> 3.2"}, {:p1_utils, "~> 1.0"}, {:cache_tab, "~> 1.0"}, {:stringprep, "~> 1.0"}, @@ -60,7 +60,7 @@ defmodule Ejabberd.Mixfile do {:exrm, "~> 1.0.0", only: :dev}, # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with # version 3.20: - {:relx, "~> 3.19.0", only: :dev}, + {:relx, "~> 3.21", only: :dev}, {:meck, "~> 0.8.4", only: :test}, {:moka, github: "processone/moka", tag: "1.0.5c", only: :test}] end diff --git a/mix.lock b/mix.lock index b2e2be3a0..e77982e7f 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, - "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, + "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, "esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, @@ -10,19 +10,18 @@ "fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []}, - "goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []}, + "goldrush": {:hex, :goldrush, "0.1.8", "2024ba375ceea47e27ea70e14d2c483b2d8610101b4e852ef7f89163cdb6e649", [:rebar3], []}, "iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, - "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]}, + "lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]}, "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, "moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]}, "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []}, - "pc": {:hex, :pc, "1.2.0", "5e07731d1f8bf997a8d0271510983e570f910b42cd59bf612e664ad6dc35742e", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, + "relx": {:hex, :relx, "3.21.0", "91e1ea9f09b4edfda8461901f4b5c5e0226e43ec161e147eeab29f7761df6eb5", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, "samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]}, "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, "stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, From e6f7233351f6c91aea5e8ab053d73646a3a7b534 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 8 Sep 2016 10:52:43 +0200 Subject: [PATCH 117/179] Support for publishing to hex.pm with latest Elixir mix --- mix.exs | 3 ++- mix.lock | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d414ea574..ee4b60fb2 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do def project do [app: :ejabberd, - version: "16.06.0", + version: "16.08.0", description: description, elixir: "~> 1.2", elixirc_paths: ["lib"], @@ -61,6 +61,7 @@ defmodule Ejabberd.Mixfile do # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with # version 3.20: {:relx, "~> 3.21", only: :dev}, + {:ex_doc, ">= 0.0.0", only: :dev}, {:meck, "~> 0.8.4", only: :test}, {:moka, github: "processone/moka", tag: "1.0.5c", only: :test}] end diff --git a/mix.lock b/mix.lock index e77982e7f..fc2cdc924 100644 --- a/mix.lock +++ b/mix.lock @@ -1,9 +1,11 @@ %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, "cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, + "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, "esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]}, + "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, "fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, From 803270fc6b8ed3ba718f7e231b149caef70aa1ae Mon Sep 17 00:00:00 2001 From: gabrielgatu Date: Thu, 8 Sep 2016 11:34:42 +0200 Subject: [PATCH 118/179] Support for Elixir configuration file #1208 Contribution for Google Summer of code 2016 by Gabriel Gatu --- config/config.exs | 2 +- config/ejabberd.exs | 169 +++++ config/ejabberd.yml | 667 ++++++++++++++++++ lib/ejabberd/config/attr.ex | 119 ++++ lib/ejabberd/config/config.ex | 145 ++++ lib/ejabberd/config/ejabberd_hook.ex | 23 + lib/ejabberd/config/ejabberd_module.ex | 70 ++ lib/ejabberd/config/logger/ejabberd_logger.ex | 32 + lib/ejabberd/config/opts_formatter.ex | 46 ++ lib/ejabberd/config/store.ex | 55 ++ lib/ejabberd/config/validator/validation.ex | 40 ++ .../config/validator/validator_attrs.ex | 28 + .../validator/validator_dependencies.ex | 30 + .../config/validator/validator_utility.ex | 30 + lib/ejabberd/config_util.ex | 18 + lib/ejabberd/module.ex | 19 + lib/mix/tasks/deps.tree.ex | 94 +++ lib/mod_presence_demo.ex | 12 +- src/ejabberd_app.erl | 14 + src/ejabberd_config.erl | 13 +- src/ejabberd_http.erl | 7 +- test/elixir-config/attr_test.exs | 87 +++ test/elixir-config/config_test.exs | 65 ++ test/elixir-config/ejabberd_logger.exs | 49 ++ test/elixir-config/shared/ejabberd.exs | 31 + .../ejabberd_different_from_default.exs | 9 + .../shared/ejabberd_for_validation.exs | 20 + test/elixir-config/validation_test.exs | 32 + 28 files changed, 1914 insertions(+), 12 deletions(-) create mode 100644 config/ejabberd.exs create mode 100644 config/ejabberd.yml create mode 100644 lib/ejabberd/config/attr.ex create mode 100644 lib/ejabberd/config/config.ex create mode 100644 lib/ejabberd/config/ejabberd_hook.ex create mode 100644 lib/ejabberd/config/ejabberd_module.ex create mode 100644 lib/ejabberd/config/logger/ejabberd_logger.ex create mode 100644 lib/ejabberd/config/opts_formatter.ex create mode 100644 lib/ejabberd/config/store.ex create mode 100644 lib/ejabberd/config/validator/validation.ex create mode 100644 lib/ejabberd/config/validator/validator_attrs.ex create mode 100644 lib/ejabberd/config/validator/validator_dependencies.ex create mode 100644 lib/ejabberd/config/validator/validator_utility.ex create mode 100644 lib/ejabberd/config_util.ex create mode 100644 lib/ejabberd/module.ex create mode 100644 lib/mix/tasks/deps.tree.ex create mode 100644 test/elixir-config/attr_test.exs create mode 100644 test/elixir-config/config_test.exs create mode 100644 test/elixir-config/ejabberd_logger.exs create mode 100644 test/elixir-config/shared/ejabberd.exs create mode 100644 test/elixir-config/shared/ejabberd_different_from_default.exs create mode 100644 test/elixir-config/shared/ejabberd_for_validation.exs create mode 100644 test/elixir-config/validation_test.exs diff --git a/config/config.exs b/config/config.exs index 4d3783480..0d1a3c720 100644 --- a/config/config.exs +++ b/config/config.exs @@ -4,7 +4,7 @@ use Mix.Config config :ejabberd, file: "config/ejabberd.yml", log_path: 'log/ejabberd.log' - + # Customize Mnesia directory: config :mnesia, dir: 'mnesiadb/' diff --git a/config/ejabberd.exs b/config/ejabberd.exs new file mode 100644 index 000000000..05c2b5d83 --- /dev/null +++ b/config/ejabberd.exs @@ -0,0 +1,169 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + log_rotate_size: 10485760, + log_rotate_date: "", + log_rotate_count: 1, + log_rate_limit: 100, + auth_method: :internal, + max_fsm_queue: 1000, + language: "en", + allow_contrib_modules: true, + hosts: ["localhost"], + shaper: shaper, + acl: acl, + access: access] + end + + defp shaper do + [normal: 1000, + fast: 50000, + max_fsm_queue: 1000] + end + + defp acl do + [local: + [user_regexp: "", loopback: [ip: "127.0.0.0/8"]]] + end + + defp access do + [max_user_sessions: [all: 10], + max_user_offline_messages: [admin: 5000, all: 100], + local: [local: :allow], + c2s: [blocked: :deny, all: :allow], + c2s_shaper: [admin: :none, all: :normal], + s2s_shaper: [all: :fast], + announce: [admin: :allow], + configure: [admin: :allow], + muc_admin: [admin: :allow], + muc_create: [local: :allow], + muc: [all: :allow], + pubsub_createnode: [local: :allow], + register: [all: :allow], + trusted_network: [loopback: :allow]] + end + + listen :ejabberd_c2s do + @opts [ + port: 5222, + max_stanza_size: 65536, + shaper: :c2s_shaper, + access: :c2s] + end + + listen :ejabberd_s2s_in do + @opts [port: 5269] + end + + listen :ejabberd_http do + @opts [ + port: 5280, + web_admin: true, + http_poll: true, + http_bind: true, + captcha: true] + end + + module :mod_adhoc do + end + + module :mod_announce do + @opts [access: :announce] + end + + module :mod_blocking do + end + + module :mod_caps do + end + + module :mod_carboncopy do + end + + module :mod_client_state do + @opts [ + drop_chat_states: true, + queue_presence: false] + end + + module :mod_configure do + end + + module :mod_disco do + end + + module :mod_irc do + end + + module :mod_http_bind do + end + + module :mod_last do + end + + module :mod_muc do + @opts [ + access: :muc, + access_create: :muc_create, + access_persistent: :muc_create, + access_admin: :muc_admin] + end + + module :mod_offline do + @opts [access_max_user_messages: :max_user_offline_messages] + end + + module :mod_ping do + end + + module :mod_privacy do + end + + module :mod_private do + end + + module :mod_pubsub do + @opts [ + access_createnode: :pubsub_createnode, + ignore_pep_from_offline: true, + last_item_cache: true, + plugins: ["flat", "hometree", "pep"]] + end + + module :mod_register do + @opts [welcome_message: [ + subject: "Welcome!", + body: "Hi.\nWelcome to this XMPP Server", + ip_access: :trusted_network, + access: :register]] + end + + module :mod_roster do + end + + module :mod_shared_roster do + end + + module :mod_stats do + end + + module :mod_time do + end + + module :mod_version do + end + + # Example of how to define a hook, called when the event + # specified is triggered. + # + # @event: Name of the event + # @opts: Params are optional. Available: :host and :priority. + # If missing, defaults are used. (host: :global | priority: 50) + # @callback Could be an anonymous function or a callback from a module, + # use the &ModuleName.function/arity format for that. + hook :register_user, [host: "localhost"], fn(user, server) -> + info("User registered: #{user} on #{server}") + end +end diff --git a/config/ejabberd.yml b/config/ejabberd.yml new file mode 100644 index 000000000..80fc3c622 --- /dev/null +++ b/config/ejabberd.yml @@ -0,0 +1,667 @@ +### +### ejabberd configuration file +### +### + +### The parameters used in this configuration file are explained in more detail +### in the ejabberd Installation and Operation Guide. +### Please consult the Guide in case of doubts, it is included with +### your copy of ejabberd, and is also available online at +### http://www.process-one.net/en/ejabberd/docs/ + +### The configuration file is written in YAML. +### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. +### However, ejabberd treats different literals as different types: +### +### - unquoted or single-quoted strings. They are called "atoms". +### Example: dog, 'Jupiter', '3.14159', YELLOW +### +### - numeric literals. Example: 3, -45.0, .0 +### +### - quoted or folded strings. +### Examples of quoted string: "Lizzard", "orange". +### Example of folded string: +### > Art thou not Romeo, +### and a Montague? + +### ======= +### LOGGING + +## +## loglevel: Verbosity of log files generated by ejabberd. +## 0: No ejabberd log at all (not recommended) +## 1: Critical +## 2: Error +## 3: Warning +## 4: Info +## 5: Debug +## +loglevel: 4 + +## +## rotation: Describe how to rotate logs. Either size and/or date can trigger +## log rotation. Setting count to N keeps N rotated logs. Setting count to 0 +## does not disable rotation, it instead rotates the file and keeps no previous +## versions around. Setting size to X rotate log when it reaches X bytes. +## To disable rotation set the size to 0 and the date to "" +## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf. +## Some examples: +## $D0 rotate every night at midnight +## $D23 rotate every day at 23:00 hr +## $W0D23 rotate every week on Sunday at 23:00 hr +## $W5D16 rotate every week on Friday at 16:00 hr +## $M1D0 rotate on the first day of every month at midnight +## $M5D6 rotate on every 5th day of the month at 6:00 hr +## +log_rotate_size: 10485760 +log_rotate_date: "" +log_rotate_count: 1 + +## +## overload protection: If you want to limit the number of messages per second +## allowed from error_logger, which is a good idea if you want to avoid a flood +## of messages when system is overloaded, you can set a limit. +## 100 is ejabberd's default. +log_rate_limit: 100 + +## +## watchdog_admins: Only useful for developers: if an ejabberd process +## consumes a lot of memory, send live notifications to these XMPP +## accounts. +## +## watchdog_admins: +## - "bob@example.com" + + +### ================ +### SERVED HOSTNAMES + +## +## hosts: Domains served by ejabberd. +## You can define one or several, for example: +## hosts: +## - "example.net" +## - "example.com" +## - "example.org" +## +hosts: + - "localhost" + +## +## route_subdomains: Delegate subdomains to other XMPP servers. +## For example, if this ejabberd serves example.org and you want +## to allow communication with an XMPP server called im.example.org. +## +## route_subdomains: s2s + +### =============== +### LISTENING PORTS + +## +## listen: The ports ejabberd will listen on, which service each is handled +## by and what options to start it with. +## +listen: + - + port: 5222 + module: ejabberd_c2s + ## + ## If TLS is compiled in and you installed a SSL + ## certificate, specify the full path to the + ## file and uncomment these lines: + ## + ## certfile: "/path/to/ssl.pem" + ## starttls: true + ## + ## To enforce TLS encryption for client connections, + ## use this instead of the "starttls" option: + ## + ## starttls_required: true + ## + ## Custom OpenSSL options + ## + ## protocol_options: + ## - "no_sslv3" + ## - "no_tlsv1" + max_stanza_size: 65536 + shaper: c2s_shaper + access: c2s + - + port: 5269 + module: ejabberd_s2s_in + ## + ## ejabberd_service: Interact with external components (transports, ...) + ## + ## - + ## port: 8888 + ## module: ejabberd_service + ## access: all + ## shaper_rule: fast + ## ip: "127.0.0.1" + ## hosts: + ## "icq.example.org": + ## password: "secret" + ## "sms.example.org": + ## password: "secret" + + ## + ## ejabberd_stun: Handles STUN Binding requests + ## + ## - + ## port: 3478 + ## transport: udp + ## module: ejabberd_stun + + ## + ## To handle XML-RPC requests that provide admin credentials: + ## + ## - + ## port: 4560 + ## module: ejabberd_xmlrpc + - + port: 5280 + module: ejabberd_http + ## request_handlers: + ## "/pub/archive": mod_http_fileserver + web_admin: true + http_poll: true + http_bind: true + ## register: true + captcha: true + +## +## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections. +## Allowed values are: false optional required required_trusted +## You must specify a certificate file. +## +## s2s_use_starttls: optional + +## +## s2s_certfile: Specify a certificate file. +## +## s2s_certfile: "/path/to/ssl.pem" + +## Custom OpenSSL options +## +## s2s_protocol_options: +## - "no_sslv3" +## - "no_tlsv1" + +## +## domain_certfile: Specify a different certificate for each served hostname. +## +## host_config: +## "example.org": +## domain_certfile: "/path/to/example_org.pem" +## "example.com": +## domain_certfile: "/path/to/example_com.pem" + +## +## S2S whitelist or blacklist +## +## Default s2s policy for undefined hosts. +## +## s2s_access: s2s + +## +## Outgoing S2S options +## +## Preferred address families (which to try first) and connect timeout +## in milliseconds. +## +## outgoing_s2s_families: +## - ipv4 +## - ipv6 +## outgoing_s2s_timeout: 10000 + +### ============== +### AUTHENTICATION + +## +## auth_method: Method used to authenticate the users. +## The default method is the internal. +## If you want to use a different method, +## comment this line and enable the correct ones. +## +auth_method: internal + +## +## Store the plain passwords or hashed for SCRAM: +## auth_password_format: plain +## auth_password_format: scram +## +## Define the FQDN if ejabberd doesn't detect it: +## fqdn: "server3.example.com" + +## +## Authentication using external script +## Make sure the script is executable by ejabberd. +## +## auth_method: external +## extauth_program: "/path/to/authentication/script" + +## +## Authentication using ODBC +## Remember to setup a database in the next section. +## +## auth_method: odbc + +## +## Authentication using PAM +## +## auth_method: pam +## pam_service: "pamservicename" + +## +## Authentication using LDAP +## +## auth_method: ldap +## +## List of LDAP servers: +## ldap_servers: +## - "localhost" +## +## Encryption of connection to LDAP servers: +## ldap_encrypt: none +## ldap_encrypt: tls +## +## Port to connect to on LDAP servers: +## ldap_port: 389 +## ldap_port: 636 +## +## LDAP manager: +## ldap_rootdn: "dc=example,dc=com" +## +## Password of LDAP manager: +## ldap_password: "******" +## +## Search base of LDAP directory: +## ldap_base: "dc=example,dc=com" +## +## LDAP attribute that holds user ID: +## ldap_uids: +## - "mail": "%u@mail.example.org" +## +## LDAP filter: +## ldap_filter: "(objectClass=shadowAccount)" + +## +## Anonymous login support: +## auth_method: anonymous +## anonymous_protocol: sasl_anon | login_anon | both +## allow_multiple_connections: true | false +## +## host_config: +## "public.example.org": +## auth_method: anonymous +## allow_multiple_connections: false +## anonymous_protocol: sasl_anon +## +## To use both anonymous and internal authentication: +## +## host_config: +## "public.example.org": +## auth_method: +## - internal +## - anonymous + +### ============== +### DATABASE SETUP + +## ejabberd by default uses the internal Mnesia database, +## so you do not necessarily need this section. +## This section provides configuration examples in case +## you want to use other database backends. +## Please consult the ejabberd Guide for details on database creation. + +## +## MySQL server: +## +## odbc_type: mysql +## odbc_server: "server" +## odbc_database: "database" +## odbc_username: "username" +## odbc_password: "password" +## +## If you want to specify the port: +## odbc_port: 1234 + +## +## PostgreSQL server: +## +## odbc_type: pgsql +## odbc_server: "server" +## odbc_database: "database" +## odbc_username: "username" +## odbc_password: "password" +## +## If you want to specify the port: +## odbc_port: 1234 +## +## If you use PostgreSQL, have a large database, and need a +## faster but inexact replacement for "select count(*) from users" +## +## pgsql_users_number_estimate: true + +## +## ODBC compatible or MSSQL server: +## +## odbc_type: odbc +## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd" + +## +## Number of connections to open to the database for each virtual host +## +## odbc_pool_size: 10 + +## +## Interval to make a dummy SQL request to keep the connections to the +## database alive. Specify in seconds: for example 28800 means 8 hours +## +## odbc_keepalive_interval: undefined + +### =============== +### TRAFFIC SHAPERS + +shaper: + ## + ## The "normal" shaper limits traffic speed to 1000 B/s + ## + normal: 1000 + + ## + ## The "fast" shaper limits traffic speed to 50000 B/s + ## + fast: 50000 + +## +## This option specifies the maximum number of elements in the queue +## of the FSM. Refer to the documentation for details. +## +max_fsm_queue: 1000 + +###. ==================== +###' ACCESS CONTROL LISTS +acl: + ## + ## The 'admin' ACL grants administrative privileges to XMPP accounts. + ## You can put here as many accounts as you want. + ## + ## admin: + ## user: + ## - "aleksey": "localhost" + ## - "ermine": "example.org" + ## + ## Blocked users + ## + ## blocked: + ## user: + ## - "baduser": "example.org" + ## - "test" + + ## Local users: don't modify this. + ## + local: + user_regexp: "" + + ## + ## More examples of ACLs + ## + ## jabberorg: + ## server: + ## - "jabber.org" + ## aleksey: + ## user: + ## - "aleksey": "jabber.ru" + ## test: + ## user_regexp: "^test" + ## user_glob: "test*" + + ## + ## Loopback network + ## + loopback: + ip: + - "127.0.0.0/8" + + ## + ## Bad XMPP servers + ## + ## bad_servers: + ## server: + ## - "xmpp.zombie.org" + ## - "xmpp.spam.com" + +## +## Define specific ACLs in a virtual host. +## +## host_config: +## "localhost": +## acl: +## admin: +## user: +## - "bob-local": "localhost" + +### ============ +### ACCESS RULES +access: + ## Maximum number of simultaneous sessions allowed for a single user: + max_user_sessions: + all: 10 + ## Maximum number of offline messages that users can have: + max_user_offline_messages: + admin: 5000 + all: 100 + ## This rule allows access only for local users: + local: + local: allow + ## Only non-blocked users can use c2s connections: + c2s: + blocked: deny + all: allow + ## For C2S connections, all users except admins use the "normal" shaper + c2s_shaper: + admin: none + all: normal + ## All S2S connections use the "fast" shaper + s2s_shaper: + all: fast + ## Only admins can send announcement messages: + announce: + admin: allow + ## Only admins can use the configuration interface: + configure: + admin: allow + ## Admins of this server are also admins of the MUC service: + muc_admin: + admin: allow + ## Only accounts of the local ejabberd server can create rooms: + muc_create: + local: allow + ## All users are allowed to use the MUC service: + muc: + all: allow + ## Only accounts on the local ejabberd server can create Pubsub nodes: + pubsub_createnode: + local: allow + ## In-band registration allows registration of any possible username. + ## To disable in-band registration, replace 'allow' with 'deny'. + register: + all: allow + ## Only allow to register from localhost + trusted_network: + loopback: allow + ## Do not establish S2S connections with bad servers + ## s2s: + ## bad_servers: deny + ## all: allow + +## By default the frequency of account registrations from the same IP +## is limited to 1 account every 10 minutes. To disable, specify: infinity +## registration_timeout: 600 + +## +## Define specific Access Rules in a virtual host. +## +## host_config: +## "localhost": +## access: +## c2s: +## admin: allow +## all: deny +## register: +## all: deny + +### ================ +### DEFAULT LANGUAGE + +## +## language: Default language used for server messages. +## +language: "en" + +## +## Set a different default language in a virtual host. +## +## host_config: +## "localhost": +## language: "ru" + +### ======= +### CAPTCHA + +## +## Full path to a script that generates the image. +## +## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh" + +## +## Host for the URL and port where ejabberd listens for CAPTCHA requests. +## +## captcha_host: "example.org:5280" + +## +## Limit CAPTCHA calls per minute for JID/IP to avoid DoS. +## +## captcha_limit: 5 + +### ======= +### MODULES + +## +## Modules enabled in all ejabberd virtual hosts. +## +modules: + mod_adhoc: {} + ## mod_admin_extra: {} + mod_announce: # recommends mod_adhoc + access: announce + mod_blocking: {} # requires mod_privacy + mod_caps: {} + mod_carboncopy: {} + mod_client_state: + drop_chat_states: true + queue_presence: false + mod_configure: {} # requires mod_adhoc + mod_disco: {} + ## mod_echo: {} + mod_irc: {} + mod_http_bind: {} + ## mod_http_fileserver: + ## docroot: "/var/www" + ## accesslog: "/var/log/ejabberd/access.log" + mod_last: {} + mod_muc: + ## host: "conference.@HOST@" + access: muc + access_create: muc_create + access_persistent: muc_create + access_admin: muc_admin + ## mod_muc_log: {} + mod_offline: + access_max_user_messages: max_user_offline_messages + mod_ping: {} + ## mod_pres_counter: + ## count: 5 + ## interval: 60 + mod_privacy: {} + mod_private: {} + ## mod_proxy65: {} + mod_pubsub: + access_createnode: pubsub_createnode + ## reduces resource comsumption, but XEP incompliant + ignore_pep_from_offline: true + ## XEP compliant, but increases resource comsumption + ## ignore_pep_from_offline: false + last_item_cache: false + plugins: + - "flat" + - "hometree" + - "pep" # pep requires mod_caps + mod_register: + ## + ## Protect In-Band account registrations with CAPTCHA. + ## + ## captcha_protected: true + + ## + ## Set the minimum informational entropy for passwords. + ## + ## password_strength: 32 + + ## + ## After successful registration, the user receives + ## a message with this subject and body. + ## + welcome_message: + subject: "Welcome!" + body: |- + Hi. + Welcome to this XMPP server. + + ## + ## When a user registers, send a notification to + ## these XMPP accounts. + ## + ## registration_watchers: + ## - "admin1@example.org" + + ## + ## Only clients in the server machine can register accounts + ## + ip_access: trusted_network + + ## + ## Local c2s or remote s2s users cannot register accounts + ## + ## access_from: deny + + access: register + mod_roster: {} + mod_shared_roster: {} + mod_stats: {} + mod_time: {} + mod_vcard: {} + mod_version: {} + +## +## Enable modules with custom options in a specific virtual host +## +## host_config: +## "localhost": +## modules: +## mod_echo: +## host: "mirror.localhost" + +## +## Enable modules management via ejabberdctl for installation and +## uninstallation of public/private contributed modules +## (enabled by default) +## + +allow_contrib_modules: true + +### Local Variables: +### mode: yaml +### End: +### vim: set filetype=yaml tabstop=8 diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex new file mode 100644 index 000000000..9d17b157d --- /dev/null +++ b/lib/ejabberd/config/attr.ex @@ -0,0 +1,119 @@ +defmodule Ejabberd.Config.Attr do + @moduledoc """ + Module used to work with the attributes parsed from + an elixir block (do...end). + + Contains functions for extracting attrs from a block + and validation. + """ + + @type attr :: {atom(), any()} + + @attr_supported [ + active: + [type: :boolean, default: true], + git: + [type: :string, default: ""], + name: + [type: :string, default: ""], + opts: + [type: :list, default: []], + dependency: + [type: :list, default: []] + ] + + @doc """ + Takes a block with annotations and extracts the list + of attributes. + """ + @spec extract_attrs_from_block_with_defaults(any()) :: [attr] + def extract_attrs_from_block_with_defaults(block) do + block + |> extract_attrs_from_block + |> put_into_list_if_not_already + |> insert_default_attrs_if_missing + end + + @doc """ + Takes an attribute or a list of attrs and validate them. + + Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes. + """ + @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}] + def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1) + def validate(attr), do: validate([attr]) |> List.first + + @doc """ + Returns the type of an attribute, given its name. + """ + @spec get_type_for_attr(atom()) :: atom() + def get_type_for_attr(attr_name) do + @attr_supported + |> Keyword.get(attr_name) + |> Keyword.get(:type) + end + + @doc """ + Returns the default value for an attribute, given its name. + """ + @spec get_default_for_attr(atom()) :: any() + def get_default_for_attr(attr_name) do + @attr_supported + |> Keyword.get(attr_name) + |> Keyword.get(:default) + end + + # Private API + + # Given an elixir block (do...end) returns a list with the annotations + # or a single annotation. + @spec extract_attrs_from_block(any()) :: [attr] | attr + defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1) + defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs) + defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value} + defp extract_attrs_from_block(nil), do: [] + + # In case extract_attrs_from_block returns a single attribute, + # then put it into a list. (Ensures attrs are always into a list). + @spec put_into_list_if_not_already([attr] | attr) :: [attr] + defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs + defp put_into_list_if_not_already(attr), do: [attr] + + # Given a list of attributes, it inserts the missing attribute with their + # default value. + @spec insert_default_attrs_if_missing([attr]) :: [attr] + defp insert_default_attrs_if_missing(attrs) do + Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) -> + case Keyword.has_key?(acc, attr_name) do + true -> acc + false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name)) + end + end + end + + # Given an attribute, validates it and return a tuple with + # {:ok, attr} or {:error, attr, cause} + @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()} + defp valid_attr?({attr_name, param} = attr) do + case Keyword.get(@attr_supported, attr_name) do + nil -> {:error, attr, :attr_not_supported} + [{:type, param_type} | _] -> case is_of_type?(param, param_type) do + true -> {:ok, attr} + false -> {:error, attr, :type_not_supported} + end + end + end + + # Given an attribute value and a type, it returns a true + # if the value its of the type specified, false otherwise. + + # Usefoul for checking if an attr value respects the type + # specified for the annotation. + @spec is_of_type?(any(), atom()) :: boolean() + defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true + defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true + defp is_of_type?(param, type) when type == :list and is_list(param), do: true + defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true + defp is_of_type?(_param, type) when type == :any, do: true + defp is_of_type?(_, _), do: false +end diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex new file mode 100644 index 000000000..4d1270bc1 --- /dev/null +++ b/lib/ejabberd/config/config.ex @@ -0,0 +1,145 @@ +defmodule Ejabberd.Config do + @moduledoc """ + Base module for configuration file. + + Imports macros for the config DSL and contains functions + for working/starting the configuration parsed. + """ + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Attr + alias Ejabberd.Config.EjabberdLogger + + defmacro __using__(_opts) do + quote do + import Ejabberd.Config, only: :macros + import Ejabberd.Logger + + @before_compile Ejabberd.Config + end + end + + # Validate the modules parsed and log validation errors at compile time. + # Could be also possible to interrupt the compilation&execution by throwing + # an exception if necessary. + def __before_compile__(_env) do + get_modules_parsed_in_order + |> EjabberdModule.validate + |> EjabberdLogger.log_errors + end + + @doc """ + Given the path of the config file, it evaluates it. + """ + def init(file_path, force \\ false) do + init_already_executed = Ejabberd.Config.Store.get(:module_name) != [] + + case force do + true -> + Ejabberd.Config.Store.stop + Ejabberd.Config.Store.start_link + do_init(file_path) + false -> + if not init_already_executed, do: do_init(file_path) + end + end + + @doc """ + Returns a list with all the opts, formatted for ejabberd. + """ + def get_ejabberd_opts do + get_general_opts + |> Dict.put(:modules, get_modules_parsed_in_order()) + |> Dict.put(:listeners, get_listeners_parsed_in_order()) + |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd + end + + @doc """ + Register the hooks defined inside the elixir config file. + """ + def start_hooks do + get_hooks_parsed_in_order() + |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1) + end + + ### + ### MACROS + ### + + defmacro listen(module, do: block) do + attrs = Attr.extract_attrs_from_block_with_defaults(block) + + quote do + Ejabberd.Config.Store.put(:listeners, %EjabberdModule{ + module: unquote(module), + attrs: unquote(attrs) + }) + end + end + + defmacro module(module, do: block) do + attrs = Attr.extract_attrs_from_block_with_defaults(block) + + quote do + Ejabberd.Config.Store.put(:modules, %EjabberdModule{ + module: unquote(module), + attrs: unquote(attrs) + }) + end + end + + defmacro hook(hook_name, opts, fun) do + quote do + Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{ + hook: unquote(hook_name), + opts: unquote(opts), + fun: unquote(fun) + }) + end + end + + # Private API + + defp do_init(file_path) do + # File evaluation + Code.eval_file(file_path) |> extract_and_store_module_name() + + # Getting start/0 config + Ejabberd.Config.Store.get(:module_name) + |> case do + nil -> IO.puts "[ ERR ] Configuration module not found." + [module] -> call_start_func_and_store_data(module) + end + + # Fetching git modules and install them + get_modules_parsed_in_order() + |> EjabberdModule.fetch_git_repos + end + + # Returns the modules from the store + defp get_modules_parsed_in_order, + do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse + + # Returns the listeners from the store + defp get_listeners_parsed_in_order, + do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse + + defp get_hooks_parsed_in_order, + do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse + + # Returns the general config options + defp get_general_opts, + do: Ejabberd.Config.Store.get(:general) |> List.first + + # Gets the general ejabberd options calling + # the start/0 function and stores them. + defp call_start_func_and_store_data(module) do + opts = apply(module, :start, []) + Ejabberd.Config.Store.put(:general, opts) + end + + # Stores the configuration module name + defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do + Ejabberd.Config.Store.put(:module_name, mod) + end +end diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex new file mode 100644 index 000000000..8b7858d23 --- /dev/null +++ b/lib/ejabberd/config/ejabberd_hook.ex @@ -0,0 +1,23 @@ +defmodule Ejabberd.Config.EjabberdHook do + @moduledoc """ + Module containing functions for manipulating + ejabberd hooks. + """ + + defstruct hook: nil, opts: [], fun: nil + + alias Ejabberd.Config.EjabberdHook + + @type t :: %EjabberdHook{} + + @doc """ + Register a hook to ejabberd. + """ + @spec start(EjabberdHook.t) :: none + def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do + host = Keyword.get(opts, :host, :global) + priority = Keyword.get(opts, :priority, 50) + + :ejabberd_hooks.add(hook, host, fun, priority) + end +end diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex new file mode 100644 index 000000000..4de9a302e --- /dev/null +++ b/lib/ejabberd/config/ejabberd_module.ex @@ -0,0 +1,70 @@ +defmodule Ejabberd.Config.EjabberdModule do + @moduledoc """ + Module representing a module block in the configuration file. + It offers functions for validation and for starting the modules. + + Warning: The name is EjabberdModule to not collide with + the already existing Elixir.Module. + """ + + @type t :: %{module: atom, attrs: [Attr.t]} + + defstruct [:module, :attrs] + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Attr + alias Ejabberd.Config.Validation + + @doc """ + Given a list of modules / single module + it runs different validators on them. + + For each module, returns a {:ok, mod} or {:error, mod, errors} + """ + def validate(modules) do + Validation.validate(modules) + end + + @doc """ + Given a list of modules, it takes only the ones with + a git attribute and tries to fetch the repo, + then, it install them through :ext_mod.install/1 + """ + @spec fetch_git_repos([EjabberdModule.t]) :: none() + def fetch_git_repos(modules) do + modules + |> Enum.filter(&is_git_module?/1) + |> Enum.each(&fetch_and_install_git_module/1) + end + + # Private API + + defp is_git_module?(%EjabberdModule{attrs: attrs}) do + case Keyword.get(attrs, :git) do + "" -> false + repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/) + end + end + + defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do + repo = Keyword.get(attrs, :git) + mod_name = case Keyword.get(attrs, :name) do + "" -> infer_mod_name_from_git_url(repo) + name -> name + end + + path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}" + fetch_and_store_repo_source_if_not_exists(path, repo) + :ext_mod.install(mod_name) # Have to check if overwrites an already present mod + end + + defp fetch_and_store_repo_source_if_not_exists(path, repo) do + unless File.exists?(path) do + IO.puts "[info] Fetching: #{repo}" + :os.cmd('git clone #{repo} #{path}') + end + end + + defp infer_mod_name_from_git_url(repo), + do: String.split(repo, "/") |> List.last |> String.replace(".git", "") +end diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex new file mode 100644 index 000000000..270fbfaa6 --- /dev/null +++ b/lib/ejabberd/config/logger/ejabberd_logger.ex @@ -0,0 +1,32 @@ +defmodule Ejabberd.Config.EjabberdLogger do + @moduledoc """ + Module used to log validation errors given validated modules + given validated modules. + """ + + alias Ejabberd.Config.EjabberdModule + + @doc """ + Given a list of modules validated, in the form of {:ok, mod} or + {:error, mod, errors}, it logs to the user the errors found. + """ + @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t] + def log_errors(modules_validated) when is_list(modules_validated) do + Enum.each modules_validated, &do_log_errors/1 + modules_validated + end + + defp do_log_errors({:ok, _mod}), do: nil + defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1 + defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1 + defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1 + + defp log_attribute_error({{attr_name, val}, :attr_not_supported}), do: + IO.puts "[ WARN ] Annotation @#{attr_name} is not supported." + + defp log_attribute_error({{attr_name, val}, :type_not_supported}), do: + IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)." + + defp log_dependency_error({module, :not_found}), do: + IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency." +end diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex new file mode 100644 index 000000000..b7010ddfe --- /dev/null +++ b/lib/ejabberd/config/opts_formatter.ex @@ -0,0 +1,46 @@ +defmodule Ejabberd.Config.OptsFormatter do + @moduledoc """ + Module for formatting options parsed into the format + ejabberd uses. + """ + + alias Ejabberd.Config.EjabberdModule + + @doc """ + Takes a keyword list with keys corresponding to + the keys requested by the ejabberd config (ex: modules: mods) + and formats them to be correctly evaluated by ejabberd. + + Look at how Config.get_ejabberd_opts/0 is constructed for + more informations. + """ + @spec format_opts_for_ejabberd([{atom(), any()}]) :: list() + def format_opts_for_ejabberd(opts) do + opts + |> format_attrs_for_ejabberd + end + + defp format_attrs_for_ejabberd(opts) when is_list(opts), + do: Enum.map opts, &format_attrs_for_ejabberd/1 + + defp format_attrs_for_ejabberd({:listeners, mods}), + do: {:listen, format_listeners_for_ejabberd(mods)} + + defp format_attrs_for_ejabberd({:modules, mods}), + do: {:modules, format_mods_for_ejabberd(mods)} + + defp format_attrs_for_ejabberd({key, opts}) when is_atom(key), + do: {key, opts} + + defp format_mods_for_ejabberd(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + {mod, attrs[:opts]} + end + end + + defp format_listeners_for_ejabberd(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + Keyword.put(attrs[:opts], :module, mod) + end + end +end diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex new file mode 100644 index 000000000..72beea64c --- /dev/null +++ b/lib/ejabberd/config/store.ex @@ -0,0 +1,55 @@ +defmodule Ejabberd.Config.Store do + @moduledoc """ + Module used for storing the modules parsed from + the configuration file. + + Example: + - Store.put(:modules, mod1) + - Store.put(:modules, mod2) + + - Store.get(:modules) :: [mod1, mod2] + + Be carefoul: when retrieving data you get them + in the order inserted into the store, which normally + is the reversed order of how the modules are specified + inside the configuration file. To resolve this just use + a Enum.reverse/1. + """ + + @name __MODULE__ + + def start_link do + Agent.start_link(fn -> %{} end, name: @name) + end + + @doc """ + Stores a value based on the key. If the key already exists, + then it inserts the new element, maintaining all the others. + It uses a list for this. + """ + @spec put(atom, any) :: :ok + def put(key, val) do + Agent.update @name, &Map.update(&1, key, [val], fn coll -> + [val | coll] + end) + end + + @doc """ + Gets a value based on the key passed. + Returns always a list. + """ + @spec get(atom) :: [any] + def get(key) do + Agent.get @name, &Map.get(&1, key, []) + end + + @doc """ + Stops the store. + It uses Agent.stop underneath, so be aware that exit + could be called. + """ + @spec stop() :: :ok + def stop do + Agent.stop @name + end +end diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex new file mode 100644 index 000000000..2fe00361a --- /dev/null +++ b/lib/ejabberd/config/validator/validation.ex @@ -0,0 +1,40 @@ +defmodule Ejabberd.Config.Validation do + @moduledoc """ + Module used to validate a list of modules. + """ + + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map} + + alias Ejabberd.Config.EjabberdModule + alias Ejabberd.Config.Attr + alias Ejabberd.Config.Validator + alias Ejabberd.Config.ValidatorUtility + + @doc """ + Given a module or a list of modules it runs validators on them + and returns {:ok, mod} or {:error, mod, errors}, for each + of them. + """ + @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result] + def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1)) + def validate(module), do: validate([module]) + + # Private API + + @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result + defp do_validate(modules, mod) do + {modules, mod, %{}} + |> Validator.Attrs.validate + |> Validator.Dependencies.validate + |> resolve_validation_result + end + + @spec resolve_validation_result(mod_validation) :: mod_validation_result + defp resolve_validation_result({_modules, mod, errors}) do + case errors do + err when err == %{} -> {:ok, mod} + err -> {:error, mod, err} + end + end +end diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex new file mode 100644 index 000000000..94117ab21 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_attrs.ex @@ -0,0 +1,28 @@ +defmodule Ejabberd.Config.Validator.Attrs do + @moduledoc """ + Validator module used to validate attributes. + """ + + # TODO: Duplicated from validator.ex !!! + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + + import Ejabberd.Config.ValidatorUtility + alias Ejabberd.Config.Attr + + @doc """ + Given a module (with the form used for validation) + it runs Attr.validate/1 on each attribute and + returns the validation tuple with the errors updated, if found. + """ + @spec validate(mod_validation) :: mod_validation + def validate({modules, mod, errors}) do + errors = Enum.reduce mod.attrs, errors, fn(attr, err) -> + case Attr.validate(attr) do + {:ok, attr} -> err + {:error, attr, cause} -> put_error(err, :attribute, {attr, cause}) + end + end + + {modules, mod, errors} + end +end diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex new file mode 100644 index 000000000..d44c8a136 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_dependencies.ex @@ -0,0 +1,30 @@ +defmodule Ejabberd.Config.Validator.Dependencies do + @moduledoc """ + Validator module used to validate dependencies specified + with the @dependency annotation. + """ + + # TODO: Duplicated from validator.ex !!! + @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map} + import Ejabberd.Config.ValidatorUtility + + @doc """ + Given a module (with the form used for validation) + it checks if the @dependency annotation is respected and + returns the validation tuple with the errors updated, if found. + """ + @spec validate(mod_validation) :: mod_validation + def validate({modules, mod, errors}) do + module_names = extract_module_names(modules) + dependencies = mod.attrs[:dependency] + + errors = Enum.reduce dependencies, errors, fn(req_module, err) -> + case req_module in module_names do + true -> err + false -> put_error(err, :dependency, {req_module, :not_found}) + end + end + + {modules, mod, errors} + end +end diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex new file mode 100644 index 000000000..17805f748 --- /dev/null +++ b/lib/ejabberd/config/validator/validator_utility.ex @@ -0,0 +1,30 @@ +defmodule Ejabberd.Config.ValidatorUtility do + @moduledoc """ + Module used as a base validator for validation modules. + Imports utility functions for working with validation structures. + """ + + alias Ejabberd.Config.EjabberdModule + + @doc """ + Inserts an error inside the errors collection, for the given key. + If the key doesn't exists then it creates an empty collection + and inserts the value passed. + """ + @spec put_error(map, atom, any) :: map + def put_error(errors, key, val) do + Map.update errors, key, [val], fn coll -> + [val | coll] + end + end + + @doc """ + Given a list of modules it extracts and returns a list + of the module names (which are Elixir.Module). + """ + @spec extract_module_names(EjabberdModule.t) :: [atom] + def extract_module_names(modules) when is_list(modules) do + modules + |> Enum.map(&Map.get(&1, :module)) + end +end diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex new file mode 100644 index 000000000..6592104a2 --- /dev/null +++ b/lib/ejabberd/config_util.ex @@ -0,0 +1,18 @@ +defmodule Ejabberd.ConfigUtil do + @moduledoc """ + Module containing utility functions for + the config file. + """ + + @doc """ + Returns true when the config file is based on elixir. + """ + @spec is_elixir_config(list) :: boolean + def is_elixir_config(filename) when is_list(filename) do + is_elixir_config(to_string(filename)) + end + + def is_elixir_config(filename) do + String.ends_with?(filename, "exs") + end +end diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex new file mode 100644 index 000000000..9fb3f040c --- /dev/null +++ b/lib/ejabberd/module.ex @@ -0,0 +1,19 @@ +defmodule Ejabberd.Module do + + defmacro __using__(opts) do + logger_enabled = Keyword.get(opts, :logger, true) + + quote do + @behaviour :gen_mod + import Ejabberd.Module + + unquote(if logger_enabled do + quote do: import Ejabberd.Logger + end) + end + end + + # gen_mod callbacks + def depends(_host, _opts), do: [] + def mod_opt_type(_), do: [] +end diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex new file mode 100644 index 000000000..94cb85a50 --- /dev/null +++ b/lib/mix/tasks/deps.tree.ex @@ -0,0 +1,94 @@ +defmodule Mix.Tasks.Ejabberd.Deps.Tree do + use Mix.Task + + alias Ejabberd.Config.EjabberdModule + + @shortdoc "Lists all ejabberd modules and their dependencies" + + @moduledoc """ + Lists all ejabberd modules and their dependencies. + + The project must have ejabberd as a dependency. + """ + + def run(_argv) do + # First we need to start manually the store to be available + # during the compilation of the config file. + Ejabberd.Config.Store.start_link + Ejabberd.Config.init(:ejabberd_config.get_ejabberd_config_path()) + + Mix.shell.info "ejabberd modules" + + Ejabberd.Config.Store.get(:modules) + |> Enum.reverse # Because of how mods are stored inside the store + |> format_mods + |> Mix.shell.info + end + + defp format_mods(mods) when is_list(mods) do + deps_tree = build_dependency_tree(mods) + mods_used_as_dependency = get_mods_used_as_dependency(deps_tree) + + keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency) + |> format_mods_into_string + end + + defp build_dependency_tree(mods) do + Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} -> + deps = attrs[:dependency] + build_dependency_tree(mods, mod, deps) + end + end + + defp build_dependency_tree(mods, mod, []), do: %{module: mod, dependency: []} + defp build_dependency_tree(mods, mod, deps) when is_list(deps) do + dependencies = Enum.map deps, fn dep -> + dep_deps = get_dependencies_of_mod(mods, dep) + build_dependency_tree(mods, dep, dep_deps) + end + + %{module: mod, dependency: dependencies} + end + + defp get_mods_used_as_dependency(mods) when is_list(mods) do + Enum.reduce mods, [], fn(mod, acc) -> + case mod do + %{dependency: []} -> acc + %{dependency: deps} -> get_mod_names(deps) ++ acc + end + end + end + + defp get_mod_names([]), do: [] + defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten + defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)] + + defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do + Enum.filter mods, fn %{module: mod} -> + not mod in mods_used_as_dep + end + end + + defp get_dependencies_of_mod(deps, mod_name) do + Enum.find(deps, &(Map.get(&1, :module) == mod_name)) + |> Map.get(:attrs) + |> Keyword.get(:dependency) + end + + defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0) + defp format_mods_into_string([], _indentation), do: "" + defp format_mods_into_string(mods, indentation) when is_list(mods) do + Enum.reduce mods, "", fn(mod, acc) -> + acc <> format_mods_into_string(mod, indentation) + end + end + + defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do + "\n├── #{mod}" <> format_mods_into_string(deps, 2) + end + + defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do + spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end + "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4) + end +end diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex index ba5abe90e..09bf58405 100644 --- a/lib/mod_presence_demo.ex +++ b/lib/mod_presence_demo.ex @@ -1,16 +1,15 @@ defmodule ModPresenceDemo do - import Ejabberd.Logger # this allow using info, error, etc for logging - @behaviour :gen_mod + use Ejabberd.Module def start(host, _opts) do info('Starting ejabberd module Presence Demo') - Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50) + Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50) :ok end def stop(host) do info('Stopping ejabberd module Presence Demo') - Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50) + Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50) :ok end @@ -18,9 +17,4 @@ defmodule ModPresenceDemo do info('Receive presence for #{user}') :none end - - # gen_mod callbacks - def depends(_host, _opts), do: [] - def mod_opt_type(_), do: [] - end diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 3b333b3b5..9d127e748 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -56,6 +56,7 @@ start(normal, _Args) -> ejabberd_admin:start(), gen_mod:start(), ext_mod:start(), + setup_if_elixir_conf_used(), ejabberd_config:start(), set_settings_from_config(), acl:start(), @@ -76,6 +77,7 @@ start(normal, _Args) -> gen_mod:start_modules(), ejabberd_listener:start_listeners(), ejabberd_service:start(), + register_elixir_config_hooks(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), Sup; start(_, _) -> @@ -240,6 +242,18 @@ opt_type(modules) -> end; opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. +setup_if_elixir_conf_used() -> + case ejabberd_config:is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config.Store':start_link(); + false -> ok + end. + +register_elixir_config_hooks() -> + case ejabberd_config:is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config':start_hooks(); + false -> ok + end. + start_elixir_application() -> case application:ensure_started(elixir) of ok -> ok; diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index b75883fb2..7d5dfbc0c 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -33,6 +33,7 @@ get_option/2, get_option/3, add_option/2, has_option/1, get_vh_by_auth_method/1, is_file_readable/1, get_version/0, get_myhosts/0, get_mylang/0, + get_ejabberd_config_path/0, is_using_elixir_config/0, prepare_opt_val/4, convert_table_to_binary/5, transform_options/1, collect_options/1, default_db/2, convert_to_yaml/1, convert_to_yaml/2, v_db/2, @@ -147,7 +148,13 @@ read_file(File) -> {include_modules_configs, true}]). read_file(File, Opts) -> - Terms1 = get_plain_terms_file(File, Opts), + Terms1 = case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of + true -> + 'Elixir.Ejabberd.Config':init(File), + 'Elixir.Ejabberd.Config':get_ejabberd_opts(); + false -> + get_plain_terms_file(File, Opts) + end, Terms_macros = case proplists:get_bool(replace_macros, Opts) of true -> replace_macros(Terms1); false -> Terms1 @@ -1042,6 +1049,10 @@ replace_modules(Modules) -> %% Elixir module naming %% ==================== +is_using_elixir_config() -> + Config = get_ejabberd_config_path(), + 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config). + %% If module name start with uppercase letter, this is an Elixir module: is_elixir_module(Module) -> case atom_to_list(Module) of diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index a79f26305..31f80be78 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) -> DefinedHandlers = gen_mod:get_opt( request_handlers, Opts, fun(Hs) -> + Hs1 = lists:map(fun + ({Mod, Path}) when is_atom(Mod) -> {Path, Mod}; + ({Path, Mod}) -> {Path, Mod} + end, Hs), + [{str:tokens( iolist_to_binary(Path), <<"/">>), - Mod} || {Path, Mod} <- Hs] + Mod} || {Path, Mod} <- Hs1] end, []), RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ Admin ++ Bind ++ XMLRPC, diff --git a/test/elixir-config/attr_test.exs b/test/elixir-config/attr_test.exs new file mode 100644 index 000000000..c5cab5bd8 --- /dev/null +++ b/test/elixir-config/attr_test.exs @@ -0,0 +1,87 @@ +defmodule Ejabberd.Config.AttrTest do + use ExUnit.Case, async: true + + alias Ejabberd.Config.Attr + + test "extract attrs from single line block" do + block = quote do + @active false + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + end + + test "extract attrs from multi line block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + assert {:opts, [http: true]} in block_res + end + + test "inserts correctly defaults attr when missing in block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, false} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, [http: true]} in block_res + assert {:dependency, []} in block_res + end + + test "inserts all defaults attr when passed an empty block" do + block = quote do + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, true} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, []} in block_res + assert {:dependency, []} in block_res + end + + test "validates attrs and returns errors, if any" do + block = quote do + @not_supported_attr true + @active "false" + @opts [http: true] + end + + block_res = + block + |> Attr.extract_attrs_from_block_with_defaults + |> Attr.validate + + assert {:ok, {:opts, [http: true]}} in block_res + assert {:ok, {:git, ""}} in block_res + assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res + assert {:error, {:active, "false"}, :type_not_supported} in block_res + end + + test "returns the correct type for an attribute" do + assert :boolean == Attr.get_type_for_attr(:active) + assert :string == Attr.get_type_for_attr(:git) + assert :string == Attr.get_type_for_attr(:name) + assert :list == Attr.get_type_for_attr(:opts) + assert :list == Attr.get_type_for_attr(:dependency) + end + + test "returns the correct default for an attribute" do + assert true == Attr.get_default_for_attr(:active) + assert "" == Attr.get_default_for_attr(:git) + assert "" == Attr.get_default_for_attr(:name) + assert [] == Attr.get_default_for_attr(:opts) + assert [] == Attr.get_default_for_attr(:dependency) + end +end diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs new file mode 100644 index 000000000..c359c49c3 --- /dev/null +++ b/test/elixir-config/config_test.exs @@ -0,0 +1,65 @@ +defmodule Ejabberd.ConfigTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "extracts successfully the module name from config file" do + assert [Ejabberd.ConfigFile] == Store.get(:module_name) + end + + test "extracts successfully general opts from config file" do + [general] = Store.get(:general) + shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000] + assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general + end + + test "extracts successfully listeners from config file" do + [listen] = Store.get(:listeners) + assert :ejabberd_c2s == listen.module + assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts] + end + + test "extracts successfully modules from config file" do + [module] = Store.get(:modules) + assert :mod_adhoc == module.module + assert [] == module.attrs[:opts] + end + + test "extracts successfully hooks from config file" do + [register_hook] = Store.get(:hooks) + + assert :register_user == register_hook.hook + assert [host: "localhost"] == register_hook.opts + assert is_function(register_hook.fun) + end + + # TODO: When enalbed, this test causes the evaluation of a different config file, so + # the other tests, that uses the store, are compromised because the data is different. + # So, until a good way is found, this test should remain disabed. + # + # test "init/2 with force:true re-initializes the config store with new data" do + # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs" + # Config.init(config_file_path, true) + # + # assert [Ejabberd.ConfigFile] == Store.get(:module_name) + # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general) + # assert [] == Store.get(:modules) + # assert [] == Store.get(:listeners) + # + # Store.stop + # end +end diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs new file mode 100644 index 000000000..d13f79aa6 --- /dev/null +++ b/test/elixir-config/ejabberd_logger.exs @@ -0,0 +1,49 @@ +defmodule Ejabberd.Config.EjabberdLoggerTest do + use ExUnit.Case + + import ExUnit.CaptureIO + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + alias Ejabberd.Config.EjabberdLogger + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "outputs correctly when attr is not supported" do + error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n" + + [_mod_irc, _mod_configure, mod_time] = Store.get(:modules) + fun = fn -> + mod_time + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end + + test "outputs correctly when dependency is not found" do + error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n" + + [_mod_irc, mod_configure, _mod_time] = Store.get(:modules) + fun = fn -> + mod_configure + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end +end diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs new file mode 100644 index 000000000..5d0243bb5 --- /dev/null +++ b/test/elixir-config/shared/ejabberd.exs @@ -0,0 +1,31 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"], + shaper: shaper] + end + + defp shaper do + [normal: 1000, + fast: 50000, + max_fsm_queue: 1000] + end + + listen :ejabberd_c2s do + @opts [ + port: 5222, + max_stanza_size: 65536, + shaper: :c2s_shaper, + access: :c2s] + end + + module :mod_adhoc do + end + + hook :register_user, [host: "localhost"], fn(user, server) -> + info("User registered: #{user} on #{server}") + end +end diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs new file mode 100644 index 000000000..a39409683 --- /dev/null +++ b/test/elixir-config/shared/ejabberd_different_from_default.exs @@ -0,0 +1,9 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end +end diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs new file mode 100644 index 000000000..8c0196c7e --- /dev/null +++ b/test/elixir-config/shared/ejabberd_for_validation.exs @@ -0,0 +1,20 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end + + module :mod_time do + @attr_not_supported true + end + + module :mod_configure do + @dependency [:mod_adhoc] + end + + module :mod_irc do + end +end diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs new file mode 100644 index 000000000..1df775966 --- /dev/null +++ b/test/elixir-config/validation_test.exs @@ -0,0 +1,32 @@ +defmodule Ejabberd.Config.ValidationTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "validates correctly the modules" do + [mod_irc, mod_configure, mod_time] = Store.get(:modules) + + [{:error, _mod, errors}] = Validation.validate(mod_configure) + assert %{dependency: [mod_adhoc: :not_found]} == errors + + [{:error, _mod, errors}] = Validation.validate(mod_time) + assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors + + [{:ok, _mod}] = Validation.validate(mod_irc) + end +end From 36ab9cc2ea4ac1163905b178e3692e9578064524 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Thu, 8 Sep 2016 16:39:34 +0300 Subject: [PATCH 119/179] Fix message routing from subscribers --- src/mod_muc_room.erl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 11cc1d5f5..ef94c3cba 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -139,7 +139,8 @@ normal_state({route, From, <<"">>, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), case is_user_online(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData) + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of true -> case fxml:get_attr_s(<<"type">>, Attrs) of @@ -527,7 +528,8 @@ normal_state({route, From, ToNick, continue_delivery -> case {(StateData#state.config)#config.allow_private_messages, - is_user_online(From, StateData)} + is_user_online(From, StateData) orelse + is_subscriber(From, StateData)} of {true, true} -> case Type of @@ -562,9 +564,7 @@ normal_state({route, From, ToNick, PmFromVisitors == anyone; (PmFromVisitors == moderators) and DstIsModerator -> - {ok, #user{nick = FromNick}} = - (?DICT):find(jid:tolower(From), - StateData#state.users), + {FromNick, _} = get_participant_data(From, StateData), FromNickJID = jid:replace_resource(StateData#state.jid, FromNick), @@ -1067,7 +1067,14 @@ get_participant_data(From, StateData) -> of {ok, #user{nick = FromNick, role = Role}} -> {FromNick, Role}; - error -> {<<"">>, moderator} + error -> + case ?DICT:find(jid:tolower(jid:remove_resource(From)), + StateData#state.subscribers) of + {ok, #subscriber{nick = FromNick}} -> + {FromNick, none}; + error -> + {<<"">>, moderator} + end end. process_presence(From, Nick, From ad39da0b0a90dab159625bfb3ba47727582cd8b8 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 8 Sep 2016 16:27:16 +0200 Subject: [PATCH 120/179] Full jid entity subscriptions should include bare jid records (#1284) --- src/node_flat_sql.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index fa4af4d57..1dd44d4d8 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -946,8 +946,9 @@ select_affiliation_subscriptions(Nidx, JID, JID) -> select_affiliation_subscriptions(Nidx, JID); select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> {result, Affiliation} = get_affiliation(Nidx, GenKey), - {result, Subscriptions} = get_subscriptions(Nidx, SubKey), - {Affiliation, Subscriptions}. + {result, BareJidSubs} = get_subscriptions(Nidx, GenKey), + {result, FullJidSubs} = get_subscriptions(Nidx, SubKey), + {Affiliation, BareJidSubs++FullJidSubs}; update_affiliation(Nidx, JID, Affiliation) -> J = encode_jid(JID), From 4bd45bada70fc233bb87bc7ab099fc2407cea30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 8 Sep 2016 16:29:19 +0200 Subject: [PATCH 121/179] Start elixir config code only if elixir was enabled in configure script --- rebar.config | 1 + src/ejabberd_app.erl | 13 +++++++++---- src/ejabberd_config.erl | 39 +++++++++++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/rebar.config b/rebar.config index 06a7d350a..3c9610021 100644 --- a/rebar.config +++ b/rebar.config @@ -74,6 +74,7 @@ {if_var_true, debug, debug_info}, {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}}, {if_var_match, db_type, mssql, {d, 'mssql'}}, + {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, {if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}}, {if_var_true, hipe, native}, {src_dirs, [asn1, src, diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 9d127e748..890ab6f90 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -255,7 +255,12 @@ register_elixir_config_hooks() -> end. start_elixir_application() -> - case application:ensure_started(elixir) of - ok -> ok; - {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) - end. + case ejabberd_config:is_elixir_enabled() of + true -> + case application:ensure_started(elixir) of + ok -> ok; + {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) + end; + _ -> + ok + end. diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 7d5dfbc0c..6ca6a40a8 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -37,7 +37,8 @@ prepare_opt_val/4, convert_table_to_binary/5, transform_options/1, collect_options/1, default_db/2, convert_to_yaml/1, convert_to_yaml/2, v_db/2, - env_binary_to_list/2, opt_type/1, may_hide_data/1]). + env_binary_to_list/2, opt_type/1, may_hide_data/1, + is_elixir_enabled/0]). -export([start/2]). @@ -148,13 +149,18 @@ read_file(File) -> {include_modules_configs, true}]). read_file(File, Opts) -> - Terms1 = case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of - true -> - 'Elixir.Ejabberd.Config':init(File), - 'Elixir.Ejabberd.Config':get_ejabberd_opts(); - false -> - get_plain_terms_file(File, Opts) - end, + Terms1 = case is_elixir_enabled() of + true -> + case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of + true -> + 'Elixir.Ejabberd.Config':init(File), + 'Elixir.Ejabberd.Config':get_ejabberd_opts(); + false -> + get_plain_terms_file(File, Opts) + end; + false -> + get_plain_terms_file(File, Opts) + end, Terms_macros = case proplists:get_bool(replace_macros, Opts) of true -> replace_macros(Terms1); false -> Terms1 @@ -1049,9 +1055,22 @@ replace_modules(Modules) -> %% Elixir module naming %% ==================== +-ifdef(ELIXIR_ENABLED). +is_elixir_enabled() -> + true. +-else. +is_elixir_enabled() -> + false. +-endif. + is_using_elixir_config() -> - Config = get_ejabberd_config_path(), - 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config). + case is_elixir_enabled() of + true -> + Config = get_ejabberd_config_path(), + 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config); + false -> + false + end. %% If module name start with uppercase letter, this is an Elixir module: is_elixir_module(Module) -> From 26a040e2d5e6621d24ef57ddadffa70af5e77708 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 8 Sep 2016 16:31:59 +0200 Subject: [PATCH 122/179] Fix typo on previous commit (#1284) --- src/node_flat_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 1dd44d4d8..86375eaec 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -948,7 +948,7 @@ select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> {result, Affiliation} = get_affiliation(Nidx, GenKey), {result, BareJidSubs} = get_subscriptions(Nidx, GenKey), {result, FullJidSubs} = get_subscriptions(Nidx, SubKey), - {Affiliation, BareJidSubs++FullJidSubs}; + {Affiliation, BareJidSubs++FullJidSubs}. update_affiliation(Nidx, JID, Affiliation) -> J = encode_jid(JID), From 41386d718dbc5f6e846baaac568ba1cae5eb7ec4 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Thu, 8 Sep 2016 17:59:40 +0200 Subject: [PATCH 123/179] Don't log an [error] message if Elixir is missing The Elixir support is still optional. Closes #1250. --- src/ejabberd_app.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 890ab6f90..fc9c5a428 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -259,7 +259,7 @@ start_elixir_application() -> true -> case application:ensure_started(elixir) of ok -> ok; - {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) + {error, _Msg} -> ?INFO_MSG("Elixir application not started.", []) end; _ -> ok From 8fd888eb2b7092bd47ddf339d8fb5b732202fd13 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Thu, 8 Sep 2016 18:11:54 +0200 Subject: [PATCH 124/179] Revert "Don't log an [error] message if Elixir is missing" This reverts commit 41386d718dbc5f6e846baaac568ba1cae5eb7ec4. The issue was fixed with commit 4bd45bada70fc233bb87bc7ab099fc2407cea30b. --- src/ejabberd_app.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index fc9c5a428..890ab6f90 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -259,7 +259,7 @@ start_elixir_application() -> true -> case application:ensure_started(elixir) of ok -> ok; - {error, _Msg} -> ?INFO_MSG("Elixir application not started.", []) + {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) end; _ -> ok From d222fed2285d9ff630c5b230118f20dca2279cbd Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 9 Sep 2016 00:21:36 +0200 Subject: [PATCH 125/179] XEP-0198: Cancel timer when waiting for resumption If an ACK timer is active while going into the 'wait_for_resume' state, cancel that timer. --- src/ejabberd_c2s.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 09df739b4..1bcb77e7f 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -1772,7 +1772,7 @@ handle_info(close, StateName, StateData) -> ?DEBUG("Timeout waiting for stream management acknowledgement of ~s", [jid:to_string(StateData#state.jid)]), close(self()), - fsm_next_state(StateName, StateData); + fsm_next_state(StateName, StateData#state{mgmt_ack_timer = undefined}); handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) -> %% This happens if the resume_session/1 request timed out; the new session %% now receives the late response. @@ -2495,6 +2495,12 @@ fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) -> {stop, normal, StateData}; fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} = StateData) -> + case StateData of + #state{mgmt_ack_timer = undefined} -> + ok; + #state{mgmt_ack_timer = Timer} -> + erlang:cancel_timer(Timer) + end, ?INFO_MSG("Waiting for resumption of stream for ~s", [jid:to_string(StateData#state.jid)]), {next_state, wait_for_resume, From fe1bf27ef380d4fbb896983573c5b2fd94029c6d Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Fri, 9 Sep 2016 13:04:47 +0300 Subject: [PATCH 126/179] Fix subscribed rooms list retreivement --- src/mod_muc.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 85c0914d1..4eb129a87 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -720,10 +720,11 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> get_subscribed_rooms(ServerHost, Host, From) -> Rooms = get_rooms(ServerHost, Host), + BareFrom = jid:remove_resource(From), lists:flatmap( fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> Subscribers = proplists:get_value(subscribers, Opts, []), - case lists:keymember(From, 1, Subscribers) of + case lists:keymember(BareFrom, 1, Subscribers) of true -> [jid:make(Name, Host, <<>>)]; false -> [] end; From d0761039ff9b69152854a90fb7a74956e4dff77c Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 9 Sep 2016 12:17:54 +0200 Subject: [PATCH 127/179] Support multiple room invitations (#1285) --- src/mod_muc_room.erl | 63 ++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index ef94c3cba..340f935ec 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -253,20 +253,13 @@ normal_state({route, From, <<"">>, Err = jlib:make_error_reply(Packet, Error), ejabberd_router:route(StateData#state.jid, From, Err), {next_state, normal_state, StateData}; - IJID -> + IJIDs -> Config = StateData#state.config, case Config#config.members_only of true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation(IJID, member, - StateData), - send_affiliation(IJID, member, - StateData), - store_room(NSD), - {next_state, normal_state, NSD}; - _ -> {next_state, normal_state, StateData} - end; + NSD = process_invitees(IJIDs, StateData), + store_room(NSD), + {next_state, normal_state, NSD}; false -> {next_state, normal_state, StateData} end end; @@ -4914,18 +4907,35 @@ is_invitation(Els) -> end, false, Els). +process_invitees(Invetees, StateDataIni) -> + lists:foldl( + fun(IJID, StateData) -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, + StateData), + send_affiliation(IJID, member, + StateData), + NSD; + _ -> StateData + end + end, + StateDataIni, + Invetees). + check_invitation(From, Packet, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of + + InviteEls = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of false -> Txt1 = <<"No 'x' element found">>, throw({error, ?ERRT_BAD_REQUEST(Lang, Txt1)}); XEl -> - case fxml:get_subtag(XEl, <<"invite">>) of + case fxml:get_subtags(XEl, <<"invite">>) of false -> Txt2 = <<"No 'invite' element found">>, throw({error, ?ERRT_BAD_REQUEST(Lang, Txt2)}); @@ -4933,20 +4943,17 @@ check_invitation(From, Packet, Lang, StateData) -> InviteEl1 end end, - JID = case - jid:from_string(fxml:get_tag_attr_s(<<"to">>, - InviteEl)) - of - error -> - Txt = <<"Incorrect value of 'to' attribute">>, - throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); - JID1 -> JID1 - end, case CanInvite of false -> Txt3 = <<"Invitations are not allowed in this conference">>, throw({error, ?ERRT_NOT_ALLOWED(Lang, Txt3)}); true -> + process_invitations(From, InviteEls, Lang, StateData) + end. + +process_invitations(From, InviteEls, Lang, StateData) -> + lists:map( + fun(InviteEl) -> Reason = fxml:get_path_s(InviteEl, [{elem, <<"reason">>}, cdata]), ContinueEl = case fxml:get_path_s(InviteEl, @@ -5015,9 +5022,19 @@ check_invitation(From, Packet, Lang, StateData) -> <<"">>})}], children = [{xmlcdata, Reason}]}, Body]}, + JID = case + jid:from_string(fxml:get_tag_attr_s(<<"to">>, + InviteEl)) + of + error -> + Txt = <<"Incorrect value of 'to' attribute">>, + throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); + JID1 -> JID1 + end, ejabberd_router:route(StateData#state.jid, JID, Msg), JID - end. + end, + InviteEls). %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. From 1aca541639fd83c30a833fedffbed8bbfa67fcfa Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Mon, 12 Sep 2016 14:41:33 +0300 Subject: [PATCH 128/179] Fix nick-to-jid mapping for MUC subscribers --- include/mod_muc_room.hrl | 1 + src/mod_muc_room.erl | 45 +++++++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 47489f3d0..551da7285 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -106,6 +106,7 @@ config = #config{} :: config(), users = (?DICT):new() :: ?TDICT, subscribers = (?DICT):new() :: ?TDICT, + subscriber_nicks = (?DICT):new() :: ?TDICT, last_voice_request_time = treap:empty() :: treap:treap(), robots = (?DICT):new() :: ?TDICT, nicks = (?DICT):new() :: ?TDICT, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 340f935ec..ac2f5daf1 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -1743,12 +1743,15 @@ update_online_user(JID, #user{nick = Nick} = User, StateData) -> set_subscriber(JID, Nick, Nodes, StateData) -> BareJID = jid:remove_resource(JID), - Subscribers = ?DICT:store(jid:tolower(BareJID), + LBareJID = jid:tolower(BareJID), + Subscribers = ?DICT:store(LBareJID, #subscriber{jid = BareJID, nick = Nick, nodes = Nodes}, StateData#state.subscribers), - NewStateData = StateData#state{subscribers = Subscribers}, + Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks), + NewStateData = StateData#state{subscribers = Subscribers, + subscriber_nicks = Nicks}, store_room(NewStateData), NewStateData. @@ -1833,10 +1836,13 @@ add_user_presence_un(JID, Presence, StateData) -> %% Find and return a list of the full JIDs of the users of Nick. %% Return jid record. find_jids_by_nick(Nick, StateData) -> - case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [User]} -> [jid:make(User)]; - {ok, Users} -> [jid:make(LJID) || LJID <- Users]; - error -> false + Nicks = ?DICT:merge(fun(_, Val, _) -> Val end, + StateData#state.nicks, + StateData#state.subscriber_nicks), + case (?DICT):find(Nick, Nicks) of + {ok, [User]} -> [jid:make(User)]; + {ok, Users} -> [jid:make(LJID) || LJID <- Users]; + error -> false end. %% Find and return the full JID of the user of Nick with @@ -1903,7 +1909,14 @@ is_nick_change(JID, Nick, StateData) -> end. nick_collision(User, Nick, StateData) -> - UserOfNick = find_jid_by_nick(Nick, StateData), + UserOfNick = case find_jid_by_nick(Nick, StateData) of + false -> + case ?DICT:find(Nick, StateData#state.subscriber_nicks) of + {ok, [J]} -> J; + error -> false + end; + J -> J + end, (UserOfNick /= false andalso jid:remove_resource(jid:tolower(UserOfNick)) /= jid:remove_resource(jid:tolower(User))). @@ -4646,10 +4659,17 @@ process_iq_mucsub(From, _Packet, sub_el = #xmlel{name = <<"unsubscribe">>}}, StateData) -> LBareJID = jid:tolower(jid:remove_resource(From)), - Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers), - NewStateData = StateData#state{subscribers = Subscribers}, - store_room(NewStateData), - {result, [], NewStateData}; + case ?DICT:find(LBareJID, StateData#state.subscribers) of + {ok, #subscriber{nick = Nick}} -> + Nicks = ?DICT:erase(Nick, StateData#state.subscriber_nicks), + Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers), + NewStateData = StateData#state{subscribers = Subscribers, + subscriber_nicks = Nicks}, + store_room(NewStateData), + {result, [], NewStateData}; + error -> + {result, [], StateData} + end; process_iq_mucsub(From, _Packet, #iq{type = get, lang = Lang, sub_el = #xmlel{name = <<"subscriptions">>}}, @@ -4674,7 +4694,8 @@ process_iq_mucsub(_From, _Packet, #iq{lang = Lang}, _StateData) -> remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> - StateData#state{subscribers = ?DICT:new()}; + StateData#state{subscribers = ?DICT:new(), + subscriber_nicks = ?DICT:new()}; true -> StateData end. From 96d05dad8f84d81661685d38e1d06745842c789f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Mon, 12 Sep 2016 15:38:27 +0200 Subject: [PATCH 129/179] Properly process OPTIONS header in http_api for all paths --- src/mod_http_api.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 73e6f7e4e..95a4ad51e 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -259,8 +259,10 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; -process([], #request{method = 'OPTIONS', data = <<>>}) -> +process([_Call], #request{method = 'OPTIONS', data = <<>>}) -> {200, ?OPTIONS_HEADER, []}; +process(_, #request{method = 'OPTIONS'}) -> + {400, ?OPTIONS_HEADER, []}; process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), json_error(400, 40, <<"Missing command name.">>). From 054382f074b2739fc6380db83454d1aae26d2f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Mon, 12 Sep 2016 15:39:00 +0200 Subject: [PATCH 130/179] Add X-Admin and basic auth header to CORS allowed headers in http_api --- src/mod_http_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 95a4ad51e..e2f6cc240 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -101,7 +101,7 @@ -define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, - <<"Content-Type">>}). + <<"Content-Type, Authorization, X-Admin">>}). -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). From 2f596b0e1042607ef9b66b6ecbb5b18b87b073ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 4 Aug 2016 16:04:43 +0200 Subject: [PATCH 131/179] Expand parsing of json input to be able to handle update_roster command --- src/mod_http_api.erl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index e2f6cc240..dbca82375 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -387,6 +387,20 @@ format_args(Args, ArgsFormat) -> L when is_list(L) -> exit({additional_unused_args, L}) end. +format_arg({Elements}, + {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}}) + when is_list(Elements) andalso + (Tuple1S == binary orelse Tuple1S == string) -> + lists:map(fun({F1, F2}) -> + {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)}; + ({Val}) when is_list(Val) -> + format_arg({Val}, Tuple) + end, Elements); +format_arg(Elements, + {list, {_ElementDefName, {list, _} = ElementDefFormat}}) + when is_list(Elements) -> + [{format_arg(Element, ElementDefFormat)} + || Element <- Elements]; format_arg(Elements, {list, {_ElementDefName, ElementDefFormat}}) when is_list(Elements) -> @@ -494,6 +508,11 @@ format_result({Code, Text}, {Name, restuple}) -> {[{<<"res">>, Code == true orelse Code == ok}, {<<"text">>, iolist_to_binary(Text)}]}}; +format_result(Code, {Name, restuple}) -> + {jlib:atom_to_binary(Name), + {[{<<"res">>, Code == true orelse Code == ok}, + {<<"text">>, <<"">>}]}}; + format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> {jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; From c3543e002d60123bac90862b77021d8e9e34ac10 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Tue, 13 Sep 2016 11:52:59 +0200 Subject: [PATCH 132/179] Allow to create room with custom config --- src/mod_muc_admin.erl | 50 ++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 9ceec7d5b..e334dca2b 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -13,6 +13,7 @@ -export([start/2, stop/1, depends/2, muc_online_rooms/1, muc_unregister_nick/1, create_room/3, destroy_room/2, + create_room_with_opts/4, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, get_user_rooms/2, get_room_occupants/2, @@ -88,6 +89,18 @@ get_commands_spec() -> module = ?MODULE, function = create_rooms_file, args = [{file, string}], result = {res, rescode}}, + #ejabberd_commands{name = create_room_with_opts, tags = [muc_room], + desc = "Create a MUC room name@service in host with given options", + module = ?MODULE, function = create_room_with_opts, + args = [{name, binary}, {service, binary}, + {host, binary}, + {options, {list, + {option, {tuple, + [{name, binary}, + {value, binary} + ]}} + }}], + result = {res, rescode}}, #ejabberd_commands{name = destroy_rooms_file, tags = [muc], desc = "Destroy the rooms indicated in file", longdesc = "Provide one room JID per line.", @@ -416,15 +429,23 @@ prepare_room_info(Room_info) -> %% ok | error %% @doc Create a room immediately with the default options. create_room(Name1, Host1, ServerHost) -> + create_room_with_opts(Name1, Host1, ServerHost, []). + +create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) -> Name = jid:nodeprep(Name1), Host = jid:nodeprep(Host1), %% Get the default room options from the muc configuration DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc, default_room_options, fun(X) -> X end, []), + %% Change default room options as required + FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts], + RoomOpts = lists:ukeymerge(1, + lists:keysort(1, FormattedRoomOpts), + lists:keysort(1, DefRoomOpts)), %% Store the room on the server, it is not started yet though at this point - mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts), + mod_muc:store_room(ServerHost, Host, Name, RoomOpts), %% Get all remaining mod_muc parameters that might be utilized Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all), @@ -445,7 +466,7 @@ create_room(Name1, Host1, ServerHost) -> Name, HistorySize, RoomShaper, - DefRoomOpts), + RoomOpts), {atomic, ok} = register_room(Host, Name, Pid), ok; _ -> @@ -764,12 +785,20 @@ send_direct_invitation(FromJid, UserJid, XmlEl) -> %% the option to change (for example title or max_users), %% and the value to assign to the new option. %% For example: -%% change_room_option("testroom", "conference.localhost", "title", "Test Room") -change_room_option(Name, Service, Option, Value) when is_atom(Option) -> - Pid = get_room_pid(Name, Service), - {ok, _} = change_room_option(Pid, Option, Value), - ok; +%% change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>) change_room_option(Name, Service, OptionString, ValueString) -> + case get_room_pid(Name, Service) of + room_not_found -> + room_not_found; + Pid -> + {Option, Value} = format_room_option(OptionString, ValueString), + Config = get_room_config(Pid), + Config2 = change_option(Option, Value, Config), + {ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}), + ok + end. + +format_room_option(OptionString, ValueString) -> Option = jlib:binary_to_atom(OptionString), Value = case Option of title -> ValueString; @@ -780,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) -> max_users -> jlib:binary_to_integer(ValueString); _ -> jlib:binary_to_atom(ValueString) end, - change_room_option(Name, Service, Option, Value). - -change_room_option(Pid, Option, Value) -> - Config = get_room_config(Pid), - Config2 = change_option(Option, Value, Config), - gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}). + {Option, Value}. %% @doc Get the Pid of an existing MUC room, or 'room_not_found'. get_room_pid(Name, Service) -> From 27999a122fe5003093605c93889093727639d563 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Thu, 15 Sep 2016 23:02:04 +0200 Subject: [PATCH 133/179] node_mb_sql: Add missing (SQL-specific) functions --- src/node_mb_sql.erl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/node_mb_sql.erl b/src/node_mb_sql.erl index a3fdf3aed..125674316 100644 --- a/src/node_mb_sql.erl +++ b/src/node_mb_sql.erl @@ -41,7 +41,8 @@ get_pending_nodes/2, get_states/1, get_state/2, set_state/1, get_items/7, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). + path_to_node/1, get_entity_subscriptions_for_send_last/2, + get_last_items/3]). init(Host, ServerHost, Opts) -> node_pep_sql:init(Host, ServerHost, Opts). @@ -101,6 +102,9 @@ set_affiliation(Nidx, Owner, Affiliation) -> get_entity_subscriptions(Host, Owner) -> node_pep_sql:get_entity_subscriptions(Host, Owner). +get_entity_subscriptions_for_send_last(Host, Owner) -> + node_pep_sql:get_entity_subscriptions_for_send_last(Host, Owner). + get_node_subscriptions(Nidx) -> node_pep_sql:get_node_subscriptions(Nidx). @@ -130,6 +134,9 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, node_pep_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). +get_last_items(Nidx, JID, Count) -> + node_pep_sql:get_last_items(Nidx, JID, Count). + get_item(Nidx, ItemId) -> node_pep_sql:get_item(Nidx, ItemId). From 662b6f10205bf681666c727bc6a7125a163d132b Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Fri, 16 Sep 2016 14:59:06 +0200 Subject: [PATCH 134/179] Update riakc to support r19 --- rebar.config | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rebar.config b/rebar.config index 3c9610021..434c16af3 100644 --- a/rebar.config +++ b/rebar.config @@ -30,12 +30,7 @@ {if_var_true, zlib, {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.1"}}}}, {if_var_true, riak, {riakc, ".*", {git, "https://github.com/basho/riak-erlang-client", - "527722d12d0433b837cdb92a60900c2cb5df8942"}}}, - %% Forces correct dependency for riakc and allow using newer meck version) - {if_var_true, riak, {hamcrest, ".*", {git, "https://github.com/hyperthunk/hamcrest-erlang", - "13f9bfb9b27d216e8e033b0e0a9a29097ed923dd"}}}, % for riak_pb-2.1.0.7 - {if_var_true, riak, {protobuffs, ".*", {git, "https://github.com/basho/erlang_protobuffs", - "6e7fc924506e2dc166a6170e580ce1d95ebbd5bd"}}}, % for riak_pb-2.1.0.7 with correct meck dependency + {tag, "2.4.1"}}}}, %% Elixir support, needed to run tests {if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", {tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}}, From da291d804c140193dfa4144a2147902b141eaf0c Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 11:54:46 +0200 Subject: [PATCH 135/179] Use mnesia calls instead of ets for Acl and Access tables (#1206) --- src/acl.erl | 6 +++--- src/ejabberd_web_admin.erl | 12 ++++++------ src/mod_configure.erl | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/acl.erl b/src/acl.erl index d3f9afe38..14439ce7a 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -449,9 +449,9 @@ access_matches(all, _Data, _Host) -> access_matches(none, _Data, _Host) -> deny; access_matches(Name, Data, Host) when is_atom(Name) -> - GAccess = ets:lookup(access, {Name, global}), + GAccess = mnesia:dirty_read(access, {Name, global}), LAccess = - if Host /= global -> ets:lookup(access, {Name, Host}); + if Host /= global -> mnesia:dirty_read(access, {Name, Host}); true -> [] end, case GAccess ++ LAccess of @@ -484,7 +484,7 @@ access_rules_matches([], _Data, _Host, Default) -> Default. get_aclspecs(ACL, Host) -> - ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}). + mnesia:dirty_read(acl, {ACL, Host}) ++ mnesia:dirty_read(acl, {ACL, global}). is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 6583fb445..184f9775b 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -747,7 +747,7 @@ process_admin(Host, _ -> nothing end, ACLs = lists:keysort(2, - ets:select(acl, + mnesia:dirty_select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), {NumLines, ACLsP} = term_to_paragraph(ACLs, 80), @@ -784,7 +784,7 @@ process_admin(Host, _ -> nothing end, ACLs = lists:keysort(2, - ets:select(acl, + mnesia:dirty_select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), make_xhtml((?H1GL((?T(<<"Access Control Lists">>)), @@ -849,7 +849,7 @@ process_admin(Host, end; _ -> nothing end, - Access = ets:select(access, + Access = mnesia:dirty_select(access, [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), {NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80), @@ -883,7 +883,7 @@ process_admin(Host, end; _ -> nothing end, - AccessRules = ets:select(access, + AccessRules = mnesia:dirty_select(access, [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), make_xhtml((?H1GL((?T(<<"Access Rules">>)), @@ -1153,7 +1153,7 @@ term_to_paragraph(T, Cols) -> term_to_id(T) -> jlib:encode_base64((term_to_binary(T))). acl_parse_query(Host, Query) -> - ACLs = ets:select(acl, + ACLs = mnesia:dirty_select(acl, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}]), case lists:keysearch(<<"submit">>, 1, Query) of @@ -1267,7 +1267,7 @@ access_rules_to_xhtml(AccessRules, Lang) -> <<"Add New">>)])])]))]). access_parse_query(Host, Query) -> - AccessRules = ets:select(access, + AccessRules = mnesia:dirty_select(access, [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), case lists:keysearch(<<"addnew">>, 1, Query) of diff --git a/src/mod_configure.erl b/src/mod_configure.erl index d0e0166a4..03779d027 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -1337,7 +1337,7 @@ get_form(Host, [<<"config">>, <<"acls">>], Lang) -> [{xmlcdata, S}]} end, str:tokens(iolist_to_binary(io_lib:format("~p.", - [ets:select(acl, + [mnesia:dirty_select(acl, [{{acl, {'$1', '$2'}, @@ -1372,7 +1372,7 @@ get_form(Host, [<<"config">>, <<"access">>], Lang) -> [{xmlcdata, S}]} end, str:tokens(iolist_to_binary(io_lib:format("~p.", - [ets:select(access, + [mnesia:dirty_select(access, [{{access, {'$1', '$2'}, From ed62d705d8ac17de2b096eaaaa347891582bbe3b Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 11:59:40 +0200 Subject: [PATCH 136/179] Don't worry about storage_type of the Acl mnesia table (#1206) --- src/acl.erl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/acl.erl b/src/acl.erl index 14439ce7a..349198182 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -75,12 +75,6 @@ -export_type([acl/0]). start() -> - case catch mnesia:table_info(acl, storage_type) of - disc_copies -> - mnesia:delete_table(acl); - _ -> - ok - end, mnesia:create_table(acl, [{ram_copies, [node()]}, {type, bag}, {local_content, true}, From 8244c1fa8c41eeb54749b0bc0b437c9ab01e2ee0 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 12:35:32 +0200 Subject: [PATCH 137/179] Store the Allow Subscription room option (#1301) --- src/mod_muc_room.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index ac2f5daf1..5aceb94c1 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -4428,6 +4428,7 @@ make_opts(StateData) -> ?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous), ?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users), ?MAKE_CONFIG_OPT(allow_voice_requests), + ?MAKE_CONFIG_OPT(allow_subscription), ?MAKE_CONFIG_OPT(mam), ?MAKE_CONFIG_OPT(voice_request_min_interval), ?MAKE_CONFIG_OPT(vcard), From f2cc81dfead352d4e8c4b3f546d1c2d734076c5c Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 12:35:53 +0200 Subject: [PATCH 138/179] Fix getting of subscribed rooms: consider also temporary ones (#1242) --- src/mod_muc.erl | 9 ++++----- src/mod_muc_room.erl | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 4eb129a87..ad2be4cce 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -718,13 +718,12 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> index = NewIndex}} end. -get_subscribed_rooms(ServerHost, Host, From) -> - Rooms = get_rooms(ServerHost, Host), +get_subscribed_rooms(_ServerHost, Host1, From) -> + Rooms = get_vh_rooms(Host1), BareFrom = jid:remove_resource(From), lists:flatmap( - fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> - Subscribers = proplists:get_value(subscribers, Opts, []), - case lists:keymember(BareFrom, 1, Subscribers) of + fun(#muc_online_room{name_host = {Name, Host}, pid = Pid}) -> + case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of true -> [jid:make(Name, Host, <<>>)]; false -> [] end; diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 5aceb94c1..8e338b676 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -800,6 +800,14 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) -> {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; +handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> + Subscribers = (?DICT):fold( + fun(_LJID, #user{is_subscriber = true} = User, Acc) -> + [User#user.jid | Acc]; + (_, _, Acc) -> + Acc + end, [], StateData#state.users), + {reply, lists:member(From, Subscribers), StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. From 9fa92092bf9c22e84985c32827691a37d664273d Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 12:54:19 +0200 Subject: [PATCH 139/179] Revert "Fix getting of subscribed rooms: consider also temporary ones (#1242)" This reverts commit f2cc81dfead352d4e8c4b3f546d1c2d734076c5c. --- src/mod_muc.erl | 9 +++++---- src/mod_muc_room.erl | 8 -------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index ad2be4cce..4eb129a87 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -718,12 +718,13 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> index = NewIndex}} end. -get_subscribed_rooms(_ServerHost, Host1, From) -> - Rooms = get_vh_rooms(Host1), +get_subscribed_rooms(ServerHost, Host, From) -> + Rooms = get_rooms(ServerHost, Host), BareFrom = jid:remove_resource(From), lists:flatmap( - fun(#muc_online_room{name_host = {Name, Host}, pid = Pid}) -> - case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of + fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> + Subscribers = proplists:get_value(subscribers, Opts, []), + case lists:keymember(BareFrom, 1, Subscribers) of true -> [jid:make(Name, Host, <<>>)]; false -> [] end; diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 8e338b676..5aceb94c1 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -800,14 +800,6 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) -> {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; -handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> - Subscribers = (?DICT):fold( - fun(_LJID, #user{is_subscriber = true} = User, Acc) -> - [User#user.jid | Acc]; - (_, _, Acc) -> - Acc - end, [], StateData#state.users), - {reply, lists:member(From, Subscribers), StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. From 5bcfcf4c5e74638960894fa2e7b9e57dfdfb62f3 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 19 Sep 2016 13:46:01 +0200 Subject: [PATCH 140/179] When getting list of subscribed rooms, check all including temporary ones (#1242) --- src/mod_muc.erl | 9 ++++----- src/mod_muc_room.erl | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 4eb129a87..ad2be4cce 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -718,13 +718,12 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> index = NewIndex}} end. -get_subscribed_rooms(ServerHost, Host, From) -> - Rooms = get_rooms(ServerHost, Host), +get_subscribed_rooms(_ServerHost, Host1, From) -> + Rooms = get_vh_rooms(Host1), BareFrom = jid:remove_resource(From), lists:flatmap( - fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> - Subscribers = proplists:get_value(subscribers, Opts, []), - case lists:keymember(BareFrom, 1, Subscribers) of + fun(#muc_online_room{name_host = {Name, Host}, pid = Pid}) -> + case gen_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of true -> [jid:make(Name, Host, <<>>)]; false -> [] end; diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 5aceb94c1..fc2aeebb6 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -800,6 +800,9 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, StateData) -> {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; +handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> + IsSubs = ?DICT:is_key(jid:split(From), StateData#state.subscribers), + {reply, IsSubs, StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. From e7787e2f335c44a5f88fea6d15d1e3a83ad55204 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 19 Sep 2016 22:46:36 +0200 Subject: [PATCH 141/179] mod_carboncopy: Don't copy MUC PMs Carbon copies of private MUC message are generally not desired, especially not when multiple clients joined the room with the same nick. In this case, the MUC service usually sends PMs to all joined resources anyway, so carbon-copying those PMs would create duplicates. --- src/mod_carboncopy.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index de8d8e1a7..e1e6d63a2 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -131,8 +131,9 @@ user_receive_packet(Packet, _C2SState, JID, _From, To) -> % - we also replicate "read" notifications check_and_forward(JID, To, Packet, Direction)-> case is_chat_message(Packet) andalso - fxml:get_subtag(Packet, <<"private">>) == false andalso - fxml:get_subtag(Packet, <<"no-copy">>) == false of + not is_muc_pm(To, Packet) andalso + fxml:get_subtag(Packet, <<"private">>) == false andalso + fxml:get_subtag(Packet, <<"no-copy">>) == false of true -> case is_carbon_copy(Packet) of false -> @@ -270,6 +271,11 @@ is_chat_message(#xmlel{name = <<"message">>} = Packet) -> end; is_chat_message(_Packet) -> false. +is_muc_pm(#jid{lresource = <<>>}, _Packet) -> + false; +is_muc_pm(_To, Packet) -> + fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) =/= false. + has_non_empty_body(Packet) -> fxml:get_subtag_cdata(Packet, <<"body">>) =/= <<"">>. From ac6f701033a42e0c81b220e78a29b0f94f8c2f99 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Tue, 27 Sep 2016 05:57:14 +0300 Subject: [PATCH 142/179] Add http_p1.erl, rest.erl, and oauth2 ReST backend for tokens. --- src/ejabberd_app.erl | 1 + src/ejabberd_oauth_rest.erl | 98 ++++++++++ src/http_p1.erl | 358 ++++++++++++++++++++++++++++++++++++ src/rest.erl | 181 ++++++++++++++++++ 4 files changed, 638 insertions(+) create mode 100644 src/ejabberd_oauth_rest.erl create mode 100644 src/http_p1.erl create mode 100644 src/rest.erl diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 890ab6f90..33da45013 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -225,6 +225,7 @@ start_apps() -> ejabberd:start_app(fast_tls), ejabberd:start_app(fast_xml), ejabberd:start_app(stringprep), + http_p1:start(), ejabberd:start_app(cache_tab). opt_type(net_ticktime) -> diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl new file mode 100644 index 000000000..aadb97084 --- /dev/null +++ b/src/ejabberd_oauth_rest.erl @@ -0,0 +1,98 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth_rest.erl +%%% Author : Alexey Shchepin +%%% Purpose : OAUTH2 REST backend +%%% Created : 26 Jul 2016 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_oauth_rest). + +-export([init/0, + store/1, + lookup/1, + clean/1, + opt_type/1]). + +-include("ejabberd.hrl"). +-include("ejabberd_oauth.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). + +init() -> + rest:start(?MYNAME), + ok. + +store(R) -> + Path = path(<<"store">>), + %% Retry 2 times, with a backoff of 500millisec + {User, Server} = R#oauth_token.us, + SJID = jid:to_string({User, Server, <<"">>}), + case rest:with_retry( + post, + [?MYNAME, Path, [], + {[{<<"token">>, R#oauth_token.token}, + {<<"user">>, SJID}, + {<<"scope">>, R#oauth_token.scope}, + {<<"expire">>, R#oauth_token.expire} + ]}], 2, 500) of + {ok, Code, _} when Code == 200 orelse Code == 201 -> + ok; + Err -> + ?ERROR_MSG("failed to store oauth record ~p: ~p", [R, Err]), + {error, Err} + end. + +lookup(Token) -> + Path = path(<<"lookup">>), + case rest:with_retry(post, [?MYNAME, Path, [], + {[{<<"token">>, Token}]}], + 2, 500) of + {ok, 200, {Data}} -> + SJID = proplists:get_value(<<"user">>, Data, <<>>), + JID = jid:from_string(SJID), + US = {JID#jid.luser, JID#jid.lserver}, + Scope = proplists:get_value(<<"scope">>, Data, []), + Expire = proplists:get_value(<<"expire">>, Data, 0), + #oauth_token{token = Token, + us = US, + scope = Scope, + expire = Expire}; + {ok, 404, _Resp} -> + false; + Other -> + ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]), + {error, rest_failed} + end. + +clean(_TS) -> + ok. + +path(Path) -> + Base = ejabberd_config:get_option(ext_api_path_oauth, + fun(X) -> iolist_to_binary(X) end, + <<"/oauth">>), + <>. + + +opt_type(ext_api_path_oauth) -> + fun (X) -> iolist_to_binary(X) end; +opt_type(_) -> [ext_api_path_oauth]. diff --git a/src/http_p1.erl b/src/http_p1.erl new file mode 100644 index 000000000..6ede758f2 --- /dev/null +++ b/src/http_p1.erl @@ -0,0 +1,358 @@ +%%%---------------------------------------------------------------------- +%%% File : http_p1.erl +%%% Author : Emilio Bustos +%%% Purpose : Provide a common API for inets / lhttpc / ibrowse +%%% Created : 29 Jul 2010 by Emilio Bustos +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(http_p1). + +-author('ebustos@process-one.net'). + +-export([start/0, stop/0, get/1, get/2, post/2, post/3, + request/3, request/4, request/5, + get_pool_size/0, set_pool_size/1]). + +-include("logger.hrl"). + +% -define(USE_INETS, 1). +-define(USE_LHTTPC, 1). +% -define(USE_IBROWSE, 1). +% inets used as default if none specified + +-ifdef(USE_IBROWSE). + +start() -> + ejabberd:start_app(ibrowse). + +stop() -> + application:stop(ibrowse). + +request(Method, URL, Hdrs, Body, Opts) -> + TimeOut = proplists:get_value(timeout, Opts, infinity), + Options = [{inactivity_timeout, TimeOut} + | proplists:delete(timeout, Opts)], + case ibrowse:send_req(URL, Hdrs, Method, Body, Options) + of + {ok, Status, Headers, Response} -> + {ok, jlib:binary_to_integer(Status), Headers, + Response}; + {error, Reason} -> {error, Reason} + end. + +get_pool_size() -> + application:get_env(ibrowse, default_max_sessions, 10). + +set_pool_size(Size) -> + application:set_env(ibrowse, default_max_sessions, Size). + +-else. + +-ifdef(USE_LHTTPC). + +start() -> + ejabberd:start_app(lhttpc). + +stop() -> + application:stop(lhttpc). + +request(Method, URL, Hdrs, Body, Opts) -> + {[TO, SO], Rest} = proplists:split(Opts, [timeout, socket_options]), + TimeOut = proplists:get_value(timeout, TO, infinity), + SockOpt = proplists:get_value(socket_options, SO, []), + Options = [{connect_options, SockOpt} | Rest], + Result = lhttpc:request(URL, Method, Hdrs, Body, TimeOut, Options), + ?DEBUG("HTTP request -> response:~n" + "** Method = ~p~n" + "** URI = ~s~n" + "** Body = ~s~n" + "** Hdrs = ~p~n" + "** Timeout = ~p~n" + "** Options = ~p~n" + "** Response = ~p", + [Method, URL, Body, Hdrs, TimeOut, Options, Result]), + case Result of + {ok, {{Status, _Reason}, Headers, Response}} -> + {ok, Status, Headers, (Response)}; + {error, Reason} -> {error, Reason} + end. + +get_pool_size() -> + Opts = proplists:get_value(lhttpc_manager, lhttpc_manager:list_pools()), + proplists:get_value(max_pool_size,Opts). + +set_pool_size(Size) -> + lhttpc_manager:set_max_pool_size(lhttpc_manager, Size). + +-else. + +start() -> + ejabberd:start_app(inets). + +stop() -> + application:stop(inets). + +to_list(Str) when is_binary(Str) -> + binary_to_list(Str); +to_list(Str) -> + Str. + +request(Method, URLRaw, HdrsRaw, Body, Opts) -> + Hdrs = lists:map(fun({N, V}) -> + {to_list(N), to_list(V)} + end, HdrsRaw), + URL = to_list(URLRaw), + + Request = case Method of + get -> {URL, Hdrs}; + head -> {URL, Hdrs}; + delete -> {URL, Hdrs}; + _ -> % post, etc. + {URL, Hdrs, + to_list(proplists:get_value(<<"content-type">>, HdrsRaw, [])), + Body} + end, + Options = case proplists:get_value(timeout, Opts, + infinity) + of + infinity -> proplists:delete(timeout, Opts); + _ -> Opts + end, + case httpc:request(Method, Request, Options, []) of + {ok, {{_, Status, _}, Headers, Response}} -> + {ok, Status, Headers, Response}; + {error, Reason} -> {error, Reason} + end. + +get_pool_size() -> + {ok, Size} = httpc:get_option(max_sessions), + Size. + +set_pool_size(Size) -> + httpc:set_option(max_sessions, Size). + +-endif. + +-endif. + +-type({header, + {type, 63, tuple, + [{type, 63, union, + [{type, 63, string, []}, {type, 63, atom, []}]}, + {type, 63, string, []}]}, + []}). + +-type({headers, + {type, 64, list, [{type, 64, header, []}]}, []}). + +-type({option, + {type, 67, union, + [{type, 67, tuple, + [{atom, 67, connect_timeout}, {type, 67, timeout, []}]}, + {type, 68, tuple, + [{atom, 68, timeout}, {type, 68, timeout, []}]}, + {type, 70, tuple, + [{atom, 70, send_retry}, + {type, 70, non_neg_integer, []}]}, + {type, 71, tuple, + [{atom, 71, partial_upload}, + {type, 71, union, + [{type, 71, non_neg_integer, []}, + {atom, 71, infinity}]}]}, + {type, 72, tuple, + [{atom, 72, partial_download}, {type, 72, pid, []}, + {type, 72, union, + [{type, 72, non_neg_integer, []}, + {atom, 72, infinity}]}]}]}, + []}). + +-type({options, + {type, 74, list, [{type, 74, option, []}]}, []}). + +-type({result, + {type, 76, union, + [{type, 76, tuple, + [{atom, 76, ok}, + {type, 76, tuple, + [{type, 76, tuple, + [{type, 76, pos_integer, []}, {type, 76, string, []}]}, + {type, 76, headers, []}, {type, 76, string, []}]}]}, + {type, 77, tuple, + [{atom, 77, error}, {type, 77, atom, []}]}]}, + []}). + +%% @spec (URL) -> Result +%% URL = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a GET request. +%% Would be the same as calling `request(get, URL, [])', +%% that is {@link request/3} with an empty header list. +%% @end +%% @see request/3 +-spec get(string()) -> result(). +get(URL) -> request(get, URL, []). + +%% @spec (URL, Hdrs) -> Result +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a GET request. +%% Would be the same as calling `request(get, URL, Hdrs)'. +%% @end +%% @see request/3 +-spec get(string(), headers()) -> result(). +get(URL, Hdrs) -> request(get, URL, Hdrs). + +%% @spec (URL, RequestBody) -> Result +%% URL = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a POST request with form data. +%% Would be the same as calling +%% `request(post, URL, [{"content-type", "x-www-form-urlencoded"}], Body)'. +%% @end +%% @see request/4 +-spec post(string(), string()) -> result(). +post(URL, Body) -> + request(post, URL, + [{<<"content-type">>, <<"x-www-form-urlencoded">>}], + Body). + +%% @spec (URL, Hdrs, RequestBody) -> Result +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a POST request. +%% Would be the same as calling +%% `request(post, URL, Hdrs, Body)'. +%% @end +%% @see request/4 +-spec post(string(), headers(), string()) -> result(). +post(URL, Hdrs, Body) -> + NewHdrs = case [X + || {X, _} <- Hdrs, + str:to_lower(X) == <<"content-type">>] + of + [] -> + [{<<"content-type">>, <<"x-www-form-urlencoded">>} + | Hdrs]; + _ -> Hdrs + end, + request(post, URL, NewHdrs, Body). + +%% @spec (Method, URL, Hdrs) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request without a body. +%% Would be the same as calling `request(Method, URL, Hdrs, [], [])', +%% that is {@link request/5} with an empty body. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers()) -> result(). +request(Method, URL, Hdrs) -> + request(Method, URL, Hdrs, [], []). + +%% @spec (Method, URL, Hdrs, RequestBody) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request with a body. +%% Would be the same as calling +%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5} +%% with no options. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers(), string()) -> result(). +request(Method, URL, Hdrs, Body) -> + request(Method, URL, Hdrs, Body, []). + +%% @spec (Method, URL, Hdrs, RequestBody, Options) -> Result +%% Method = atom() +%% URL = string() +%% Hdrs = [{Header, Value}] +%% Header = string() +%% Value = string() +%% RequestBody = string() +%% Options = [Option] +%% Option = {timeout, Milliseconds | infinity} | +%% {connect_timeout, Milliseconds | infinity} | +%% {socket_options, [term()]} | + +%% Milliseconds = integer() +%% Result = {ok, StatusCode, Hdrs, ResponseBody} +%% | {error, Reason} +%% StatusCode = integer() +%% ResponseBody = string() +%% Reason = connection_closed | connect_timeout | timeout +%% @doc Sends a request with a body. +%% Would be the same as calling +%% `request(Method, URL, Hdrs, Body, [])', that is {@link request/5} +%% with no options. +%% @end +%% @see request/5 +-spec request(atom(), string(), headers(), string(), options()) -> result(). + +% ibrowse {response_format, response_format()} | +% Options - [option()] +% Option - {sync, boolean()} | {stream, StreamTo} | {body_format, body_format()} | {full_result, +% boolean()} | {headers_as_is, boolean()} +%body_format() = string() | binary() +% The body_format option is only valid for the synchronous request and the default is string. +% When making an asynchronous request the body will always be received as a binary. +% lhttpc: always binary + diff --git a/src/rest.erl b/src/rest.erl new file mode 100644 index 000000000..01b04f66a --- /dev/null +++ b/src/rest.erl @@ -0,0 +1,181 @@ +%%%---------------------------------------------------------------------- +%%% File : rest.erl +%%% Author : Christophe Romain +%%% Purpose : Generic REST client +%%% Created : 16 Oct 2014 by Christophe Romain +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(rest). + +-behaviour(ejabberd_config). + +-export([start/1, stop/1, get/2, get/3, post/4, delete/2, + request/6, with_retry/4, opt_type/1]). + +-include("logger.hrl"). + +-define(HTTP_TIMEOUT, 10000). +-define(CONNECT_TIMEOUT, 8000). + +start(Host) -> + http_p1:start(), + Pool_size = + ejabberd_config:get_option({ext_api_http_pool_size, Host}, + fun(X) when is_integer(X), X > 0-> + X + end, + 100), + http_p1:set_pool_size(Pool_size). + +stop(_Host) -> + ok. + +with_retry(Method, Args, MaxRetries, Backoff) -> + with_retry(Method, Args, 0, MaxRetries, Backoff). +with_retry(Method, Args, Retries, MaxRetries, Backoff) -> + case apply(?MODULE, Method, Args) of + %% Only retry on timeout errors + {error, {http_error,{error,Error}}} + when Retries < MaxRetries + andalso (Error == 'timeout' orelse Error == 'connect_timeout') -> + timer:sleep(round(math:pow(2, Retries)) * Backoff), + with_retry(Method, Args, Retries+1, MaxRetries, Backoff); + Result -> + Result + end. + +get(Server, Path) -> + request(Server, get, Path, [], "application/json", <<>>). +get(Server, Path, Params) -> + request(Server, get, Path, Params, "application/json", <<>>). + +delete(Server, Path) -> + request(Server, delete, Path, [], "application/json", <<>>). + +post(Server, Path, Params, Content) -> + Data = case catch jiffy:encode(Content) of + {'EXIT', Reason} -> + ?ERROR_MSG("HTTP content encodage failed:~n" + "** Content = ~p~n" + "** Err = ~p", + [Content, Reason]), + <<>>; + Encoded -> + Encoded + end, + request(Server, post, Path, Params, "application/json", Data). + +request(Server, Method, Path, Params, Mime, Data) -> + URI = url(Server, Path, Params), + Opts = [{connect_timeout, ?CONNECT_TIMEOUT}, + {timeout, ?HTTP_TIMEOUT}], + Hdrs = [{"connection", "keep-alive"}, + {"content-type", Mime}, + {"User-Agent", "ejabberd"}], + Begin = os:timestamp(), + Result = case catch http_p1:request(Method, URI, Hdrs, Data, Opts) of + {ok, Code, _, <<>>} -> + {ok, Code, []}; + {ok, Code, _, <<" ">>} -> + {ok, Code, []}; + {ok, Code, _, <<"\r\n">>} -> + {ok, Code, []}; + {ok, Code, _, Body} -> + try jiffy:decode(Body) of + JSon -> + {ok, Code, JSon} + catch + _:Error -> + ?ERROR_MSG("HTTP response decode failed:~n" + "** URI = ~s~n" + "** Body = ~p~n" + "** Err = ~p", + [URI, Body, Error]), + {error, {invalid_json, Body}} + end; + {error, Reason} -> + ?ERROR_MSG("HTTP request failed:~n" + "** URI = ~s~n" + "** Err = ~p", + [URI, Reason]), + {error, {http_error, {error, Reason}}}; + {'EXIT', Reason} -> + ?ERROR_MSG("HTTP request failed:~n" + "** URI = ~s~n" + "** Err = ~p", + [URI, Reason]), + {error, {http_error, {error, Reason}}} + end, + ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]), + case Result of + {ok, _, _} -> + End = os:timestamp(), + Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms + ejabberd_hooks:run(backend_api_response_time, Server, + [Server, Method, Path, Elapsed]); + {error, {http_error,{error,timeout}}} -> + ejabberd_hooks:run(backend_api_timeout, Server, + [Server, Method, Path]); + {error, {http_error,{error,connect_timeout}}} -> + ejabberd_hooks:run(backend_api_timeout, Server, + [Server, Method, Path]); + {error, _} -> + ejabberd_hooks:run(backend_api_error, Server, + [Server, Method, Path]) + end, + Result. + +%%%---------------------------------------------------------------------- +%%% HTTP helpers +%%%---------------------------------------------------------------------- + +base_url(Server, Path) -> + Tail = case iolist_to_binary(Path) of + <<$/, Ok/binary>> -> Ok; + Ok -> Ok + end, + case Tail of + <<"http", _Url/binary>> -> Tail; + _ -> + Base = ejabberd_config:get_option({ext_api_url, Server}, + fun(X) -> + iolist_to_binary(X) + end, + <<"http://localhost/api">>), + <> + end. + +url(Server, Path, []) -> + binary_to_list(base_url(Server, Path)); +url(Server, Path, Params) -> + Base = base_url(Server, Path), + [<<$&, ParHead/binary>> | ParTail] = + [<<"&", (iolist_to_binary(Key))/binary, "=", + (ejabberd_http:url_encode(Value))/binary>> + || {Key, Value} <- Params], + Tail = iolist_to_binary([ParHead | ParTail]), + binary_to_list(<>). + +opt_type(ext_api_http_pool_size) -> + fun (X) when is_integer(X), X > 0 -> X end; +opt_type(ext_api_url) -> + fun (X) -> iolist_to_binary(X) end; +opt_type(_) -> [ext_api_http_pool_size, ext_api_url]. From acab2270f15b653fb21a65a826448e91d5e69873 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Tue, 27 Sep 2016 07:12:10 +0300 Subject: [PATCH 143/179] Use inets instead of lhttpc in http_p1 --- src/http_p1.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http_p1.erl b/src/http_p1.erl index 6ede758f2..f430bbe11 100644 --- a/src/http_p1.erl +++ b/src/http_p1.erl @@ -33,8 +33,8 @@ -include("logger.hrl"). -% -define(USE_INETS, 1). --define(USE_LHTTPC, 1). +-define(USE_INETS, 1). +% -define(USE_LHTTPC, 1). % -define(USE_IBROWSE, 1). % inets used as default if none specified From d4b4f35a0ecff8fdad416a7f3bab85a2b7a7e375 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 27 Sep 2016 23:22:30 +0200 Subject: [PATCH 144/179] ejabberd_http: Handle missing POST data gracefully Return a "bad request" error instead of crashing if receiving POST/PUT data fails. --- src/ejabberd_http.erl | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 31f80be78..e6e49d9b2 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -396,18 +396,21 @@ extract_path_query(#state{request_method = Method, socket = _Socket} = State) when (Method =:= 'POST' orelse Method =:= 'PUT') andalso is_integer(Len) -> - {NewState, Data} = recv_data(State, Len), - ?DEBUG("client data: ~p~n", [Data]), - case catch url_decode_q_split(Path) of - {'EXIT', _} -> {NewState, false}; - {NPath, _Query} -> - LPath = normalize_path([NPE - || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case catch parse_urlencoded(Data) of - {'EXIT', _Reason} -> []; - LQ -> LQ - end, - {NewState, {LPath, LQuery, Data}} + case recv_data(State, Len) of + error -> {State, false}; + {NewState, Data} -> + ?DEBUG("client data: ~p~n", [Data]), + case catch url_decode_q_split(Path) of + {'EXIT', _} -> {NewState, false}; + {NPath, _Query} -> + LPath = normalize_path([NPE + || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + LQuery = case catch parse_urlencoded(Data) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {NewState, {LPath, LQuery, Data}} + end end; extract_path_query(State) -> {State, false}. @@ -525,7 +528,7 @@ recv_data(State, Len, Acc) -> recv_data(State, Len - byte_size(Data), <>); Err -> ?DEBUG("Cannot receive HTTP data: ~p", [Err]), - <<"">> + error end; _ -> Trail = (State#state.trail), From 6f538545b49d371db9b9ad2a346563692479e587 Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 28 Sep 2016 11:03:46 +0200 Subject: [PATCH 145/179] Fix 404 response formatting (thanks to Kaggggggga)(#1306) --- src/mod_http_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index dbca82375..7a95f8c6f 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -226,7 +226,7 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> - {404, 40, <<"Command not found.">>}; + json_format({404, 44, <<"Command not found.">>}); _:{error,{_,invalid_json}} = _Err -> ?DEBUG("Bad Request: ~p", [_Err]), badrequest_response(<<"Invalid JSON input">>); From a42bf67957c18d66c05fac3f651ab198b59bef06 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 29 Sep 2016 11:20:56 +0200 Subject: [PATCH 146/179] PubSub: fix error type on item deletion with insufficient priviledge --- src/node_flat.erl | 10 +++++----- src/node_flat_sql.erl | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/node_flat.erl b/src/node_flat.erl index 2fb24ee69..7ead1d351 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -419,11 +419,11 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) -> #pubsub_state{affiliation = Affiliation, items = Items} = GenState, Allowed = Affiliation == publisher orelse Affiliation == owner orelse - PublishModel == open orelse - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), if not Allowed -> {error, ?ERR_FORBIDDEN}; true -> diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 86375eaec..e7e5fb417 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -271,11 +271,11 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) -> {result, Affiliation} = get_affiliation(Nidx, GenKey), Allowed = Affiliation == publisher orelse Affiliation == owner orelse - PublishModel == open orelse - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), if not Allowed -> {error, ?ERR_FORBIDDEN}; true -> From 767dba8f3bc526a502a57afd2f8313b630f15877 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 29 Sep 2016 12:00:59 +0200 Subject: [PATCH 147/179] PubSub: fix notification on subscription change --- src/mod_pubsub.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index d64d05737..31170bc74 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -2916,15 +2916,11 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> {error, ?ERR_BAD_REQUEST}; _ -> Notify = fun (JID, Sub, _SubId) -> - Stanza = #xmlel{name = <<"message">>, attrs = [], - children = - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = + Stanza = event_stanza( [#xmlel{name = <<"subscription">>, attrs = [{<<"jid">>, jid:to_string(JID)}, {<<"subscription">>, subscription_to_string(Sub)} - | nodeAttr(Node)]}]}]}, + | nodeAttr(Node)]}]), ejabberd_router:route(service_jid(Host), jid:make(JID), Stanza) end, Action = fun (#pubsub_node{type = Type, id = Nidx, owners = O}) -> From 15ebb791603aa32424111a5e3f95c6bdcdb9055f Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Thu, 29 Sep 2016 16:10:11 +0200 Subject: [PATCH 148/179] PubSub: creation jid must be bare jid --- src/node_flat_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index e7e5fb417..15cf9b37a 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -1051,6 +1051,6 @@ raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> El -> [El] end, #pubsub_item{itemid = {ItemId, Nidx}, - creation = {ToTime(Creation), JID}, + creation = {ToTime(Creation), jid:remove_resource(JID)}, modification = {ToTime(Modification), JID}, payload = Payload}. From 9cee3760db4f304d8354c33b641b84f5423e1a80 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Sun, 2 Oct 2016 22:01:03 +0200 Subject: [PATCH 149/179] ejabberd_sm: Clean up old offline session entries If the number of offline sessions exceeds the 'max_user_sessions' limit, remove the oldest entry from the table. --- src/ejabberd_sm.erl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 16e0f9114..3369b7ca0 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -713,10 +713,18 @@ get_resource_sessions(User, Server, Resource) -> check_max_sessions(LUser, LServer) -> Mod = get_sm_backend(LServer), - SIDs = [S#session.sid || S <- online(Mod:get_sessions(LUser, LServer))], + Ss = Mod:get_sessions(LUser, LServer), + {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss), MaxSessions = get_max_user_sessions(LUser, LServer), - if length(SIDs) =< MaxSessions -> ok; - true -> {_, Pid} = lists:min(SIDs), Pid ! replaced + if length(OnlineSs) =< MaxSessions -> ok; + true -> + #session{sid = {_, Pid}} = lists:min(OnlineSs), + Pid ! replaced + end, + if length(OfflineSs) =< MaxSessions -> ok; + true -> + #session{sid = SID, usr = {_, _, R}} = lists:min(OfflineSs), + Mod:delete_session(LUser, LServer, R, SID) end. %% Get the user_max_session setting From 98e0123ca48cc23f699b5c61598740fa6b8a320e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 5 Oct 2016 13:21:11 +0200 Subject: [PATCH 150/179] New api permissions framework --- src/acl.erl | 38 +- src/ejabberd_access_permissions.erl | 527 ++++++++++++++++++++++++++++ src/ejabberd_admin.erl | 3 +- src/ejabberd_app.erl | 1 + src/ejabberd_commands.erl | 53 ++- src/ejabberd_config.erl | 3 +- src/ejabberd_ctl.erl | 13 +- src/ejabberd_oauth.erl | 25 ++ src/mod_http_api.erl | 195 +++++----- 9 files changed, 723 insertions(+), 135 deletions(-) create mode 100644 src/ejabberd_access_permissions.erl diff --git a/src/acl.erl b/src/acl.erl index 349198182..1476081dd 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -36,7 +36,8 @@ acl_rule_verify/1, access_matches/3, transform_access_rules_config/1, parse_ip_netmask/1, - access_rules_validator/1, shaper_rules_validator/1]). + access_rules_validator/1, shaper_rules_validator/1, + normalize_spec/1, resolve_access/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -437,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) -> acl_rule_matches(_ACL, _Data, _Host) -> false. --spec access_matches(atom()|list(), any(), global|binary()) -> any(). -access_matches(all, _Data, _Host) -> - allow; -access_matches(none, _Data, _Host) -> - deny; -access_matches(Name, Data, Host) when is_atom(Name) -> +resolve_access(all, _Host) -> + all; +resolve_access(none, _Host) -> + none; +resolve_access(Name, Host) when is_atom(Name) -> GAccess = mnesia:dirty_read(access, {Name, global}), LAccess = - if Host /= global -> mnesia:dirty_read(access, {Name, Host}); - true -> [] - end, + if Host /= global -> mnesia:dirty_read(access, {Name, Host}); + true -> [] + end, case GAccess ++ LAccess of [] -> - deny; + []; AccessList -> - Rules = lists:flatmap( + lists:flatmap( fun(#access{rules = Rs}) -> Rs - end, AccessList), - access_rules_matches(Rules, Data, Host) + end, AccessList) end; -access_matches(Rules, Data, Host) when is_list(Rules) -> - access_rules_matches(Rules, Data, Host). +resolve_access(Rules, _Host) when is_list(Rules) -> + Rules. +-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny. +access_matches(Rules, Data, Host) -> + case resolve_access(Rules, Host) of + all -> allow; + none -> deny; + RRules -> access_rules_matches(RRules, Data, Host) + end. -spec access_rules_matches(list(), any(), global|binary()) -> any(). diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl new file mode 100644 index 000000000..f37de9a13 --- /dev/null +++ b/src/ejabberd_access_permissions.erl @@ -0,0 +1,527 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_access_permissions.erl +%%% Author : Paweł Chmielowski +%%% Purpose : Administrative functions and commands +%%% Created : 7 Sep 2016 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- +-module(ejabberd_access_permissions). +-author("pawel@process-one.net"). + +-include("ejabberd_commands.hrl"). +-include("logger.hrl"). + +-behaviour(gen_server). +-behavior(ejabberd_config). + +%% API +-export([start_link/0, + parse_api_permissions/1, + can_access/2, + invalidate/0, + opt_type/1, + show_current_definitions/0, + register_permission_addon/2, + unregister_permission_addon/1]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-define(SERVER, ?MODULE). + +-record(state, { + definitions = none, + fragments_generators = [] +}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +-spec can_access(atom(), map()) -> allow | deny. +can_access(Cmd, CallerInfo) -> + gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}). + +-spec invalidate() -> ok. +invalidate() -> + gen_server:cast(?MODULE, invalidate). + +-spec register_permission_addon(atom(), fun()) -> ok. +register_permission_addon(Name, Fun) -> + gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}). + +-spec unregister_permission_addon(atom()) -> ok. +unregister_permission_addon(Name) -> + gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}). + +-spec show_current_definitions() -> any(). +show_current_definitions() -> + gen_server:call(?MODULE, show_current_definitions). + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% +%% @end +%%-------------------------------------------------------------------- +-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}. +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% +%% @spec init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @end +%%-------------------------------------------------------------------- +-spec init(Args :: term()) -> + {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | + {stop, Reason :: term()} | ignore. +init([]) -> + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @end +%%-------------------------------------------------------------------- +-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()}, + State :: #state{}) -> + {reply, Reply :: term(), NewState :: #state{}} | + {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | + {stop, Reason :: term(), NewState :: #state{}}. +handle_call({can_access, Cmd, CallerInfo}, _From, State) -> + CallerModule = maps:get(caller_module, CallerInfo, none), + Host = maps:get(caller_host, CallerInfo, global), + {State2, Defs} = get_definitions(State), + Res = lists:foldl( + fun({Name, _} = Def, none) -> + case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of + true -> + ?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]), + allow; + _ -> + none + end; + (_, Val) -> + Val + end, none, Defs), + Res2 = case Res of + allow -> allow; + _ -> + ?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]), + deny + end, + {reply, Res2, State2}; +handle_call(show_current_definitions, _From, State) -> + {State2, Defs} = get_definitions(State), + {reply, Defs, State2}; +handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) -> + NGens = lists:keystore(Name, 1, Gens, {Name, Fun}), + {reply, ok, State#state{fragments_generators = NGens}}; +handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) -> + NGens = lists:keydelete(Name, 1, Gens), + {reply, ok, State#state{fragments_generators = NGens}}; +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @end +%%-------------------------------------------------------------------- +-spec handle_cast(Request :: term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}. +handle_cast(invalidate, State) -> + {noreply, State#state{definitions = none}}; +handle_cast(_Request, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @spec handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +-spec handle_info(Info :: timeout() | term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}. +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), + State :: #state{}) -> term(). +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{}, + Extra :: term()) -> + {ok, NewState :: #state{}} | {error, Reason :: term()}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) -> + DefaultOptions = [{<<"console commands">>, + [ejabberd_ctl], + [{acl, all}], + {all, none}}, + {<<"admin access">>, + [], + [{acl, admin}], + {all, [start, stop]}}], + NDefs = case Defs of + none -> + ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions), + AllCommands = ejabberd_commands:get_commands_definition(), + Frags = lists:foldl( + fun({_Name, Generator}, Acc) -> + Acc ++ Generator() + end, [], Gens), + lists:map( + fun({Name, {From, Who, {Add, Del}}}) -> + Cmds = filter_commands_with_permissions(AllCommands, Add, Del), + {Name, {From, Who, Cmds}} + end, ApiPerms ++ Frags); + V -> + V + end, + {State#state{definitions = NDefs}, NDefs}. + +matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) -> + case lists:member(Cmd, What) of + true -> + case From == [] orelse lists:member(Module, From) of + true -> + Scope = maps:get(scope, CallerInfo, none), + lists:any( + fun({access, Access}) when Scope == none -> + acl:access_matches(Access, CallerInfo, Host) == allow; + ({acl, _} = Acl) when Scope == none -> + acl:acl_rule_matches(Acl, CallerInfo, Host); + ({oauth, List}) when Scope /= none -> + lists:all( + fun({access, Access}) -> + acl:access_matches(Access, CallerInfo, Host) == allow; + ({acl, _} = Acl) -> + acl:acl_rule_matches(Acl, CallerInfo, Host); + ({scope, Scopes}) -> + ejabberd_oauth:scope_in_scope_list(Scope, Scopes) + end, List); + (_) -> + false + end, Who); + _ -> + false + end; + _ -> + false + end. + +filter_commands_with_permissions(AllCommands, Add, Del) -> + CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []), + CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []), + lists:map(fun(#ejabberd_commands{name = N}) -> N end, + CommandsAdd -- CommandsDel). + +filter_commands_with_patterns([], _Patterns, Acc) -> + Acc; +filter_commands_with_patterns([C | CRest], Patterns, Acc) -> + case command_matches_patterns(C, Patterns) of + true -> + filter_commands_with_patterns(CRest, Patterns, [C | Acc]); + _ -> + filter_commands_with_patterns(CRest, Patterns, Acc) + end. + +command_matches_patterns(_, all) -> + true; +command_matches_patterns(_, none) -> + false; +command_matches_patterns(_, []) -> + false; +command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) -> + case lists:member(Tag, Tags) of + true -> + true; + _ -> + command_matches_patterns(C, Tail) + end; +command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) -> + true; +command_matches_patterns(C, [_ | Tail]) -> + command_matches_patterns(C, Tail). + +%%%=================================================================== +%%% Options parsing code +%%%=================================================================== + +parse_api_permissions(Data) when is_list(Data) -> + throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}). + +parse_api_permission(Name, Args) -> + {From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of + {error, Msg} -> + report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]); + Val -> Val + end, + {Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}. + +parse_from(_Name, Module) when is_atom(Module) -> + [Module]; +parse_from(Name, Modules) when is_list(Modules) -> + lists:foreach(fun(Module) when is_atom(Module) -> + ok; + (Val) -> + report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>, + [Val, Name]) + end, Modules), + Modules; +parse_from(Name, Val) -> + report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>, + [Val, Name]). + +parse_who(Name, Atom, ParseOauth) when is_atom(Atom) -> + parse_who(Name, [Atom], ParseOauth); +parse_who(Name, Defs, ParseOauth) when is_list(Defs) -> + lists:map( + fun([{access, Val}]) -> + try acl:access_rules_validator(Val) of + Rule -> + {access, Rule} + catch + throw:{invalid_syntax, Msg} -> + report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>, + [Msg, Name]); + throw:{replace_with, NVal} -> + {access, NVal}; + error:_ -> + report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>, + [Val, Name]) + end; + ([{oauth, OauthList}]) when is_list(OauthList) -> + case ParseOauth of + oauth -> + {oauth, parse_who(Name, lists:flatten(OauthList), scope)}; + scope -> + report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>, + [Name]) + end; + ({scope, ScopeList}) -> + case ParseOauth of + oauth -> + report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>, + [Name]); + scope -> + ScopeList2 = case ScopeList of + V when is_binary(V) -> [V]; + V2 when is_list(V2) -> V2; + V3 -> + report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>, + [V3, Name]) + end, + {scope, ScopeList2} + end; + (Atom) when is_atom(Atom) -> + {acl, Atom}; + ([Other]) -> + try acl:normalize_spec(Other) of + Rule2 -> + {acl, Rule2} + catch + _:_ -> + report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>, + [Other, Name]) + end; + (Invalid) -> + report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>, + [Invalid, Name]) + end, Defs); +parse_who(Name, Val, _ParseOauth) -> + report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>, + [Val, Name]). + +parse_what(Name, Binary) when is_binary(Binary) -> + parse_what(Name, [Binary]); +parse_what(Name, Defs) when is_list(Defs) -> + {A, D} = lists:foldl( + fun(Def, {Add, Del}) -> + case parse_single_what(Def) of + {error, Err} -> + report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>, + [Err, Def, Name]); + all -> + {case Add of none -> none; _ -> all end, Del}; + {neg, all} -> + {none, all}; + {neg, Value} -> + {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end}; + Value -> + {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del} + end + end, {[], []}, Defs), + case {A, D} of + {[], _} -> + {none, all}; + {A2, []} -> + {A2, none}; + V -> + V + end; +parse_what(Name, Val) -> + report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>, + [Val, Name]). + +parse_single_what(<<"*">>) -> + all; +parse_single_what(<<"!*">>) -> + {neg, all}; +parse_single_what(<<"!", Rest/binary>>) -> + case parse_single_what(Rest) of + {neg, _} -> + {error, <<"Double negation">>}; + {error, _} = Err -> + Err; + V -> + {neg, V} + end; +parse_single_what(<<"[tag:", Rest/binary>>) -> + case binary:split(Rest, <<"]">>) of + [TagName, <<"">>] -> + case parse_single_what(TagName) of + {error, _} = Err -> + Err; + V when is_atom(V) -> + {tag, V}; + _ -> + {error, <<"Invalid tag">>} + end; + _ -> + {error, <<"Invalid tag">>} + end; +parse_single_what(Binary) when is_binary(Binary) -> + case is_valid_command_name(Binary) of + true -> + binary_to_atom(Binary, latin1); + _ -> + {error, <<"Invalid value">>} + end; +parse_single_what(_) -> + {error, <<"Invalid value">>}. + +is_valid_command_name(<<>>) -> + false; +is_valid_command_name(Val) -> + is_valid_command_name2(Val). + +is_valid_command_name2(<<>>) -> + true; +is_valid_command_name2(<>) when K >= $a andalso K =< $z orelse K == $_ -> + is_valid_command_name2(Rest); +is_valid_command_name2(_) -> + false. + +key_split(Args, Fields) -> + {_, Order1, Results1, Required1} = lists:foldl( + fun({Field, Default}, {Idx, Order, Results, Required}) -> + {Idx + 1, Order#{Field => Idx}, [Default | Results], Required}; + (Field, {Idx, Order, Results, Required}) -> + {Idx + 1, Order#{Field => Idx}, [none | Results], Required#{Field => 1}} + end, {1, #{}, [], #{}}, Fields), + key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}). + +key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 -> + parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]); +key_split([], Results, _Order, _Required, _Duplicates) -> + Results; +key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) -> + case maps:find(Arg, Order) of + {ok, Idx} -> + case maps:is_key(Arg, Duplicates) of + false -> + Results2 = setelement(Idx, Results, Value), + key_split(Rest, Results2, Order, maps:remove(Arg, Required), Duplicates#{Arg => 1}); + true -> + parse_error(<<"Duplicate field '~s'">>, [Arg]) + end; + _ -> + parse_error(<<"Unknown field '~s'">>, [Arg]) + end. + +report_error(Format, Args) -> + throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}). + +parse_error(Format, Args) -> + {error, iolist_to_binary(io_lib:format(Format, Args))}. + +opt_type(api_permissions) -> + fun parse_api_permissions/1; +opt_type(_) -> + [api_permissions]. diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index ae2fac3e1..8622ea8d0 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -403,7 +403,8 @@ registered_vhosts() -> reload_config() -> ejabberd_config:reload_file(), acl:start(), - shaper:start(). + shaper:start(), + ejabberd_access_permissions:invalidate(). %%% %%% Cluster management diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 33da45013..e4087142b 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -51,6 +51,7 @@ start(normal, _Args) -> db_init(), start(), translate:start(), + ejabberd_access_permissions:start_link(), ejabberd_ctl:init(), ejabberd_commands:init(), ejabberd_admin:start(), diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index d5649b2d7..173071d6f 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -218,23 +218,26 @@ get_command_format/1, get_command_format/2, get_command_format/3, - get_command_policy_and_scope/1, + get_command_policy_and_scope/1, get_command_definition/1, get_command_definition/2, get_tags_commands/0, get_tags_commands/1, - get_exposed_commands/0, + get_exposed_commands/0, register_commands/1, - unregister_commands/1, - expose_commands/1, + unregister_commands/1, + expose_commands/1, execute_command/2, execute_command/3, execute_command/4, execute_command/5, execute_command/6, - opt_type/1, - get_commands_spec/0 - ]). + opt_type/1, + get_commands_spec/0, + get_commands_definition/0, + get_commands_definition/1, + execute_command2/3, + execute_command2/4]). -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). @@ -280,7 +283,8 @@ init() -> {attributes, record_info(fields, ejabberd_commands)}, {type, bag}]), mnesia:add_table_copy(ejabberd_commands, node(), ram_copies), - register_commands(get_commands_spec()). + register_commands(get_commands_spec()), + ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0). -spec register_commands([ejabberd_commands()]) -> ok. @@ -296,7 +300,9 @@ register_commands(Commands) -> mnesia:dirty_write(Command) %% ?DEBUG("This command is already defined:~n~p", [Command]) end, - Commands). + Commands), + ejabberd_access_permissions:invalidate(), + ok. -spec unregister_commands([ejabberd_commands()]) -> ok. @@ -306,7 +312,9 @@ unregister_commands(Commands) -> fun(Command) -> mnesia:dirty_delete_object(Command) end, - Commands). + Commands), + ejabberd_access_permissions:invalidate(), + ok. %% @doc Expose command through ejabberd ReST API. %% Pass a list of command names or policy to expose. @@ -427,6 +435,9 @@ get_command_definition(Name, Version) -> _E -> throw({error, unknown_command}) end. +get_commands_definition() -> + get_commands_definition(?DEFAULT_VERSION). + -spec get_commands_definition(integer()) -> [ejabberd_commands()]. % @doc Returns all commands for a given API version @@ -448,6 +459,18 @@ get_commands_definition(Version) -> end, lists:foldl(F, [], L). +execute_command2(Name, Arguments, CallerInfo) -> + execute_command(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). + +execute_command2(Name, Arguments, CallerInfo, Version) -> + Command = get_command_definition(Name, Version), + case ejabberd_access_permissions:can_access(Name, CallerInfo) of + allow -> + do_execute_command(Command, Arguments); + _ -> + throw({error, access_rules_unauthorized}) + end. + %% @spec (Name::atom(), Arguments) -> ResultTerm %% where %% Arguments = [any()] @@ -811,6 +834,8 @@ is_admin(_Name, admin, _Extra) -> true; is_admin(_Name, {_User, _Server, _, false}, _Extra) -> false; +is_admin(_Name, Map, _extra) when is_map(Map) -> + true; is_admin(Name, Auth, Extra) -> {ACLInfo, Server} = case Auth of {U, S, _, _} -> @@ -832,6 +857,14 @@ is_admin(Name, Auth, Extra) -> deny -> false end. +permission_addon() -> + [{<<"'commands' option compatibility shim">>, + {[], + [{access, ejabberd_config:get_option(commands_admin_access, + fun(V) -> V end, + none)}], + {get_exposed_commands(), []}}}]. + opt_type(commands_admin_access) -> fun acl:access_rules_validator/1; opt_type(commands) -> fun(V) when is_list(V) -> V end; diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 6ca6a40a8..517f2fd2f 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -178,7 +178,8 @@ read_file(File, Opts) -> -spec load_file(string()) -> ok. load_file(File) -> - State = read_file(File), + State0 = read_file(File), + State = validate_opts(State0), set_opts(State). -spec reload_file() -> ok. diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index d52b55cf9..a96a28016 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) -> {ArgsFormat, ResultFormat} -> case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, - Auth, Command, - ArgsFormatted, - Version), + CI = case Auth of + {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S}; + _ -> #{} + end, + CI2 = CI#{caller_module => ?MODULE}, + Result = ejabberd_commands:execute_command2(Command, + ArgsFormatted, + CI2, + Version), format_result(Result, ResultFormat); {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {NumCompa, TextCompa} = diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 4541190ad..d11548c22 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -42,8 +42,10 @@ associate_access_code/3, associate_access_token/3, associate_refresh_token/3, + check_token/1, check_token/4, check_token/2, + scope_in_scope_list/2, process/2, opt_type/1]). @@ -305,6 +307,29 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) -> %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), {ok, AppContext}. +scope_in_scope_list(Scope, ScopeList) -> + TokenScopeSet = oauth2_priv_set:new(Scope), + lists:any(fun(Scope2) -> + oauth2_priv_set:is_member(Scope2, TokenScopeSet) end, + ScopeList). + +check_token(Token) -> + case lookup(Token) of + {ok, #oauth_token{us = US, + scope = TokenScope, + expire = Expire}} -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + if + Expire > TS -> + {ok, US, TokenScope}; + true -> + {false, expired} + end; + _ -> + {false, not_found} + end. + check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 7a95f8c6f..491383769 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -118,9 +118,11 @@ %% ------------------- start(_Host, _Opts) -> + ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0), ok. stop(_Host) -> + ejabberd_access_permissions:unregister_permission_addon(?MODULE), ok. depends(_Host, _Opts) -> @@ -130,76 +132,39 @@ depends(_Host, _Opts) -> %% basic auth %% ---------- -check_permissions(Request, Command) -> - case catch binary_to_existing_atom(Command, utf8) of - Call when is_atom(Call) -> - {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call), - check_permissions2(Request, Call, CommandPolicy, Scope); - _ -> - json_error(404, 40, <<"Endpoint not found.">>) - end. - -check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList) - when HTTPAuth /= undefined -> - Admin = - case lists:keysearch(<<"X-Admin">>, 1, Headers) of - {value, {_, <<"true">>}} -> true; - _ -> false - end, - Auth = - case HTTPAuth of - {SJID, Pass} -> - case jid:from_string(SJID) of - #jid{user = User, server = Server} -> - case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of - true -> {ok, {User, Server, Pass, Admin}}; - false -> false - end; - _ -> - false - end; - {oauth, Token, _} -> - case oauth_check_token(ScopeList, Token) of - {ok, user, {User, Server}} -> - {ok, {User, Server, {oauth, Token}, Admin}}; - {false, Reason} -> - {false, Reason} - end; - _ -> - false - end, - case Auth of - {ok, A} -> {allowed, Call, A}; - {false, no_matching_scope} -> outofscope_response(); - _ -> unauthorized_response() +extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) -> + Info = case HTTPAuth of + {SJID, Pass} -> + case jid:from_string(SJID) of + #jid{luser = User, lserver = Server} -> + case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of + true -> + #{usr => {User, Server, <<"">>}, caller_server => Server}; + false -> + {error, invalid_auth} + end; + _ -> + {error, invalid_auth} + end; + {oauth, Token, _} -> + case ejabberd_oauth:check_token(Token) of + {ok, {U, S}, Scope} -> + #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; + {false, Reason} -> + {error, Reason} + end; + _ -> + #{} + end, + case Info of + Map when is_map(Map) -> + Map#{caller_module => ?MODULE, ip => IP}; + _ -> + ?DEBUG("Invalid auth data: ~p", [Info]), + Info end; -check_permissions2(_Request, Call, open, _Scope) -> - {allowed, Call, noauth}; -check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) -> - Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, - fun(V) -> V end, - none), - Res = acl:match_rule(global, Access, IP), - case Res of - all -> - {allowed, Call, admin}; - [all] -> - {allowed, Call, admin}; - allow -> - {allowed, Call, admin}; - Commands when is_list(Commands) -> - case lists:member(Call, Commands) of - true -> {allowed, Call, admin}; - _ -> outofscope_response() - end; - _E -> - {allowed, Call, noauth} - end; -check_permissions2(_Request, _Call, _Policy, _Scope) -> - unauthorized_response(). - -oauth_check_token(ScopeList, Token) when is_list(ScopeList) -> - ejabberd_oauth:check_token(ScopeList, Token). +extract_auth(#request{ip = IP}) -> + #{ip => IP, caller_module => ?MODULE}. %% ------------------ %% command processing @@ -210,19 +175,12 @@ oauth_check_token(ScopeList, Token) when is_list(ScopeList) -> process(_, #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), badrequest_response(<<"Missing POST data">>); -process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) -> +process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) -> Version = get_api_version(Req), try Args = extract_args(Data), log(Call, Args, IPPort), - case check_permissions(Req, Call) of - {allowed, Cmd, Auth} -> - Result = handle(Cmd, Auth, Args, Version, IP), - json_format(Result); - %% Warning: check_permission direcly formats 401 reply if not authorized - ErrorResponse -> - ErrorResponse - end + perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> @@ -234,7 +192,7 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; -process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> +process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> Version = get_api_version(Req), try Args = case Data of @@ -242,14 +200,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> _ -> Data end, log(Call, Args, IP), - case check_permissions(Req, Call) of - {allowed, Cmd, Auth} -> - Result = handle(Cmd, Auth, Args, Version, IP), - json_format(Result); - %% Warning: check_permission direcly formats 401 reply if not authorized - ErrorResponse -> - ErrorResponse - end + perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> @@ -267,6 +218,22 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), json_error(400, 40, <<"Missing command name.">>). +perform_call(Command, Args, Req, Version) -> + case catch binary_to_existing_atom(Command, utf8) of + Call when is_atom(Call) -> + case extract_auth(Req) of + {error, expired} -> invalid_token_response(); + {error, not_found} -> invalid_token_response(); + {error, invalid_auth} -> unauthorized_response(); + {error, _} -> unauthorized_response(); + Auth when is_map(Auth) -> + Result = handle(Call, Auth, Args, Version), + json_format(Result) + end; + _ -> + json_error(404, 40, <<"Endpoint not found.">>) + end. + %% Be tolerant to make API more easily usable from command-line pipe. extract_args(<<"\n">>) -> []; extract_args(Data) -> @@ -298,7 +265,7 @@ get_api_version([]) -> %% TODO Check accept types of request before decided format of reply. % generic ejabberd command handler -handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> +handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> case ejabberd_commands:get_command_format(Call, Auth, Version) of {ArgsSpec, _} when is_list(ArgsSpec) -> Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], @@ -315,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> [{Key, undefined}|Acc] end, [], ArgsSpec), try - handle2(Call, Auth, match(Args2, Spec), Version, IP) + handle2(Call, Auth, match(Args2, Spec), Version) catch throw:not_found -> {404, <<"not_found">>}; throw:{not_found, Why} when is_atom(Why) -> @@ -354,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> {400, <<"Error">>} end. -handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) -> +handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), ArgsFormatted = format_args(Args, ArgsF), - ejabberd_command(Auth, Call, ArgsFormatted, Version, IP). + case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Call, Auth, Res, Version) + end. get_elem_delete(A, L) -> case proplists:get_all_values(A, L) of @@ -456,18 +428,6 @@ process_unicode_codepoints(Str) -> match(Args, Spec) -> [{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec]. -ejabberd_command(Auth, Cmd, Args, Version, IP) -> - Access = case Auth of - admin -> []; - _ -> undefined - end, - case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of - {error, Error} -> - throw(Error); - Res -> - format_command_result(Cmd, Auth, Res, Version) - end. - format_command_result(Cmd, Auth, Result, Version) -> {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of @@ -538,6 +498,9 @@ format_error_result(_ErrorAtom, Code, Msg) -> {500, Code, iolist_to_binary(Msg)}. unauthorized_response() -> + json_error(401, 10, <<"You are not authorized to call this command.">>). + +invalid_token_response() -> json_error(401, 10, <<"Oauth Token is invalid or expired.">>). outofscope_response() -> @@ -571,5 +534,31 @@ log(Call, Args, {Addr, Port}) -> log(Call, Args, IP) -> ?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]). +permission_addon() -> + Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, + fun(V) -> V end, + none), + Rules = acl:resolve_access(Access, global), + R = lists:filtermap( + fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow -> + {true, {[{allow, AclRules}], {[<<"*">>], []}}}; + ({List, AclRules}) when is_list(List) -> + {true, {[{allow, AclRules}], {List, []}}}; + (_) -> + false + end, Rules), + case R of + [] -> + none; + _ -> + {_, Res} = lists:foldl( + fun({R2, L2}, {Idx, Acc}) -> + {Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ", + (integer_to_binary(Idx))/binary>>, + {[?MODULE], [{access, R2}], L2}} | Acc]} + end, {1, []}, R), + Res + end. + mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1; mod_opt_type(_) -> [admin_ip_access]. From 695d22ef95f4b366b0b66123d37dee6674ee79db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Wed, 5 Oct 2016 13:54:29 +0200 Subject: [PATCH 151/179] Initialize ejabberd_access_permissions in elixir tests --- test/ejabberd_admin_test.exs | 1 + test/ejabberd_commands_mock_test.exs | 1 + test/ejabberd_commands_test.exs | 1 + test/mod_admin_extra_test.exs | 1 + test/mod_http_api_mock_test.exs | 13 +++++++------ test/mod_http_api_test.exs | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs index 1c999314c..31b8ab2e2 100644 --- a/test/ejabberd_admin_test.exs +++ b/test/ejabberd_admin_test.exs @@ -28,6 +28,7 @@ defmodule EjabberdAdminTest do # For some myterious reason, :ejabberd_commands.init mays # sometimes fails if module is not loaded before {:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands) + {:ok, _} = :ejabberd_access_permissions.start_link() :ejabberd_commands.init :ejabberd_admin.start :ok diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 785e74cd7..419a989d6 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -50,6 +50,7 @@ defmodule EjabberdCommandsMockTest do :mnesia.start :ok = :jid.start :ok = :ejabberd_config.start(["domain1", "domain2"], []) + {:ok, _} = :ejabberd_access_permissions.start_link() :ok = :acl.start EjabberdOauthMock.init on_exit fn -> :meck.unload end diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs index 10b656140..c8219d0cf 100644 --- a/test/ejabberd_commands_test.exs +++ b/test/ejabberd_commands_test.exs @@ -30,6 +30,7 @@ defmodule EjabberdCommandsTest do :mnesia.start :stringprep.start :ok = :ejabberd_config.start(["localhost"], []) + {:ok, _} = :ejabberd_access_permissions.start_link() :ejabberd_commands.init :ok diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs index 03422264f..fde66f03f 100644 --- a/test/mod_admin_extra_test.exs +++ b/test/mod_admin_extra_test.exs @@ -45,6 +45,7 @@ defmodule EjabberdModAdminExtraTest do rescue _ -> :ok end + {:ok, _} = :ejabberd_access_permissions.start_link() :ejabberd_commands.init :ok = :ejabberd_config.start([@domain], []) :mod_admin_extra.start(@domain, []) diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index 9cba35365..4809ecd59 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -46,6 +46,7 @@ defmodule ModHttpApiMockTest do :mnesia.start :stringprep.start :ejabberd_config.start([@domain], []) + {:ok, _} = :ejabberd_access_permissions.start_link() :ejabberd_commands.init rescue _ -> :ok @@ -240,10 +241,10 @@ defmodule ModHttpApiMockTest do result = :ejabberd_oauth.process([], req) assert 200 = elem(result, 0) #http code {kv} = :jiffy.decode(elem(result,2)) - assert {_, "bearer"} = List.keyfind(kv, "token_type", 0) - assert {_, @command} = List.keyfind(kv, "scope", 0) - assert {_, 4000} = List.keyfind(kv, "expires_in", 0) - {"access_token", _token} = List.keyfind(kv, "access_token", 0) + assert {_, "bearer"} = List.keyfind(kv, "token_type", 0) + assert {_, @command} = List.keyfind(kv, "scope", 0) + assert {_, 4000} = List.keyfind(kv, "expires_in", 0) + {"access_token", _token} = List.keyfind(kv, "access_token", 0) #missing grant_type req = request(method: :POST, @@ -254,7 +255,7 @@ defmodule ModHttpApiMockTest do result = :ejabberd_oauth.process([], req) assert 400 = elem(result, 0) #http code {kv} = :jiffy.decode(elem(result,2)) - assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0) + assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0) # incorrect user/pass @@ -266,7 +267,7 @@ defmodule ModHttpApiMockTest do result = :ejabberd_oauth.process([], req) assert 400 = elem(result, 0) #http code {kv} = :jiffy.decode(elem(result,2)) - assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0) + assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0) assert :meck.validate :ejabberd_auth assert :meck.validate :ejabberd_commands diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index e2ae3d784..efe090a3e 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -31,6 +31,7 @@ defmodule ModHttpApiTest do :ok = :mnesia.start :stringprep.start :ok = :ejabberd_config.start(["localhost"], []) + {:ok, _} = :ejabberd_access_permissions.start_link() :ok = :ejabberd_commands.init :ok = :ejabberd_commands.register_commands(cmds) on_exit fn -> From a78c3422cd7380af21580c935167417ac427a176 Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 5 Oct 2016 15:01:33 +0200 Subject: [PATCH 152/179] Fix typos in Czech translation (#1318) --- priv/msgs/cs.msg | 5 ++--- priv/msgs/cs.po | 8 ++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/priv/msgs/cs.msg b/priv/msgs/cs.msg index f0c749887..01897aadf 100644 --- a/priv/msgs/cs.msg +++ b/priv/msgs/cs.msg @@ -144,7 +144,7 @@ {"Import Users from Dir at ","Importovat uživatele z adresáře na "}. {"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}. {"Improper message type","Nesprávný typ zprávy"}. -{"Incoming s2s Connections:",""}. +{"Incoming s2s Connections:","Příchozí s2s spojení:"}. {"Incorrect password","Nesprávné heslo"}. {"Invalid affiliation: ~s","Neplatné přiřazení: ~s"}. {"Invalid role: ~s","Neplatná role: ~s"}. @@ -333,7 +333,6 @@ {"September",". září"}. {"Server ~b","Server ~b"}. {"Server:","Server:"}. -{"Server","Server"}. {"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. {"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. {"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}. @@ -409,10 +408,10 @@ {"User JID","Jabber ID uživatele"}. {"User Management","Správa uživatelů"}. {"Username:","Uživatelské jméno:"}. -{"User ~s",""}. {"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}. {"Users Last Activity","Poslední aktivita uživatele"}. {"Users","Uživatelé"}. +{"User ~s","Uživatel ~s"}. {"User","Uživatel"}. {"Validate","Ověřit"}. {"vCard User Search","Hledání uživatelů podle vizitek"}. diff --git a/priv/msgs/cs.po b/priv/msgs/cs.po index 81c60756e..4f06621ac 100644 --- a/priv/msgs/cs.po +++ b/priv/msgs/cs.po @@ -242,9 +242,7 @@ msgstr "Odchozí s2s spojení:" #: ejabberd_web_admin.erl:1559 msgid "Incoming s2s Connections:" -msgstr "" -"Příchozí\n" -" s2s spojení:" +msgstr "Příchozí s2s spojení:" #: ejabberd_web_admin.erl:1595 ejabberd_web_admin.erl:1794 #: ejabberd_web_admin.erl:1804 ejabberd_web_admin.erl:2214 mod_roster.erl:1429 @@ -258,9 +256,7 @@ msgstr "Změnit heslo" #: ejabberd_web_admin.erl:1673 msgid "User ~s" -msgstr "" -"Uživatel\n" -" ~s" +msgstr "Uživatel ~s" #: ejabberd_web_admin.erl:1684 msgid "Connected Resources:" From 8accb8ee0c4a14d35f0296fc09b2989bff949275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 6 Oct 2016 10:47:17 +0200 Subject: [PATCH 153/179] Use proper default value for api_permissions --- src/ejabberd_access_permissions.erl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index f37de9a13..ac023b3f2 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -228,15 +228,16 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%=================================================================== +-spec get_definitions(#state{}) -> {#state{}, any()}. get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) -> DefaultOptions = [{<<"console commands">>, - [ejabberd_ctl], - [{acl, all}], - {all, none}}, + {[ejabberd_ctl], + [{acl, all}], + {all, none}}}, {<<"admin access">>, - [], - [{acl, admin}], - {all, [start, stop]}}], + {[], + [{acl, admin}], + {all, [start, stop]}}}], NDefs = case Defs of none -> ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions), From 438dbc8bda36fcee672926a0692fdcd276c30727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 6 Oct 2016 10:59:31 +0200 Subject: [PATCH 154/179] Make handling of oauth clauses be more consistent with other rules --- src/ejabberd_access_permissions.erl | 34 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index ac023b3f2..3b88e6ade 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -267,15 +267,18 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) -> acl:access_matches(Access, CallerInfo, Host) == allow; ({acl, _} = Acl) when Scope == none -> acl:acl_rule_matches(Acl, CallerInfo, Host); - ({oauth, List}) when Scope /= none -> - lists:all( - fun({access, Access}) -> - acl:access_matches(Access, CallerInfo, Host) == allow; - ({acl, _} = Acl) -> - acl:acl_rule_matches(Acl, CallerInfo, Host); - ({scope, Scopes}) -> - ejabberd_oauth:scope_in_scope_list(Scope, Scopes) - end, List); + ({oauth, Scopes, List}) when Scope /= none -> + case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of + true -> + lists:any( + fun({access, Access}) -> + acl:access_matches(Access, CallerInfo, Host) == allow; + ({acl, _} = Acl) -> + acl:acl_rule_matches(Acl, CallerInfo, Host) + end, List); + _ -> + false + end; (_) -> false end, Who); @@ -370,7 +373,18 @@ parse_who(Name, Defs, ParseOauth) when is_list(Defs) -> ([{oauth, OauthList}]) when is_list(OauthList) -> case ParseOauth of oauth -> - {oauth, parse_who(Name, lists:flatten(OauthList), scope)}; + Nested = parse_who(Name, lists:flatten(OauthList), scope), + {Scopes, Rest} = lists:partition( + fun({scope, _}) -> true; + (_) -> false + end, Nested), + case Scopes of + [] -> + report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>, + [Name]); + _ -> + {oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest} + end; scope -> report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>, [Name]) From b01fbfadf3c5070db09277f0d0d7282e06ae56f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 6 Oct 2016 11:03:26 +0200 Subject: [PATCH 155/179] Use correct field for oauth scope --- src/ejabberd_access_permissions.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 3b88e6ade..766982a01 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -261,7 +261,7 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) -> true -> case From == [] orelse lists:member(Module, From) of true -> - Scope = maps:get(scope, CallerInfo, none), + Scope = maps:get(oauth_scope, CallerInfo, none), lists:any( fun({access, Access}) when Scope == none -> acl:access_matches(Access, CallerInfo, Host) == allow; From 0ae84a646f91413b3cdf309c72d1a3c77256cdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Thu, 6 Oct 2016 11:17:10 +0200 Subject: [PATCH 156/179] Disable one test for now, we may change how this part is handled --- test/mod_http_api_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index efe090a3e..c68270f1f 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -47,12 +47,12 @@ defmodule ModHttpApiTest do assert Enum.member?(commands, :user_cmd) end - test "We can call open commands without authentication" do - setup_mocks() - :ejabberd_commands.expose_commands([:open_cmd]) - request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {200, _, _} = :mod_http_api.process(["open_cmd"], request) - end +# test "We can call open commands without authentication" do +# setup_mocks() +# :ejabberd_commands.expose_commands([:open_cmd]) +# request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") +# {200, _, _} = :mod_http_api.process(["open_cmd"], request) +# end # This related to the commands config file option test "Attempting to access a command that is not exposed as HTTP API returns 403" do From e54ba3db5b6a3af66cfc84b9624cf2438c83449b Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Thu, 6 Oct 2016 23:20:45 +0200 Subject: [PATCH 157/179] XEP-0198: Cope with invalid 'from'/'to' attributes Check whether the 'from' and 'to' attributes are valid before bouncing or resending a stanza from the stream management queue. They might be invalid in certain corner cases. Thanks to Evgeniy for spotting this. --- src/ejabberd_c2s.erl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 226c5e0da..6068c85ef 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2998,10 +2998,13 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F) lists:foreach( fun({_, Time, #xmlel{attrs = Attrs} = El}) -> From_s = fxml:get_attr_s(<<"from">>, Attrs), - From = jid:from_string(From_s), To_s = fxml:get_attr_s(<<"to">>, Attrs), - To = jid:from_string(To_s), - F(From, To, El, Time) + case {jid:from_string(From_s), jid:from_string(To_s)} of + {#jid{} = From, #jid{} = To} -> + F(From, To, El, Time); + {_, _} -> + ?DEBUG("Dropping stanza due to invalid JID(s)", []) + end end, queue:to_list(Queue)) end; handle_unacked_stanzas(_StateData, _F) -> From d701230555a57bbaa5d700ed1b8575f953cac2c9 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Fri, 7 Oct 2016 00:36:47 +0200 Subject: [PATCH 158/179] Make map syntax compatible with Erlang/OTP 17 --- src/ejabberd_access_permissions.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 766982a01..153be0940 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -506,9 +506,9 @@ is_valid_command_name2(_) -> key_split(Args, Fields) -> {_, Order1, Results1, Required1} = lists:foldl( fun({Field, Default}, {Idx, Order, Results, Required}) -> - {Idx + 1, Order#{Field => Idx}, [Default | Results], Required}; + {Idx + 1, maps:put(Field, Idx, Order), [Default | Results], Required}; (Field, {Idx, Order, Results, Required}) -> - {Idx + 1, Order#{Field => Idx}, [none | Results], Required#{Field => 1}} + {Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)} end, {1, #{}, [], #{}}, Fields), key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}). @@ -522,7 +522,7 @@ key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) -> case maps:is_key(Arg, Duplicates) of false -> Results2 = setelement(Idx, Results, Value), - key_split(Rest, Results2, Order, maps:remove(Arg, Required), Duplicates#{Arg => 1}); + key_split(Rest, Results2, Order, maps:remove(Arg, Required), maps:put(Arg, 1, Duplicates)); true -> parse_error(<<"Duplicate field '~s'">>, [Arg]) end; From dffcfe74d4f3ecbdc6c0e0f082b1f278e8851a79 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 10 Oct 2016 00:17:17 +0200 Subject: [PATCH 159/179] Don't let "send_message" duplicate the message In the past, the "send_message" command sent a copy of the message to each resource if the message was addressed to the bare JID of a local online user. When message carbons are enabled, this creates duplicates; and with MAM enabled, each copy is archived. Therefore, "send_message" no longer creates copies of the message. --- src/mod_admin_extra.erl | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 2bb436f31..8ce11b872 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -1357,44 +1357,9 @@ srg_user_del(User, Host, Group, GroupHost) -> %% @doc Send a message to a Jabber account. %% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok send_message(Type, From, To, Subject, Body) -> + FromJID = jid:from_string(From), + ToJID = jid:from_string(To), Packet = build_packet(Type, Subject, Body), - send_packet_all_resources(From, To, Packet). - -%% @doc Send a packet to a Jabber account. -%% If a resource was specified in the JID, -%% the packet is sent only to that specific resource. -%% If no resource was specified in the JID, -%% and the user is remote or local but offline, -%% the packet is sent to the bare JID. -%% If the user is local and is online in several resources, -%% the packet is sent to all its resources. -send_packet_all_resources(FromJIDString, ToJIDString, Packet) -> - FromJID = jid:from_string(FromJIDString), - ToJID = jid:from_string(ToJIDString), - ToUser = ToJID#jid.user, - ToServer = ToJID#jid.server, - case ToJID#jid.resource of - <<>> -> - send_packet_all_resources(FromJID, ToUser, ToServer, Packet); - Res -> - send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet) - end. - -send_packet_all_resources(FromJID, ToUser, ToServer, Packet) -> - case ejabberd_sm:get_user_resources(ToUser, ToServer) of - [] -> - send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet); - ToResources -> - lists:foreach( - fun(ToResource) -> - send_packet_all_resources(FromJID, ToUser, ToServer, - ToResource, Packet) - end, - ToResources) - end. - -send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) -> - ToJID = jid:make(ToU, ToS, ToR), ejabberd_router:route(FromJID, ToJID, Packet). build_packet(Type, Subject, Body) -> From ead7e21037230d0313a0cb49a83250b37ad9160d Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 11 Oct 2016 22:20:22 +0200 Subject: [PATCH 160/179] Ignore offline sessions in statistics Offline sessions should not be counted when reporting the number of connected resources. Apart from that, this number is now also reported when using a non-default session management backend. --- src/ejabberd_web_admin.erl | 4 ++-- src/mod_stats.erl | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 184f9775b..fb57fa560 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -1552,7 +1552,7 @@ su_to_list({Server, User}) -> %%%% get_stats get_stats(global, Lang) -> - OnlineUsers = mnesia:table_info(session, size), + OnlineUsers = ejabberd_sm:connected_users_number(), RegisteredUsers = lists:foldl(fun (Host, Total) -> ejabberd_auth:get_vh_registered_users_number(Host) + Total @@ -2178,7 +2178,7 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) -> CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]), CPUTimeS = list_to_binary(io_lib:format("~.3f", [element(1, CPUTime) / 1000])), - OnlineUsers = mnesia:table_info(session, size), + OnlineUsers = ejabberd_sm:connected_users_number(), TransactionsCommitted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]), TransactionsAborted = ejabberd_cluster:call(Node, mnesia, diff --git a/src/mod_stats.erl b/src/mod_stats.erl index 99059839a..66bbb5b5b 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -161,13 +161,8 @@ get_local_stat(Server, [], Name) end; get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/online">> -> - case catch mnesia:table_info(session, size) of - {'EXIT', _Reason} -> - ?STATERR(<<"500">>, <<"Internal Server Error">>); - Users -> - ?STATVAL((iolist_to_binary(integer_to_list(Users))), - <<"users">>) - end; + Users = ejabberd_sm:connected_users_number(), + ?STATVAL((iolist_to_binary(integer_to_list(Users))), <<"users">>); get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/total">> -> NumUsers = lists:foldl(fun (Host, Total) -> From b693601dd1cc65681d3b518e69843b0d7f6d0800 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Wed, 12 Oct 2016 23:10:25 +0200 Subject: [PATCH 161/179] Don't let MAM messages go into offline storage --- src/mod_mam.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index f6d3c8f1f..8f6492047 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -966,13 +966,14 @@ send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) -> NS == ?NS_MAM_0; NS == ?NS_MAM_1 -> [{<<"complete">>, jlib:atom_to_binary(IsComplete)}] end, + Hint = [#xmlel{name = <<"no-store">>, attrs = [{<<"xmlns">>, ?NS_HINTS}]}], Els = lists:map( fun({ID, _IDInt, El}) -> #xmlel{name = <<"message">>, children = [#xmlel{name = <<"result">>, attrs = [{<<"xmlns">>, NS}, {<<"id">>, ID}|QIDAttr], - children = [El]}]} + children = [El]} | Hint]} end, Msgs), RSMOut = make_rsm_out(Msgs, RSM, Count, QIDAttr ++ CompleteAttr, NS), if NS == ?NS_MAM_TMP; NS == ?NS_MAM_1 -> @@ -990,7 +991,7 @@ send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) -> end, Els), ejabberd_router:route( To, From, #xmlel{name = <<"message">>, - children = RSMOut}), + children = RSMOut ++ Hint}), ignore end. From d97e777c9bc721a6b342b1f941150de0f94ff15d Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Thu, 13 Oct 2016 22:34:29 +0200 Subject: [PATCH 162/179] Always include with ad-hoc responses XEP-0050 says: "The result for each stage (other than the last) of a command's execution SHOULD include an element." Some clients insist on this. --- src/adhoc.erl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/adhoc.erl b/src/adhoc.erl index 6970584f9..23ffd8dd8 100644 --- a/src/adhoc.erl +++ b/src/adhoc.erl @@ -112,9 +112,17 @@ produce_response( ProvidedSessionID /= <<"">> -> ProvidedSessionID; true -> jlib:now_to_utc_string(p1_time_compat:timestamp()) end, - case Actions of - [] -> + case {Actions, Status} of + {[], completed} -> ActionsEls = []; + {[], _} -> + ActionsEls = [ + #xmlel{ + name = <<"actions">>, + attrs = [{<<"execute">>, <<"complete">>}], + children = [#xmlel{name = <<"complete">>}] + } + ]; _ -> case DefaultAction of <<"">> -> ActionsElAttrs = []; From a1faecc4c968c769da97f400032c9c02e011dfae Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 14 Oct 2016 12:52:59 +0200 Subject: [PATCH 163/179] Introduce muc_invite hook This adds a new hook that is triggered for each invite to an MUC room: - muc_invite(RoomJID, RoomConfig, From, To, Reason) -> ok where - RoomJID = From = To = #jid (see jlib.h) - RoomConfig = #config (see mod_muc_room.hrl) - Reason = binary() --- src/mod_muc_room.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index fc2aeebb6..5bee4f0b7 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -5056,6 +5056,8 @@ process_invitations(From, InviteEls, Lang, StateData) -> throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); JID1 -> JID1 end, + ejabberd_hooks:run(muc_invite, StateData#state.server_host, + [StateData#state.jid, StateData#state.config, From, JID, Reason]), ejabberd_router:route(StateData#state.jid, JID, Msg), JID end, From fd6f0f94b5b4b9e3abc7e65e8d639864c9774563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Fri, 14 Oct 2016 13:55:50 +0200 Subject: [PATCH 164/179] Convert ejabberd_xmlrpc to new api_permissions --- src/ejabberd_access_permissions.erl | 5 +- src/ejabberd_commands.erl | 2 +- src/ejabberd_xmlrpc.erl | 80 +++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 153be0940..7ce75aa9c 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -129,7 +129,8 @@ init([]) -> handle_call({can_access, Cmd, CallerInfo}, _From, State) -> CallerModule = maps:get(caller_module, CallerInfo, none), Host = maps:get(caller_host, CallerInfo, global), - {State2, Defs} = get_definitions(State), + {State2, Defs0} = get_definitions(State), + Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0, Res = lists:foldl( fun({Name, _} = Def, none) -> case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of @@ -257,7 +258,7 @@ get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) {State#state{definitions = NDefs}, NDefs}. matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) -> - case lists:member(Cmd, What) of + case What == all orelse lists:member(Cmd, What) of true -> case From == [] orelse lists:member(Module, From) of true -> diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 173071d6f..8d74ad5a2 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -460,7 +460,7 @@ get_commands_definition(Version) -> lists:foldl(F, [], L). execute_command2(Name, Arguments, CallerInfo) -> - execute_command(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). + execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). execute_command2(Name, Arguments, CallerInfo, Version) -> Command = get_command_definition(Name, Version), diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 6680451e4..1dd88f837 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -47,7 +47,8 @@ -record(state, {access_commands = [] :: list(), auth = noauth :: noauth | {binary(), binary(), binary()}, - get_auth = true :: boolean()}). + get_auth = true :: boolean(), + ip :: inet:ip_address()}). %% Test: @@ -195,7 +196,7 @@ socket_type() -> raw. %% ----------------------------- %% HTTP interface %% ----------------------------- -process(_, #request{method = 'POST', data = Data, opts = Opts}) -> +process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) -> AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts, fun(L) when is_list(L) -> L end, undefined), @@ -206,7 +207,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) -> lists:flatmap( fun({Ac, AcOpts}) -> Commands = gen_mod:get_opt( - commands, AcOpts, + commands, lists:flatten(AcOpts), fun(A) when is_atom(A) -> A; (L) when is_list(L) -> @@ -219,15 +220,15 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) -> options, AcOpts, fun(L) when is_list(L) -> L end, []), - [{Ac, Commands, CommOpts}]; + [{<<"ejabberd_xmlrpc compatibility shim">>, {[?MODULE], [{access, Ac}], Commands}}]; (Wrong) -> ?WARNING_MSG("wrong options format for ~p: ~p", [?MODULE, Wrong]), [] - end, AccessCommandsOpts) + end, lists:flatten(AccessCommandsOpts)) end, GetAuth = true, - State = #state{access_commands = AccessCommands, get_auth = GetAuth}, + State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP}, case fxml_stream:parse_element(Data) of {error, _} -> {400, [], @@ -258,21 +259,35 @@ process(_, _) -> %% Access verification %% ----------------------------- -get_auth(AuthList) -> - Admin = - case lists:keysearch(admin, 1, AuthList) of - {value, {admin, true}} -> true; - _ -> false - end, +extract_auth(AuthList) -> + ?DEBUG("AUTHLIST ~p", [AuthList]), try get_attrs([user, server, token], AuthList) of - [U, S, T] -> {U, S, {oauth, T}, Admin} + [U0, S0, T] -> + U = jid:nodeprep(U0), + S = jid:nameprep(S0), + case ejabberd_oauth:check_token(T) of + {ok, {U, S}, Scope} -> + #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; + {false, Reason} -> + {error, Reason}; + _ -> + {error, not_found} + end catch exit:{attribute_not_found, _Attr, _} -> try get_attrs([user, server, password], AuthList) of - [U, S, P] -> {U, S, P, Admin} + [U0, S0, P] -> + U = jid:nodeprep(U0), + S = jid:nameprep(S0), + case ejabberd_auth:check_password(U, <<"">>, S, P) of + true -> + #{usr => {U, S, <<"">>}, caller_server => S}; + false -> + {error, invalid_auth} + end catch - exit:{attribute_not_found, Attr, _} -> - throw({error, missing_auth_arguments, Attr}) + exit:{attribute_not_found, _Attr, _} -> + #{} end end. @@ -300,12 +315,28 @@ get_auth(AuthList) -> %% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}). %% {ok,{response,[152]}} -handler(#state{get_auth = true, auth = noauth} = State, +handler(#state{get_auth = true, auth = noauth, ip = IP} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) -> - try get_auth(AuthList) of + try extract_auth(AuthList) of + {error, invalid_auth} -> + build_fault_response(-118, + "Invalid authentication data", + []); + {error, not_found} -> + build_fault_response(-118, + "Invalid oauth token", + []); + {error, expired} -> + build_fault_response(-118, + "Invalid oauth token", + []); + {error, Value} -> + build_fault_response(-118, + "Invalid authentication data: ~p", + [Value]); Auth -> - handler(State#state{get_auth = false, auth = Auth}, + handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}}, {call, Method, Arguments}) catch {error, missing_auth_arguments, _Attr} -> @@ -393,9 +424,14 @@ build_fault_response(Code, ParseString, ParseArgs) -> do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) -> ArgsFormatted = format_args(AttrL, ArgsF), + Auth2 = case AccessCommands of + V when is_list(V) -> + Auth#{extra_permissions => AccessCommands}; + _ -> + Auth + end, Result = - ejabberd_commands:execute_command(AccessCommands, Auth, - Command, ArgsFormatted), + ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2), ResultFormatted = format_result(Result, ResultF), {command_result, ResultFormatted}. @@ -489,6 +525,8 @@ process_unicode_codepoints(Str) -> format_result({error, Error}, _) -> throw({error, Error}); +format_result({error, _Type, _Code, Error}, _) -> + throw({error, Error}); format_result(String, string) -> lists:flatten(String); format_result(Atom, {Name, atom}) -> {struct, From 305d4c05dcde2b7003d822dded3a13a7bbab046f Mon Sep 17 00:00:00 2001 From: Jerome Sautret Date: Mon, 10 Oct 2016 11:32:36 +0200 Subject: [PATCH 165/179] Fix delete_old_messages command for SQL backends --- src/mod_offline_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index feefd3dd0..d9de50e04 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -81,7 +81,7 @@ remove_old_messages(Days, LServer) -> [<<"DELETE FROM spool" " WHERE created_at < " "NOW() - INTERVAL '">>, - integer_to_list(Days), <<"';">>]) of + integer_to_list(Days), <<"' DAY;">>]) of {updated, N} -> ?INFO_MSG("~p message(s) deleted from offline spool", [N]); _Error -> From 4c5460f0bd640e11948b8cb9d090a4cdfcfa53e7 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 18 Oct 2016 08:17:21 +0300 Subject: [PATCH 166/179] Get rid of compile warnings for random/crypto modules on R19 --- rebar.config | 1 + src/cyrsasl_scram.erl | 4 ++-- src/ejabberd.erl | 4 +--- src/ejabberd_auth_mnesia.erl | 2 +- src/ejabberd_auth_riak.erl | 2 +- src/ejabberd_auth_sql.erl | 2 +- src/ejabberd_s2s_out.erl | 3 +-- src/ejabberd_service.erl | 4 ++-- src/extauth.erl | 3 +-- src/mod_echo.erl | 2 +- src/randoms.erl | 19 +++++++++++++++++-- 11 files changed, 29 insertions(+), 17 deletions(-) diff --git a/rebar.config b/rebar.config index 434c16af3..27439109b 100644 --- a/rebar.config +++ b/rebar.config @@ -71,6 +71,7 @@ {if_var_match, db_type, mssql, {d, 'mssql'}}, {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, {if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}}, + {if_version_above, "18", {d, 'STRONG_RAND_BYTES'}}, {if_var_true, hipe, native}, {src_dirs, [asn1, src, {if_var_true, tools, tools}, diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl index 18f52b48f..1c464e121 100644 --- a/src/cyrsasl_scram.erl +++ b/src/cyrsasl_scram.erl @@ -87,7 +87,7 @@ mech_step(#state{step = 2} = State, ClientIn) -> if is_tuple(Ret) -> Ret; true -> TempSalt = - crypto:rand_bytes(?SALT_LENGTH), + randoms:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Ret, TempSalt, @@ -101,7 +101,7 @@ mech_step(#state{step = 2} = State, ClientIn) -> str:substr(ClientIn, str:str(ClientIn, <<"n=">>)), ServerNonce = - jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)), + jlib:encode_base64(randoms:bytes(?NONCE_LENGTH)), ServerFirstMessage = iolist_to_binary( ["r=", diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 6bd2422ae..5a6fc64d7 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -105,8 +105,6 @@ start_app([], _Type, _StartFlag) -> ok. check_app_modules(App, StartFlag) -> - {A, B, C} = p1_time_compat:timestamp(), - random:seed(A, B, C), sleep(5000), case application:get_key(App, modules) of {ok, Mods} -> @@ -140,7 +138,7 @@ exit_or_halt(Reason, StartFlag) -> end. sleep(N) -> - timer:sleep(random:uniform(N)). + timer:sleep(randoms:uniform(N)). get_module_file(App, Mod) -> BaseName = atom_to_list(Mod), diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 2a4554d15..f36c9fbc7 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -450,7 +450,7 @@ password_to_scram(Password) -> ?SCRAM_DEFAULT_ITERATION_COUNT). password_to_scram(Password, IterationCount) -> - Salt = crypto:rand_bytes(?SALT_LENGTH), + Salt = randoms:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Password, Salt, IterationCount), StoredKey = diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl index c74f1b28e..05add262e 100644 --- a/src/ejabberd_auth_riak.erl +++ b/src/ejabberd_auth_riak.erl @@ -270,7 +270,7 @@ password_to_scram(Password) -> ?SCRAM_DEFAULT_ITERATION_COUNT). password_to_scram(Password, IterationCount) -> - Salt = crypto:rand_bytes(?SALT_LENGTH), + Salt = randoms:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Password, Salt, IterationCount), StoredKey = diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index d6d945e02..93dac4f4f 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -406,7 +406,7 @@ password_to_scram(Password) -> ?SCRAM_DEFAULT_ITERATION_COUNT). password_to_scram(Password, IterationCount) -> - Salt = crypto:rand_bytes(?SALT_LENGTH), + Salt = randoms:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Password, Salt, IterationCount), StoredKey = diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index a30f2f438..ae3433a6a 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -1099,13 +1099,12 @@ get_addr_port(Server) -> ?DEBUG("srv lookup of '~s': ~p~n", [Server, HEnt#hostent.h_addr_list]), AddrList = HEnt#hostent.h_addr_list, - random:seed(p1_time_compat:timestamp()), case catch lists:map(fun ({Priority, Weight, Port, Host}) -> N = case Weight of 0 -> 0; _ -> - (Weight + 1) * random:uniform() + (Weight + 1) * randoms:uniform() end, {Priority * 65536 - N, Host, Port} end, diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 9dd7c831e..26374c1f1 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -135,13 +135,13 @@ init([{SockMod, Socket}, Opts]) -> fun({H, Os}, D) -> P = proplists:get_value( password, Os, - p1_sha:sha(crypto:rand_bytes(20))), + p1_sha:sha(randoms:bytes(20))), dict:store(H, P, D) end, dict:new(), HOpts); false -> Pass = proplists:get_value( password, Opts, - p1_sha:sha(crypto:rand_bytes(20))), + p1_sha:sha(randoms:bytes(20))), dict:from_list([{global, Pass}]) end, %% privilege access to entities data diff --git a/src/extauth.erl b/src/extauth.erl index 50330b47b..6063d3670 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -102,8 +102,7 @@ call_port(Server, Msg) -> receive {eauth, Result} -> Result end. random_instance(MaxNum) -> - random:seed(p1_time_compat:timestamp()), - random:uniform(MaxNum) - 1. + randoms:uniform(MaxNum) - 1. get_instances(Server) -> ejabberd_config:get_option( diff --git a/src/mod_echo.erl b/src/mod_echo.erl index 96651aebf..da3f5cf0f 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -172,7 +172,7 @@ do_client_version(disabled, _From, _To) -> ok; do_client_version(enabled, From, To) -> ToS = jid:to_string(To), Random_resource = - iolist_to_binary(integer_to_list(random:uniform(100000))), + iolist_to_binary(integer_to_list(randoms:uniform(100000))), From2 = From#jid{resource = Random_resource, lresource = Random_resource}, Packet = #xmlel{name = <<"iq">>, diff --git a/src/randoms.erl b/src/randoms.erl index 52fceef4e..75bc280a2 100644 --- a/src/randoms.erl +++ b/src/randoms.erl @@ -27,14 +27,29 @@ -author('alexey@process-one.net'). --export([get_string/0]). +-export([get_string/0, uniform/0, uniform/1, bytes/1]). -export([start/0]). +-define(THRESHOLD, 16#10000000000000000). + start() -> ok. get_string() -> - R = crypto:rand_uniform(0, 16#10000000000000000), + R = crypto:rand_uniform(0, ?THRESHOLD), jlib:integer_to_binary(R). +uniform() -> + crypto:rand_uniform(0, ?THRESHOLD)/?THRESHOLD. + +uniform(N) -> + crypto:rand_uniform(0, N). + +-ifdef(STRONG_RAND_BYTES). +bytes(N) -> + crypto:strong_rand_bytes(N). +-else. +bytes(N) -> + crypto:rand_bytes(N). +-endif. From d19552f4648d87b08e0740f467f112fd4782724a Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 18 Oct 2016 08:35:47 +0300 Subject: [PATCH 167/179] Fix randoms:uniform/1 return Make sure randoms:uniform/1 returns values from the same interval as deprecated random:uniform/1 --- src/randoms.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/randoms.erl b/src/randoms.erl index 75bc280a2..1353f48af 100644 --- a/src/randoms.erl +++ b/src/randoms.erl @@ -44,7 +44,7 @@ uniform() -> crypto:rand_uniform(0, ?THRESHOLD)/?THRESHOLD. uniform(N) -> - crypto:rand_uniform(0, N). + crypto:rand_uniform(1, N+1). -ifdef(STRONG_RAND_BYTES). bytes(N) -> From 3b876875e9f3e07d16fd75ed8a78a8ffd88a671a Mon Sep 17 00:00:00 2001 From: colm Date: Tue, 18 Oct 2016 21:59:34 +0100 Subject: [PATCH 168/179] mod_muc: made the constant MAX_ROOMS_DISCOITEMS configurable --- src/mod_muc.erl | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/mod_muc.erl b/src/mod_muc.erl index ad2be4cce..6b878b05b 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -43,7 +43,7 @@ forget_room/3, create_room/5, shutdown_rooms/1, - process_iq_disco_items/4, + process_iq_disco_items/5, broadcast_service_message/2, export/1, import/1, @@ -66,13 +66,12 @@ server_host = <<"">> :: binary(), access = {none, none, none, none} :: {atom(), atom(), atom(), atom()}, history_size = 20 :: non_neg_integer(), + max_rooms_discoitems = 100 :: non_neg_integer(), default_room_opts = [] :: list(), room_shaper = none :: shaper:shaper()}). -define(PROCNAME, ejabberd_mod_muc). --define(MAX_ROOMS_DISCOITEMS, 100). - -type muc_room_opts() :: [{atom(), any()}]. -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass. @@ -154,7 +153,7 @@ forget_room(ServerHost, Host, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:forget_room(LServer, Host, Name). -process_iq_disco_items(Host, From, To, +process_iq_disco_items(Host, From, To, MaxRoomsDiscoItems, #iq{lang = Lang} = IQ) -> Rsm = jlib:rsm_decode(IQ), DiscoNode = fxml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el), @@ -162,7 +161,7 @@ process_iq_disco_items(Host, From, To, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], - children = iq_disco_items(Host, From, Lang, DiscoNode, Rsm)}]}, + children = iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, DiscoNode, Rsm)}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)). can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; @@ -200,6 +199,9 @@ init([Host, Opts]) -> HistorySize = gen_mod:get_opt(history_size, Opts, fun(I) when is_integer(I), I>=0 -> I end, 20), + MaxRoomsDiscoItems = gen_mod:get_opt(max_rooms_discoitems, Opts, + fun(I) when is_integer(I), I>=0 -> I end, + 100), DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts, fun(L) when is_list(L) -> L end, []), @@ -265,6 +267,7 @@ init([Host, Opts]) -> access = {Access, AccessCreate, AccessAdmin, AccessPersistent}, default_room_opts = DefRoomOpts, history_size = HistorySize, + max_rooms_discoitems = MaxRoomsDiscoItems, room_shaper = RoomShaper}}. handle_call(stop, _From, State) -> @@ -293,9 +296,10 @@ handle_info({route, From, To, Packet}, #state{host = Host, server_host = ServerHost, access = Access, default_room_opts = DefRoomOpts, history_size = HistorySize, + max_rooms_discoitems = MaxRoomsDiscoItems, room_shaper = RoomShaper} = State) -> case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) of + From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); _ -> @@ -326,12 +330,12 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- do_route(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) -> + From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) -> {AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access, case acl:match_rule(ServerHost, AccessRoute, From) of allow -> do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts); + From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems); _ -> #xmlel{attrs = Attrs} = Packet, Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), @@ -343,7 +347,7 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper, do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts) -> + From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) -> {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access, {Room, _, Nick} = jid:tolower(To), #xmlel{name = Name, attrs = Attrs} = Packet, @@ -374,7 +378,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, jlib:iq_to_xml(Res)); #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ -> spawn(?MODULE, process_iq_disco_items, - [Host, From, To, IQ]); + [Host, From, To, MaxRoomsDiscoItems, IQ]); #iq{type = get, xmlns = (?NS_REGISTER) = XMLNS, lang = Lang, sub_el = _SubEl} = IQ -> @@ -636,15 +640,15 @@ iq_disco_info(ServerHost, Lang) -> [] end. -iq_disco_items(Host, From, Lang, <<>>, none) -> +iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<>>, none) -> Rooms = get_vh_rooms(Host), - case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of + case erlang:length(Rooms) < MaxRoomsDiscoItems of true -> iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}); false -> - iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) + iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"nonemptyrooms">>, none) end; -iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) -> +iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"nonemptyrooms">>, none) -> XmlEmpty = #xmlel{name = <<"item">>, attrs = [{<<"jid">>, <<"conference.localhost">>}, @@ -653,9 +657,9 @@ iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) -> children = []}, Query = {get_disco_item, only_non_empty, From, Lang}, [XmlEmpty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)]; -iq_disco_items(Host, From, Lang, <<"emptyrooms">>, none) -> +iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"emptyrooms">>, none) -> iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang}); -iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) -> +iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, _DiscoNode, Rsm) -> {Rooms, RsmO} = get_vh_rooms(Host, Rsm), RsmOut = jlib:rsm_encode(RsmO), iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut. @@ -984,6 +988,8 @@ mod_opt_type(max_room_id) -> fun (infinity) -> infinity; (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(max_rooms_discoitems) -> + fun (I) when is_integer(I), I >= 0 -> I end; mod_opt_type(regexp_room_id) -> fun iolist_to_binary/1; mod_opt_type(max_room_name) -> @@ -1011,8 +1017,8 @@ mod_opt_type(user_presence_shaper) -> mod_opt_type(_) -> [access, access_admin, access_create, access_persistent, db_type, default_room_options, history_size, host, - max_room_desc, max_room_id, max_room_name, regexp_room_id, - max_user_conferences, max_users, + max_room_desc, max_room_id, max_room_name, + max_rooms_discoitems, max_user_conferences, max_users, max_users_admin_threshold, max_users_presence, min_message_interval, min_presence_interval, - room_shaper, user_message_shaper, user_presence_shaper]. + regexp_room_id, room_shaper, user_message_shaper, user_presence_shaper]. From 059a806bb061d4e89cb0ffc2a08dafd341b16ae7 Mon Sep 17 00:00:00 2001 From: Christophe Romain Date: Wed, 19 Oct 2016 13:57:19 +0200 Subject: [PATCH 169/179] Let mix be able to cope with configured deps --- mix.exs | 62 ++++++++++++++++++++++++++++++++++++++++++-------------- mix.lock | 13 +++--------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/mix.exs b/mix.exs index ee4b60fb2..c77f2abb4 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do def project do [app: :ejabberd, - version: "16.08.0", + version: "16.11.0", description: description, elixir: "~> 1.2", elixirc_paths: ["lib"], @@ -17,7 +17,7 @@ defmodule Ejabberd.Mixfile do deps: deps] end - defp description do + def description do """ Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform. """ @@ -28,9 +28,8 @@ defmodule Ejabberd.Mixfile do applications: [:ssl], included_applications: [:lager, :mnesia, :p1_utils, :cache_tab, :fast_tls, :stringprep, :fast_xml, - :stun, :fast_yaml, :ezlib, :iconv, - :esip, :jiffy, :p1_oauth2, :eredis, - :p1_mysql, :p1_pgsql, :sqlite3]] + :stun, :fast_yaml, :esip, :jiffy, :p1_oauth2] + ++ cond_apps] end defp erlc_options do @@ -51,22 +50,40 @@ defmodule Ejabberd.Mixfile do {:esip, "~> 1.0"}, {:jiffy, "~> 0.14.7"}, {:p1_oauth2, "~> 0.6.1"}, - {:p1_mysql, "~> 1.0"}, - {:p1_pgsql, "~> 1.1"}, - {:sqlite3, "~> 1.1"}, - {:ezlib, "~> 1.0"}, - {:iconv, "~> 1.0"}, - {:eredis, "~> 1.0"}, {:exrm, "~> 1.0.0", only: :dev}, # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with # version 3.20: {:relx, "~> 3.21", only: :dev}, - {:ex_doc, ">= 0.0.0", only: :dev}, - {:meck, "~> 0.8.4", only: :test}, - {:moka, github: "processone/moka", tag: "1.0.5c", only: :test}] + {:ex_doc, ">= 0.0.0", only: :dev}] + ++ cond_deps end - defp package do + defp cond_deps do + for {:true, dep} <- [{config(:mysql), {:p1_mysql, "~> 1.0"}}, + {config(:pgsql), {:p1_pgsql, "~> 1.1"}}, + {config(:sqlite), {:sqlite3, "~> 1.1"}}, + {config(:riak), {:riakc, "~> 2.4"}}, + {config(:redis), {:eredis, "~> 1.0"}}, + {config(:zlib), {:ezlib, "~> 1.0"}}, + {config(:iconv), {:iconv, "~> 1.0"}}, + {config(:pam), {:p1_pam, "~> 1.0"}}, + {config(:tools), {:luerl, github: "rvirding/luerl", tag: "v0.2"}}, + {config(:tools), {:meck, "~> 0.8.4"}}, + {config(:tools), {:moka, github: "processone/moka", tag: "1.0.5c"}}], do: + dep + end + + defp cond_apps do + for {:true, app} <- [{config(:redis), :eredis}, + {config(:mysql), :p1_mysql}, + {config(:pgsql), :p1_pgsql}, + {config(:sqlite), :sqlite3}, + {config(:zlib), :ezlib}, + {config(:iconv), :iconv}], do: + app + end + + def package do [# These are the default files included in the package files: ["lib", "src", "priv", "mix.exs", "include", "README.md", "COPYING"], maintainers: ["ProcessOne"], @@ -76,6 +93,21 @@ defmodule Ejabberd.Mixfile do "Source" => "https://github.com/processone/ejabberd", "ProcessOne" => "http://www.process-one.net/"}] end + + def vars do + case :file.consult("vars.config") do + {:ok,config} -> config + _ -> [zlib: true, iconv: true] + end + end + + defp config(key) do + case vars[key] do + nil -> false + value -> value + end + end + end defmodule Mix.Tasks.Compile.Asn1 do diff --git a/mix.lock b/mix.lock index fc2cdc924..e515fd346 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,10 @@ %{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []}, "cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []}, - "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, - "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, + "earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []}, "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]}, "esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]}, - "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, + "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]}, "ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []}, "fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, @@ -16,15 +15,9 @@ "iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []}, "lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]}, - "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, - "moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]}, - "p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []}, - "p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []}, "p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []}, "providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]}, - "relx": {:hex, :relx, "3.21.0", "91e1ea9f09b4edfda8461901f4b5c5e0226e43ec161e147eeab29f7761df6eb5", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, - "samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]}, - "sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []}, + "relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]}, "stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}, "stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}} From 0212559ca7c3bdf922d06504c3986cbaa06a2671 Mon Sep 17 00:00:00 2001 From: Badlop Date: Thu, 20 Oct 2016 20:34:48 +0200 Subject: [PATCH 170/179] If a participant can change subject, let asubscriber too (#1345) --- src/mod_muc_room.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 5bee4f0b7..6010e0bbf 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -959,6 +959,7 @@ process_groupchat_message(From, _ -> case can_change_subject(Role, + IsSubscriber, StateData) of true -> @@ -2821,10 +2822,10 @@ check_subject(Packet) -> SubjEl -> fxml:get_tag_cdata(SubjEl) end. -can_change_subject(Role, StateData) -> +can_change_subject(Role, IsSubscriber, StateData) -> case (StateData#state.config)#config.allow_change_subj of - true -> Role == moderator orelse Role == participant; + true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; _ -> Role == moderator end. From 2ab72bcd009536a6bef2be04c71470f17e19d0b2 Mon Sep 17 00:00:00 2001 From: Badlop Date: Thu, 20 Oct 2016 21:56:19 +0200 Subject: [PATCH 171/179] Nidx may be integer or binary, so use jlib:i2l instead As reported in https://www.ejabberd.im/forum/28580/erlang-function-integertobinary1-throwing-badargs-exception --- src/node_flat_sql.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 15cf9b37a..61156ee06 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -688,7 +688,7 @@ get_items(Nidx, _From, before -> {<<">">>, <<"asc">>}; _ -> {<<"is not">>, <<"desc">>} end, - SNidx = integer_to_binary(Nidx), + SNidx = jlib:i2l(Nidx), [AttrName, Id] = case I of undefined when IncIndex =/= undefined -> case catch @@ -790,7 +790,7 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM get_last_items(Nidx, _From, Count) -> Limit = jlib:i2l(Count), - SNidx = integer_to_binary(Nidx), + SNidx = jlib:i2l(Nidx), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( [<<"select top ">>, Limit, @@ -890,7 +890,7 @@ del_items(Nidx, [ItemId]) -> del_item(Nidx, ItemId); del_items(Nidx, ItemIds) -> I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>), - SNidx = integer_to_binary(Nidx), + SNidx = jlib:i2l(Nidx), catch ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, I, <<") and nodeid='">>, SNidx, <<"';">>]). From 509776a0d1fd98b3db80815a42ad3d853a68bdfd Mon Sep 17 00:00:00 2001 From: Prasad Vaidya Date: Fri, 21 Oct 2016 13:57:47 +0530 Subject: [PATCH 172/179] Fix: Replace erlang function with fail-safe jlib function. --- src/ejabberd_sql.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index b19f16414..5900b414b 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -629,7 +629,7 @@ generic_sql_query_format(SQLQuery) -> generic_escape() -> #sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end, - integer = fun(X) -> integer_to_binary(X) end, + integer = fun(X) -> jlib:i2l(X) end, boolean = fun(true) -> <<"1">>; (false) -> <<"0">> end @@ -646,7 +646,7 @@ sqlite_sql_query_format(SQLQuery) -> sqlite_escape() -> #sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end, - integer = fun(X) -> integer_to_binary(X) end, + integer = fun(X) -> jlib:i2l(X) end, boolean = fun(true) -> <<"1">>; (false) -> <<"0">> end @@ -664,13 +664,13 @@ mssql_sql_query(SQLQuery) -> pgsql_prepare(SQLQuery, State) -> Escape = #sql_escape{_ = fun(X) -> X end}, N = length((SQLQuery#sql_query.args)(Escape)), - Args = [<<$$, (integer_to_binary(I))/binary>> || I <- lists:seq(1, N)], + Args = [<<$$, (jlib:i2l(I))/binary>> || I <- lists:seq(1, N)], Query = (SQLQuery#sql_query.format_query)(Args), pgsql:prepare(State#state.db_ref, SQLQuery#sql_query.hash, Query). pgsql_execute_escape() -> #sql_escape{string = fun(X) -> X end, - integer = fun(X) -> [integer_to_binary(X)] end, + integer = fun(X) -> [jlib:i2l(X)] end, boolean = fun(true) -> "1"; (false) -> "0" end From cdafd3254b25856fb9aaa171f2f5c3bf1be52cf2 Mon Sep 17 00:00:00 2001 From: Prasad Vaidya Date: Fri, 21 Oct 2016 15:02:39 +0530 Subject: [PATCH 173/179] Rollback minor change, to avoid redundant use of fail-safe function --- src/ejabberd_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index 5900b414b..27c2815ba 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -664,7 +664,7 @@ mssql_sql_query(SQLQuery) -> pgsql_prepare(SQLQuery, State) -> Escape = #sql_escape{_ = fun(X) -> X end}, N = length((SQLQuery#sql_query.args)(Escape)), - Args = [<<$$, (jlib:i2l(I))/binary>> || I <- lists:seq(1, N)], + Args = [<<$$, (integer_to_binary(I))/binary>> || I <- lists:seq(1, N)], Query = (SQLQuery#sql_query.format_query)(Args), pgsql:prepare(State#state.db_ref, SQLQuery#sql_query.hash, Query). From 149cc9654f17c8edfbcd79a2d1973721a70eafe3 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 24 Oct 2016 13:42:33 +0200 Subject: [PATCH 174/179] Append ; to privacy_list_data exporting lines (thanks to Marcio Luciano Donada) --- src/mod_privacy_sql.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl index 6da917e9d..a700db391 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -238,7 +238,7 @@ export(Server) -> "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," " %(Order)d, %(MatchAll)b, %(MatchIQ)b," " %(MatchMessage)b, %(MatchPresenceIn)b," - " %(MatchPresenceOut)b)") + " %(MatchPresenceOut)b);") || {SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, From 62db0309421b9152361aa946c6b9319b8274edd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Fri, 4 Nov 2016 12:57:57 +0100 Subject: [PATCH 175/179] Merge mod_opt_type from db sub-modules to main module mod_opt_type --- src/ejabberd_config.erl | 33 +++++++++++++++++++++++---------- src/gen_mod.erl | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 517f2fd2f..af26767f8 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -29,16 +29,16 @@ -export([start/0, load_file/1, reload_file/0, read_file/1, add_global_option/2, add_local_option/2, get_global_option/2, get_local_option/2, - get_global_option/3, get_local_option/3, - get_option/2, get_option/3, add_option/2, has_option/1, - get_vh_by_auth_method/1, is_file_readable/1, - get_version/0, get_myhosts/0, get_mylang/0, - get_ejabberd_config_path/0, is_using_elixir_config/0, - prepare_opt_val/4, convert_table_to_binary/5, - transform_options/1, collect_options/1, default_db/2, - convert_to_yaml/1, convert_to_yaml/2, v_db/2, - env_binary_to_list/2, opt_type/1, may_hide_data/1, - is_elixir_enabled/0]). + get_global_option/3, get_local_option/3, + get_option/2, get_option/3, add_option/2, has_option/1, + get_vh_by_auth_method/1, is_file_readable/1, + get_version/0, get_myhosts/0, get_mylang/0, + get_ejabberd_config_path/0, is_using_elixir_config/0, + prepare_opt_val/4, convert_table_to_binary/5, + transform_options/1, collect_options/1, default_db/2, + convert_to_yaml/1, convert_to_yaml/2, v_db/2, + env_binary_to_list/2, opt_type/1, may_hide_data/1, + is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1]). -export([start/2]). @@ -893,6 +893,19 @@ v_db(Mod, Type) -> [] -> erlang:error(badarg) end. +-spec v_dbs(module()) -> [atom()]. + +v_dbs(Mod) -> + lists:flatten(ets:match(module_db, {Mod, '$1'})). + +-spec v_dbs_mods(module()) -> [module()]. + +v_dbs_mods(Mod) -> + lists:map(fun([M]) -> + binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_", + (atom_to_binary(M, utf8))/binary>>, utf8) + end, ets:match(module_db, {Mod, '$1'})). + -spec default_db(binary(), module()) -> atom(). default_db(Host, Module) -> diff --git a/src/gen_mod.erl b/src/gen_mod.erl index c4306577c..aaf452aeb 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -308,10 +308,47 @@ get_opt_host(Host, Opts, Default) -> Val = get_opt(host, Opts, fun iolist_to_binary/1, Default), ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). + +get_module_mod_opt_type_fun(Module) -> + DBSubMods = ejabberd_config:v_dbs_mods(Module), + fun(Opt) -> + Res = lists:foldl(fun(Mod, {Funs, ArgsList, _} = Acc) -> + case catch Mod:mod_opt_type(Opt) of + Fun when is_function(Fun) -> + {[Fun | Funs], ArgsList, true}; + L when is_list(L) -> + {Funs, L ++ ArgsList, true}; + _ -> + Acc + end + end, {[], [], false}, [Module | DBSubMods]), + case Res of + {[], [], false} -> + throw({'EXIT', {undef, mod_opt_type}}); + {[], Args, _} -> Args; + {Funs, _, _} -> + fun(Val) -> + lists:any(fun(F) -> + try F(Val) of + _ -> + true + catch {replace_with, _NewVal} = E -> + throw(E); + {invalid_syntax, _Error} = E2 -> + throw(E2); + _:_ -> + false + end + end, Funs) + end + end + end. + validate_opts(Module, Opts) -> + ModOptFun = get_module_mod_opt_type_fun(Module), lists:filtermap( fun({Opt, Val}) -> - case catch Module:mod_opt_type(Opt) of + case catch ModOptFun(Opt) of VFun when is_function(VFun) -> try VFun(Val) of _ -> From bab71f083232aa725925ddb66af371a6852da4a3 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 4 Nov 2016 16:35:59 +0100 Subject: [PATCH 176/179] Replace ctlscriptpath and produce ejabberd.service file (#434) --- Makefile.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile.in b/Makefile.in index eb1474926..18c611e96 100644 --- a/Makefile.in +++ b/Makefile.in @@ -207,6 +207,11 @@ install: all copy-files > ejabberd.init chmod 755 ejabberd.init # + # Service script + $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*" ejabberd.service.template \ + > ejabberd.service + chmod 755 ejabberd.service + # # Spool directory $(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR) $(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT) From 5e723bc90e9701c7112d741558bfd8b45ea637b0 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 4 Nov 2016 16:54:31 +0100 Subject: [PATCH 177/179] Fix reading room jids from file for create and destroy_rooms_file commands --- src/mod_muc_admin.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index e334dca2b..c9da1c66c 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -514,7 +514,7 @@ destroy_room({N, H, SH}) -> %% The file encoding must be UTF-8 destroy_rooms_file(Filename) -> - {ok, F} = file:open(Filename, [read, binary]), + {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), @@ -533,7 +533,7 @@ read_room(F) -> eof -> eof; String -> case io_lib:fread("~s", String) of - {ok, [RoomJID], _} -> split_roomjid(RoomJID); + {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID)); {error, What} -> io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) end @@ -551,7 +551,7 @@ split_roomjid(RoomJID) -> %%---------------------------- create_rooms_file(Filename) -> - {ok, F} = file:open(Filename, [read, binary]), + {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), From 49d3b7ec1de954cae7fb87cfd74267dfb9dcdf51 Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 4 Nov 2016 17:28:28 +0100 Subject: [PATCH 178/179] Throw error if room name or host has invalid characters (#1360) --- src/mod_muc_admin.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index c9da1c66c..bd1c55f66 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -432,8 +432,8 @@ create_room(Name1, Host1, ServerHost) -> create_room_with_opts(Name1, Host1, ServerHost, []). create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) -> - Name = jid:nodeprep(Name1), - Host = jid:nodeprep(Host1), + true = (error /= (Name = jid:nodeprep(Name1))), + true = (error /= (Host = jid:nodeprep(Host1))), %% Get the default room options from the muc configuration DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc, From bd060bc1bb0b5078ddc00189556256418cac1a6f Mon Sep 17 00:00:00 2001 From: Badlop Date: Fri, 4 Nov 2016 18:44:15 +0100 Subject: [PATCH 179/179] Support several groups separated by ; in add_rosteritem command --- src/mod_admin_extra.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 8ce11b872..48732ea35 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -377,6 +377,7 @@ get_commands_spec() -> #ejabberd_commands{name = add_rosteritem, tags = [roster], desc = "Add an item to a user's roster (supports ODBC)", + longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"", module = ?MODULE, function = add_rosteritem, args = [{localuser, binary}, {localserver, binary}, {user, binary}, {server, binary}, @@ -1204,11 +1205,13 @@ push_roster_item(LU, LS, R, U, S, Action) -> ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ). build_roster_item(U, S, {add, Nick, Subs, Group}) -> + GNames = binary:split(Group,<<";">>, [global]), + GroupEls = [{xmlel, <<"group">>, [], [{xmlcdata, GName}]} || GName <- GNames], {xmlel, <<"item">>, [{<<"jid">>, jid:to_string(jid:make(U, S, <<>>))}, {<<"name">>, Nick}, {<<"subscription">>, Subs}], - [{xmlel, <<"group">>, [], [{xmlcdata, Group}]}] + GroupEls }; build_roster_item(U, S, remove) -> {xmlel, <<"item">>,