diff options
90 files changed, 2120 insertions, 1042 deletions
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 81be06dc3..c5c34b743 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -26,6 +26,25 @@ {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. +-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', @@ -36,19 +55,25 @@ function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, + %% 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() | '_', 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 :: [{atom(),atom(),atom()}|atom()], result :: rterm()}. %% @type ejabberd_commands() = #ejabberd_commands{ 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/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/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 4d82856ca..d985f3f3b 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -53,6 +53,7 @@ members_by_default = true :: boolean(), members_only = false :: boolean(), allow_user_invites = false :: boolean(), + allow_subscription = false :: boolean(), password_protected = false :: boolean(), password = <<"">> :: binary(), anonymous = true :: boolean(), @@ -76,6 +77,8 @@ jid :: jid(), nick :: binary(), role :: role(), + is_subscriber = false :: boolean(), + subscriptions = [] :: [binary()], last_presence :: xmlel() }). diff --git a/include/ns.hrl b/include/ns.hrl index c7f556372..a150746e7 100644 --- a/include/ns.hrl +++ b/include/ns.hrl @@ -164,3 +164,11 @@ -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_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">>). +-define(NS_MUCSUB_NODES_PARTICIPANTS, <<"urn:xmpp:mucsub:nodes:participants">>). +-define(NS_MUCSUB_NODES_AFFILIATIONS, <<"urn:xmpp:mucsub:nodes:affiliations">>). +-define(NS_MUCSUB_NODES_SUBJECT, <<"urn:xmpp:mucsub:nodes:subject">>). +-define(NS_MUCSUB_NODES_CONFIG, <<"urn:xmpp:mucsub:nodes:config">>). +-define(NS_MUCSUB_NODES_SYSTEM, <<"urn:xmpp:mucsub:nodes:system">>). 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 @@ -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 @@ -56,7 +58,12 @@ 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}, + {:meck, "~> 0.8.4", only: :test}, + {:moka, github: "processone/moka", tag: "1.0.5b", only: :test}] end defp package do @@ -2,25 +2,28 @@ "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], [{: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.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]}]}, + "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.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], [{: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.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], [{: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 a07498be6..693f4f58b 100644 --- a/rebar.config +++ b/rebar.config @@ -10,12 +10,12 @@ {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.5"}}}, - {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.4"}}}, + {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.5"}}}, - {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.6"}}}, - {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.4"}}}, + {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"}}}, {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"}}}, @@ -39,12 +39,12 @@ "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, {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"}}}, {if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv", - {tag, "1.0.0"}}}}, + {tag, "1.0.1"}}}}, {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", diff --git a/rebar.config.script b/rebar.config.script index 166f1cbec..ccafba7ec 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,17 +47,31 @@ 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; + (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 -> 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 +80,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; @@ -146,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. diff --git a/src/acl.erl b/src/acl.erl index 31a7547dd..897996976 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -31,10 +31,11 @@ -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, + parse_ip_netmask/1, access_rules_validator/1, shaper_rules_validator/1]). -include("ejabberd.hrl"). @@ -274,6 +275,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_admin.erl b/src/ejabberd_admin.erl index 87ac76875..f20aeebf0 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 = [{mod_register, access, 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_c2s.erl b/src/ejabberd_c2s.erl index f431f6890..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; @@ -2873,8 +2880,8 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F) 0 -> ok; N -> - ?INFO_MSG("~B stanzas were not acknowledged by ~s", - [N, jid:to_string(StateData#state.jid)]), + ?DEBUG("~B stanza(s) were not acknowledged by ~s", + [N, jid:to_string(StateData#state.jid)]), lists:foreach( fun({_, Time, #xmlel{attrs = Attrs} = El}) -> From_s = fxml:get_attr_s(<<"from">>, Attrs), @@ -2951,6 +2958,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) diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 9d41f50c2..33edcb7c7 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -218,14 +218,15 @@ 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, 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. @@ -319,8 +341,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 +353,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,27 +378,33 @@ 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(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. @@ -388,16 +416,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()]. @@ -405,20 +433,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 @@ -427,7 +455,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). @@ -488,41 +516,62 @@ 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, #{}). + execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}). execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) -> Auth = case is_admin(Name, Auth1, CallerInfo) of true -> admin; false -> Auth1 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_command2(Auth, Command, Arguments) + ok -> execute_check_policy(Auth, TokenJID, Command, Arguments) + end. + + +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 = []} = Command, Arguments) -> + do_execute_command(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 ? + 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 -> + throw({error, access_rules_unauthorized}) 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) -> +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]), @@ -592,31 +641,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; @@ -627,8 +676,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 -> @@ -680,9 +729,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) -> @@ -705,19 +754,23 @@ 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), + 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, - []), + []), Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0), CommandsList = list_commands_policy(Version), OpenCmds = [N || {N, _, _, open} <- CommandsList], @@ -727,27 +780,32 @@ 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. +%% 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(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; +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/ejabberd_config.erl b/src/ejabberd_config.erl index df9debd62..87a918704 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -220,9 +220,6 @@ env_binary_to_list(Application, Parameter) -> %% in which the options 'include_config_file' were parsed %% and the terms in those files were included. %% @spec(iolist()) -> [term()] -get_plain_terms_file(File) -> - get_plain_terms_file(File, [{include_files, true}]). - get_plain_terms_file(File, Opts) when is_binary(File) -> get_plain_terms_file(binary_to_list(File), Opts); get_plain_terms_file(File1, Opts) -> 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), diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 57c1baab2..0af158562 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/1, 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"). @@ -57,6 +56,7 @@ -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). +-include("ejabberd_oauth.hrl"). -include("ejabberd_commands.hrl"). @@ -65,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()} | server_admin, - scope = [] :: [binary()], - expire :: integer() - }). --define(EXPIRE, 3600). +-define(EXPIRE, 31536000). 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), @@ -90,57 +85,61 @@ 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 = [{scopes, string}], + args = [{jid, string},{ttl, integer}, {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", + 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", 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" } ]. -oauth_issue_token(ScopesString) -> +oauth_issue_token(Jid, TTLSeconds, 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, [{expiry_time, TTLSeconds}]), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, VerifiedScope} = oauth2_response:scope(Response), + {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " 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) -> @@ -148,8 +147,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())]. @@ -170,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}; @@ -189,21 +180,11 @@ 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}. -authenticate_user({User, Server}, {password, Password} = Ctx) -> +authenticate_user({User, Server}, Ctx) -> case jid:make(User, Server, <<"">>) of #jid{} = JID -> Access = @@ -213,11 +194,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} @@ -229,8 +215,8 @@ authenticate_user({User, Server}, {password, Password} = Ctx) -> 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], + 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), oauth2_priv_set:new(RegisteredScope)) of @@ -244,27 +230,35 @@ 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_exposed_commands()), + ScopeMap. %% 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(), - 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) -> @@ -272,58 +266,63 @@ 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, <<"">>), + Expire = case proplists:get_value(expiry_time, AppContext, undefined) of + undefined -> + proplists:get_value(<<"expiry_time">>, Context, 0); + 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. + seconds_since_epoch(ExpiresIn) + 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 }, - mnesia:dirty_write(R), + DBMod = get_db_backend(), + DBMod:store(R), {ok, AppContext}. associate_refresh_token(_RefreshToken, _Context, AppContext) -> %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), {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 - [#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, - 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) -> - case catch mnesia:dirty_read(oauth_token, Token) of - [#oauth_token{us = US, - scope = TokenScope, - expire = Expire}] -> +check_token(ScopeList, Token) -> + 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, - 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; + 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; _ -> @@ -358,12 +357,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), @@ -372,6 +368,15 @@ process(_Handlers, ?INPUT(<<"hidden">>, <<"scope">>, Scope), ?INPUT(<<"hidden">>, <<"state">>, State), ?BR, + ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]), + ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], + [ + ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), + ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), + ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), + ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>), + ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), + ?BR, ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) ]), Top = @@ -415,11 +420,16 @@ 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, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> jlib:binary_to_integer(TTL) + end, case oauth2:authorize_password({Username, Server}, ClientId, RedirectURI, @@ -427,10 +437,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, @@ -459,11 +477,82 @@ 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_error(400, <<"invalid_grant">>, Error) + end; + _OtherGrantType -> + json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type) + end; + 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) -> + {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, + {<<"Cache-Control">>, <<"no-store">>}, + {<<"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">>}, @@ -577,7 +666,7 @@ css() -> text-decoration: underline; } - .container > .section { + .container > .section { background: #424A55; } @@ -595,4 +684,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 <alexey@process-one.net> +%%% Purpose : OAUTH2 mnesia backend +%%% Created : 20 Jul 2016 by Alexey Shchepin <alexey@process-one.net> +%%% +%%% +%%% 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). + diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 465fb587a..9d72b17b4 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), @@ -382,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 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/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}; diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 3281f6430..6583fb445 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -74,20 +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]}. - -is_acl_match(Host, Rules, Jid) -> - lists:any(fun (Rule) -> - allow == acl:match_rule(Host, Rule, Jid) - end, - Rules). + 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]}. %%%================================== %%%% Menu Items Access @@ -138,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()] @@ -266,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}} @@ -1333,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, @@ -2965,7 +2972,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=%%%%,%%%=: diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index 2d19100a9..7bace05dd 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -186,7 +186,7 @@ delete(LServer, Table, ConvertFun) -> mnesia:write_lock_table(Table), {_N, SQLs} = mnesia:foldl( - fun(R, {N, SQLs} = Acc) -> + fun(R, Acc) -> case ConvertFun(LServer, R) of [] -> Acc; diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 1cc65ac21..c4306577c 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -53,6 +53,7 @@ -callback start(binary(), opts()) -> any(). -callback stop(binary()) -> any(). -callback mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. +-callback depends(binary(), opts()) -> [{module(), hard | soft}]. -export_type([opts/0]). -export_type([db_type/0]). @@ -77,18 +78,56 @@ start_modules() -> get_modules_options(Host) -> ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( + {modules, Host}, + fun(Mods) -> + lists:map( fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} + {M, A} end, Mods) - end, []). + end, []). + +sort_modules(Host, ModOpts) -> + G = digraph:new([acyclic]), + lists:foreach( + fun({Mod, Opts}) -> + digraph:add_vertex(G, Mod, Opts), + Deps = try Mod:depends(Host, Opts) catch _:undef -> [] end, + lists:foreach( + fun({DepMod, Type}) -> + case lists:keyfind(DepMod, 1, ModOpts) of + false when Type == hard -> + ErrTxt = io_lib:format( + "failed to load module '~s' " + "because it depends on module '~s' " + "which is not found in the config", + [Mod, DepMod]), + ?ERROR_MSG(ErrTxt, []), + digraph:del_vertex(G, Mod), + maybe_halt_ejabberd(ErrTxt); + false when Type == soft -> + ?WARNING_MSG("module '~s' is recommended for " + "module '~s' but is not found in " + "the config", + [DepMod, Mod]); + {DepMod, DepOpts} -> + digraph:add_vertex(G, DepMod, DepOpts), + case digraph:add_edge(G, DepMod, Mod) of + {error, {bad_edge, Path}} -> + ?WARNING_MSG("cyclic dependency detected " + "between modules: ~p", + [Path]); + _ -> + ok + end + end + end, Deps) + end, ModOpts), + [digraph:vertex(G, V) || V <- digraph_utils:topsort(G)]. -spec start_modules(binary()) -> any(). start_modules(Host) -> - Modules = get_modules_options(Host), + Modules = sort_modules(Host, get_modules_options(Host)), lists:foreach( fun({Module, Opts}) -> start_module(Host, Module, Opts) @@ -121,16 +160,20 @@ start_module(Host, Module, Opts0) -> [Module, Host, Opts, Class, Reason, erlang:get_stacktrace()]), ?CRITICAL_MSG(ErrorText, []), - case is_app_running(ejabberd) of - true -> - erlang:raise(Class, Reason, erlang:get_stacktrace()); - false -> - ?CRITICAL_MSG("ejabberd initialization was aborted " - "because a module start failed.", - []), - timer:sleep(3000), - erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199)) - end + maybe_halt_ejabberd(ErrorText), + erlang:raise(Class, Reason, erlang:get_stacktrace()) + end. + +maybe_halt_ejabberd(ErrorText) -> + case is_app_running(ejabberd) of + false -> + ?CRITICAL_MSG("ejabberd initialization was aborted " + "because a module start failed.", + []), + timer:sleep(3000), + erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199)); + true -> + ok end. is_app_running(AppName) -> diff --git a/src/jid.erl b/src/jid.erl index 0c3ac77c0..a730bd949 100644 --- a/src/jid.erl +++ b/src/jid.erl @@ -50,11 +50,35 @@ -spec start() -> ok. start() -> + {ok, Owner} = ets_owner(), 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. +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() -> + receive + _ -> + ets_keepalive() + end. + -spec make(binary(), binary(), binary()) -> jid() | error. make(User, Server, Resource) -> 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 = <<OldDesc/binary, ", ", NewDesc/binary>>, - 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. diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index 9e0682f7d..e12e0de1e 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -35,7 +35,7 @@ process_sm_iq/3, get_local_commands/5, get_local_identity/5, get_local_features/5, get_sm_commands/5, get_sm_identity/5, get_sm_features/5, - ping_item/4, ping_command/4, mod_opt_type/1]). + ping_item/4, ping_command/4, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -284,6 +284,9 @@ ping_command(_Acc, _From, _To, end; ping_command(Acc, _From, _To, _Request) -> Acc. +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(report_commands_node) -> fun (B) when is_boolean(B) -> B end; diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 562087d96..2ad1cc28e 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -47,7 +47,7 @@ srg_delete/2, srg_list/1, srg_get_info/2, srg_get_members/2, srg_user_add/4, srg_user_del/4, send_message/5, send_stanza/3, send_stanza_c2s/4, privacy_set/3, - stats/1, stats/2, mod_opt_type/1, get_commands_spec/0]). + stats/1, stats/2, mod_opt_type/1, get_commands_spec/0, depends/2]). -include("ejabberd.hrl"). @@ -66,6 +66,8 @@ start(_Host, _Opts) -> stop(_Host) -> ejabberd_commands:unregister_commands(get_commands_spec()). +depends(_Host, _Opts) -> + []. %%% %%% Register commands @@ -861,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) -> @@ -901,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_announce.erl b/src/mod_announce.erl index fc57d6bd1..52ff2de92 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -33,7 +33,7 @@ -export([start/2, init/0, stop/1, export/1, import/1, import/3, announce/3, send_motd/1, disco_identity/5, - disco_features/5, disco_items/5, + disco_features/5, disco_items/5, depends/2, send_announcement_to_all/3, announce_commands/4, announce_items/4, mod_opt_type/1]). @@ -74,6 +74,9 @@ start(Host, Opts) -> register(gen_mod:get_module_proc(Host, ?PROCNAME), proc_lib:spawn(?MODULE, init, [])). +depends(_Host, _Opts) -> + [{mod_adhoc, hard}]. + init() -> loop(). diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index af06e650d..818d53259 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -30,7 +30,7 @@ -protocol({xep, 191, '1.2'}). -export([start/2, stop/1, process_iq/3, - process_iq_set/4, process_iq_get/5, mod_opt_type/1]). + process_iq_set/4, process_iq_get/5, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -63,6 +63,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_BLOCKING). +depends(_Host, _Opts) -> + [{mod_privacy, hard}]. + process_iq(_From, _To, IQ) -> SubEl = IQ#iq.sub_el, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 966a9baa6..3ed3149bb 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -41,7 +41,7 @@ import_start/2, import_stop/2]). %% gen_mod callbacks --export([start/2, start_link/2, stop/1]). +-export([start/2, start_link/2, stop/1, depends/2]). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, @@ -306,6 +306,9 @@ c2s_broadcast_recipients(InAcc, Host, C2SState, end; c2s_broadcast_recipients(Acc, _, _, _, _, _) -> Acc. +depends(_Host, _Opts) -> + []. + init([Host, Opts]) -> Mod = gen_mod:db_mod(Host, Opts, ?MODULE), Mod:init(Host, Opts), diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index bb20bd2f9..de8d8e1a7 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -37,7 +37,7 @@ -export([user_send_packet/4, user_receive_packet/5, iq_handler2/3, iq_handler1/3, remove_connection/4, - is_carbon_copy/1, mod_opt_type/1]). + is_carbon_copy/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -278,6 +278,9 @@ list(User, Server) -> Mod = gen_mod:db_mod(Server, ?MODULE), Mod:list(User, Server). +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index fe66f34e2..036175cce 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -31,7 +31,7 @@ -behavior(gen_mod). %% gen_mod callbacks. --export([start/2, stop/1, mod_opt_type/1]). +-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, @@ -142,6 +142,11 @@ mod_opt_type(queue_pep) -> fun(B) when is_boolean(B) -> B end; mod_opt_type(_) -> [queue_presence, queue_chat_states, queue_pep]. +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. + +depends(_Host, _Opts) -> + []. + %%-------------------------------------------------------------------- %% ejabberd_hooks callbacks. %%-------------------------------------------------------------------- diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 9d4086559..d0e0166a4 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -35,7 +35,8 @@ get_local_features/5, get_local_items/5, adhoc_local_items/4, adhoc_local_commands/4, get_sm_identity/5, get_sm_features/5, get_sm_items/5, - adhoc_sm_items/4, adhoc_sm_commands/4, mod_opt_type/1]). + adhoc_sm_items/4, adhoc_sm_commands/4, mod_opt_type/1, + depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -95,6 +96,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_COMMANDS). +depends(_Host, _Opts) -> + [{mod_adhoc, hard}, {mod_last, soft}]. + %%%----------------------------------------------------------------------- -define(INFO_IDENTITY(Category, Type, Name, Lang), @@ -1913,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, diff --git a/src/mod_configure2.erl b/src/mod_configure2.erl index a8287b4d4..85b7740d0 100644 --- a/src/mod_configure2.erl +++ b/src/mod_configure2.erl @@ -32,7 +32,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - mod_opt_type/1, opt_type/1]). + mod_opt_type/1, opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -201,6 +201,9 @@ process_get(#xmlel{name = <<"last">>, attrs = Attrs}, Lang) -> %% {result, }; process_get(_, _) -> {error, ?ERR_BAD_REQUEST}. +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 0d5abcb4b..2e7b80c18 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -39,7 +39,7 @@ get_sm_identity/5, get_sm_features/5, get_sm_items/5, get_info/5, register_feature/2, unregister_feature/2, register_extra_domain/2, unregister_extra_domain/2, - transform_module_options/1, mod_opt_type/1]). + transform_module_options/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -534,6 +534,9 @@ values_to_xml(Values) -> end, Values). +depends(_Host, _Opts) -> + []. + mod_opt_type(extra_domains) -> fun (Hs) -> [iolist_to_binary(H) || H <- Hs] end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; diff --git a/src/mod_echo.erl b/src/mod_echo.erl index 7d9f81f82..ee904d798 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -37,7 +37,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -200,5 +200,8 @@ do_client_version(enabled, From, To) -> ?INFO_MSG("Information of the client: ~s~s", [ToS, Values_string2]). +depends(_Host, _Opts) -> + []. + mod_opt_type(host) -> fun iolist_to_binary/1; mod_opt_type(_) -> [host]. diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index f0460e704..c57ac21b0 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -33,7 +33,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include_lib("stdlib/include/ms_transform.hrl"). -include("ejabberd.hrl"). @@ -120,6 +120,9 @@ stop(Host) -> supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). +depends(_Host, _Opts) -> + []. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 595c121cd..ba3a14cf8 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -74,7 +74,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, process/2, mod_opt_type/1]). +-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("jlib.hrl"). @@ -123,6 +123,9 @@ start(_Host, _Opts) -> stop(_Host) -> ok. +depends(_Host, _Opts) -> + []. + %% ---------- %% basic auth %% ---------- @@ -130,13 +133,13 @@ stop(_Host) -> 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() + json_error(404, 40, <<"Endpoint not found.">>) 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 @@ -156,11 +159,9 @@ 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}}; - {ok, server_admin} -> %% token whas generated using issue_token command line - {ok, admin}; false -> false end; @@ -171,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), @@ -193,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 @@ -221,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 @@ -265,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); @@ -279,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 @@ -310,8 +315,10 @@ 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:{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) -> @@ -367,28 +374,33 @@ format_args(Args, ArgsFormat) -> L when is_list(L) -> exit({additional_unused_args, L}) end. -format_arg({array, 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}}) +format_arg(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]; @@ -402,7 +414,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) -> @@ -486,9 +498,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">>). @@ -498,6 +508,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]); diff --git a/src/mod_http_bind.erl b/src/mod_http_bind.erl index 1a07867e0..9a3a379f7 100644 --- a/src/mod_http_bind.erl +++ b/src/mod_http_bind.erl @@ -37,7 +37,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, process/2, mod_opt_type/1]). +-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -109,6 +109,8 @@ mod_opt_type(max_pause) -> fun (I) when is_integer(I), I > 0 -> I end; mod_opt_type(_) -> [max_inactivity, max_pause]. +depends(_Host, _Opts) -> + []. %%%---------------------------------------------------------------------- %%% Help Web Page diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index 346dc41c8..37e02edd8 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -46,7 +46,7 @@ %% utility for other http modules -export([content_type/3]). --export([reopen_log/1, mod_opt_type/1]). +-export([reopen_log/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -109,6 +109,9 @@ stop(Host) -> supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). +depends(_Host, _Opts) -> + []. + %%==================================================================== %% API %%==================================================================== diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index a2aa75465..b166f2b66 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -68,6 +68,7 @@ -export([start_link/3, start/2, stop/1, + depends/2, mod_opt_type/1]). %% gen_server callbacks. @@ -222,6 +223,11 @@ mod_opt_type(_) -> dir_mode, docroot, put_url, get_url, service_url, custom_headers, rm_on_unregister, thumbnail]. +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. + +depends(_Host, _Opts) -> + []. + %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index e08cd0b91..3051a4e87 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -39,6 +39,7 @@ -export([start_link/3, start/2, stop/1, + depends/2, mod_opt_type/1]). %% gen_server callbacks. @@ -109,6 +110,11 @@ mod_opt_type(max_days) -> mod_opt_type(_) -> [access_soft_quota, access_hard_quota, max_days]. +-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. + +depends(_Host, _Opts) -> + [{mod_http_upload, hard}]. + %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- @@ -245,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()}. @@ -293,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 -> @@ -308,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. diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl index a6f9f619d..4f54ecd79 100644 --- a/src/mod_ip_blacklist.erl +++ b/src/mod_ip_blacklist.erl @@ -36,7 +36,7 @@ -export([update_bl_c2s/0]). --export([is_ip_in_c2s_blacklist/3, mod_opt_type/1]). +-export([is_ip_in_c2s_blacklist/3, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -65,6 +65,9 @@ preinit(Parent, State) -> error:_ -> Parent ! {ok, Pid, true} end. +depends(_Host, _Opts) -> + []. + %% TODO: stop(_Host) -> ok. diff --git a/src/mod_irc.erl b/src/mod_irc.erl index 4bcf69c3b..2206028b7 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -38,7 +38,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -99,6 +99,9 @@ stop(Host) -> gen_server:call(Proc, stop), supervisor:delete_child(ejabberd_sup, Proc). +depends(_Host, _Opts) -> + []. + %%==================================================================== %% gen_server callbacks %%==================================================================== diff --git a/src/mod_last.erl b/src/mod_last.erl index 92694cf13..ce9148841 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -37,7 +37,7 @@ process_sm_iq/3, on_presence_update/4, import/1, import/3, store_last_info/4, get_last_info/2, remove_user/2, transform_options/1, mod_opt_type/1, - opt_type/1, register_user/2]). + opt_type/1, register_user/2, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -255,6 +255,9 @@ transform_options({node_start, {_, _, _} = Now}, Opts) -> transform_options(Opt, Opts) -> [Opt|Opts]. +depends(_Host, _Opts) -> + []. + mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 18bffd909..7e1460695 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -31,13 +31,13 @@ -behaviour(gen_mod). %% API --export([start/2, stop/1]). +-export([start/2, stop/1, depends/2]). -export([user_send_packet/4, user_send_packet_strip_tag/4, user_receive_packet/5, process_iq_v0_2/3, process_iq_v0_3/3, disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1, muc_process_iq/4, muc_filter_message/5, message_is_archived/5, delete_old_messages/2, - get_commands_spec/0, msg_to_el/4]). + get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/4]). -include("jlib.hrl"). -include("logger.hrl"). @@ -102,6 +102,12 @@ start(Host, Opts) -> disco_sm_features, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + ejabberd_hooks:add(remove_room, Host, ?MODULE, + remove_room, 50), + ejabberd_hooks:add(get_room_config, Host, ?MODULE, + get_room_config, 50), + ejabberd_hooks:add(set_room_option, Host, ?MODULE, + set_room_option, 50), ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE, remove_user, 50), case gen_mod:get_opt(assume_mam_usage, Opts, @@ -149,6 +155,12 @@ stop(Host) -> disco_sm_features, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + ejabberd_hooks:delete(remove_room, Host, ?MODULE, + remove_room, 50), + ejabberd_hooks:delete(get_room_config, Host, ?MODULE, + get_room_config, 50), + ejabberd_hooks:delete(set_room_option, Host, ?MODULE, + set_room_option, 50), ejabberd_hooks:delete(anonymous_purge_hook, Host, ?MODULE, remove_user, 50), case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage, @@ -165,6 +177,9 @@ stop(Host) -> ejabberd_commands:unregister_commands(get_commands_spec()), ok. +depends(_Host, _Opts) -> + []. + remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), @@ -177,6 +192,41 @@ remove_room(LServer, Name, Host) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_room(LServer, LName, LHost). +get_room_config(X, RoomState, _From, Lang) -> + Config = RoomState#state.config, + Label = <<"Enable message archiving">>, + Var = <<"muc#roomconfig_mam">>, + Val = case Config#config.mam of + true -> <<"1">>; + _ -> <<"0">> + end, + XField = #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"boolean">>}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}, + X ++ [XField]. + +set_room_option(_Acc, <<"muc#roomconfig_mam">> = Opt, Vals, Lang) -> + try + Val = case Vals of + [<<"0">>|_] -> false; + [<<"false">>|_] -> false; + [<<"1">>|_] -> true; + [<<"true">>|_] -> true + end, + {#config.mam, Val} + catch _:{case_clause, _} -> + Txt = <<"Value of '~s' should be boolean">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} + end; +set_room_option(Acc, _Opt, _Vals, _Lang) -> + Acc. + user_receive_packet(Pkt, C2SState, JID, Peer, To) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, @@ -982,6 +1032,8 @@ 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 -> diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index bbbe543f5..20ed8d4f1 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -295,25 +295,3 @@ make_sql_query(User, LServer, Start, End, With, RSM) -> {QueryPage, [<<"SELECT COUNT(*) FROM archive WHERE username='">>, SUser, <<"'">>, WithClause, StartClause, EndClause, <<";">>]}. - -update(LServer, Table, Fields, Vals, Where) -> - UPairs = lists:zipwith(fun (A, B) -> - <<A/binary, "='", B/binary, "'">> - end, - Fields, Vals), - case ejabberd_sql:sql_query(LServer, - [<<"update ">>, Table, <<" set ">>, - join(UPairs, <<", ">>), <<" where ">>, Where, - <<";">>]) - of - {updated, 1} -> {updated, 1}; - _ -> - ejabberd_sql:sql_query(LServer, - [<<"insert into ">>, Table, <<"(">>, - join(Fields, <<", ">>), <<") values ('">>, - join(Vals, <<"', '">>), <<"');">>]) - end. - -%% Almost a copy of string:join/2. -join([], _Sep) -> []; -join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl index 40484e488..f1d487e0e 100644 --- a/src/mod_metrics.erl +++ b/src/mod_metrics.erl @@ -39,7 +39,8 @@ s2s_send_packet, s2s_receive_packet, remove_user, register_user]). --export([start/2, stop/1, send_metrics/4, opt_type/1, mod_opt_type/1]). +-export([start/2, stop/1, send_metrics/4, opt_type/1, mod_opt_type/1, + depends/2]). -export([offline_message_hook/3, sm_register_connection_hook/3, sm_remove_connection_hook/3, @@ -59,6 +60,9 @@ stop(Host) -> [ejabberd_hooks:delete(Hook, Host, ?MODULE, Hook, 20) || Hook <- ?HOOKS]. +depends(_Host, _Opts) -> + []. + %%==================================================================== %% Hooks handlers %%==================================================================== diff --git a/src/mod_mix.erl b/src/mod_mix.erl index c0835b74e..b373ad13d 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -14,7 +14,7 @@ %% API -export([start_link/2, start/2, stop/1, process_iq/3, disco_items/5, disco_identity/5, disco_info/5, - disco_features/5, mod_opt_type/1]). + disco_features/5, mod_opt_type/1, depends/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -343,6 +343,9 @@ is_not_subscribed({error, ErrEl}) -> _ -> false end. +depends(_Host, _Opts) -> + [{mod_pubsub, hard}]. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(host) -> fun iolist_to_binary/1; mod_opt_type(_) -> [host, iqdisc]. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index b46585066..4c5b45ff5 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -53,7 +53,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -105,6 +105,9 @@ stop(Host) -> supervisor:delete_child(ejabberd_sup, Proc), {wait, Rooms}. +depends(_Host, _Opts) -> + [{mod_mam, soft}]. + shutdown_rooms(Host) -> MyHost = gen_mod:get_module_opt_host(Host, mod_muc, <<"conference.@HOST@">>), @@ -147,18 +150,10 @@ restore_room(ServerHost, Host, Name) -> forget_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), - remove_room_mam(LServer, Host, Name), + ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:forget_room(LServer, Host, Name). -remove_room_mam(LServer, Host, Name) -> - case gen_mod:is_loaded(LServer, mod_mam) of - true -> - mod_mam:remove_room(LServer, Name, Host); - false -> - ok - end. - process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> Rsm = jlib:rsm_decode(IQ), @@ -230,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 -> @@ -426,6 +422,18 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, iq_get_vcard(Lang)}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); + #iq{type = get, xmlns = ?NS_MUCSUB, + sub_el = #xmlel{name = <<"subscriptions">>} = SubEl} = IQ -> + RoomJIDs = get_subscribed_rooms(ServerHost, Host, From), + Subs = lists:map( + fun(J) -> + #xmlel{name = <<"subscription">>, + attrs = [{<<"jid">>, + jid:to_string(J)}]} + end, RoomJIDs), + Res = IQ#iq{type = result, + sub_el = [SubEl#xmlel{children = Subs}]}, + ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); #iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ -> Res = IQ#iq{type = result, sub_el = @@ -598,6 +606,8 @@ iq_disco_info(ServerHost, Lang) -> #xmlel{name = <<"feature">>, attrs = [{<<"var">>, ?NS_RSM}], children = []}, #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUCSUB}], children = []}, + #xmlel{name = <<"feature">>, attrs = [{<<"var">>, ?NS_VCARD}], children = []}] ++ case gen_mod:is_loaded(ServerHost, mod_mam) of true -> @@ -693,6 +703,19 @@ 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), + lists:flatmap( + fun(#muc_room{name_host = {Name, _}, opts = Opts}) -> + Subscribers = proplists:get_value(subscribers, Opts, []), + case lists:keymember(From, 1, Subscribers) of + true -> [jid:make(Name, Host, <<>>)]; + false -> [] + end; + (_) -> + [] + end, Rooms). + %% @doc Return the position of desired room in the list of rooms. %% The room must exist in the list. The count starts in 0. %% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer() diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 5fbda4f28..0b5e79f60 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -11,7 +11,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, muc_online_rooms/1, +-export([start/2, stop/1, depends/2, muc_online_rooms/1, muc_unregister_nick/1, create_room/3, destroy_room/2, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, @@ -49,6 +49,9 @@ stop(Host) -> ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50). +depends(_Host, _Opts) -> + [{mod_muc, hard}]. + %%% %%% Register commands %%% diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 167a96e37..ec4711b43 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -41,7 +41,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1, opt_type/1]). + mod_opt_type/1, opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -109,6 +109,9 @@ transform_module_options(Opts) -> Opt end, Opts). +depends(_Host, _Opts) -> + [{mod_muc, hard}]. + %%==================================================================== %% gen_server callbacks %%==================================================================== diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index eabff103d..773953c4a 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -113,13 +113,7 @@ init([Host, ServerHost, Access, Room, HistorySize, just_created = true, room_shaper = Shaper}), State1 = set_opts(DefRoomOpts, State), - if (State1#state.config)#config.persistent -> - mod_muc:store_room(State1#state.server_host, - State1#state.host, - State1#state.room, - make_opts(State1)); - true -> ok - end, + store_room(State1), ?INFO_MSG("Created MUC room ~s@~s by ~s", [Room, Host, jid:to_string(Creator)]), add_to_log(room_existence, created, State1), @@ -268,16 +262,7 @@ normal_state({route, From, <<"">>, StateData), send_affiliation(IJID, member, StateData), - case - (NSD#state.config)#config.persistent - of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> ok - end, + store_room(NSD), {next_state, normal_state, NSD}; _ -> {next_state, normal_state, StateData} end; @@ -427,6 +412,7 @@ normal_state({route, From, <<"">>, or (XMLNS == (?NS_DISCO_INFO)) or (XMLNS == (?NS_DISCO_ITEMS)) or (XMLNS == (?NS_VCARD)) + or (XMLNS == (?NS_MUCSUB)) or (XMLNS == (?NS_CAPTCHA)) -> Res1 = case XMLNS of ?NS_MUC_ADMIN -> @@ -444,6 +430,8 @@ normal_state({route, From, <<"">>, process_iq_disco_items(From, Type, Lang, StateData); ?NS_VCARD -> process_iq_vcard(From, Type, Lang, SubEl, StateData); + ?NS_MUCSUB -> + process_iq_mucsub(From, Packet, IQ, StateData); ?NS_CAPTCHA -> process_iq_captcha(From, Type, Lang, SubEl, StateData) end, @@ -458,12 +446,22 @@ normal_state({route, From, <<"">>, XMLNS}], children = Res}]}, SD}; + {ignore, SD} -> {ignore, SD}; + {error, Error, ResStateData} -> + {IQ#iq{type = error, + sub_el = [SubEl, Error]}, + ResStateData}; {error, Error} -> {IQ#iq{type = error, sub_el = [SubEl, Error]}, StateData} end, - ejabberd_router:route(StateData#state.jid, From, jlib:iq_to_xml(IQRes)), + if IQRes /= ignore -> + ejabberd_router:route( + StateData#state.jid, From, jlib:iq_to_xml(IQRes)); + true -> + ok + end, case NewStateData of stop -> {stop, normal, StateData}; _ -> {next_state, normal_state, NewStateData} @@ -678,11 +676,12 @@ handle_event({service_message, Msg}, _StateName, children = [#xmlel{name = <<"body">>, attrs = [], children = [{xmlcdata, Msg}]}]}, - send_multiple( + send_wrapped_multiple( StateData#state.jid, - StateData#state.server_host, StateData#state.users, - MessagePkt), + MessagePkt, + ?NS_MUCSUB_NODES_MESSAGES, + StateData), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; @@ -866,9 +865,11 @@ terminate(Reason, _StateName, StateData) -> Nick = Info#user.nick, case Reason of shutdown -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet); + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); _ -> ok end, tab_remove_online_user(LJID, StateData) @@ -894,14 +895,13 @@ process_groupchat_message(From, is_user_allowed_message_nonparticipant(From, StateData) of true -> - {FromNick, Role} = get_participant_data(From, - StateData), - if (Role == moderator) or (Role == participant) or + {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData), + if (Role == moderator) or (Role == participant) or IsSubscriber or ((StateData#state.config)#config.moderated == false) -> - {NewStateData1, IsAllowed} = case check_subject(Packet) - of + Subject = check_subject(Packet), + {NewStateData1, IsAllowed} = case Subject of false -> {StateData, true}; - Subject -> + _ -> case can_change_subject(Role, StateData) @@ -914,16 +914,7 @@ process_groupchat_message(From, subject_author = FromNick}, - case - (NSD#state.config)#config.persistent - of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> ok - end, + store_room(NSD), {NSD, true}; _ -> {StateData, false} end @@ -942,11 +933,13 @@ process_groupchat_message(From, {next_state, normal_state, StateData}; NewPacket1 -> NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), - send_multiple(jid:replace_resource(StateData#state.jid, - FromNick), - StateData#state.server_host, - StateData#state.users, - NewPacket), + Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; + true -> ?NS_MUCSUB_NODES_SUBJECT + end, + send_wrapped_multiple( + jid:replace_resource(StateData#state.jid, FromNick), + StateData#state.users, + NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> add_message_to_history(FromNick, From, @@ -1013,9 +1006,9 @@ get_participant_data(From, StateData) -> case (?DICT):find(jid:tolower(From), StateData#state.users) of - {ok, #user{nick = FromNick, role = Role}} -> - {FromNick, Role}; - error -> {<<"">>, moderator} + {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} -> + {FromNick, Role, IsSubscriber}; + error -> {<<"">>, moderator, false} end. process_presence(From, Nick, @@ -1023,6 +1016,7 @@ 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, @@ -1059,7 +1053,7 @@ process_presence(From, Nick, Status_el -> fxml:get_tag_cdata(Status_el) end, - remove_online_user(From, NewState, Reason); + remove_online_user(From, NewState, IsSubscriber, Reason); <<"error">> -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " @@ -1115,22 +1109,28 @@ process_presence(From, Nick, Nick), From, Err), StateData; - _ -> change_nick(From, Nick, StateData) + _ -> + case is_initial_presence(From, StateData) of + true -> + subscriber_becomes_available( + From, Nick, Packet, StateData); + false -> + change_nick(From, Nick, StateData) + end end; _NotNickChange -> - Stanza = case - {(StateData#state.config)#config.allow_visitor_status, - is_visitor(From, StateData)} - of - {false, true} -> - strip_status(Packet); - _Allowed -> Packet - end, - NewState = add_user_presence(From, Stanza, - StateData), - send_new_presence( - From, NewState, StateData), - NewState + 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 end end end, @@ -1140,6 +1140,25 @@ process_presence(From, Nick, {next_state, normal_state, StateData} end. +maybe_strip_status_from_presence(From, Packet, StateData) -> + case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _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), + 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) == [] @@ -1157,6 +1176,15 @@ 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. + %% Check if the user is occupant of the room, or at least is an admin or owner. is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), @@ -1324,6 +1352,7 @@ 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">>, @@ -1338,7 +1367,7 @@ expulse_participant(Packet, From, StateData, Reason1) -> Reason2}]}]}, StateData), send_new_presence(From, NewState, StateData), - remove_online_user(From, NewState). + remove_online_user(From, NewState, IsSubscriber). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). @@ -1439,15 +1468,16 @@ set_role(JID, Role, StateData) -> StateData#state.nicks}, LJIDs); _ -> - {lists:foldl(fun (J, Us) -> - {ok, User} = (?DICT):find(J, - Us), - (?DICT):store(J, - User#user{role = - Role}, - Us) - end, - StateData#state.users, LJIDs), + {lists:foldl( + fun (J, Us) -> + {ok, User} = (?DICT):find(J, Us), + if User#user.last_presence == undefined -> + Us; + true -> + (?DICT):store(J, User#user{role = Role}, Us) + end + end, + StateData#state.users, LJIDs), StateData#state.nicks} end, StateData#state{users = Users, nicks = Nicks}. @@ -1617,27 +1647,66 @@ prepare_room_queue(StateData) -> {empty, _} -> StateData end. -add_online_user(JID, Nick, Role, StateData) -> +update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, + is_subscriber = IsSubscriber} = User, StateData) -> LJID = jid:tolower(JID), - Users = (?DICT):store(LJID, - #user{jid = JID, nick = Nick, role = Role}, - StateData#state.users), - add_to_log(join, Nick, StateData), + Nicks1 = case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{nick = OldNick}} -> + case lists:delete( + LJID, ?DICT:fetch(OldNick, StateData#state.nicks)) of + [] -> + ?DICT:erase(OldNick, StateData#state.nicks); + LJIDs -> + ?DICT:store(OldNick, LJIDs, StateData#state.nicks) + end; + error -> + StateData#state.nicks + end, Nicks = (?DICT):update(Nick, - fun (Entry) -> - case lists:member(LJID, Entry) of - true -> Entry; - false -> [LJID | Entry] - end - end, - [LJID], StateData#state.nicks), + fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, + [LJID], Nicks1), + Users = (?DICT):update(LJID, + fun(U) -> + U#user{nick = Nick, + subscriptions = Nodes, + is_subscriber = IsSubscriber} + end, User, StateData#state.users), + NewStateData = StateData#state{users = Users, nicks = Nicks}, + case {?DICT:find(LJID, StateData#state.users), + ?DICT:find(LJID, NewStateData#state.users)} of + {{ok, #user{nick = Old}}, {ok, #user{nick = New}}} when Old /= New -> + send_nick_changing(JID, Old, NewStateData, true, true); + _ -> + ok + end, + NewStateData. + +add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> tab_add_online_user(JID, StateData), - StateData#state{users = Users, nicks = Nicks}. + 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. -remove_online_user(JID, StateData) -> - remove_online_user(JID, StateData, <<"">>). +remove_online_user(JID, StateData, IsSubscriber) -> + remove_online_user(JID, StateData, IsSubscriber, <<"">>). -remove_online_user(JID, StateData, Reason) -> +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, + StateData#state{users = Users}; +remove_online_user(JID, StateData, _IsSubscriber, Reason) -> LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), @@ -1742,10 +1811,12 @@ find_jid_by_nick(Nick, StateData) -> error -> false end. -higher_presence(Pres1, Pres2) -> +higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), - Pri1 > Pri2. + Pri1 > Pri2; +higher_presence(Pres1, Pres2) -> + Pres1 > Pres2. get_priority_from_presence(PresencePacket) -> case fxml:get_subtag(PresencePacket, <<"priority">>) of @@ -1784,9 +1855,10 @@ nick_collision(User, Nick, StateData) -> /= jid:remove_resource(jid:tolower(User))). add_new_user(From, Nick, - #xmlel{attrs = Attrs, children = Els} = Packet, + #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + UserRoomJID = jid:replace_resource(StateData#state.jid, Nick), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), @@ -1802,6 +1874,7 @@ add_new_user(From, Nick, fun(I) when is_integer(I), I>0 -> I end, 10), Collision = nick_collision(From, Nick, StateData), + IsSubscribeRequest = Name /= <<"presence">>, case {(ServiceAffiliation == owner orelse ((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) @@ -1814,91 +1887,116 @@ add_new_user(From, Nick, of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> Txt = <<"Too many users in this conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {false, _, _, _} when NConferences >= MaxConferences -> Txt = <<"You have joined too many conferences">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {false, _, _, _} -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERR_SERVICE_UNAVAILABLE, + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, _, none} -> - Err = jlib:make_error_reply(Packet, - case Affiliation of - outcast -> - ErrText = - <<"You have been banned from this room">>, - ?ERRT_FORBIDDEN(Lang, ErrText); - _ -> - ErrText = - <<"Membership is required to enter this room">>, - ?ERRT_REGISTRATION_REQUIRED(Lang, - ErrText) - end), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = case Affiliation of + outcast -> + ErrText = <<"You have been banned from this room">>, + ?ERRT_FORBIDDEN(Lang, ErrText); + _ -> + ErrText = <<"Membership is required to enter this room">>, + ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) + end, + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, true, _, _} -> ErrText = <<"That nickname is already in use by another occupant">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_CONFLICT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, false, _} -> ErrText = <<"That nickname is registered by another person">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_CONFLICT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, Els, From, StateData) of true -> - NewState = add_user_presence(From, Packet, - add_online_user(From, Nick, Role, - StateData)), - send_existing_presences(From, NewState), - send_initial_presence(From, NewState, StateData), - Shift = count_stanza_shift(Nick, Els, NewState), - case send_history(From, Shift, NewState) of - true -> ok; - _ -> send_subject(From, StateData) - end, - case NewState#state.just_created of - true -> NewState#state{just_created = false}; - false -> - Robots = (?DICT):erase(From, StateData#state.robots), - NewState#state{robots = Robots} - end; + Nodes = get_subscription_nodes(Packet), + NewStateData = + if not IsSubscribeRequest -> + NewState = add_user_presence( + From, Packet, + add_online_user(From, Nick, Role, + IsSubscribeRequest, + Nodes, StateData)), + send_existing_presences(From, NewState), + send_initial_presence(From, NewState, StateData), + Shift = count_stanza_shift(Nick, Els, NewState), + case send_history(From, Shift, NewState) of + true -> ok; + _ -> send_subject(From, StateData) + end, + NewState; + true -> + add_online_user(From, Nick, none, + IsSubscribeRequest, + Nodes, StateData) + end, + ResultState = + case NewStateData#state.just_created of + true -> + NewStateData#state{just_created = false}; + false -> + Robots = (?DICT):erase(From, StateData#state.robots), + NewStateData#state{robots = Robots} + end, + if not IsSubscribeRequest -> ResultState; + true -> {result, subscription_nodes_to_events(Nodes), ResultState} + end; nopass -> ErrText = <<"A password is required to enter this room">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_AUTHORIZED(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; captcha_required -> SID = fxml:get_attr_s(<<"id">>, Attrs), RoomJID = StateData#state.jid, @@ -1906,7 +2004,7 @@ add_new_user(From, Nick, Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha(SID, RoomJID, To, Lang, Limiter, From) - of + of {ok, ID, CaptchaEls} -> MsgPkt = #xmlel{name = <<"message">>, attrs = [{<<"id">>, ID}], @@ -1914,38 +2012,43 @@ add_new_user(From, Nick, Robots = (?DICT):store(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(RoomJID, From, MsgPkt), - StateData#state{robots = Robots}; + NewState = StateData#state{robots = Robots}, + if not IsSubscribeRequest -> + NewState; + true -> + {ignore, NewState} + end; {error, limit} -> ErrText = <<"Too many CAPTCHA requests">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; _ -> ErrText = <<"Unable to generate a CAPTCHA">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_INTERNAL_SERVER_ERROR(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + Err = ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end end; _ -> ErrText = <<"Incorrect password">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_AUTHORIZED(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end end end. @@ -2111,6 +2214,15 @@ 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). @@ -2159,6 +2271,24 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> OldStateData) end. +is_ra_changed(_, _IsInitialPresence = true, _, _) -> + false; +is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> + JID = case LJID of + #jid{} -> LJID; + _ -> jid:make(LJID) + end, + NewRole = get_role(LJID, NewStateData), + NewAff = get_affiliation(JID, NewStateData), + OldRole = get_role(LJID, OldStateData), + OldAff = get_affiliation(JID, OldStateData), + if (NewRole == none) and (NewAff == OldAff) -> + %% A user is leaving the room; + false; + true -> + (NewRole /= OldRole) or (NewAff /= OldAff) + end. + send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users), @@ -2188,56 +2318,72 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> false -> (?DICT):to_list(StateData#state.users) end, - lists:foreach(fun ({LUJID, Info}) -> - {Role, Presence} = - if - LNJID == LUJID -> {Role0, Presence0}; - true -> {Role1, Presence1} - end, - SRole = role_to_list(Role), - ItemAttrs = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(RealJID)}, - {<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}]; - _ -> - [{<<"affiliation">>, SAffiliation}, - {<<"role">>, SRole}] - end, - ItemEls = case Reason of - <<"">> -> []; - _ -> - [#xmlel{name = <<"reason">>, - attrs = [], - children = - [{xmlcdata, Reason}]}] - end, - StatusEls = status_els(IsInitialPresence, NJID, Info, - StateData), - Packet = fxml:append_subtags(Presence, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs - = - ItemAttrs, - children - = - ItemEls} - | StatusEls]}]), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) - end, - UserList). + lists:foreach( + fun({LUJID, Info}) -> + {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0}; + true -> {Role1, Presence1} + end, + SRole = role_to_list(Role), + ItemAttrs = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jid:to_string(RealJID)}, + {<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}]; + _ -> + [{<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}] + end, + ItemEls = case Reason of + <<"">> -> []; + _ -> + [#xmlel{name = <<"reason">>, + attrs = [], + children = + [{xmlcdata, Reason}]}] + end, + StatusEls = status_els(IsInitialPresence, NJID, Info, + StateData), + Pres = if Presence == undefined -> #xmlel{name = <<"presence">>}; + true -> Presence + end, + Packet = fxml:append_subtags(Pres, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs + = + ItemAttrs, + children + = + ItemEls} + | StatusEls]}]), + Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of + true -> ?NS_MUCSUB_NODES_AFFILIATIONS; + false -> ?NS_MUCSUB_NODES_PRESENCE + end, + 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, + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) and + (IsInitialPresence or (Type == <<"unavailable">>)) -> + Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, Node2, StateData); + true -> + ok + end + end, + UserList). send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of @@ -2249,90 +2395,94 @@ send_existing_presences1(ToJID, StateData) -> LToJID = jid:tolower(ToJID), {ok, #user{jid = RealToJID, role = Role}} = (?DICT):find(LToJID, StateData#state.users), - lists:foreach(fun ({FromNick, _Users}) -> - LJID = find_jid_by_nick(FromNick, StateData), - #user{jid = FromJID, role = FromRole, - last_presence = Presence} = - (?DICT):fetch(jid:tolower(LJID), - StateData#state.users), - PresenceBroadcast = - lists:member( - FromRole, (StateData#state.config)#config.presence_broadcast), - case {RealToJID, PresenceBroadcast} of - {FromJID, _} -> ok; - {_, false} -> ok; - _ -> - FromAffiliation = get_affiliation(LJID, - StateData), - ItemAttrs = case Role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - jid:to_string(FromJID)}, - {<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}]; - _ -> - [{<<"affiliation">>, - affiliation_to_list(FromAffiliation)}, - {<<"role">>, - role_to_list(FromRole)}] - end, - Packet = fxml:append_subtags(Presence, - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs, - children - = - []}]}]), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - FromNick), - RealToJID, Packet) - end - end, - (?DICT):to_list(StateData#state.nicks)). + lists:foreach( + fun({FromNick, _Users}) -> + LJID = find_jid_by_nick(FromNick, StateData), + #user{jid = FromJID, role = FromRole, + last_presence = Presence} = + (?DICT):fetch(jid:tolower(LJID), + StateData#state.users), + PresenceBroadcast = + lists:member( + FromRole, (StateData#state.config)#config.presence_broadcast), + case {RealToJID, PresenceBroadcast} of + {FromJID, _} -> ok; + {_, false} -> ok; + _ -> + FromAffiliation = get_affiliation(LJID, StateData), + ItemAttrs = case Role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jid:to_string(FromJID)}, + {<<"affiliation">>, + affiliation_to_list(FromAffiliation)}, + {<<"role">>, + role_to_list(FromRole)}]; + _ -> + [{<<"affiliation">>, + affiliation_to_list(FromAffiliation)}, + {<<"role">>, + role_to_list(FromRole)}] + end, + Packet = fxml:append_subtags( + Presence, + [#xmlel{name = + <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs, + children + = + []}]}]), + send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), + RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData) + end + end, + (?DICT):to_list(StateData#state.nicks)). -change_nick(JID, Nick, StateData) -> +set_nick(JID, Nick, State) -> LJID = jid:tolower(JID), - {ok, #user{nick = OldNick}} = (?DICT):find(LJID, - StateData#state.users), + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, State#state.users), Users = (?DICT):update(LJID, fun (#user{} = User) -> User#user{nick = Nick} end, - StateData#state.users), - OldNickUsers = (?DICT):fetch(OldNick, - StateData#state.nicks), - NewNickUsers = case (?DICT):find(Nick, - StateData#state.nicks) - of - {ok, U} -> U; - error -> [] + State#state.users), + OldNickUsers = (?DICT):fetch(OldNick, State#state.nicks), + NewNickUsers = case (?DICT):find(Nick, State#state.nicks) of + {ok, U} -> U; + error -> [] end, - SendOldUnavailable = length(OldNickUsers) == 1, - SendNewAvailable = SendOldUnavailable orelse - NewNickUsers == [], Nicks = case OldNickUsers of - [LJID] -> - (?DICT):store(Nick, [LJID | NewNickUsers], - (?DICT):erase(OldNick, StateData#state.nicks)); - [_ | _] -> - (?DICT):store(Nick, [LJID | NewNickUsers], - (?DICT):store(OldNick, OldNickUsers -- [LJID], - StateData#state.nicks)) + [LJID] -> + (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], + (?DICT):erase(OldNick, State#state.nicks)); + [_ | _] -> + (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], + (?DICT):store(OldNick, OldNickUsers -- [LJID], + State#state.nicks)) end, - NewStateData = StateData#state{users = Users, - nicks = Nicks}, + State#state{users = Users, nicks = Nicks}. + +change_nick(JID, Nick, StateData) -> + LJID = jid:tolower(JID), + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, StateData#state.users), + OldNickUsers = (?DICT):fetch(OldNick, StateData#state.nicks), + NewNickUsers = case (?DICT):find(Nick, StateData#state.nicks) of + {ok, U} -> U; + error -> [] + end, + SendOldUnavailable = length(OldNickUsers) == 1, + SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [], + NewStateData = set_nick(JID, Nick, StateData), case presence_broadcast_allowed(JID, NewStateData) of true -> send_nick_changing(JID, OldNick, NewStateData, @@ -2352,7 +2502,7 @@ send_nick_changing(JID, OldNick, StateData, Affiliation = get_affiliation(JID, StateData), SAffiliation = affiliation_to_list(Affiliation), SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) -> + lists:foreach(fun ({_LJID, Info}) when Presence /= undefined -> ItemAttrs1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false @@ -2428,17 +2578,23 @@ send_nick_changing(JID, OldNick, StateData, = []}|Status110]}]), if SendOldUnavailable -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - OldNick), - Info#user.jid, Packet1); + send_wrapped(jid:replace_resource(StateData#state.jid, + OldNick), + Info#user.jid, Packet1, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); true -> ok end, if SendNewAvailable -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet2); + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet2, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); true -> ok - end + end; + (_) -> + ok end, (?DICT):to_list(StateData#state.users)). @@ -2731,13 +2887,7 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> jid:to_string(StateData#state.jid), Res]), NSD = lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)), - case (NSD#state.config)#config.persistent of - true -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, NSD#state.room, - make_opts(NSD)); - _ -> ok - end, + store_room(NSD), {result, [], NSD}; Err -> Err end. @@ -3194,9 +3344,18 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, Code}], children = []}]}]}, - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) + RoomJIDNick = jid:replace_resource( + StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + IsSubscriber = Info#user.is_subscriber, + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) -> + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); + true -> + ok + end end, (?DICT):to_list(StateData#state.users)). @@ -3686,6 +3845,9 @@ get_config(Lang, StateData, From) -> ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, <<"muc#roomconfig_allowvoicerequests">>, (Config#config.allow_voice_requests)), + ?BOOLXFIELD(<<"Allow subscription">>, + <<"muc#roomconfig_allow_subscription">>, + (Config#config.allow_subscription)), ?STRINGXFIELD(<<"Minimum interval between voice requests " "(in seconds)">>, <<"muc#roomconfig_voicerequestmininterval">>, @@ -3698,14 +3860,6 @@ get_config(Lang, StateData, From) -> (Config#config.captcha_protected))]; false -> [] end ++ - case gen_mod:is_loaded(StateData#state.server_host, mod_mam) of - true -> - [?BOOLXFIELD(<<"Enable message archiving">>, - <<"muc#roomconfig_mam">>, - (Config#config.mam))]; - false -> [] - end - ++ [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, <<"muc#roomconfig_captcha_whitelist">>, ((?SETS):to_list(Config#config.captcha_whitelist)))] @@ -3720,6 +3874,10 @@ get_config(Lang, StateData, From) -> (Config#config.logging))]; _ -> [] end, + X = ejabberd_hooks:run_fold(get_room_config, + StateData#state.server_host, + Res, + [StateData, From, Lang]), {result, [#xmlel{name = <<"instructions">>, attrs = [], children = @@ -3730,7 +3888,7 @@ get_config(Lang, StateData, From) -> #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = Res}], + children = X}], StateData}. set_config(XEl, StateData, Lang) -> @@ -3738,7 +3896,8 @@ set_config(XEl, StateData, Lang) -> case XData of invalid -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)}; _ -> - case set_xoption(XData, StateData#state.config) of + case set_xoption(XData, StateData#state.config, + StateData#state.server_host, Lang) of #config{} = Config -> Res = change_config(Config, StateData), {result, _, NSD} = Res, @@ -3760,30 +3919,30 @@ set_config(XEl, StateData, Lang) -> -define(SET_BOOL_XOPT(Opt, Val), case Val of <<"0">> -> - set_xoption(Opts, Config#config{Opt = false}); + set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang); <<"false">> -> - set_xoption(Opts, Config#config{Opt = false}); - <<"1">> -> set_xoption(Opts, Config#config{Opt = true}); + set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang); + <<"1">> -> set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang); <<"true">> -> - set_xoption(Opts, Config#config{Opt = true}); + set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang); _ -> Txt = <<"Value of '~s' should be boolean">>, ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)} + {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} end). -define(SET_NAT_XOPT(Opt, Val), case catch jlib:binary_to_integer(Val) of I when is_integer(I), I > 0 -> - set_xoption(Opts, Config#config{Opt = I}); + set_xoption(Opts, Config#config{Opt = I}, ServerHost, Lang); _ -> Txt = <<"Value of '~s' should be integer">>, ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)} + {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)} end). -define(SET_STRING_XOPT(Opt, Val), - set_xoption(Opts, Config#config{Opt = Val})). + set_xoption(Opts, Config#config{Opt = Val}, ServerHost, Lang)). -define(SET_JIDMULTI_XOPT(Opt, Vals), begin @@ -3795,33 +3954,33 @@ set_config(XEl, StateData, Lang) -> (_, Set1) -> Set1 end, (?SETS):empty(), Vals), - set_xoption(Opts, Config#config{Opt = Set}) + set_xoption(Opts, Config#config{Opt = Set}, ServerHost, Lang) end). -set_xoption([], Config) -> Config; +set_xoption([], Config, _ServerHost, _Lang) -> Config; set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_STRING_XOPT(title, Val); set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_STRING_XOPT(description, Val); set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_change_subj, Val); set_xoption([{<<"allow_query_users">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_query_users, Val); set_xoption([{<<"allow_private_messages">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_private_messages, Val); set_xoption([{<<"allow_private_messages_from_visitors">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> case Val of <<"anyone">> -> ?SET_STRING_XOPT(allow_private_messages_from_visitors, @@ -3835,62 +3994,66 @@ set_xoption([{<<"allow_private_messages_from_visitors">>, _ -> Txt = <<"Value of 'allow_private_messages_from_visitors' " "should be anyone|moderators|nobody">>, - {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)} + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_visitor_status, Val); set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(public, Val); set_xoption([{<<"public_list">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(public_list, Val); set_xoption([{<<"muc#roomconfig_persistentroom">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(persistent, Val); set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(moderated, Val); set_xoption([{<<"members_by_default">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(members_by_default, Val); set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(members_only, Val); set_xoption([{<<"captcha_protected">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(captcha_protected, Val); set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_user_invites, Val); +set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]} + | Opts], + Config, ServerHost, Lang) -> + ?SET_BOOL_XOPT(allow_subscription, Val); set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(password_protected, Val); set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_STRING_XOPT(password, Val); set_xoption([{<<"anonymous">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(anonymous, Val); set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], - Config) -> + Config, ServerHost, Lang) -> Roles = lists:foldl( fun(_S, error) -> error; @@ -3906,27 +4069,28 @@ set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], error -> Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should " "be moderator|participant|visitor">>, - {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}; + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {M, P, V} -> Res = if M -> [moderator]; true -> [] end ++ if P -> [participant]; true -> [] end ++ if V -> [visitor]; true -> [] end, - set_xoption(Opts, Config#config{presence_broadcast = Res}) + set_xoption(Opts, Config#config{presence_broadcast = Res}, + ServerHost, Lang) end; set_xoption([{<<"muc#roomconfig_allowvoicerequests">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(allow_voice_requests, Val); set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_NAT_XOPT(voice_request_min_interval, Val); set_xoption([{<<"muc#roomconfig_whois">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> case Val of <<"moderators">> -> ?SET_BOOL_XOPT(anonymous, @@ -3937,37 +4101,44 @@ set_xoption([{<<"muc#roomconfig_whois">>, [Val]} _ -> Txt = <<"Value of 'muc#roomconfig_whois' should be " "moderators|anyone">>, - {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)} + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> case Val of <<"none">> -> ?SET_STRING_XOPT(max_users, none); _ -> ?SET_NAT_XOPT(max_users, Val) end; set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], - Config) -> + Config, ServerHost, Lang) -> ?SET_BOOL_XOPT(logging, Val); -set_xoption([{<<"muc#roomconfig_mam">>, [Val]}|Opts], Config) -> - ?SET_BOOL_XOPT(mam, Val); set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, Vals} | Opts], - Config) -> + Config, ServerHost, Lang) -> JIDs = [jid:from_string(Val) || Val <- Vals], ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); -set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> - set_xoption(Opts, Config); -set_xoption([{Opt, _Vals} | _Opts], _Config) -> +set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config, ServerHost, Lang) -> + set_xoption(Opts, Config, ServerHost, Lang); +set_xoption([{Opt, Vals} | Opts], Config, ServerHost, Lang) -> Txt = <<"Unknown option '~s'">>, ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), - {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)}. + Err = {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)}, + case ejabberd_hooks:run_fold(set_room_option, + ServerHost, + Err, + [Opt, Vals, Lang]) of + {error, Reason} -> + {error, Reason}; + {Pos, Val} -> + set_xoption(Opts, setelement(Pos, Config, Val), ServerHost, Lang) + end. change_config(Config, StateData) -> send_config_change_info(Config, StateData), - NSD = StateData#state{config = Config}, + NSD = remove_subscriptions(StateData#state{config = Config}), case {(StateData#state.config)#config.persistent, Config#config.persistent} of @@ -4015,10 +4186,11 @@ send_config_change_info(New, #state{config = Old} = StateData) -> children = [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC_USER}], children = StatusEls}]}, - send_multiple(StateData#state.jid, - StateData#state.server_host, - StateData#state.users, - Message). + send_wrapped_multiple(StateData#state.jid, + StateData#state.users, + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData). remove_nonmembers(StateData) -> lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> @@ -4148,6 +4320,18 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; + allow_subscription -> + 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); affiliations -> StateData#state{affiliations = (?DICT):from_list(Val)}; subject -> StateData#state{subject = Val}; @@ -4161,6 +4345,13 @@ 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), [?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), @@ -4187,7 +4378,8 @@ make_opts(StateData) -> {affiliations, (?DICT):to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, - {subject_author, StateData#state.subject_author}]. + {subject_author, StateData#state.subject_author}, + {subscribers, Subscribers}]. destroy_room(DEl, StateData) -> lists:foreach(fun ({_LJID, Info}) -> @@ -4210,9 +4402,10 @@ destroy_room(DEl, StateData) -> children = []}, DEl]}]}, - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_CONFIG, StateData) end, (?DICT):to_list(StateData#state.users)), case (StateData#state.config)#config.persistent of @@ -4264,6 +4457,10 @@ process_iq_disco_info(_From, get, Lang, StateData) -> <<"muc_moderated">>, <<"muc_unmoderated">>), ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ case Config#config.allow_subscription of + true -> [?FEATURE(?NS_MUCSUB)]; + false -> [] + end ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> @@ -4361,6 +4558,118 @@ process_iq_vcard(From, set, Lang, SubEl, StateData) -> {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. +process_iq_mucsub(From, Packet, + #iq{type = set, lang = Lang, + sub_el = #xmlel{name = <<"subscribe">>} = SubEl}, + #state{config = Config} = StateData) -> + case fxml:get_tag_attr_s(<<"nick">>, SubEl) of + <<"">> -> + 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 -> + Nodes = get_subscription_nodes(Packet), + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick)} of + {true, _} -> + ErrText = <<"That nickname is already in use by another occupant">>, + {error, ?ERRT_CONFLICT(Lang, ErrText)}; + {_, false} -> + ErrText = <<"That nickname is registered by another person">>, + {error, ?ERRT_CONFLICT(Lang, ErrText)}; + _ -> + NewStateData = add_online_user( + From, Nick, Role, true, Nodes, StateData), + {result, subscription_nodes_to_events(Nodes), NewStateData} + end; + {ok, #user{role = Role}} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = add_online_user( + From, Nick, Role, true, Nodes, StateData), + {result, subscription_nodes_to_events(Nodes), NewStateData}; + error -> + add_new_user(From, Nick, Packet, StateData) + end; + _ -> + Err = ?ERRT_NOT_ALLOWED(Lang, <<"Subscriptions are not allowed">>), + {error, Err} + end; +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}; + _ -> + {result, [], StateData} + end; +process_iq_mucsub(_From, _Packet, #iq{type = set, 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) -> + 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); + true -> + StateData + end. + +get_subscription_nodes(#xmlel{name = <<"iq">>} = Packet) -> + case fxml:get_subtag_with_xmlns(Packet, <<"subscribe">>, ?NS_MUCSUB) of + #xmlel{children = Els} -> + lists:flatmap( + fun(#xmlel{name = <<"event">>, attrs = Attrs}) -> + Node = fxml:get_attr_s(<<"node">>, Attrs), + case lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, + ?NS_MUCSUB_NODES_MESSAGES, + ?NS_MUCSUB_NODES_AFFILIATIONS, + ?NS_MUCSUB_NODES_SUBJECT, + ?NS_MUCSUB_NODES_CONFIG, + ?NS_MUCSUB_NODES_PARTICIPANTS]) of + true -> + [Node]; + false -> + [] + end; + (_) -> + [] + end, Els); + false -> + [] + end; +get_subscription_nodes(_) -> + []. + +subscription_nodes_to_events(Nodes) -> + [#xmlel{name = <<"event">>, attrs = [{<<"node">>, Node}]} || Node <- Nodes]. + get_title(StateData) -> case (StateData#state.config)#config.title of <<"">> -> StateData#state.room; @@ -4484,9 +4793,9 @@ send_voice_request(From, StateData) -> Moderators = search_role(moderator, StateData), FromNick = find_nick_by_jid(From, StateData), lists:foreach(fun ({_, User}) -> - ejabberd_router:route(StateData#state.jid, User#user.jid, - prepare_request_form(From, FromNick, - <<"">>)) + ejabberd_router:route( + StateData#state.jid, User#user.jid, + prepare_request_form(From, FromNick, <<"">>)) end, Moderators). @@ -4777,6 +5086,46 @@ tab_count_user(JID) -> element_size(El) -> byte_size(fxml:element_to_binary(El)). +store_room(StateData) -> + if (StateData#state.config)#config.persistent -> + mod_muc:store_room(StateData#state.server_host, + StateData#state.host, StateData#state.room, + make_opts(StateData)); + true -> + ok + end. + +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 -> + ok + end; + _ -> + ejabberd_router:route(From, To, Packet) + end. + +wrap(From, To, Packet, Node) -> + Pkt1 = jlib:replace_from_to(From, To, Packet), + Pkt2 = #xmlel{attrs = Attrs} = jlib:remove_attr(<<"xmlns">>, Pkt1), + Pkt3 = Pkt2#xmlel{attrs = [{<<"xmlns">>, <<"jabber:client">>}|Attrs]}, + Item = #xmlel{name = <<"item">>, + attrs = [{<<"id">>, randoms:get_string()}], + children = [Pkt3]}, + Items = #xmlel{name = <<"items">>, attrs = [{<<"node">>, Node}], + children = [Item]}, + Event = #xmlel{name = <<"event">>, + attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}], + children = [Items]}, + #xmlel{name = <<"message">>, children = [Event]}. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast @@ -4784,6 +5133,12 @@ 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( + fun({_, #user{jid = To}}) -> + send_wrapped(From, To, Packet, Node, State) + end, ?DICT:to_list(Users)). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaninful content diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index cbe2a4e50..df385c28c 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -40,7 +40,7 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). --export([purge_loop/1, mod_opt_type/1]). +-export([purge_loop/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -1219,6 +1219,9 @@ stj(String) -> jid:from_string(String). jts(String) -> jid:to_string(String). +depends(_Host, _Opts) -> + []. + mod_opt_type(access) -> fun acl:access_rules_validator/1; mod_opt_type(host) -> fun iolist_to_binary/1; diff --git a/src/mod_offline.erl b/src/mod_offline.erl index a794a0371..799605c69 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -66,7 +66,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -deprecated({get_queue_length,2}). @@ -125,6 +125,8 @@ stop(Host) -> supervisor:delete_child(ejabberd_sup, Proc), ok. +depends(_Host, _Opts) -> + []. %%==================================================================== %% gen_server callbacks @@ -791,7 +793,7 @@ get_messages_subset(User, Host, MsgsAll) -> fun(A) when is_atom(A) -> A end, max_user_offline_messages), MaxOfflineMsgs = case get_max_user_messages(Access, - {User, Host}, Host) + User, Host) of Number when is_integer(Number) -> Number; _ -> 100 diff --git a/src/mod_ping.erl b/src/mod_ping.erl index f1c175a91..d1b3f9322 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -55,7 +55,7 @@ handle_cast/2, handle_info/2, code_change/3]). -export([iq_ping/3, user_online/3, user_offline/3, - user_send/4, mod_opt_type/1]). + user_send/4, mod_opt_type/1, depends/2]). -record(state, {host = <<"">>, @@ -253,6 +253,9 @@ cancel_timer(TRef) -> _ -> ok end. +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(ping_interval) -> fun (I) when is_integer(I), I > 0 -> I end; diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index 1118b7bbc..e6f2cfbab 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -28,7 +28,7 @@ -behavior(gen_mod). -export([start/2, stop/1, check_packet/6, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -48,6 +48,9 @@ stop(Host) -> ?MODULE, check_packet, 25), ok. +depends(_Host, _Opts) -> + []. + check_packet(_, _User, Server, _PrivacyList, {From, To, #xmlel{name = Name, attrs = Attrs}}, Dir) -> case Name of diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index ad13c27cd..18ff78371 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -36,7 +36,7 @@ check_packet/6, remove_user/2, is_list_needdb/1, updated_list/3, item_to_xml/1, get_user_lists/2, import/3, - set_privacy_list/1, mod_opt_type/1]). + set_privacy_list/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -593,6 +593,9 @@ import(LServer, DBType, Data) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Data). +depends(_Host, _Opts) -> + []. + mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_private.erl b/src/mod_private.erl index 38e42ca4c..f0e4632f6 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -33,7 +33,7 @@ -export([start/2, stop/1, process_sm_iq/3, import/3, remove_user/2, get_data/2, export/1, import/1, - mod_opt_type/1, set_data/3]). + mod_opt_type/1, set_data/3, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -173,6 +173,9 @@ import(LServer, DBType, PD) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, PD). +depends(_Host, _Opts) -> + []. + mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index 2737de7ad..beea35725 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -39,7 +39,7 @@ %% supervisor callbacks. -export([init/1]). --export([start_link/2, mod_opt_type/1]). +-export([start_link/2, mod_opt_type/1, depends/2]). -define(PROCNAME, ejabberd_mod_proxy65). @@ -84,6 +84,9 @@ init([Host, Opts]) -> {{one_for_one, 10, 1}, [StreamManager, StreamSupervisor, Service]}}. +depends(_Host, _Opts) -> + []. + mod_opt_type(auth_type) -> fun (plain) -> plain; (anonymous) -> anonymous diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 53378c355..81afc9a06 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -77,7 +77,7 @@ %% API and gen_server callbacks -export([start_link/2, start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). + terminate/2, code_change/3, depends/2]). -export([send_loop/1, mod_opt_type/1]). @@ -347,6 +347,18 @@ init_send_loop(ServerHost) -> end, {Pid, State}. +depends(ServerHost, Opts) -> + Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), + Plugins = gen_mod:get_opt(plugins, Opts, + fun(A) when is_list(A) -> A end, [?STDNODE]), + lists:flatmap( + fun(Name) -> + Plugin = plugin(ServerHost, Name), + try apply(Plugin, depends, [Host, ServerHost, Opts]) + catch _:undef -> [] + end + end, Plugins). + %% @doc Call the init/1 function for each plugin declared in the config file. %% The default plugin module is implicit. %% <p>The Erlang code for the plugin is located in a module called @@ -406,7 +418,8 @@ send_loop(State) -> {_, Node} = NodeRec#pubsub_node.nodeid, Nidx = NodeRec#pubsub_node.id, Options = NodeRec#pubsub_node.options, - send_items(Host, Node, Nidx, PType, Options, SubJID, last) + [send_items(Host, Node, Nidx, PType, Options, SubJID, last) + || NodeRec#pubsub_node.type == PType] end, lists:usort(Subs)) end, diff --git a/src/mod_register.erl b/src/mod_register.erl index 43499d2e5..45cd78fef 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -37,7 +37,7 @@ unauthenticated_iq_register/4, try_register/5, process_iq/3, send_registration_notifications/3, transform_options/1, transform_module_options/1, - mod_opt_type/1, opt_type/1]). + mod_opt_type/1, opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -72,6 +72,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_REGISTER). +depends(_Host, _Opts) -> + []. + stream_feature_register(Acc, Host) -> AF = gen_mod:get_module_opt(Host, ?MODULE, access_from, fun(A) -> A end, diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 2b7e5f532..76de1677f 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -55,7 +55,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, process/2, mod_opt_type/1]). +-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -76,6 +76,9 @@ start(_Host, _Opts) -> stop(_Host) -> ok. +depends(_Host, _Opts) -> + [{mod_register, hard}]. + %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- diff --git a/src/mod_roster.erl b/src/mod_roster.erl index f79061560..a75041bc7 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -49,7 +49,7 @@ get_jid_info/4, item_to_xml/1, webadmin_page/3, webadmin_user/4, get_versioning_feature/2, roster_versioning_enabled/1, roster_version/2, - mod_opt_type/1, set_roster/1]). + mod_opt_type/1, set_roster/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -136,6 +136,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER). +depends(_Host, _Opts) -> + []. + process_iq(From, To, IQ) when ((From#jid.luser == <<"">>) andalso (From#jid.resource == <<"">>)) -> process_iq_manager(From, To, IQ); diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 899978091..61f59a990 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -244,31 +244,6 @@ raw_to_record(LServer, askmessage = SAskMessage} end. -record_to_string(#roster{us = {User, _Server}, - jid = JID, name = Name, subscription = Subscription, - ask = Ask, askmessage = AskMessage}) -> - Username = ejabberd_sql:escape(User), - SJID = - ejabberd_sql:escape(jid:to_string(jid:tolower(JID))), - Nick = ejabberd_sql:escape(Name), - SSubscription = case Subscription of - both -> <<"B">>; - to -> <<"T">>; - from -> <<"F">>; - none -> <<"N">> - end, - SAsk = case Ask of - subscribe -> <<"S">>; - unsubscribe -> <<"U">>; - both -> <<"B">>; - out -> <<"O">>; - in -> <<"I">>; - none -> <<"N">> - end, - SAskMessage = ejabberd_sql:escape(AskMessage), - [Username, SJID, Nick, SSubscription, SAsk, SAskMessage, - <<"N">>, <<"">>, <<"item">>]. - record_to_row( #roster{us = {LUser, _LServer}, jid = JID, name = Name, subscription = Subscription, diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl index 3e1da3a4e..ae264bbc9 100644 --- a/src/mod_service_log.erl +++ b/src/mod_service_log.erl @@ -30,7 +30,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, log_user_send/4, - log_user_receive/5, mod_opt_type/1]). + log_user_receive/5, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -51,6 +51,9 @@ stop(Host) -> ?MODULE, log_user_receive, 50), ok. +depends(_Host, _Opts) -> + []. + log_user_send(Packet, _C2SState, From, To) -> log_packet(From, To, Packet, From#jid.lserver), Packet. diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 76a619c9b..b472e1aab 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -39,7 +39,7 @@ delete_group/2, get_group_opts/2, set_group_opts/3, get_group_users/2, get_group_explicit_users/2, is_user_in_group/3, add_user_to_group/3, opts_to_binary/1, - remove_user_from_group/3, mod_opt_type/1]). + remove_user_from_group/3, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -132,6 +132,9 @@ stop(Host) -> %%ejabberd_hooks:delete(remove_user, Host, %% ?MODULE, remove_user, 50), +depends(_Host, _Opts) -> + []. + get_user_roster(Items, US) -> {U, S} = US, DisplayedGroups = get_user_displayed_groups(US), diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 34588b90c..22f50d302 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -41,7 +41,7 @@ -export([get_user_roster/2, get_subscription_lists/3, get_jid_info/4, process_item/2, in_subscription/6, - out_subscription/4, mod_opt_type/1, opt_type/1]). + out_subscription/4, mod_opt_type/1, opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -105,6 +105,9 @@ stop(Host) -> supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). +depends(_Host, _Opts) -> + [{mod_roster, hard}]. + %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- diff --git a/src/mod_sic.erl b/src/mod_sic.erl index b4eae0daf..49b65a0ee 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -32,7 +32,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - process_sm_iq/3, mod_opt_type/1]). + process_sm_iq/3, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -55,6 +55,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SIC). +depends(_Host, _Opts) -> + []. + process_local_iq(#jid{user = User, server = Server, resource = Resource}, _To, #iq{type = get, sub_el = _SubEl} = IQ) -> diff --git a/src/mod_sip.erl b/src/mod_sip.erl index 6ffe56331..816100f47 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -34,7 +34,7 @@ -export([data_in/2, data_out/2, message_in/2, message_out/2, request/2, request/3, response/2, - locate/1, mod_opt_type/1]). + locate/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -62,6 +62,9 @@ start(_Host, _Opts) -> stop(_Host) -> ok. +depends(_Host, _Opts) -> + []. + data_in(Data, #sip_socket{type = Transport, addr = {MyIP, MyPort}, peer = {PeerIP, PeerPort}}) -> diff --git a/src/mod_stats.erl b/src/mod_stats.erl index c14cf8d15..99059839a 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -32,7 +32,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -48,6 +48,9 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_STATS). +depends(_Host, _Opts) -> + []. + process_local_iq(_From, To, #iq{id = _ID, type = Type, xmlns = XMLNS, sub_el = SubEl, lang = Lang} = diff --git a/src/mod_time.erl b/src/mod_time.erl index 740b654a7..90296f3d8 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -33,7 +33,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -86,5 +86,8 @@ process_local_iq(_From, _To, sign(N) when N < 0 -> <<"-">>; sign(_) -> <<"+">>. +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 5e042528b..aca9d7462 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -34,7 +34,7 @@ -export([start/2, init/3, stop/1, get_sm_features/5, process_local_iq/3, process_sm_iq/3, string2lower/1, - remove_user/2, export/1, import/1, import/3, + remove_user/2, export/1, import/1, import/3, depends/2, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). -include("ejabberd.hrl"). @@ -594,6 +594,9 @@ import(LServer, DBType, VCard) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, VCard). +depends(_Host, _Opts) -> + []. + mod_opt_type(allow_return_all) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 46f81af09..a0ad305a9 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -40,7 +40,7 @@ -export([start/2, start_link/2, stop/1, get_sm_features/5, process_local_iq/3, process_sm_iq/3, remove_user/1, route/4, transform_module_options/1, - mod_opt_type/1, opt_type/1]). + mod_opt_type/1, opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -138,6 +138,9 @@ stop(Host) -> supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). +depends(_Host, _Opts) -> + []. + terminate(_Reason, State) -> Host = State#state.serverhost, gen_iq_handler:remove_iq_handler(ejabberd_local, Host, diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 041b0b64c..f2101df91 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -13,7 +13,7 @@ -export([start/2, stop/1]). -export([update_presence/3, vcard_set/3, export/1, - import/1, import/3, mod_opt_type/1]). + import/1, import/3, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -46,6 +46,9 @@ stop(Host) -> vcard_set, 100), ok. +depends(_Host, _Opts) -> + []. + %%==================================================================== %% Hooks %%==================================================================== diff --git a/src/mod_version.erl b/src/mod_version.erl index 7f7759f2d..8a035763f 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -32,7 +32,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - mod_opt_type/1]). + mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -93,6 +93,9 @@ get_os() -> #xmlel{name = <<"os">>, attrs = [], children = [{xmlcdata, OS}]}. +depends(_Host, _Opts) -> + []. + mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(show_os) -> fun (B) when is_boolean(B) -> B end; diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 2030b12c7..fa4af4d57 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -365,21 +365,22 @@ get_entity_subscriptions(Host, Owner) -> H = encode_host(Host), SJ = encode_jid(SubKey), GJ = encode_jid(GenKey), - GJLike = <<(encode_jid_like(GenKey))/binary, "%">>, + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, Query = case SubKey of GenKey -> ?SQL("select @(node)s, @(type)s, @(i.nodeid)d," " @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and jid like %(GJLike)s" - " escape '^' and host=%(H)s"); + "where i.nodeid = n.nodeid and " + "(jid=%(GJ)s or jid like %(GJLike)s escape '^')" + " and host=%(H)s"); _ -> ?SQL("select @(node)s, @(type)s, @(i.nodeid)d," " @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and jid in" - " (%(SJ)s, %(GJ)s) and host=%(H)s") + "where i.nodeid = n.nodeid and" + " jid in (%(SJ)s, %(GJ)s) and host=%(H)s") end, Reply = case catch ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> @@ -415,7 +416,7 @@ get_entity_subscriptions_for_send_last(Host, Owner) -> H = encode_host(Host), SJ = encode_jid(SubKey), GJ = encode_jid(GenKey), - GJLike = <<(encode_jid_like(GenKey))/binary, "%">>, + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, Query = case SubKey of GenKey -> @@ -423,8 +424,9 @@ get_entity_subscriptions_for_send_last(Host, Owner) -> " @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n, pubsub_node_option o " "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' " - "and val='on_sub_and_presence' and jid like %(GJLike)s" - " escape '^' and host=%(H)s"); + "and val='on_sub_and_presence' and " + "(jid=%(GJ)s or jid like %(GJLike)s escape '^')" + " and host=%(H)s"); _ -> ?SQL("select @(node)s, @(type)s, @(i.nodeid)d," " @(jid)s, @(subscriptions)s " @@ -912,11 +914,13 @@ first_in_list(Pred, [H | T]) -> end. itemids(Nidx, {_U, _S, _R} = JID) -> - SJID = <<(ejabberd_sql:escape(encode_jid_like(JID)))/binary, "%">>, + SJID = encode_jid(JID), + SJIDLike = <<(ejabberd_sql:escape(encode_jid_like(JID)))/binary, "/%">>, case catch ejabberd_sql:sql_query_t( ?SQL("select @(itemid)s from pubsub_item where " - "nodeid=%(Nidx)d and publisher like %(SJID)s escape '^' " + "nodeid=%(Nidx)d and (publisher=%(SJID)s" + " or publisher like %(SJIDLike)s escape '^') " "order by modification desc")) of {selected, RItems} -> @@ -1033,13 +1037,6 @@ encode_subscriptions(Subscriptions) -> %%% record getter/setter -state_to_raw(Nidx, State) -> - {JID, _} = State#pubsub_state.stateid, - J = ejabberd_sql:escape(encode_jid(JID)), - A = encode_affiliation(State#pubsub_state.affiliation), - S = encode_subscriptions(State#pubsub_state.subscriptions), - [<<"'">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"'">>]. - raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}); raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> diff --git a/src/node_pep.erl b/src/node_pep.erl index 504e39fa3..1677ed4bd 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -45,11 +45,13 @@ 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, depends/3]). + +depends(_Host, _ServerHost, _Opts) -> + [{mod_caps, hard}]. init(Host, ServerHost, Opts) -> node_flat:init(Host, ServerHost, Opts), - complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> @@ -245,21 +247,3 @@ node_to_path(Node) -> path_to_node(Path) -> node_flat:path_to_node(Path). - -%%% -%%% Internal -%%% - -%% @doc Check mod_caps is enabled, otherwise show warning. -%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. -%% Check that the mod_caps module is enabled in that Jabber Host -%% If not, show a warning message in the ejabberd log file. -complain_if_modcaps_disabled(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_caps) of - false -> - ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub " - "of host ~p. This plugin requires mod_caps " - "but it does not seems enabled, please check config.", - [ServerHost]); - true -> ok - end. diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 1df173fd7..ec7795475 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -45,12 +45,14 @@ 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, depends/3, get_entity_subscriptions_for_send_last/2, get_last_items/3]). +depends(_Host, _ServerHost, _Opts) -> + [{mod_caps, hard}]. + init(Host, ServerHost, Opts) -> node_flat_sql:init(Host, ServerHost, Opts), - complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> @@ -237,21 +239,3 @@ node_to_path(Node) -> path_to_node(Path) -> node_flat_sql:path_to_node(Path). - -%%% -%%% Internal -%%% - -%% @doc Check mod_caps is enabled, otherwise show warning. -%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host. -%% Check that the mod_caps module is enabled in that Jabber Host -%% If not, show a warning message in the ejabberd log file. -complain_if_modcaps_disabled(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_caps) of - false -> - ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub " - "of host ~p. This plugin requires mod_caps " - "to be enabled, but it isn't.", - [ServerHost]); - true -> ok - end. diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl index 1ad4046cd..edfdbc1d5 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -191,18 +191,25 @@ get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(Host, Node) -> - H = node_flat_sql:encode_host(Host), - N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "%">>, - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(type)s, @(nodeid)d from " - "pubsub_node where host=%(H)s" - " and node like %(N)s escape '^'")) - of - {selected, RItems} -> - [raw_to_node(Host, Item) || Item <- RItems]; - _ -> - [] + case get_node(Host, Node) of + {error, _} -> + []; + Rec -> + H = node_flat_sql:encode_host(Host), + N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "/%">>, + Sub = case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(type)s, @(nodeid)d from " + "pubsub_node where host=%(H)s" + " and node like %(N)s escape '^'" + " and \"type\"='hometree'")) + of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end, + [Rec|Sub] end. create_node(Host, Node, Type, Owner, Options, Parents) -> @@ -252,11 +259,12 @@ create_node(Host, Node, Type, Owner, Options, Parents) -> delete_node(Host, Node) -> H = node_flat_sql:encode_host(Host), - N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "%">>, + N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "/%">>, Removed = get_subnodes_tree(Host, Node), catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_node where host=%(H)s" - " and node like %(N)s escape '^'")), + " and (node=%(Node)s" + " or (\"type\"='hometree' and node like %(N)s escape '^'))")), Removed. %% helpers diff --git a/test/acl_test.exs b/test/acl_test.exs index 551c74ae0..4bd8e6989 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 + :stringprep.start :ok = :ejabberd_config.start(["domain1", "domain2"], []) :ok = :acl.start end diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs index 487cf6a4b..439a3c1d3 100644 --- a/test/ejabberd_commands_mock_test.exs +++ b/test/ejabberd_commands_mock_test.exs @@ -18,9 +18,13 @@ # # ---------------------------------------------------------------------- +## TODO Fix next test error: add admin user ACL + defmodule EjabberdCommandsMockTest do use ExUnit.Case, async: false + require EjabberdOauthMock + @author "jsautret@process-one.net" # mocked callback module @@ -44,8 +48,11 @@ defmodule EjabberdCommandsMockTest do _ -> :ok end :mnesia.start + :ok = :jid.start + :ok = :ejabberd_config.start(["domain1", "domain2"], []) + :ok = :acl.start EjabberdOauthMock.init - :ok + on_exit fn -> :meck.unload end end setup do @@ -180,7 +187,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 @@ -313,9 +320,8 @@ defmodule EjabberdCommandsMockTest do end - 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 @@ -393,13 +399,47 @@ defmodule EjabberdCommandsMockTest do assert :meck.validate @module end + test "Commands can perform extra check on access" do + mock_commands_config [:admin, :open] + + 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, @host}) +# :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 # 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 @@ -408,10 +448,12 @@ 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 :meck.expect(:acl, :access_matches, fn(:commands_admin_access, info, _scope) -> 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/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 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 47b1fe94a..9cba35365 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 @@ -70,9 +71,9 @@ 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_commands, + :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_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) -> @@ -123,9 +124,9 @@ 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_commands, + :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_exposed_commands, fn () -> [@acommand] end) :meck.expect(:ejabberd_commands, :execute_command, fn (:undefined, {@user, @domain, {:oauth, _token}, false}, @@ -134,7 +135,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 +148,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 +198,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 @@ -193,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_exposed_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 diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs index 99b8d9b28..e2ae3d784 100644 --- a/test/mod_http_api_test.exs +++ b/test/mod_http_api_test.exs @@ -31,43 +31,43 @@ 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 # 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, []) + :ejabberd_commands.expose_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]}]]) + :ejabberd_commands.expose_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 @@ -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 |