Merge branch 'master' of github.com:processone/ejabberd
This commit is contained in:
commit
42bede77a1
|
@ -207,6 +207,11 @@ install: all copy-files
|
|||
> ejabberd.init
|
||||
chmod 755 ejabberd.init
|
||||
#
|
||||
# Service script
|
||||
$(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*" ejabberd.service.template \
|
||||
> ejabberd.service
|
||||
chmod 755 ejabberd.service
|
||||
#
|
||||
# Spool directory
|
||||
$(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR)
|
||||
$(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT)
|
||||
|
|
62
mix.exs
62
mix.exs
|
@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
|
|||
|
||||
def project do
|
||||
[app: :ejabberd,
|
||||
version: "16.08.0",
|
||||
version: "16.11.0",
|
||||
description: description,
|
||||
elixir: "~> 1.2",
|
||||
elixirc_paths: ["lib"],
|
||||
|
@ -17,7 +17,7 @@ defmodule Ejabberd.Mixfile do
|
|||
deps: deps]
|
||||
end
|
||||
|
||||
defp description do
|
||||
def description do
|
||||
"""
|
||||
Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform.
|
||||
"""
|
||||
|
@ -28,9 +28,8 @@ defmodule Ejabberd.Mixfile do
|
|||
applications: [:ssl],
|
||||
included_applications: [:lager, :mnesia, :p1_utils, :cache_tab,
|
||||
:fast_tls, :stringprep, :fast_xml,
|
||||
:stun, :fast_yaml, :ezlib, :iconv,
|
||||
:esip, :jiffy, :p1_oauth2, :eredis,
|
||||
:p1_mysql, :p1_pgsql, :sqlite3]]
|
||||
:stun, :fast_yaml, :esip, :jiffy, :p1_oauth2]
|
||||
++ cond_apps]
|
||||
end
|
||||
|
||||
defp erlc_options do
|
||||
|
@ -51,22 +50,40 @@ defmodule Ejabberd.Mixfile do
|
|||
{:esip, "~> 1.0"},
|
||||
{:jiffy, "~> 0.14.7"},
|
||||
{:p1_oauth2, "~> 0.6.1"},
|
||||
{:p1_mysql, "~> 1.0"},
|
||||
{:p1_pgsql, "~> 1.1"},
|
||||
{:sqlite3, "~> 1.1"},
|
||||
{:ezlib, "~> 1.0"},
|
||||
{:iconv, "~> 1.0"},
|
||||
{:eredis, "~> 1.0"},
|
||||
{:exrm, "~> 1.0.0", only: :dev},
|
||||
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
|
||||
# version 3.20:
|
||||
{:relx, "~> 3.21", only: :dev},
|
||||
{:ex_doc, ">= 0.0.0", only: :dev},
|
||||
{:meck, "~> 0.8.4", only: :test},
|
||||
{:moka, github: "processone/moka", tag: "1.0.5c", only: :test}]
|
||||
{:ex_doc, ">= 0.0.0", only: :dev}]
|
||||
++ cond_deps
|
||||
end
|
||||
|
||||
defp package do
|
||||
defp cond_deps do
|
||||
for {:true, dep} <- [{config(:mysql), {:p1_mysql, "~> 1.0"}},
|
||||
{config(:pgsql), {:p1_pgsql, "~> 1.1"}},
|
||||
{config(:sqlite), {:sqlite3, "~> 1.1"}},
|
||||
{config(:riak), {:riakc, "~> 2.4"}},
|
||||
{config(:redis), {:eredis, "~> 1.0"}},
|
||||
{config(:zlib), {:ezlib, "~> 1.0"}},
|
||||
{config(:iconv), {:iconv, "~> 1.0"}},
|
||||
{config(:pam), {:p1_pam, "~> 1.0"}},
|
||||
{config(:tools), {:luerl, github: "rvirding/luerl", tag: "v0.2"}},
|
||||
{config(:tools), {:meck, "~> 0.8.4"}},
|
||||
{config(:tools), {:moka, github: "processone/moka", tag: "1.0.5c"}}], do:
|
||||
dep
|
||||
end
|
||||
|
||||
defp cond_apps do
|
||||
for {:true, app} <- [{config(:redis), :eredis},
|
||||
{config(:mysql), :p1_mysql},
|
||||
{config(:pgsql), :p1_pgsql},
|
||||
{config(:sqlite), :sqlite3},
|
||||
{config(:zlib), :ezlib},
|
||||
{config(:iconv), :iconv}], do:
|
||||
app
|
||||
end
|
||||
|
||||
def package do
|
||||
[# These are the default files included in the package
|
||||
files: ["lib", "src", "priv", "mix.exs", "include", "README.md", "COPYING"],
|
||||
maintainers: ["ProcessOne"],
|
||||
|
@ -76,6 +93,21 @@ defmodule Ejabberd.Mixfile do
|
|||
"Source" => "https://github.com/processone/ejabberd",
|
||||
"ProcessOne" => "http://www.process-one.net/"}]
|
||||
end
|
||||
|
||||
def vars do
|
||||
case :file.consult("vars.config") do
|
||||
{:ok,config} -> config
|
||||
_ -> [zlib: true, iconv: true]
|
||||
end
|
||||
end
|
||||
|
||||
defp config(key) do
|
||||
case vars[key] do
|
||||
nil -> false
|
||||
value -> value
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
defmodule Mix.Tasks.Compile.Asn1 do
|
||||
|
|
13
mix.lock
13
mix.lock
|
@ -1,11 +1,10 @@
|
|||
%{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []},
|
||||
"cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
|
||||
"earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []},
|
||||
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
|
||||
"earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []},
|
||||
"erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
|
||||
"esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]},
|
||||
"ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
|
||||
"ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
|
||||
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
|
||||
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
|
||||
"fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
|
@ -16,15 +15,9 @@
|
|||
"iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
|
||||
"lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]},
|
||||
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
|
||||
"moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]},
|
||||
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
|
||||
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
|
||||
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
|
||||
"p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []},
|
||||
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
|
||||
"relx": {:hex, :relx, "3.21.0", "91e1ea9f09b4edfda8461901f4b5c5e0226e43ec161e147eeab29f7761df6eb5", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
|
||||
"samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]},
|
||||
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
|
||||
"relx": {:hex, :relx, "3.21.1", "f989dc520730efd9075e9f4debcb8ba1d7d1e86b018b0bcf45a2eb80270b4ad6", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
|
||||
"stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
|
||||
"stun": {:hex, :stun, "1.0.7", "904dc6f26a3c30c54881c4c3003699f2a4968067ee6b3aecdf9895aad02df75e", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]}}
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
{"Import Users from Dir at ","Importovat uživatele z adresáře na "}.
|
||||
{"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}.
|
||||
{"Improper message type","Nesprávný typ zprávy"}.
|
||||
{"Incoming s2s Connections:",""}.
|
||||
{"Incoming s2s Connections:","Příchozí s2s spojení:"}.
|
||||
{"Incorrect password","Nesprávné heslo"}.
|
||||
{"Invalid affiliation: ~s","Neplatné přiřazení: ~s"}.
|
||||
{"Invalid role: ~s","Neplatná role: ~s"}.
|
||||
|
@ -333,7 +333,6 @@
|
|||
{"September",". září"}.
|
||||
{"Server ~b","Server ~b"}.
|
||||
{"Server:","Server:"}.
|
||||
{"Server","Server"}.
|
||||
{"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
|
||||
{"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}.
|
||||
{"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}.
|
||||
|
@ -409,10 +408,10 @@
|
|||
{"User JID","Jabber ID uživatele"}.
|
||||
{"User Management","Správa uživatelů"}.
|
||||
{"Username:","Uživatelské jméno:"}.
|
||||
{"User ~s",""}.
|
||||
{"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}.
|
||||
{"Users Last Activity","Poslední aktivita uživatele"}.
|
||||
{"Users","Uživatelé"}.
|
||||
{"User ~s","Uživatel ~s"}.
|
||||
{"User","Uživatel"}.
|
||||
{"Validate","Ověřit"}.
|
||||
{"vCard User Search","Hledání uživatelů podle vizitek"}.
|
||||
|
|
|
@ -242,9 +242,7 @@ msgstr "Odchozí s2s spojení:"
|
|||
|
||||
#: ejabberd_web_admin.erl:1559
|
||||
msgid "Incoming s2s Connections:"
|
||||
msgstr ""
|
||||
"Příchozí\n"
|
||||
" s2s spojení:"
|
||||
msgstr "Příchozí s2s spojení:"
|
||||
|
||||
#: ejabberd_web_admin.erl:1595 ejabberd_web_admin.erl:1794
|
||||
#: ejabberd_web_admin.erl:1804 ejabberd_web_admin.erl:2214 mod_roster.erl:1429
|
||||
|
@ -258,9 +256,7 @@ msgstr "Změnit heslo"
|
|||
|
||||
#: ejabberd_web_admin.erl:1673
|
||||
msgid "User ~s"
|
||||
msgstr ""
|
||||
"Uživatel\n"
|
||||
" ~s"
|
||||
msgstr "Uživatel ~s"
|
||||
|
||||
#: ejabberd_web_admin.erl:1684
|
||||
msgid "Connected Resources:"
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
{if_var_match, db_type, mssql, {d, 'mssql'}},
|
||||
{if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
|
||||
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
|
||||
{if_version_above, "18", {d, 'STRONG_RAND_BYTES'}},
|
||||
{if_var_true, hipe, native},
|
||||
{src_dirs, [asn1, src,
|
||||
{if_var_true, tools, tools},
|
||||
|
|
38
src/acl.erl
38
src/acl.erl
|
@ -36,7 +36,8 @@
|
|||
acl_rule_verify/1, access_matches/3,
|
||||
transform_access_rules_config/1,
|
||||
parse_ip_netmask/1,
|
||||
access_rules_validator/1, shaper_rules_validator/1]).
|
||||
access_rules_validator/1, shaper_rules_validator/1,
|
||||
normalize_spec/1, resolve_access/2]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
-include("logger.hrl").
|
||||
|
@ -437,30 +438,35 @@ acl_rule_matches({node_glob, {UR, SR}}, #{usr := {U, S, _}}, _Host) ->
|
|||
acl_rule_matches(_ACL, _Data, _Host) ->
|
||||
false.
|
||||
|
||||
-spec access_matches(atom()|list(), any(), global|binary()) -> any().
|
||||
access_matches(all, _Data, _Host) ->
|
||||
allow;
|
||||
access_matches(none, _Data, _Host) ->
|
||||
deny;
|
||||
access_matches(Name, Data, Host) when is_atom(Name) ->
|
||||
resolve_access(all, _Host) ->
|
||||
all;
|
||||
resolve_access(none, _Host) ->
|
||||
none;
|
||||
resolve_access(Name, Host) when is_atom(Name) ->
|
||||
GAccess = mnesia:dirty_read(access, {Name, global}),
|
||||
LAccess =
|
||||
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
|
||||
true -> []
|
||||
end,
|
||||
if Host /= global -> mnesia:dirty_read(access, {Name, Host});
|
||||
true -> []
|
||||
end,
|
||||
case GAccess ++ LAccess of
|
||||
[] ->
|
||||
deny;
|
||||
[];
|
||||
AccessList ->
|
||||
Rules = lists:flatmap(
|
||||
lists:flatmap(
|
||||
fun(#access{rules = Rs}) ->
|
||||
Rs
|
||||
end, AccessList),
|
||||
access_rules_matches(Rules, Data, Host)
|
||||
end, AccessList)
|
||||
end;
|
||||
access_matches(Rules, Data, Host) when is_list(Rules) ->
|
||||
access_rules_matches(Rules, Data, Host).
|
||||
resolve_access(Rules, _Host) when is_list(Rules) ->
|
||||
Rules.
|
||||
|
||||
-spec access_matches(atom()|list(), any(), global|binary()) -> allow|deny.
|
||||
access_matches(Rules, Data, Host) ->
|
||||
case resolve_access(Rules, Host) of
|
||||
all -> allow;
|
||||
none -> deny;
|
||||
RRules -> access_rules_matches(RRules, Data, Host)
|
||||
end.
|
||||
|
||||
-spec access_rules_matches(list(), any(), global|binary()) -> any().
|
||||
|
||||
|
|
|
@ -112,9 +112,17 @@ produce_response(
|
|||
ProvidedSessionID /= <<"">> -> ProvidedSessionID;
|
||||
true -> jlib:now_to_utc_string(p1_time_compat:timestamp())
|
||||
end,
|
||||
case Actions of
|
||||
[] ->
|
||||
case {Actions, Status} of
|
||||
{[], completed} ->
|
||||
ActionsEls = [];
|
||||
{[], _} ->
|
||||
ActionsEls = [
|
||||
#xmlel{
|
||||
name = <<"actions">>,
|
||||
attrs = [{<<"execute">>, <<"complete">>}],
|
||||
children = [#xmlel{name = <<"complete">>}]
|
||||
}
|
||||
];
|
||||
_ ->
|
||||
case DefaultAction of
|
||||
<<"">> -> ActionsElAttrs = [];
|
||||
|
|
|
@ -87,7 +87,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
|
|||
if is_tuple(Ret) -> Ret;
|
||||
true ->
|
||||
TempSalt =
|
||||
crypto:rand_bytes(?SALT_LENGTH),
|
||||
randoms:bytes(?SALT_LENGTH),
|
||||
SaltedPassword =
|
||||
scram:salted_password(Ret,
|
||||
TempSalt,
|
||||
|
@ -101,7 +101,7 @@ mech_step(#state{step = 2} = State, ClientIn) ->
|
|||
str:substr(ClientIn,
|
||||
str:str(ClientIn, <<"n=">>)),
|
||||
ServerNonce =
|
||||
jlib:encode_base64(crypto:rand_bytes(?NONCE_LENGTH)),
|
||||
jlib:encode_base64(randoms:bytes(?NONCE_LENGTH)),
|
||||
ServerFirstMessage =
|
||||
iolist_to_binary(
|
||||
["r=",
|
||||
|
|
|
@ -105,8 +105,6 @@ start_app([], _Type, _StartFlag) ->
|
|||
ok.
|
||||
|
||||
check_app_modules(App, StartFlag) ->
|
||||
{A, B, C} = p1_time_compat:timestamp(),
|
||||
random:seed(A, B, C),
|
||||
sleep(5000),
|
||||
case application:get_key(App, modules) of
|
||||
{ok, Mods} ->
|
||||
|
@ -140,7 +138,7 @@ exit_or_halt(Reason, StartFlag) ->
|
|||
end.
|
||||
|
||||
sleep(N) ->
|
||||
timer:sleep(random:uniform(N)).
|
||||
timer:sleep(randoms:uniform(N)).
|
||||
|
||||
get_module_file(App, Mod) ->
|
||||
BaseName = atom_to_list(Mod),
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% File : ejabberd_access_permissions.erl
|
||||
%%% Author : Paweł Chmielowski <pawel@process-one.net>
|
||||
%%% Purpose : Administrative functions and commands
|
||||
%%% Created : 7 Sep 2016 by Paweł Chmielowski <pawel@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.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ejabberd_access_permissions).
|
||||
-author("pawel@process-one.net").
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("logger.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
-behavior(ejabberd_config).
|
||||
|
||||
%% API
|
||||
-export([start_link/0,
|
||||
parse_api_permissions/1,
|
||||
can_access/2,
|
||||
invalidate/0,
|
||||
opt_type/1,
|
||||
show_current_definitions/0,
|
||||
register_permission_addon/2,
|
||||
unregister_permission_addon/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
definitions = none,
|
||||
fragments_generators = []
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec can_access(atom(), map()) -> allow | deny.
|
||||
can_access(Cmd, CallerInfo) ->
|
||||
gen_server:call(?MODULE, {can_access, Cmd, CallerInfo}).
|
||||
|
||||
-spec invalidate() -> ok.
|
||||
invalidate() ->
|
||||
gen_server:cast(?MODULE, invalidate).
|
||||
|
||||
-spec register_permission_addon(atom(), fun()) -> ok.
|
||||
register_permission_addon(Name, Fun) ->
|
||||
gen_server:call(?MODULE, {register_config_fragment_generator, Name, Fun}).
|
||||
|
||||
-spec unregister_permission_addon(atom()) -> ok.
|
||||
unregister_permission_addon(Name) ->
|
||||
gen_server:call(?MODULE, {unregister_config_fragment_generator, Name}).
|
||||
|
||||
-spec show_current_definitions() -> any().
|
||||
show_current_definitions() ->
|
||||
gen_server:call(?MODULE, show_current_definitions).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
%% Starts the server
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Initializes the server
|
||||
%%
|
||||
%% @spec init(Args) -> {ok, State} |
|
||||
%% {ok, State, Timeout} |
|
||||
%% ignore |
|
||||
%% {stop, Reason}
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore.
|
||||
init([]) ->
|
||||
{ok, #state{}}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Handling call messages
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}.
|
||||
handle_call({can_access, Cmd, CallerInfo}, _From, State) ->
|
||||
CallerModule = maps:get(caller_module, CallerInfo, none),
|
||||
Host = maps:get(caller_host, CallerInfo, global),
|
||||
{State2, Defs0} = get_definitions(State),
|
||||
Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0,
|
||||
Res = lists:foldl(
|
||||
fun({Name, _} = Def, none) ->
|
||||
case matches_definition(Def, Cmd, CallerModule, Host, CallerInfo) of
|
||||
true ->
|
||||
?DEBUG("Command '~p' execution allowed by rule '~s' (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
|
||||
allow;
|
||||
_ ->
|
||||
none
|
||||
end;
|
||||
(_, Val) ->
|
||||
Val
|
||||
end, none, Defs),
|
||||
Res2 = case Res of
|
||||
allow -> allow;
|
||||
_ ->
|
||||
?DEBUG("Command '~p' execution denied (CallerInfo=~p)", [Cmd, CallerInfo]),
|
||||
deny
|
||||
end,
|
||||
{reply, Res2, State2};
|
||||
handle_call(show_current_definitions, _From, State) ->
|
||||
{State2, Defs} = get_definitions(State),
|
||||
{reply, Defs, State2};
|
||||
handle_call({register_config_fragment_generator, Name, Fun}, _From, #state{fragments_generators = Gens} = State) ->
|
||||
NGens = lists:keystore(Name, 1, Gens, {Name, Fun}),
|
||||
{reply, ok, State#state{fragments_generators = NGens}};
|
||||
handle_call({unregister_config_fragment_generator, Name}, _From, #state{fragments_generators = Gens} = State) ->
|
||||
NGens = lists:keydelete(Name, 1, Gens),
|
||||
{reply, ok, State#state{fragments_generators = NGens}};
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Handling cast messages
|
||||
%%
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}.
|
||||
handle_cast(invalidate, State) ->
|
||||
{noreply, State#state{definitions = none}};
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Handling all non call/cast messages
|
||||
%%
|
||||
%% @spec handle_info(Info, State) -> {noreply, State} |
|
||||
%% {noreply, State, Timeout} |
|
||||
%% {stop, Reason, State}
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}.
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
%%
|
||||
%% @spec terminate(Reason, State) -> void()
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term().
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @private
|
||||
%% @doc
|
||||
%% Convert process state when code is changed
|
||||
%%
|
||||
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
|
||||
%% @end
|
||||
%%--------------------------------------------------------------------
|
||||
-spec code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}.
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_definitions(#state{}) -> {#state{}, any()}.
|
||||
get_definitions(#state{definitions = Defs, fragments_generators = Gens} = State) ->
|
||||
DefaultOptions = [{<<"console commands">>,
|
||||
{[ejabberd_ctl],
|
||||
[{acl, all}],
|
||||
{all, none}}},
|
||||
{<<"admin access">>,
|
||||
{[],
|
||||
[{acl, admin}],
|
||||
{all, [start, stop]}}}],
|
||||
NDefs = case Defs of
|
||||
none ->
|
||||
ApiPerms = ejabberd_config:get_option(api_permissions, fun(A) -> A end, DefaultOptions),
|
||||
AllCommands = ejabberd_commands:get_commands_definition(),
|
||||
Frags = lists:foldl(
|
||||
fun({_Name, Generator}, Acc) ->
|
||||
Acc ++ Generator()
|
||||
end, [], Gens),
|
||||
lists:map(
|
||||
fun({Name, {From, Who, {Add, Del}}}) ->
|
||||
Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
|
||||
{Name, {From, Who, Cmds}}
|
||||
end, ApiPerms ++ Frags);
|
||||
V ->
|
||||
V
|
||||
end,
|
||||
{State#state{definitions = NDefs}, NDefs}.
|
||||
|
||||
matches_definition({_Name, {From, Who, What}}, Cmd, Module, Host, CallerInfo) ->
|
||||
case What == all orelse lists:member(Cmd, What) of
|
||||
true ->
|
||||
case From == [] orelse lists:member(Module, From) of
|
||||
true ->
|
||||
Scope = maps:get(oauth_scope, CallerInfo, none),
|
||||
lists:any(
|
||||
fun({access, Access}) when Scope == none ->
|
||||
acl:access_matches(Access, CallerInfo, Host) == allow;
|
||||
({acl, _} = Acl) when Scope == none ->
|
||||
acl:acl_rule_matches(Acl, CallerInfo, Host);
|
||||
({oauth, Scopes, List}) when Scope /= none ->
|
||||
case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of
|
||||
true ->
|
||||
lists:any(
|
||||
fun({access, Access}) ->
|
||||
acl:access_matches(Access, CallerInfo, Host) == allow;
|
||||
({acl, _} = Acl) ->
|
||||
acl:acl_rule_matches(Acl, CallerInfo, Host)
|
||||
end, List);
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
(_) ->
|
||||
false
|
||||
end, Who);
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
filter_commands_with_permissions(AllCommands, Add, Del) ->
|
||||
CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
|
||||
CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []),
|
||||
lists:map(fun(#ejabberd_commands{name = N}) -> N end,
|
||||
CommandsAdd -- CommandsDel).
|
||||
|
||||
filter_commands_with_patterns([], _Patterns, Acc) ->
|
||||
Acc;
|
||||
filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
|
||||
case command_matches_patterns(C, Patterns) of
|
||||
true ->
|
||||
filter_commands_with_patterns(CRest, Patterns, [C | Acc]);
|
||||
_ ->
|
||||
filter_commands_with_patterns(CRest, Patterns, Acc)
|
||||
end.
|
||||
|
||||
command_matches_patterns(_, all) ->
|
||||
true;
|
||||
command_matches_patterns(_, none) ->
|
||||
false;
|
||||
command_matches_patterns(_, []) ->
|
||||
false;
|
||||
command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) ->
|
||||
case lists:member(Tag, Tags) of
|
||||
true ->
|
||||
true;
|
||||
_ ->
|
||||
command_matches_patterns(C, Tail)
|
||||
end;
|
||||
command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
|
||||
true;
|
||||
command_matches_patterns(C, [_ | Tail]) ->
|
||||
command_matches_patterns(C, Tail).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Options parsing code
|
||||
%%%===================================================================
|
||||
|
||||
parse_api_permissions(Data) when is_list(Data) ->
|
||||
throw({replace_with, [parse_api_permission(Name, Args) || {Name, Args} <- Data]}).
|
||||
|
||||
parse_api_permission(Name, Args) ->
|
||||
{From, Who, What} = case key_split(Args, [{from, []}, {who, none}, {what, []}]) of
|
||||
{error, Msg} ->
|
||||
report_error(<<"~s inside api_permission '~s' section">>, [Msg, Name]);
|
||||
Val -> Val
|
||||
end,
|
||||
{Name, {parse_from(Name, From), parse_who(Name, Who, oauth), parse_what(Name, What)}}.
|
||||
|
||||
parse_from(_Name, Module) when is_atom(Module) ->
|
||||
[Module];
|
||||
parse_from(Name, Modules) when is_list(Modules) ->
|
||||
lists:foreach(fun(Module) when is_atom(Module) ->
|
||||
ok;
|
||||
(Val) ->
|
||||
report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
|
||||
[Val, Name])
|
||||
end, Modules),
|
||||
Modules;
|
||||
parse_from(Name, Val) ->
|
||||
report_error(<<"Invalid value '~p' used inside 'from' section for api_permission '~s'">>,
|
||||
[Val, Name]).
|
||||
|
||||
parse_who(Name, Atom, ParseOauth) when is_atom(Atom) ->
|
||||
parse_who(Name, [Atom], ParseOauth);
|
||||
parse_who(Name, Defs, ParseOauth) when is_list(Defs) ->
|
||||
lists:map(
|
||||
fun([{access, Val}]) ->
|
||||
try acl:access_rules_validator(Val) of
|
||||
Rule ->
|
||||
{access, Rule}
|
||||
catch
|
||||
throw:{invalid_syntax, Msg} ->
|
||||
report_error(<<"Invalid access rule: '~s' used inside 'who' section for api_permission '~s'">>,
|
||||
[Msg, Name]);
|
||||
throw:{replace_with, NVal} ->
|
||||
{access, NVal};
|
||||
error:_ ->
|
||||
report_error(<<"Invalid access rule '~p' used inside 'who' section for api_permission '~s'">>,
|
||||
[Val, Name])
|
||||
end;
|
||||
([{oauth, OauthList}]) when is_list(OauthList) ->
|
||||
case ParseOauth of
|
||||
oauth ->
|
||||
Nested = parse_who(Name, lists:flatten(OauthList), scope),
|
||||
{Scopes, Rest} = lists:partition(
|
||||
fun({scope, _}) -> true;
|
||||
(_) -> false
|
||||
end, Nested),
|
||||
case Scopes of
|
||||
[] ->
|
||||
report_error(<<"Oauth rule must contain at least one scope rule in 'who' section for api_permission '~s'">>,
|
||||
[Name]);
|
||||
_ ->
|
||||
{oauth, lists:foldl(fun({scope, S}, A) -> S ++ A end, [], Scopes), Rest}
|
||||
end;
|
||||
scope ->
|
||||
report_error(<<"Oauth rule can't be embeded inside other oauth rule in 'who' section for api_permission '~s'">>,
|
||||
[Name])
|
||||
end;
|
||||
({scope, ScopeList}) ->
|
||||
case ParseOauth of
|
||||
oauth ->
|
||||
report_error(<<"Scope can be included only inside oauth rule in 'who' section for api_permission '~s'">>,
|
||||
[Name]);
|
||||
scope ->
|
||||
ScopeList2 = case ScopeList of
|
||||
V when is_binary(V) -> [V];
|
||||
V2 when is_list(V2) -> V2;
|
||||
V3 ->
|
||||
report_error(<<"Invalid value for scope '~p' in 'who' section for api_permission '~s'">>,
|
||||
[V3, Name])
|
||||
end,
|
||||
{scope, ScopeList2}
|
||||
end;
|
||||
(Atom) when is_atom(Atom) ->
|
||||
{acl, Atom};
|
||||
([Other]) ->
|
||||
try acl:normalize_spec(Other) of
|
||||
Rule2 ->
|
||||
{acl, Rule2}
|
||||
catch
|
||||
_:_ ->
|
||||
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
|
||||
[Other, Name])
|
||||
end;
|
||||
(Invalid) ->
|
||||
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
|
||||
[Invalid, Name])
|
||||
end, Defs);
|
||||
parse_who(Name, Val, _ParseOauth) ->
|
||||
report_error(<<"Invalid value '~p' used inside 'who' section for api_permission '~s'">>,
|
||||
[Val, Name]).
|
||||
|
||||
parse_what(Name, Binary) when is_binary(Binary) ->
|
||||
parse_what(Name, [Binary]);
|
||||
parse_what(Name, Defs) when is_list(Defs) ->
|
||||
{A, D} = lists:foldl(
|
||||
fun(Def, {Add, Del}) ->
|
||||
case parse_single_what(Def) of
|
||||
{error, Err} ->
|
||||
report_error(<<"~s used in value '~p' in 'what' section for api_permission '~s'">>,
|
||||
[Err, Def, Name]);
|
||||
all ->
|
||||
{case Add of none -> none; _ -> all end, Del};
|
||||
{neg, all} ->
|
||||
{none, all};
|
||||
{neg, Value} ->
|
||||
{Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end};
|
||||
Value ->
|
||||
{case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
|
||||
end
|
||||
end, {[], []}, Defs),
|
||||
case {A, D} of
|
||||
{[], _} ->
|
||||
{none, all};
|
||||
{A2, []} ->
|
||||
{A2, none};
|
||||
V ->
|
||||
V
|
||||
end;
|
||||
parse_what(Name, Val) ->
|
||||
report_error(<<"Invalid value '~p' used inside 'what' section for api_permission '~s'">>,
|
||||
[Val, Name]).
|
||||
|
||||
parse_single_what(<<"*">>) ->
|
||||
all;
|
||||
parse_single_what(<<"!*">>) ->
|
||||
{neg, all};
|
||||
parse_single_what(<<"!", Rest/binary>>) ->
|
||||
case parse_single_what(Rest) of
|
||||
{neg, _} ->
|
||||
{error, <<"Double negation">>};
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
V ->
|
||||
{neg, V}
|
||||
end;
|
||||
parse_single_what(<<"[tag:", Rest/binary>>) ->
|
||||
case binary:split(Rest, <<"]">>) of
|
||||
[TagName, <<"">>] ->
|
||||
case parse_single_what(TagName) of
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
V when is_atom(V) ->
|
||||
{tag, V};
|
||||
_ ->
|
||||
{error, <<"Invalid tag">>}
|
||||
end;
|
||||
_ ->
|
||||
{error, <<"Invalid tag">>}
|
||||
end;
|
||||
parse_single_what(Binary) when is_binary(Binary) ->
|
||||
case is_valid_command_name(Binary) of
|
||||
true ->
|
||||
binary_to_atom(Binary, latin1);
|
||||
_ ->
|
||||
{error, <<"Invalid value">>}
|
||||
end;
|
||||
parse_single_what(_) ->
|
||||
{error, <<"Invalid value">>}.
|
||||
|
||||
is_valid_command_name(<<>>) ->
|
||||
false;
|
||||
is_valid_command_name(Val) ->
|
||||
is_valid_command_name2(Val).
|
||||
|
||||
is_valid_command_name2(<<>>) ->
|
||||
true;
|
||||
is_valid_command_name2(<<K:8, Rest/binary>>) when K >= $a andalso K =< $z orelse K == $_ ->
|
||||
is_valid_command_name2(Rest);
|
||||
is_valid_command_name2(_) ->
|
||||
false.
|
||||
|
||||
key_split(Args, Fields) ->
|
||||
{_, Order1, Results1, Required1} = lists:foldl(
|
||||
fun({Field, Default}, {Idx, Order, Results, Required}) ->
|
||||
{Idx + 1, maps:put(Field, Idx, Order), [Default | Results], Required};
|
||||
(Field, {Idx, Order, Results, Required}) ->
|
||||
{Idx + 1, maps:put(Field, Idx, Order), [none | Results], maps:put(Field, 1, Required)}
|
||||
end, {1, #{}, [], #{}}, Fields),
|
||||
key_split(Args, list_to_tuple(Results1), Order1, Required1, #{}).
|
||||
|
||||
key_split([], _Results, _Order, Required, _Duplicates) when map_size(Required) > 0 ->
|
||||
parse_error(<<"Missing fields '~s">>, [str:join(maps:keys(Required), <<", ">>)]);
|
||||
key_split([], Results, _Order, _Required, _Duplicates) ->
|
||||
Results;
|
||||
key_split([{Arg, Value} | Rest], Results, Order, Required, Duplicates) ->
|
||||
case maps:find(Arg, Order) of
|
||||
{ok, Idx} ->
|
||||
case maps:is_key(Arg, Duplicates) of
|
||||
false ->
|
||||
Results2 = setelement(Idx, Results, Value),
|
||||
key_split(Rest, Results2, Order, maps:remove(Arg, Required), maps:put(Arg, 1, Duplicates));
|
||||
true ->
|
||||
parse_error(<<"Duplicate field '~s'">>, [Arg])
|
||||
end;
|
||||
_ ->
|
||||
parse_error(<<"Unknown field '~s'">>, [Arg])
|
||||
end.
|
||||
|
||||
report_error(Format, Args) ->
|
||||
throw({invalid_syntax, iolist_to_binary(io_lib:format(Format, Args))}).
|
||||
|
||||
parse_error(Format, Args) ->
|
||||
{error, iolist_to_binary(io_lib:format(Format, Args))}.
|
||||
|
||||
opt_type(api_permissions) ->
|
||||
fun parse_api_permissions/1;
|
||||
opt_type(_) ->
|
||||
[api_permissions].
|
|
@ -404,7 +404,8 @@ registered_vhosts() ->
|
|||
reload_config() ->
|
||||
ejabberd_config:reload_file(),
|
||||
acl:start(),
|
||||
shaper:start().
|
||||
shaper:start(),
|
||||
ejabberd_access_permissions:invalidate().
|
||||
|
||||
%%%
|
||||
%%% Cluster management
|
||||
|
|
|
@ -51,6 +51,7 @@ start(normal, _Args) ->
|
|||
db_init(),
|
||||
start(),
|
||||
translate:start(),
|
||||
ejabberd_access_permissions:start_link(),
|
||||
ejabberd_ctl:init(),
|
||||
ejabberd_commands:init(),
|
||||
ejabberd_admin:start(),
|
||||
|
|
|
@ -450,7 +450,7 @@ password_to_scram(Password) ->
|
|||
?SCRAM_DEFAULT_ITERATION_COUNT).
|
||||
|
||||
password_to_scram(Password, IterationCount) ->
|
||||
Salt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
Salt = randoms:bytes(?SALT_LENGTH),
|
||||
SaltedPassword = scram:salted_password(Password, Salt,
|
||||
IterationCount),
|
||||
StoredKey =
|
||||
|
|
|
@ -270,7 +270,7 @@ password_to_scram(Password) ->
|
|||
?SCRAM_DEFAULT_ITERATION_COUNT).
|
||||
|
||||
password_to_scram(Password, IterationCount) ->
|
||||
Salt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
Salt = randoms:bytes(?SALT_LENGTH),
|
||||
SaltedPassword = scram:salted_password(Password, Salt,
|
||||
IterationCount),
|
||||
StoredKey =
|
||||
|
|
|
@ -406,7 +406,7 @@ password_to_scram(Password) ->
|
|||
?SCRAM_DEFAULT_ITERATION_COUNT).
|
||||
|
||||
password_to_scram(Password, IterationCount) ->
|
||||
Salt = crypto:rand_bytes(?SALT_LENGTH),
|
||||
Salt = randoms:bytes(?SALT_LENGTH),
|
||||
SaltedPassword = scram:salted_password(Password, Salt,
|
||||
IterationCount),
|
||||
StoredKey =
|
||||
|
|
|
@ -2998,10 +2998,13 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F)
|
|||
lists:foreach(
|
||||
fun({_, Time, #xmlel{attrs = Attrs} = El}) ->
|
||||
From_s = fxml:get_attr_s(<<"from">>, Attrs),
|
||||
From = jid:from_string(From_s),
|
||||
To_s = fxml:get_attr_s(<<"to">>, Attrs),
|
||||
To = jid:from_string(To_s),
|
||||
F(From, To, El, Time)
|
||||
case {jid:from_string(From_s), jid:from_string(To_s)} of
|
||||
{#jid{} = From, #jid{} = To} ->
|
||||
F(From, To, El, Time);
|
||||
{_, _} ->
|
||||
?DEBUG("Dropping stanza due to invalid JID(s)", [])
|
||||
end
|
||||
end, queue:to_list(Queue))
|
||||
end;
|
||||
handle_unacked_stanzas(_StateData, _F) ->
|
||||
|
|
|
@ -218,23 +218,26 @@
|
|||
get_command_format/1,
|
||||
get_command_format/2,
|
||||
get_command_format/3,
|
||||
get_command_policy_and_scope/1,
|
||||
get_command_policy_and_scope/1,
|
||||
get_command_definition/1,
|
||||
get_command_definition/2,
|
||||
get_tags_commands/0,
|
||||
get_tags_commands/1,
|
||||
get_exposed_commands/0,
|
||||
get_exposed_commands/0,
|
||||
register_commands/1,
|
||||
unregister_commands/1,
|
||||
expose_commands/1,
|
||||
unregister_commands/1,
|
||||
expose_commands/1,
|
||||
execute_command/2,
|
||||
execute_command/3,
|
||||
execute_command/4,
|
||||
execute_command/5,
|
||||
execute_command/6,
|
||||
opt_type/1,
|
||||
get_commands_spec/0
|
||||
]).
|
||||
opt_type/1,
|
||||
get_commands_spec/0,
|
||||
get_commands_definition/0,
|
||||
get_commands_definition/1,
|
||||
execute_command2/3,
|
||||
execute_command2/4]).
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("ejabberd.hrl").
|
||||
|
@ -280,7 +283,8 @@ init() ->
|
|||
{attributes, record_info(fields, ejabberd_commands)},
|
||||
{type, bag}]),
|
||||
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
|
||||
register_commands(get_commands_spec()).
|
||||
register_commands(get_commands_spec()),
|
||||
ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0).
|
||||
|
||||
-spec register_commands([ejabberd_commands()]) -> ok.
|
||||
|
||||
|
@ -296,7 +300,9 @@ register_commands(Commands) ->
|
|||
mnesia:dirty_write(Command)
|
||||
%% ?DEBUG("This command is already defined:~n~p", [Command])
|
||||
end,
|
||||
Commands).
|
||||
Commands),
|
||||
ejabberd_access_permissions:invalidate(),
|
||||
ok.
|
||||
|
||||
-spec unregister_commands([ejabberd_commands()]) -> ok.
|
||||
|
||||
|
@ -306,7 +312,9 @@ unregister_commands(Commands) ->
|
|||
fun(Command) ->
|
||||
mnesia:dirty_delete_object(Command)
|
||||
end,
|
||||
Commands).
|
||||
Commands),
|
||||
ejabberd_access_permissions:invalidate(),
|
||||
ok.
|
||||
|
||||
%% @doc Expose command through ejabberd ReST API.
|
||||
%% Pass a list of command names or policy to expose.
|
||||
|
@ -427,6 +435,9 @@ get_command_definition(Name, Version) ->
|
|||
_E -> throw({error, unknown_command})
|
||||
end.
|
||||
|
||||
get_commands_definition() ->
|
||||
get_commands_definition(?DEFAULT_VERSION).
|
||||
|
||||
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
||||
|
||||
% @doc Returns all commands for a given API version
|
||||
|
@ -448,6 +459,18 @@ get_commands_definition(Version) ->
|
|||
end,
|
||||
lists:foldl(F, [], L).
|
||||
|
||||
execute_command2(Name, Arguments, CallerInfo) ->
|
||||
execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION).
|
||||
|
||||
execute_command2(Name, Arguments, CallerInfo, Version) ->
|
||||
Command = get_command_definition(Name, Version),
|
||||
case ejabberd_access_permissions:can_access(Name, CallerInfo) of
|
||||
allow ->
|
||||
do_execute_command(Command, Arguments);
|
||||
_ ->
|
||||
throw({error, access_rules_unauthorized})
|
||||
end.
|
||||
|
||||
%% @spec (Name::atom(), Arguments) -> ResultTerm
|
||||
%% where
|
||||
%% Arguments = [any()]
|
||||
|
@ -811,6 +834,8 @@ is_admin(_Name, admin, _Extra) ->
|
|||
true;
|
||||
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
|
||||
false;
|
||||
is_admin(_Name, Map, _extra) when is_map(Map) ->
|
||||
true;
|
||||
is_admin(Name, Auth, Extra) ->
|
||||
{ACLInfo, Server} = case Auth of
|
||||
{U, S, _, _} ->
|
||||
|
@ -832,6 +857,14 @@ is_admin(Name, Auth, Extra) ->
|
|||
deny -> false
|
||||
end.
|
||||
|
||||
permission_addon() ->
|
||||
[{<<"'commands' option compatibility shim">>,
|
||||
{[],
|
||||
[{access, ejabberd_config:get_option(commands_admin_access,
|
||||
fun(V) -> V end,
|
||||
none)}],
|
||||
{get_exposed_commands(), []}}}].
|
||||
|
||||
opt_type(commands_admin_access) -> fun acl:access_rules_validator/1;
|
||||
opt_type(commands) ->
|
||||
fun(V) when is_list(V) -> V end;
|
||||
|
|
|
@ -29,16 +29,16 @@
|
|||
-export([start/0, load_file/1, reload_file/0, read_file/1,
|
||||
add_global_option/2, add_local_option/2,
|
||||
get_global_option/2, get_local_option/2,
|
||||
get_global_option/3, get_local_option/3,
|
||||
get_option/2, get_option/3, add_option/2, has_option/1,
|
||||
get_vh_by_auth_method/1, is_file_readable/1,
|
||||
get_version/0, get_myhosts/0, get_mylang/0,
|
||||
get_ejabberd_config_path/0, is_using_elixir_config/0,
|
||||
prepare_opt_val/4, convert_table_to_binary/5,
|
||||
transform_options/1, collect_options/1, default_db/2,
|
||||
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
|
||||
env_binary_to_list/2, opt_type/1, may_hide_data/1,
|
||||
is_elixir_enabled/0]).
|
||||
get_global_option/3, get_local_option/3,
|
||||
get_option/2, get_option/3, add_option/2, has_option/1,
|
||||
get_vh_by_auth_method/1, is_file_readable/1,
|
||||
get_version/0, get_myhosts/0, get_mylang/0,
|
||||
get_ejabberd_config_path/0, is_using_elixir_config/0,
|
||||
prepare_opt_val/4, convert_table_to_binary/5,
|
||||
transform_options/1, collect_options/1, default_db/2,
|
||||
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
|
||||
env_binary_to_list/2, opt_type/1, may_hide_data/1,
|
||||
is_elixir_enabled/0, v_dbs/1, v_dbs_mods/1]).
|
||||
|
||||
-export([start/2]).
|
||||
|
||||
|
@ -178,7 +178,8 @@ read_file(File, Opts) ->
|
|||
-spec load_file(string()) -> ok.
|
||||
|
||||
load_file(File) ->
|
||||
State = read_file(File),
|
||||
State0 = read_file(File),
|
||||
State = validate_opts(State0),
|
||||
set_opts(State).
|
||||
|
||||
-spec reload_file() -> ok.
|
||||
|
@ -892,6 +893,19 @@ v_db(Mod, Type) ->
|
|||
[] -> erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-spec v_dbs(module()) -> [atom()].
|
||||
|
||||
v_dbs(Mod) ->
|
||||
lists:flatten(ets:match(module_db, {Mod, '$1'})).
|
||||
|
||||
-spec v_dbs_mods(module()) -> [module()].
|
||||
|
||||
v_dbs_mods(Mod) ->
|
||||
lists:map(fun([M]) ->
|
||||
binary_to_atom(<<(atom_to_binary(Mod, utf8))/binary, "_",
|
||||
(atom_to_binary(M, utf8))/binary>>, utf8)
|
||||
end, ets:match(module_db, {Mod, '$1'})).
|
||||
|
||||
-spec default_db(binary(), module()) -> atom().
|
||||
|
||||
default_db(Host, Module) ->
|
||||
|
|
|
@ -321,10 +321,15 @@ call_command([CmdString | Args], Auth, AccessCommands, Version) ->
|
|||
{ArgsFormat, ResultFormat} ->
|
||||
case (catch format_args(Args, ArgsFormat)) of
|
||||
ArgsFormatted when is_list(ArgsFormatted) ->
|
||||
Result = ejabberd_commands:execute_command(AccessCommands,
|
||||
Auth, Command,
|
||||
ArgsFormatted,
|
||||
Version),
|
||||
CI = case Auth of
|
||||
{U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
|
||||
_ -> #{}
|
||||
end,
|
||||
CI2 = CI#{caller_module => ?MODULE},
|
||||
Result = ejabberd_commands:execute_command2(Command,
|
||||
ArgsFormatted,
|
||||
CI2,
|
||||
Version),
|
||||
format_result(Result, ResultFormat);
|
||||
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
||||
{NumCompa, TextCompa} =
|
||||
|
|
|
@ -42,8 +42,10 @@
|
|||
associate_access_code/3,
|
||||
associate_access_token/3,
|
||||
associate_refresh_token/3,
|
||||
check_token/1,
|
||||
check_token/4,
|
||||
check_token/2,
|
||||
scope_in_scope_list/2,
|
||||
process/2,
|
||||
opt_type/1]).
|
||||
|
||||
|
@ -305,6 +307,29 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) ->
|
|||
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
|
||||
{ok, AppContext}.
|
||||
|
||||
scope_in_scope_list(Scope, ScopeList) ->
|
||||
TokenScopeSet = oauth2_priv_set:new(Scope),
|
||||
lists:any(fun(Scope2) ->
|
||||
oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
|
||||
ScopeList).
|
||||
|
||||
check_token(Token) ->
|
||||
case lookup(Token) of
|
||||
{ok, #oauth_token{us = US,
|
||||
scope = TokenScope,
|
||||
expire = Expire}} ->
|
||||
{MegaSecs, Secs, _} = os:timestamp(),
|
||||
TS = 1000000 * MegaSecs + Secs,
|
||||
if
|
||||
Expire > TS ->
|
||||
{ok, US, TokenScope};
|
||||
true ->
|
||||
{false, expired}
|
||||
end;
|
||||
_ ->
|
||||
{false, not_found}
|
||||
end.
|
||||
|
||||
check_token(User, Server, ScopeList, Token) ->
|
||||
LUser = jid:nodeprep(User),
|
||||
LServer = jid:nameprep(Server),
|
||||
|
|
|
@ -1099,13 +1099,12 @@ get_addr_port(Server) ->
|
|||
?DEBUG("srv lookup of '~s': ~p~n",
|
||||
[Server, HEnt#hostent.h_addr_list]),
|
||||
AddrList = HEnt#hostent.h_addr_list,
|
||||
random:seed(p1_time_compat:timestamp()),
|
||||
case catch lists:map(fun ({Priority, Weight, Port,
|
||||
Host}) ->
|
||||
N = case Weight of
|
||||
0 -> 0;
|
||||
_ ->
|
||||
(Weight + 1) * random:uniform()
|
||||
(Weight + 1) * randoms:uniform()
|
||||
end,
|
||||
{Priority * 65536 - N, Host, Port}
|
||||
end,
|
||||
|
|
|
@ -135,13 +135,13 @@ init([{SockMod, Socket}, Opts]) ->
|
|||
fun({H, Os}, D) ->
|
||||
P = proplists:get_value(
|
||||
password, Os,
|
||||
p1_sha:sha(crypto:rand_bytes(20))),
|
||||
p1_sha:sha(randoms:bytes(20))),
|
||||
dict:store(H, P, D)
|
||||
end, dict:new(), HOpts);
|
||||
false ->
|
||||
Pass = proplists:get_value(
|
||||
password, Opts,
|
||||
p1_sha:sha(crypto:rand_bytes(20))),
|
||||
p1_sha:sha(randoms:bytes(20))),
|
||||
dict:from_list([{global, Pass}])
|
||||
end,
|
||||
%% privilege access to entities data
|
||||
|
|
|
@ -713,10 +713,18 @@ get_resource_sessions(User, Server, Resource) ->
|
|||
|
||||
check_max_sessions(LUser, LServer) ->
|
||||
Mod = get_sm_backend(LServer),
|
||||
SIDs = [S#session.sid || S <- online(Mod:get_sessions(LUser, LServer))],
|
||||
Ss = Mod:get_sessions(LUser, LServer),
|
||||
{OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
|
||||
MaxSessions = get_max_user_sessions(LUser, LServer),
|
||||
if length(SIDs) =< MaxSessions -> ok;
|
||||
true -> {_, Pid} = lists:min(SIDs), Pid ! replaced
|
||||
if length(OnlineSs) =< MaxSessions -> ok;
|
||||
true ->
|
||||
#session{sid = {_, Pid}} = lists:min(OnlineSs),
|
||||
Pid ! replaced
|
||||
end,
|
||||
if length(OfflineSs) =< MaxSessions -> ok;
|
||||
true ->
|
||||
#session{sid = SID, usr = {_, _, R}} = lists:min(OfflineSs),
|
||||
Mod:delete_session(LUser, LServer, R, SID)
|
||||
end.
|
||||
|
||||
%% Get the user_max_session setting
|
||||
|
|
|
@ -629,7 +629,7 @@ generic_sql_query_format(SQLQuery) ->
|
|||
|
||||
generic_escape() ->
|
||||
#sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end,
|
||||
integer = fun(X) -> integer_to_binary(X) end,
|
||||
integer = fun(X) -> jlib:i2l(X) end,
|
||||
boolean = fun(true) -> <<"1">>;
|
||||
(false) -> <<"0">>
|
||||
end
|
||||
|
@ -646,7 +646,7 @@ sqlite_sql_query_format(SQLQuery) ->
|
|||
|
||||
sqlite_escape() ->
|
||||
#sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end,
|
||||
integer = fun(X) -> integer_to_binary(X) end,
|
||||
integer = fun(X) -> jlib:i2l(X) end,
|
||||
boolean = fun(true) -> <<"1">>;
|
||||
(false) -> <<"0">>
|
||||
end
|
||||
|
@ -670,7 +670,7 @@ pgsql_prepare(SQLQuery, State) ->
|
|||
|
||||
pgsql_execute_escape() ->
|
||||
#sql_escape{string = fun(X) -> X end,
|
||||
integer = fun(X) -> [integer_to_binary(X)] end,
|
||||
integer = fun(X) -> [jlib:i2l(X)] end,
|
||||
boolean = fun(true) -> "1";
|
||||
(false) -> "0"
|
||||
end
|
||||
|
|
|
@ -1552,7 +1552,7 @@ su_to_list({Server, User}) ->
|
|||
%%%% get_stats
|
||||
|
||||
get_stats(global, Lang) ->
|
||||
OnlineUsers = mnesia:table_info(session, size),
|
||||
OnlineUsers = ejabberd_sm:connected_users_number(),
|
||||
RegisteredUsers = lists:foldl(fun (Host, Total) ->
|
||||
ejabberd_auth:get_vh_registered_users_number(Host)
|
||||
+ Total
|
||||
|
@ -2178,7 +2178,7 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) ->
|
|||
CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]),
|
||||
CPUTimeS = list_to_binary(io_lib:format("~.3f",
|
||||
[element(1, CPUTime) / 1000])),
|
||||
OnlineUsers = mnesia:table_info(session, size),
|
||||
OnlineUsers = ejabberd_sm:connected_users_number(),
|
||||
TransactionsCommitted = ejabberd_cluster:call(Node, mnesia,
|
||||
system_info, [transaction_commits]),
|
||||
TransactionsAborted = ejabberd_cluster:call(Node, mnesia,
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
-record(state,
|
||||
{access_commands = [] :: list(),
|
||||
auth = noauth :: noauth | {binary(), binary(), binary()},
|
||||
get_auth = true :: boolean()}).
|
||||
get_auth = true :: boolean(),
|
||||
ip :: inet:ip_address()}).
|
||||
|
||||
%% Test:
|
||||
|
||||
|
@ -195,7 +196,7 @@ socket_type() -> raw.
|
|||
%% -----------------------------
|
||||
%% HTTP interface
|
||||
%% -----------------------------
|
||||
process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
|
||||
process(_, #request{method = 'POST', data = Data, opts = Opts, ip = {IP, _}}) ->
|
||||
AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts,
|
||||
fun(L) when is_list(L) -> L end,
|
||||
undefined),
|
||||
|
@ -206,7 +207,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
|
|||
lists:flatmap(
|
||||
fun({Ac, AcOpts}) ->
|
||||
Commands = gen_mod:get_opt(
|
||||
commands, AcOpts,
|
||||
commands, lists:flatten(AcOpts),
|
||||
fun(A) when is_atom(A) ->
|
||||
A;
|
||||
(L) when is_list(L) ->
|
||||
|
@ -219,15 +220,15 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) ->
|
|||
options, AcOpts,
|
||||
fun(L) when is_list(L) -> L end,
|
||||
[]),
|
||||
[{Ac, Commands, CommOpts}];
|
||||
[{<<"ejabberd_xmlrpc compatibility shim">>, {[?MODULE], [{access, Ac}], Commands}}];
|
||||
(Wrong) ->
|
||||
?WARNING_MSG("wrong options format for ~p: ~p",
|
||||
[?MODULE, Wrong]),
|
||||
[]
|
||||
end, AccessCommandsOpts)
|
||||
end, lists:flatten(AccessCommandsOpts))
|
||||
end,
|
||||
GetAuth = true,
|
||||
State = #state{access_commands = AccessCommands, get_auth = GetAuth},
|
||||
State = #state{access_commands = AccessCommands, get_auth = GetAuth, ip = IP},
|
||||
case fxml_stream:parse_element(Data) of
|
||||
{error, _} ->
|
||||
{400, [],
|
||||
|
@ -258,21 +259,35 @@ process(_, _) ->
|
|||
%% Access verification
|
||||
%% -----------------------------
|
||||
|
||||
get_auth(AuthList) ->
|
||||
Admin =
|
||||
case lists:keysearch(admin, 1, AuthList) of
|
||||
{value, {admin, true}} -> true;
|
||||
_ -> false
|
||||
end,
|
||||
extract_auth(AuthList) ->
|
||||
?DEBUG("AUTHLIST ~p", [AuthList]),
|
||||
try get_attrs([user, server, token], AuthList) of
|
||||
[U, S, T] -> {U, S, {oauth, T}, Admin}
|
||||
[U0, S0, T] ->
|
||||
U = jid:nodeprep(U0),
|
||||
S = jid:nameprep(S0),
|
||||
case ejabberd_oauth:check_token(T) of
|
||||
{ok, {U, S}, Scope} ->
|
||||
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
|
||||
{false, Reason} ->
|
||||
{error, Reason};
|
||||
_ ->
|
||||
{error, not_found}
|
||||
end
|
||||
catch
|
||||
exit:{attribute_not_found, _Attr, _} ->
|
||||
try get_attrs([user, server, password], AuthList) of
|
||||
[U, S, P] -> {U, S, P, Admin}
|
||||
[U0, S0, P] ->
|
||||
U = jid:nodeprep(U0),
|
||||
S = jid:nameprep(S0),
|
||||
case ejabberd_auth:check_password(U, <<"">>, S, P) of
|
||||
true ->
|
||||
#{usr => {U, S, <<"">>}, caller_server => S};
|
||||
false ->
|
||||
{error, invalid_auth}
|
||||
end
|
||||
catch
|
||||
exit:{attribute_not_found, Attr, _} ->
|
||||
throw({error, missing_auth_arguments, Attr})
|
||||
exit:{attribute_not_found, _Attr, _} ->
|
||||
#{}
|
||||
end
|
||||
end.
|
||||
|
||||
|
@ -300,12 +315,28 @@ get_auth(AuthList) ->
|
|||
%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}).
|
||||
%% {ok,{response,[152]}}
|
||||
|
||||
handler(#state{get_auth = true, auth = noauth} = State,
|
||||
handler(#state{get_auth = true, auth = noauth, ip = IP} = State,
|
||||
{call, Method,
|
||||
[{struct, AuthList} | Arguments] = AllArgs}) ->
|
||||
try get_auth(AuthList) of
|
||||
try extract_auth(AuthList) of
|
||||
{error, invalid_auth} ->
|
||||
build_fault_response(-118,
|
||||
"Invalid authentication data",
|
||||
[]);
|
||||
{error, not_found} ->
|
||||
build_fault_response(-118,
|
||||
"Invalid oauth token",
|
||||
[]);
|
||||
{error, expired} ->
|
||||
build_fault_response(-118,
|
||||
"Invalid oauth token",
|
||||
[]);
|
||||
{error, Value} ->
|
||||
build_fault_response(-118,
|
||||
"Invalid authentication data: ~p",
|
||||
[Value]);
|
||||
Auth ->
|
||||
handler(State#state{get_auth = false, auth = Auth},
|
||||
handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}},
|
||||
{call, Method, Arguments})
|
||||
catch
|
||||
{error, missing_auth_arguments, _Attr} ->
|
||||
|
@ -393,9 +424,14 @@ build_fault_response(Code, ParseString, ParseArgs) ->
|
|||
do_command(AccessCommands, Auth, Command, AttrL, ArgsF,
|
||||
ResultF) ->
|
||||
ArgsFormatted = format_args(AttrL, ArgsF),
|
||||
Auth2 = case AccessCommands of
|
||||
V when is_list(V) ->
|
||||
Auth#{extra_permissions => AccessCommands};
|
||||
_ ->
|
||||
Auth
|
||||
end,
|
||||
Result =
|
||||
ejabberd_commands:execute_command(AccessCommands, Auth,
|
||||
Command, ArgsFormatted),
|
||||
ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth2),
|
||||
ResultFormatted = format_result(Result, ResultF),
|
||||
{command_result, ResultFormatted}.
|
||||
|
||||
|
@ -489,6 +525,8 @@ process_unicode_codepoints(Str) ->
|
|||
|
||||
format_result({error, Error}, _) ->
|
||||
throw({error, Error});
|
||||
format_result({error, _Type, _Code, Error}, _) ->
|
||||
throw({error, Error});
|
||||
format_result(String, string) -> lists:flatten(String);
|
||||
format_result(Atom, {Name, atom}) ->
|
||||
{struct,
|
||||
|
|
|
@ -102,8 +102,7 @@ call_port(Server, Msg) ->
|
|||
receive {eauth, Result} -> Result end.
|
||||
|
||||
random_instance(MaxNum) ->
|
||||
random:seed(p1_time_compat:timestamp()),
|
||||
random:uniform(MaxNum) - 1.
|
||||
randoms:uniform(MaxNum) - 1.
|
||||
|
||||
get_instances(Server) ->
|
||||
ejabberd_config:get_option(
|
||||
|
|
|
@ -308,10 +308,47 @@ get_opt_host(Host, Opts, Default) ->
|
|||
Val = get_opt(host, Opts, fun iolist_to_binary/1, Default),
|
||||
ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host).
|
||||
|
||||
|
||||
get_module_mod_opt_type_fun(Module) ->
|
||||
DBSubMods = ejabberd_config:v_dbs_mods(Module),
|
||||
fun(Opt) ->
|
||||
Res = lists:foldl(fun(Mod, {Funs, ArgsList, _} = Acc) ->
|
||||
case catch Mod:mod_opt_type(Opt) of
|
||||
Fun when is_function(Fun) ->
|
||||
{[Fun | Funs], ArgsList, true};
|
||||
L when is_list(L) ->
|
||||
{Funs, L ++ ArgsList, true};
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, {[], [], false}, [Module | DBSubMods]),
|
||||
case Res of
|
||||
{[], [], false} ->
|
||||
throw({'EXIT', {undef, mod_opt_type}});
|
||||
{[], Args, _} -> Args;
|
||||
{Funs, _, _} ->
|
||||
fun(Val) ->
|
||||
lists:any(fun(F) ->
|
||||
try F(Val) of
|
||||
_ ->
|
||||
true
|
||||
catch {replace_with, _NewVal} = E ->
|
||||
throw(E);
|
||||
{invalid_syntax, _Error} = E2 ->
|
||||
throw(E2);
|
||||
_:_ ->
|
||||
false
|
||||
end
|
||||
end, Funs)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
validate_opts(Module, Opts) ->
|
||||
ModOptFun = get_module_mod_opt_type_fun(Module),
|
||||
lists:filtermap(
|
||||
fun({Opt, Val}) ->
|
||||
case catch Module:mod_opt_type(Opt) of
|
||||
case catch ModOptFun(Opt) of
|
||||
VFun when is_function(VFun) ->
|
||||
try VFun(Val) of
|
||||
_ ->
|
||||
|
|
|
@ -377,6 +377,7 @@ get_commands_spec() ->
|
|||
|
||||
#ejabberd_commands{name = add_rosteritem, tags = [roster],
|
||||
desc = "Add an item to a user's roster (supports ODBC)",
|
||||
longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"",
|
||||
module = ?MODULE, function = add_rosteritem,
|
||||
args = [{localuser, binary}, {localserver, binary},
|
||||
{user, binary}, {server, binary},
|
||||
|
@ -1204,11 +1205,13 @@ push_roster_item(LU, LS, R, U, S, Action) ->
|
|||
ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ).
|
||||
|
||||
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
|
||||
GNames = binary:split(Group,<<";">>, [global]),
|
||||
GroupEls = [{xmlel, <<"group">>, [], [{xmlcdata, GName}]} || GName <- GNames],
|
||||
{xmlel, <<"item">>,
|
||||
[{<<"jid">>, jid:to_string(jid:make(U, S, <<>>))},
|
||||
{<<"name">>, Nick},
|
||||
{<<"subscription">>, Subs}],
|
||||
[{xmlel, <<"group">>, [], [{xmlcdata, Group}]}]
|
||||
GroupEls
|
||||
};
|
||||
build_roster_item(U, S, remove) ->
|
||||
{xmlel, <<"item">>,
|
||||
|
@ -1357,44 +1360,9 @@ srg_user_del(User, Host, Group, GroupHost) ->
|
|||
%% @doc Send a message to a Jabber account.
|
||||
%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok
|
||||
send_message(Type, From, To, Subject, Body) ->
|
||||
FromJID = jid:from_string(From),
|
||||
ToJID = jid:from_string(To),
|
||||
Packet = build_packet(Type, Subject, Body),
|
||||
send_packet_all_resources(From, To, Packet).
|
||||
|
||||
%% @doc Send a packet to a Jabber account.
|
||||
%% If a resource was specified in the JID,
|
||||
%% the packet is sent only to that specific resource.
|
||||
%% If no resource was specified in the JID,
|
||||
%% and the user is remote or local but offline,
|
||||
%% the packet is sent to the bare JID.
|
||||
%% If the user is local and is online in several resources,
|
||||
%% the packet is sent to all its resources.
|
||||
send_packet_all_resources(FromJIDString, ToJIDString, Packet) ->
|
||||
FromJID = jid:from_string(FromJIDString),
|
||||
ToJID = jid:from_string(ToJIDString),
|
||||
ToUser = ToJID#jid.user,
|
||||
ToServer = ToJID#jid.server,
|
||||
case ToJID#jid.resource of
|
||||
<<>> ->
|
||||
send_packet_all_resources(FromJID, ToUser, ToServer, Packet);
|
||||
Res ->
|
||||
send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet)
|
||||
end.
|
||||
|
||||
send_packet_all_resources(FromJID, ToUser, ToServer, Packet) ->
|
||||
case ejabberd_sm:get_user_resources(ToUser, ToServer) of
|
||||
[] ->
|
||||
send_packet_all_resources(FromJID, ToUser, ToServer, <<>>, Packet);
|
||||
ToResources ->
|
||||
lists:foreach(
|
||||
fun(ToResource) ->
|
||||
send_packet_all_resources(FromJID, ToUser, ToServer,
|
||||
ToResource, Packet)
|
||||
end,
|
||||
ToResources)
|
||||
end.
|
||||
|
||||
send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) ->
|
||||
ToJID = jid:make(ToU, ToS, ToR),
|
||||
ejabberd_router:route(FromJID, ToJID, Packet).
|
||||
|
||||
build_packet(Type, Subject, Body) ->
|
||||
|
|
|
@ -172,7 +172,7 @@ do_client_version(disabled, _From, _To) -> ok;
|
|||
do_client_version(enabled, From, To) ->
|
||||
ToS = jid:to_string(To),
|
||||
Random_resource =
|
||||
iolist_to_binary(integer_to_list(random:uniform(100000))),
|
||||
iolist_to_binary(integer_to_list(randoms:uniform(100000))),
|
||||
From2 = From#jid{resource = Random_resource,
|
||||
lresource = Random_resource},
|
||||
Packet = #xmlel{name = <<"iq">>,
|
||||
|
|
|
@ -118,9 +118,11 @@
|
|||
%% -------------------
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
ejabberd_access_permissions:register_permission_addon(?MODULE, fun permission_addon/0),
|
||||
ok.
|
||||
|
||||
stop(_Host) ->
|
||||
ejabberd_access_permissions:unregister_permission_addon(?MODULE),
|
||||
ok.
|
||||
|
||||
depends(_Host, _Opts) ->
|
||||
|
@ -130,76 +132,39 @@ depends(_Host, _Opts) ->
|
|||
%% basic auth
|
||||
%% ----------
|
||||
|
||||
check_permissions(Request, Command) ->
|
||||
case catch binary_to_existing_atom(Command, utf8) of
|
||||
Call when is_atom(Call) ->
|
||||
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
|
||||
check_permissions2(Request, Call, CommandPolicy, Scope);
|
||||
_ ->
|
||||
json_error(404, 40, <<"Endpoint not found.">>)
|
||||
end.
|
||||
|
||||
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
|
||||
when HTTPAuth /= undefined ->
|
||||
Admin =
|
||||
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
|
||||
{value, {_, <<"true">>}} -> true;
|
||||
_ -> false
|
||||
end,
|
||||
Auth =
|
||||
case HTTPAuth of
|
||||
{SJID, Pass} ->
|
||||
case jid:from_string(SJID) of
|
||||
#jid{user = User, server = Server} ->
|
||||
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
||||
true -> {ok, {User, Server, Pass, Admin}};
|
||||
false -> false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
{oauth, Token, _} ->
|
||||
case oauth_check_token(ScopeList, Token) of
|
||||
{ok, user, {User, Server}} ->
|
||||
{ok, {User, Server, {oauth, Token}, Admin}};
|
||||
{false, Reason} ->
|
||||
{false, Reason}
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
case Auth of
|
||||
{ok, A} -> {allowed, Call, A};
|
||||
{false, no_matching_scope} -> outofscope_response();
|
||||
_ -> unauthorized_response()
|
||||
extract_auth(#request{auth = HTTPAuth, ip = {IP, _}}) ->
|
||||
Info = case HTTPAuth of
|
||||
{SJID, Pass} ->
|
||||
case jid:from_string(SJID) of
|
||||
#jid{luser = User, lserver = Server} ->
|
||||
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
||||
true ->
|
||||
#{usr => {User, Server, <<"">>}, caller_server => Server};
|
||||
false ->
|
||||
{error, invalid_auth}
|
||||
end;
|
||||
_ ->
|
||||
{error, invalid_auth}
|
||||
end;
|
||||
{oauth, Token, _} ->
|
||||
case ejabberd_oauth:check_token(Token) of
|
||||
{ok, {U, S}, Scope} ->
|
||||
#{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
|
||||
{false, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
_ ->
|
||||
#{}
|
||||
end,
|
||||
case Info of
|
||||
Map when is_map(Map) ->
|
||||
Map#{caller_module => ?MODULE, ip => IP};
|
||||
_ ->
|
||||
?DEBUG("Invalid auth data: ~p", [Info]),
|
||||
Info
|
||||
end;
|
||||
check_permissions2(_Request, Call, open, _Scope) ->
|
||||
{allowed, Call, noauth};
|
||||
check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) ->
|
||||
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
|
||||
fun(V) -> V end,
|
||||
none),
|
||||
Res = acl:match_rule(global, Access, IP),
|
||||
case Res of
|
||||
all ->
|
||||
{allowed, Call, admin};
|
||||
[all] ->
|
||||
{allowed, Call, admin};
|
||||
allow ->
|
||||
{allowed, Call, admin};
|
||||
Commands when is_list(Commands) ->
|
||||
case lists:member(Call, Commands) of
|
||||
true -> {allowed, Call, admin};
|
||||
_ -> outofscope_response()
|
||||
end;
|
||||
_E ->
|
||||
{allowed, Call, noauth}
|
||||
end;
|
||||
check_permissions2(_Request, _Call, _Policy, _Scope) ->
|
||||
unauthorized_response().
|
||||
|
||||
oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
|
||||
ejabberd_oauth:check_token(ScopeList, Token).
|
||||
extract_auth(#request{ip = IP}) ->
|
||||
#{ip => IP, caller_module => ?MODULE}.
|
||||
|
||||
%% ------------------
|
||||
%% command processing
|
||||
|
@ -210,19 +175,12 @@ oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
|
|||
process(_, #request{method = 'POST', data = <<>>}) ->
|
||||
?DEBUG("Bad Request: no data", []),
|
||||
badrequest_response(<<"Missing POST data">>);
|
||||
process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} = Req) ->
|
||||
process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
|
||||
Version = get_api_version(Req),
|
||||
try
|
||||
Args = extract_args(Data),
|
||||
log(Call, Args, IPPort),
|
||||
case check_permissions(Req, Call) of
|
||||
{allowed, Cmd, Auth} ->
|
||||
Result = handle(Cmd, Auth, Args, Version, IP),
|
||||
json_format(Result);
|
||||
%% Warning: check_permission direcly formats 401 reply if not authorized
|
||||
ErrorResponse ->
|
||||
ErrorResponse
|
||||
end
|
||||
perform_call(Call, Args, Req, Version)
|
||||
catch
|
||||
%% TODO We need to refactor to remove redundant error return formatting
|
||||
throw:{error, unknown_command} ->
|
||||
|
@ -234,7 +192,7 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} =
|
|||
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
||||
badrequest_response()
|
||||
end;
|
||||
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
||||
process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
|
||||
Version = get_api_version(Req),
|
||||
try
|
||||
Args = case Data of
|
||||
|
@ -242,14 +200,7 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
|||
_ -> Data
|
||||
end,
|
||||
log(Call, Args, IP),
|
||||
case check_permissions(Req, Call) of
|
||||
{allowed, Cmd, Auth} ->
|
||||
Result = handle(Cmd, Auth, Args, Version, IP),
|
||||
json_format(Result);
|
||||
%% Warning: check_permission direcly formats 401 reply if not authorized
|
||||
ErrorResponse ->
|
||||
ErrorResponse
|
||||
end
|
||||
perform_call(Call, Args, Req, Version)
|
||||
catch
|
||||
%% TODO We need to refactor to remove redundant error return formatting
|
||||
throw:{error, unknown_command} ->
|
||||
|
@ -267,6 +218,22 @@ process(_Path, Request) ->
|
|||
?DEBUG("Bad Request: no handler ~p", [Request]),
|
||||
json_error(400, 40, <<"Missing command name.">>).
|
||||
|
||||
perform_call(Command, Args, Req, Version) ->
|
||||
case catch binary_to_existing_atom(Command, utf8) of
|
||||
Call when is_atom(Call) ->
|
||||
case extract_auth(Req) of
|
||||
{error, expired} -> invalid_token_response();
|
||||
{error, not_found} -> invalid_token_response();
|
||||
{error, invalid_auth} -> unauthorized_response();
|
||||
{error, _} -> unauthorized_response();
|
||||
Auth when is_map(Auth) ->
|
||||
Result = handle(Call, Auth, Args, Version),
|
||||
json_format(Result)
|
||||
end;
|
||||
_ ->
|
||||
json_error(404, 40, <<"Endpoint not found.">>)
|
||||
end.
|
||||
|
||||
%% Be tolerant to make API more easily usable from command-line pipe.
|
||||
extract_args(<<"\n">>) -> [];
|
||||
extract_args(Data) ->
|
||||
|
@ -298,7 +265,7 @@ get_api_version([]) ->
|
|||
%% TODO Check accept types of request before decided format of reply.
|
||||
|
||||
% generic ejabberd command handler
|
||||
handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
||||
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
||||
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
||||
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
||||
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
||||
|
@ -315,7 +282,7 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
|||
[{Key, undefined}|Acc]
|
||||
end, [], ArgsSpec),
|
||||
try
|
||||
handle2(Call, Auth, match(Args2, Spec), Version, IP)
|
||||
handle2(Call, Auth, match(Args2, Spec), Version)
|
||||
catch throw:not_found ->
|
||||
{404, <<"not_found">>};
|
||||
throw:{not_found, Why} when is_atom(Why) ->
|
||||
|
@ -354,10 +321,15 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
|||
{400, <<"Error">>}
|
||||
end.
|
||||
|
||||
handle2(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
||||
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
||||
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
|
||||
ArgsFormatted = format_args(Args, ArgsF),
|
||||
ejabberd_command(Auth, Call, ArgsFormatted, Version, IP).
|
||||
case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of
|
||||
{error, Error} ->
|
||||
throw(Error);
|
||||
Res ->
|
||||
format_command_result(Call, Auth, Res, Version)
|
||||
end.
|
||||
|
||||
get_elem_delete(A, L) ->
|
||||
case proplists:get_all_values(A, L) of
|
||||
|
@ -456,18 +428,6 @@ process_unicode_codepoints(Str) ->
|
|||
match(Args, Spec) ->
|
||||
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
|
||||
|
||||
ejabberd_command(Auth, Cmd, Args, Version, IP) ->
|
||||
Access = case Auth of
|
||||
admin -> [];
|
||||
_ -> undefined
|
||||
end,
|
||||
case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version, #{ip => IP}) of
|
||||
{error, Error} ->
|
||||
throw(Error);
|
||||
Res ->
|
||||
format_command_result(Cmd, Auth, Res, Version)
|
||||
end.
|
||||
|
||||
format_command_result(Cmd, Auth, Result, Version) ->
|
||||
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
|
||||
case {ResultFormat, Result} of
|
||||
|
@ -538,6 +498,9 @@ format_error_result(_ErrorAtom, Code, Msg) ->
|
|||
{500, Code, iolist_to_binary(Msg)}.
|
||||
|
||||
unauthorized_response() ->
|
||||
json_error(401, 10, <<"You are not authorized to call this command.">>).
|
||||
|
||||
invalid_token_response() ->
|
||||
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
|
||||
|
||||
outofscope_response() ->
|
||||
|
@ -571,5 +534,31 @@ log(Call, Args, {Addr, Port}) ->
|
|||
log(Call, Args, IP) ->
|
||||
?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]).
|
||||
|
||||
permission_addon() ->
|
||||
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
|
||||
fun(V) -> V end,
|
||||
none),
|
||||
Rules = acl:resolve_access(Access, global),
|
||||
R = lists:filtermap(
|
||||
fun({V, AclRules}) when V == all; V == [all]; V == [allow]; V == allow ->
|
||||
{true, {[{allow, AclRules}], {[<<"*">>], []}}};
|
||||
({List, AclRules}) when is_list(List) ->
|
||||
{true, {[{allow, AclRules}], {List, []}}};
|
||||
(_) ->
|
||||
false
|
||||
end, Rules),
|
||||
case R of
|
||||
[] ->
|
||||
none;
|
||||
_ ->
|
||||
{_, Res} = lists:foldl(
|
||||
fun({R2, L2}, {Idx, Acc}) ->
|
||||
{Idx+1, [{<<"'mod_http_api admin_ip_access' option compatibility shim ",
|
||||
(integer_to_binary(Idx))/binary>>,
|
||||
{[?MODULE], [{access, R2}], L2}} | Acc]}
|
||||
end, {1, []}, R),
|
||||
Res
|
||||
end.
|
||||
|
||||
mod_opt_type(admin_ip_access) -> fun acl:access_rules_validator/1;
|
||||
mod_opt_type(_) -> [admin_ip_access].
|
||||
|
|
|
@ -966,13 +966,14 @@ send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) ->
|
|||
NS == ?NS_MAM_0; NS == ?NS_MAM_1 ->
|
||||
[{<<"complete">>, jlib:atom_to_binary(IsComplete)}]
|
||||
end,
|
||||
Hint = [#xmlel{name = <<"no-store">>, attrs = [{<<"xmlns">>, ?NS_HINTS}]}],
|
||||
Els = lists:map(
|
||||
fun({ID, _IDInt, El}) ->
|
||||
#xmlel{name = <<"message">>,
|
||||
children = [#xmlel{name = <<"result">>,
|
||||
attrs = [{<<"xmlns">>, NS},
|
||||
{<<"id">>, ID}|QIDAttr],
|
||||
children = [El]}]}
|
||||
children = [El]} | Hint]}
|
||||
end, Msgs),
|
||||
RSMOut = make_rsm_out(Msgs, RSM, Count, QIDAttr ++ CompleteAttr, NS),
|
||||
if NS == ?NS_MAM_TMP; NS == ?NS_MAM_1 ->
|
||||
|
@ -990,7 +991,7 @@ send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) ->
|
|||
end, Els),
|
||||
ejabberd_router:route(
|
||||
To, From, #xmlel{name = <<"message">>,
|
||||
children = RSMOut}),
|
||||
children = RSMOut ++ Hint}),
|
||||
ignore
|
||||
end.
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
forget_room/3,
|
||||
create_room/5,
|
||||
shutdown_rooms/1,
|
||||
process_iq_disco_items/4,
|
||||
process_iq_disco_items/5,
|
||||
broadcast_service_message/2,
|
||||
export/1,
|
||||
import/1,
|
||||
|
@ -66,13 +66,12 @@
|
|||
server_host = <<"">> :: binary(),
|
||||
access = {none, none, none, none} :: {atom(), atom(), atom(), atom()},
|
||||
history_size = 20 :: non_neg_integer(),
|
||||
max_rooms_discoitems = 100 :: non_neg_integer(),
|
||||
default_room_opts = [] :: list(),
|
||||
room_shaper = none :: shaper:shaper()}).
|
||||
|
||||
-define(PROCNAME, ejabberd_mod_muc).
|
||||
|
||||
-define(MAX_ROOMS_DISCOITEMS, 100).
|
||||
|
||||
-type muc_room_opts() :: [{atom(), any()}].
|
||||
-callback init(binary(), gen_mod:opts()) -> any().
|
||||
-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass.
|
||||
|
@ -154,7 +153,7 @@ forget_room(ServerHost, Host, Name) ->
|
|||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
Mod:forget_room(LServer, Host, Name).
|
||||
|
||||
process_iq_disco_items(Host, From, To,
|
||||
process_iq_disco_items(Host, From, To, MaxRoomsDiscoItems,
|
||||
#iq{lang = Lang} = IQ) ->
|
||||
Rsm = jlib:rsm_decode(IQ),
|
||||
DiscoNode = fxml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el),
|
||||
|
@ -162,7 +161,7 @@ process_iq_disco_items(Host, From, To,
|
|||
sub_el =
|
||||
[#xmlel{name = <<"query">>,
|
||||
attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}],
|
||||
children = iq_disco_items(Host, From, Lang, DiscoNode, Rsm)}]},
|
||||
children = iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, DiscoNode, Rsm)}]},
|
||||
ejabberd_router:route(To, From, jlib:iq_to_xml(Res)).
|
||||
|
||||
can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false;
|
||||
|
@ -200,6 +199,9 @@ init([Host, Opts]) ->
|
|||
HistorySize = gen_mod:get_opt(history_size, Opts,
|
||||
fun(I) when is_integer(I), I>=0 -> I end,
|
||||
20),
|
||||
MaxRoomsDiscoItems = gen_mod:get_opt(max_rooms_discoitems, Opts,
|
||||
fun(I) when is_integer(I), I>=0 -> I end,
|
||||
100),
|
||||
DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts,
|
||||
fun(L) when is_list(L) -> L end,
|
||||
[]),
|
||||
|
@ -265,6 +267,7 @@ init([Host, Opts]) ->
|
|||
access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
|
||||
default_room_opts = DefRoomOpts,
|
||||
history_size = HistorySize,
|
||||
max_rooms_discoitems = MaxRoomsDiscoItems,
|
||||
room_shaper = RoomShaper}}.
|
||||
|
||||
handle_call(stop, _From, State) ->
|
||||
|
@ -293,9 +296,10 @@ handle_info({route, From, To, Packet},
|
|||
#state{host = Host, server_host = ServerHost,
|
||||
access = Access, default_room_opts = DefRoomOpts,
|
||||
history_size = HistorySize,
|
||||
max_rooms_discoitems = MaxRoomsDiscoItems,
|
||||
room_shaper = RoomShaper} = State) ->
|
||||
case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
|
||||
From, To, Packet, DefRoomOpts) of
|
||||
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) of
|
||||
{'EXIT', Reason} ->
|
||||
?ERROR_MSG("~p", [Reason]);
|
||||
_ ->
|
||||
|
@ -326,12 +330,12 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
|
||||
From, To, Packet, DefRoomOpts) ->
|
||||
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) ->
|
||||
{AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
|
||||
case acl:match_rule(ServerHost, AccessRoute, From) of
|
||||
allow ->
|
||||
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
|
||||
From, To, Packet, DefRoomOpts);
|
||||
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems);
|
||||
_ ->
|
||||
#xmlel{attrs = Attrs} = Packet,
|
||||
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
|
||||
|
@ -343,7 +347,7 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|||
|
||||
|
||||
do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
|
||||
From, To, Packet, DefRoomOpts) ->
|
||||
From, To, Packet, DefRoomOpts, MaxRoomsDiscoItems) ->
|
||||
{_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access,
|
||||
{Room, _, Nick} = jid:tolower(To),
|
||||
#xmlel{name = Name, attrs = Attrs} = Packet,
|
||||
|
@ -374,7 +378,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
|
|||
jlib:iq_to_xml(Res));
|
||||
#iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ ->
|
||||
spawn(?MODULE, process_iq_disco_items,
|
||||
[Host, From, To, IQ]);
|
||||
[Host, From, To, MaxRoomsDiscoItems, IQ]);
|
||||
#iq{type = get, xmlns = (?NS_REGISTER) = XMLNS,
|
||||
lang = Lang, sub_el = _SubEl} =
|
||||
IQ ->
|
||||
|
@ -636,15 +640,15 @@ iq_disco_info(ServerHost, Lang) ->
|
|||
[]
|
||||
end.
|
||||
|
||||
iq_disco_items(Host, From, Lang, <<>>, none) ->
|
||||
iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<>>, none) ->
|
||||
Rooms = get_vh_rooms(Host),
|
||||
case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of
|
||||
case erlang:length(Rooms) < MaxRoomsDiscoItems of
|
||||
true ->
|
||||
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang});
|
||||
false ->
|
||||
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none)
|
||||
iq_disco_items(Host, From, Lang, MaxRoomsDiscoItems, <<"nonemptyrooms">>, none)
|
||||
end;
|
||||
iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) ->
|
||||
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"nonemptyrooms">>, none) ->
|
||||
XmlEmpty = #xmlel{name = <<"item">>,
|
||||
attrs =
|
||||
[{<<"jid">>, <<"conference.localhost">>},
|
||||
|
@ -653,9 +657,9 @@ iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) ->
|
|||
children = []},
|
||||
Query = {get_disco_item, only_non_empty, From, Lang},
|
||||
[XmlEmpty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)];
|
||||
iq_disco_items(Host, From, Lang, <<"emptyrooms">>, none) ->
|
||||
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, <<"emptyrooms">>, none) ->
|
||||
iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang});
|
||||
iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) ->
|
||||
iq_disco_items(Host, From, Lang, _MaxRoomsDiscoItems, _DiscoNode, Rsm) ->
|
||||
{Rooms, RsmO} = get_vh_rooms(Host, Rsm),
|
||||
RsmOut = jlib:rsm_encode(RsmO),
|
||||
iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut.
|
||||
|
@ -984,6 +988,8 @@ mod_opt_type(max_room_id) ->
|
|||
fun (infinity) -> infinity;
|
||||
(I) when is_integer(I), I > 0 -> I
|
||||
end;
|
||||
mod_opt_type(max_rooms_discoitems) ->
|
||||
fun (I) when is_integer(I), I >= 0 -> I end;
|
||||
mod_opt_type(regexp_room_id) ->
|
||||
fun iolist_to_binary/1;
|
||||
mod_opt_type(max_room_name) ->
|
||||
|
@ -1011,8 +1017,8 @@ mod_opt_type(user_presence_shaper) ->
|
|||
mod_opt_type(_) ->
|
||||
[access, access_admin, access_create, access_persistent,
|
||||
db_type, default_room_options, history_size, host,
|
||||
max_room_desc, max_room_id, max_room_name, regexp_room_id,
|
||||
max_user_conferences, max_users,
|
||||
max_room_desc, max_room_id, max_room_name,
|
||||
max_rooms_discoitems, max_user_conferences, max_users,
|
||||
max_users_admin_threshold, max_users_presence,
|
||||
min_message_interval, min_presence_interval,
|
||||
room_shaper, user_message_shaper, user_presence_shaper].
|
||||
regexp_room_id, room_shaper, user_message_shaper, user_presence_shaper].
|
||||
|
|
|
@ -432,8 +432,8 @@ create_room(Name1, Host1, ServerHost) ->
|
|||
create_room_with_opts(Name1, Host1, ServerHost, []).
|
||||
|
||||
create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
|
||||
Name = jid:nodeprep(Name1),
|
||||
Host = jid:nodeprep(Host1),
|
||||
true = (error /= (Name = jid:nodeprep(Name1))),
|
||||
true = (error /= (Host = jid:nodeprep(Host1))),
|
||||
|
||||
%% Get the default room options from the muc configuration
|
||||
DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
|
||||
|
@ -514,7 +514,7 @@ destroy_room({N, H, SH}) ->
|
|||
%% The file encoding must be UTF-8
|
||||
|
||||
destroy_rooms_file(Filename) ->
|
||||
{ok, F} = file:open(Filename, [read, binary]),
|
||||
{ok, F} = file:open(Filename, [read]),
|
||||
RJID = read_room(F),
|
||||
Rooms = read_rooms(F, RJID, []),
|
||||
file:close(F),
|
||||
|
@ -533,7 +533,7 @@ read_room(F) ->
|
|||
eof -> eof;
|
||||
String ->
|
||||
case io_lib:fread("~s", String) of
|
||||
{ok, [RoomJID], _} -> split_roomjid(RoomJID);
|
||||
{ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID));
|
||||
{error, What} ->
|
||||
io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String])
|
||||
end
|
||||
|
@ -551,7 +551,7 @@ split_roomjid(RoomJID) ->
|
|||
%%----------------------------
|
||||
|
||||
create_rooms_file(Filename) ->
|
||||
{ok, F} = file:open(Filename, [read, binary]),
|
||||
{ok, F} = file:open(Filename, [read]),
|
||||
RJID = read_room(F),
|
||||
Rooms = read_rooms(F, RJID, []),
|
||||
file:close(F),
|
||||
|
|
|
@ -959,6 +959,7 @@ process_groupchat_message(From,
|
|||
_ ->
|
||||
case
|
||||
can_change_subject(Role,
|
||||
IsSubscriber,
|
||||
StateData)
|
||||
of
|
||||
true ->
|
||||
|
@ -2821,10 +2822,10 @@ check_subject(Packet) ->
|
|||
SubjEl -> fxml:get_tag_cdata(SubjEl)
|
||||
end.
|
||||
|
||||
can_change_subject(Role, StateData) ->
|
||||
can_change_subject(Role, IsSubscriber, StateData) ->
|
||||
case (StateData#state.config)#config.allow_change_subj
|
||||
of
|
||||
true -> Role == moderator orelse Role == participant;
|
||||
true -> Role == moderator orelse Role == participant orelse IsSubscriber == true;
|
||||
_ -> Role == moderator
|
||||
end.
|
||||
|
||||
|
@ -5056,6 +5057,8 @@ process_invitations(From, InviteEls, Lang, StateData) ->
|
|||
throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)});
|
||||
JID1 -> JID1
|
||||
end,
|
||||
ejabberd_hooks:run(muc_invite, StateData#state.server_host,
|
||||
[StateData#state.jid, StateData#state.config, From, JID, Reason]),
|
||||
ejabberd_router:route(StateData#state.jid, JID, Msg),
|
||||
JID
|
||||
end,
|
||||
|
|
|
@ -81,7 +81,7 @@ remove_old_messages(Days, LServer) ->
|
|||
[<<"DELETE FROM spool"
|
||||
" WHERE created_at < "
|
||||
"NOW() - INTERVAL '">>,
|
||||
integer_to_list(Days), <<"';">>]) of
|
||||
integer_to_list(Days), <<"' DAY;">>]) of
|
||||
{updated, N} ->
|
||||
?INFO_MSG("~p message(s) deleted from offline spool", [N]);
|
||||
_Error ->
|
||||
|
|
|
@ -238,7 +238,7 @@ export(Server) ->
|
|||
"values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s,"
|
||||
" %(Order)d, %(MatchAll)b, %(MatchIQ)b,"
|
||||
" %(MatchMessage)b, %(MatchPresenceIn)b,"
|
||||
" %(MatchPresenceOut)b)")
|
||||
" %(MatchPresenceOut)b);")
|
||||
|| {SType, SValue, SAction, Order,
|
||||
MatchAll, MatchIQ,
|
||||
MatchMessage, MatchPresenceIn,
|
||||
|
|
|
@ -161,13 +161,8 @@ get_local_stat(Server, [], Name)
|
|||
end;
|
||||
get_local_stat(_Server, [], Name)
|
||||
when Name == <<"users/all-hosts/online">> ->
|
||||
case catch mnesia:table_info(session, size) of
|
||||
{'EXIT', _Reason} ->
|
||||
?STATERR(<<"500">>, <<"Internal Server Error">>);
|
||||
Users ->
|
||||
?STATVAL((iolist_to_binary(integer_to_list(Users))),
|
||||
<<"users">>)
|
||||
end;
|
||||
Users = ejabberd_sm:connected_users_number(),
|
||||
?STATVAL((iolist_to_binary(integer_to_list(Users))), <<"users">>);
|
||||
get_local_stat(_Server, [], Name)
|
||||
when Name == <<"users/all-hosts/total">> ->
|
||||
NumUsers = lists:foldl(fun (Host, Total) ->
|
||||
|
|
|
@ -688,7 +688,7 @@ get_items(Nidx, _From,
|
|||
before -> {<<">">>, <<"asc">>};
|
||||
_ -> {<<"is not">>, <<"desc">>}
|
||||
end,
|
||||
SNidx = integer_to_binary(Nidx),
|
||||
SNidx = jlib:i2l(Nidx),
|
||||
[AttrName, Id] = case I of
|
||||
undefined when IncIndex =/= undefined ->
|
||||
case catch
|
||||
|
@ -790,7 +790,7 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
|
|||
|
||||
get_last_items(Nidx, _From, Count) ->
|
||||
Limit = jlib:i2l(Count),
|
||||
SNidx = integer_to_binary(Nidx),
|
||||
SNidx = jlib:i2l(Nidx),
|
||||
Query = fun(mssql, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
[<<"select top ">>, Limit,
|
||||
|
@ -890,7 +890,7 @@ del_items(Nidx, [ItemId]) ->
|
|||
del_item(Nidx, ItemId);
|
||||
del_items(Nidx, ItemIds) ->
|
||||
I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>),
|
||||
SNidx = integer_to_binary(Nidx),
|
||||
SNidx = jlib:i2l(Nidx),
|
||||
catch
|
||||
ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>,
|
||||
I, <<") and nodeid='">>, SNidx, <<"';">>]).
|
||||
|
|
|
@ -27,14 +27,29 @@
|
|||
|
||||
-author('alexey@process-one.net').
|
||||
|
||||
-export([get_string/0]).
|
||||
-export([get_string/0, uniform/0, uniform/1, bytes/1]).
|
||||
|
||||
-export([start/0]).
|
||||
|
||||
-define(THRESHOLD, 16#10000000000000000).
|
||||
|
||||
start() ->
|
||||
ok.
|
||||
|
||||
get_string() ->
|
||||
R = crypto:rand_uniform(0, 16#10000000000000000),
|
||||
R = crypto:rand_uniform(0, ?THRESHOLD),
|
||||
jlib:integer_to_binary(R).
|
||||
|
||||
uniform() ->
|
||||
crypto:rand_uniform(0, ?THRESHOLD)/?THRESHOLD.
|
||||
|
||||
uniform(N) ->
|
||||
crypto:rand_uniform(1, N+1).
|
||||
|
||||
-ifdef(STRONG_RAND_BYTES).
|
||||
bytes(N) ->
|
||||
crypto:strong_rand_bytes(N).
|
||||
-else.
|
||||
bytes(N) ->
|
||||
crypto:rand_bytes(N).
|
||||
-endif.
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule EjabberdAdminTest do
|
|||
# For some myterious reason, :ejabberd_commands.init mays
|
||||
# sometimes fails if module is not loaded before
|
||||
{:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
:ejabberd_commands.init
|
||||
:ejabberd_admin.start
|
||||
:ok
|
||||
|
|
|
@ -50,6 +50,7 @@ defmodule EjabberdCommandsMockTest do
|
|||
:mnesia.start
|
||||
:ok = :jid.start
|
||||
:ok = :ejabberd_config.start(["domain1", "domain2"], [])
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
:ok = :acl.start
|
||||
EjabberdOauthMock.init
|
||||
on_exit fn -> :meck.unload end
|
||||
|
|
|
@ -30,6 +30,7 @@ defmodule EjabberdCommandsTest do
|
|||
:mnesia.start
|
||||
:stringprep.start
|
||||
:ok = :ejabberd_config.start(["localhost"], [])
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
|
||||
:ejabberd_commands.init
|
||||
:ok
|
||||
|
|
|
@ -45,6 +45,7 @@ defmodule EjabberdModAdminExtraTest do
|
|||
rescue
|
||||
_ -> :ok
|
||||
end
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
:ejabberd_commands.init
|
||||
:ok = :ejabberd_config.start([@domain], [])
|
||||
:mod_admin_extra.start(@domain, [])
|
||||
|
|
|
@ -46,6 +46,7 @@ defmodule ModHttpApiMockTest do
|
|||
:mnesia.start
|
||||
:stringprep.start
|
||||
:ejabberd_config.start([@domain], [])
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
:ejabberd_commands.init
|
||||
rescue
|
||||
_ -> :ok
|
||||
|
@ -240,10 +241,10 @@ defmodule ModHttpApiMockTest do
|
|||
result = :ejabberd_oauth.process([], req)
|
||||
assert 200 = elem(result, 0) #http code
|
||||
{kv} = :jiffy.decode(elem(result,2))
|
||||
assert {_, "bearer"} = List.keyfind(kv, "token_type", 0)
|
||||
assert {_, @command} = List.keyfind(kv, "scope", 0)
|
||||
assert {_, 4000} = List.keyfind(kv, "expires_in", 0)
|
||||
{"access_token", _token} = List.keyfind(kv, "access_token", 0)
|
||||
assert {_, "bearer"} = List.keyfind(kv, "token_type", 0)
|
||||
assert {_, @command} = List.keyfind(kv, "scope", 0)
|
||||
assert {_, 4000} = List.keyfind(kv, "expires_in", 0)
|
||||
{"access_token", _token} = List.keyfind(kv, "access_token", 0)
|
||||
|
||||
#missing grant_type
|
||||
req = request(method: :POST,
|
||||
|
@ -254,7 +255,7 @@ defmodule ModHttpApiMockTest do
|
|||
result = :ejabberd_oauth.process([], req)
|
||||
assert 400 = elem(result, 0) #http code
|
||||
{kv} = :jiffy.decode(elem(result,2))
|
||||
assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0)
|
||||
assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0)
|
||||
|
||||
|
||||
# incorrect user/pass
|
||||
|
@ -266,7 +267,7 @@ defmodule ModHttpApiMockTest do
|
|||
result = :ejabberd_oauth.process([], req)
|
||||
assert 400 = elem(result, 0) #http code
|
||||
{kv} = :jiffy.decode(elem(result,2))
|
||||
assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0)
|
||||
assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0)
|
||||
|
||||
assert :meck.validate :ejabberd_auth
|
||||
assert :meck.validate :ejabberd_commands
|
||||
|
|
|
@ -31,6 +31,7 @@ defmodule ModHttpApiTest do
|
|||
:ok = :mnesia.start
|
||||
:stringprep.start
|
||||
:ok = :ejabberd_config.start(["localhost"], [])
|
||||
{:ok, _} = :ejabberd_access_permissions.start_link()
|
||||
:ok = :ejabberd_commands.init
|
||||
:ok = :ejabberd_commands.register_commands(cmds)
|
||||
on_exit fn ->
|
||||
|
@ -46,12 +47,12 @@ defmodule ModHttpApiTest do
|
|||
assert Enum.member?(commands, :user_cmd)
|
||||
end
|
||||
|
||||
test "We can call open commands without authentication" do
|
||||
setup_mocks()
|
||||
:ejabberd_commands.expose_commands([:open_cmd])
|
||||
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
||||
{200, _, _} = :mod_http_api.process(["open_cmd"], request)
|
||||
end
|
||||
# test "We can call open commands without authentication" do
|
||||
# setup_mocks()
|
||||
# :ejabberd_commands.expose_commands([:open_cmd])
|
||||
# request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
||||
# {200, _, _} = :mod_http_api.process(["open_cmd"], request)
|
||||
# end
|
||||
|
||||
# This related to the commands config file option
|
||||
test "Attempting to access a command that is not exposed as HTTP API returns 403" do
|
||||
|
|
Loading…
Reference in New Issue