1
0
Fork 0
mirror of https://github.com/processone/ejabberd synced 2025-10-03 01:39:35 +02:00
This commit is contained in:
badlop 2025-09-03 14:27:18 +00:00 committed by GitHub
commit f07d080b37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
383 changed files with 87051 additions and 59713 deletions

11
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,11 @@
# .git-blame-ignore-revs
# Use this file when viewing blame:
# git blame --ignore-revs-file .git-blame-ignore-revs
# Or configure git to use always this file:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Accumulated patch to binarize and indent code
9deb294328bb3f9eb6bd2c0e7cd500732e9b5830
# Result of running "make format indent" for the first time
6b7d15f0271686b6902a0edc82f407e529b35a90

View file

@ -13,6 +13,7 @@ SED = @SED@
ERL = @ERL@
EPMD = @EPMD@
IEX = @IEX@
EMACS = @EMACS@
INSTALLUSER=@INSTALLUSER@
INSTALLGROUP=@INSTALLGROUP@
@ -266,11 +267,15 @@ _build/edoc/logo.png: edoc_compile
#' format / indent
#
.SILENT: format indent
FORMAT_LOG=/tmp/ejabberd-format.log
format:
tools/rebar3-format.sh $(REBAR3)
tools/rebar3-format.sh $(FORMAT_LOG) $(REBAR3)
indent:
tools/emacs-indent.sh
tools/emacs-indent.sh $(FORMAT_LOG) $(EMACS)
#.
#' copy-files
@ -714,7 +719,7 @@ help:
@echo " translations Extract translation files"
@echo " TAGS Generate tags file for text editors"
@echo ""
@echo " format Format source code using rebar3_format"
@echo " format Format source code using efmt [rebar3]"
@echo " indent Indent source code using erlang-mode [emacs]"
@echo ""
@echo " dialyzer Run Dialyzer static analyzer"

View file

@ -3,7 +3,6 @@
{vsn, "0.0.1"},
{registered, []},
{applications, [kernel, stdlib]},
{env,[]},
{env, []},
{modules, []},
{links, []}
]}.
{links, []}]}.

View file

@ -2,6 +2,7 @@
-export([init/1]).
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
{ok, State1} = configure_deps_prv:init(State),

View file

@ -5,13 +5,14 @@
-define(PROVIDER, 'configure-deps').
-define(DEPS, [install_deps]).
%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([
{namespace, default},
Provider = providers:create(
[{namespace, default},
{name, ?PROVIDER}, % The 'user friendly' name of the task
{module, ?MODULE}, % The module implementation of the task
{bare, true}, % The task can be run by the user, always true
@ -19,8 +20,7 @@ init(State) ->
{example, "rebar3 configure-deps"}, % How to use the plugin
{opts, []}, % list of options understood by the plugin
{short_desc, "Explicitly run ./configure for dependencies"},
{desc, "A rebar plugin to allow explicitly running ./configure on dependencies. Useful if dependencies might change prior to compilation when configure is run."}
]),
{desc, "A rebar plugin to allow explicitly running ./configure on dependencies. Useful if dependencies might change prior to compilation when configure is run."}]),
{ok, rebar_state:add_provider(State, Provider)}.
@ -30,25 +30,30 @@ do(State) ->
lists:foreach(fun do_app/1, Apps),
{ok, State}.
exec_configure({'configure-deps', Cmd}, Dir) ->
rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, true}]);
exec_configure(_, Acc) -> Acc.
parse_pre_hooks({pre_hooks, PreHooks}, Acc) ->
lists:foldl(fun exec_configure/2, Acc, PreHooks);
parse_pre_hooks(_, Acc) -> Acc.
parse_additions({add, App, Additions}, {MyApp, Dir}) when App == MyApp ->
lists:foldl(fun parse_pre_hooks/2, Dir, Additions),
{MyApp, Dir};
parse_additions(_, Acc) -> Acc.
do_app(App) ->
Dir = rebar_app_info:dir(App),
Opts = rebar_app_info:opts(App),
Overrides = rebar_opts:get(Opts, overrides),
lists:foldl(fun parse_additions/2, {binary_to_atom(rebar_app_info:name(App), utf8), Dir}, Overrides).
-spec format_error(any()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).

View file

@ -88,6 +88,8 @@ AC_PATH_PROG([ESCRIPT], [escript], [], [$ERLANG_ROOT_DIR/bin])
#locating make
AC_CHECK_PROG([MAKE], [make], [make], [])
AC_PATH_TOOL(EMACS, emacs, , [${extra_erl_path}$PATH])
if test "x$ESCRIPT" = "x"; then
AC_MSG_ERROR(['escript' was not found])
fi

View file

@ -3,79 +3,101 @@
%% SEQUENCE and SET, and macro definitions for each value
%% definition,in module ELDAPv3
-record('LDAPMessage', {
messageID, protocolOp, controls = asn1_NOVALUE
}).
-record('AttributeValueAssertion', {
attributeDesc, assertionValue
}).
-record('LDAPMessage',{
messageID, protocolOp, controls = asn1_NOVALUE}).
-record('Attribute', {
type, vals
}).
-record('AttributeValueAssertion',{
attributeDesc, assertionValue}).
-record('LDAPResult', {
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE
}).
-record('Attribute',{
type, vals}).
-record('Control', {
controlType, criticality = asn1_DEFAULT, controlValue = asn1_NOVALUE
}).
-record('LDAPResult',{
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE}).
-record('BindRequest', {
version, name, authentication
}).
-record('Control',{
controlType, criticality = asn1_DEFAULT, controlValue = asn1_NOVALUE}).
-record('SaslCredentials', {
mechanism, credentials = asn1_NOVALUE
}).
-record('BindRequest',{
version, name, authentication}).
-record('BindResponse', {
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, serverSaslCreds = asn1_NOVALUE
}).
-record('SaslCredentials',{
mechanism, credentials = asn1_NOVALUE}).
-record('SearchRequest', {
baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes
}).
-record('BindResponse',{
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, serverSaslCreds = asn1_NOVALUE}).
-record('SubstringFilter', {
type, substrings
}).
-record('SearchRequest',{
baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes}).
-record('MatchingRuleAssertion', {
matchingRule = asn1_NOVALUE, type = asn1_NOVALUE, matchValue, dnAttributes = asn1_DEFAULT
}).
-record('SubstringFilter',{
type, substrings}).
-record('SearchResultEntry', {
objectName, attributes
}).
-record('MatchingRuleAssertion',{
matchingRule = asn1_NOVALUE, type = asn1_NOVALUE, matchValue, dnAttributes = asn1_DEFAULT}).
-record('PartialAttributeList_SEQOF', {
type, vals
}).
-record('SearchResultEntry',{
objectName, attributes}).
-record('ModifyRequest', {
object, modification
}).
-record('PartialAttributeList_SEQOF',{
type, vals}).
-record('ModifyRequest_modification_SEQOF', {
operation, modification
}).
-record('ModifyRequest',{
object, modification}).
-record('AttributeTypeAndValues', {
type, vals
}).
-record('ModifyRequest_modification_SEQOF',{
operation, modification}).
-record('AddRequest', {
entry, attributes
}).
-record('AttributeTypeAndValues',{
type, vals}).
-record('AttributeList_SEQOF', {
type, vals
}).
-record('AddRequest',{
entry, attributes}).
-record('ModifyDNRequest', {
entry, newrdn, deleteoldrdn, newSuperior = asn1_NOVALUE
}).
-record('AttributeList_SEQOF',{
type, vals}).
-record('CompareRequest', {
entry, ava
}).
-record('ModifyDNRequest',{
entry, newrdn, deleteoldrdn, newSuperior = asn1_NOVALUE}).
-record('ExtendedRequest', {
requestName, requestValue = asn1_NOVALUE
}).
-record('CompareRequest',{
entry, ava}).
-record('ExtendedResponse', {
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, responseName = asn1_NOVALUE, response = asn1_NOVALUE
}).
-record('ExtendedRequest',{
requestName, requestValue = asn1_NOVALUE}).
-record('PasswdModifyRequestValue', {
userIdentity = asn1_NOVALUE, oldPasswd = asn1_NOVALUE, newPasswd = asn1_NOVALUE
}).
-record('ExtendedResponse',{
resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, responseName = asn1_NOVALUE, response = asn1_NOVALUE}).
-record('PasswdModifyRequestValue',{
userIdentity = asn1_NOVALUE, oldPasswd = asn1_NOVALUE, newPasswd = asn1_NOVALUE}).
-record('PasswdModifyResponseValue',{
genPasswd = asn1_NOVALUE}).
-record('PasswdModifyResponseValue', {
genPasswd = asn1_NOVALUE
}).
-define('maxInt', 2147483647).
-define('passwdModifyOID', [49,46,51,46,54,46,49,46,52,46,49,46,52,50,48,51,46,49,46,49,49,46,49]).
-define('passwdModifyOID', [49, 46, 51, 46, 54, 46, 49, 46, 52, 46, 49, 46, 52, 50, 48, 51, 46, 49, 46, 49, 49, 46, 49]).

View file

@ -42,8 +42,11 @@
{<<"Access-Control-Max-Age">>, <<"86400">>}).
-define(OPTIONS_HEADER,
[?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
[?CT_PLAIN,
?AC_ALLOW_ORIGIN,
?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS,
?AC_MAX_AGE]).
-define(HEADER(CType),
[CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).

View file

@ -18,5 +18,7 @@
%%%
%%%----------------------------------------------------------------------
-record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1',
password = <<"">> :: binary() | scram() | '_'}).
-record(passwd, {
us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1',
password = <<"">> :: binary() | scram() | '_'
}).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-type aterm() :: {atom(), atype()}.
-type atype() :: integer | string | binary | any | atom |
@ -104,4 +106,3 @@
args_example :: none | [any()] | '_',
result_example :: any()
}.

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(request,
{method :: method(),

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(oauth_token, {
token = <<"">> :: binary() | '_',

View file

@ -1,3 +1,6 @@
%% @efmt:off
%% @indent-begin
-define(ROUTES_CACHE, routes_cache).
-type local_hint() :: integer() | {apply, atom(), atom()}.

View file

@ -27,10 +27,13 @@
-record(session_counter, {vhost, count}).
-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()}
| {num_stanzas_in, non_neg_integer()}
| {atom(), term()}].
-type info() :: [{conn, atom()} |
{ip, ip()} |
{node, atom()} |
{oor, boolean()} |
{auth_module, atom()} |
{num_stanzas_in, non_neg_integer()} |
{atom(), term()}].
-type prio() :: undefined | integer().
-endif.

View file

@ -30,46 +30,62 @@
-define(SQL_INSERT(Table, Fields), ?SQL_INSERT_MARK(Table, Fields)).
-ifdef(COMPILER_REPORTS_ONLY_LINES).
-record(sql_query, {hash :: binary(),
-record(sql_query, {
hash :: binary(),
format_query :: fun(),
format_res :: fun(),
args :: fun(),
flags :: non_neg_integer(),
loc :: {module(), pos_integer()}}).
loc :: {module(), pos_integer()}
}).
-else.
-record(sql_query, {hash :: binary(),
-record(sql_query, {
hash :: binary(),
format_query :: fun(),
format_res :: fun(),
args :: fun(),
flags :: non_neg_integer(),
loc :: {module(), {pos_integer(), pos_integer()}}}).
loc :: {module(), {pos_integer(), pos_integer()}}
}).
-endif.
-record(sql_escape, {string :: fun((binary()) -> binary()),
-record(sql_escape, {
string :: fun((binary()) -> binary()),
integer :: fun((integer()) -> binary()),
boolean :: fun((boolean()) -> binary()),
in_array_string :: fun((binary()) -> binary()),
like_escape :: fun(() -> binary())}).
like_escape :: fun(() -> binary())
}).
-record(sql_index, {columns,
-record(sql_index, {
columns,
unique = false :: boolean(),
meta = #{}}).
-record(sql_column, {name :: binary(),
meta = #{}
}).
-record(sql_column, {
name :: binary(),
type,
default = false,
opts = []}).
-record(sql_table, {name :: binary(),
opts = []
}).
-record(sql_table, {
name :: binary(),
columns :: [#sql_column{}],
indices = [] :: [#sql_index{}],
post_create}).
-record(sql_schema, {version :: integer(),
post_create
}).
-record(sql_schema, {
version :: integer(),
tables :: [#sql_table{}],
update = []}).
-record(sql_references, {table :: binary(),
column :: binary()}).
update = []
}).
-record(sql_references, {
table :: binary(),
column :: binary()
}).
-record(sql_schema_info,
{db_type :: pgsql | mysql | sqlite,
-record(sql_schema_info, {
db_type :: pgsql | mysql | sqlite,
db_version :: any(),
new_schema = true :: boolean()}).
new_schema = true :: boolean()
}).

View file

@ -59,29 +59,36 @@
-define(INPUT(Type, Name, Value),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"value">>, Value}])).
-define(INPUTPH(Type, Name, Value, PlaceHolder),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"value">>, Value}, {<<"placeholder">>, PlaceHolder}])).
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"value">>, Value},
{<<"placeholder">>, PlaceHolder}])).
-define(INPUTT(Type, Name, Value),
?INPUT(Type, Name, (translate:translate(Lang, Value)))).
-define(INPUTD(Type, Name, Value),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"class">>, <<"btn-danger">>}, {<<"value">>, Value}])).
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"class">>, <<"btn-danger">>},
{<<"value">>, Value}])).
-define(INPUTTD(Type, Name, Value),
?INPUTD(Type, Name, (translate:translate(Lang, Value)))).
-define(INPUTS(Type, Name, Value, Size),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"value">>, Value}, {<<"size">>, Size}])).
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"value">>, Value},
{<<"size">>, Size}])).
-define(INPUTST(Type, Name, Value, Size),
?INPUT(Type, Name, (translate:translate(Lang, Value)), Size)).
@ -92,7 +99,8 @@
-define(TEXTAREA(Name, Rows, Cols, Value),
?XAC(<<"textarea">>,
[{<<"name">>, Name}, {<<"rows">>, Rows},
[{<<"name">>, Name},
{<<"rows">>, Rows},
{<<"cols">>, Cols}],
Value)).
@ -107,7 +115,8 @@
-define(XREST(Text), ?XRES((translate:translate(Lang, Text)))).
-define(GL(Ref, Title),
?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}],
?XAE(<<"div">>,
[{<<"class">>, <<"guidelink">>}],
[?XAE(<<"a">>,
[{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>},
{<<"target">>, <<"_blank">>}],
@ -120,7 +129,8 @@
?H1GLraw(Name, <<"admin/configuration/", RefConf/binary>>, Title)).
-define(ANCHORL(Ref),
?XAE(<<"div">>, [{<<"class">>, <<"anchorlink">>}],
?XAE(<<"div">>,
[{<<"class">>, <<"anchorlink">>}],
[?XAE(<<"a">>,
[{<<"href">>, <<"#", Ref/binary>>}],
[?C(unicode:characters_to_binary(""))])])).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-define(LDAP_PORT, 389).

View file

@ -42,8 +42,11 @@
{<<"Cache-Control">>, <<"max-age=0, no-cache, no-store">>}).
-define(OPTIONS_HEADER,
[?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
[?CT_PLAIN,
?AC_ALLOW_ORIGIN,
?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS,
?AC_MAX_AGE]).
-define(HEADER,
[?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS, ?NO_CACHE]).

View file

@ -48,37 +48,62 @@
-define(CINFO, ";49;92m"). % green
-define(CWARNING, ";49;93m"). % light yellow
-define(CERROR, ";49;91m"). % light magenta
-define(CCRITICAL,";49;31m"). % light red
-define(CCRITICAL, ";49;31m"). % light red
-define(DEBUG(Format, Args),
begin ?LOG_DEBUG(Format, Args,
#{clevel => ?CLEAD ++ ?CDEBUG,
ctext => ?CMID ++ ?CDEBUG}),
ok end).
begin
?LOG_DEBUG(Format,
Args,
#{
clevel => ?CLEAD ++ ?CDEBUG,
ctext => ?CMID ++ ?CDEBUG
}),
ok
end).
-define(INFO_MSG(Format, Args),
begin ?LOG_INFO(Format, Args,
#{clevel => ?CLEAD ++ ?CINFO,
ctext => ?CCLEAN}),
ok end).
begin
?LOG_INFO(Format,
Args,
#{
clevel => ?CLEAD ++ ?CINFO,
ctext => ?CCLEAN
}),
ok
end).
-define(WARNING_MSG(Format, Args),
begin ?LOG_WARNING(Format, Args,
#{clevel => ?CLEAD ++ ?CWARNING,
ctext => ?CMID ++ ?CWARNING}),
ok end).
begin
?LOG_WARNING(Format,
Args,
#{
clevel => ?CLEAD ++ ?CWARNING,
ctext => ?CMID ++ ?CWARNING
}),
ok
end).
-define(ERROR_MSG(Format, Args),
begin ?LOG_ERROR(Format, Args,
#{clevel => ?CLEAD ++ ?CERROR,
ctext => ?CMID ++ ?CERROR}),
ok end).
begin
?LOG_ERROR(Format,
Args,
#{
clevel => ?CLEAD ++ ?CERROR,
ctext => ?CMID ++ ?CERROR
}),
ok
end).
-define(CRITICAL_MSG(Format, Args),
begin ?LOG_CRITICAL(Format, Args,
#{clevel => ?CLEAD++ ?CCRITICAL,
ctext => ?CMID ++ ?CCRITICAL}),
ok end).
begin
?LOG_CRITICAL(Format,
Args,
#{
clevel => ?CLEAD ++ ?CCRITICAL,
ctext => ?CMID ++ ?CCRITICAL
}),
ok
end).
-endif.
%% Use only when trying to troubleshoot test problem with ExUnit

View file

@ -18,8 +18,12 @@
%%%
%%%----------------------------------------------------------------------
-record(motd, {server = <<"">> :: binary(),
packet = #xmlel{} :: xmlel()}).
-record(motd, {
server = <<"">> :: binary(),
packet = #xmlel{} :: xmlel()
}).
-record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1',
dummy = [] :: [] | '_'}).
-record(motd_users, {
us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1',
dummy = [] :: [] | '_'
}).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-define(MODULE_ANTISPAM, mod_antispam).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(caps_features,
{node_pair = {<<"">>, <<"">>} :: {binary(), binary()},

View file

@ -18,6 +18,8 @@
%%%
%%%----------------------------------------------------------------------
-record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()},
-record(last_activity, {
us = {<<"">>, <<"">>} :: {binary(), binary()},
timestamp = 0 :: non_neg_integer(),
status = <<"">> :: binary()}).
status = <<"">> :: binary()
}).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(archive_msg,
{us = {<<"">>, <<"">>} :: {binary(), binary()},

View file

@ -18,8 +18,8 @@
%%%
%%%----------------------------------------------------------------------
-record(room_version,
{id :: binary(),
-record(room_version, {
id :: binary(),
%% use the same field names as in Synapse
enforce_key_validity :: boolean(),
special_case_aliases_auth :: boolean(),

View file

@ -18,19 +18,25 @@
%%%
%%%----------------------------------------------------------------------
-record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} |
-record(muc_room, {
name_host = {<<"">>, <<"">>} :: {binary(), binary()} |
{'_', binary()},
opts = [] :: list() | '_'}).
opts = [] :: list() | '_'
}).
-record(muc_registered,
{us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1',
nick = <<"">> :: binary()}).
-record(muc_registered, {
us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1',
nick = <<"">> :: binary()
}).
-record(muc_online_room,
{name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_',
pid :: pid() | '$2' | '_' | '$1'}).
-record(muc_online_room, {
name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_',
pid :: pid() | '$2' | '_' | '$1'
}).
-record(muc_online_users, {us :: {binary(), binary()},
-record(muc_online_users, {
us :: {binary(), binary()},
resource :: binary() | '_',
room :: binary() | '_' | '$1',
host :: binary() | '_' | '$2'}).
host :: binary() | '_' | '$2'
}).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-define(MAX_USERS_DEFAULT, 200).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(offline_msg,
{us = {<<"">>, <<"">>} :: {binary(), binary()},

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(privacy, {us = {<<"">>, <<"">>} :: {binary(), binary()},
default = none :: none | binary(),

View file

@ -18,7 +18,11 @@
%%%
%%%----------------------------------------------------------------------
-record(private_storage,
{usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() |
'$1' | '_'},
xml = #xmlel{} :: xmlel() | '_' | '$1'}).
-record(private_storage, {
usns = {<<"">>, <<"">>, <<"">>} :: {binary(),
binary(),
binary() |
'$1' |
'_'},
xml = #xmlel{} :: xmlel() | '_' | '$1'
}).

View file

@ -68,6 +68,8 @@
%% RFC 1928 defined timeout.
-define(SOCKS5_REPLY_TIMEOUT, 10000).
-record(s5_request, {rsv = 0 :: integer(),
-record(s5_request, {
rsv = 0 :: integer(),
cmd = connect :: connect | udp,
sha1 = <<"">> :: binary()}).
sha1 = <<"">> :: binary()
}).

View file

@ -16,6 +16,9 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(push_session,
{us = {<<"">>, <<"">>} :: {binary(), binary()},
timestamp = erlang:timestamp() :: erlang:timestamp(),

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-record(roster,
{

View file

@ -18,8 +18,12 @@
%%%
%%%----------------------------------------------------------------------
-record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()},
opts = [] :: list() | '_' | '$2'}).
-record(sr_group, {
group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()},
opts = [] :: list() | '_' | '$2'
}).
-record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()},
group_host = {<<"">>, <<"">>} :: {binary(), binary()}}).
-record(sr_user, {
us = {<<"">>, <<"">>} :: {binary(), binary()},
group_host = {<<"">>, <<"">>} :: {binary(), binary()}
}).

View file

@ -18,11 +18,35 @@
%%%
%%%----------------------------------------------------------------------
-record(vcard_search,
{us, user, luser, fn, lfn, family, lfamily, given,
lgiven, middle, lmiddle, nickname, lnickname, bday,
lbday, ctry, lctry, locality, llocality, email, lemail,
orgname, lorgname, orgunit, lorgunit}).
-record(vcard_search, {
us,
user,
luser,
fn,
lfn,
family,
lfamily,
given,
lgiven,
middle,
lmiddle,
nickname,
lnickname,
bday,
lbday,
ctry,
lctry,
locality,
llocality,
email,
lemail,
orgname,
lorgname,
orgunit,
lorgunit
}).
-record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(),
vcard = #xmlel{} :: xmlel()}).
-record(vcard, {
us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(),
vcard = #xmlel{} :: xmlel()
}).

View file

@ -15,6 +15,9 @@
%%% limitations under the License.
%%%
%%%-------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
-define(MQTT_VERSION_4, 4).
-define(MQTT_VERSION_5, 5).

View file

@ -17,6 +17,8 @@
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%% @efmt:off
%% @indent-begin
%% -------------------------------
%% Pubsub constants

View file

@ -1,5 +1,6 @@
-module(configure_deps).
-export(['configure-deps'/2]).
'configure-deps'(Config, Vals) ->
{ok, Config}.

View file

@ -1,6 +1,7 @@
-module(deps_erl_opts).
-export([preprocess/2]).
preprocess(Config, Dirs) ->
ExtraOpts = rebar_config:get(Config, deps_erl_opts, []),
Opts = rebar_config:get(Config, erl_opts, []),
@ -8,5 +9,7 @@ preprocess(Config, Dirs) ->
lists:keystore(element(1, Opt), 1, Acc, Opt);
(Opt, Acc) ->
[Opt | lists:delete(Opt, Acc)]
end, Opts, ExtraOpts),
end,
Opts,
ExtraOpts),
{ok, rebar_config:set(Config, erl_opts, NewOpts), []}.

View file

@ -1,9 +1,11 @@
-module(override_deps_versions2).
-export([preprocess/2, 'pre_update-deps'/2, new_replace/1, new_replace/0]).
preprocess(Config, _Dirs) ->
update_deps(Config).
update_deps(Config) ->
LocalDeps = rebar_config:get_local(Config, deps, []),
TopDeps = case rebar_config:get_xconf(Config, top_deps, []) of
@ -16,7 +18,8 @@ update_deps(Config) ->
false -> Dep;
TopDep -> TopDep
end
end, LocalDeps),
end,
LocalDeps),
%io:format("LD ~p~n", [LocalDeps]),
%io:format("TD ~p~n", [TopDeps]),
@ -38,13 +41,17 @@ update_deps(Config) ->
end,
{ok, Config2}.
new_replace() ->
old_rebar_config:new().
new_replace(Config) ->
NC = old_rebar_config:new(Config),
{ok, Conf, _} = update_deps(NC),
Conf.
replace_mod(Beam) ->
{ok, {_, [{exports, Exports}]}} = beam_lib:chunks(Beam, [exports]),
Funcs = lists:filtermap(
@ -65,13 +72,15 @@ replace_mod(Beam) ->
Args)]
end,
{true, erl_syntax:function(erl_syntax:abstract(Name),
[erl_syntax:clause(Args, none,
[erl_syntax:clause(Args,
none,
Call)])}
end, Exports),
end,
Exports),
Forms0 = ([erl_syntax:attribute(erl_syntax:abstract(module),
[erl_syntax:abstract(rebar_config)])]
++ Funcs),
Forms = [erl_syntax:revert(Form) || Form <- Forms0],
[erl_syntax:abstract(rebar_config)])] ++
Funcs),
Forms = [ erl_syntax:revert(Form) || Form <- Forms0 ],
%io:format("--------------------------------------------------~n"
% "~s~n",
% [[erl_pp:form(Form) || Form <- Forms]]),
@ -83,15 +92,18 @@ replace_mod(Beam) ->
args(0) ->
[];
args(N) ->
[arg(N) | args(N-1)].
[arg(N) | args(N - 1)].
arg(N) ->
erl_syntax:variable(list_to_atom("A"++integer_to_list(N))).
erl_syntax:variable(list_to_atom("A" ++ integer_to_list(N))).
rename(BeamBin0, Name) ->
BeamBin = replace_in_atab(BeamBin0, Name),
update_form_size(BeamBin).
%% Replace the first atom of the atom table with the new name
replace_in_atab(<<"Atom", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) ->
replace_first_atom(<<"Atom">>, Cnk, CnkSz0, Rest, latin1, Name);
@ -100,6 +112,7 @@ replace_in_atab(<<"AtU8", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) ->
replace_in_atab(<<C, Rest/binary>>, Name) ->
<<C, (replace_in_atab(Rest, Name))/binary>>.
replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) ->
<<NumAtoms:32, NameSz0:8, _Name0:NameSz0/binary, CnkRest/binary>> = Cnk,
NumPad0 = num_pad_bytes(CnkSz0),
@ -120,7 +133,8 @@ num_pad_bytes(BinSize) ->
N -> N
end.
%% Update the size within the top-level form
update_form_size(<<"FOR1", _OldSz:32, Rest/binary>> = Bin) ->
Sz = size(Bin) - 8,
<<"FOR1", Sz:32, Rest/binary>>.
<<"FOR1", Sz:32, Rest/binary>>.

View file

@ -1,20 +1,28 @@
-module(override_opts).
-export([preprocess/2]).
override_opts(override, Config, Opts) ->
lists:foldl(fun({Opt, Value}, Conf) ->
rebar_config:set(Conf, Opt, Value)
end, Config, Opts);
end,
Config,
Opts);
override_opts(add, Config, Opts) ->
lists:foldl(fun({Opt, Value}, Conf) ->
V = rebar_config:get_local(Conf, Opt, []),
rebar_config:set(Conf, Opt, V ++ Value)
end, Config, Opts);
end,
Config,
Opts);
override_opts(del, Config, Opts) ->
lists:foldl(fun({Opt, Value}, Conf) ->
V = rebar_config:get_local(Conf, Opt, []),
rebar_config:set(Conf, Opt, V -- Value)
end, Config, Opts).
end,
Config,
Opts).
preprocess(Config, _Dirs) ->
Overrides = rebar_config:get_local(Config, overrides, []),
@ -26,14 +34,16 @@ preprocess(Config, _Dirs) ->
try
Config3 = case rebar_app_utils:load_app_file(Config2, _Dirs) of
{ok, C, AppName, _AppData} ->
lists:foldl(fun({Type, AppName2, Opts}, Conf1) when
AppName2 == AppName ->
lists:foldl(fun({Type, AppName2, Opts}, Conf1)
when AppName2 == AppName ->
override_opts(Type, Conf1, Opts);
({Type, Opts}, Conf1a) ->
override_opts(Type, Conf1a, Opts);
(_, Conf2) ->
Conf2
end, C, TopOverrides);
end,
C,
TopOverrides);
_ ->
Config2
end,

View file

@ -23,63 +23,70 @@
%%%
{deps, [{if_not_rebar3,
{if_version_below, "24",
{base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}
}},
{if_version_below,
"24",
{base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}}},
{cache_tab, "~> 1.0.33", {git, "https://github.com/processone/cache_tab", {tag, "1.0.33"}}},
{eimp, "~> 1.0.26", {git, "https://github.com/processone/eimp", {tag, "1.0.26"}}},
{if_var_true, pam,
{if_var_true,
pam,
{epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}},
{if_var_true, redis,
{if_var_true,
redis,
{if_not_rebar3,
{eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}
}},
{if_var_true, redis,
{eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}}},
{if_var_true,
redis,
{if_rebar3,
{if_version_below, "21",
{if_version_below,
"21",
{eredis, "1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}},
{eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}}
}}},
{if_var_true, sip,
{eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}}}}},
{if_var_true,
sip,
{esip, "~> 1.0.59", {git, "https://github.com/processone/esip", {tag, "1.0.59"}}}},
{if_var_true, zlib,
{if_var_true,
zlib,
{ezlib, "~> 1.0.15", {git, "https://github.com/processone/ezlib", {tag, "1.0.15"}}}},
{fast_tls, "~> 1.1.25", {git, "https://github.com/processone/fast_tls", {tag, "1.1.25"}}},
{fast_xml, "~> 1.1.57", {git, "https://github.com/processone/fast_xml", {tag, "1.1.57"}}},
{fast_yaml, "~> 1.0.39", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.39"}}},
{idna, "~> 6.0", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}},
{if_version_below, "27",
{jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}
},
{if_version_above, "23",
{if_version_below,
"27",
{jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}},
{if_version_above,
"23",
{jose, "~> 1.11.10", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.10"}}},
{jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
},
{if_version_below, "22",
{lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}
},
{if_var_true, lua,
{jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}},
{if_version_below,
"22",
{lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}},
{if_var_true,
lua,
{if_version_below, "21",
{luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}},
{luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}}
}},
{luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}}}},
{mqtree, "~> 1.0.19", {git, "https://github.com/processone/mqtree", {tag, "1.0.19"}}},
{p1_acme, "~> 1.0.28", {git, "https://github.com/processone/p1_acme", {tag, "1.0.28"}}},
{if_var_true, mysql,
{if_var_true,
mysql,
{p1_mysql, "~> 1.0.26", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.26"}}}},
{p1_oauth2, "~> 0.6.14", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.14"}}},
{if_var_true, pgsql,
{if_var_true,
pgsql,
{p1_pgsql, "~> 1.1.35", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.35"}}}},
{p1_utils, "~> 1.0.28", {git, "https://github.com/processone/p1_utils", {tag, "1.0.28"}}},
{pkix, "~> 1.0.10", {git, "https://github.com/processone/pkix", {tag, "1.0.10"}}},
{if_var_true, sqlite,
{if_var_true,
sqlite,
{sqlite3, "~> 1.1.15", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.15"}}}},
{stringprep, "~> 1.0.33", {git, "https://github.com/processone/stringprep", {tag, "1.0.33"}}},
{if_var_true, stun,
{if_var_true,
stun,
{stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}},
{xmpp, "~> 1.11.1", {git, "https://github.com/processone/xmpp", {tag, "1.11.1"}}},
{yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}}
]}.
{yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}}]}.
{gitonly_deps, [ejabberd_po]}.
@ -159,22 +166,17 @@
%% https://github.com/Supersonido/rebar_mix/issues/27#issuecomment-894873335
%% Let's use this fixed rebar_mix fork, see its PR:
%% https://github.com/Supersonido/rebar_mix/pull/31
{if_var_true, elixir, {rebar_mix, ".*",
{if_var_true, elixir,
{rebar_mix, ".*",
{git, "https://github.com/bsanyi/rebar_mix.git",
{branch, "consolidation_fix"}}}
}]}}.
{branch, "consolidation_fix"}}}}]}}.
{if_rebar3, {project_plugins, [configure_deps,
{if_var_true, tools, rebar3_format},
{if_var_true, tools, {rebar3_lint, "4.1.1"}}
]}}.
{if_not_rebar3, {plugins, [
deps_erl_opts, override_deps_versions2, override_opts, configure_deps
]}}.
{if_var_true, tools, rebar3_efmt},
{if_var_true, tools, {rebar3_lint, "4.1.1"}}]}}.
{if_not_rebar3, {plugins, [deps_erl_opts, override_deps_versions2, override_opts, configure_deps]}}.
{if_rebar3, {if_var_true, elixir,
{provider_hooks, [
{post, [{compile, {mix, consolidate_protocols}}]}
]}}}.
{provider_hooks, [{post, [{compile, {mix, consolidate_protocols}}]}]}}}.
%% Compiling Jose 1.11.10 with Erlang/OTP 27.0 throws warnings on public_key deprecated functions
{if_rebar3, {overrides, [{del, jose, [{erl_opts, [warnings_as_errors]}]}]}}.
@ -192,16 +194,13 @@
{if_rebar3,
{xref_checks,
[deprecated_function_calls, deprecated_functions, locals_not_used,
undefined_function_calls, undefined_functions]}
}.
undefined_function_calls, undefined_functions]}}.
{if_not_rebar3,
{xref_checks,
[deprecated_function_calls, deprecated_functions,
undefined_function_calls, undefined_functions]}
}.
undefined_function_calls, undefined_functions]}}.
{xref_exclusions, [
"(\"gen_transport\":_/_)",
{xref_exclusions, ["(\"gen_transport\":_/_)",
"(\"eprof\":_/_)",
{if_var_false, elixir, "(\"Elixir.*\":_/_)"},
{if_var_false, http, "(\"lhttpc\":_/_)"},
@ -222,11 +221,26 @@
{dialyzer, [{get_warnings, false}, % Show warnings of dependencies
{if_version_above, "25",
{plt_extra_apps,
[asn1, odbc, public_key, stdlib, syntax_tools,
idna, jose,
cache_tab, eimp, fast_tls, fast_xml, fast_yaml,
mqtree, p1_acme, p1_oauth2, p1_utils, pkix,
stringprep, xmpp, yconf,
[asn1,
odbc,
public_key,
stdlib,
syntax_tools,
idna,
jose,
cache_tab,
eimp,
fast_tls,
fast_xml,
fast_yaml,
mqtree,
p1_acme,
p1_oauth2,
p1_utils,
pkix,
stringprep,
xmpp,
yconf,
{if_version_below, "27", jiffy},
{if_var_true, pam, epam},
{if_var_true, redis, eredis},
@ -238,8 +252,19 @@
{if_var_true, stun, stun},
{if_var_true, sqlite, sqlite3}]},
{plt_extra_apps, % For Erlang/OTP 25 and older
[cache_tab, eimp, fast_tls, fast_xml, fast_yaml,
mqtree, p1_acme, p1_oauth2, p1_utils, pkix, stringprep, xmpp, yconf,
[cache_tab,
eimp,
fast_tls,
fast_xml,
fast_yaml,
mqtree,
p1_acme,
p1_oauth2,
p1_utils,
pkix,
stringprep,
xmpp,
yconf,
{if_var_true, pam, epam},
{if_var_true, redis, eredis},
{if_var_true, sip, esip},
@ -248,8 +273,7 @@
{if_var_true, mysql, p1_mysql},
{if_var_true, pgsql, p1_pgsql},
{if_var_true, stun, stun},
{if_var_true, sqlite, sqlite3}]}
} ]}.
{if_var_true, sqlite, sqlite3}]}}]}.
{ct_opts, [{keep_logs, 20}]}.
@ -277,12 +301,10 @@
{copy, "{{base_dir}}/consolidated/*", "lib/ejabberd-{{release_version}}/ebin/"},
{copy, "rel/overlays/iex", "releases/{{release_version}}/"},
{if_var_true, elixir,
{template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"}
},
{template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"}},
{copy, "inetrc", "conf/inetrc"},
{copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"},
{copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]}
]}.
{copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]}]}.
{profiles, [{prod, [{relx, [{debug_info, strip},
{dev_mode, false},
@ -311,8 +333,7 @@
--config rel/relive.config \
--eval sync:go(). \
--script rel/relive.escript \
--name ejabberd@localhost"}]}
]}.
--name ejabberd@localhost"}]}]}.
%% Local Variables:
%% mode: erlang

View file

@ -4,23 +4,34 @@
%% ex: ft=erlang ts=4 sw=4 et
-define(TIMEOUT, 60000).
-define(INFO(Fmt,Args), io:format(Fmt,Args)).
-define(INFO(Fmt, Args), io:format(Fmt, Args)).
main([NodeName, Cookie, ReleasePackage]) ->
TargetNode = start_distribution(NodeName, Cookie),
{ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release,
[ReleasePackage], ?TIMEOUT),
{ok, Vsn} = rpc:call(TargetNode,
release_handler,
unpack_release,
[ReleasePackage],
?TIMEOUT),
?INFO("Unpacked Release ~p~n", [Vsn]),
{ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler,
check_install_release, [Vsn], ?TIMEOUT),
{ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler,
install_release, [Vsn], ?TIMEOUT),
{ok, OtherVsn, Desc} = rpc:call(TargetNode,
release_handler,
check_install_release,
[Vsn],
?TIMEOUT),
{ok, OtherVsn, Desc} = rpc:call(TargetNode,
release_handler,
install_release,
[Vsn],
?TIMEOUT),
?INFO("Installed Release ~p~n", [Vsn]),
ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT),
?INFO("Made Release ~p Permanent~n", [Vsn]);
main(_) ->
init:stop(1).
start_distribution(NodeName, Cookie) ->
MyNode = make_script_node(NodeName),
{ok, _Pid} = net_kernel:start([MyNode, shortnames]),
@ -36,9 +47,11 @@ start_distribution(NodeName, Cookie) ->
end,
TargetNode.
make_target_node(Node) ->
[_, Host] = string:tokens(atom_to_list(node()), "@"),
list_to_atom(lists:concat([Node, "@", Host])).
make_script_node(Node) ->
list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])).

View file

@ -1,5 +1,6 @@
#!/usr/bin/env escript
main(_) ->
Base = "_build/relive",
prepare(Base, "", none),
@ -9,6 +10,7 @@ main(_) ->
c:erlangrc([os:cmd("echo -n $HOME")]),
ok.
prepare(BaseDir, SuffixDir, MFA) ->
Dir = filename:join(BaseDir, SuffixDir),
case file:make_dir(Dir) of

File diff suppressed because it is too large Load diff

View file

@ -27,8 +27,12 @@
-export([validator/1, validators/0]).
-export([loaded_shared_roster_module/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include("logger.hrl").
@ -51,26 +55,32 @@
{ip, ip_mask()}.
-type access() :: [{action(), [access_rule()]}].
-type acl() :: atom() | access().
-type match() :: #{ip => inet:ip_address(),
-type match() :: #{
ip => inet:ip_address(),
usr => jid:ljid(),
atom() => term()}.
atom() => term()
}.
-export_type([acl/0, acl_rule/0, access/0, access_rule/0, match/0]).
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec match_rule(global | binary(), atom() | access(),
-spec match_rule(global | binary(),
atom() | access(),
jid:jid() | jid:ljid() | inet:ip_address() | match()) -> action().
match_rule(_, all, _) ->
allow;
match_rule(_, none, _) ->
deny;
match_rule(Host, Access, Match) when is_map(Match) ->
Rules = if is_atom(Access) -> read_access(Access, Host);
Rules = if
is_atom(Access) -> read_access(Access, Host);
true -> Access
end,
match_rules(Host, Rules, Match, deny);
@ -79,6 +89,7 @@ match_rule(Host, Access, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 ->
match_rule(Host, Access, JID) ->
match_rule(Host, Access, #{usr => jid:tolower(JID)}).
-spec match_acl(global | binary(), access_rule(), match()) -> boolean().
match_acl(_Host, {acl, all}, _) ->
true;
@ -88,7 +99,8 @@ match_acl(Host, {acl, ACLName}, Match) ->
lists:any(
fun(ACL) ->
match_acl(Host, ACL, Match)
end, read_acl(ACLName, Host));
end,
read_acl(ACLName, Host));
match_acl(_Host, {ip, {Net, Mask}}, #{ip := {IP, _Port}}) ->
misc:match_ip_mask(IP, Net, Mask);
match_acl(_Host, {ip, {Net, Mask}}, #{ip := IP}) ->
@ -131,6 +143,7 @@ match_acl(_Host, {node_glob, {UR, SR}}, #{usr := {U, S, _}}) ->
match_acl(_, _, _) ->
false.
-spec match_rules(global | binary(), [{T, [access_rule()]}], match(), T) -> T.
match_rules(Host, [{Return, Rules} | Rest], Match, Default) ->
case match_acls(Host, Rules, Match) of
@ -142,6 +155,7 @@ match_rules(Host, [{Return, Rules} | Rest], Match, Default) ->
match_rules(_Host, [], _Match, Default) ->
Default.
-spec match_acls(global | binary(), [access_rule()], match()) -> boolean().
match_acls(_Host, [], _Match) ->
false;
@ -149,12 +163,15 @@ match_acls(Host, Rules, Match) ->
lists:all(
fun(Rule) ->
match_acl(Host, Rule, Match)
end, Rules).
end,
Rules).
-spec reload_from_config() -> ok.
reload_from_config() ->
gen_server:call(?MODULE, reload_from_config, timer:minutes(1)).
-spec validator(access_rules | acl) -> econf:validator().
validator(access_rules) ->
econf:options(
@ -165,6 +182,7 @@ validator(acl) ->
#{'_' => acl_validator()},
[{disallowed, [all, none]}, unique]).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -177,6 +195,7 @@ init([]) ->
ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20),
{ok, #{hosts => Hosts}}.
-spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}.
handle_call(reload_from_config, _, State) ->
NewHosts = ejabberd_option:hosts(),
@ -186,24 +205,29 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
-spec handle_info(term(), state()) -> {noreply, state()}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(any(), state()) -> ok.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20).
-spec code_change(term(), state(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -217,18 +241,21 @@ load_from_config(NewHosts) ->
load_tab(access, NewHosts, fun ejabberd_option:access_rules/1),
?DEBUG("Access rules loaded successfully", []).
-spec create_tab(atom()) -> atom().
create_tab(Tab) ->
_ = mnesia:delete_table(Tab),
ets:new(Tab, [named_table, set, {read_concurrency, true}]).
-spec load_tab(atom(), [binary()], fun((global | binary()) -> {atom(), list()})) -> ok.
load_tab(Tab, Hosts, Fun) ->
Old = ets:tab2list(Tab),
New = lists:flatmap(
fun(Host) ->
[{{Name, Host}, List} || {Name, List} <- Fun(Host)]
end, [global|Hosts]),
[ {{Name, Host}, List} || {Name, List} <- Fun(Host) ]
end,
[global | Hosts]),
ets:insert(Tab, New),
lists:foreach(
fun({Key, _}) ->
@ -236,7 +263,9 @@ load_tab(Tab, Hosts, Fun) ->
false -> ets:delete(Tab, Key);
true -> ok
end
end, Old).
end,
Old).
-spec read_access(atom(), global | binary()) -> access().
read_access(Name, Host) ->
@ -245,6 +274,7 @@ read_access(Name, Host) ->
[] -> []
end.
-spec read_acl(atom(), global | binary()) -> [acl_rule()].
read_acl(Name, Host) ->
case ets:lookup(acl, {Name, Host}) of
@ -252,11 +282,13 @@ read_acl(Name, Host) ->
[] -> []
end.
%%%===================================================================
%%% Validators
%%%===================================================================
validators() ->
#{ip => econf:list_or_single(econf:ip_mask()),
#{
ip => econf:list_or_single(econf:ip_mask()),
user => user_validator(econf:user(), econf:domain()),
user_regexp => user_validator(econf:re([unicode]), econf:domain()),
user_glob => user_validator(econf:glob([unicode]), econf:domain()),
@ -269,51 +301,62 @@ validators() ->
node_regexp => node_validator(econf:re([unicode]), econf:re([unicode])),
node_glob => node_validator(econf:glob([unicode]), econf:glob([unicode])),
shared_group => user_validator(econf:binary(), econf:domain()),
acl => econf:atom()}.
acl => econf:atom()
}.
rule_validator() ->
rule_validator(validators()).
rule_validator(RVs) ->
econf:and_then(
econf:non_empty(econf:options(RVs, [])),
fun(Rules) ->
lists:flatmap(
fun({Type, Rs}) when is_list(Rs) ->
[{Type, R} || R <- Rs];
[ {Type, R} || R <- Rs ];
(Other) ->
[Other]
end, Rules)
end,
Rules)
end).
access_validator() ->
econf:and_then(
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) -> {(econf:atom())(K), V};
(A) -> {acl, (econf:atom())(A)}
end, lists:flatten(L));
end,
lists:flatten(L));
(A) ->
[{acl, (econf:atom())(A)}]
end,
rule_validator()).
access_rules_validator() ->
econf:and_then(
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) -> {(econf:atom())(K), V};
(A) -> {(econf:atom())(A), [{acl, all}]}
end, lists:flatten(L));
end,
lists:flatten(L));
(Bad) ->
Bad
end,
econf:non_empty(
econf:options(
#{allow => access_validator(),
deny => access_validator()},
#{
allow => access_validator(),
deny => access_validator()
},
[]))).
acl_validator() ->
econf:and_then(
fun(L) when is_list(L) -> lists:flatten(L);
@ -321,6 +364,7 @@ acl_validator() ->
end,
rule_validator(maps:remove(acl, validators()))).
user_validator(UV, SV) ->
econf:and_then(
econf:list_or_single(
@ -338,6 +382,7 @@ user_validator(UV, SV) ->
end),
fun lists:flatten/1).
node_validator(UV, SV) ->
econf:and_then(
econf:and_then(
@ -345,6 +390,7 @@ node_validator(UV, SV) ->
fun lists:flatten/1),
econf:map(UV, SV)).
%%%===================================================================
%%% Aux
%%%===================================================================
@ -352,6 +398,7 @@ node_validator(UV, SV) ->
match_regexp(Data, RegExp) ->
re:run(Data, RegExp) /= nomatch.
-spec loaded_shared_roster_module(global | binary()) -> atom().
loaded_shared_roster_module(global) ->
loaded_shared_roster_module(ejabberd_config:get_myname());

View file

@ -65,28 +65,37 @@
-export_type([validator/0, validator/1, validators/0]).
-export_type([error_reason/0, error_return/0]).
%%%===================================================================
%%% API
%%%===================================================================
parse(File, Validators, Options) ->
try yconf:parse(File, Validators, Options)
catch _:{?MODULE, Reason, Ctx} ->
try
yconf:parse(File, Validators, Options)
catch
_:{?MODULE, Reason, Ctx} ->
{error, Reason, Ctx}
end.
validate(Validator, Y) ->
try yconf:validate(Validator, Y)
catch _:{?MODULE, Reason, Ctx} ->
try
yconf:validate(Validator, Y)
catch
_:{?MODULE, Reason, Ctx} ->
{error, Reason, Ctx}
end.
replace_macros(Y) ->
yconf:replace_macros(Y).
-spec fail(error_reason()) -> no_return().
fail(Reason) ->
yconf:fail(?MODULE, Reason).
format_error({bad_module, Mod}, Ctx)
when Ctx == [listen, module];
Ctx == [listen, request_handlers] ->
@ -105,7 +114,8 @@ format_error({bad_module, Mod}, Ctx)
"Elixir.Mod" ++ _ -> true;
_ -> false
end
end, ejabberd_config:beams(all)),
end,
ejabberd_config:beams(all)),
format("~ts: unknown ~ts: ~ts. Did you mean ~ts?",
[yconf:format_ctx(Ctx),
format_module_type(Ctx),
@ -127,7 +137,7 @@ format_error({bad_export, {F, A}, Mod}, Ctx)
format("~ts: third-party ~ts '~ts' doesn't export "
"function ~ts/~B. If it's really a ~ts, "
"consider to upgrade it",
[Slogan, Type, format_module(Mod),F, A, Type]);
[Slogan, Type, format_module(Mod), F, A, Type]);
false ->
format("~ts: '~ts' doesn't match any known ~ts",
[Slogan, format_module(Mod), Type])
@ -151,6 +161,7 @@ format_error({bad_yaml, _, _} = Why, _) ->
format_error(Reason, Ctx) ->
yconf:format_ctx(Ctx) ++ ": " ++ format_error(Reason).
format_error({bad_db_type, _, Atom}) ->
format("unsupported database: ~ts", [Atom]);
format_error({bad_lang, Lang}) ->
@ -198,13 +209,16 @@ format_error(eimp_error) ->
format_error({mqtt_codec, Reason}) ->
mqtt_codec:format_error(Reason);
format_error({external_module_error, Module, Error}) ->
try Module:format_error(Error)
catch _:_ ->
try
Module:format_error(Error)
catch
_:_ ->
format("Invalid value", [])
end;
format_error(Reason) ->
yconf:format_error(Reason).
-spec format_module(atom() | string()) -> string().
format_module(Mod) when is_atom(Mod) ->
format_module(atom_to_list(Mod));
@ -214,6 +228,7 @@ format_module(Mod) ->
M -> M
end.
format_module_type([listen, module]) ->
"listening module";
format_module_type([listen, request_handlers]) ->
@ -221,18 +236,21 @@ format_module_type([listen, request_handlers]) ->
format_module_type([modules]) ->
"ejabberd module".
format_known(_, Known) when length(Known) > 20 ->
"";
format_known(Prefix, Known) ->
[Prefix, " are: ", format_join(Known)].
format_join([]) ->
"(empty)";
format_join([H|_] = L) when is_atom(H) ->
format_join([atom_to_binary(A, utf8) || A <- L]);
format_join([H | _] = L) when is_atom(H) ->
format_join([ atom_to_binary(A, utf8) || A <- L ]);
format_join(L) ->
str:join(lists:sort(L), <<", ">>).
%% All duplicated options having list-values are grouped
%% into a single option with all list-values being concatenated
-spec group_dups(list(T)) -> list(T).
@ -244,11 +262,14 @@ group_dups(Y1) ->
{Option, Vals} when is_list(Vals) ->
lists:keyreplace(Option, 1, Acc, {Option, Vals ++ Values});
_ ->
[{Option, Values}|Acc]
[{Option, Values} | Acc]
end;
(Other, Acc) ->
[Other|Acc]
end, [], Y1)).
[Other | Acc]
end,
[],
Y1)).
%%%===================================================================
%%% Validators from yconf
@ -256,156 +277,206 @@ group_dups(Y1) ->
pos_int() ->
yconf:pos_int().
pos_int(Inf) ->
yconf:pos_int(Inf).
non_neg_int() ->
yconf:non_neg_int().
non_neg_int(Inf) ->
yconf:non_neg_int(Inf).
int() ->
yconf:int().
int(Min, Max) ->
yconf:int(Min, Max).
number(Min) ->
yconf:number(Min).
octal() ->
yconf:octal().
binary() ->
yconf:binary().
binary(Re) ->
yconf:binary(Re).
binary(Re, Opts) ->
yconf:binary(Re, Opts).
enum(L) ->
yconf:enum(L).
bool() ->
yconf:bool().
atom() ->
yconf:atom().
string() ->
yconf:string().
string(Re) ->
yconf:string(Re).
string(Re, Opts) ->
yconf:string(Re, Opts).
any() ->
yconf:any().
url() ->
yconf:url().
url(Schemes) ->
yconf:url(Schemes).
file() ->
yconf:file().
file(Type) ->
yconf:file(Type).
directory() ->
yconf:directory().
directory(Type) ->
yconf:directory(Type).
ip() ->
yconf:ip().
ipv4() ->
yconf:ipv4().
ipv6() ->
yconf:ipv6().
ip_mask() ->
yconf:ip_mask().
port() ->
yconf:port().
re() ->
yconf:re().
re(Opts) ->
yconf:re(Opts).
glob() ->
yconf:glob().
glob(Opts) ->
yconf:glob(Opts).
path() ->
yconf:path().
binary_sep(Sep) ->
yconf:binary_sep(Sep).
timeout(Units) ->
yconf:timeout(Units).
timeout(Units, Inf) ->
yconf:timeout(Units, Inf).
base64() ->
yconf:base64().
non_empty(F) ->
yconf:non_empty(F).
list(F) ->
yconf:list(F).
list(F, Opts) ->
yconf:list(F, Opts).
list_or_single(F) ->
yconf:list_or_single(F).
list_or_single(F, Opts) ->
yconf:list_or_single(F, Opts).
map(F1, F2) ->
yconf:map(F1, F2).
map(F1, F2, Opts) ->
yconf:map(F1, F2, Opts).
either(F1, F2) ->
yconf:either(F1, F2).
and_then(F1, F2) ->
yconf:and_then(F1, F2).
options(V) ->
yconf:options(V).
options(V, O) ->
yconf:options(V, O).
%%%===================================================================
%%% Custom validators
%%%===================================================================
beam() ->
beam([]).
beam(Exports) ->
and_then(
non_empty(binary()),
@ -417,32 +488,39 @@ beam(Exports) ->
(yconf:beam(Exports))(Val)
end).
acl() ->
either(
atom(),
acl:access_rules_validator()).
shaper() ->
either(
atom(),
ejabberd_shaper:shaper_rules_validator()).
-spec url_or_file() -> yconf:validator({file | url, binary()}).
url_or_file() ->
either(
and_then(url(), fun(URL) -> {url, URL} end),
and_then(file(), fun(File) -> {file, File} end)).
-spec lang() -> yconf:validator(binary()).
lang() ->
and_then(
binary(),
fun(Lang) ->
try xmpp_lang:check(Lang)
catch _:_ -> fail({bad_lang, Lang})
try
xmpp_lang:check(Lang)
catch
_:_ -> fail({bad_lang, Lang})
end
end).
-spec pem() -> yconf:validator(binary()).
pem() ->
and_then(
@ -455,16 +533,20 @@ pem() ->
end
end).
-spec jid() -> yconf:validator(jid:jid()).
jid() ->
and_then(
binary(),
fun(Val) ->
try jid:decode(Val)
catch _:{bad_jid, _} = Reason -> fail(Reason)
try
jid:decode(Val)
catch
_:{bad_jid, _} = Reason -> fail(Reason)
end
end).
-spec user() -> yconf:validator(binary()).
user() ->
and_then(
@ -476,6 +558,7 @@ user() ->
end
end).
-spec domain() -> yconf:validator(binary()).
domain() ->
and_then(
@ -486,11 +569,13 @@ domain() ->
unicode:characters_to_binary(idna:decode(binary_to_list(Domain)), utf8);
{<<"">>, Domain, <<"">>} -> Domain;
_ -> fail({bad_domain, Val})
catch _:{bad_jid, _} ->
catch
_:{bad_jid, _} ->
fail({bad_domain, Val})
end
end).
-spec resource() -> yconf:validator(binary()).
resource() ->
and_then(
@ -502,6 +587,7 @@ resource() ->
end
end).
-spec db_type(module()) -> yconf:validator(atom()).
db_type(M) ->
and_then(
@ -518,10 +604,12 @@ db_type(M) ->
end
end).
-spec queue_type() -> yconf:validator(ram | file).
queue_type() ->
enum([ram, file]).
-spec ldap_filter() -> yconf:validator(binary()).
ldap_filter() ->
and_then(
@ -533,7 +621,10 @@ ldap_filter() ->
end
end).
-ifdef(SIP).
sip_uri() ->
and_then(
binary(),
@ -543,8 +634,11 @@ sip_uri() ->
URI -> URI
end
end).
-endif.
-spec host() -> yconf:validator(binary()).
host() ->
fun(Domain) ->
@ -556,15 +650,18 @@ host() ->
end
end.
-spec hosts() -> yconf:validator([binary()]).
hosts() ->
list(host(), [unique]).
-spec vcard_temp() -> yconf:validator().
vcard_temp() ->
and_then(
vcard_validator(
vcard_temp, undefined,
vcard_temp,
undefined,
[{version, undefined, binary()},
{fn, undefined, binary()},
{n, undefined, vcard_name()},
@ -602,25 +699,30 @@ vcard_temp() ->
-spec vcard_name() -> yconf:validator().
vcard_name() ->
vcard_validator(
vcard_name, undefined,
vcard_name,
undefined,
[{family, undefined, binary()},
{given, undefined, binary()},
{middle, undefined, binary()},
{prefix, undefined, binary()},
{suffix, undefined, binary()}]).
-spec vcard_photo() -> yconf:validator().
vcard_photo() ->
vcard_validator(
vcard_photo, undefined,
vcard_photo,
undefined,
[{type, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_adr() -> yconf:validator().
vcard_adr() ->
vcard_validator(
vcard_adr, [],
vcard_adr,
[],
[{home, false, bool()},
{work, false, bool()},
{postal, false, bool()},
@ -636,10 +738,12 @@ vcard_adr() ->
{pcode, undefined, binary()},
{ctry, undefined, binary()}]).
-spec vcard_label() -> yconf:validator().
vcard_label() ->
vcard_validator(
vcard_label, [],
vcard_label,
[],
[{home, false, bool()},
{work, false, bool()},
{postal, false, bool()},
@ -649,10 +753,12 @@ vcard_label() ->
{pref, false, bool()},
{line, [], list(binary())}]).
-spec vcard_tel() -> yconf:validator().
vcard_tel() ->
vcard_validator(
vcard_tel, [],
vcard_tel,
[],
[{home, false, bool()},
{work, false, bool()},
{voice, false, bool()},
@ -668,10 +774,12 @@ vcard_tel() ->
{pref, false, bool()},
{number, undefined, binary()}]).
-spec vcard_email() -> yconf:validator().
vcard_email() ->
vcard_validator(
vcard_email, [],
vcard_email,
[],
[{home, false, bool()},
{work, false, bool()},
{internet, false, bool()},
@ -679,53 +787,67 @@ vcard_email() ->
{x400, false, bool()},
{userid, undefined, binary()}]).
-spec vcard_geo() -> yconf:validator().
vcard_geo() ->
vcard_validator(
vcard_geo, undefined,
vcard_geo,
undefined,
[{lat, undefined, binary()},
{lon, undefined, binary()}]).
-spec vcard_logo() -> yconf:validator().
vcard_logo() ->
vcard_validator(
vcard_logo, undefined,
vcard_logo,
undefined,
[{type, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_org() -> yconf:validator().
vcard_org() ->
vcard_validator(
vcard_org, undefined,
vcard_org,
undefined,
[{name, undefined, binary()},
{units, [], list(binary())}]).
-spec vcard_sound() -> yconf:validator().
vcard_sound() ->
vcard_validator(
vcard_sound, undefined,
vcard_sound,
undefined,
[{phonetic, undefined, binary()},
{binval, undefined, base64()},
{extval, undefined, binary()}]).
-spec vcard_key() -> yconf:validator().
vcard_key() ->
vcard_validator(
vcard_key, undefined,
vcard_key,
undefined,
[{type, undefined, binary()},
{cred, undefined, binary()}]).
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec db_module(module(), atom()) -> module().
db_module(M, Type) ->
try list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type))
catch _:system_limit ->
try
list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type))
catch
_:system_limit ->
fail({bad_length, 255})
end.
format_addr_port({IP, Port}) ->
IPStr = case tuple_size(IP) of
4 -> inet:ntoa(IP);
@ -733,23 +855,26 @@ format_addr_port({IP, Port}) ->
end,
IPStr ++ ":" ++ integer_to_list(Port).
-spec format(iolist(), list()) -> string().
format(Fmt, Args) ->
lists:flatten(io_lib:format(Fmt, Args)).
-spec vcard_validator(atom(), term(), [{atom(), term(), validator()}]) -> validator().
vcard_validator(Name, Default, Schema) ->
Defaults = [{Key, Val} || {Key, Val, _} <- Schema],
Defaults = [ {Key, Val} || {Key, Val, _} <- Schema ],
and_then(
options(
maps:from_list([{Key, Fun} || {Key, _, Fun} <- Schema]),
maps:from_list([ {Key, Fun} || {Key, _, Fun} <- Schema ]),
[{return, map}, {unique, true}]),
fun(Options) ->
merge(Defaults, Options, Name, Default)
end).
-spec merge([{atom(), term()}], #{atom() => term()}, atom(), T) -> tuple() | T.
merge(_, Options, _, Default) when Options == #{} ->
Default;
merge(Defaults, Options, Name, _) ->
list_to_tuple([Name|[maps:get(Key, Options, Val) || {Key, Val} <- Defaults]]).
list_to_tuple([Name | [ maps:get(Key, Options, Val) || {Key, Val} <- Defaults ]]).

View file

@ -43,24 +43,34 @@
-protocol({xep, 440, '0.4.0', '24.02', "complete", ""}).
-protocol({xep, 474, '0.4.0', '24.02', "complete", "0.4.0 since 25.03"}).
-export([start/0, stop/0, halt/0, start_app/1, start_app/2,
get_pid_file/0, check_apps/0, module_name/1, is_loaded/0]).
-export([start/0,
stop/0,
halt/0,
start_app/1, start_app/2,
get_pid_file/0,
check_apps/0,
module_name/1,
is_loaded/0]).
-include("logger.hrl").
start() ->
case application:ensure_all_started(ejabberd) of
{error, Err} -> error_logger:error_msg("Failed to start ejabberd application: ~p", [Err]);
Ok -> Ok
end.
stop() ->
application:stop(ejabberd).
halt() ->
ejabberd_logger:flush(),
erlang:halt(1, [{flush, true}]).
-spec get_pid_file() -> false | string().
get_pid_file() ->
case os:getenv("EJABBERD_PID_PATH") of
@ -72,27 +82,31 @@ get_pid_file() ->
Path
end.
start_app(App) ->
start_app(App, temporary).
start_app(App, Type) ->
StartFlag = not is_loaded(),
start_app(App, Type, StartFlag).
is_loaded() ->
Apps = application:which_applications(),
lists:keymember(ejabberd, 1, Apps).
start_app(App, Type, StartFlag) when is_atom(App) ->
start_app([App], Type, StartFlag);
start_app([App|Apps], Type, StartFlag) ->
case application:start(App,Type) of
start_app([App | Apps], Type, StartFlag) ->
case application:start(App, Type) of
ok ->
start_app(Apps, Type, StartFlag);
{error, {already_started, _}} ->
start_app(Apps, Type, StartFlag);
{error, {not_started, DepApp}} ->
case lists:member(DepApp, [App|Apps]) of
case lists:member(DepApp, [App | Apps]) of
true ->
Reason = io_lib:format(
"Failed to start Erlang application '~ts': "
@ -100,7 +114,7 @@ start_app([App|Apps], Type, StartFlag) ->
[App, DepApp]),
exit_or_halt(Reason, StartFlag);
false ->
start_app([DepApp,App|Apps], Type, StartFlag)
start_app([DepApp, App | Apps], Type, StartFlag)
end;
{error, Why} ->
Reason = io_lib:format(
@ -111,6 +125,7 @@ start_app([App|Apps], Type, StartFlag) ->
start_app([], _Type, _StartFlag) ->
ok.
check_app_modules(App, StartFlag) ->
case application:get_key(App, modules) of
{ok, Mods} ->
@ -127,38 +142,44 @@ check_app_modules(App, StartFlag) ->
_ ->
ok
end
end, Mods);
end,
Mods);
_ ->
%% No modules? This is strange
ok
end.
check_apps() ->
spawn(
fun() ->
Apps = [ejabberd |
[App || {App, _, _} <- application:which_applications(),
App /= ejabberd, App /= hex]],
Apps = [ejabberd | [ App || {App, _, _} <- application:which_applications(),
App /= ejabberd,
App /= hex ]],
?DEBUG("Checking consistency of applications: ~ts",
[misc:join_atoms(Apps, <<", ">>)]),
misc:peach(
fun(App) ->
check_app_modules(App, true)
end, Apps),
end,
Apps),
?DEBUG("All applications are intact", []),
lists:foreach(fun erlang:garbage_collect/1, processes())
end).
-spec exit_or_halt(iodata(), boolean()) -> no_return().
exit_or_halt(Reason, StartFlag) ->
?CRITICAL_MSG(Reason, []),
if StartFlag ->
if
StartFlag ->
%% Wait for the critical message is written in the console/log
halt();
true ->
erlang:error(application_start_failed)
end.
get_module_file(App, Mod) ->
BaseName = atom_to_list(Mod),
case code:lib_dir(App) of
@ -168,8 +189,9 @@ get_module_file(App, Mod) ->
filename:join([Dir, "ebin", BaseName ++ ".beam"])
end.
module_name([Dir, _, <<H,_/binary>> | _] = Mod) when H >= 65, H =< 90 ->
Module = str:join([elixir_name(M) || M<-tl(Mod)], <<>>),
module_name([Dir, _, <<H, _/binary>> | _] = Mod) when H >= 65, H =< 90 ->
Module = str:join([ elixir_name(M) || M <- tl(Mod) ], <<>>),
Prefix = case elixir_name(Dir) of
<<"Ejabberd">> -> <<"Elixir.Ejabberd.">>;
Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">>
@ -185,29 +207,33 @@ module_name([<<"auth">> | T] = Mod) ->
end;
module_name([<<"ejabberd">> | _] = Mod) ->
Module = str:join([erlang_name(M) || M<-Mod], $_),
Module = str:join([ erlang_name(M) || M <- Mod ], $_),
misc:binary_to_atom(Module);
module_name(Mod) when is_list(Mod) ->
Module = str:join([erlang_name(M) || M<-tl(Mod)], $_),
Module = str:join([ erlang_name(M) || M <- tl(Mod) ], $_),
misc:binary_to_atom(Module).
elixir_name(Atom) when is_atom(Atom) ->
elixir_name(misc:atom_to_binary(Atom));
elixir_name(<<H,T/binary>>) when H >= 65, H =< 90 ->
elixir_name(<<H, T/binary>>) when H >= 65, H =< 90 ->
<<H, T/binary>>;
elixir_name(<<H,T/binary>>) ->
<<(H-32), T/binary>>.
elixir_name(<<H, T/binary>>) ->
<<(H - 32), T/binary>>.
erlang_name(Atom) when is_atom(Atom) ->
misc:atom_to_binary(Atom);
erlang_name(Bin) when is_binary(Bin) ->
Bin.
format_error({Reason, File}) when is_list(Reason), is_list(File) ->
Reason ++ ": " ++ File;
format_error(Term) ->
io_lib:format("~p", [Term]).
hint() ->
"This usually means that ejabberd or Erlang "
"was compiled/installed incorrectly.".

View file

@ -48,8 +48,7 @@
-define(SERVER, ?MODULE).
-define(CACHE_TAB, access_permissions_cache).
-record(state,
{definitions = none :: none | [definition()]}).
-record(state, {definitions = none :: none | [definition()]}).
-type state() :: #state{}.
-type rule() :: {access, acl:access()} |
@ -59,14 +58,17 @@
-type from() :: atom().
-type permission() :: {binary(), {[from()], [who()], {what(), what()}}}.
-type definition() :: {binary(), {[from()], [who()], [atom()] | all}}.
-type caller_info() :: #{caller_module => module(),
-type caller_info() :: #{
caller_module => module(),
caller_host => global | binary(),
tag => binary() | none,
extra_permissions => [definition()],
atom() => term()}.
atom() => term()
}.
-export_type([permission/0]).
%%%===================================================================
%%% API
%%%===================================================================
@ -82,37 +84,47 @@ can_access(Cmd, CallerInfo) ->
case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of
true ->
?DEBUG("Command '~p' execution allowed by rule "
"'~ts'~n (CallerInfo=~p)", [Cmd, Name, CallerInfo]),
"'~ts'~n (CallerInfo=~p)",
[Cmd, Name, CallerInfo]),
allow;
_ ->
none
end;
(_, Val) ->
Val
end, none, Defs),
end,
none,
Defs),
case Res of
allow -> allow;
_ ->
?DEBUG("Command '~p' execution denied~n "
"(CallerInfo=~p)", [Cmd, CallerInfo]),
"(CallerInfo=~p)",
[Cmd, CallerInfo]),
deny
end.
-spec invalidate() -> ok.
invalidate() ->
gen_server:cast(?MODULE, invalidate),
ets_cache:delete(?CACHE_TAB, definitions).
-spec show_current_definitions() -> [definition()].
show_current_definitions() ->
ets_cache:lookup(?CACHE_TAB, definitions,
ets_cache:lookup(?CACHE_TAB,
definitions,
fun() ->
{cache, gen_server:call(?MODULE, show_current_definitions)}
end).
start_link() ->
ets_cache:new(?CACHE_TAB, [{max_size, 2}]),
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -122,8 +134,10 @@ init([]) ->
ets_cache:new(access_permissions),
{ok, #state{}}.
-spec handle_call(show_current_definitions | term(),
term(), state()) -> {reply, term(), state()}.
term(),
state()) -> {reply, term(), state()}.
handle_call(show_current_definitions, _From, State) ->
{State2, Defs} = get_definitions(State),
{reply, Defs, State2};
@ -131,6 +145,7 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_cast(invalidate | term(), state()) -> {noreply, state()}.
handle_cast(invalidate, State) ->
{noreply, State#state{definitions = none}};
@ -138,16 +153,20 @@ handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, invalidate, 90).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -161,7 +180,8 @@ get_definitions(#state{definitions = none} = State) ->
fun({Name, {From, Who, {Add, Del}}}) ->
Cmds = filter_commands_with_permissions(AllCommands, Add, Del),
{Name, {From, Who, Cmds}}
end, ApiPerms),
end,
ApiPerms),
NDefs = case lists:keyfind(<<"console commands">>, 1, NDefs0) of
false ->
[{<<"console commands">>,
@ -173,8 +193,13 @@ get_definitions(#state{definitions = none} = State) ->
end,
{State#state{definitions = NDefs}, NDefs}.
-spec matches_definition(definition(), atom(), module(),
atom(), global | binary(), caller_info()) -> boolean().
-spec matches_definition(definition(),
atom(),
module(),
atom(),
global | binary(),
caller_info()) -> boolean().
matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInfo) ->
case What == all orelse lists:member(Cmd, What) of
true ->
@ -200,13 +225,15 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInf
acl:match_acl(Host, Acl, CallerInfo);
({acl, Acl}) ->
acl:match_acl(Host, Acl, CallerInfo)
end, List);
end,
List);
_ ->
false
end;
(_) ->
false
end, Who);
end,
Who);
_ ->
false
end;
@ -214,6 +241,7 @@ matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInf
false
end.
-spec filter_commands_with_permissions([#ejabberd_commands{}], what(), what()) -> [atom()].
filter_commands_with_permissions(AllCommands, Add, Del) ->
CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []),
@ -221,7 +249,9 @@ filter_commands_with_permissions(AllCommands, Add, Del) ->
lists:map(fun(#ejabberd_commands{name = N}) -> N end,
CommandsAdd -- CommandsDel).
-spec filter_commands_with_patterns([#ejabberd_commands{}], what(),
-spec filter_commands_with_patterns([#ejabberd_commands{}],
what(),
[#ejabberd_commands{}]) -> [#ejabberd_commands{}].
filter_commands_with_patterns([], _Patterns, Acc) ->
Acc;
@ -233,6 +263,7 @@ filter_commands_with_patterns([C | CRest], Patterns, Acc) ->
filter_commands_with_patterns(CRest, Patterns, Acc)
end.
-spec command_matches_patterns(#ejabberd_commands{}, what()) -> boolean().
command_matches_patterns(_, all) ->
true;
@ -252,6 +283,7 @@ command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) ->
command_matches_patterns(C, [_ | Tail]) ->
command_matches_patterns(C, Tail).
%%%===================================================================
%%% Validators
%%%===================================================================
@ -272,7 +304,9 @@ parse_what(Defs) ->
Value ->
{case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del}
end
end, {[], []}, Defs),
end,
{[], []},
Defs),
case {A, D} of
{[], _} ->
{none, all};
@ -282,6 +316,7 @@ parse_what(Defs) ->
V
end.
-spec parse_single_what(binary()) -> atom() | {neg, atom()} | {tag, atom()} | {error, string()}.
parse_single_what(<<"*">>) ->
all;
@ -316,13 +351,15 @@ parse_single_what(B) ->
_ -> binary_to_atom(B, latin1)
end.
validator(Map, Opts) ->
econf:and_then(
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) -> {(econf:atom())(K), V};
(A) -> {acl, (econf:atom())(A)}
end, lists:flatten(L));
end,
lists:flatten(L));
(A) ->
[{acl, (econf:atom())(A)}]
end,
@ -332,25 +369,29 @@ validator(Map, Opts) ->
lists:flatmap(
fun({Type, Rs}) when is_list(Rs) ->
case maps:is_key(Type, acl:validators()) of
true -> [{acl, {Type, R}} || R <- Rs];
true -> [ {acl, {Type, R}} || R <- Rs ];
false -> [{Type, Rs}]
end;
(Other) ->
[Other]
end, Rules)
end,
Rules)
end)).
validator(from) ->
fun(L) when is_list(L) ->
lists:map(
fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)};
(A) -> (econf:enum([ejabberd_ctl,
(A) ->
(econf:enum([ejabberd_ctl,
ejabberd_web_admin,
ejabberd_xmlrpc,
mod_adhoc_api,
mod_cron,
mod_http_api]))(A)
end, lists:flatten(L));
end,
lists:flatten(L));
(A) ->
[(econf:enum([ejabberd_ctl,
ejabberd_web_admin,
@ -367,23 +408,28 @@ validator(who) ->
validator(#{access => econf:acl(), oauth => validator(oauth)}, []);
validator(oauth) ->
econf:and_then(
validator(#{access => econf:acl(),
validator(#{
access => econf:acl(),
scope => econf:non_empty(
econf:list_or_single(econf:binary()))},
econf:list_or_single(econf:binary()))
},
[{required, [scope]}]),
fun(Os) ->
{[Scopes], Rest} = proplists:split(Os, [scope]),
{lists:flatten([S || {_, S} <- Scopes]), Rest}
{lists:flatten([ S || {_, S} <- Scopes ]), Rest}
end).
validator() ->
econf:map(
econf:binary(),
econf:and_then(
econf:options(
#{from => validator(from),
#{
from => validator(from),
what => validator(what),
who => validator(who)}),
who => validator(who)
}),
fun(Os) ->
{proplists:get_value(from, Os, []),
proplists:get_value(who, Os, none),

View file

@ -27,11 +27,17 @@
%% Hooks
-export([ejabberd_started/0, register_certfiles/0, cert_expired/2]).
%% ejabberd commands
-export([get_commands_spec/0, request_certificate/1,
revoke_certificate/1, list_certificates/0]).
-export([get_commands_spec/0,
request_certificate/1,
revoke_certificate/1,
list_certificates/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% WebAdmin
-export([webadmin_menu_node/3, webadmin_page_node/3]).
@ -39,6 +45,7 @@
-include("ejabberd_commands.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-include_lib("public_key/include/public_key.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("xmpp/include/xmpp.hrl").
@ -52,41 +59,50 @@
-type cert() :: #'OTPCertificate'{}.
-type cert_type() :: ec | rsa.
-type io_error() :: file:posix().
-type issue_result() :: ok | p1_acme:issue_return() |
-type issue_result() :: ok |
p1_acme:issue_return() |
{error, {file, io_error()} |
{idna_failed, binary()}}.
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec register_certfiles() -> ok.
register_certfiles() ->
lists:foreach(fun ejabberd_pkix:add_certfile/1,
list_certfiles()).
-spec process([binary()], _) -> {integer(), [{binary(), binary()}], binary()}.
process([Token], _) ->
?DEBUG("Received ACME challenge request for token: ~ts", [Token]),
try ets:lookup_element(acme_challenge, Token, 2) of
Key -> {200, [{<<"Content-Type">>,
Key ->
{200,
[{<<"Content-Type">>,
<<"application/octet-stream">>}],
Key}
catch _:_ ->
catch
_:_ ->
{404, [], <<>>}
end;
process(_, _) ->
{404, [], <<>>}.
-spec cert_expired(_, pkix:cert_info()) -> ok | stop.
cert_expired(_, #{domains := Domains, files := Files}) ->
CertFiles = list_certfiles(),
case lists:any(
fun({File, _}) ->
lists:member(File, CertFiles)
end, Files) of
end,
Files) of
true ->
gen_server:cast(?MODULE, {request, Domains}),
stop;
@ -94,13 +110,16 @@ cert_expired(_, #{domains := Domains, files := Files}) ->
ok
end.
-spec ejabberd_started() -> ok.
ejabberd_started() ->
gen_server:cast(?MODULE, ejabberd_started).
default_directory_url() ->
<<"https://acme-v02.api.letsencrypt.org/directory">>.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -119,7 +138,8 @@ init([]) ->
register_certfiles(),
{ok, #state{}}.
handle_call({request, [_|_] = Domains}, _From, State) ->
handle_call({request, [_ | _] = Domains}, _From, State) ->
?INFO_MSG("Requesting new certificate for ~ts from ~ts",
[misc:format_hosts_list(Domains), directory_url()]),
{Ret, State1} = issue_request(State, Domains),
@ -132,6 +152,7 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(ejabberd_started, State) ->
case request_on_start() of
{true, Domains} ->
@ -142,7 +163,7 @@ handle_cast(ejabberd_started, State) ->
false ->
{noreply, State}
end;
handle_cast({request, [_|_] = Domains}, State) ->
handle_cast({request, [_ | _] = Domains}, State) ->
?INFO_MSG("Requesting renewal of certificate for ~ts from ~ts",
[misc:format_hosts_list(Domains), directory_url()]),
{_, State1} = issue_request(State, Domains),
@ -151,10 +172,12 @@ handle_cast(Request, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Request]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 60),
ejabberd_hooks:delete(config_reloaded, ?MODULE, register_certfiles, 40),
@ -164,9 +187,11 @@ terminate(_Reason, _State) ->
ejabberd_hooks:delete(webadmin_page_node, ?MODULE, webadmin_page_node, 110),
ejabberd_commands:unregister_commands(get_commands_spec()).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -182,7 +207,9 @@ register_challenge(Auth, Ref) ->
lists:map(
fun(#{token := Token, key := Key}) ->
{Token, Key, Ref}
end, Auth)).
end,
Auth)).
-spec unregister_challenge(reference()) -> non_neg_integer().
unregister_challenge(Ref) ->
@ -195,10 +222,11 @@ unregister_challenge(Ref) ->
Ref1 == Ref
end)).
%%%===================================================================
%%% Issuance
%%%===================================================================
-spec issue_request(state(), [binary(),...]) -> {issue_result(), state()}.
-spec issue_request(state(), [binary(), ...]) -> {issue_result(), state()}.
issue_request(State, Domains) ->
case check_idna(Domains) of
{ok, AsciiDomains} ->
@ -222,18 +250,28 @@ issue_request(State, Domains) ->
{Err, State}
end.
-spec issue_request(state(), binary(), [binary(),...], [string(), ...], priv_key(),
cert_type(), [binary()]) -> {issue_result(), state()}.
-spec issue_request(state(),
binary(),
[binary(), ...],
[string(), ...],
priv_key(),
cert_type(),
[binary()]) -> {issue_result(), state()}.
issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact) ->
Ref = make_ref(),
ChallengeFun = fun(Auth) -> register_challenge(Auth, Ref) end,
Ret = case p1_acme:issue(DirURL, AsciiDomains, AccKey,
Ret = case p1_acme:issue(DirURL,
AsciiDomains,
AccKey,
[{cert_type, CertType},
{contact, Contact},
{debug_fun, debug_fun()},
{challenge_fun, ChallengeFun}]) of
{ok, #{cert_key := CertKey,
cert_chain := Certs}} ->
{ok, #{
cert_key := CertKey,
cert_chain := Certs
}} ->
case store_cert(CertKey, Certs, CertType, Domains) of
{ok, Path} ->
ejabberd_pkix:add_certfile(Path),
@ -257,15 +295,19 @@ issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact) -
unregister_challenge(Ref),
Ret.
%%%===================================================================
%%% Revocation
%%%===================================================================
revoke_request(State, Cert, Key, Path) ->
case p1_acme:revoke(directory_url(), Cert, Key,
case p1_acme:revoke(directory_url(),
Cert,
Key,
[{debug_fun, debug_fun()}]) of
ok ->
?INFO_MSG("Certificate from file ~ts has been "
"revoked successfully", [Path]),
"revoked successfully",
[Path]),
case delete_file(Path) of
ok ->
ejabberd_pkix:del_certfile(Path),
@ -280,6 +322,7 @@ revoke_request(State, Cert, Key, Path) ->
{Err, State}
end.
%%%===================================================================
%%% File management
%%%===================================================================
@ -288,29 +331,38 @@ acme_dir() ->
MnesiaDir = mnesia:system_info(directory),
filename:join(MnesiaDir, "acme").
-spec acme_certs_dir(atom()) -> file:filename_all().
acme_certs_dir(Tag) ->
filename:join(acme_dir(), Tag).
-spec account_file() -> file:filename_all().
account_file() ->
filename:join(acme_dir(), "account.key").
-spec cert_file(cert_type(), [binary()]) -> file:filename_all().
cert_file(CertType, Domains) ->
L = [erlang:atom_to_binary(CertType, latin1)|Domains],
L = [erlang:atom_to_binary(CertType, latin1) | Domains],
Hash = str:sha(str:join(L, <<0>>)),
filename:join(acme_certs_dir(live), Hash).
-spec prep_path(file:filename_all()) -> binary().
prep_path(Path) ->
unicode:characters_to_binary(Path).
-spec list_certfiles() -> [binary()].
list_certfiles() ->
filelib:fold_files(
acme_certs_dir(live), "^[0-9a-f]{40}$", false,
fun(F, Fs) -> [prep_path(F)|Fs] end, []).
acme_certs_dir(live),
"^[0-9a-f]{40}$",
false,
fun(F, Fs) -> [prep_path(F) | Fs] end,
[]).
-spec read_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}.
read_account_key() ->
@ -318,7 +370,7 @@ read_account_key() ->
case pkix:read_file(Path) of
{ok, _, KeyMap} ->
case maps:keys(KeyMap) of
[#'ECPrivateKey'{} = Key|_] -> {ok, Key};
[#'ECPrivateKey'{} = Key | _] -> {ok, Key};
_ ->
?WARNING_MSG("File ~ts doesn't contain ACME account key. "
"Trying to create a new one...",
@ -339,6 +391,7 @@ read_account_key() ->
{error, {file, Reason}}
end.
-spec create_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}.
create_account_key() ->
Path = account_file(),
@ -355,6 +408,7 @@ create_account_key() ->
{error, {file, Reason}}
end.
-spec store_cert(priv_key(), [cert()], cert_type(), [binary()]) -> {ok, file:filename_all()} |
{error, {file, io_error()}}.
store_cert(Key, Chain, CertType, Domains) ->
@ -365,7 +419,8 @@ store_cert(Key, Chain, CertType, Domains) ->
DerCert = public_key:pkix_encode(
element(1, Cert), Cert, otp),
{'Certificate', DerCert, not_encrypted}
end, Chain),
end,
Chain),
PEM = public_key:pem_encode(PemChain ++ PemKey),
Path = cert_file(CertType, Domains),
?DEBUG("Storing certificate for ~ts in ~ts",
@ -377,6 +432,7 @@ store_cert(Key, Chain, CertType, Domains) ->
{error, {file, Reason}}
end.
-spec read_cert(file:filename_all()) -> {ok, [cert()], priv_key()} |
{error, {file, io_error()} |
{bad_cert, _, _} |
@ -386,8 +442,8 @@ read_cert(Path) ->
case pkix:read_file(Path) of
{ok, CertsMap, KeysMap} ->
case {maps:to_list(CertsMap), maps:keys(KeysMap)} of
{[_|_] = Certs, [CertKey]} ->
{ok, [Cert || {Cert, _} <- lists:keysort(2, Certs)], CertKey};
{[_ | _] = Certs, [CertKey]} ->
{ok, [ Cert || {Cert, _} <- lists:keysort(2, Certs) ], CertKey};
_ ->
{error, unexpected_certfile}
end;
@ -397,6 +453,7 @@ read_cert(Path) ->
Err
end.
-spec write_file(file:filename_all(), iodata()) -> ok | {error, io_error()}.
write_file(Path, Data) ->
case ensure_dir(Path) of
@ -418,6 +475,7 @@ write_file(Path, Data) ->
Err
end.
-spec delete_file(file:filename_all()) -> ok | {error, io_error()}.
delete_file(Path) ->
case file:delete(Path) of
@ -428,6 +486,7 @@ delete_file(Path) ->
Err
end.
-spec ensure_dir(file:filename_all()) -> ok | {error, io_error()}.
ensure_dir(Path) ->
case filelib:ensure_dir(Path) of
@ -439,6 +498,7 @@ ensure_dir(Path) ->
Err
end.
-spec delete_obsolete_data() -> ok.
delete_obsolete_data() ->
Path = filename:join(ejabberd_pkix:certs_dir(), "acme"),
@ -451,33 +511,47 @@ delete_obsolete_data() ->
ok
end.
%%%===================================================================
%%% ejabberd commands
%%%===================================================================
get_commands_spec() ->
[#ejabberd_commands{name = request_certificate, tags = [acme],
[#ejabberd_commands{
name = request_certificate,
tags = [acme],
desc = "Requests certificates for all or some domains",
longdesc = "Domains can be `all`, or a list of domains separared with comma characters",
module = ?MODULE, function = request_certificate,
module = ?MODULE,
function = request_certificate,
args_desc = ["Domains for which to acquire a certificate"],
args_example = ["example.com,domain.tld,conference.domain.tld"],
args = [{domains, string}],
result = {res, restuple}},
#ejabberd_commands{name = list_certificates, tags = [acme],
result = {res, restuple}
},
#ejabberd_commands{
name = list_certificates,
tags = [acme],
desc = "Lists all ACME certificates",
module = ?MODULE, function = list_certificates,
module = ?MODULE,
function = list_certificates,
args = [],
result = {certificates,
{list, {certificate,
{tuple, [{domain, string},
{file, string},
{used, string}]}}}}},
#ejabberd_commands{name = revoke_certificate, tags = [acme],
{used, string}]}}}}
},
#ejabberd_commands{
name = revoke_certificate,
tags = [acme],
desc = "Revokes the selected ACME certificate",
module = ?MODULE, function = revoke_certificate,
module = ?MODULE,
function = revoke_certificate,
args_desc = ["Filename of the certificate"],
args = [{file, string}],
result = {res, restuple}}].
result = {res, restuple}
}].
-spec request_certificate(iodata()) -> {ok | error, string()}.
request_certificate(Arg) ->
@ -490,16 +564,18 @@ request_certificate(Arg) ->
Domains ->
gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT)
end;
[_|_] = Domains ->
[_ | _] = Domains ->
case lists:dropwhile(
fun(D) ->
try ejabberd_router:is_my_route(D) of
true -> not is_ip_or_localhost(D);
false -> false
catch _:{invalid_domain, _} -> false
catch
_:{invalid_domain, _} -> false
end
end, Domains) of
[Bad|_] ->
end,
Domains) of
[Bad | _] ->
{error, {invalid_host, Bad}};
[] ->
gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT)
@ -512,11 +588,12 @@ request_certificate(Arg) ->
{error, Why} -> {error, format_error(Why)}
end.
-spec revoke_certificate(iodata()) -> {ok | error, string()}.
revoke_certificate(Path0) ->
Path = prep_path(Path0),
Ret = case read_cert(Path) of
{ok, [Cert|_], Key} ->
{ok, [Cert | _], Key} ->
gen_server:call(?MODULE, {revoke, Cert, Key, Path}, ?CALL_TIMEOUT);
{error, _} = Err ->
Err
@ -526,43 +603,54 @@ revoke_certificate(Path0) ->
{error, Reason} -> {error, format_error(Reason)}
end.
-spec list_certificates() -> [{binary(), binary(), boolean()}].
list_certificates() ->
Known = lists:flatmap(
fun(Path) ->
try
{ok, [Cert|_], _} = read_cert(Path),
{ok, [Cert | _], _} = read_cert(Path),
Domains = pkix:extract_domains(Cert),
[{Domain, Path} || Domain <- Domains]
catch _:{badmatch, _} ->
[ {Domain, Path} || Domain <- Domains ]
catch
_:{badmatch, _} ->
[]
end
end, list_certfiles()),
end,
list_certfiles()),
Used = lists:foldl(
fun(Domain, S) ->
try
{ok, Path} = ejabberd_pkix:get_certfile_no_default(Domain),
{ok, [Cert|_], _} = read_cert(Path),
{ok, [Cert | _], _} = read_cert(Path),
{ok, #{files := Files}} = pkix:get_cert_info(Cert),
lists:foldl(fun sets:add_element/2,
S, [{Domain, File} || {File, _} <- Files])
catch _:{badmatch, _} ->
S,
[ {Domain, File} || {File, _} <- Files ])
catch
_:{badmatch, _} ->
S
end
end, sets:new(), all_domains()),
end,
sets:new(),
all_domains()),
lists:sort(
lists:map(
fun({Domain, Path} = E) ->
{Domain, Path, sets:is_element(E, Used)}
end, Known)).
end,
Known)).
%%%===================================================================
%%% WebAdmin
%%%===================================================================
webadmin_menu_node(Acc, _Node, _Lang) ->
Acc ++ [{<<"acme">>, <<"ACME">>}].
webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) ->
Head = ?H1GLraw(<<"ACME Certificates">>, <<"admin/configuration/basic/#acme">>, <<"ACME">>),
Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [request_certificate, R]),
@ -571,28 +659,34 @@ webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) ->
{stop, Head ++ Get ++ Set};
webadmin_page_node(Acc, _, _) -> Acc.
%%%===================================================================
%%% Other stuff
%%%===================================================================
-spec all_domains() -> [binary(),...].
-spec all_domains() -> [binary(), ...].
all_domains() ->
ejabberd_option:hosts() ++ ejabberd_router:get_all_routes().
-spec auto_domains() -> [binary()].
auto_domains() ->
lists:filter(
fun(Host) ->
not is_ip_or_localhost(Host)
end, all_domains()).
end,
all_domains()).
-spec directory_url() -> binary().
directory_url() ->
maps:get(ca_url, ejabberd_option:acme(), default_directory_url()).
-spec debug_fun() -> fun((string(), list()) -> ok).
debug_fun() ->
fun(Fmt, Args) -> ?DEBUG(Fmt, Args) end.
-spec request_on_start() -> false | {true, [binary()]}.
request_on_start() ->
Config = ejabberd_option:acme(),
@ -604,9 +698,10 @@ request_on_start() ->
_ ->
case lists:filter(
fun(Host) ->
not (have_cert_for_domain(Host)
orelse is_ip_or_localhost(Host))
end, auto_domains()) of
not (have_cert_for_domain(Host) orelse
is_ip_or_localhost(Host))
end,
auto_domains()) of
[] -> false;
Hosts ->
case have_acme_listener() of
@ -626,13 +721,16 @@ request_on_start() ->
end
end.
well_known() ->
[<<".well-known">>, <<"acme-challenge">>].
-spec have_cert_for_domain(binary()) -> boolean().
have_cert_for_domain(Host) ->
ejabberd_pkix:get_certfile_no_default(Host) /= error.
-spec is_ip_or_localhost(binary()) -> boolean().
is_ip_or_localhost(Host) ->
Parts = binary:split(Host, <<".">>),
@ -642,26 +740,38 @@ is_ip_or_localhost(Host) ->
_ -> TLD == "localhost"
end.
-spec have_acme_listener() -> boolean().
have_acme_listener() ->
lists:any(
fun({_, ejabberd_http, #{tls := false,
request_handlers := Handlers}}) ->
fun({_,
ejabberd_http,
#{
tls := false,
request_handlers := Handlers
}}) ->
lists:keymember(well_known(), 1, Handlers);
(_) ->
false
end, ejabberd_option:listen()).
end,
ejabberd_option:listen()).
-spec check_idna([binary()]) -> {ok, [string()]} | {error, {idna_failed, binary()}}.
check_idna(Domains) ->
lists:foldl(
fun(D, {ok, Ds}) ->
try {ok, [idna:utf8_to_ascii(D)|Ds]}
catch _:_ -> {error, {idna_failed, D}}
try
{ok, [idna:utf8_to_ascii(D) | Ds]}
catch
_:_ -> {error, {idna_failed, D}}
end;
(_, Err) ->
Err
end, {ok, []}, Domains).
end,
{ok, []},
Domains).
-spec format_error(term()) -> string().
format_error({file, Reason}) ->

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@
%%% Application API
%%%
start(normal, _Args) ->
try
{T1, _} = statistics(wall_clock),
@ -64,7 +65,8 @@ start(normal, _Args) ->
{T2, _} = statistics(wall_clock),
?INFO_MSG("ejabberd ~ts is started in the node ~p in ~.2fs",
[ejabberd_option:version(),
node(), (T2-T1)/1000]),
node(),
(T2 - T1) / 1000]),
maybe_print_elixir_version(),
?INFO_MSG("~ts",
[erlang:system_info(system_version)]),
@ -78,13 +80,15 @@ start(normal, _Args) ->
[ejabberd_config:format_error(Err)]),
ejabberd:halt()
end
catch throw:{?MODULE, Error} ->
catch
throw:{?MODULE, Error} ->
?DEBUG("Failed to start ejabberd application: ~p", [Error]),
ejabberd:halt()
end;
start(_, _) ->
{error, badarg}.
start_included_apps() ->
{ok, Apps} = application:get_key(ejabberd, included_applications),
lists:foreach(
@ -92,11 +96,13 @@ start_included_apps() ->
ok;
(lager) ->
ok;
(os_mon)->
(os_mon) ->
ok;
(App) ->
application:ensure_all_started(App)
end, Apps).
end,
Apps).
%% Prepare the application for termination.
%% This function is called when an application is about to be stopped,
@ -113,16 +119,19 @@ prep_stop(State) ->
gen_mod:stop(),
State.
%% All the processes were killed when this function is called
stop(_State) ->
?INFO_MSG("ejabberd ~ts is stopped in the node ~p",
[ejabberd_option:version(), node()]),
delete_pid_file().
%%%
%%% Internal functions
%%%
%% If ejabberd is running on some Windows machine, get nameservers and add to Erlang
maybe_add_nameservers() ->
case os:type() of
@ -130,15 +139,18 @@ maybe_add_nameservers() ->
_ -> ok
end.
add_windows_nameservers() ->
IPTs = win32_dns:get_nameservers(),
?INFO_MSG("Adding machine's DNS IPs to Erlang system:~n~p", [IPTs]),
lists:foreach(fun(IPT) -> inet_db:add_ns(IPT) end, IPTs).
%%%
%%% PID file
%%%
write_pid_file() ->
case ejabberd:get_pid_file() of
false ->
@ -147,6 +159,7 @@ write_pid_file() ->
write_pid_file(os:getpid(), PidFilename)
end.
write_pid_file(Pid, PidFilename) ->
case file:write_file(PidFilename, io_lib:format("~ts~n", [Pid])) of
ok ->
@ -157,6 +170,7 @@ write_pid_file(Pid, PidFilename) ->
throw({?MODULE, Err})
end.
delete_pid_file() ->
case ejabberd:get_pid_file() of
false ->
@ -165,6 +179,7 @@ delete_pid_file() ->
file:delete(PidFilename)
end.
file_queue_init() ->
QueueDir = case ejabberd_option:queue_dir() of
undefined ->
@ -178,11 +193,14 @@ file_queue_init() ->
Err -> throw({?MODULE, Err})
end.
%%%
%%% Elixir
%%%
-ifdef(ELIXIR_ENABLED).
is_using_elixir_config() ->
Config = ejabberd_config:path(),
try 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config) of
@ -191,36 +209,55 @@ is_using_elixir_config() ->
_:_ -> false
end.
setup_if_elixir_conf_used() ->
case is_using_elixir_config() of
true -> 'Elixir.Ejabberd.Config.Store':start_link();
false -> ok
end.
register_elixir_config_hooks() ->
case is_using_elixir_config() of
true -> 'Elixir.Ejabberd.Config':start_hooks();
false -> ok
end.
start_elixir_application() ->
case application:ensure_started(elixir) of
ok -> ok;
{error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
end.
maybe_start_exsync() ->
case os:getenv("RELIVE") of
"true" -> rpc:call(node(), 'Elixir.ExSync.Application', start, []);
_ -> ok
end.
maybe_print_elixir_version() ->
?INFO_MSG("Elixir ~ts", [maps:get(build, 'Elixir.System':build_info())]).
-else.
setup_if_elixir_conf_used() -> ok.
register_elixir_config_hooks() -> ok.
start_elixir_application() -> ok.
maybe_start_exsync() -> ok.
maybe_print_elixir_version() -> ok.
-endif.

File diff suppressed because it is too large Load diff

View file

@ -39,36 +39,57 @@
anonymous_user_exist/2,
allow_multiple_connections/1,
register_connection/3,
unregister_connection/3
]).
unregister_connection/3]).
-export([login/2, check_password/4, user_exists/2,
get_users/2, count_users/2, store_type/1,
-export([login/2,
check_password/4,
user_exists/2,
get_users/2,
count_users/2,
store_type/1,
plain_password_required/1]).
-include("logger.hrl").
-include_lib("xmpp/include/jid.hrl").
start(Host) ->
ejabberd_hooks:add(sm_register_connection_hook, Host,
?MODULE, register_connection, 100),
ejabberd_hooks:add(sm_remove_connection_hook, Host,
?MODULE, unregister_connection, 100),
ejabberd_hooks:add(sm_register_connection_hook,
Host,
?MODULE,
register_connection,
100),
ejabberd_hooks:add(sm_remove_connection_hook,
Host,
?MODULE,
unregister_connection,
100),
ok.
stop(Host) ->
ejabberd_hooks:delete(sm_register_connection_hook, Host,
?MODULE, register_connection, 100),
ejabberd_hooks:delete(sm_remove_connection_hook, Host,
?MODULE, unregister_connection, 100).
ejabberd_hooks:delete(sm_register_connection_hook,
Host,
?MODULE,
register_connection,
100),
ejabberd_hooks:delete(sm_remove_connection_hook,
Host,
?MODULE,
unregister_connection,
100).
use_cache(_) ->
false.
%% Return true if anonymous is allowed for host or false otherwise
allow_anonymous(Host) ->
lists:member(?MODULE, ejabberd_auth:auth_modules(Host)).
%% Return true if anonymous mode is enabled and if anonymous protocol is SASL
%% anonymous protocol can be: sasl_anon|login_anon|both
is_sasl_anonymous_enabled(Host) ->
@ -82,6 +103,7 @@ is_sasl_anonymous_enabled(Host) ->
end
end.
%% Return true if anonymous login is enabled on the server
%% anonymous login can be use using standard authentication method (i.e. with
%% clients that do not support anonymous login)
@ -96,26 +118,32 @@ is_login_anonymous_enabled(Host) ->
end
end.
%% Return the anonymous protocol to use: sasl_anon|login_anon|both
%% defaults to login_anon
anonymous_protocol(Host) ->
ejabberd_option:anonymous_protocol(Host).
%% Return true if multiple connections have been allowed in the config file
%% defaults to false
allow_multiple_connections(Host) ->
ejabberd_option:allow_multiple_connections(Host).
anonymous_user_exist(User, Server) ->
lists:any(
fun({_LResource, Info}) ->
proplists:get_value(auth_module, Info) == ?MODULE
end, ejabberd_sm:get_user_info(User, Server)).
end,
ejabberd_sm:get_user_info(User, Server)).
%% Register connection
-spec register_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok.
register_connection(_SID,
#jid{luser = LUser, lserver = LServer, lresource = LResource}, Info) ->
#jid{luser = LUser, lserver = LServer, lresource = LResource},
Info) ->
case proplists:get_value(auth_module, Info) of
?MODULE ->
% Register user only if we are first resource
@ -129,10 +157,12 @@ register_connection(_SID,
ok
end.
%% Remove an anonymous user from the anonymous users table
-spec unregister_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any().
unregister_connection(_SID,
#jid{luser = LUser, lserver = LServer}, Info) ->
#jid{luser = LUser, lserver = LServer},
Info) ->
case proplists:get_value(auth_module, Info) of
?MODULE ->
% Remove user data only if there is no more resources around
@ -146,6 +176,7 @@ unregister_connection(_SID,
ok
end.
%% ---------------------------------
%% Specific anonymous auth functions
%% ---------------------------------
@ -159,6 +190,7 @@ check_password(User, _AuthzId, Server, _Password) ->
false -> login(User, Server)
end}.
login(User, Server) ->
case is_login_anonymous_enabled(Server) of
false -> false;
@ -173,17 +205,22 @@ login(User, Server) ->
end
end.
get_users(Server, _) ->
[{U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server)].
[ {U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server) ].
count_users(Server, Opts) ->
length(get_users(Server, Opts)).
user_exists(User, Server) ->
{nocache, anonymous_user_exist(User, Server)}.
plain_password_required(_) ->
false.
store_type(_) ->
external.

View file

@ -29,41 +29,57 @@
-behaviour(ejabberd_auth).
-export([start/1, stop/1, reload/1, set_password/3, check_password/4,
try_register/3, user_exists/2, remove_user/2,
store_type/1, plain_password_required/1]).
-export([start/1,
stop/1,
reload/1,
set_password/3,
check_password/4,
try_register/3,
user_exists/2,
remove_user/2,
store_type/1,
plain_password_required/1]).
-include("logger.hrl").
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start(Host) ->
extauth:start(Host).
stop(Host) ->
extauth:stop(Host).
reload(Host) ->
extauth:reload(Host).
plain_password_required(_) -> true.
store_type(_) -> external.
check_password(User, AuthzId, Server, Password) ->
if AuthzId /= <<>> andalso AuthzId /= User ->
if
AuthzId /= <<>> andalso AuthzId /= User ->
{nocache, false};
true ->
check_password_extauth(User, AuthzId, Server, Password)
end.
set_password(User, Server, Password) ->
case extauth:set_password(User, Server, Password) of
Res when is_boolean(Res) -> {cache, {ok, Password}};
{error, Reason} -> failure(User, Server, set_password, Reason)
end.
try_register(User, Server, Password) ->
case extauth:try_register(User, Server, Password) of
true -> {cache, {ok, Password}};
@ -71,12 +87,14 @@ try_register(User, Server, Password) ->
{error, Reason} -> failure(User, Server, try_register, Reason)
end.
user_exists(User, Server) ->
case extauth:user_exists(User, Server) of
Res when is_boolean(Res) -> {cache, Res};
{error, Reason} -> failure(User, Server, user_exists, Reason)
end.
remove_user(User, Server) ->
case extauth:remove_user(User, Server) of
false -> {error, not_allowed};
@ -86,8 +104,10 @@ remove_user(User, Server) ->
Err
end.
check_password_extauth(User, _AuthzId, Server, Password) ->
if Password /= <<"">> ->
if
Password /= <<"">> ->
case extauth:check_password(User, Server, Password) of
Res when is_boolean(Res) -> {cache, Res};
{error, Reason} ->
@ -98,8 +118,10 @@ check_password_extauth(User, _AuthzId, Server, Password) ->
{nocache, false}
end.
-spec failure(binary(), binary(), atom(), any()) -> {nocache, {error, db_failure}}.
failure(User, Server, Fun, Reason) ->
?ERROR_MSG("External authentication program failed when calling "
"'~ts' for ~ts@~ts: ~p", [Fun, User, Server, Reason]),
"'~ts' for ~ts@~ts: ~p",
[Fun, User, Server, Reason]),
{nocache, {error, db_failure}}.

View file

@ -29,16 +29,21 @@
-behaviour(ejabberd_auth).
-export([start/1, stop/1, check_password/4,
store_type/1, plain_password_required/1,
user_exists/2, use_cache/1
]).
-export([start/1,
stop/1,
check_password/4,
store_type/1,
plain_password_required/1,
user_exists/2,
use_cache/1]).
%% 'ejabberd_hooks' callback:
-export([check_decoded_jwt/5]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
@ -52,30 +57,38 @@ start(Host) ->
case ejabberd_option:jwt_key(Host) of
undefined ->
?ERROR_MSG("Option jwt_key is not configured for ~ts: "
"JWT authentication won't work", [Host]);
"JWT authentication won't work",
[Host]);
_ ->
ok
end.
stop(Host) ->
ejabberd_hooks:delete(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100).
plain_password_required(_Host) -> true.
store_type(_Host) -> external.
-spec check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}.
check_password(User, AuthzId, Server, Token) ->
%% MREMOND: Should we move the AuthzId check at a higher level in
%% the call stack?
if AuthzId /= <<>> andalso AuthzId /= User ->
if
AuthzId /= <<>> andalso AuthzId /= User ->
{nocache, false};
true ->
if Token == <<"">> -> {nocache, false};
if
Token == <<"">> -> {nocache, false};
true ->
Res = check_jwt_token(User, Server, Token),
Rule = ejabberd_option:jwt_auth_only_rule(Server),
case acl:match_rule(Server, Rule,
case acl:match_rule(Server,
Rule,
jid:make(User, Server, <<"">>)) of
deny ->
{nocache, Res};
@ -85,6 +98,7 @@ check_password(User, AuthzId, Server, Token) ->
end
end.
user_exists(User, Host) ->
%% Checking that the user has an active session
%% If the session was negociated by the JWT auth method then we define that the user exists
@ -94,9 +108,11 @@ user_exists(User, Host) ->
_ -> false
end}.
use_cache(_) ->
false.
%%%----------------------------------------------------------------------
%%% 'ejabberd_hooks' callback
%%%----------------------------------------------------------------------
@ -107,7 +123,8 @@ check_decoded_jwt(true, Fields, _Signature, Server, User) ->
try
JID = jid:decode(SJid),
JID#jid.luser == User andalso JID#jid.lserver == Server
catch error:{bad_jid, _} ->
catch
error:{bad_jid, _} ->
false
end;
_ -> % error | {ok, _UnknownType}
@ -116,6 +133,7 @@ check_decoded_jwt(true, Fields, _Signature, Server, User) ->
check_decoded_jwt(Acc, _, _, _, _) ->
Acc.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
@ -136,8 +154,7 @@ check_jwt_token(User, Server, Token) ->
check_decoded_jwt,
Server,
true,
[Fields, Signature, Server, User]
);
[Fields, Signature, Server, User]);
true ->
%% return false, if token has expired
false

View file

@ -31,19 +31,32 @@
-behaviour(ejabberd_auth).
%% gen_server callbacks
-export([init/1, handle_info/2, handle_call/3,
handle_cast/2, terminate/2, code_change/3]).
-export([init/1,
handle_info/2,
handle_call/3,
handle_cast/2,
terminate/2,
code_change/3]).
-export([start/1, stop/1, start_link/1, set_password/3,
check_password/4, user_exists/2,
get_users/2, count_users/2,
store_type/1, plain_password_required/1,
-export([start/1,
stop/1,
start_link/1,
set_password/3,
check_password/4,
user_exists/2,
get_users/2,
count_users/2,
store_type/1,
plain_password_required/1,
reload/1]).
-include("logger.hrl").
-include("eldap.hrl").
%%
%% @efmt:off
%% @indent-begin
-record(state,
{host = <<"">> :: binary(),
eldap_id = <<"">> :: binary(),
@ -61,29 +74,42 @@
deref_aliases = never :: never | searching | finding | always,
dn_filter :: binary() | undefined,
dn_filter_attrs = [] :: [binary()]}).
%% @indent-end
%% @efmt:on
%%
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-define(LDAP_SEARCH_TIMEOUT, 5).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
ChildSpec = {Proc, {?MODULE, start_link, [Host]},
transient, 1000, worker, [?MODULE]},
ChildSpec = {Proc,
{?MODULE, start_link, [Host]},
transient,
1000,
worker,
[?MODULE]},
supervisor:start_child(ejabberd_backend_sup, ChildSpec).
stop(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
case supervisor:terminate_child(ejabberd_backend_sup, Proc) of
@ -91,35 +117,49 @@ stop(Host) ->
Err -> Err
end.
start_link(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:start_link({local, Proc}, ?MODULE, Host, []).
terminate(_Reason, _State) -> ok.
init(Host) ->
process_flag(trap_exit, true),
State = parse_options(Host),
eldap_pool:start_link(State#state.eldap_id,
State#state.servers, State#state.backups,
State#state.port, State#state.dn,
State#state.password, State#state.tls_options),
State#state.servers,
State#state.backups,
State#state.port,
State#state.dn,
State#state.password,
State#state.tls_options),
eldap_pool:start_link(State#state.bind_eldap_id,
State#state.servers, State#state.backups,
State#state.port, State#state.dn,
State#state.password, State#state.tls_options),
State#state.servers,
State#state.backups,
State#state.port,
State#state.dn,
State#state.password,
State#state.tls_options),
{ok, State}.
reload(Host) ->
stop(Host),
start(Host).
plain_password_required(_) -> true.
store_type(_) -> external.
check_password(User, AuthzId, Server, Password) ->
if AuthzId /= <<>> andalso AuthzId /= User ->
if
AuthzId /= <<>> andalso AuthzId /= User ->
{nocache, false};
Password == <<"">> ->
{nocache, false};
@ -130,33 +170,39 @@ check_password(User, AuthzId, Server, Password) ->
end
end.
set_password(User, Server, Password) ->
{ok, State} = eldap_utils:get_state(Server, ?MODULE),
case find_user_dn(User, State) of
false -> {cache, {error, db_failure}};
DN ->
case eldap_pool:modify_passwd(State#state.eldap_id, DN,
case eldap_pool:modify_passwd(State#state.eldap_id,
DN,
Password) of
ok -> {cache, {ok, Password}};
_Err -> {nocache, {error, db_failure}}
end
end.
get_users(Server, []) ->
case catch get_users_ldap(Server) of
{'EXIT', _} -> [];
Result -> Result
end.
count_users(Server, Opts) ->
length(get_users(Server, Opts)).
user_exists(User, Server) ->
case catch user_exists_ldap(User, Server) of
{'EXIT', _Error} -> {nocache, {error, db_failure}};
Result -> {cache, Result}
end.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
@ -165,14 +211,15 @@ check_password_ldap(User, Server, Password) ->
case find_user_dn(User, State) of
false -> false;
DN ->
case eldap_pool:bind(State#state.bind_eldap_id, DN,
Password)
of
case eldap_pool:bind(State#state.bind_eldap_id,
DN,
Password) of
ok -> true;
_ -> false
end
end.
get_users_ldap(Server) ->
{ok, State} = eldap_utils:get_state(Server, ?MODULE),
UIDs = State#state.uids,
@ -186,24 +233,21 @@ get_users_ldap(Server) ->
{filter, EldapFilter},
{timeout, ?LDAP_SEARCH_TIMEOUT},
{deref_aliases, State#state.deref_aliases},
{attributes, ResAttrs}])
of
{attributes, ResAttrs}]) of
#eldap_search_result{entries = Entries} ->
lists:flatmap(fun (#eldap_entry{attributes = Attrs,
object_name = DN}) ->
lists:flatmap(fun(#eldap_entry{
attributes = Attrs,
object_name = DN
}) ->
case is_valid_dn(DN, Attrs, State) of
false -> [];
_ ->
case
eldap_utils:find_ldap_attrs(UIDs,
Attrs)
of
case eldap_utils:find_ldap_attrs(UIDs,
Attrs) of
<<"">> -> [];
{User, UIDFormat} ->
case
eldap_utils:get_user_part(User,
UIDFormat)
of
case eldap_utils:get_user_part(User,
UIDFormat) of
{ok, U} ->
case jid:nodeprep(U) of
error -> [];
@ -222,6 +266,7 @@ get_users_ldap(Server) ->
_ -> []
end.
user_exists_ldap(User, Server) ->
{ok, State} = eldap_utils:get_state(Server, ?MODULE),
case find_user_dn(User, State) of
@ -229,6 +274,7 @@ user_exists_ldap(User, Server) ->
_DN -> true
end.
handle_call(get_state, _From, State) ->
{reply, {ok, State}, State};
handle_call(stop, _From, State) ->
@ -237,68 +283,75 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
find_user_dn(User, State) ->
ResAttrs = result_attrs(State),
case eldap_filter:parse(State#state.ufilter,
[{<<"%u">>, User}])
of
[{<<"%u">>, User}]) of
{ok, Filter} ->
case eldap_pool:search(State#state.eldap_id,
[{base, State#state.base}, {filter, Filter},
[{base, State#state.base},
{filter, Filter},
{deref_aliases, State#state.deref_aliases},
{attributes, ResAttrs}])
of
#eldap_search_result{entries =
[#eldap_entry{attributes = Attrs,
object_name = DN}
| _]} ->
{attributes, ResAttrs}]) of
#eldap_search_result{
entries =
[#eldap_entry{
attributes = Attrs,
object_name = DN
} | _]
} ->
is_valid_dn(DN, Attrs, State);
_ -> false
end;
_ -> false
end.
%% Check that the DN is valid, based on the dn filter
is_valid_dn(DN, _, #state{dn_filter = undefined}) -> DN;
is_valid_dn(DN, Attrs, State) ->
DNAttrs = State#state.dn_filter_attrs,
UIDs = State#state.uids,
Values = [{<<"%s">>,
eldap_utils:get_ldap_attr(Attr, Attrs), 1}
|| Attr <- DNAttrs],
Values = [ {<<"%s">>,
eldap_utils:get_ldap_attr(Attr, Attrs),
1}
|| Attr <- DNAttrs ],
SubstValues = case eldap_utils:find_ldap_attrs(UIDs,
Attrs)
of
Attrs) of
<<"">> -> Values;
{S, UAF} ->
case eldap_utils:get_user_part(S, UAF) of
{ok, U} -> [{<<"%u">>, U} | Values];
_ -> Values
end
end
++ [{<<"%d">>, State#state.host}, {<<"%D">>, DN}],
end ++
[{<<"%d">>, State#state.host}, {<<"%D">>, DN}],
case eldap_filter:parse(State#state.dn_filter,
SubstValues)
of
SubstValues) of
{ok, EldapFilter} ->
case eldap_pool:search(State#state.eldap_id,
[{base, State#state.base},
{filter, EldapFilter},
{deref_aliases, State#state.deref_aliases},
{attributes, [<<"dn">>]}])
of
{attributes, [<<"dn">>]}]) of
#eldap_search_result{entries = [_ | _]} -> DN;
_ -> false
end;
_ -> false
end.
result_attrs(#state{uids = UIDs,
dn_filter_attrs = DNFilterAttrs}) ->
lists:foldl(fun ({UID}, Acc) -> [UID | Acc];
result_attrs(#state{
uids = UIDs,
dn_filter_attrs = DNFilterAttrs
}) ->
lists:foldl(fun({UID}, Acc) -> [UID | Acc];
({UID, _}, Acc) -> [UID | Acc]
end,
DNFilterAttrs, UIDs).
DNFilterAttrs,
UIDs).
%%%----------------------------------------------------------------------
%%% Auxiliary functions
@ -319,7 +372,9 @@ parse_options(Host) ->
end,
SearchFilter = eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}]),
{DNFilter, DNFilterAttrs} = ejabberd_option:ldap_dn_filter(Host),
#state{host = Host, eldap_id = Eldap_ID,
#state{
host = Host,
eldap_id = Eldap_ID,
bind_eldap_id = Bind_Eldap_ID,
servers = Cfg#eldap_config.servers,
backups = Cfg#eldap_config.backups,
@ -329,6 +384,9 @@ parse_options(Host) ->
password = Cfg#eldap_config.password,
base = Cfg#eldap_config.base,
deref_aliases = Cfg#eldap_config.deref_aliases,
uids = UIDs, ufilter = UserFilter,
uids = UIDs,
ufilter = UserFilter,
sfilter = SearchFilter,
dn_filter = DNFilter, dn_filter_attrs = DNFilterAttrs}.
dn_filter = DNFilter,
dn_filter_attrs = DNFilterAttrs
}.

View file

@ -29,19 +29,34 @@
-behaviour(ejabberd_auth).
-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3,
get_users/2, init_db/0,
count_users/2, get_password/2,
remove_user/2, store_type/1, import/2,
plain_password_required/1, use_cache/1, drop_password_type/2, set_password_instance/3]).
-export([start/1,
stop/1,
set_password_multiple/3,
try_register_multiple/3,
get_users/2,
init_db/0,
count_users/2,
get_password/2,
remove_user/2,
store_type/1,
import/2,
plain_password_required/1,
use_cache/1,
drop_password_type/2,
set_password_instance/3]).
-export([need_transform/1, transform/1]).
-include("logger.hrl").
-include_lib("xmpp/include/scram.hrl").
-include("ejabberd_auth.hrl").
-record(reg_users_counter, {vhost = <<"">> :: binary(),
count = 0 :: integer() | '$1'}).
-record(reg_users_counter, {
vhost = <<"">> :: binary(),
count = 0 :: integer() | '$1'
}).
%%%----------------------------------------------------------------------
%%% API
@ -51,27 +66,35 @@ start(Host) ->
update_reg_users_counter_table(Host),
ok.
stop(_Host) ->
ok.
init_db() ->
ejabberd_mnesia:create(?MODULE, passwd,
ejabberd_mnesia:create(?MODULE,
passwd,
[{disc_only_copies, [node()]},
{attributes, record_info(fields, passwd)}]),
ejabberd_mnesia:create(?MODULE, reg_users_counter,
ejabberd_mnesia:create(?MODULE,
reg_users_counter,
[{ram_copies, [node()]},
{attributes, record_info(fields, reg_users_counter)}]).
update_reg_users_counter_table(Server) ->
Set = get_users(Server, []),
Size = length(Set),
LServer = jid:nameprep(Server),
F = fun () ->
mnesia:write(#reg_users_counter{vhost = LServer,
count = Size})
F = fun() ->
mnesia:write(#reg_users_counter{
vhost = LServer,
count = Size
})
end,
mnesia:sync_dirty(F).
use_cache(Host) ->
case mnesia:table_info(passwd, storage_type) of
disc_only_copies ->
@ -80,12 +103,15 @@ use_cache(Host) ->
false
end.
plain_password_required(Server) ->
store_type(Server) == scram.
store_type(Server) ->
ejabberd_auth:password_format(Server).
set_password_multiple(User, Server, Passwords) ->
F = fun() ->
lists:foreach(
@ -93,7 +119,8 @@ set_password_multiple(User, Server, Passwords) ->
mnesia:write(#passwd{us = {User, Server, Hash}, password = Password});
(Plain) ->
mnesia:write(#passwd{us = {User, Server, plain}, password = Plain})
end, Passwords)
end,
Passwords)
end,
case mnesia:transaction(F) of
{atomic, ok} ->
@ -103,6 +130,7 @@ set_password_multiple(User, Server, Passwords) ->
{nocache, {error, db_failure}}
end.
set_password_instance(User, Server, Password) ->
F = fun() ->
case Password of
@ -120,9 +148,11 @@ set_password_instance(User, Server, Password) ->
{error, db_failure}
end.
try_register_multiple(User, Server, Passwords) ->
F = fun() ->
case mnesia:select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'},
case mnesia:select(passwd,
[{{'_', {'$1', '$2', '_'}, '$3'},
[{'==', '$1', User},
{'==', '$2', Server}],
['$3']}]) of
@ -132,7 +162,8 @@ try_register_multiple(User, Server, Passwords) ->
mnesia:write(#passwd{us = {User, Server, Hash}, password = Password});
(Plain) ->
mnesia:write(#passwd{us = {User, Server, plain}, password = Plain})
end, Passwords),
end,
Passwords),
mnesia:dirty_update_counter(reg_users_counter, Server, 1),
{ok, Passwords};
[_] ->
@ -147,11 +178,13 @@ try_register_multiple(User, Server, Passwords) ->
{nocache, {error, db_failure}}
end.
get_users(Server, []) ->
Users = mnesia:dirty_select(passwd,
[{#passwd{us = '$1', _ = '_'},
[{'==', {element, 2, '$1'}, Server}], ['$1']}]),
misc:lists_uniq([{U, S} || {U, S, _} <- Users]);
[{'==', {element, 2, '$1'}, Server}],
['$1']}]),
misc:lists_uniq([ {U, S} || {U, S, _} <- Users ]);
get_users(Server, [{from, Start}, {to, End}])
when is_integer(Start) and is_integer(End) ->
get_users(Server, [{limit, End - Start + 1}, {offset, Start}]);
@ -163,28 +196,32 @@ get_users(Server, [{limit, Limit}, {offset, Offset}])
Users ->
Set = lists:keysort(1, Users),
L = length(Set),
Start = if Offset < 1 -> 1;
Start = if
Offset < 1 -> 1;
Offset > L -> L;
true -> Offset
end,
lists:sublist(Set, Start, Limit)
end;
get_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) ->
Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)],
Set = [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ],
lists:keysort(1, Set);
get_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}])
when is_binary(Prefix) and is_integer(Start) and is_integer(End) ->
get_users(Server, [{prefix, Prefix}, {limit, End - Start + 1},
get_users(Server,
[{prefix, Prefix},
{limit, End - Start + 1},
{offset, Start}]);
get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}])
when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) ->
case [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)] of
case [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ] of
[] ->
[];
Users ->
Set = lists:keysort(1, Users),
L = length(Set),
Start = if Offset < 1 -> 1;
Start = if
Offset < 1 -> 1;
Offset > L -> L;
true -> Offset
end,
@ -193,40 +230,52 @@ get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}])
get_users(Server, _) ->
get_users(Server, []).
count_users(Server, []) ->
case mnesia:dirty_select(
reg_users_counter,
[{#reg_users_counter{vhost = Server, count = '$1'},
[], ['$1']}]) of
[],
['$1']}]) of
[Count] -> Count;
_ -> 0
end;
count_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) ->
Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)],
Set = [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ],
length(Set);
count_users(Server, _) ->
count_users(Server, []).
get_password(User, Server) ->
case mnesia:dirty_select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'},
case mnesia:dirty_select(passwd,
[{{'_', {'$1', '$2', '_'}, '$3'},
[{'==', '$1', User},
{'==', '$2', Server}],
['$3']}]) of
[_|_] = List ->
[_ | _] = List ->
List2 = lists:map(
fun({scram, SK, SEK, Salt, IC}) ->
#scram{storedkey = SK, serverkey = SEK,
salt = Salt, hash = sha, iterationcount = IC};
#scram{
storedkey = SK,
serverkey = SEK,
salt = Salt,
hash = sha,
iterationcount = IC
};
(Other) -> Other
end, List),
end,
List),
{cache, {ok, List2}};
_ ->
{cache, error}
end.
drop_password_type(Server, Hash) ->
F = fun() ->
Keys = mnesia:select(passwd, [{{'_', '$1', '_'},
Keys = mnesia:select(passwd,
[{{'_', '$1', '_'},
[{'==', {element, 3, '$1'}, Hash},
{'==', {element, 2, '$1'}, Server}],
['$1']}]),
@ -241,9 +290,11 @@ drop_password_type(Server, Hash) ->
{error, db_failure}
end.
remove_user(User, Server) ->
F = fun () ->
Keys = mnesia:select(passwd, [{{'_', '$1', '_'},
F = fun() ->
Keys = mnesia:select(passwd,
[{{'_', '$1', '_'},
[{'==', {element, 1, '$1'}, User},
{'==', {element, 2, '$1'}, Server}],
['$1']}]),
@ -259,6 +310,7 @@ remove_user(User, Server) ->
{error, db_failure}
end.
need_transform(#reg_users_counter{}) ->
false;
need_transform({passwd, {_U, _S, _T}, _Pass}) ->
@ -266,17 +318,21 @@ need_transform({passwd, {_U, _S, _T}, _Pass}) ->
need_transform({passwd, {_U, _S}, _Pass}) ->
true.
transform({passwd, {U, S}, Pass})
when is_list(U) orelse is_list(S) orelse is_list(Pass) ->
NewUS = {iolist_to_binary(U), iolist_to_binary(S)},
NewPass = case Pass of
#scram{storedkey = StoredKey,
#scram{
storedkey = StoredKey,
serverkey = ServerKey,
salt = Salt} ->
salt = Salt
} ->
Pass#scram{
storedkey = iolist_to_binary(StoredKey),
serverkey = iolist_to_binary(ServerKey),
salt = iolist_to_binary(Salt)};
salt = iolist_to_binary(Salt)
};
_ ->
iolist_to_binary(Pass)
end,
@ -285,13 +341,21 @@ transform(#passwd{us = {U, S}, password = Password} = P)
when is_binary(Password) ->
P#passwd{us = {U, S, plain}, password = Password};
transform({passwd, {U, S}, {scram, SK, SEK, Salt, IC}}) ->
#passwd{us = {U, S, sha},
password = #scram{storedkey = SK, serverkey = SEK,
salt = Salt, hash = sha, iterationcount = IC}};
#passwd{
us = {U, S, sha},
password = #scram{
storedkey = SK,
serverkey = SEK,
salt = Salt,
hash = sha,
iterationcount = IC
}
};
transform(#passwd{us = {U, S}, password = #scram{hash = Hash}} = P) ->
P#passwd{us = {U, S, Hash}};
transform(Other) -> Other.
import(LServer, [LUser, Password, _TimeStamp]) ->
mnesia:dirty_write(
#passwd{us = {LUser, LServer}, password = Password}).

View file

@ -28,17 +28,25 @@
-behaviour(ejabberd_auth).
-export([start/1, stop/1, check_password/4,
user_exists/2, store_type/1, plain_password_required/1]).
-export([start/1,
stop/1,
check_password/4,
user_exists/2,
store_type/1,
plain_password_required/1]).
start(_Host) ->
ejabberd:start_app(epam).
stop(_Host) ->
ok.
check_password(User, AuthzId, Host, Password) ->
if AuthzId /= <<>> andalso AuthzId /= User ->
if
AuthzId /= <<>> andalso AuthzId /= User ->
false;
true ->
Service = get_pam_service(Host),
@ -53,6 +61,7 @@ check_password(User, AuthzId, Host, Password) ->
end
end.
user_exists(User, Host) ->
Service = get_pam_service(Host),
UserInfo = case get_pam_userinfotype(Host) of
@ -65,15 +74,19 @@ user_exists(User, Host) ->
_Err -> {nocache, {error, db_failure}}
end.
plain_password_required(_) -> true.
store_type(_) -> external.
%%====================================================================
%% Internal functions
%%====================================================================
get_pam_service(Host) ->
ejabberd_option:pam_service(Host).
get_pam_userinfotype(Host) ->
ejabberd_option:pam_userinfotype(Host).

View file

@ -25,22 +25,33 @@
-module(ejabberd_auth_sql).
-author('alexey@process-one.net').
-behaviour(ejabberd_auth).
-export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3,
get_users/2, count_users/2, get_password/2,
remove_user/2, store_type/1, plain_password_required/1,
export/1, which_users_exists/2, drop_password_type/2, set_password_instance/3]).
-export([start/1,
stop/1,
set_password_multiple/3,
try_register_multiple/3,
get_users/2,
count_users/2,
get_password/2,
remove_user/2,
store_type/1,
plain_password_required/1,
export/1,
which_users_exists/2,
drop_password_type/2,
set_password_instance/3]).
-export([sql_schemas/0]).
-include_lib("xmpp/include/scram.hrl").
-include("logger.hrl").
-include("ejabberd_sql_pt.hrl").
-include("ejabberd_auth.hrl").
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
@ -48,9 +59,9 @@ start(Host) ->
ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
ok.
sql_schemas() ->
[
#sql_schema{
[#sql_schema{
version = 2,
tables =
[#sql_table{
@ -60,22 +71,35 @@ sql_schemas() ->
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"type">>, type = smallint},
#sql_column{name = <<"password">>, type = text},
#sql_column{name = <<"serverkey">>, type = {text, 128},
default = true},
#sql_column{name = <<"salt">>, type = {text, 128},
default = true},
#sql_column{name = <<"iterationcount">>, type = integer,
default = true},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
#sql_column{
name = <<"serverkey">>,
type = {text, 128},
default = true
},
#sql_column{
name = <<"salt">>,
type = {text, 128},
default = true
},
#sql_column{
name = <<"iterationcount">>,
type = integer,
default = true
},
#sql_column{
name = <<"created_at">>,
type = timestamp,
default = true
}],
indices = [#sql_index{
columns = [<<"server_host">>, <<"username">>, <<"type">>],
unique = true}]}],
update = [
{add_column, <<"users">>, <<"type">>},
{update_primary_key,<<"users">>,
[<<"server_host">>, <<"username">>, <<"type">>]}
]},
unique = true
}]
}],
update = [{add_column, <<"users">>, <<"type">>},
{update_primary_key, <<"users">>,
[<<"server_host">>, <<"username">>, <<"type">>]}]
},
#sql_schema{
version = 1,
tables =
@ -85,40 +109,73 @@ sql_schemas() ->
[#sql_column{name = <<"username">>, type = text},
#sql_column{name = <<"server_host">>, type = text},
#sql_column{name = <<"password">>, type = text},
#sql_column{name = <<"serverkey">>, type = {text, 128},
default = true},
#sql_column{name = <<"salt">>, type = {text, 128},
default = true},
#sql_column{name = <<"iterationcount">>, type = integer,
default = true},
#sql_column{name = <<"created_at">>, type = timestamp,
default = true}],
#sql_column{
name = <<"serverkey">>,
type = {text, 128},
default = true
},
#sql_column{
name = <<"salt">>,
type = {text, 128},
default = true
},
#sql_column{
name = <<"iterationcount">>,
type = integer,
default = true
},
#sql_column{
name = <<"created_at">>,
type = timestamp,
default = true
}],
indices = [#sql_index{
columns = [<<"server_host">>, <<"username">>],
unique = true}]}]}].
unique = true
}]
}]
}].
stop(_Host) -> ok.
plain_password_required(Server) ->
store_type(Server) == scram.
store_type(Server) ->
ejabberd_auth:password_format(Server).
hash_to_num(plain) -> 1;
hash_to_num(sha) -> 2;
hash_to_num(sha256) -> 3;
hash_to_num(sha512) -> 4.
num_to_hash(2) -> sha;
num_to_hash(3) -> sha256;
num_to_hash(4) -> sha512.
set_password_instance(User, Server, #scram{hash = Hash, storedkey = SK, serverkey = SEK,
salt = Salt, iterationcount = IC}) ->
set_password_instance(User,
Server,
#scram{
hash = Hash,
storedkey = SK,
serverkey = SEK,
salt = Salt,
iterationcount = IC
}) ->
F = fun() ->
set_password_scram_t(User, Server, Hash,
SK, SEK, Salt, IC)
set_password_scram_t(User,
Server,
Hash,
SK,
SEK,
Salt,
IC)
end,
case ejabberd_sql:sql_transaction(Server, F) of
{atomic, _} ->
@ -137,20 +194,32 @@ set_password_instance(User, Server, Plain) ->
{error, db_failure}
end.
set_password_multiple(User, Server, Passwords) ->
F =
fun() ->
ejabberd_sql:sql_query_t(
?SQL("delete from users where username=%(User)s and %(Server)H")),
lists:foreach(
fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK,
salt = Salt, iterationcount = IC}) ->
fun(#scram{
hash = Hash,
storedkey = SK,
serverkey = SEK,
salt = Salt,
iterationcount = IC
}) ->
set_password_scram_t(
User, Server, Hash,
SK, SEK, Salt, IC);
User,
Server,
Hash,
SK,
SEK,
Salt,
IC);
(Plain) ->
set_password_t(User, Server, Plain)
end, Passwords)
end,
Passwords)
end,
case ejabberd_sql:sql_transaction(Server, F) of
{atomic, _} ->
@ -159,6 +228,7 @@ set_password_multiple(User, Server, Passwords) ->
{nocache, {error, db_failure}}
end.
try_register_multiple(User, Server, Passwords) ->
F =
fun() ->
@ -166,14 +236,25 @@ try_register_multiple(User, Server, Passwords) ->
?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of
{selected, [{0}]} ->
lists:foreach(
fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK,
salt = Salt, iterationcount = IC}) ->
fun(#scram{
hash = Hash,
storedkey = SK,
serverkey = SEK,
salt = Salt,
iterationcount = IC
}) ->
set_password_scram_t(
User, Server, Hash,
SK, SEK, Salt, IC);
User,
Server,
Hash,
SK,
SEK,
Salt,
IC);
(Plain) ->
set_password_t(User, Server, Plain)
end, Passwords),
end,
Passwords),
{cache, {ok, Passwords}};
{selected, _} ->
{nocache, {error, exists}};
@ -188,13 +269,15 @@ try_register_multiple(User, Server, Passwords) ->
{nocache, {error, db_failure}}
end.
get_users(Server, Opts) ->
case list_users(Server, Opts) of
{selected, Res} ->
[{U, Server} || {U} <- Res];
[ {U, Server} || {U} <- Res ];
_ -> []
end.
count_users(Server, Opts) ->
case users_number(Server, Opts) of
{selected, [{Res}]} ->
@ -202,6 +285,7 @@ count_users(Server, Opts) ->
_Other -> 0
end.
get_password(User, Server) ->
case get_password_scram(Server, User) of
{selected, []} ->
@ -225,24 +309,30 @@ get_password(User, Server) ->
update_password_type(User, Server, 2),
{sha, Other}
end,
#scram{storedkey = SK,
#scram{
storedkey = SK,
serverkey = ServerKey,
salt = Salt,
hash = Hash,
iterationcount = IterationCount};
iterationcount = IterationCount
};
({Type, StoredKey, ServerKey, Salt, IterationCount}) ->
Hash = num_to_hash(Type),
#scram{storedkey = StoredKey,
#scram{
storedkey = StoredKey,
serverkey = ServerKey,
salt = Salt,
hash = Hash,
iterationcount = IterationCount}
end, Passwords),
iterationcount = IterationCount
}
end,
Passwords),
{cache, {ok, Converted}};
_ ->
{nocache, error}
end.
remove_user(User, Server) ->
case del_user(Server, User) of
{updated, _} ->
@ -251,6 +341,7 @@ remove_user(User, Server) ->
{error, db_failure}
end.
drop_password_type(LServer, Hash) ->
Type = hash_to_num(Hash),
ejabberd_sql:sql_query(
@ -258,8 +349,14 @@ drop_password_type(LServer, Hash) ->
?SQL("delete from users"
" where type=%(Type)d and %(LServer)H")).
set_password_scram_t(LUser, LServer, Hash,
StoredKey, ServerKey, Salt, IterationCount) ->
set_password_scram_t(LUser,
LServer,
Hash,
StoredKey,
ServerKey,
Salt,
IterationCount) ->
Type = hash_to_num(Hash),
?SQL_UPSERT_T(
"users",
@ -271,6 +368,7 @@ set_password_scram_t(LUser, LServer, Hash,
"salt=%(Salt)s",
"iterationcount=%(IterationCount)d"]).
set_password_t(LUser, LServer, Password) ->
?SQL_UPSERT_T(
"users",
@ -282,18 +380,21 @@ set_password_t(LUser, LServer, Password) ->
"salt=''",
"iterationcount=0"]).
update_password_type(LUser, LServer, Type, Password) ->
ejabberd_sql:sql_query(
LServer,
?SQL("update users set type=%(Type)d, password=%(Password)s"
" where username=%(LUser)s and type=0 and %(LServer)H")).
update_password_type(LUser, LServer, Type) ->
ejabberd_sql:sql_query(
LServer,
?SQL("update users set type=%(Type)d"
" where username=%(LUser)s and type=0 and %(LServer)H")).
get_password_scram(LServer, LUser) ->
ejabberd_sql:sql_query(
LServer,
@ -301,11 +402,13 @@ get_password_scram(LServer, LUser) ->
" from users"
" where username=%(LUser)s and %(LServer)H")).
del_user(LServer, LUser) ->
ejabberd_sql:sql_query(
LServer,
?SQL("delete from users where username=%(LUser)s and %(LServer)H")).
list_users(LServer, []) ->
ejabberd_sql:sql_query(
LServer,
@ -319,7 +422,8 @@ list_users(LServer,
when is_binary(Prefix) and is_integer(Start) and
is_integer(End) ->
list_users(LServer,
[{prefix, Prefix}, {limit, End - Start + 1},
[{prefix, Prefix},
{limit, End - Start + 1},
{offset, Start - 1}]);
list_users(LServer, [{limit, Limit}, {offset, Offset}])
when is_integer(Limit) and is_integer(Offset) ->
@ -342,12 +446,12 @@ list_users(LServer,
"order by username "
"limit %(Limit)d offset %(Offset)d")).
users_number(LServer) ->
ejabberd_sql:sql_query(
LServer,
fun(pgsql, _) ->
case
ejabberd_option:pgsql_users_number_estimate(LServer) of
case ejabberd_option:pgsql_users_number_estimate(LServer) of
true ->
ejabberd_sql:sql_query_t(
?SQL("select @(reltuples :: bigint)d from pg_class"
@ -361,6 +465,7 @@ users_number(LServer) ->
?SQL("select @(count(distinct username))d from users where %(LServer)H"))
end).
users_number(LServer, [{prefix, Prefix}])
when is_binary(Prefix) ->
SPrefix = ejabberd_sql:escape_like_arg(Prefix),
@ -372,15 +477,17 @@ users_number(LServer, [{prefix, Prefix}])
users_number(LServer, []) ->
users_number(LServer).
which_users_exists(LServer, LUsers) when length(LUsers) =< 100 ->
try ejabberd_sql:sql_query(
LServer,
?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of
{selected, Matching} ->
[U || {U} <- Matching];
[ U || {U} <- Matching ];
{error, _} = E ->
E
catch _:B ->
catch
_:B ->
{error, B}
end;
which_users_exists(LServer, LUsers) ->
@ -397,6 +504,7 @@ which_users_exists(LServer, LUsers) ->
end
end.
export(_Server) ->
[{passwd,
fun(Host, #passwd{us = {LUser, LServer, plain}, password = Password})
@ -409,7 +517,8 @@ export(_Server) ->
"server_host=%(LServer)s",
"type=1",
"password=%(Password)s"])];
(Host, {passwd, {LUser, LServer, _},
(Host,
{passwd, {LUser, LServer, _},
{scram, StoredKey, ServerKey, Salt, IterationCount}})
when LServer == Host ->
Hash = sha,

View file

@ -29,12 +29,14 @@
%% Supervisor callbacks
-export([init/1]).
%%%===================================================================
%%% API functions
%%%===================================================================
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================

View file

@ -34,7 +34,11 @@
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-export([register_task/5, task_status/1, abort_task/1]).
@ -47,36 +51,46 @@
%%% API
%%%===================================================================
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
register_task(Type, Steps, Rate, JobState, JobFun) ->
gen_server:call(?MODULE, {register_task, Type, Steps, Rate, JobState, JobFun}).
task_status(Type) ->
gen_server:call(?MODULE, {task_status, Type}).
abort_task(Type) ->
gen_server:call(?MODULE, {abort_task, Type}).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
{ok, State :: #state{}} |
{ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} |
ignore).
init([]) ->
{ok, #state{}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
-spec(handle_call(Request :: term(),
From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
@ -120,6 +134,7 @@ handle_call({abort_task, Type}, _From, #state{tasks = Tasks} = State) ->
handle_call(_Request, _From, State = #state{}) ->
{reply, ok, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
@ -153,6 +168,7 @@ handle_cast({task_error, Type, Pid, Error}, #state{tasks = Tasks} = State) ->
handle_cast(_Request, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
@ -162,6 +178,7 @@ handle_cast(_Request, State = #state{}) ->
handle_info(_Info, State = #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
@ -172,18 +189,22 @@ handle_info(_Info, State = #state{}) ->
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
-spec(code_change(OldVsn :: term() | {down, term()},
State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
work_loop(Task, JobState, JobFun, Rate, StartDate, CurrentProgress) ->
try JobFun(JobState) of
{ok, _NewState, 0} ->
@ -192,14 +213,16 @@ work_loop(Task, JobState, JobFun, Rate, StartDate, CurrentProgress) ->
gen_server:cast(?MODULE, {task_progress, Task, self(), Count}),
NewProgress = CurrentProgress + Count,
TimeSpent = erlang:monotonic_time(second) - StartDate,
SleepTime = max(0, NewProgress/Rate*60 - TimeSpent),
SleepTime = max(0, NewProgress / Rate * 60 - TimeSpent),
receive
abort -> ok
after round(SleepTime*1000) ->
after
round(SleepTime * 1000) ->
work_loop(Task, NewState, JobFun, Rate, StartDate, NewProgress)
end;
{error, Error} ->
gen_server:cast(?MODULE, {task_error, Task, self(), Error})
catch _:_ ->
catch
_:_ ->
gen_server:cast(?MODULE, {task_error, Task, self(), internal_error})
end.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@
-export([get_c2s_limits/0]).
%% Get first c2s configuration limitations to apply it to other c2s
%% connectors.
get_c2s_limits() ->
@ -40,13 +41,16 @@ get_c2s_limits() ->
select_opts_values(Opts)
end.
%% Only get access, shaper and max_stanza_size values
select_opts_values(Opts) ->
maps:fold(
fun(Opt, Val, Acc) when Opt == access;
Opt == shaper;
Opt == max_stanza_size ->
[{Opt, Val}|Acc];
[{Opt, Val} | Acc];
(_, _, Acc) ->
Acc
end, [], Opts).
end,
[],
Opts).

View file

@ -34,57 +34,82 @@
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-export([create_captcha/6, build_captcha_html/2,
check_captcha/2, process_reply/1, process/2,
is_feature_available/0, create_captcha_x/5,
host_up/1, host_down/1,
config_reloaded/0, process_iq/1]).
-export([create_captcha/6,
build_captcha_html/2,
check_captcha/2,
process_reply/1,
process/2,
is_feature_available/0,
create_captcha_x/5,
host_up/1,
host_down/1,
config_reloaded/0,
process_iq/1]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("translate.hrl").
-define(CAPTCHA_LIFETIME, 120000).
-define(LIMIT_PERIOD, 60*1000*1000).
-define(LIMIT_PERIOD, 60 * 1000 * 1000).
-type image_error() :: efbig | enodata | limit | malformed_image | timeout.
-type priority() :: neg_integer().
-type callback() :: fun((captcha_succeed | captcha_failed) -> any()).
-record(state, {limits = treap:empty() :: treap:treap(),
enabled = false :: boolean()}).
-record(state, {
limits = treap:empty() :: treap:treap(),
enabled = false :: boolean()
}).
-record(captcha, {id :: binary(),
-record(captcha, {
id :: binary(),
pid :: pid() | undefined,
key :: binary(),
tref :: reference(),
args :: any()}).
args :: any()
}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [],
gen_server:start_link({local, ?MODULE},
?MODULE,
[],
[]).
-spec captcha_text(binary()) -> binary().
captcha_text(Lang) ->
translate:translate(Lang, ?T("Enter the text you see")).
-spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field().
mk_ocr_field(Lang, CID, Type) ->
URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>},
[_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]),
xmpp:set_els(F, [#media{uri = [URI]}]).
update_captcha_key(_Id, Key, Key) ->
ok;
update_captcha_key(Id, _Key, Key2) ->
true = ets:update_element(captcha, Id, [{4, Key2}]).
-spec create_captcha(binary(), jid(), jid(),
binary(), any(),
-spec create_captcha(binary(),
jid(),
jid(),
binary(),
any(),
callback() | term()) -> {error, image_error()} |
{ok, binary(), [text()], [xmpp_element()]}.
create_captcha(SID, From, To, Lang, Limiter, Args) ->
@ -95,24 +120,34 @@ create_captcha(SID, From, To, Lang, Limiter, Args) ->
CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>,
Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image},
Fs = captcha_form:encode(
[{from, To}, {challenge, Id}, {sid, SID},
[{from, To},
{challenge, Id},
{sid, SID},
mk_ocr_field(Lang, CID, Type)],
Lang, [challenge]),
Lang,
[challenge]),
X = #xdata{type = form, fields = Fs},
Captcha = #xcaptcha{xdata = X},
BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. "
"To unblock your subscription request, visit ~s"), [JID, get_url(Id)]},
"To unblock your subscription request, visit ~s"),
[JID, get_url(Id)]},
Body = xmpp:mk_text(BodyString, Lang),
OOB = #oob_x{url = get_url(Id)},
Hint = #hint{type = 'no-store'},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
ets:insert(captcha,
#captcha{id = Id, pid = self(), key = Key, tref = Tref,
args = Args}),
#captcha{
id = Id,
pid = self(),
key = Key,
tref = Tref,
args = Args
}),
{ok, Id, Body, [Hint, OOB, Captcha, Data]};
Err -> Err
end.
-spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) ->
{ok, [xmpp_element()]} | {error, image_error()}.
create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) ->
@ -124,75 +159,105 @@ create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) ->
HelpTxt = translate:translate(
Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")),
Imageurl = get_url(<<Id/binary, "/image">>),
[H|T] = captcha_form:encode(
[H | T] = captcha_form:encode(
[{'captcha-fallback-text', HelpTxt},
{'captcha-fallback-url', Imageurl},
{from, To}, {challenge, Id}, {sid, SID},
{from, To},
{challenge, Id},
{sid, SID},
mk_ocr_field(Lang, CID, Type)],
Lang, [challenge]),
Captcha = X#xdata{type = form, fields = [H|Fs ++ T]},
Lang,
[challenge]),
Captcha = X#xdata{type = form, fields = [H | Fs ++ T]},
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}),
{ok, [Captcha, Data]};
Err -> Err
end.
-spec build_captcha_html(binary(), binary()) -> captcha_not_found |
{xmlel(),
{xmlel(), cdata(),
xmlel(), xmlel()}}.
{xmlel(),
cdata(),
xmlel(),
xmlel()}}.
build_captcha_html(Id, Lang) ->
case lookup_captcha(Id) of
{ok, _} ->
ImgEl = #xmlel{name = <<"img">>,
ImgEl = #xmlel{
name = <<"img">>,
attrs =
[{<<"src">>, get_url(<<Id/binary, "/image">>)}],
children = []},
children = []
},
Text = {xmlcdata, captcha_text(Lang)},
IdEl = #xmlel{name = <<"input">>,
IdEl = #xmlel{
name = <<"input">>,
attrs =
[{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>},
[{<<"type">>, <<"hidden">>},
{<<"name">>, <<"id">>},
{<<"value">>, Id}],
children = []},
KeyEl = #xmlel{name = <<"input">>,
children = []
},
KeyEl = #xmlel{
name = <<"input">>,
attrs =
[{<<"type">>, <<"text">>}, {<<"name">>, <<"key">>},
[{<<"type">>, <<"text">>},
{<<"name">>, <<"key">>},
{<<"size">>, <<"10">>}],
children = []},
FormEl = #xmlel{name = <<"form">>,
children = []
},
FormEl = #xmlel{
name = <<"form">>,
attrs =
[{<<"action">>, get_url(Id)},
{<<"name">>, <<"captcha">>},
{<<"method">>, <<"POST">>}],
children =
[ImgEl,
#xmlel{name = <<"br">>, attrs = [],
children = []},
#xmlel{
name = <<"br">>,
attrs = [],
children = []
},
Text,
#xmlel{name = <<"br">>, attrs = [],
children = []},
IdEl, KeyEl,
#xmlel{name = <<"br">>, attrs = [],
children = []},
#xmlel{name = <<"input">>,
#xmlel{
name = <<"br">>,
attrs = [],
children = []
},
IdEl,
KeyEl,
#xmlel{
name = <<"br">>,
attrs = [],
children = []
},
#xmlel{
name = <<"input">>,
attrs =
[{<<"type">>, <<"submit">>},
{<<"name">>, <<"enter">>},
{<<"value">>, ?T("OK")}],
children = []}]},
children = []
}]
},
{FormEl, {ImgEl, Text, IdEl, KeyEl}};
_ -> captcha_not_found
end.
-spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}.
process_reply(#xdata{} = X) ->
Required = [<<"challenge">>, <<"ocr">>],
Fs = lists:filter(
fun(#xdata_field{var = Var}) ->
lists:member(Var, [<<"FORM_TYPE">>|Required])
end, X#xdata.fields),
lists:member(Var, [<<"FORM_TYPE">> | Required])
end,
X#xdata.fields),
try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of
Props ->
Id = proplists:get_value(challenge, Props),
@ -202,7 +267,8 @@ process_reply(#xdata{} = X) ->
captcha_non_valid -> {error, bad_match};
captcha_not_found -> {error, not_found}
end
catch _:{captcha_form, Why} ->
catch
_:{captcha_form, Why} ->
?WARNING_MSG("Malformed CAPTCHA form: ~ts",
[captcha_form:format_error(Why)]),
{error, malformed}
@ -212,6 +278,7 @@ process_reply(#xcaptcha{xdata = #xdata{} = X}) ->
process_reply(_) ->
{error, malformed}.
-spec process_iq(iq()) -> iq().
process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) ->
case process_reply(El) of
@ -231,20 +298,29 @@ process_iq(#iq{lang = Lang} = IQ) ->
Txt = ?T("No module is handling this query"),
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
process(_Handlers,
#request{method = 'GET', lang = Lang,
path = [_, Id]}) ->
#request{
method = 'GET',
lang = Lang,
path = [_, Id]
}) ->
case build_captcha_html(Id, Lang) of
{FormEl, _} ->
Form = #xmlel{name = <<"div">>,
Form = #xmlel{
name = <<"div">>,
attrs = [{<<"align">>, <<"center">>}],
children = [FormEl]},
children = [FormEl]
},
ejabberd_web:make_xhtml([Form]);
captcha_not_found -> ejabberd_web:error(not_found)
end;
process(_Handlers,
#request{method = 'GET', path = [_, Id, <<"image">>],
ip = IP}) ->
#request{
method = 'GET',
path = [_, Id, <<"image">>],
ip = IP
}) ->
{Addr, _Port} = IP,
case lookup_captcha(Id) of
{ok, #captcha{key = Key}} ->
@ -262,16 +338,23 @@ process(_Handlers,
_ -> ejabberd_web:error(not_found)
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = Lang,
path = [_, Id]}) ->
#request{
method = 'POST',
q = Q,
lang = Lang,
path = [_, Id]
}) ->
ProvidedKey = proplists:get_value(<<"key">>, Q, none),
case check_captcha(Id, ProvidedKey) of
captcha_valid ->
Form = #xmlel{name = <<"p">>, attrs = [],
Form = #xmlel{
name = <<"p">>,
attrs = [],
children =
[{xmlcdata,
translate:translate(Lang,
?T("The CAPTCHA is valid."))}]},
?T("The CAPTCHA is valid."))}]
},
ejabberd_web:make_xhtml([Form]);
captcha_non_valid -> ejabberd_web:error(not_allowed);
captcha_not_found -> ejabberd_web:error(not_found)
@ -279,16 +362,23 @@ process(_Handlers,
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
host_up(Host) ->
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA,
?MODULE, process_iq).
gen_iq_handler:add_iq_handler(ejabberd_sm,
Host,
?NS_CAPTCHA,
?MODULE,
process_iq).
host_down(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA).
config_reloaded() ->
gen_server:call(?MODULE, config_reloaded, timer:minutes(1)).
init([]) ->
_ = mnesia:delete_table(captcha),
_ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]),
@ -303,7 +393,9 @@ init([]) ->
{stop, Reason}
end.
handle_call({is_limited, Limiter, RateLimit}, _From,
handle_call({is_limited, Limiter, RateLimit},
_From,
State) ->
NowPriority = now_priority(),
CleanPriority = NowPriority + (?LIMIT_PERIOD),
@ -312,11 +404,15 @@ handle_call({is_limited, Limiter, RateLimit}, _From,
{ok, _, Rate} when Rate >= RateLimit ->
{reply, true, State#state{limits = Limits}};
{ok, Priority, Rate} ->
NewLimits = treap:insert(Limiter, Priority, Rate + 1,
NewLimits = treap:insert(Limiter,
Priority,
Rate + 1,
Limits),
{reply, false, State#state{limits = NewLimits}};
_ ->
NewLimits = treap:insert(Limiter, NowPriority, 1,
NewLimits = treap:insert(Limiter,
NowPriority,
1,
Limits),
{reply, false, State#state{limits = NewLimits}}
end;
@ -341,10 +437,12 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({remove_id, Id}, State) ->
?DEBUG("CAPTCHA ~p timed out", [Id]),
case ets:lookup(captcha, Id) of
@ -358,35 +456,43 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{enabled = Enabled}) ->
if Enabled -> unregister_handlers();
if
Enabled -> unregister_handlers();
true -> ok
end,
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70).
register_handlers() ->
ejabberd_hooks:add(host_up, ?MODULE, host_up, 50),
ejabberd_hooks:add(host_down, ?MODULE, host_down, 50),
lists:foreach(fun host_up/1, ejabberd_option:hosts()).
unregister_handlers() ->
ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50),
ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50),
lists:foreach(fun host_down/1, ejabberd_option:hosts()).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
-spec create_image() -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image() ->
create_image(undefined).
-spec create_image(term()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image(Limiter) ->
Key = str:substr(p1_rand:get_string(), 1, 6),
create_image(Limiter, Key).
-spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
create_image(Limiter, Key) ->
@ -395,6 +501,7 @@ create_image(Limiter, Key) ->
false -> do_create_image(Key)
end.
-spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} |
{error, image_error()}.
do_create_image(Key) ->
@ -406,6 +513,7 @@ do_create_image(Key) ->
do_create_image(Key, FileName)
end.
do_create_image(Key, Module) when is_atom(Module) ->
Function = create_image,
erlang:apply(Module, Function, [Key]);
@ -439,6 +547,7 @@ do_create_image(Key, FileName) when is_binary(FileName) ->
{error, Reason}
end.
get_prog_name() ->
case ejabberd_option:captcha_cmd() of
undefined ->
@ -452,6 +561,7 @@ get_prog_name() ->
FileName
end.
maybe_warning_norequesthandler() ->
Host = hd(ejabberd_option:hosts()),
AutoURL = get_auto_url(any, ?MODULE, Host),
@ -469,6 +579,7 @@ maybe_warning_norequesthandler() ->
ok
end.
-spec get_url(binary()) -> binary().
get_url(Str) ->
case ejabberd_option:captcha_url() of
@ -483,6 +594,7 @@ get_url(Str) ->
<<URL/binary, $/, Str/binary>>
end.
-spec parse_captcha_host() -> binary().
parse_captcha_host() ->
CaptchaHost = ejabberd_option:captcha_host(),
@ -500,6 +612,7 @@ parse_captcha_host() ->
<<"http://", (ejabberd_config:get_myname())/binary>>
end.
get_auto_url(Tls, Module, Host) ->
case find_handler_port_path(Tls, Module) of
[] -> undefined;
@ -515,12 +628,15 @@ get_auto_url(Tls, Module, Host) ->
true -> <<"https">>
end,
<<Protocol/binary,
"://", Host/binary, ":",
"://",
Host/binary,
":",
(integer_to_binary(Port))/binary,
"/",
(str:join(Path, <<"/">>))/binary>>
end.
find_handler_port_path(Tls, Module) ->
lists:filtermap(
fun({{Port, _, _},
@ -532,19 +648,24 @@ find_handler_port_path(Tls, Module) ->
{Path, Module} -> {true, {ThisTls, Port, Path}}
end;
(_) -> false
end, ets:tab2list(ejabberd_listener)).
end,
ets:tab2list(ejabberd_listener)).
get_transfer_protocol(PortString) ->
PortNumber = binary_to_integer(PortString),
PortListeners = get_port_listeners(PortNumber),
get_captcha_transfer_protocol(PortListeners).
get_port_listeners(PortNumber) ->
AllListeners = ejabberd_option:listen(),
lists:filter(
fun({{Port, _IP, _Transport}, _Module, _Opts}) ->
Port == PortNumber
end, AllListeners).
end,
AllListeners).
get_captcha_transfer_protocol([]) ->
throw(<<"The port number mentioned in captcha_host "
@ -556,7 +677,8 @@ get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) ->
case lists:any(
fun({_, ?MODULE}) -> true;
({_, _}) -> false
end, Handlers) of
end,
Handlers) of
true ->
case maps:get(tls, Opts) of
true -> https;
@ -568,37 +690,43 @@ get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) ->
get_captcha_transfer_protocol([_ | Listeners]) ->
get_captcha_transfer_protocol(Listeners).
is_limited(undefined) -> false;
is_limited(Limiter) ->
case ejabberd_option:captcha_limit() of
infinity -> false;
Int ->
case catch gen_server:call(?MODULE,
{is_limited, Limiter, Int}, 5000)
of
{is_limited, Limiter, Int},
5000) of
true -> true;
false -> false;
Err -> ?ERROR_MSG("Call failed: ~p", [Err]), false
end
end.
-define(CMD_TIMEOUT, 5000).
-define(MAX_FILE_SIZE, 64 * 1024).
-spec cmd(string()) -> {ok, binary()} | {error, image_error()}.
cmd(Cmd) ->
Port = open_port({spawn, Cmd}, [stream, eof, binary]),
TRef = erlang:start_timer(?CMD_TIMEOUT, self(),
TRef = erlang:start_timer(?CMD_TIMEOUT,
self(),
timeout),
recv_data(Port, TRef, <<>>).
-spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}.
recv_data(Port, TRef, Buf) ->
receive
{Port, {data, Bytes}} ->
NewBuf = <<Buf/binary, Bytes/binary>>,
if byte_size(NewBuf) > (?MAX_FILE_SIZE) ->
if
byte_size(NewBuf) > (?MAX_FILE_SIZE) ->
return(Port, TRef, {error, efbig});
true -> recv_data(Port, TRef, NewBuf)
end;
@ -610,6 +738,7 @@ recv_data(Port, TRef, Buf) ->
return(Port, TRef, {error, timeout})
end.
-spec return(port(), reference(), {ok, binary()} | {error, image_error()}) ->
{ok, binary()} | {error, image_error()}.
return(Port, TRef, Result) ->
@ -617,12 +746,14 @@ return(Port, TRef, Result) ->
catch port_close(Port),
Result.
is_feature_available() ->
case get_prog_name() of
PathOrModule when is_binary(PathOrModule) -> true;
false -> false
end.
check_captcha_setup() ->
case is_feature_available() of
true ->
@ -639,6 +770,7 @@ check_captcha_setup() ->
false
end.
-spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}.
lookup_captcha(Id) ->
case ets:lookup(captcha, Id) of
@ -646,6 +778,7 @@ lookup_captcha(Id) ->
[] -> {error, enoent}
end.
-spec check_captcha(binary(), binary()) -> captcha_not_found |
captcha_valid |
captcha_non_valid.
@ -655,7 +788,8 @@ check_captcha(Id, ProvidedKey) ->
{ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} ->
ets:delete(captcha, Id),
misc:cancel_timer(Tref),
if ValidKey == ProvidedKey ->
if
ValidKey == ProvidedKey ->
callback(captcha_succeed, Pid, Args),
captcha_valid;
true ->
@ -666,18 +800,21 @@ check_captcha(Id, ProvidedKey) ->
captcha_not_found
end.
-spec clean_treap(treap:treap(), priority()) -> treap:treap().
clean_treap(Treap, CleanPriority) ->
case treap:is_empty(Treap) of
true -> Treap;
false ->
{_Key, Priority, _Value} = treap:get_root(Treap),
if Priority > CleanPriority ->
if
Priority > CleanPriority ->
clean_treap(treap:delete_root(Treap), CleanPriority);
true -> Treap
end
end.
-spec callback(captcha_succeed | captcha_failed,
pid() | undefined,
callback() | term()) -> any().
@ -688,6 +825,7 @@ callback(Result, Pid, Args) when is_pid(Pid) ->
callback(_, _, _) ->
ok.
-spec now_priority() -> priority().
now_priority() ->
-erlang:system_time(microsecond).

View file

@ -24,14 +24,27 @@
-behaviour(gen_server).
%% API
-export([start_link/0, call/4, call/5, multicall/3, multicall/4, multicall/5,
-export([start_link/0,
call/4, call/5,
multicall/3, multicall/4, multicall/5,
eval_everywhere/3, eval_everywhere/4]).
%% Backend dependent API
-export([get_nodes/0, get_known_nodes/0, join/1, leave/1, subscribe/0,
subscribe/1, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1]).
-export([get_nodes/0,
get_known_nodes/0,
join/1,
leave/1,
subscribe/0, subscribe/1,
node_id/0,
get_node_by_id/1,
send/2,
wait_for_sync/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% hooks
-export([set_ticktime/0]).
@ -39,6 +52,7 @@
-type dst() :: pid() | atom() | {atom(), node()}.
-callback init() -> ok | {error, any()}.
-callback get_nodes() -> [node()].
-callback get_known_nodes() -> [node()].
@ -52,42 +66,51 @@
-record(state, {}).
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec call(node(), module(), atom(), [any()]) -> any().
call(Node, Module, Function, Args) ->
call(Node, Module, Function, Args, rpc_timeout()).
-spec call(node(), module(), atom(), [any()], timeout()) -> any().
call(Node, Module, Function, Args, Timeout) ->
rpc:call(Node, Module, Function, Args, Timeout).
-spec multicall(module(), atom(), [any()]) -> {list(), [node()]}.
multicall(Module, Function, Args) ->
multicall(get_nodes(), Module, Function, Args).
-spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}.
multicall(Nodes, Module, Function, Args) ->
multicall(Nodes, Module, Function, Args, rpc_timeout()).
-spec multicall([node()], module(), atom(), list(), timeout()) -> {list(), [node()]}.
multicall(Nodes, Module, Function, Args, Timeout) ->
rpc:multicall(Nodes, Module, Function, Args, Timeout).
-spec eval_everywhere(module(), atom(), [any()]) -> ok.
eval_everywhere(Module, Function, Args) ->
eval_everywhere(get_nodes(), Module, Function, Args),
ok.
-spec eval_everywhere([node()], module(), atom(), [any()]) -> ok.
eval_everywhere(Nodes, Module, Function, Args) ->
rpc:eval_everywhere(Nodes, Module, Function, Args),
ok.
%%%===================================================================
%%% Backend dependent API
%%%===================================================================
@ -96,31 +119,37 @@ get_nodes() ->
Mod = get_mod(),
Mod:get_nodes().
-spec get_known_nodes() -> [node()].
get_known_nodes() ->
Mod = get_mod(),
Mod:get_known_nodes().
-spec join(node()) -> ok | {error, any()}.
join(Node) ->
Mod = get_mod(),
Mod:join(Node).
-spec leave(node()) -> ok | {error, any()}.
leave(Node) ->
Mod = get_mod(),
Mod:leave(Node).
-spec node_id() -> binary().
node_id() ->
Mod = get_mod(),
Mod:node_id().
-spec get_node_by_id(binary()) -> node().
get_node_by_id(ID) ->
Mod = get_mod(),
Mod:get_node_by_id(ID).
%% Note that false positive returns are possible, while false negatives are not.
%% In other words: positive return value (i.e. 'true') doesn't guarantee
%% successful delivery, while negative return value ('false') means
@ -144,20 +173,24 @@ send(Dst, Msg) ->
Mod = get_mod(),
Mod:send(Dst, Msg).
-spec wait_for_sync(timeout()) -> ok | {error, any()}.
wait_for_sync(Timeout) ->
Mod = get_mod(),
Mod:wait_for_sync(Timeout).
-spec subscribe() -> ok.
subscribe() ->
subscribe(self()).
-spec subscribe(dst()) -> ok.
subscribe(Proc) ->
Mod = get_mod(),
Mod:subscribe(Proc).
%%%===================================================================
%%% Hooks
%%%===================================================================
@ -168,11 +201,13 @@ set_ticktime() ->
?ERROR_MSG("Failed to set new net_ticktime because "
"the net kernel is busy changing it to the "
"previously configured value. Please wait for "
"~B seconds and retry", [Time]);
"~B seconds and retry",
[Time]);
_ ->
ok
end.
%%%===================================================================
%%% gen_server API
%%%===================================================================
@ -181,7 +216,8 @@ init([]) ->
Nodes = ejabberd_option:cluster_nodes(),
lists:foreach(fun(Node) ->
net_kernel:connect_node(Node)
end, Nodes),
end,
Nodes),
Mod = get_mod(),
case Mod:init() of
ok ->
@ -192,14 +228,17 @@ init([]) ->
{stop, Reason}
end.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({node_up, Node}, State) ->
?INFO_MSG("Node ~ts has joined", [Node]),
{noreply, State};
@ -210,12 +249,15 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, set_ticktime, 50).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -223,5 +265,6 @@ get_mod() ->
Backend = ejabberd_option:cluster_backend(),
list_to_existing_atom("ejabberd_cluster_" ++ atom_to_list(Backend)).
rpc_timeout() ->
ejabberd_option:rpc_timeout().

View file

@ -27,26 +27,37 @@
-behaviour(ejabberd_cluster).
%% API
-export([init/0, get_nodes/0, join/1, leave/1,
get_known_nodes/0, node_id/0, get_node_by_id/1,
send/2, wait_for_sync/1, subscribe/1]).
-export([init/0,
get_nodes/0,
join/1,
leave/1,
get_known_nodes/0,
node_id/0,
get_node_by_id/1,
send/2,
wait_for_sync/1,
subscribe/1]).
-include("logger.hrl").
-spec init() -> ok.
init() ->
ok.
-spec get_nodes() -> [node()].
get_nodes() ->
mnesia:system_info(running_db_nodes).
-spec get_known_nodes() -> [node()].
get_known_nodes() ->
lists:usort(mnesia:system_info(db_nodes)
++ mnesia:system_info(extra_db_nodes)).
lists:usort(mnesia:system_info(db_nodes) ++
mnesia:system_info(extra_db_nodes)).
-spec join(node()) -> ok | {error, any()}.
@ -72,12 +83,13 @@ join(Node) ->
{error, {no_ping, Node}}
end.
-spec leave(node()) -> ok | {error, any()}.
leave(Node) ->
case {node(), net_adm:ping(Node)} of
{Node, _} ->
Cluster = get_nodes()--[Node],
Cluster = get_nodes() -- [Node],
leave(Cluster, Node);
{_, pong} ->
rpc:call(Node, ?MODULE, leave, [Node], 10000);
@ -87,9 +99,11 @@ leave(Node) ->
{aborted, Reason} -> {error, Reason}
end
end.
leave([], Node) ->
{error, {no_cluster, Node}};
leave([Master|_], Node) ->
leave([Master | _], Node) ->
application:stop(ejabberd),
application:stop(mnesia),
spawn(fun() ->
@ -99,18 +113,22 @@ leave([Master|_], Node) ->
end),
ok.
-spec node_id() -> binary().
node_id() ->
integer_to_binary(erlang:phash2(node())).
-spec get_node_by_id(binary()) -> node().
get_node_by_id(Hash) ->
try binary_to_integer(Hash) of
I -> match_node_id(I)
catch _:_ ->
catch
_:_ ->
node()
end.
-spec send({atom(), node()}, term()) -> boolean().
send(Dst, Msg) ->
case erlang:send(Dst, Msg, [nosuspend, noconnect]) of
@ -118,34 +136,41 @@ send(Dst, Msg) ->
_ -> false
end.
-spec wait_for_sync(timeout()) -> ok.
wait_for_sync(Timeout) ->
?INFO_MSG("Waiting for Mnesia synchronization to complete", []),
mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout),
ok.
-spec subscribe(_) -> ok.
subscribe(_) ->
ok.
%%%===================================================================
%%% Internal functions
%%%===================================================================
replicate_database(Node) ->
mnesia:change_table_copy_type(schema, node(), disc_copies),
lists:foreach(
fun(Table) ->
Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]),
mnesia:add_table_copy(Table, node(), Type)
end, mnesia:system_info(tables)--[schema]).
end,
mnesia:system_info(tables) -- [schema]).
-spec match_node_id(integer()) -> node().
match_node_id(I) ->
match_node_id(I, get_nodes()).
-spec match_node_id(integer(), [node()]) -> node().
match_node_id(I, [Node|Nodes]) ->
match_node_id(I, [Node | Nodes]) ->
case erlang:phash2(Node) of
I -> Node;
_ -> match_node_id(I, Nodes)

View file

@ -31,43 +31,40 @@
-define(DEFAULT_VERSION, 1000000).
-export([start_link/0,
list_commands/0,
list_commands/1,
list_commands/2,
get_command_format/1,
get_command_format/2,
get_command_format/3,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
register_commands/1,
register_commands/2,
register_commands/3,
unregister_commands/1,
unregister_commands/3,
list_commands/0, list_commands/1, list_commands/2,
get_command_format/1, get_command_format/2, get_command_format/3,
get_command_definition/1, get_command_definition/2,
get_tags_commands/0, get_tags_commands/1,
register_commands/1, register_commands/2, register_commands/3,
unregister_commands/1, unregister_commands/3,
get_commands_spec/0,
get_commands_definition/0,
get_commands_definition/1,
execute_command2/3,
execute_command2/4]).
get_commands_definition/0, get_commands_definition/1,
execute_command2/3, execute_command2/4]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include("ejabberd_commands.hrl").
-include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map().
-record(state, {}).
get_commands_spec() ->
[
#ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation],
[#ejabberd_commands{
name = gen_html_doc_for_commands,
tags = [documentation],
desc = "Generates html documentation for ejabberd_commands",
module = ejabberd_commands_doc, function = generate_html_output,
module = ejabberd_commands_doc,
function = generate_html_output,
args = [{file, binary}, {regexp, binary}, {examples, binary}],
result = {res, rescode},
args_desc = ["Path to file where generated "
@ -78,10 +75,14 @@ get_commands_spec() ->
"that will have example invocation include in markdown document"],
result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok},
#ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation],
result_example = ok
},
#ejabberd_commands{
name = gen_markdown_doc_for_commands,
tags = [documentation],
desc = "Generates markdown documentation for ejabberd_commands",
module = ejabberd_commands_doc, function = generate_md_output,
module = ejabberd_commands_doc,
function = generate_md_output,
args = [{file, binary}, {regexp, binary}, {examples, binary}],
result = {res, rescode},
args_desc = ["Path to file where generated "
@ -93,28 +94,39 @@ get_commands_spec() ->
"that will have example invocation include in markdown document"],
result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok},
#ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation],
result_example = ok
},
#ejabberd_commands{
name = gen_markdown_doc_for_tags,
tags = [documentation],
desc = "Generates markdown documentation for ejabberd_commands",
note = "added in 21.12",
module = ejabberd_commands_doc, function = generate_tags_md,
module = ejabberd_commands_doc,
function = generate_tags_md,
args = [{file, binary}],
result = {res, rescode},
args_desc = ["Path to file where generated "
"documentation should be stored"],
result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/tags.md"],
result_example = ok}].
result_example = ok
}].
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
try mnesia:transform_table(ejabberd_commands, ignore,
try
mnesia:transform_table(ejabberd_commands,
ignore,
record_info(fields, ejabberd_commands))
catch exit:{aborted, {no_exists, _}} -> ok
catch
exit:{aborted, {no_exists, _}} -> ok
end,
ejabberd_mnesia:create(?MODULE, ejabberd_commands,
ejabberd_mnesia:create(?MODULE,
ejabberd_commands,
[{ram_copies, [node()]},
{local_content, true},
{attributes, record_info(fields, ejabberd_commands)},
@ -122,29 +134,36 @@ init([]) ->
register_commands(get_commands_spec()),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec register_commands([ejabberd_commands()]) -> ok.
register_commands(Commands) ->
register_commands(unknown, Commands).
-spec register_commands(atom(), [ejabberd_commands()]) -> ok.
register_commands(Definer, Commands) ->
@ -164,6 +183,7 @@ register_commands(Definer, Commands) ->
ejabberd_access_permissions:invalidate(),
ok.
-spec register_commands(binary(), atom(), [ejabberd_commands()]) -> ok.
register_commands(Host, Definer, Commands) ->
@ -174,11 +194,12 @@ register_commands(Host, Definer, Commands) ->
ok
end.
register_command_prepare(Command, Definer) ->
Tags1 = Command#ejabberd_commands.tags,
Tags2 = case Command#ejabberd_commands.version of
0 -> Tags1;
Version -> Tags1 ++ [list_to_atom("v"++integer_to_list(Version))]
Version -> Tags1 ++ [list_to_atom("v" ++ integer_to_list(Version))]
end,
Command#ejabberd_commands{definer = Definer, tags = Tags2}.
@ -193,6 +214,7 @@ unregister_commands(Commands) ->
Commands),
ejabberd_access_permissions:invalidate().
-spec unregister_commands(binary(), atom(), [ejabberd_commands()]) -> ok.
unregister_commands(Host, Definer, Commands) ->
@ -203,20 +225,25 @@ unregister_commands(Host, Definer, Commands) ->
ok
end.
-spec list_commands() -> [{atom(), [aterm()], string()}].
list_commands() ->
list_commands(?DEFAULT_VERSION).
-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
[ {Name, Args, Desc} || #ejabberd_commands{
name = Name,
args = Args,
tags = Tags,
desc = Desc} <- Commands,
not lists:member(internal, Tags)].
desc = Desc
} <- Commands,
not lists:member(internal, Tags) ].
-spec list_commands(integer(), map()) -> [{atom(), [aterm()], string()}].
@ -225,25 +252,30 @@ list_commands(Version, CallerInfo) ->
fun({Name, _Args, _Desc}) ->
allow == ejabberd_access_permissions:can_access(Name, CallerInfo)
end,
list_commands(Version)
).
list_commands(Version)).
-spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}.
-spec get_command_format(atom()) -> {[aterm()], [{atom(), atom()}], rterm()}.
get_command_format(Name) ->
get_command_format(Name, noauth, ?DEFAULT_VERSION).
get_command_format(Name, Version) when is_integer(Version) ->
get_command_format(Name, noauth, Version);
get_command_format(Name, Auth) ->
get_command_format(Name, Auth, ?DEFAULT_VERSION).
-spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(),atom()}], rterm()}.
-spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(), atom()}], rterm()}.
get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args,
#ejabberd_commands{
args = Args,
result = Result,
args_rename = Rename,
policy = Policy} =
policy = Policy
} =
get_command_definition(Name, Version),
case Policy of
user when Admin;
@ -253,11 +285,13 @@ get_command_format(Name, Auth, Version) ->
{Args, Rename, Result}
end.
-spec get_command_definition(atom()) -> ejabberd_commands().
get_command_definition(Name) ->
get_command_definition(Name, ?DEFAULT_VERSION).
-spec get_command_definition(atom(), integer()) -> ejabberd_commands().
get_command_definition(Name, Version) ->
@ -270,13 +304,15 @@ get_command_definition(Name, Version) ->
when N == Name, V =< Version ->
{V, C}
end)))) of
[{_, Command} | _ ] -> Command;
[{_, Command} | _] -> Command;
_E -> throw({error, unknown_command})
end.
get_commands_definition() ->
get_commands_definition(?DEFAULT_VERSION).
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
get_commands_definition(Version) ->
@ -291,20 +327,22 @@ get_commands_definition(Version) ->
end)))),
F = fun({_Name, _V, Command}, []) ->
[Command];
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
({Name, _V, _Command}, [#ejabberd_commands{name = Name} | _T] = Acc) ->
Acc;
({_Name, _V, Command}, Acc) -> [Command | Acc]
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),
FrontedCalledInternal =
maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin
andalso lists:member(internal, Command#ejabberd_commands.tags),
maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin andalso
lists:member(internal, Command#ejabberd_commands.tags),
case {ejabberd_access_permissions:can_access(Name, CallerInfo),
FrontedCalledInternal} of
{allow, false} ->
@ -323,18 +361,19 @@ do_execute_command(Command, Arguments) ->
ejabberd_hooks:run(api_call, [Module, Function, Arguments]),
apply(Module, Function, Arguments).
-spec get_tags_commands() -> [{string(), [string()]}].
get_tags_commands() ->
get_tags_commands(?DEFAULT_VERSION).
-spec get_tags_commands(integer()) -> [{string(), [string()]}].
get_tags_commands(Version) ->
CommandTags = [{Name, Tags} ||
#ejabberd_commands{name = Name, tags = Tags}
<- get_commands_definition(Version),
not lists:member(internal, Tags)],
CommandTags = [ {Name, Tags}
|| #ejabberd_commands{name = Name, tags = Tags} <- get_commands_definition(Version),
not lists:member(internal, Tags) ],
Dict = lists:foldl(
fun({CommandNameAtom, CTags}, D) ->
CommandName = atom_to_list(CommandNameAtom),
@ -355,6 +394,7 @@ get_tags_commands(Version) ->
CommandTags),
orddict:to_list(Dict).
%% -----------------------------
%% Access verification
%% -----------------------------

View file

@ -33,7 +33,7 @@
-include("ejabberd_commands.hrl").
-define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end).
-define(TAG_BIN(N), (atom_to_binary(N, latin1))/binary).
-define(TAG_BIN(N), (atom_to_binary(N, latin1)) / binary).
-define(TAG_STR(N), atom_to_list(N)).
-define(TAG(N), if HTMLOutput -> [<<"<", ?TAG_BIN(N), "/>">>]; true -> md_tag(N, <<"">>) end).
-define(TAG(N, V), if HTMLOutput -> [<<"<", ?TAG_BIN(N), ">">>, V, <<"</", ?TAG_BIN(N), ">">>]; true -> md_tag(N, V) end).
@ -42,13 +42,13 @@
-define(TAG_R(N, C, V), ?TAG(N, C, ?RAW(V))).
-define(SPAN(N, V), ?TAG_R(span, ??N, V)).
-define(STR(A), ?SPAN(str,[<<"\"">>, A, <<"\"">>])).
-define(NUM(A), ?SPAN(num,integer_to_binary(A))).
-define(FIELD(A), ?SPAN(field,A)).
-define(ID(A), ?SPAN(id,A)).
-define(OP(A), ?SPAN(op,A)).
-define(STR(A), ?SPAN(str, [<<"\"">>, A, <<"\"">>])).
-define(NUM(A), ?SPAN(num, integer_to_binary(A))).
-define(FIELD(A), ?SPAN(field, A)).
-define(ID(A), ?SPAN(id, A)).
-define(OP(A), ?SPAN(op, A)).
-define(ARG(A), ?FIELD(atom_to_list(A))).
-define(KW(A), ?SPAN(kw,A)).
-define(KW(A), ?SPAN(kw, A)).
-define(BR, <<"\n">>).
-define(ARG_S(A), ?STR(atom_to_list(A))).
@ -63,12 +63,16 @@
-define(STR_A(A), ?STR(atom_to_list(A))).
-define(ID_A(A), ?ID(atom_to_list(A))).
list_join_with([], _M) ->
[];
list_join_with([El|Tail], M) ->
list_join_with([El | Tail], M) ->
lists:reverse(lists:foldl(fun(E, Acc) ->
[E, M | Acc]
end, [El], Tail)).
end,
[El],
Tail)).
md_tag(dt, V) ->
[<<"- ">>, V];
@ -91,6 +95,7 @@ md_tag('div', V) ->
md_tag(_, V) ->
V.
perl_gen({Name, integer}, Int, _Indent, HTMLOutput) ->
[?ARG(Name), ?OP_L(" => "), ?NUM(Int)];
perl_gen({Name, string}, Str, _Indent, HTMLOutput) ->
@ -100,27 +105,55 @@ perl_gen({Name, binary}, Str, _Indent, HTMLOutput) ->
perl_gen({Name, atom}, Atom, _Indent, HTMLOutput) ->
[?ARG(Name), ?OP_L(" => "), ?STR_A(Atom)];
perl_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) ->
Res = lists:map(fun({A,B})->perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))),
Res = lists:map(fun({A, B}) -> perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))),
[?ARG(Name), ?OP_L(" => {"), list_join_with(Res, [?OP_L(", ")]), ?OP_L("}")];
perl_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) ->
Res = lists:map(fun(E) -> [?OP_L("{"), perl_gen(ElDesc, E, Indent, HTMLOutput), ?OP_L("}")] end, List),
[?ARG(Name), ?OP_L(" => ["), list_join_with(Res, [?OP_L(", ")]), ?OP_L("]")].
perl_call(Name, ArgsDesc, Values, HTMLOutput) ->
{Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ perl\n">>} end,
[Preamble,
Indent, ?ID_L("XMLRPC::Lite"), ?OP_L("->"), ?ID_L("proxy"), ?OP_L("("), ?ID_L("$url"), ?OP_L(")->"),
?ID_L("call"), ?OP_L("("), ?STR_A(Name), ?OP_L(", {"), ?BR, Indent, <<" ">>,
list_join_with(lists:map(fun({A,B})->perl_gen(A, B, <<Indent/binary, " ">>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]),
?BR, Indent, ?OP_L("})->"), ?ID_L("results"), ?OP_L("()")].
Indent,
?ID_L("XMLRPC::Lite"),
?OP_L("->"),
?ID_L("proxy"),
?OP_L("("),
?ID_L("$url"),
?OP_L(")->"),
?ID_L("call"),
?OP_L("("),
?STR_A(Name),
?OP_L(", {"),
?BR,
Indent,
<<" ">>,
list_join_with(lists:map(fun({A, B}) -> perl_gen(A, B, <<Indent/binary, " ">>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]),
?BR,
Indent,
?OP_L("})->"),
?ID_L("results"),
?OP_L("()")].
java_gen_map(Vals, Indent, HTMLOutput) ->
{Split, NL} = case Indent of
none -> {<<" ">>, <<" ">>};
_ -> {[?BR, <<" ", Indent/binary>>], [?BR, Indent]}
end,
[?KW_L("new "), ?ID_L("HashMap"), ?OP_L("<"), ?ID_L("String"), ?OP_L(", "), ?ID_L("Object"),
?OP_L(">() {{"), Split, list_join_with(Vals, Split), NL, ?OP_L("}}")].
[?KW_L("new "),
?ID_L("HashMap"),
?OP_L("<"),
?ID_L("String"),
?OP_L(", "),
?ID_L("Object"),
?OP_L(">() {{"),
Split,
list_join_with(Vals, Split),
NL,
?OP_L("}}")].
java_gen({Name, integer}, Int, _Indent, HTMLOutput) ->
[?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Integer"), ?OP_L("("), ?NUM(Int), ?OP_L("));")];
@ -137,25 +170,73 @@ java_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) ->
java_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) ->
{NI, NI2, I} = case List of
[_] -> {" ", " ", Indent};
_ -> {[?BR, <<" ", Indent/binary>>],
_ ->
{[?BR, <<" ", Indent/binary>>],
[?BR, <<" ", Indent/binary>>],
<<" ", Indent/binary>>}
end,
Res = lists:map(fun(E) -> java_gen_map([java_gen(ElDesc, E, I, HTMLOutput)], none, HTMLOutput) end, List),
[?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Object"), ?OP_L("[] {"), NI,
list_join_with(Res, [?OP_L(","), NI]), NI2, ?OP_L("});")].
[?ID_L("put"),
?OP_L("("),
?STR_A(Name),
?OP_L(", "),
?KW_L("new "),
?ID_L("Object"),
?OP_L("[] {"),
NI,
list_join_with(Res, [?OP_L(","), NI]),
NI2,
?OP_L("});")].
java_call(Name, ArgsDesc, Values, HTMLOutput) ->
{Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ java\n">>} end,
[Preamble,
Indent, ?ID_L("XmlRpcClientConfigImpl config"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClientConfigImpl"), ?OP_L("();"), ?BR,
Indent, ?ID_L("config"), ?OP_L("."), ?ID_L("setServerURL"), ?OP_L("("), ?ID_L("url"), ?OP_L(");"), ?BR, Indent, ?BR,
Indent, ?ID_L("XmlRpcClient client"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClient"), ?OP_L("();"), ?BR,
Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("setConfig"), ?OP_L("("), ?ID_L("config"), ?OP_L(");"), ?BR, Indent, ?BR,
Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("execute"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "),
java_gen_map(lists:map(fun({A,B})->java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput),
Indent,
?ID_L("XmlRpcClientConfigImpl config"),
?OP_L(" = "),
?KW_L("new "),
?ID_L("XmlRpcClientConfigImpl"),
?OP_L("();"),
?BR,
Indent,
?ID_L("config"),
?OP_L("."),
?ID_L("setServerURL"),
?OP_L("("),
?ID_L("url"),
?OP_L(");"),
?BR,
Indent,
?BR,
Indent,
?ID_L("XmlRpcClient client"),
?OP_L(" = "),
?KW_L("new "),
?ID_L("XmlRpcClient"),
?OP_L("();"),
?BR,
Indent,
?ID_L("client"),
?OP_L("."),
?ID_L("setConfig"),
?OP_L("("),
?ID_L("config"),
?OP_L(");"),
?BR,
Indent,
?BR,
Indent,
?ID_L("client"),
?OP_L("."),
?ID_L("execute"),
?OP_L("("),
?STR_A(Name),
?OP_L(", "),
java_gen_map(lists:map(fun({A, B}) -> java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput),
?OP_L(");")].
-define(XML_S(N, V), ?OP_L("<"), ?FIELD_L(??N), ?OP_L(">"), V).
-define(XML_E(N), ?OP_L("</"), ?FIELD_L(??N), ?OP_L(">")).
-define(XML(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?BR, Indent, ?XML_E(N)).
@ -163,51 +244,75 @@ java_call(Name, ArgsDesc, Values, HTMLOutput) ->
-define(XML_L(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?XML_E(N)).
-define(XML_L(N, Indent, D, V), ?XML_L(N, [Indent, lists:duplicate(D, <<" ">>)], V)).
xml_gen({Name, integer}, Int, Indent, HTMLOutput) ->
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1,
?XML(value,
Indent,
1,
[?XML_L(integer, Indent, 2, ?ID(integer_to_binary(Int)))])])];
xml_gen({Name, string}, Str, Indent, HTMLOutput) ->
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1,
?XML(value,
Indent,
1,
[?XML_L(string, Indent, 2, ?ID(Str))])])];
xml_gen({Name, binary}, Str, Indent, HTMLOutput) ->
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1,
?XML(value,
Indent,
1,
[?XML_L(string, Indent, 2, ?ID(Str))])])];
xml_gen({Name, atom}, Atom, Indent, HTMLOutput) ->
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1,
?XML(value,
Indent,
1,
[?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])];
xml_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) ->
NewIndent = <<" ", Indent/binary>>,
Res = lists:map(fun({A, B}) -> xml_gen(A, B, NewIndent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))),
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])];
xml_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) ->
Ind1 = <<" ", Indent/binary>>,
Ind2 = <<" ", Ind1/binary>>,
Res = lists:map(fun(E) -> [?XML(value, Ind1, [?XML(struct, Ind1, 1, xml_gen(ElDesc, E, Ind2, HTMLOutput))])] end, List),
[?XML(member, Indent,
[?XML(member,
Indent,
[?XML_L(name, Indent, 1, ?ID_A(Name)),
?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])].
xml_call(Name, ArgsDesc, Values, HTMLOutput) ->
{Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ xml">>} end,
Res = lists:map(fun({A, B}) -> xml_gen(A, B, <<Indent/binary, " ">>, HTMLOutput) end, lists:zip(ArgsDesc, Values)),
[Preamble,
?XML(methodCall, Indent,
?XML(methodCall,
Indent,
[?XML_L(methodName, Indent, 1, ?ID_A(Name)),
?XML(params, Indent, 1,
[?XML(param, Indent, 2,
[?XML(value, Indent, 3,
?XML(params,
Indent,
1,
[?XML(param,
Indent,
2,
[?XML(value,
Indent,
3,
[?XML(struct, Indent, 4, Res)])])])])].
% [?ARG_S(Name), ?OP_L(": "), ?STR(Str)];
json_gen({_Name, integer}, Int, _Indent, HTMLOutput) ->
[?NUM(Int)];
@ -220,15 +325,22 @@ json_gen({_Name, atom}, Atom, _Indent, HTMLOutput) ->
json_gen({_Name, rescode}, Val, _Indent, HTMLOutput) ->
[?ID_A(Val == ok orelse Val == true)];
json_gen({_Name, restuple}, {Val, Str}, _Indent, HTMLOutput) ->
[?OP_L("{"), ?STR_L("res"), ?OP_L(": "), ?ID_A(Val == ok orelse Val == true), ?OP_L(", "),
?STR_L("text"), ?OP_L(": "), ?STR(Str), ?OP_L("}")];
[?OP_L("{"),
?STR_L("res"),
?OP_L(": "),
?ID_A(Val == ok orelse Val == true),
?OP_L(", "),
?STR_L("text"),
?OP_L(": "),
?STR(Str),
?OP_L("}")];
json_gen({_Name, {list, {_, {tuple, [{_, atom}, ValFmt]}}}}, List, Indent, HTMLOutput) ->
Indent2 = <<" ", Indent/binary>>,
Res = lists:map(fun({N, V})->[?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List),
Res = lists:map(fun({N, V}) -> [?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List),
[?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")];
json_gen({_Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) ->
Indent2 = <<" ", Indent/binary>>,
Res = lists:map(fun({{N, _} = A, B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end,
Res = lists:map(fun({{N, _} = A, B}) -> [?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end,
lists:zip(Fields, tuple_to_list(Tuple))),
[?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")];
json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) ->
@ -236,6 +348,7 @@ json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) ->
Res = lists:map(fun(E) -> json_gen(ElDesc, E, Indent2, HTMLOutput) end, List),
[?OP_L("["), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("]")].
json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) ->
{Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<"">>, <<"~~~ json\n">>} end,
{Code, ResultStr} = case {ResultDesc, Result} of
@ -255,23 +368,40 @@ json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) ->
500 -> <<" 500 Internal Server Error">>
end,
[Preamble,
Indent, ?ID_L("POST /api/"), ?ID_A(Name), ?BR,
Indent, ?OP_L("{"), ?BR, Indent, <<" ">>,
list_join_with(lists:map(fun({{N,_}=A,B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, <<Indent/binary, " ">>, HTMLOutput)] end,
lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]),
?BR, Indent, ?OP_L("}"), ?BR, Indent, ?BR, Indent,
?ID_L("HTTP/1.1"), ?ID(CodeStr), ?BR, Indent,
ResultStr
].
Indent,
?ID_L("POST /api/"),
?ID_A(Name),
?BR,
Indent,
?OP_L("{"),
?BR,
Indent,
<<" ">>,
list_join_with(lists:map(fun({{N, _} = A, B}) -> [?STR_A(N), ?OP_L(": "), json_gen(A, B, <<Indent/binary, " ">>, HTMLOutput)] end,
lists:zip(ArgsDesc, Values)),
[?OP_L(","), ?BR, Indent, <<" ">>]),
?BR,
Indent,
?OP_L("}"),
?BR,
Indent,
?BR,
Indent,
?ID_L("HTTP/1.1"),
?ID(CodeStr),
?BR,
Indent,
ResultStr].
generate_example_input({_Name, integer}, {LastStr, LastNum}) ->
{LastNum+1, {LastStr, LastNum+1}};
{LastNum + 1, {LastStr, LastNum + 1}};
generate_example_input({_Name, string}, {LastStr, LastNum}) ->
{string:chars(LastStr+1, 5), {LastStr+1, LastNum}};
{string:chars(LastStr + 1, 5), {LastStr + 1, LastNum}};
generate_example_input({_Name, binary}, {LastStr, LastNum}) ->
{iolist_to_binary(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}};
{iolist_to_binary(string:chars(LastStr + 1, 5)), {LastStr + 1, LastNum}};
generate_example_input({_Name, atom}, {LastStr, LastNum}) ->
{list_to_atom(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}};
{list_to_atom(string:chars(LastStr + 1, 5)), {LastStr + 1, LastNum}};
generate_example_input({_Name, rescode}, {LastStr, LastNum}) ->
{ok, {LastStr, LastNum}};
generate_example_input({_Name, restuple}, {LastStr, LastNum}) ->
@ -280,36 +410,50 @@ generate_example_input({_Name, {tuple, Fields}}, Data) ->
{R, D} = lists:foldl(fun(Field, {Res2, Data2}) ->
{Res3, Data3} = generate_example_input(Field, Data2),
{[Res3 | Res2], Data3}
end, {[], Data}, Fields),
end,
{[], Data},
Fields),
{list_to_tuple(lists:reverse(R)), D};
generate_example_input({_Name, {list, Desc}}, Data) ->
{R1, D1} = generate_example_input(Desc, Data),
{R2, D2} = generate_example_input(Desc, D1),
{[R1, R2], D2}.
gen_calls(#ejabberd_commands{args_example=none, args=ArgsDesc} = C, HTMLOutput, Langs) ->
gen_calls(#ejabberd_commands{args_example = none, args = ArgsDesc} = C, HTMLOutput, Langs) ->
{R, _} = lists:foldl(fun(Arg, {Res, Data}) ->
{Res3, Data3} = generate_example_input(Arg, Data),
{[Res3 | Res], Data3}
end, {[], {$a-1, 0}}, ArgsDesc),
gen_calls(C#ejabberd_commands{args_example=lists:reverse(R)}, HTMLOutput, Langs);
gen_calls(#ejabberd_commands{result_example=none, result=ResultDesc} = C, HTMLOutput, Langs) ->
{R, _} = generate_example_input(ResultDesc, {$a-1, 0}),
gen_calls(C#ejabberd_commands{result_example=R}, HTMLOutput, Langs);
gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc,
result_example=Result, result=ResultDesc,
name=Name}, HTMLOutput, Langs) ->
end,
{[], {$a - 1, 0}},
ArgsDesc),
gen_calls(C#ejabberd_commands{args_example = lists:reverse(R)}, HTMLOutput, Langs);
gen_calls(#ejabberd_commands{result_example = none, result = ResultDesc} = C, HTMLOutput, Langs) ->
{R, _} = generate_example_input(ResultDesc, {$a - 1, 0}),
gen_calls(C#ejabberd_commands{result_example = R}, HTMLOutput, Langs);
gen_calls(#ejabberd_commands{
args_example = Values,
args = ArgsDesc,
result_example = Result,
result = ResultDesc,
name = Name
},
HTMLOutput,
Langs) ->
Perl = perl_call(Name, ArgsDesc, Values, HTMLOutput),
Java = java_call(Name, ArgsDesc, Values, HTMLOutput),
XML = xml_call(Name, ArgsDesc, Values, HTMLOutput),
JSON = json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput),
if HTMLOutput ->
[?TAG(ul, "code-samples-names",
if
HTMLOutput ->
[?TAG(ul,
"code-samples-names",
[case lists:member(<<"java">>, Langs) of true -> ?TAG(li, <<"Java">>); _ -> [] end,
case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, <<"Perl">>); _ -> [] end,
case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, <<"XML">>); _ -> [] end,
case lists:member(<<"json">>, Langs) of true -> ?TAG(li, <<"JSON">>); _ -> [] end]),
?TAG(ul, "code-samples",
?TAG(ul,
"code-samples",
[case lists:member(<<"java">>, Langs) of true -> ?TAG(li, ?TAG(pre, Java)); _ -> [] end,
case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, ?TAG(pre, Perl)); _ -> [] end,
case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end,
@ -323,7 +467,8 @@ gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc,
case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end,
<<"\n\n">>];
_ ->
[<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end,
[<<"\n">>,
case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end,
case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end,
case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end,
case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end,
@ -336,12 +481,13 @@ gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc,
end
end.
format_type({list, {_, {tuple, Els}}}) ->
io_lib:format("[~ts]", [format_type({tuple, Els})]);
format_type({list, El}) ->
io_lib:format("[~ts]", [format_type(El)]);
format_type({tuple, Els}) ->
Args = [format_type(El) || El <- Els],
Args = [ format_type(El) || El <- Els ],
io_lib:format("{~ts}", [string:join(Args, ", ")]);
format_type({Name, Type}) ->
io_lib:format("~ts::~ts", [Name, format_type(Type)]);
@ -352,39 +498,64 @@ format_type(atom) ->
format_type(Type) ->
io_lib:format("~p", [Type]).
gen_param(Name, Type, undefined, HTMLOutput) ->
[?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))])];
gen_param(Name, Type, Desc, HTMLOutput) ->
[?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]),
?TAG(dd, ?RAW(Desc))].
make_tags(HTMLOutput) ->
TagsList = ejabberd_commands:get_tags_commands(1000000),
lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList).
-dialyzer({no_match, gen_tags/2}).
gen_tags({TagName, Commands}, HTMLOutput) ->
[?TAG(h1, TagName) | [?TAG(p, ?RAW("* _`"++C++"`_")) || C <- Commands]].
gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
args=Args, args_desc=ArgsDesc, note=Note, definer=Definer,
result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) ->
-dialyzer({no_match, gen_tags/2}).
gen_tags({TagName, Commands}, HTMLOutput) ->
[?TAG(h1, TagName) | [ ?TAG(p, ?RAW("* _`" ++ C ++ "`_")) || C <- Commands ]].
gen_doc(#ejabberd_commands{
name = Name,
tags = Tags,
desc = Desc,
longdesc = LongDesc,
args = Args,
args_desc = ArgsDesc,
note = Note,
definer = Definer,
result = Result,
result_desc = ResultDesc
} = Cmd,
HTMLOutput,
Langs) ->
try
ArgsText = case ArgsDesc of
none ->
[?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput)
|| {AName, Type} <- Args])];
[?TAG(ul,
"args-list",
[ gen_param(AName, Type, undefined, HTMLOutput)
|| {AName, Type} <- Args ])];
_ ->
[?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput)
|| {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])]
[?TAG(dl,
"args-list",
[ gen_param(AName, Type, ADesc, HTMLOutput)
|| {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc) ])]
end,
ResultText = case Result of
{res,rescode} ->
[?TAG(dl, [gen_param(res, integer,
{res, rescode} ->
[?TAG(dl,
[gen_param(res,
integer,
"Status code (`0` on success, `1` otherwise)",
HTMLOutput)])];
{res,restuple} ->
[?TAG(dl, [gen_param(res, string,
{res, restuple} ->
[?TAG(dl,
[gen_param(res,
string,
"Raw result string",
HTMLOutput)])];
{RName, Type} ->
@ -395,14 +566,14 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
[?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])]
end
end,
TagsText = ?RAW(string:join(["_`"++atom_to_list(Tag)++"`_" || Tag <- Tags], ", ")),
TagsText = ?RAW(string:join([ "_`" ++ atom_to_list(Tag) ++ "`_" || Tag <- Tags ], ", ")),
IsDefinerMod = case Definer of
unknown -> false;
_ -> lists:member(gen_mod, lists:flatten(proplists:get_all_values(behaviour, Definer:module_info(attributes))))
end,
ModuleText = case IsDefinerMod of
true ->
[?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`"++atom_to_list(Definer)++"`_"))];
[?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`" ++ atom_to_list(Definer) ++ "`_"))];
false ->
[]
end,
@ -411,7 +582,8 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
_ -> ?TAG('div', "note-down", ?RAW(Note))
end,
{NotePre, NotePost} =
if HTMLOutput -> {[], NoteEl};
if
HTMLOutput -> {[], NoteEl};
true -> {NoteEl, []}
end,
@ -423,11 +595,13 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
_ -> ?TAG(p, ?RAW(LongDesc))
end,
NotePost,
?TAG(h2, <<"Arguments:">>), ArgsText,
?TAG(h2, <<"Result:">>), ResultText,
?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)]
++ ModuleText ++ [
?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)]
?TAG(h2, <<"Arguments:">>),
ArgsText,
?TAG(h2, <<"Result:">>),
ResultText,
?TAG(h2, <<"Tags:">>),
?TAG(p, TagsText)] ++
ModuleText ++ [?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)]
catch
_:Ex ->
throw(iolist_to_binary(io_lib:format(
@ -435,6 +609,7 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
[Name, Ex])))
end.
get_version_mark("") ->
"";
get_version_mark(Note) ->
@ -445,9 +620,11 @@ get_version_mark(Note) ->
_ -> " 🟤"
end.
make_command_name(Name, Note) ->
atom_to_list(Name) ++ get_version_mark(Note).
find_commands_definitions() ->
lists:flatmap(
fun(Mod) ->
@ -458,20 +635,24 @@ find_commands_definitions() ->
_ ->
[]
end,
[C#ejabberd_commands{definer = Mod} || C <- Cs]
end, ejabberd_config:beams(all)).
[ C#ejabberd_commands{definer = Mod} || C <- Cs ]
end,
ejabberd_config:beams(all)).
generate_html_output(File, RegExp, Languages) ->
Cmds = find_commands_definitions(),
{ok, RE} = re:compile(RegExp),
Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) ->
Cmds2 = lists:filter(fun(#ejabberd_commands{name = Name, module = Module}) ->
re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse
re:run(atom_to_list(Module), RE, [{capture, none}]) == match
end, Cmds),
Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) ->
end,
Cmds),
Cmds3 = lists:sort(fun(#ejabberd_commands{name = N1}, #ejabberd_commands{name = N2}) ->
N1 =< N2
end, Cmds2),
Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3],
end,
Cmds2),
Cmds4 = [ maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3 ],
Langs = binary:split(Languages, <<",">>, [global]),
Out = lists:map(fun(C) -> gen_doc(C, true, Langs) end, Cmds4),
{ok, Fh} = file:open(File, [write]),
@ -479,35 +660,43 @@ generate_html_output(File, RegExp, Languages) ->
file:close(Fh),
ok.
maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) ->
maybe_add_policy_arguments(#ejabberd_commands{args = Args1, policy = user} = Cmd) ->
Args2 = [{user, binary}, {host, binary} | Args1],
Cmd#ejabberd_commands{args = Args2};
maybe_add_policy_arguments(Cmd) ->
Cmd.
generate_md_output(File, <<"runtime">>, Languages) ->
Cmds = lists:map(fun({N, _, _}) ->
ejabberd_commands:get_command_definition(N)
end, ejabberd_commands:list_commands()),
end,
ejabberd_commands:list_commands()),
generate_md_output(File, <<".">>, Languages, Cmds);
generate_md_output(File, RegExp, Languages) ->
Cmds = find_commands_definitions(),
generate_md_output(File, RegExp, Languages, Cmds).
generate_md_output(File, RegExp, Languages, Cmds) ->
{ok, RE} = re:compile(RegExp),
Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) ->
Cmds2 = lists:filter(fun(#ejabberd_commands{name = Name, module = Module}) ->
re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse
re:run(atom_to_list(Module), RE, [{capture, none}]) == match
end, Cmds),
Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) ->
end,
Cmds),
Cmds3 = lists:sort(fun(#ejabberd_commands{name = N1}, #ejabberd_commands{name = N2}) ->
N1 =< N2
end, Cmds2),
Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3],
end,
Cmds2),
Cmds4 = [ maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3 ],
Langs = binary:split(Languages, <<",">>, [global]),
Version = binary_to_list(ejabberd_config:version()),
Header = ["# API Reference\n\n"
"This section describes API commands of ejabberd ", Version, ". "
"This section describes API commands of ejabberd ",
Version,
". "
"The commands that changed in this version are marked with 🟤.\n\n"],
Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4),
{ok, Fh} = file:open(File, [write, {encoding, utf8}]),
@ -515,16 +704,20 @@ generate_md_output(File, RegExp, Languages, Cmds) ->
file:close(Fh),
ok.
generate_tags_md(File) ->
Version = binary_to_list(ejabberd_config:version()),
Header = ["# API Tags\n\n"
"This section enumerates the API tags of ejabberd ", Version, ". \n\n"],
"This section enumerates the API tags of ejabberd ",
Version,
". \n\n"],
Tags = make_tags(false),
{ok, Fh} = file:open(File, [write, {encoding, utf8}]),
io:format(Fh, "~ts~ts", [Header, Tags]),
file:close(Fh),
ok.
html_pre() ->
"<!DOCTYPE>
<html>
@ -650,8 +843,9 @@ html_pre() ->
}
</script>".
html_post() ->
"<script>
"<script>
var ul = document.getElementsByTagName('ul');
for (var i = 0; i < ul.length; i++) {
if (ul[i].className == 'code-samples-names')

View file

@ -63,6 +63,7 @@
{error, error_reason()}.
-type host_config() :: #{{atom(), binary() | global} => term()}.
-callback opt_type(atom()) -> econf:validator().
-callback options() -> [atom() | {atom(), term()}].
-callback globals() -> [atom()].
@ -74,6 +75,7 @@
-dialyzer([no_opaque_union]).
-endif.
%%%===================================================================
%%% API
%%%===================================================================
@ -81,6 +83,7 @@
load() ->
load(path()).
-spec load(file:filename_all()) -> ok | error_return().
load(Path) ->
ConfigFile = unicode:characters_to_binary(Path),
@ -97,6 +100,7 @@ load(Path) ->
Err
end.
-spec reload() -> ok | error_return().
reload() ->
ejabberd_systemd:reloading(),
@ -111,11 +115,13 @@ reload() ->
lists:foreach(
fun(Host) ->
ejabberd_hooks:run(host_up, [Host])
end, AddHosts),
end,
AddHosts),
lists:foreach(
fun(Host) ->
ejabberd_hooks:run(host_down, [Host])
end, DelHosts),
end,
DelHosts),
ejabberd_hooks:run(config_reloaded, []),
% logger is started too early to be able to use hooks, so
% we need to call it separately
@ -130,15 +136,18 @@ reload() ->
ejabberd_systemd:ready(),
Res.
-spec dump() -> ok | error_return().
dump() ->
dump(stdout).
-spec dump(stdout | file:filename_all()) -> ok | error_return().
dump(Output) ->
Y = get_option(yaml_config),
dump(Y, Output).
-spec dump(term(), stdout | file:filename_all()) -> ok | error_return().
dump(Y, Output) ->
Data = fast_yaml:encode(Y),
@ -149,17 +158,22 @@ dump(Y, Output) ->
try
ok = filelib:ensure_dir(FileName),
ok = file:write_file(FileName, Data)
catch _:{badmatch, {error, Reason}} ->
catch
_:{badmatch, {error, Reason}} ->
{error, {write_file, FileName, Reason}}
end
end.
-spec get_option(option(), term()) -> term().
get_option(Opt, Default) ->
try get_option(Opt)
catch _:badarg -> Default
try
get_option(Opt)
catch
_:badarg -> Default
end.
-spec get_option(option()) -> term().
get_option(Opt) when is_atom(Opt) ->
get_option({Opt, global});
@ -168,8 +182,10 @@ get_option({O, Host} = Opt) ->
undefined -> ejabberd_options;
T -> T
end,
try ets:lookup_element(Tab, Opt, 2)
catch ?EX_RULE(error, badarg, St) when Host /= global ->
try
ets:lookup_element(Tab, Opt, 2)
catch
?EX_RULE(error, badarg, St) when Host /= global ->
StackTrace = ?EX_STACK(St),
Val = get_option({O, global}),
?DEBUG("Option '~ts' is not defined for virtual host '~ts'. "
@ -179,6 +195,7 @@ get_option({O, Host} = Opt) ->
Val
end.
-spec set_option(option(), term()) -> ok.
set_option(Opt, Val) when is_atom(Opt) ->
set_option({Opt, global}, Val);
@ -190,46 +207,56 @@ set_option(Opt, Val) ->
ets:insert(Tab, {Opt, Val}),
ok.
-spec get_version() -> binary().
get_version() ->
get_option(version).
-spec get_myhosts() -> [binary(), ...].
get_myhosts() ->
get_option(hosts).
-spec get_myname() -> binary().
get_myname() ->
get_option(host).
-spec get_mylang() -> binary().
get_mylang() ->
get_lang(global).
-spec get_lang(global | binary()) -> binary().
get_lang(Host) ->
get_option({language, Host}).
-spec get_uri() -> binary().
get_uri() ->
<<"https://www.process-one.net/ejabberd/">>.
-spec get_copyright() -> binary().
get_copyright() ->
<<"Copyright (c) ProcessOne">>.
-spec get_shared_key() -> binary().
get_shared_key() ->
get_option(shared_key).
-spec get_node_start() -> integer().
get_node_start() ->
get_option(node_start).
-spec fsm_limit_opts([proplists:property()]) -> [{max_queue, pos_integer()}].
fsm_limit_opts(Opts) ->
case lists:keyfind(max_fsm_queue, 1, Opts) of
{_, I} when is_integer(I), I>0 ->
{_, I} when is_integer(I), I > 0 ->
[{max_queue, I}];
false ->
case get_option(max_fsm_queue) of
@ -238,6 +265,7 @@ fsm_limit_opts(Opts) ->
end
end.
-spec codec_options() -> [xmpp:decode_option()].
codec_options() ->
case get_option(validate_stream) of
@ -245,6 +273,7 @@ codec_options() ->
false -> [ignore_els]
end.
%% Do not use this function in runtime:
%% It's slow and doesn't read 'version' option from the config.
%% Use ejabberd_option:version() instead.
@ -262,22 +291,27 @@ version() ->
end
end.
-spec default_db(binary() | global, module()) -> atom().
default_db(Host, Module) ->
default_db(default_db, db_type, Host, Module, mnesia).
-spec default_db(binary() | global, module(), atom()) -> atom().
default_db(Host, Module, Default) ->
default_db(default_db, db_type, Host, Module, Default).
-spec default_ram_db(binary() | global, module()) -> atom().
default_ram_db(Host, Module) ->
default_db(default_ram_db, ram_db_type, Host, Module, mnesia).
-spec default_ram_db(binary() | global, module(), atom()) -> atom().
default_ram_db(Host, Module, Default) ->
default_db(default_ram_db, ram_db_type, Host, Module, Default).
-spec default_db(default_db | default_ram_db, db_type | ram_db_type, binary() | global, module(), atom()) -> atom().
default_db(Opt, ModOpt, Host, Mod, Default) ->
Type = get_option({Opt, Host}),
@ -292,12 +326,13 @@ default_db(Opt, ModOpt, Host, Mod, Default) ->
Default
end.
-spec beams(local | external | all) -> [module()].
beams(local) ->
{ok, Mods} = application:get_key(ejabberd, modules),
Mods;
beams(external) ->
ExtMods = [Name || {Name, _Details} <- ext_mod:installed()],
ExtMods = [ Name || {Name, _Details} <- ext_mod:installed() ],
lists:foreach(
fun(ExtMod) ->
ExtModPath = ext_mod:module_ebin_dir(ExtMod),
@ -305,11 +340,12 @@ beams(external) ->
true -> ok;
false -> code:add_patha(ExtModPath)
end
end, ExtMods),
end,
ExtMods),
case application:get_env(ejabberd, external_beams) of
{ok, Path0} ->
Paths = case Path0 of
[L|_] = V when is_list(L) -> V;
[L | _] = V when is_list(L) -> V;
L -> [L]
end,
CustMods = lists:foldl(
@ -319,9 +355,11 @@ beams(external) ->
false -> code:add_patha(Path)
end,
Beams = filelib:wildcard(filename:join(Path, "*\.beam")),
CM ++ [list_to_atom(filename:rootname(filename:basename(Beam)))
|| Beam <- Beams]
end, [], Paths),
CM ++ [ list_to_atom(filename:rootname(filename:basename(Beam)))
|| Beam <- Beams ]
end,
[],
Paths),
CustMods ++ ExtMods;
_ ->
ExtMods
@ -329,6 +367,7 @@ beams(external) ->
beams(all) ->
beams(local) ++ beams(external).
-spec may_hide_data(term()) -> term().
may_hide_data(Data) ->
case get_option(hide_sensitive_log_data) of
@ -336,6 +375,7 @@ may_hide_data(Data) ->
true -> "hidden_by_ejabberd"
end.
%% Some Erlang apps expects env parameters to be list and not binary.
%% For example, Mnesia is not able to start if mnesia dir is passed as a binary.
%% However, binary is most common on Elixir, so it is easy to make a setup mistake.
@ -357,6 +397,7 @@ env_binary_to_list(Application, Parameter) ->
Other
end.
%% ejabberd_options calls this function when parsing options inside host_config
-spec validators([atom()]) -> {econf:validators(), [atom()]}.
validators(Disallowed) ->
@ -364,6 +405,7 @@ validators(Disallowed) ->
DefinedKeywords = get_defined_keywords(Host),
validators(Disallowed, DefinedKeywords).
%% validate/1 calls this function when parsing toplevel options
-spec validators([atom()], [any()]) -> {econf:validators(), [atom()]}.
validators(Disallowed, DK) ->
@ -371,17 +413,22 @@ validators(Disallowed, DK) ->
Validators = lists:foldl(
fun(M, Vs) ->
maps:merge(Vs, validators(M, Disallowed, DK))
end, #{}, Modules),
end,
#{},
Modules),
Required = lists:flatmap(
fun(M) ->
[O || O <- M:options(), is_atom(O)]
end, Modules),
[ O || O <- M:options(), is_atom(O) ]
end,
Modules),
{Validators, Required}.
-spec convert_to_yaml(file:filename()) -> ok | error_return().
convert_to_yaml(File) ->
convert_to_yaml(File, stdout).
-spec convert_to_yaml(file:filename(),
stdout | file:filename()) -> ok | error_return().
convert_to_yaml(File, Output) ->
@ -392,6 +439,7 @@ convert_to_yaml(File, Output) ->
Err
end.
-spec format_error(error_return()) -> string().
format_error({error, Reason, Ctx}) ->
econf:format_error(Reason, Ctx);
@ -410,7 +458,8 @@ format_error({error, {old_config, Path, Reason}}) ->
case Reason of
{_, _, _} -> "at line ";
_ -> ""
end, file:format_error(Reason)]));
end,
file:format_error(Reason)]));
format_error({error, {write_file, Path, Reason}}) ->
lists:flatten(
io_lib:format(
@ -427,14 +476,17 @@ format_error({error, {exception, Class, Reason, St}}) ->
"file attached and the following stacktrace included:~n** ~ts",
[misc:format_exception(2, Class, Reason, St)])).
%% @format-begin
replace_keywords(Host, Value) ->
Keywords = get_defined_keywords(Host) ++ get_predefined_keywords(Host),
replace_keywords(Host, Value, Keywords).
replace_keywords(Host, List, Keywords) when is_list(List) ->
[replace_keywords(Host, Element, Keywords) || Element <- List];
[ replace_keywords(Host, Element, Keywords) || Element <- List ];
replace_keywords(Host, Atom, Keywords) when is_atom(Atom) ->
Str = atom_to_list(Atom),
Bin = iolist_to_binary(Str),
@ -451,7 +503,7 @@ replace_keywords(Host, Atom, Keywords) when is_atom(Atom) ->
end
end;
replace_keywords(_Host, Binary, Keywords) when is_binary(Binary) ->
lists:foldl(fun ({Key, Replacement}, V) when is_binary(Replacement) ->
lists:foldl(fun({Key, Replacement}, V) when is_binary(Replacement) ->
misc:expand_keyword(<<"@", Key/binary, "@">>, V, Replacement);
({_, _}, V) ->
V
@ -463,6 +515,7 @@ replace_keywords(Host, {Element1, Element2}, Keywords) ->
replace_keywords(_Host, Value, _DK) ->
Value.
get_defined_keywords(Host) ->
Tab = case get_tmp_config() of
undefined ->
@ -472,6 +525,7 @@ get_defined_keywords(Host) ->
end,
get_defined_keywords(Tab, Host).
get_defined_keywords(Tab, Host) ->
KeysHost =
case ets:lookup(Tab, {define_keyword, Host}) of
@ -498,9 +552,11 @@ get_defined_keywords(Tab, Host) ->
end,
lists:reverse(KeysTemp ++ KeysGlobal ++ KeysHost).
get_defined_keywords_yaml_config(Y) ->
[{erlang:atom_to_binary(KwAtom, latin1), KwValue}
|| {KwAtom, KwValue} <- proplists:get_value(define_keyword, Y, [])].
[ {erlang:atom_to_binary(KwAtom, latin1), KwValue}
|| {KwAtom, KwValue} <- proplists:get_value(define_keyword, Y, []) ].
get_predefined_keywords(Host) ->
HostList =
@ -517,8 +573,8 @@ get_predefined_keywords(Host) ->
LogDirPath =
iolist_to_binary(filename:dirname(
ejabberd_logger:get_log_path())),
HostList
++ [{<<"HOME">>, list_to_binary(Home)},
HostList ++
[{<<"HOME">>, list_to_binary(Home)},
{<<"CONFIG_PATH">>, ConfigDirPath},
{<<"LOG_PATH">>, LogDirPath},
{<<"SEMVER">>, ejabberd_option:version()},
@ -526,6 +582,7 @@ get_predefined_keywords(Host) ->
misc:semver_to_xxyy(
ejabberd_option:version())}].
resolve_host_alias(Host) ->
case lists:member(Host, ejabberd_option:hosts()) of
true ->
@ -534,6 +591,7 @@ resolve_host_alias(Host) ->
resolve_host_alias2(Host)
end.
resolve_host_alias2(Host) ->
Result =
lists:filter(fun({Alias1, _Vhost}) -> is_glob_match(Host, Alias1) end,
@ -548,6 +606,7 @@ resolve_host_alias2(Host) ->
Host
end.
%% Copied from ejabberd-2.0.0/src/acl.erl
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
@ -560,12 +619,14 @@ is_regexp_match(String, RegExp) ->
false
end.
is_glob_match(String, <<"!", Glob/binary>>) ->
not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob));
is_glob_match(String, Glob) ->
is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)).
%% @format-end
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -584,6 +645,7 @@ path() ->
end
end).
-spec get_env_config() -> {ok, string()} | undefined.
get_env_config() ->
%% First case: the filename can be specified with: erl -config "/path/to/ejabberd.yml".
@ -596,16 +658,19 @@ get_env_config() ->
application:get_env(ejabberd, file)
end.
-spec create_tmp_config() -> ok.
create_tmp_config() ->
T = ets:new(options, [private]),
put(ejabberd_options, T),
ok.
-spec get_tmp_config() -> ets:tid() | undefined.
get_tmp_config() ->
get(ejabberd_options).
-spec delete_tmp_config() -> ok.
delete_tmp_config() ->
case get_tmp_config() of
@ -617,6 +682,7 @@ delete_tmp_config() ->
ok
end.
-spec callback_modules(local | external | all) -> [module()].
callback_modules(local) ->
[ejabberd_options];
@ -625,15 +691,17 @@ callback_modules(external) ->
fun(M) ->
case code:ensure_loaded(M) of
{module, _} ->
erlang:function_exported(M, options, 0)
andalso erlang:function_exported(M, opt_type, 1);
erlang:function_exported(M, options, 0) andalso
erlang:function_exported(M, opt_type, 1);
{error, _} ->
false
end
end, beams(external));
end,
beams(external));
callback_modules(all) ->
misc:lists_uniq(callback_modules(local) ++ callback_modules(external)).
-spec validators(module(), [atom()], [any()]) -> econf:validators().
validators(Mod, Disallowed, DK) ->
Keywords = DK ++ get_predefined_keywords(global),
@ -644,8 +712,10 @@ validators(Mod, Disallowed, DK) ->
true -> false;
false ->
Type =
try Mod:opt_type(O)
catch _:_ ->
try
Mod:opt_type(O)
catch
_:_ ->
ejabberd_options:opt_type(O)
end,
TypeProcessed =
@ -656,11 +726,14 @@ validators(Mod, Disallowed, DK) ->
Type),
{true, {O, TypeProcessed}}
end
end, proplists:get_keys(Mod:options()))).
end,
proplists:get_keys(Mod:options()))).
read_file(File) ->
read_file(File, [replace_macros, include_files, include_modules_configs]).
read_file(File, Opts) ->
{Opts1, Opts2} = proplists:split(Opts, [replace_macros, include_files]),
Ret = case filename:extension(File) of
@ -672,8 +745,9 @@ read_file(File, Opts) ->
lists:foreach(
fun(F) ->
?INFO_MSG("Loading third-party configuration from ~ts", [F])
end, Files),
read_yaml_files([File|Files], lists:flatten(Opts1));
end,
Files),
read_yaml_files([File | Files], lists:flatten(Opts1));
_ ->
read_erlang_file(File, lists:flatten(Opts1))
end,
@ -690,6 +764,7 @@ read_file(File, Opts) ->
Err
end.
get_additional_macros() ->
MacroStrings = lists:foldl(fun([$E, $J, $A, $B, $B, $E, $R, $D, $_, $M, $A, $C, $R, $O, $_ | MacroString], Acc) ->
[parse_macro_string(MacroString) | Acc];
@ -700,11 +775,13 @@ get_additional_macros() ->
os:getenv()),
{additional_macros, MacroStrings}.
parse_macro_string(MacroString) ->
[NameString, ValueString] = string:split(MacroString, "="),
{ok, [ValueDecoded]} = fast_yaml:decode(ValueString),
{list_to_atom(NameString), ValueDecoded}.
read_yaml_files(Files, Opts) ->
ParseOpts = [plain_as_atom, get_additional_macros() | lists:flatten(Opts)],
lists:foldl(
@ -715,7 +792,10 @@ read_yaml_files(Files, Opts) ->
end;
(_, Err) ->
Err
end, {ok, []}, Files).
end,
{ok, []},
Files).
read_erlang_file(File, _) ->
case ejabberd_old_config:read_file(File) of
@ -725,6 +805,7 @@ read_erlang_file(File, _) ->
Err
end.
-spec maybe_install_contrib_modules(term()) -> [atom()].
maybe_install_contrib_modules(Options) ->
case {lists:keysearch(allow_contrib_modules, 1, Options),
@ -737,6 +818,7 @@ maybe_install_contrib_modules(Options) ->
[]
end.
-spec validate(term()) -> {ok, [{atom(), term()}]} | error_return().
validate(Y1) ->
case pre_validate(Y1) of
@ -765,17 +847,22 @@ validate(Y1) ->
Err
end.
-spec pre_validate(term()) -> {ok, [{atom(), term()}]} | error_return().
pre_validate(Y1) ->
econf:validate(
econf:and_then(
econf:options(
#{hosts => ejabberd_options:opt_type(hosts),
#{
hosts => ejabberd_options:opt_type(hosts),
loglevel => ejabberd_options:opt_type(loglevel),
version => ejabberd_options:opt_type(version),
'_' => econf:any()},
'_' => econf:any()
},
[{required, [hosts]}]),
fun econf:group_dups/1), Y1).
fun econf:group_dups/1),
Y1).
-spec load_file(binary()) -> ok | error_return().
load_file(File) ->
@ -802,10 +889,12 @@ load_file(File) ->
Err ->
abort(Err)
end
catch ?EX_RULE(Class, Reason, St) ->
catch
?EX_RULE(Class, Reason, St) ->
{error, {exception, Class, Reason, ?EX_STACK(St)}}
end.
-spec commit() -> ok.
commit() ->
T = get_tmp_config(),
@ -813,16 +902,19 @@ commit() ->
ets:insert(ejabberd_options, NewOpts),
delete_tmp_config().
-spec abort(error_return()) -> error_return().
abort(Err) ->
delete_tmp_config(),
try ets:lookup_element(ejabberd_options, {loglevel, global}, 2) of
Level -> set_loglevel(Level)
catch _:badarg ->
catch
_:badarg ->
ok
end,
Err.
-spec set_host_config([{atom(), term()}]) -> {ok, host_config()} | error_return().
set_host_config(Opts) ->
Map1 = lists:foldl(
@ -831,7 +923,9 @@ set_host_config(Opts) ->
maps:put({Opt, global}, Val, M);
(_, M) ->
M
end, #{}, Opts),
end,
#{},
Opts),
HostOpts = proplists:get_value(host_config, Opts, []),
AppendHostOpts = proplists:get_value(append_host_config, Opts, []),
Map2 = lists:foldl(
@ -839,8 +933,12 @@ set_host_config(Opts) ->
lists:foldl(
fun({Opt, Val}, M2) ->
maps:put({Opt, Host}, Val, M2)
end, M1, Opts1)
end, Map1, HostOpts),
end,
M1,
Opts1)
end,
Map1,
HostOpts),
Map3 = lists:foldl(
fun(_, {error, _} = Err) ->
Err;
@ -849,21 +947,28 @@ set_host_config(Opts) ->
fun(_, {error, _} = Err) ->
Err;
({Opt, L1}, M2) when is_list(L1) ->
L2 = try maps:get({Opt, Host}, M2)
catch _:{badkey, _} ->
L2 = try
maps:get({Opt, Host}, M2)
catch
_:{badkey, _} ->
maps:get({Opt, global}, M2, [])
end,
L3 = L2 ++ L1,
maps:put({Opt, Host}, L3, M2);
({Opt, _}, _) ->
{error, {merge_conflict, Opt, Host}}
end, M1, Opts1)
end, Map2, AppendHostOpts),
end,
M1,
Opts1)
end,
Map2,
AppendHostOpts),
case Map3 of
{error, _} -> Map3;
_ -> {ok, Map3}
end.
-spec apply_defaults(ets:tid(), [binary()], host_config()) -> ok.
apply_defaults(Tab, Hosts, Map) ->
Defaults1 = defaults(),
@ -873,9 +978,12 @@ apply_defaults(Tab, Hosts, Map) ->
fun(Host) ->
set_option(host, Host),
apply_defaults(Tab, Host, Map, Defaults2)
end, Hosts).
end,
Hosts).
-spec apply_defaults(ets:tid(), global | binary(),
-spec apply_defaults(ets:tid(),
global | binary(),
host_config(),
[atom() | {atom(), term()}]) -> ok.
apply_defaults(Tab, Host, Map, Defaults) ->
@ -884,13 +992,15 @@ apply_defaults(Tab, Host, Map, Defaults) ->
try maps:get({Opt, Host}, Map) of
Val ->
ets:insert(Tab, {{Opt, Host}, Val})
catch _:{badkey, _} when Host == global ->
catch
_:{badkey, _} when Host == global ->
Default1 = compute_default(Default, Host),
ets:insert(Tab, {{Opt, Host}, Default1});
_:{badkey, _} ->
try maps:get({Opt, global}, Map) of
V -> ets:insert(Tab, {{Opt, Host}, V})
catch _:{badkey, _} ->
catch
_:{badkey, _} ->
Default1 = compute_default(Default, Host),
ets:insert(Tab, {{Opt, Host}, Default1})
end
@ -900,7 +1010,9 @@ apply_defaults(Tab, Host, Map, Defaults) ->
ets:insert(Tab, {{Opt, Host}, Val});
(_) ->
ok
end, Defaults).
end,
Defaults).
-spec defaults() -> [atom() | {atom(), term()}].
defaults() ->
@ -912,10 +1024,15 @@ defaults() ->
(Opt, Acc1) ->
case lists:member(Opt, Acc1) of
true -> Acc1;
false -> [Opt|Acc1]
false -> [Opt | Acc1]
end
end, Acc, Mod:options())
end, ejabberd_options:options(), callback_modules(external)).
end,
Acc,
Mod:options())
end,
ejabberd_options:options(),
callback_modules(external)).
-spec globals() -> [atom()].
globals() ->
@ -926,7 +1043,9 @@ globals() ->
true -> Mod:globals();
false -> []
end
end, callback_modules(all))).
end,
callback_modules(all))).
%% The module validator depends on virtual host, so we have to
%% validate modules in this separate function.
@ -938,20 +1057,25 @@ validate_modules(Hosts) ->
ModOpts = get_option({modules, Host}),
case gen_mod:validate(Host, ModOpts) of
{ok, ModOpts1} ->
{ok, [{{modules, Host}, ModOpts1}|Acc]};
{ok, [{{modules, Host}, ModOpts1} | Acc]};
Err ->
Err
end;
(_, Err) ->
Err
end, {ok, []}, Hosts).
end,
{ok, []},
Hosts).
-spec delete_host_options([binary()]) -> ok.
delete_host_options(Hosts) ->
lists:foreach(
fun(Host) ->
ets:match_delete(ejabberd_options, {{'_', Host}, '_'})
end, Hosts).
end,
Hosts).
-spec compute_default(fun((global | binary()) -> T) | T, global | binary()) -> T.
compute_default(F, Host) when is_function(F, 1) ->
@ -959,11 +1083,13 @@ compute_default(F, Host) when is_function(F, 1) ->
compute_default(Val, _) ->
Val.
-spec set_fqdn() -> ok.
set_fqdn() ->
FQDNs = get_option(fqdn),
xmpp:set_config([{fqdn, FQDNs}]).
-spec set_shared_key() -> ok.
set_shared_key() ->
Key = case erlang:get_cookie() of
@ -974,10 +1100,12 @@ set_shared_key() ->
end,
set_option(shared_key, Key).
-spec set_node_start(integer()) -> ok.
set_node_start(UnixTime) ->
set_option(node_start, UnixTime).
-spec set_loglevel(logger:level()) -> ok.
set_loglevel(Level) ->
ejabberd_logger:set(Level).

View file

@ -23,6 +23,7 @@
-include("logger.hrl").
%%%===================================================================
%%% API
%%%===================================================================
@ -40,6 +41,7 @@ map_reduce(Y) ->
end,
econf:validate(F, Y).
%%%===================================================================
%%% Transformer
%%%===================================================================
@ -48,11 +50,12 @@ transform(Y) ->
{Y2, Acc2} = update(Y1, Acc1),
filter(global, Y2, Acc2).
transform(Host, Y, Acc) ->
filtermapfoldr(
fun({Opt, HostOpts}, Acc1) when (Opt == host_config orelse
Opt == append_host_config)
andalso Host == global ->
Opt == append_host_config) andalso
Host == global ->
case filtermapfoldr(
fun({Host1, Opts}, Acc2) ->
case transform(Host1, Opts, Acc2) of
@ -61,7 +64,9 @@ transform(Host, Y, Acc) ->
{Opts1, Acc3} ->
{{true, {Host1, Opts1}}, Acc3}
end
end, Acc1, HostOpts) of
end,
Acc1,
HostOpts) of
{[], Acc4} ->
{false, Acc4};
{HostOpts1, Acc4} ->
@ -69,7 +74,10 @@ transform(Host, Y, Acc) ->
end;
({Opt, Val}, Acc1) ->
transform(Host, Opt, Val, Acc1)
end, Acc, Y).
end,
Acc,
Y).
transform(Host, modules, ModOpts, Acc) ->
{ModOpts1, Acc2} =
@ -77,14 +85,18 @@ transform(Host, modules, ModOpts, Acc) ->
fun({Mod, Opts}, Acc1) ->
Opts1 = transform_module_options(Opts),
transform_module(Host, Mod, Opts1, Acc1)
end, Acc, ModOpts),
end,
Acc,
ModOpts),
{{true, {modules, ModOpts1}}, Acc2};
transform(global, listen, Listeners, Acc) ->
{Listeners1, Acc2} =
lists:mapfoldr(
fun(Opts, Acc1) ->
transform_listener(Opts, Acc1)
end, Acc, Listeners),
end,
Acc,
Listeners),
{{true, {listen, Listeners1}}, Acc2};
transform(_Host, Opt, CertFile, Acc) when (Opt == domain_certfile) orelse
(Opt == c2s_certfile) orelse
@ -116,7 +128,8 @@ transform(_Host, acme, ACME, Acc) ->
end;
(Opt) ->
Opt
end, ACME),
end,
ACME),
{{true, {acme, ACME1}}, Acc};
transform(Host, s2s_use_starttls, required_trusted, Acc) ->
?WARNING_MSG("The value 'required_trusted' of option "
@ -126,7 +139,7 @@ transform(Host, s2s_use_starttls, required_trusted, Acc) ->
"been automatically removed from the configuration. ~ts",
[adjust_hint()]),
Hosts = maps:get(remove_s2s_dialback, Acc, []),
Acc1 = maps:put(remove_s2s_dialback, [Host|Hosts], Acc),
Acc1 = maps:put(remove_s2s_dialback, [Host | Hosts], Acc),
{{true, {s2s_use_starttls, required}}, Acc1};
transform(Host, define_macro, Macro, Acc) when is_binary(Host) ->
?WARNING_MSG("The option 'define_macro' is not supported inside 'host_config'. "
@ -136,22 +149,27 @@ transform(Host, define_macro, Macro, Acc) when is_binary(Host) ->
transform(_Host, _Opt, _Val, Acc) ->
{true, Acc}.
update(Y, Acc) ->
set_certfiles(Y, Acc).
filter(Host, Y, Acc) ->
lists:filtermap(
fun({Opt, HostOpts}) when (Opt == host_config orelse
Opt == append_host_config)
andalso Host == global ->
Opt == append_host_config) andalso
Host == global ->
HostOpts1 = lists:map(
fun({Host1, Opts1}) ->
{Host1, filter(Host1, Opts1, Acc)}
end, HostOpts),
end,
HostOpts),
{true, {Opt, HostOpts1}};
({Opt, Val}) ->
filter(Host, Opt, Val, Acc)
end, Y).
end,
Y).
filter(_Host, log_rotate_date, _, _) ->
warn_removed_option(log_rotate_date),
@ -204,7 +222,8 @@ filter(_Host, auth_method, Ms, _) ->
fun(internal) -> mnesia;
(odbc) -> sql;
(M) -> M
end, Ms),
end,
Ms),
{true, {auth_method, Ms1}};
filter(_Host, default_ram_db, internal, _) ->
{true, {default_ram_db, mnesia}};
@ -215,7 +234,8 @@ filter(_Host, extauth_cache, _, _) ->
"and has no effect, use authentication "
"or global cache configuration options: "
"auth_use_cache, auth_cache_life_time, "
"use_cache, cache_life_time, and so on", []),
"use_cache, cache_life_time, and so on",
[]),
false;
filter(_Host, extauth_instances, Val, _) ->
warn_replaced_option(extauth_instances, extauth_pool_size),
@ -242,11 +262,13 @@ filter(Host, modules, ModOpts, State) ->
false;
(_) ->
true
end, ModOpts),
end,
ModOpts),
{true, {modules, ModOpts1}};
filter(_, _, _, _) ->
true.
%%%===================================================================
%%% Listener transformers
%%%===================================================================
@ -256,6 +278,7 @@ transform_listener(Opts, Acc) ->
Opts3 = remove_inet_options(Opts2),
collect_listener_certfiles(Opts3, Acc).
transform_request_handlers(Opts) ->
case lists:keyfind(module, 1, Opts) of
{_, ejabberd_http} ->
@ -266,6 +289,7 @@ transform_request_handlers(Opts) ->
Opts
end.
transform_turn_ip(Opts) ->
case lists:keyfind(module, 1, Opts) of
{_, ejabberd_stun} ->
@ -274,6 +298,7 @@ transform_turn_ip(Opts) ->
Opts
end.
replace_request_handlers(Opts) ->
Handlers = proplists:get_value(request_handlers, Opts, []),
Handlers1 =
@ -281,33 +306,36 @@ replace_request_handlers(Opts) ->
fun({captcha, IsEnabled}, Acc) ->
Handler = {<<"/captcha">>, ejabberd_captcha},
warn_replaced_handler(captcha, Handler, IsEnabled),
[Handler|Acc];
[Handler | Acc];
({register, IsEnabled}, Acc) ->
Handler = {<<"/register">>, mod_register_web},
warn_replaced_handler(register, Handler, IsEnabled),
[Handler|Acc];
[Handler | Acc];
({web_admin, IsEnabled}, Acc) ->
Handler = {<<"/admin">>, ejabberd_web_admin},
warn_replaced_handler(web_admin, Handler, IsEnabled),
[Handler|Acc];
[Handler | Acc];
({http_bind, IsEnabled}, Acc) ->
Handler = {<<"/bosh">>, mod_bosh},
warn_replaced_handler(http_bind, Handler, IsEnabled),
[Handler|Acc];
[Handler | Acc];
({xmlrpc, IsEnabled}, Acc) ->
Handler = {<<"/">>, ejabberd_xmlrpc},
warn_replaced_handler(xmlrpc, Handler, IsEnabled),
Acc ++ [Handler];
(_, Acc) ->
Acc
end, Handlers, Opts),
end,
Handlers,
Opts),
Handlers2 = lists:map(
fun({Path, mod_http_bind}) ->
warn_replaced_module(mod_http_bind, mod_bosh),
{Path, mod_bosh};
(PathMod) ->
PathMod
end, Handlers1),
end,
Handlers1),
Opts1 = lists:filtermap(
fun({captcha, _}) -> false;
({register, _}) -> false;
@ -323,12 +351,14 @@ replace_request_handlers(Opts) ->
({request_handlers, _}) ->
false;
(_) -> true
end, Opts),
end,
Opts),
case Handlers2 of
[] -> Opts1;
_ -> [{request_handlers, Handlers2}|Opts1]
_ -> [{request_handlers, Handlers2} | Opts1]
end.
remove_xmlrpc_access_commands(Opts) ->
lists:filter(
fun({access_commands, _}) ->
@ -336,7 +366,9 @@ remove_xmlrpc_access_commands(Opts) ->
false;
(_) ->
true
end, Opts).
end,
Opts).
replace_turn_ip(Opts) ->
lists:filtermap(
@ -345,7 +377,9 @@ replace_turn_ip(Opts) ->
{true, {turn_ipv4_address, Val}};
(_) ->
true
end, Opts).
end,
Opts).
remove_inet_options(Opts) ->
lists:filter(
@ -354,11 +388,14 @@ remove_inet_options(Opts) ->
false;
(_) ->
true
end, Opts).
end,
Opts).
collect_listener_certfiles(Opts, Acc) ->
Mod = proplists:get_value(module, Opts),
if Mod == ejabberd_http;
if
Mod == ejabberd_http;
Mod == ejabberd_c2s;
Mod == ejabberd_s2s_in ->
case lists:keyfind(certfile, 1, Opts) of
@ -369,7 +406,7 @@ collect_listener_certfiles(Opts, Acc) ->
[Mod, adjust_hint()]),
CertFiles = maps:get(certfiles, Acc, []),
{proplists:delete(certfile, Opts),
maps:put(certfiles, [CertFile|CertFiles], Acc)};
maps:put(certfiles, [CertFile | CertFiles], Acc)};
false ->
{Opts, Acc}
end;
@ -377,6 +414,7 @@ collect_listener_certfiles(Opts, Acc) ->
{Opts, Acc}
end.
%%%===================================================================
%%% Module transformers
%%% NOTE: transform_module_options/1 is called before transform_module/4
@ -409,7 +447,9 @@ transform_module_options(Opts) ->
false;
(_) ->
true
end, Opts).
end,
Opts).
transform_module(Host, mod_http_bind, Opts, Acc) ->
warn_replaced_module(mod_http_bind, mod_bosh),
@ -419,7 +459,7 @@ transform_module(Host, mod_vcard_xupdate_odbc, Opts, Acc) ->
transform_module(Host, mod_vcard_xupdate, Opts, Acc);
transform_module(Host, mod_vcard_ldap, Opts, Acc) ->
warn_replaced_module(mod_vcard_ldap, mod_vcard, ldap),
transform_module(Host, mod_vcard, [{db_type, ldap}|Opts], Acc);
transform_module(Host, mod_vcard, [{db_type, ldap} | Opts], Acc);
transform_module(Host, M, Opts, Acc) when (M == mod_announce_odbc orelse
M == mod_blocking_odbc orelse
M == mod_caps_odbc orelse
@ -434,7 +474,7 @@ transform_module(Host, M, Opts, Acc) when (M == mod_announce_odbc orelse
M == mod_vcard_odbc) ->
M1 = strip_odbc_suffix(M),
warn_replaced_module(M, M1, sql),
transform_module(Host, M1, [{db_type, sql}|Opts], Acc);
transform_module(Host, M1, [{db_type, sql} | Opts], Acc);
transform_module(_Host, mod_blocking, Opts, Acc) ->
Opts1 = lists:filter(
fun({db_type, _}) ->
@ -442,7 +482,8 @@ transform_module(_Host, mod_blocking, Opts, Acc) ->
false;
(_) ->
true
end, Opts),
end,
Opts),
{{mod_blocking, Opts1}, Acc};
transform_module(_Host, mod_carboncopy, Opts, Acc) ->
Opts1 = lists:filter(
@ -455,7 +496,8 @@ transform_module(_Host, mod_carboncopy, Opts, Acc) ->
false;
(_) ->
true
end, Opts),
end,
Opts),
{{mod_carboncopy, Opts1}, Acc};
transform_module(_Host, mod_http_api, Opts, Acc) ->
Opts1 = lists:filter(
@ -464,7 +506,8 @@ transform_module(_Host, mod_http_api, Opts, Acc) ->
false;
(_) ->
true
end, Opts),
end,
Opts),
{{mod_http_api, Opts1}, Acc};
transform_module(_Host, mod_http_upload, Opts, Acc) ->
Opts1 = lists:filter(
@ -473,7 +516,8 @@ transform_module(_Host, mod_http_upload, Opts, Acc) ->
true;
(_) ->
true
end, Opts),
end,
Opts),
{{mod_http_upload, Opts1}, Acc};
transform_module(_Host, mod_pubsub, Opts, Acc) ->
Opts1 = lists:map(
@ -483,9 +527,15 @@ transform_module(_Host, mod_pubsub, Opts, Acc) ->
fun(Plugin) ->
case lists:member(
Plugin,
[<<"buddy">>, <<"club">>, <<"dag">>,
<<"dispatch">>, <<"hometree">>, <<"mb">>,
<<"mix">>, <<"online">>, <<"private">>,
[<<"buddy">>,
<<"club">>,
<<"dag">>,
<<"dispatch">>,
<<"hometree">>,
<<"mb">>,
<<"mix">>,
<<"online">>,
<<"private">>,
<<"public">>]) of
true ->
?WARNING_MSG(
@ -498,18 +548,22 @@ transform_module(_Host, mod_pubsub, Opts, Acc) ->
false ->
true
end
end, Plugins)};
end,
Plugins)};
(Opt) ->
Opt
end, Opts),
end,
Opts),
{{mod_pubsub, Opts1}, Acc};
transform_module(_Host, Mod, Opts, Acc) ->
{{Mod, Opts}, Acc}.
strip_odbc_suffix(M) ->
[_|T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
[_ | T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
list_to_atom(string:join(lists:reverse(T), "_")).
%%%===================================================================
%%% Aux
%%%===================================================================
@ -517,17 +571,21 @@ filtermapfoldr(Fun, Init, List) ->
lists:foldr(
fun(X, {Ret, Acc}) ->
case Fun(X, Acc) of
{true, Acc1} -> {[X|Ret], Acc1};
{{true, X1}, Acc1} -> {[X1|Ret], Acc1};
{true, Acc1} -> {[X | Ret], Acc1};
{{true, X1}, Acc1} -> {[X1 | Ret], Acc1};
{false, Acc1} -> {Ret, Acc1}
end
end, {[], Init}, List).
end,
{[], Init},
List).
set_certfiles(Y, #{certfiles := CertFiles} = Acc) ->
{lists:keystore(certfiles, 1, Y, {certfiles, CertFiles}), Acc};
set_certfiles(Y, Acc) ->
{Y, Acc}.
%%%===================================================================
%%% Warnings
%%%===================================================================
@ -536,14 +594,18 @@ warn_replaced_module(From, To) ->
"replaced by ~ts. ~ts",
[From, To, adjust_hint()]).
warn_replaced_module(From, To, Type) ->
?WARNING_MSG("Module ~ts is deprecated and was automatically "
"replaced by ~ts with db_type: ~ts. ~ts",
[From, To, Type, adjust_hint()]).
warn_removed_module(Mod) ->
?WARNING_MSG("Module ~ts is deprecated and was automatically "
"removed from the configuration. ~ts", [Mod, adjust_hint()]).
"removed from the configuration. ~ts",
[Mod, adjust_hint()]).
warn_replaced_handler(Opt, {Path, Module}, false) ->
?WARNING_MSG("Listening option '~ts' is deprecated, "
@ -556,28 +618,36 @@ warn_replaced_handler(Opt, {Path, Module}, true) ->
"HTTP request handler: \"~ts\" -> ~ts. ~ts",
[Opt, Path, Module, adjust_hint()]).
warn_deprecated_option(OldOpt, NewOpt) ->
?WARNING_MSG("Option '~ts' is deprecated. Use option '~ts' instead.",
[OldOpt, NewOpt]).
warn_replaced_option(OldOpt, NewOpt) ->
?WARNING_MSG("Option '~ts' is deprecated and was automatically "
"replaced by '~ts'. ~ts",
[OldOpt, NewOpt, adjust_hint()]).
warn_removed_option(Opt) ->
?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. "
"Please remove it from the configuration.", [Opt]).
"Please remove it from the configuration.",
[Opt]).
warn_removed_option(OldOpt, NewOpt) ->
?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. "
"Use option '~ts' instead.", [OldOpt, NewOpt]).
"Use option '~ts' instead.",
[OldOpt, NewOpt]).
warn_removed_module_option(Opt, Mod) ->
?WARNING_MSG("Option '~ts' of module ~ts is deprecated "
"and has no effect anymore. ~ts",
[Opt, Mod, adjust_hint()]).
warn_huge_timeout(Opt, T) when is_integer(T), T >= 1000 ->
?WARNING_MSG("Value '~B' of option '~ts' is too big, "
"are you sure you have set seconds?",
@ -585,11 +655,13 @@ warn_huge_timeout(Opt, T) when is_integer(T), T >= 1000 ->
warn_huge_timeout(_, _) ->
ok.
adjust_hint() ->
"Please adjust your configuration file accordingly. "
"Hint: run `ejabberdctl dump-config` command to view current "
"configuration as it is seen by ejabberd.".
%%%===================================================================
%%% Very raw validator: just to make sure we get properly typed terms
%%% Expand it if you need to transform more options, but don't
@ -597,7 +669,8 @@ adjust_hint() ->
%%%===================================================================
validator() ->
Validators =
#{s2s_use_starttls => econf:atom(),
#{
s2s_use_starttls => econf:atom(),
certfiles => econf:list(econf:any()),
c2s_certfile => econf:binary(),
s2s_certfile => econf:binary(),
@ -606,13 +679,16 @@ validator() ->
default_ram_db => econf:atom(),
auth_method => econf:list_or_single(econf:atom()),
acme => econf:options(
#{ca_url => econf:binary(),
'_' => econf:any()},
#{
ca_url => econf:binary(),
'_' => econf:any()
},
[unique]),
listen =>
econf:list(
econf:options(
#{captcha => econf:bool(),
#{
captcha => econf:bool(),
register => econf:bool(),
web_admin => econf:bool(),
http_bind => econf:bool(),
@ -622,23 +698,31 @@ validator() ->
certfile => econf:binary(),
request_handlers =>
econf:map(econf:binary(), econf:atom()),
'_' => econf:any()},
'_' => econf:any()
},
[])),
modules =>
econf:options(
#{'_' =>
#{
'_' =>
econf:options(
#{db_type => econf:atom(),
#{
db_type => econf:atom(),
plugins => econf:list(econf:binary()),
'_' => econf:any()},
[])},
'_' => econf:any()
},
[])
},
[]),
'_' => econf:any()},
'_' => econf:any()
},
econf:options(
Validators#{host_config =>
Validators#{
host_config =>
econf:map(econf:binary(),
econf:options(Validators, [])),
append_host_config =>
econf:map(econf:binary(),
econf:options(Validators, []))},
econf:options(Validators, []))
},
[]).

View file

@ -30,9 +30,14 @@
-export([start/0, start_link/0, process/1, process/2, process2/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([get_commands_spec/0, format_arg/2,
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-export([get_commands_spec/0,
format_arg/2,
get_usage_command/4]).
-include("ejabberd_ctl.hrl").
@ -49,6 +54,7 @@
%% Module
%%-----------------------------
start() ->
disable_logging(),
[SNode, Timeout, Args] = case init:get_plain_arguments() of
@ -66,8 +72,11 @@ start() ->
_ ->
case net_kernel:longnames() of
true ->
lists:flatten([SNode, "@", inet_db:gethostname(),
".", inet_db:res_option(domain)]);
lists:flatten([SNode,
"@",
inet_db:gethostname(),
".",
inet_db:res_option(domain)]);
false ->
lists:flatten([SNode, "@", inet_db:gethostname()]);
_ ->
@ -89,47 +98,58 @@ start() ->
end,
halt(Status).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_commands:unregister_commands(get_commands_spec()),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%-----------------------------
%% Process http
%%-----------------------------
-spec process_http([binary()], tuple()) -> {non_neg_integer(), [{binary(), binary()}], string()}.
process_http([_Call], #request{data = Data, path = [<<"ctl">> | _]}) ->
Args = [binary_to_list(E) || E <- misc:json_decode(Data)],
Args = [ binary_to_list(E) || E <- misc:json_decode(Data) ],
process_http2(Args, ?DEFAULT_VERSION).
process_http2(["--version", Arg | Args], _) ->
Version =
try
list_to_integer(Arg)
catch _:_ ->
catch
_:_ ->
throw({invalid_version, Arg})
end,
process_http2(Args, Version);
@ -142,14 +162,17 @@ process_http2(Args, Version) ->
end,
{200, [{<<"status-code">>, integer_to_binary(Code)}], String2}.
%%-----------------------------
%% Process command line
%%-----------------------------
-spec process([string()]) -> non_neg_integer().
process(Args) ->
process(Args, ?DEFAULT_VERSION).
-spec process([string() | binary()], non_neg_integer() | tuple()) -> non_neg_integer().
process([Call], Request) when is_binary(Call) and is_record(Request, request) ->
@ -166,7 +189,8 @@ process(["status"], _Version) ->
EjabberdLogPath = ejabberd_logger:get_log_path(),
print("ejabberd is not running in that node~n"
"Check for error messages: ~ts~n"
"or other files in that directory.~n", [EjabberdLogPath]),
"or other files in that directory.~n",
[EjabberdLogPath]),
?STATUS_ERROR;
true ->
print("ejabberd ~ts is running in that node~n", [ejabberd_option:version()]),
@ -228,7 +252,8 @@ process(["--version", Arg | Args], _) ->
Version =
try
list_to_integer(Arg)
catch _:_ ->
catch
_:_ ->
throw({invalid_version, Arg})
end,
process(Args, Version);
@ -242,24 +267,30 @@ process(Args, Version) ->
end,
Code.
-spec process2(Args::[string()], AccessCommands::any()) ->
{String::string(), Code::integer()}.
-spec process2(Args :: [string()], AccessCommands :: any()) ->
{String :: string(), Code :: integer()}.
process2(Args, AccessCommands) ->
process2(Args, AccessCommands, ?DEFAULT_VERSION).
process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
list_to_binary(Pass), true}, Version);
process2(Args,
AccessCommands,
{list_to_binary(User),
list_to_binary(Server),
list_to_binary(Pass),
true},
Version);
process2(Args, AccessCommands, Version) ->
process2(Args, AccessCommands, noauth, Version).
process2(Args, AccessCommands, Auth, Version) ->
case try_run_ctp(Args, Auth, AccessCommands, Version) of
{String, wrong_command_arguments}
when is_list(String) ->
io:format(lists:flatten(["\n" | String]++["\n"])),
io:format(lists:flatten(["\n" | String] ++ ["\n"])),
[CommandString | _] = Args,
process(["help" | [CommandString]], Version),
{lists:flatten(String), ?STATUS_USAGE};
@ -276,13 +307,14 @@ process2(Args, AccessCommands, Auth, Version) ->
{"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR}
end.
determine_string_type(String, Version) ->
TagsCommands = ejabberd_commands:get_tags_commands(Version),
CommandsNames = case lists:keysearch(String, 1, TagsCommands) of
{value, {String, CNs}} -> CNs;
false -> []
end,
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
AllCommandsNames = [ atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version) ],
Cmds = filter_commands(AllCommandsNames, String),
case {CommandsNames, Cmds} of
{[], []} -> no_idea;
@ -291,10 +323,12 @@ determine_string_type(String, Version) ->
{_, _} -> both
end.
%%-----------------------------
%% Command calling
%%-----------------------------
try_run_ctp(Args, Auth, AccessCommands, Version) ->
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
false when Args /= [] ->
@ -315,6 +349,7 @@ try_run_ctp(Args, Auth, AccessCommands, Version) ->
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
end.
try_call_command(Args, Auth, AccessCommands, Version) ->
try call_command(Args, Auth, AccessCommands, Version) of
{Reason, wrong_command_arguments} ->
@ -323,7 +358,7 @@ try_call_command(Args, Auth, AccessCommands, Version) ->
Res
catch
throw:{error, unknown_command} ->
KnownCommands = [Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version)],
KnownCommands = [ Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version) ],
UnknownCommand = list_to_atom(hd(Args)),
{io_lib:format(
"Error: unknown command '~ts'. Did you mean '~ts'?",
@ -338,11 +373,12 @@ try_call_command(Args, Auth, AccessCommands, Version) ->
?STATUS_ERROR}
end.
-spec call_command(Args::[string()],
Auth::noauth | {binary(), binary(), binary(), true},
AccessCommands::[any()],
Version::integer()) ->
string() | integer() | {string(), integer()} | {error, ErrorType::any()}.
-spec call_command(Args :: [string()],
Auth :: noauth | {binary(), binary(), binary(), true},
AccessCommands :: [any()],
Version :: integer()) ->
string() | integer() | {string(), integer()} | {error, ErrorType :: any()}.
call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
CmdStringU = ejabberd_regexp:greplace(
list_to_binary(CmdString), <<"-">>, <<"_">>),
@ -360,11 +396,11 @@ call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
CI2,
Version),
format_result_preliminary(Result, ResultFormat, Version);
{'EXIT', {function_clause,[{lists,zip,[A1,A2|_], _} | _]}} ->
{'EXIT', {function_clause, [{lists, zip, [A1, A2 | _], _} | _]}} ->
{NumCompa, TextCompa} =
case {length(A1), length(A2)} of
{L1, L2} when L1 < L2 -> {L2-L1, "less argument"};
{L1, L2} when L1 > L2 -> {L1-L2, "more argument"}
{L1, L2} when L1 < L2 -> {L2 - L1, "less argument"};
{L1, L2} when L1 > L2 -> {L1 - L2, "more argument"}
end,
process(["help" | [CmdString]], Version),
{io_lib:format("Error: the command '~ts' requires ~p ~ts.",
@ -377,6 +413,7 @@ call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
%% Format arguments
%%-----------------------------
format_args(Args, ArgsFormat) ->
lists:foldl(
fun({{_ArgName, ArgFormat}, Arg}, Res) ->
@ -386,6 +423,7 @@ format_args(Args, ArgsFormat) ->
[],
lists:zip(ArgsFormat, Args)).
format_arg(Arg, integer) ->
format_arg2(Arg, "~d");
format_arg(Arg, binary) ->
@ -397,9 +435,9 @@ format_arg(Arg, string) ->
Parse = "~" ++ NumChars ++ "c",
format_arg2(Arg, Parse);
format_arg(Arg, {list, {_ArgName, ArgFormat}}) ->
[format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")];
[ format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",") ];
format_arg(Arg, {list, ArgFormat}) ->
[format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",")];
[ format_arg(string:trim(Element), ArgFormat) || Element <- string:tokens(Arg, ",") ];
format_arg(Arg, {tuple, Elements}) ->
Args = string:tokens(Arg, ":"),
list_to_tuple(format_args(Args, Elements));
@ -408,19 +446,23 @@ format_arg(Arg, Format) ->
JSON = misc:json_decode(S),
mod_http_api:format_arg(JSON, Format).
format_arg2(Arg, Parse)->
format_arg2(Arg, Parse) ->
{ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg),
Arg2.
%%-----------------------------
%% Format result
%%-----------------------------
format_result_preliminary(Result, {A, {list, B}}, Version) ->
format_result(Result, {A, {top_result_list, B}}, Version);
format_result_preliminary(Result, ResultFormat, Version) ->
format_result(Result, ResultFormat, Version).
format_result({error, ErrorAtom}, _, _Version) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
@ -436,7 +478,7 @@ format_result(Atom, {_Name, atom}, _Version) ->
format_result(Int, {_Name, integer}, _Version) ->
io_lib:format("~p", [Int]);
format_result([A|_]=String, {_Name, string}, _Version) when is_list(String) and is_integer(A) ->
format_result([A | _] = String, {_Name, string}, _Version) when is_list(String) and is_integer(A) ->
io_lib:format("~ts", [String]);
format_result(Binary, {_Name, binary}, _Version) when is_binary(Binary) ->
@ -466,8 +508,7 @@ format_result({Code, Text}, {_Name, restuple}, _Version) ->
format_result([], {_Name, {top_result_list, _ElementsDef}}, _Version) ->
"";
format_result([FirstElement | Elements], {_Name, {top_result_list, ElementsDef}}, Version) ->
[format_result(FirstElement, ElementsDef, Version) |
lists:map(
[format_result(FirstElement, ElementsDef, Version) | lists:map(
fun(Element) ->
["\n" | format_result(Element, ElementsDef, Version)]
end,
@ -497,8 +538,7 @@ format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}, Version)
format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}, Version) ->
ElementsList = tuple_to_list(ElementsTuple),
[{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef),
[format_result(FirstE, FirstD, Version) |
lists:map(
[format_result(FirstE, FirstD, Version) | lists:map(
fun({Element, ElementDef}) ->
["\t" | format_result(Element, ElementDef, Version)]
end,
@ -507,6 +547,7 @@ format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}, Version) ->
format_result(404, {_Name, _}, _Version) ->
make_status(not_found).
make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
@ -515,22 +556,25 @@ make_status(Error) ->
io:format("Error: ~p~n", [Error]),
?STATUS_ERROR.
get_list_commands(Version) ->
try ejabberd_commands:list_commands(Version) of
Commands ->
[tuple_command_help(Command) || Command <- Commands]
[ tuple_command_help(Command) || Command <- Commands ]
catch
exit:_ ->
[]
end.
%% Return: {string(), [string()], string()}
tuple_command_help({Name, _Args, Desc}) ->
{Args, _, _} = ejabberd_commands:get_command_format(Name, admin),
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
Arguments = [ atom_to_list(ArgN) || {ArgN, _ArgF} <- Args ],
CallString = atom_to_list(Name),
{CallString, Arguments, Desc}.
has_tuple_args(Args) ->
lists:any(
fun({_Name, tuple}) -> true;
@ -541,6 +585,7 @@ has_tuple_args(Args) ->
end,
Args).
has_list_args(Args) ->
lists:any(
fun({_Name, list}) -> true;
@ -549,6 +594,7 @@ has_list_args(Args) ->
end,
Args).
%%-----------------------------
%% Print help
%%-----------------------------
@ -573,20 +619,36 @@ has_list_args(Args) ->
-define(N2, "\e[0m").
-define(B(S), case ShCode of true -> [?N1, S, ?N2]; false -> S end).
print_usage(Version) ->
{MaxC, ShCode} = get_shell_info(),
print_usage(dual, MaxC, ShCode, Version).
print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands = get_list_commands(Version),
print(
["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("name"), "] [--version ", ?A("apiv"), "] ",
"[--auth ", ?A("user host pass"), "] ",
?C("command"), " [", ?A("arguments"), "]\n"
["Usage: ",
"ejabberdctl",
" [--no-timeout] [--node ",
?A("name"),
"] [--version ",
?A("apiv"),
"] ",
"[--auth ",
?A("user host pass"),
"] ",
?C("command"),
" [",
?A("arguments"),
"]\n"
"\n"
"Available commands in this ejabberd node:\n"], []),
"Available commands in this ejabberd node:\n"],
[]),
print_usage_commands(HelpMode, MaxC, ShCode, AllCommands).
print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
CmdDescsSorted = lists:keysort(1, Commands),
@ -623,12 +685,13 @@ print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
get_shell_info() ->
%% This function was introduced in OTP R12B-0
try io:columns() of
{ok, C} -> {C-2, true};
{ok, C} -> {C - 2, true};
{error, enotsup} -> {78, false}
catch
_:_ -> {78, false}
end.
%% Split this command description in several lines of proper length
prepare_description(DescInit, MaxC, Desc) ->
case string:find(Desc, "\n") of
@ -638,10 +701,12 @@ prepare_description(DescInit, MaxC, Desc) ->
Desc
end.
prepare_description2(DescInit, MaxC, Desc) ->
Words = string:tokens(Desc, " "),
prepare_long_line(DescInit, MaxC, Words).
prepare_long_line(DescInit, MaxC, Words) ->
MaxSegmentLen = MaxC - DescInit,
MarginString = lists:duplicate(DescInit, $\s), % Put spaces
@ -649,24 +714,29 @@ prepare_long_line(DescInit, MaxC, Words) ->
MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments),
[FirstSegment | MoreSegmentsMixed].
mix_desc_segments(MarginString, Segments) ->
[["\n", MarginString, Segment] || Segment <- Segments].
[ ["\n", MarginString, Segment] || Segment <- Segments ].
split_desc_segments(MaxL, Words) ->
join(MaxL, Words).
%% Join words in a segment,
%% but stop adding to a segment if adding this word would pass L
join(L, Words) ->
join(L, Words, 0, [], []).
join(_Len, [], _CurSegLen, CurSeg, AllSegs) ->
lists:reverse([CurSeg | AllSegs]);
join(Len, [Word | Tail], CurSegLen, CurSeg, AllSegs) ->
WordLen = length(Word),
SegSize = WordLen + CurSegLen + 1,
{NewCurSeg, NewAllSegs, NewCurSegLen} =
if SegSize < Len ->
if
SegSize < Len ->
{[CurSeg, " ", Word], AllSegs, SegSize};
true ->
{Word, [CurSeg | AllSegs], WordLen}
@ -688,31 +758,43 @@ format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual)
format_command_lines(CALD, _MaxCmdLen, _MaxC, ShCode, short) ->
lists:map(
fun({Cmd, Args, _CmdArgsL, _Desc}) ->
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n"]
end, CALD);
[" ", ?C(Cmd), [ [" ", ?A(Arg)] || Arg <- Args ], "\n"]
end,
CALD);
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) ->
lists:map(
fun({Cmd, Args, CmdArgsL, Desc}) ->
DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc),
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args],
DescFmt = prepare_description(MaxCmdLen + 4, MaxC, Desc),
[" ",
?C(Cmd),
[ [" ", ?A(Arg)] || Arg <- Args ],
lists:duplicate(MaxCmdLen - CmdArgsL + 1, $\s),
DescFmt, "\n"]
end, CALD);
DescFmt,
"\n"]
end,
CALD);
format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
lists:map(
fun({Cmd, Args, _CmdArgsL, Desc}) ->
DescFmt = prepare_description(13, MaxC, Desc),
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n",
" ", DescFmt, "\n"]
end, CALD).
[" ",
?C(Cmd),
[ [" ", ?A(Arg)] || Arg <- Args ],
"\n",
" ",
DescFmt,
"\n"]
end,
CALD).
%%-----------------------------
%% Print Tags
%%-----------------------------
print_usage_tags(MaxC, ShCode, Version) ->
print("Available tags and list of commands:", []),
TagsCommands = ejabberd_commands:get_tags_commands(Version),
@ -726,6 +808,7 @@ print_usage_tags(MaxC, ShCode, Version) ->
TagsCommands),
print("\n\n", []).
print_usage_tags_long(MaxC, ShCode, Version) ->
print("Available tags and commands details:", []),
TagsCommands = ejabberd_commands:get_tags_commands(Version),
@ -737,9 +820,11 @@ print_usage_tags_long(MaxC, ShCode, Version) ->
fun(NameString) ->
C = ejabberd_commands:get_command_definition(
list_to_atom(NameString), Version),
#ejabberd_commands{name = Name,
#ejabberd_commands{
name = Name,
args = Args,
desc = Desc} = C,
desc = Desc
} = C,
tuple_command_help({Name, Args, Desc})
end,
CommandsNames),
@ -748,6 +833,7 @@ print_usage_tags_long(MaxC, ShCode, Version) ->
TagsCommands),
print("\n", []).
print_usage_tags(Tag, MaxC, ShCode, Version) ->
print(["Available commands with tag ", ?G(Tag), ":", "\n", "\n"], []),
HelpMode = long,
@ -760,9 +846,11 @@ print_usage_tags(Tag, MaxC, ShCode, Version) ->
fun(NameString) ->
C = ejabberd_commands:get_command_definition(
list_to_atom(NameString), Version),
#ejabberd_commands{name = Name,
#ejabberd_commands{
name = Name,
args = Args,
desc = Desc} = C,
desc = Desc
} = C,
tuple_command_help({Name, Args, Desc})
end,
CommandsNames),
@ -774,6 +862,10 @@ print_usage_tags(Tag, MaxC, ShCode, Version) ->
%% Print usage of 'help' command
%%-----------------------------
%%
%% @efmt:off
%% @indent-begin
print_usage_help(MaxC, ShCode) ->
LongDesc =
["This special ", ?C("help"), " command provides help of ejabberd commands.\n\n"
@ -812,22 +904,31 @@ print_usage_help(MaxC, ShCode) ->
result = {help, string}},
print(get_usage_command2("help", C, MaxC, ShCode), []).
%% @indent-end
%% @efmt:on
%%-----------------------------
%% Print usage command
%%-----------------------------
-spec print_usage_commands2(CmdSubString::string(), MaxC::integer(),
ShCode::boolean(), Version::integer()) -> ok.
%%
%%-----------------------------
%% Print usage command
%%-----------------------------
-spec print_usage_commands2(CmdSubString :: string(),
MaxC :: integer(),
ShCode :: boolean(),
Version :: integer()) -> ok.
print_usage_commands2(CmdSubString, MaxC, ShCode, Version) ->
%% Get which command names match this substring
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
AllCommandsNames = [ atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version) ],
Cmds = filter_commands(AllCommandsNames, CmdSubString),
case Cmds of
[] -> io:format("Error: no command found that match '~ts'~n", [CmdSubString]);
_ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
end.
print_usage_commands3([Cmd], MaxC, ShCode, Version) ->
print_usage_command(Cmd, MaxC, ShCode, Version);
print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
@ -835,9 +936,11 @@ print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
fun(NameString) ->
C = ejabberd_commands:get_command_definition(
list_to_atom(NameString), Version),
#ejabberd_commands{name = Name,
#ejabberd_commands{
name = Name,
args = Args,
desc = Desc} = C,
desc = Desc
} = C,
tuple_command_help({Name, Args, Desc})
end,
Cmds),
@ -845,12 +948,14 @@ print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
print_usage_commands(long, MaxC, ShCode, CommandsList), %% que aqui solo muestre un par de lineas
ok.
filter_commands(All, SubString) ->
case lists:member(SubString, All) of
true -> [SubString];
false -> filter_commands_regexp(All, SubString)
end.
filter_commands_regexp(All, Glob) ->
RegExp = ejabberd_regexp:sh_to_awk(list_to_binary(Glob)),
lists:filter(
@ -864,21 +969,27 @@ filter_commands_regexp(All, Glob) ->
end,
All).
maybe_add_policy_arguments(Args, user) ->
[{user, binary}, {host, binary} | Args];
maybe_add_policy_arguments(Args, _) ->
Args.
-spec print_usage_command(Cmd::string(), MaxC::integer(),
ShCode::boolean(), Version::integer()) -> ok.
-spec print_usage_command(Cmd :: string(),
MaxC :: integer(),
ShCode :: boolean(),
Version :: integer()) -> ok.
print_usage_command(Cmd, MaxC, ShCode, Version) ->
print(get_usage_command(Cmd, MaxC, ShCode, Version), []).
get_usage_command(Cmd, MaxC, ShCode, Version) ->
Name = list_to_atom(Cmd),
C = ejabberd_commands:get_command_definition(Name, Version),
get_usage_command2(Cmd, C, MaxC, ShCode).
get_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{
tags = TagsAtoms,
@ -891,25 +1002,26 @@ get_usage_command2(Cmd, C, MaxC, ShCode) ->
policy = Policy,
longdesc = LongDesc,
note = Note,
result = ResultDef} = C,
result = ResultDef
} = C,
NameFmt = [" ", ?B("Command Name"), ": ", ?C(Cmd), "\n"],
%% Initial indentation of result is 13 = length(" Arguments: ")
ArgsDef = maybe_add_policy_arguments(ArgsDefPreliminary, Policy),
ArgsDetailed = add_args_desc(ArgsDef, ArgsDesc),
Args = [format_usage_ctype1(ArgDetailed, 13, ShCode) || ArgDetailed <- ArgsDetailed],
Args = [ format_usage_ctype1(ArgDetailed, 13, ShCode) || ArgDetailed <- ArgsDetailed ],
ArgsMargin = lists:duplicate(13, $\s),
ArgsListFmt = case Args of
[] -> "\n";
_ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args]
_ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args ]
end,
ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt],
%% Initial indentation of result is 11 = length(" Returns: ")
ResultFmt = format_usage_ctype(ResultDef, 11),
ReturnsFmt = [" ",?B("Result"),": ", ResultFmt],
ReturnsFmt = [" ", ?B("Result"), ": ", ResultFmt],
ExampleMargin = lists:duplicate(11, $\s),
Example = format_usage_example(Cmd, ArgsExample, ResultExample, ExampleMargin),
@ -917,27 +1029,27 @@ get_usage_command2(Cmd, C, MaxC, ShCode) ->
[] ->
"";
_ ->
ExampleListFmt = [ [Ex, "\n", ExampleMargin] || Ex <- Example],
[" ",?B("Example"),": ", ExampleListFmt, "\n"]
ExampleListFmt = [ [Ex, "\n", ExampleMargin] || Ex <- Example ],
[" ", ?B("Example"), ": ", ExampleListFmt, "\n"]
end,
TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])],
TagsFmt = [" ", ?B("Tags"), ":", prepare_long_line(8, MaxC, [ ?G(atom_to_list(TagA)) || TagA <- TagsAtoms ])],
IsDefinerMod = case Definer of
unknown -> true;
_ -> lists:member([gen_mod], proplists:get_all_values(behaviour, Definer:module_info(attributes)))
end,
ModuleFmt = case IsDefinerMod of
true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"];
true -> [" ", ?B("Module"), ": ", atom_to_list(Definer), "\n\n"];
false -> []
end,
NoteFmt = case Note of
"" -> [];
_ -> [" ",?B("Note"),": ", Note, "\n\n"]
_ -> [" ", ?B("Note"), ": ", Note, "\n\n"]
end,
DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)],
DescFmt = [" ", ?B("Description"), ":", prepare_description(15, MaxC, Desc)],
LongDescFmt = case LongDesc of
"" -> "";
@ -955,24 +1067,29 @@ get_usage_command2(Cmd, C, MaxC, ShCode) ->
First = case Cmd of
"help" -> "";
_ -> [NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
_ ->
[NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
"\n\n", ExampleFmt, TagsFmt, "\n\n", ModuleFmt, NoteFmt, DescFmt, "\n\n"]
end,
[First, LongDescFmt, NoteEjabberdctlList, NoteEjabberdctlTuple].
%%-----------------------------
%% Format Arguments Help
%%-----------------------------
add_args_desc(Definitions, none) ->
Descriptions = lists:duplicate(length(Definitions), ""),
add_args_desc(Definitions, Descriptions);
add_args_desc(Definitions, Descriptions) ->
lists:zipwith(fun({Name, Type}, Description) ->
{Name, Type, Description} end,
{Name, Type, Description}
end,
Definitions,
Descriptions).
format_usage_ctype1({_Name, _Type} = Definition, Indentation, ShCode) ->
[Arg] = add_args_desc([Definition], none),
format_usage_ctype1(Arg, Indentation, ShCode);
@ -993,20 +1110,20 @@ format_usage_ctype1({Name, Type, Description}, Indentation, ShCode) ->
end,
DescriptionText = case Description of
"" -> "";
Description -> " : "++Description
Description -> " : " ++ Description
end,
io_lib:format("~p::~s~s", [Name, TypeString, DescriptionText]).
format_usage_ctype(Type, _Indentation)
when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary)
or (Type==rescode) or (Type==restuple) ->
when (Type == atom) or (Type == integer) or (Type == string) or (Type == binary) or
(Type == rescode) or (Type == restuple) ->
io_lib:format("~p", [Type]);
format_usage_ctype({Name, Type}, _Indentation)
when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary)
or (Type==rescode) or (Type==restuple)
or (Type==any) ->
when (Type == atom) or (Type == integer) or (Type == string) or (Type == binary) or
(Type == rescode) or (Type == restuple) or
(Type == any) ->
io_lib:format("~p::~p", [Name, Type]);
format_usage_ctype({Name, {list, ElementDef}}, Indentation) ->
@ -1031,21 +1148,32 @@ format_usage_tuple([ElementDef | ElementsDef], Indentation) ->
MarginString = lists:duplicate(Indentation, $\s), % Put spaces
[ElementFmt, ",\n", MarginString, format_usage_tuple(ElementsDef, Indentation)].
print(Format, Args) ->
io:format(lists:flatten(Format), Args).
-ifdef(LAGER).
disable_logging() ->
ok.
-else.
disable_logging() ->
logger:set_primary_config(level, none).
-endif.
%%-----------------------------
%% Format Example Help
%%-----------------------------
format_usage_example(_Cmd, none, _ResultExample, _Indentation) ->
"";
format_usage_example(Cmd, ArgsExample, ResultExample, Indentation) ->
@ -1053,6 +1181,7 @@ format_usage_example(Cmd, ArgsExample, ResultExample, Indentation) ->
Result = format_usage_result([ResultExample], [], Indentation),
[lists:join(" ", ["ejabberdctl", Cmd] ++ Arguments) | Result].
format_usage_arguments([], R) ->
lists:reverse(R);
@ -1060,7 +1189,7 @@ format_usage_arguments([Argument | Arguments], R)
when is_integer(Argument) ->
format_usage_arguments(Arguments, [integer_to_list(Argument) | R]);
format_usage_arguments([[Integer|_] = Argument | Arguments], R)
format_usage_arguments([[Integer | _] = Argument | Arguments], R)
when is_list(Argument) and is_integer(Integer) ->
Result = case contains_more_than_letters(Argument) of
true -> ["\"", Argument, "\""];
@ -1094,6 +1223,7 @@ format_usage_arguments([Argument | Arguments], R)
format_usage_arguments([Argument | Arguments], R) ->
format_usage_arguments(Arguments, [Argument | R]).
format_usage_result([none], _R, _Indentation) ->
"";
format_usage_result([], R, _Indentation) ->
@ -1111,18 +1241,18 @@ format_usage_result([Argument | Arguments], R, Indentation)
when is_integer(Argument) ->
format_usage_result(Arguments, [integer_to_list(Argument) | R], Indentation);
format_usage_result([[Integer|_] = Argument | Arguments], R, Indentation)
format_usage_result([[Integer | _] = Argument | Arguments], R, Indentation)
when is_list(Argument) and is_integer(Integer) ->
format_usage_result(Arguments, [Argument | R], Indentation);
format_usage_result([[Element | _] = Argument | Arguments], R, Indentation)
when is_list(Argument) and is_tuple(Element) ->
ArgumentFmt = format_usage_result(Argument, [], Indentation),
format_usage_result(Arguments, [lists:join("\n"++Indentation, ArgumentFmt) | R], Indentation);
format_usage_result(Arguments, [lists:join("\n" ++ Indentation, ArgumentFmt) | R], Indentation);
format_usage_result([Argument | Arguments], R, Indentation)
when is_list(Argument) ->
format_usage_result(Arguments, [lists:join("\n"++Indentation, Argument) | R], Indentation);
format_usage_result(Arguments, [lists:join("\n" ++ Indentation, Argument) | R], Indentation);
format_usage_result([Argument | Arguments], R, Indentation)
when is_tuple(Argument) ->
@ -1132,23 +1262,31 @@ format_usage_result([Argument | Arguments], R, Indentation)
format_usage_result([Argument | Arguments], R, Indentation) ->
format_usage_result(Arguments, [Argument | R], Indentation).
contains_more_than_letters(Argument) ->
lists:any(fun(I) when (I < $A) -> true;
(I) when (I > $z) -> true;
(_) -> false end,
(_) -> false
end,
Argument).
%%-----------------------------
%% Register commands
%%-----------------------------
get_commands_spec() ->
[
#ejabberd_commands{name = help, tags = [ejabberdctl],
[#ejabberd_commands{
name = help,
tags = [ejabberdctl],
desc = "Get list of commands, or help of a command (only ejabberdctl)",
longdesc = "This command is exclusive for the ejabberdctl command-line script, "
"don't attempt to execute it using any other API frontend."},
#ejabberd_commands{name = mnesia_change, tags = [ejabberdctl, mnesia],
"don't attempt to execute it using any other API frontend."
},
#ejabberd_commands{
name = mnesia_change,
tags = [ejabberdctl, mnesia],
desc = "Change the erlang node name in the mnesia database (only ejabberdctl)",
longdesc = "This command internally calls the _`mnesia_change_nodename`_ API. "
"This is a special command that starts and stops ejabberd several times: "
@ -1158,13 +1296,19 @@ get_commands_spec() ->
note = "added in 25.08",
args = [{old_node_name, string}],
args_desc = ["Old erlang node name"],
args_example = ["ejabberd@oldmachine"]},
#ejabberd_commands{name = mnesia_info_ctl, tags = [ejabberdctl, mnesia],
args_example = ["ejabberd@oldmachine"]
},
#ejabberd_commands{
name = mnesia_info_ctl,
tags = [ejabberdctl, mnesia],
desc = "Show information of Mnesia system (only ejabberdctl)",
note = "renamed in 24.02",
longdesc = "This command is exclusive for the ejabberdctl command-line script, "
"don't attempt to execute it using any other API frontend."},
#ejabberd_commands{name = print_sql_schema, tags = [ejabberdctl, sql],
"don't attempt to execute it using any other API frontend."
},
#ejabberd_commands{
name = print_sql_schema,
tags = [ejabberdctl, sql],
desc = "Print SQL schema for the given RDBMS (only ejabberdctl)",
longdesc = "This command is exclusive for the ejabberdctl command-line script, "
"don't attempt to execute it using any other API frontend.",
@ -1173,5 +1317,5 @@ get_commands_spec() ->
args_desc = ["Database type: pgsql | mysql | sqlite",
"Your database version: 16.1, 8.2.0...",
"Use new schema: 0, false, 1 or true"],
args_example = ["pgsql", "16.1", "true"]}
].
args_example = ["pgsql", "16.1", "true"]
}].

View file

@ -29,12 +29,14 @@
%% Supervisor callbacks
-export([init/1]).
%%%===================================================================
%%% API functions
%%%===================================================================
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================

View file

@ -27,12 +27,14 @@
-include("ejabberd_commands.hrl").
-include("translate.hrl").
%%%===================================================================
%%% API
%%%===================================================================
man() ->
man(<<"en">>).
man(Lang) when is_list(Lang) ->
man(list_to_binary(Lang));
man(Lang) ->
@ -48,14 +50,15 @@ man(Lang) ->
Example = maps:get(example, Map, []),
Note = maps:get(note, Map, []),
Apitags = get_module_apitags(M),
{[{M, Descr, DocOpts, #{example => Example, note => Note, apitags => Apitags}}|Mods], SubMods};
{[{M, Descr, DocOpts, #{example => Example, note => Note, apitags => Apitags}} | Mods], SubMods};
#{opts := DocOpts} ->
{ParentMod, Backend} = strip_backend_suffix(M),
{Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)};
#{} ->
warn("module ~s is not properly documented", [M]),
Acc
catch _:undef ->
catch
_:undef ->
case erlang:function_exported(
M, mod_options, 1) of
true ->
@ -68,13 +71,18 @@ man(Lang) ->
false ->
Acc
end
end, {[], dict:new()}, ejabberd_config:beams(all)),
end,
{[], dict:new()},
ejabberd_config:beams(all)),
Doc = lists:flatmap(
fun(M) ->
try M:doc()
catch _:undef -> []
try
M:doc()
catch
_:undef -> []
end
end, ejabberd_config:callback_modules(all)),
end,
ejabberd_config:callback_modules(all)),
Version = binary_to_list(ejabberd_config:version()),
Options =
["TOP LEVEL OPTIONS",
@ -85,7 +93,8 @@ man(Lang) ->
lists:flatmap(
fun(Opt) ->
opt_to_man(Lang, Opt, 1)
end, lists:keysort(1, Doc)),
end,
lists:keysort(1, Doc)),
ModDoc1 = lists:map(
fun({M, Descr, DocOpts, Ex}) ->
case dict:find(M, SubModDoc) of
@ -94,7 +103,8 @@ man(Lang) ->
error ->
{M, Descr, DocOpts, [], Ex}
end
end, ModDoc),
end,
ModDoc),
ModOptions =
[io_lib:nl(),
"MODULES",
@ -114,10 +124,11 @@ man(Lang) ->
io_lib:nl()] ++
format_versions(Lang, Example) ++ [io_lib:nl()] ++
tr_multi(Lang, Descr) ++ [io_lib:nl()] ++
opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++
opts_to_man(Lang, [{M, '', DocOpts} | Backends]) ++
format_example(0, Lang, Example) ++ [io_lib:nl()] ++
format_apitags(Lang, Example)
end, lists:keysort(1, ModDoc1)),
end,
lists:keysort(1, ModDoc1)),
ListenOptions =
[io_lib:nl(),
"LISTENERS",
@ -127,13 +138,14 @@ man(Lang) ->
io_lib:nl(),
"TODO"],
AsciiData =
[[unicode:characters_to_binary(Line), io_lib:nl()]
|| Line <- man_header(Lang) ++ Options ++ [io_lib:nl()]
++ ModOptions ++ ListenOptions ++ man_footer(Lang)],
[ [unicode:characters_to_binary(Line), io_lib:nl()]
|| Line <- man_header(Lang) ++ Options ++ [io_lib:nl()] ++
ModOptions ++ ListenOptions ++ man_footer(Lang) ],
warn_undocumented_modules(ModDoc1),
warn_undocumented_options(Doc),
write_man(AsciiData).
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -143,7 +155,8 @@ opts_to_man(Lang, [{_, _, []}]) ->
opts_to_man(Lang, Backends) ->
lists:flatmap(
fun({_, Backend, DocOpts}) when DocOpts /= [] ->
Text = if Backend == '' ->
Text = if
Backend == '' ->
tr(Lang, ?T("Available options"));
true ->
lists:flatten(
@ -151,25 +164,27 @@ opts_to_man(Lang, Backends) ->
tr(Lang, ?T("Available options for '~s' backend")),
[Backend]))
end,
[Text ++ ":", lists:duplicate(length(Text)+1, $^)|
lists:flatmap(
[Text ++ ":", lists:duplicate(length(Text) + 1, $^) | lists:flatmap(
fun(Opt) -> opt_to_man(Lang, Opt, 1) end,
lists:keysort(1, DocOpts))] ++ [io_lib:nl()];
(_) ->
[]
end, Backends).
end,
Backends).
opt_to_man(Lang, {Option, Options}, Level) ->
[format_option(Lang, Option, Options)|format_versions(Lang, Options)++format_desc(Lang, Options)] ++
[format_option(Lang, Option, Options) | format_versions(Lang, Options) ++ format_desc(Lang, Options)] ++
format_example(Level, Lang, Options);
opt_to_man(Lang, {Option, Options, Children}, Level) ->
[format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++
[format_option(Lang, Option, Options) | format_desc(Lang, Options)] ++
lists:append(
[[H ++ ":"|T]
|| [H|T] <- lists:map(
fun(Opt) -> opt_to_man(Lang, Opt, Level+1) end,
lists:keysort(1, Children))]) ++
[io_lib:nl()|format_example(Level, Lang, Options)].
[ [H ++ ":" | T]
|| [H | T] <- lists:map(
fun(Opt) -> opt_to_man(Lang, Opt, Level + 1) end,
lists:keysort(1, Children)) ]) ++
[io_lib:nl() | format_example(Level, Lang, Options)].
get_version_mark(#{note := Note}) ->
[XX, YY | _] = string:tokens(binary_to_list(ejabberd_option:version()), "."),
@ -181,6 +196,7 @@ get_version_mark(#{note := Note}) ->
get_version_mark(_) ->
"".
format_option(Lang, Option, #{value := Val} = Options) ->
VersionMark = get_version_mark(Options),
"*" ++ atom_to_list(Option) ++ VersionMark ++ "*: 'pass:[" ++
@ -188,22 +204,24 @@ format_option(Lang, Option, #{value := Val} = Options) ->
format_option(_Lang, Option, #{}) ->
"*" ++ atom_to_list(Option) ++ "*::".
format_versions(_Lang, #{note := Note}) when Note /= [] ->
["_Note_ about this option: " ++ Note ++ ". "];
format_versions(_, _) ->
[].
%% @format-begin
get_module_apitags(M) ->
AllCommands = ejabberd_commands:get_commands_definition(),
Tags = [C#ejabberd_commands.tags || C <- AllCommands, C#ejabberd_commands.module == M],
Tags = [ C#ejabberd_commands.tags || C <- AllCommands, C#ejabberd_commands.module == M ],
TagsClean =
lists:sort(
misc:lists_uniq(
lists:flatten(Tags))),
TagsStrings = [atom_to_list(C) || C <- TagsClean],
TagsStrings = [ atom_to_list(C) || C <- TagsClean ],
TagFiltering =
fun ("internal") ->
fun("internal") ->
false;
([$v | Rest]) ->
{error, no_integer} == string:to_integer(Rest);
@ -212,22 +230,26 @@ get_module_apitags(M) ->
end,
TagsFiltered = lists:filter(TagFiltering, TagsStrings),
TagsUrls =
[["_`../../developer/ejabberd-api/admin-tags.md#", C, "|", C, "`_"] || C <- TagsFiltered],
[ ["_`../../developer/ejabberd-api/admin-tags.md#", C, "|", C, "`_"] || C <- TagsFiltered ],
lists:join(", ", TagsUrls).
format_apitags(_Lang, #{apitags := TagsString}) when TagsString /= "" ->
["**API Tags:** ", TagsString];
format_apitags(_, _) ->
[].
%% @format-end
format_desc(Lang, #{desc := Desc}) ->
tr_multi(Lang, Desc).
format_example(Level, Lang, #{example := [_|_] = Example}) ->
format_example(Level, Lang, #{example := [_ | _] = Example}) ->
case lists:all(fun is_list/1, Example) of
true ->
if Level == 0 ->
if
Level == 0 ->
["*Example*:",
"^^^^^^^^^^"];
true ->
@ -239,27 +261,31 @@ format_example(Level, Lang, #{example := [_|_] = Example}) ->
lists:flatmap(
fun({Text, Lines}) ->
[tr(Lang, Text)] ++ format_yaml(Lines)
end, Example);
end,
Example);
false ->
lists:flatmap(
fun(Block) ->
["+", "*Examples*:", "+"|Block]
["+", "*Examples*:", "+" | Block]
end,
lists:map(
fun({Text, Lines}) ->
[tr(Lang, Text), "+"] ++ format_yaml(Lines)
end, Example))
end,
Example))
end;
format_example(_, _, _) ->
[].
format_yaml(Lines) ->
["==========================",
"[source,yaml]",
"----"|Lines] ++
"----" | Lines] ++
["----",
"=========================="].
man_header(Lang) ->
["ejabberd.yml(5)",
"===============",
@ -276,14 +302,17 @@ man_header(Lang) ->
io_lib:nl(),
"DESCRIPTION",
"-----------",
tr(Lang, ?T("The configuration file is written in "
tr(Lang,
?T("The configuration file is written in "
"https://en.wikipedia.org/wiki/YAML[YAML] language.")),
io_lib:nl(),
tr(Lang, ?T("WARNING: YAML is indentation sensitive, so make sure you respect "
tr(Lang,
?T("WARNING: YAML is indentation sensitive, so make sure you respect "
"indentation, or otherwise you will get pretty cryptic "
"configuration errors.")),
io_lib:nl(),
tr(Lang, ?T("Logically, configuration options are split into 3 main categories: "
tr(Lang,
?T("Logically, configuration options are split into 3 main categories: "
"'Modules', 'Listeners' and everything else called 'Top Level' options. "
"Thus this document is split into 3 main chapters describing each "
"category separately. So, the contents of ejabberd.yml will typically "
@ -308,17 +337,20 @@ man_header(Lang) ->
"----",
"==========================",
io_lib:nl(),
tr(Lang, ?T("Any configuration error (such as syntax error, unknown option "
tr(Lang,
?T("Any configuration error (such as syntax error, unknown option "
"or invalid option value) is fatal in the sense that ejabberd will "
"refuse to load the whole configuration file and will not start or will "
"abort configuration reload.")),
io_lib:nl(),
tr(Lang, ?T("All options can be changed in runtime by running 'ejabberdctl "
tr(Lang,
?T("All options can be changed in runtime by running 'ejabberdctl "
"reload-config' command. Configuration reload is atomic: either all options "
"are accepted and applied simultaneously or the new configuration is "
"refused without any impact on currently running configuration.")),
io_lib:nl(),
tr(Lang, ?T("Some options can be specified for particular virtual host(s) only "
tr(Lang,
?T("Some options can be specified for particular virtual host(s) only "
"using 'host_config' or 'append_host_config' options. Such options "
"are called 'local'. Examples are 'modules', 'auth_method' and 'default_db'. "
"The options that cannot be defined per virtual host are called 'global'. "
@ -327,14 +359,16 @@ man_header(Lang) ->
"section - ejabberd will refuse to load such configuration.")),
io_lib:nl(),
str:format(
tr(Lang, ?T("It is not recommended to write ejabberd.yml from scratch. Instead it is "
tr(Lang,
?T("It is not recommended to write ejabberd.yml from scratch. Instead it is "
"better to start from \"default\" configuration file available at ~s. "
"Once you get ejabberd running you can start changing configuration "
"options to meet your requirements.")),
[default_config_url()]),
io_lib:nl(),
str:format(
tr(Lang, ?T("Note that this document is intended to provide comprehensive description of "
tr(Lang,
?T("Note that this document is intended to provide comprehensive description of "
"all configuration options that can be consulted to understand the meaning "
"of a particular option, its format and possible values. It will be quite "
"hard to understand how to configure ejabberd by reading this document only "
@ -343,6 +377,7 @@ man_header(Lang) ->
[configuration_guide_url()]),
io_lib:nl()].
man_footer(Lang) ->
{Year, _, _} = date(),
[io_lib:nl(),
@ -353,7 +388,8 @@ man_footer(Lang) ->
"VERSION",
"-------",
str:format(
tr(Lang, ?T("This document describes the configuration file of ejabberd ~ts. "
tr(Lang,
?T("This document describes the configuration file of ejabberd ~ts. "
"Configuration options of other ejabberd versions "
"may differ significantly.")),
[ejabberd_config:version()]),
@ -379,6 +415,7 @@ man_footer(Lang) ->
"Copyright (c) 2002-" ++ integer_to_list(Year) ++
" https://www.process-one.net[ProcessOne]."].
tr(Lang, {Format, Args}) ->
unicode:characters_to_list(
str:format(
@ -387,12 +424,14 @@ tr(Lang, {Format, Args}) ->
tr(Lang, Txt) ->
unicode:characters_to_list(translate:translate(Lang, iolist_to_binary(Txt))).
tr_multi(Lang, Txt) when is_binary(Txt) ->
tr_multi(Lang, [Txt]);
tr_multi(Lang, {Format, Args}) ->
tr_multi(Lang, [{Format, Args}]);
tr_multi(Lang, Lines) when is_list(Lines) ->
[tr(Lang, Txt) || Txt <- Lines].
[ tr(Lang, Txt) || Txt <- Lines ].
write_man(AsciiData) ->
case file:get_cwd() of
@ -425,12 +464,14 @@ write_man(AsciiData) ->
[file:format_error(Reason)]))}
end.
have_a2x() ->
case os:find_executable("a2x") of
false -> false;
Path -> {true, Path}
end.
run_a2x(Cwd, AsciiDocFile) ->
case have_a2x() of
false ->
@ -445,6 +486,7 @@ run_a2x(Cwd, AsciiDocFile) ->
end
end.
warn_undocumented_modules(Docs) ->
lists:foreach(
fun({M, _, DocOpts, Backends, _}) ->
@ -452,8 +494,11 @@ warn_undocumented_modules(Docs) ->
lists:foreach(
fun({SubM, _, SubOpts}) ->
warn_undocumented_module(SubM, SubOpts)
end, Backends)
end, Docs).
end,
Backends)
end,
Docs).
warn_undocumented_module(M, DocOpts) ->
try M:mod_options(ejabberd_config:get_myname()) of
@ -471,11 +516,14 @@ warn_undocumented_module(M, DocOpts) ->
true ->
ok
end
end, Defaults)
catch _:undef ->
end,
Defaults)
catch
_:undef ->
ok
end.
warn_undocumented_options(Docs) ->
Opts = lists:flatmap(
fun(M) ->
@ -484,11 +532,14 @@ warn_undocumented_options(Docs) ->
lists:map(
fun({O, _}) -> O;
(O) when is_atom(O) -> O
end, Defaults)
catch _:undef ->
end,
Defaults)
catch
_:undef ->
[]
end
end, ejabberd_config:callback_modules(all)),
end,
ejabberd_config:callback_modules(all)),
lists:foreach(
fun(Opt) ->
case lists:keymember(Opt, 1, Docs) of
@ -497,19 +548,24 @@ warn_undocumented_options(Docs) ->
true ->
ok
end
end, Opts).
end,
Opts).
warn(Format, Args) ->
io:format(standard_error, "Warning: " ++ Format ++ "~n", Args).
strip_backend_suffix(M) ->
[H|T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
[H | T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
{list_to_atom(string:join(lists:reverse(T), "_")), list_to_atom(H)}.
default_config_url() ->
"<https://github.com/processone/ejabberd/blob/" ++
binary_to_list(binary:part(ejabberd_config:version(), {0,5})) ++
binary_to_list(binary:part(ejabberd_config:version(), {0, 5})) ++
"/ejabberd.yml.example>".
configuration_guide_url() ->
"<https://docs.ejabberd.im/admin/configuration>".

View file

@ -28,20 +28,12 @@
%% External exports
-export([start_link/0,
add/3,
add/4,
add/5,
delete/3,
delete/4,
delete/5,
subscribe/4,
subscribe/5,
unsubscribe/4,
unsubscribe/5,
run/2,
run/3,
run_fold/3,
run_fold/4]).
add/3, add/4, add/5,
delete/3, delete/4, delete/5,
subscribe/4, subscribe/5,
unsubscribe/4, unsubscribe/5,
run/2, run/3,
run_fold/3, run_fold/4]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
@ -50,14 +42,10 @@
handle_info/2,
terminate/2]).
-export(
[
get_tracing_options/3,
-export([get_tracing_options/3,
trace_off/3,
trace_on/5,human_readable_time_string/1
]
).
trace_on/5,
human_readable_time_string/1]).
-include("logger.hrl").
-include("ejabberd_stacktrace.hrl").
@ -69,17 +57,21 @@
-define(TRACE_HOOK_KEY, '$trace_hook').
-define(TIMING_KEY, '$trace_hook_timer').
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec add(atom(), fun(), integer()) -> ok.
%% @doc See add/4.
add(Hook, Function, Seq) when is_function(Function) ->
add(Hook, global, undefined, Function, Seq).
-spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom(), integer()) -> ok.
add(Hook, Host, Function, Seq) when is_function(Function) ->
add(Hook, Host, undefined, Function, Seq);
@ -89,10 +81,12 @@ add(Hook, Host, Function, Seq) when is_function(Function) ->
add(Hook, Module, Function, Seq) ->
add(Hook, global, Module, Function, Seq).
-spec add(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok.
add(Hook, Host, Module, Function, Seq) ->
gen_server:call(?MODULE, {add, Hook, Host, Module, Function, Seq}).
-spec subscribe(atom(), atom(), atom(), any()) -> ok.
%% @doc Add a subscriber to this hook.
%%
@ -117,15 +111,18 @@ add(Hook, Host, Module, Function, Seq) ->
subscribe(Hook, Module, Function, InitArg) ->
subscribe(Hook, global, Module, Function, InitArg).
-spec subscribe(atom(), binary() | global, atom(), atom(), any()) -> ok.
subscribe(Hook, Host, Module, Function, InitArg) ->
gen_server:call(?MODULE, {subscribe, Hook, Host, Module, Function, InitArg}).
-spec delete(atom(), fun(), integer()) -> ok.
%% @doc See del/4.
delete(Hook, Function, Seq) when is_function(Function) ->
delete(Hook, global, undefined, Function, Seq).
-spec delete(atom(), binary() | atom(), atom() | fun(), integer()) -> ok.
delete(Hook, Host, Function, Seq) when is_function(Function) ->
delete(Hook, Host, undefined, Function, Seq);
@ -135,17 +132,18 @@ delete(Hook, Host, Function, Seq) when is_function(Function) ->
delete(Hook, Module, Function, Seq) ->
delete(Hook, global, Module, Function, Seq).
-spec delete(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok.
delete(Hook, Host, Module, Function, Seq) ->
gen_server:call(?MODULE, {delete, Hook, Host, Module, Function, Seq}).
-spec unsubscribe(atom(), atom(), atom(), any()) -> ok.
%% @doc Removes a subscriber from this hook.
unsubscribe(Hook, Module, Function, InitArg) ->
unsubscribe(Hook, global, Module, Function, InitArg).
-spec unsubscribe(atom(), binary() | global, atom(), atom(), any()) -> ok.
unsubscribe(Hook, Host, Module, Function, InitArg) ->
gen_server:call(?MODULE, {unsubscribe, Hook, Host, Module, Function, InitArg}).
@ -157,6 +155,7 @@ unsubscribe(Hook, Host, Module, Function, InitArg) ->
run(Hook, Args) ->
run(Hook, global, Args).
-spec run(atom(), binary() | global, list()) -> ok.
run(Hook, Host, Args) ->
try ets:lookup(hooks, {Hook, Host}) of
@ -186,10 +185,12 @@ run(Hook, Host, Args) ->
end;
[] ->
ok
catch _:badarg ->
catch
_:badarg ->
ok
end.
-spec run_fold(atom(), T, list()) -> T.
%% @doc Run the calls (and subscribers) of this hook in order.
%% The arguments passed to the function are: [Val | Args].
@ -199,6 +200,7 @@ run(Hook, Host, Args) ->
run_fold(Hook, Val, Args) ->
run_fold(Hook, global, Val, Args).
-spec run_fold(atom(), binary() | global, T, list()) -> T.
run_fold(Hook, Host, Val, Args) ->
try ets:lookup(hooks, {Hook, Host}) of
@ -228,10 +230,12 @@ run_fold(Hook, Host, Val, Args) ->
end;
[] ->
Val
catch _:badarg ->
catch
_:badarg ->
Val
end.
get_tracing_options(Hook, Host, Pid) when Pid == erlang:self() ->
do_get_tracing_options(Hook, Host, erlang:get(?TRACE_HOOK_KEY));
get_tracing_options(Hook, Host, Pid) when erlang:is_pid(Pid) ->
@ -247,17 +251,17 @@ get_tracing_options(Hook, Host, Pid) when erlang:is_pid(Pid) ->
undefined
end.
trace_on(Hook, Host, Pid, #{}=Opts, Timeout) when Pid == erlang:self() ->
trace_on(Hook, Host, Pid, #{} = Opts, Timeout) when Pid == erlang:self() ->
do_trace_on(Hook, Host, Opts, Timeout);
trace_on(Hook, Host, Proc, #{}=Opts, Timeout) ->
trace_on(Hook, Host, Proc, #{} = Opts, Timeout) ->
try sys:replace_state(
Proc,
fun(State) ->
do_trace_on(Hook, Host, Opts, Timeout),
State
end,
15000
) of
15000) of
_ -> % process state
ok
catch
@ -265,6 +269,7 @@ trace_on(Hook, Host, Proc, #{}=Opts, Timeout) ->
{error, Reason}
end.
trace_off(Hook, Host, Pid) when Pid == erlang:self() ->
do_trace_off(Hook, Host);
trace_off(Hook, Host, Proc) ->
@ -274,8 +279,7 @@ trace_off(Hook, Host, Proc) ->
do_trace_off(Hook, Host),
State
end,
15000
) of
15000) of
_ -> % process state
ok
catch
@ -283,6 +287,7 @@ trace_off(Hook, Host, Proc) ->
{error, Reason}
end.
%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------
@ -290,6 +295,7 @@ init([]) ->
_ = ets:new(hooks, [named_table, {read_concurrency, true}]),
{ok, #state{}}.
handle_call({add, Hook, Host, Module, Function, Seq}, _From, State) ->
HookFormat = {Seq, Module, Function},
Reply = handle_add(Hook, Host, HookFormat),
@ -310,6 +316,7 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_add(atom(), atom(), hook()) -> ok.
handle_add(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
@ -328,6 +335,7 @@ handle_add(Hook, Host, El) ->
ok
end.
-spec handle_delete(atom(), atom(), hook()) -> ok.
handle_delete(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
@ -339,6 +347,7 @@ handle_delete(Hook, Host, El) ->
ok
end.
-spec handle_subscribe(atom(), atom(), subscriber()) -> ok.
handle_subscribe(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
@ -355,6 +364,7 @@ handle_subscribe(Hook, Host, El) ->
ok
end.
-spec handle_unsubscribe(atom(), atom(), subscriber()) -> ok.
handle_unsubscribe(Hook, Host, El) ->
case ets:lookup(hooks, {Hook, Host}) of
@ -365,20 +375,25 @@ handle_unsubscribe(Hook, Host, El) ->
ok
end.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
@ -396,6 +411,7 @@ run1([{_Seq, Module, Function} | Ls], Hook, Args) ->
run1(Ls, Hook, Args)
end.
-spec run_fold1([hook()], atom(), T, list()) -> T.
run_fold1([], _Hook, Val, _Args) ->
Val;
@ -412,6 +428,7 @@ run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) ->
run_fold1(Ls, Hook, NewVal, Args)
end.
-spec run1([hook()], atom(), list(), binary() | global, [subscriber()]) -> [subscriber()].
run1([], _Hook, _Args, _Host, SubscriberList) ->
SubscriberList;
@ -428,6 +445,7 @@ run1([{Seq, Module, Function} | Ls], Hook, Args, Host, SubscriberList) ->
run1(Ls, Hook, Args, Host, SubscriberList3)
end.
-spec run_fold1([hook()], atom(), T, list(), binary() | global, [subscriber()]) -> {T, [subscriber()]}.
run_fold1([], _Hook, Val, _Args, _Host, SubscriberList) ->
{Val, SubscriberList};
@ -446,28 +464,35 @@ run_fold1([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, SubscriberList)
run_fold1(Ls, Hook, NewVal, Args, Host, SubscriberList3)
end.
-spec safe_apply(atom(), atom(), atom() | fun(), list()) -> any().
safe_apply(Hook, Module, Function, Args) ->
?DEBUG("Running hook ~p: ~p:~p/~B",
[Hook, Module, Function, length(Args)]),
try if is_function(Function) ->
try
if
is_function(Function) ->
apply(Function, Args);
true ->
apply(Module, Function, Args)
end
catch ?EX_RULE(E, R, St) when E /= exit; R /= normal ->
catch
?EX_RULE(E, R, St) when E /= exit; R /= normal ->
Stack = ?EX_STACK(St),
?ERROR_MSG("Hook ~p crashed when running ~p:~p/~p:~n" ++
string:join(
["** ~ts"|
["** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(Args))]],
["** ~ts" | [ "** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(Args)) ]],
"~n"),
[Hook, Module, Function, length(Args),
misc:format_exception(2, E, R, Stack)|Args]),
[Hook,
Module,
Function,
length(Args),
misc:format_exception(2, E, R, Stack) | Args]),
'EXIT'
end.
-spec call_subscriber_list([subscriber()], binary() | global, atom(), {atom(), atom(), integer(), list()} | list(), subscriber_event(), [subscriber()]) -> any().
call_subscriber_list([], _Host, _Hook, _CallbackOrArgs, _Event, []) ->
[];
@ -480,24 +505,29 @@ call_subscriber_list([{Mod, Func, InitArg} | SubscriberList], Host, Hook, Callba
try apply(Mod, Func, SubscriberArgs) of
State ->
call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, [{Mod, Func, State} | Result])
catch ?EX_RULE(E, R, St) when E /= exit; R /= normal ->
catch
?EX_RULE(E, R, St) when E /= exit; R /= normal ->
Stack = ?EX_STACK(St),
?ERROR_MSG("Hook subscriber ~p crashed when running ~p:~p/~p:~n" ++
string:join(
["** ~ts"|
["** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(SubscriberArgs))]],
["** ~ts" | [ "** Arg " ++ integer_to_list(I) ++ " = ~p"
|| I <- lists:seq(1, length(SubscriberArgs)) ]],
"~n"),
[Hook, Mod, Func, length(SubscriberArgs),
misc:format_exception(2, E, R, Stack)|SubscriberArgs]),
[Hook,
Mod,
Func,
length(SubscriberArgs),
misc:format_exception(2, E, R, Stack) | SubscriberArgs]),
%% Do not append subscriber for next calls:
call_subscriber_list(SubscriberList, Host, Hook, CallbackOrArgs, Event, Result)
end.
%%%----------------------------------------------------------------------
%%% Internal tracing functions
%%%----------------------------------------------------------------------
do_trace_on(Hook, Host, Opts, Timeout) when erlang:is_list(Host) ->
do_trace_on(Hook, erlang:list_to_binary(Host), Opts, Timeout);
do_trace_on(Hook, Host, Opts, undefined) ->
@ -516,9 +546,9 @@ do_trace_on(Hook, Host, Opts, undefined) ->
#{} when Hook == all ->
% Remove other hooks by just adding all:
erlang:put(?TRACE_HOOK_KEY, #{all => #{Host => Opts}});
#{}=TraceHooksOpts when Host == <<"*">> -> % Want to trace a hook for all hosts
#{} = TraceHooksOpts when Host == <<"*">> -> % Want to trace a hook for all hosts
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => #{Host => Opts}});
#{}=TraceHooksOpts ->
#{} = TraceHooksOpts ->
case maps:get(Hook, TraceHooksOpts, #{}) of
#{<<"*">> := _} -> % Already tracing this hook for all hosts
ok;
@ -538,12 +568,12 @@ do_trace_on(Hook, Host, Opts, TimeoutSeconds) -> % Trace myself `Timeout` time
receive
{_, MonitorRef, _, _, _} ->
ok
after Timeout ->
after
Timeout ->
trace_off(Hook, Host, ParentPid)
end,
erlang:exit(normal)
end
) of
end) of
_ ->
do_trace_on(Hook, Host, Opts, undefined) % ok
catch
@ -551,6 +581,7 @@ do_trace_on(Hook, Host, Opts, TimeoutSeconds) -> % Trace myself `Timeout` time
{error, Reason}
end.
do_trace_off(Hook, Host) when erlang:is_list(Host) ->
do_trace_off(Hook, erlang:list_to_binary(Host));
do_trace_off(Hook, Host) ->
@ -568,7 +599,7 @@ do_trace_off(Hook, Host) ->
true ->
erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts2})
end;
#{}=TraceHooksOpts when Host == <<"*">> ->
#{} = TraceHooksOpts when Host == <<"*">> ->
% Remove tracing of this hook for all hosts:
TraceHooksOpts2 = maps:remove(Hook, TraceHooksOpts),
if
@ -578,9 +609,9 @@ do_trace_off(Hook, Host) ->
true ->
erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts2)
end;
#{}=TraceHooksOpts ->
#{} = TraceHooksOpts ->
case maps:get(Hook, TraceHooksOpts, undefined) of
#{}=HostOpts ->
#{} = HostOpts ->
NewHostOpts = maps:remove(Host, HostOpts),
if
NewHostOpts == #{} ->
@ -597,6 +628,7 @@ do_trace_off(Hook, Host) ->
end,
ok.
do_get_tracing_options(Hook, Host, MaybeMap) ->
case MaybeMap of
undefined ->
@ -605,7 +637,7 @@ do_get_tracing_options(Hook, Host, MaybeMap) ->
Opts;
#{all := HostOpts} -> % Tracing all hooks for some hosts
maps:get(Host, HostOpts, undefined);
#{}=TraceHooksOpts ->
#{} = TraceHooksOpts ->
HostOpts = maps:get(Hook, TraceHooksOpts, #{}),
case maps:get(Host, HostOpts, undefined) of
undefined ->
@ -615,6 +647,7 @@ do_get_tracing_options(Hook, Host, MaybeMap) ->
end
end.
run2([], Hook, Args, Host, Opts, SubscriberList) ->
foreach_stop_hook_tracing(Opts, Hook, Host, Args, undefined),
SubscriberList;
@ -634,6 +667,7 @@ run2([{Seq, Module, Function} | Ls], Hook, Args, Host, TracingOpts, SubscriberLi
run2(Ls, Hook, Args, Host, TracingOpts, SubscriberList3)
end.
run_fold2([], Hook, Val, Args, Host, Opts, SubscriberList) ->
fold_stop_hook_tracing(Opts, Hook, Host, [Val | Args], undefined),
{Val, SubscriberList};
@ -656,30 +690,39 @@ run_fold2([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, TracingOpts, Su
run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts, SubscriberList3)
end.
foreach_start_hook_tracing(TracingOpts, Hook, Host, Args) ->
run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], foreach).
foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) ->
run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], foreach).
foreach_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) ->
run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], foreach).
foreach_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) ->
run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], foreach).
fold_start_hook_tracing(TracingOpts, Hook, Host, Args) ->
run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], fold).
fold_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) ->
run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], fold).
fold_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) ->
run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], fold).
fold_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) ->
run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], fold).
run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) ->
EventHandlerList = maps:get(event_handler_list, TracingOpts, default_tracing_event_handler_list()),
EventHandlerOpts = maps:get(event_handler_options, TracingOpts, #{}),
@ -687,13 +730,11 @@ run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) ->
erlang:is_list(EventHandlerList) ->
lists:foreach(
fun(EventHandler) ->
try
if
try if
erlang:is_function(EventHandler) ->
erlang:apply(
EventHandler,
[Event, EventArgs, RunType, Hook, Host, EventHandlerOpts, TracingOpts]
);
[Event, EventArgs, RunType, Hook, Host, EventHandlerOpts, TracingOpts]);
true ->
EventHandler:handle_hook_tracing_event(
Event,
@ -702,10 +743,8 @@ run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) ->
Hook,
Host,
EventHandlerOpts,
TracingOpts
)
end
of
TracingOpts)
end of
_ ->
ok
catch
@ -713,21 +752,21 @@ run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) ->
Stack = ?EX_STACK(St),
?ERROR_MSG(
"(~0p|~ts|~0p) Tracing event '~0p' handler exception(~0p): ~0p: ~0p",
[Hook, Host, erlang:self(), EventHandler, E, R, Stack]
),
[Hook, Host, erlang:self(), EventHandler, E, R, Stack]),
ok
end
end,
EventHandlerList
); % ok
EventHandlerList); % ok
true ->
?ERROR_MSG("(~0p|~ts|~0p) Bad event handler list: ~0p", [Hook, Host, erlang:self(), EventHandlerList]),
ok
end.
default_tracing_event_handler_list() ->
[fun tracing_timing_event_handler/7].
tracing_timing_event_handler(start_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
HookStart = erlang:system_time(nanosecond),
% Generate new event:
@ -737,12 +776,11 @@ tracing_timing_event_handler(stop_hook, EventArgs, RunType, Hook, Host, _, Traci
TimingMap = #{} = erlang:get(?TIMING_KEY),
{HookStart, CallbackList} = maps:get({Hook, Host}, TimingMap),
{CallbackListTiming, CallbackListTotal} = lists:foldl(
fun({_, _, _, CallbackStart, CallbackStop}=CallbackTimingInfo, {CallbackListTimingX, Total}) ->
fun({_, _, _, CallbackStart, CallbackStop} = CallbackTimingInfo, {CallbackListTimingX, Total}) ->
{CallbackListTimingX ++ [CallbackTimingInfo], Total + (CallbackStop - CallbackStart)}
end,
{[], 0},
CallbackList
),
CallbackList),
% Generate new event:
run_event_handlers(
TracingOpts,
@ -750,8 +788,7 @@ tracing_timing_event_handler(stop_hook, EventArgs, RunType, Hook, Host, _, Traci
Host,
stop_hook_timing,
EventArgs ++ [HookStart, HookStop, CallbackListTiming, CallbackListTotal],
RunType
);
RunType);
tracing_timing_event_handler(start_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) ->
CallbackStart = erlang:system_time(nanosecond),
% Generate new event:
@ -766,27 +803,24 @@ tracing_timing_event_handler(stop_callback, EventArgs, RunType, Hook, Host, _, T
Host,
stop_callback_timing,
EventArgs ++ [CallbackStart, CallbackStop],
RunType
),
RunType),
ok;
tracing_timing_event_handler(start_hook_timing, [_, HookStart], RunType, Hook, Host, EventHandlerOpts, _) ->
tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing started\n", [Hook, Host, erlang:self(), RunType]),
case erlang:get(?TIMING_KEY) of
#{}=TimingMap ->
#{} = TimingMap ->
erlang:put(?TIMING_KEY, TimingMap#{{Hook, Host} => {HookStart, []}});
_ ->
erlang:put(?TIMING_KEY, #{{Hook, Host} => {HookStart, []}})
end,
ok;
tracing_timing_event_handler(
stop_hook_timing,
tracing_timing_event_handler(stop_hook_timing,
[_, _, HookStart, HookStop, CallbackListTiming, CallbackListTotal],
RunType,
Hook,
Host,
EventHandlerOpts,
_
) ->
_) ->
if
erlang:length(CallbackListTiming) < 2 -> % We don't need sorted timing result
ok;
@ -794,39 +828,31 @@ tracing_timing_event_handler(
CallbackListTimingText =
lists:foldl(
fun({Mod, Func, Arity, Diff}, CallbackListTimingText) ->
CallbackListTimingText
++ "\n\t"
++ mfa_string({Mod, Func, Arity})
++ " -> "
++ human_readable_time_string(Diff)
CallbackListTimingText ++
"\n\t" ++
mfa_string({Mod, Func, Arity}) ++
" -> " ++
human_readable_time_string(Diff)
end,
"",
lists:keysort(
4,
[
{Mod, Func, Arity, CallbackStop - CallbackStart} ||
{Mod, Func, Arity, CallbackStart, CallbackStop} <- CallbackListTiming
]
)
),
[ {Mod, Func, Arity, CallbackStop - CallbackStart}
|| {Mod, Func, Arity, CallbackStart, CallbackStop} <- CallbackListTiming ])),
tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) All callbacks took ~ts to run. Sorted running time:"
++ CallbackListTimingText
++ "\n",
[Hook, Host, erlang:self(), RunType, human_readable_time_string(CallbackListTotal)]
),
"(~0p|~ts|~0p|~0p) All callbacks took ~ts to run. Sorted running time:" ++
CallbackListTimingText ++
"\n",
[Hook, Host, erlang:self(), RunType, human_readable_time_string(CallbackListTotal)]),
tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) Time calculations for all callbacks took ~ts\n",
[
Hook,
[Hook,
Host,
erlang:self(),
RunType,
human_readable_time_string((HookStop - HookStart) - CallbackListTotal)
]
)
human_readable_time_string((HookStop - HookStart) - CallbackListTotal)])
end,
tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing stopped\n", [Hook, Host, erlang:self(), RunType]),
TimingMap = #{} = erlang:get(?TIMING_KEY),
@ -845,43 +871,37 @@ tracing_timing_event_handler(start_callback_timing, [Mod, Func, Args, _, Callbac
?TIMING_KEY,
TimingMap#{
{Hook, Host} => {HookStart, [{Mod, Func, erlang:length(Args), CallbackStart} | Callbacks]}
}
),
}),
ok;
tracing_timing_event_handler(
stop_callback_timing,
tracing_timing_event_handler(stop_callback_timing,
[Mod, Func, _, _, _, CallbackStart, CallbackStop],
RunType,
Hook,
Host,
EventHandlerOpts,
_
) ->
_) ->
TimingMap = #{} = erlang:get(?TIMING_KEY),
{HookStart, [{Mod, Func, Arity, CallbackStart} | Callbacks]} = maps:get({Hook, Host}, TimingMap),
maps:get(output_for_each_callback, maps:get(timing, EventHandlerOpts, #{}), false) andalso tracing_output(
EventHandlerOpts,
"(~0p|~ts|~0p|~0p) "
++ mfa_string({Mod, Func, Arity})
++ " took "
++ human_readable_time_string(CallbackStop - CallbackStart)
++ "\n",
[Hook, Host, erlang:self(), RunType]
),
"(~0p|~ts|~0p|~0p) " ++
mfa_string({Mod, Func, Arity}) ++
" took " ++
human_readable_time_string(CallbackStop - CallbackStart) ++
"\n",
[Hook, Host, erlang:self(), RunType]),
erlang:put(
?TIMING_KEY,
TimingMap#{
{Hook, Host} => {HookStart, [{Mod, Func, Arity, CallbackStart, CallbackStop} | Callbacks]}
}
),
}),
ok;
tracing_timing_event_handler(_, _, _, _, _, _, _) ->
ok.
tracing_output(#{output_function := OutputF}, Text, Args) ->
try
OutputF(Text, Args)
of
try OutputF(Text, Args) of
_ ->
ok
catch
@ -901,11 +921,13 @@ tracing_output(#{output_log_level := Output}, Text, Args) ->
tracing_output(Opts, Text, Args) ->
tracing_output(Opts#{output_log_level => info}, Text, Args).
mfa_string({_, Fun, _}) when erlang:is_function(Fun) ->
io_lib:format("~0p", [Fun]);
mfa_string({Mod, Func, Arity}) ->
erlang:atom_to_list(Mod) ++ ":" ++ erlang:atom_to_list(Func) ++ "/" ++ erlang:integer_to_list(Arity).
human_readable_time_string(TimeNS) ->
{Time, Unit, Decimals} =
if

View file

@ -29,20 +29,28 @@
-author('alexey@process-one.net').
%% External exports
-export([start/3, start_link/3,
accept/1, receive_headers/1, recv_file/2,
listen_opt_type/1, listen_options/0,
-export([start/3,
start_link/3,
accept/1,
receive_headers/1,
recv_file/2,
listen_opt_type/1,
listen_options/0,
apply_custom_headers/2]).
-export([init/3]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_stacktrace.hrl").
-include_lib("kernel/include/file.hrl").
-record(state, {sockmod,
-record(state, {
sockmod,
socket,
request_method,
request_version,
@ -89,19 +97,24 @@
-define(SEND_BUF, 65536).
-define(MAX_POST_SIZE, 20971520). %% 20Mb
start(SockMod, Socket, Opts) ->
{ok,
proc_lib:spawn(ejabberd_http, init,
proc_lib:spawn(ejabberd_http,
init,
[SockMod, Socket, Opts])}.
start_link(SockMod, Socket, Opts) ->
{ok,
proc_lib:spawn_link(ejabberd_http, init,
proc_lib:spawn_link(ejabberd_http,
init,
[SockMod, Socket, Opts])}.
init(SockMod, Socket, Opts) ->
TLSEnabled = proplists:get_bool(tls, Opts),
TLSOpts1 = lists:filter(fun ({ciphers, _}) -> true;
TLSOpts1 = lists:filter(fun({ciphers, _}) -> true;
({dhfile, _}) -> true;
({cafile, _}) -> true;
({protocol_options, _}) -> true;
@ -115,10 +128,11 @@ init(SockMod, Socket, Opts) ->
TLSOpts3 = case ejabberd_pkix:get_certfile(
ejabberd_config:get_myname()) of
error -> TLSOpts2;
{ok, CertFile} -> [{certfile, CertFile}|TLSOpts2]
{ok, CertFile} -> [{certfile, CertFile} | TLSOpts2]
end,
TLSOpts = [verify_none | TLSOpts3],
{SockMod1, Socket1} = if TLSEnabled ->
{SockMod1, Socket1} = if
TLSEnabled ->
inet:setopts(Socket, [{recbuf, ?RECV_BUF}]),
{ok, TLSSocket} = fast_tls:tcp_to_tls(Socket,
TLSOpts),
@ -134,23 +148,27 @@ init(SockMod, Socket, Opts) ->
CustomHeaders = proplists:get_value(custom_headers, Opts, []),
AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts),
State = #state{sockmod = SockMod1,
State = #state{
sockmod = SockMod1,
socket = Socket1,
custom_headers = CustomHeaders,
options = Opts,
allow_unencrypted_sasl2 = AllowUnencryptedSasl2,
request_handlers = RequestHandlers,
sock_peer_name = SockPeer,
addr_re = RE},
addr_re = RE
},
try receive_headers(State) of
V -> V
catch
{error, _} -> State
end.
accept(_Pid) ->
ok.
send_text(_State, none) ->
ok;
send_text(State, Text) ->
@ -165,6 +183,7 @@ send_text(State, Text) ->
exit(normal)
end.
send_file(State, Fd, Size, FileName) ->
try
case State#state.sockmod of
@ -180,8 +199,10 @@ send_file(State, Fd, Size, FileName) ->
ok
end
end
catch _:{case_clause, {error, Why}} ->
if Why /= closed ->
catch
_:{case_clause, {error, Why}} ->
if
Why /= closed ->
?WARNING_MSG("Failed to read ~ts: ~ts",
[FileName, file_format_error(Why)]),
exit(normal);
@ -190,6 +211,7 @@ send_file(State, Fd, Size, FileName) ->
end
end.
receive_headers(#state{trail = Trail} = State) ->
SockMod = State#state.sockmod,
Socket = State#state.socket,
@ -206,10 +228,13 @@ receive_headers(#state{trail = Trail} = State) ->
parse_headers(State#state{trail = <<Trail/binary, D/binary>>})
end.
parse_headers(#state{trail = <<>>} = State) ->
receive_headers(State);
parse_headers(#state{request_method = Method,
trail = Data} =
parse_headers(#state{
request_method = Method,
trail = Data
} =
State) ->
PktType = case Method of
undefined -> http_bin;
@ -228,6 +253,7 @@ parse_headers(#state{request_method = Method,
ok
end.
process_header(State, Data) ->
SockMod = State#state.sockmod,
Socket = State#state.socket,
@ -244,45 +270,62 @@ process_header(State, Data) ->
{abs_path, P};
_ -> Uri
end,
State#state{request_method = Method,
request_version = Version, request_path = Path,
request_keepalive = KeepAlive};
State#state{
request_method = Method,
request_version = Version,
request_path = Path,
request_keepalive = KeepAlive
};
{ok, {http_header, _, 'Connection' = Name, _, Conn}} ->
KeepAlive1 = case misc:tolower(Conn) of
<<"keep-alive">> -> true;
<<"close">> -> false;
_ -> State#state.request_keepalive
end,
State#state{request_keepalive = KeepAlive1,
request_headers = add_header(Name, Conn, State)};
State#state{
request_keepalive = KeepAlive1,
request_headers = add_header(Name, Conn, State)
};
{ok,
{http_header, _, 'Authorization' = Name, _, Auth}} ->
State#state{request_auth = parse_auth(Auth),
request_headers = add_header(Name, Auth, State)};
State#state{
request_auth = parse_auth(Auth),
request_headers = add_header(Name, Auth, State)
};
{ok,
{http_header, _, 'Content-Length' = Name, _, SLen}} ->
case catch binary_to_integer(SLen) of
Len when is_integer(Len) ->
State#state{request_content_length = Len,
request_headers = add_header(Name, SLen, State)};
State#state{
request_content_length = Len,
request_headers = add_header(Name, SLen, State)
};
_ -> State
end;
{ok,
{http_header, _, 'Accept-Language' = Name, _, Langs}} ->
State#state{request_lang = parse_lang(Langs),
request_headers = add_header(Name, Langs, State)};
State#state{
request_lang = parse_lang(Langs),
request_headers = add_header(Name, Langs, State)
};
{ok, {http_header, _, 'Host' = Name, _, Value}} ->
{Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value),
State#state{request_host = ejabberd_config:resolve_host_alias(Host),
State#state{
request_host = ejabberd_config:resolve_host_alias(Host),
request_port = Port,
request_tp = TP,
request_headers = add_header(Name, Value, State)};
request_headers = add_header(Name, Value, State)
};
{ok, {http_header, _, Name, _, Value}} when is_binary(Name) ->
State#state{request_headers =
add_header(normalize_header_name(Name), Value, State)};
State#state{
request_headers =
add_header(normalize_header_name(Name), Value, State)
};
{ok, {http_header, _, Name, _, Value}} ->
State#state{request_headers =
add_header(Name, Value, State)};
State#state{
request_headers =
add_header(Name, Value, State)
};
{ok, http_eoh} when State#state.request_host == undefined;
State#state.request_host == error ->
{State1, Out} = process_request(State),
@ -290,43 +333,53 @@ process_header(State, Data) ->
process_header(State, {ok, {http_error, <<>>}});
{ok, http_eoh} ->
?DEBUG("(~w) http query: ~w ~p~n",
[State#state.socket, State#state.request_method,
[State#state.socket,
State#state.request_method,
element(2, State#state.request_path)]),
{State3, Out} = process_request(State),
send_text(State3, Out),
case State3#state.request_keepalive of
true ->
#state{sockmod = SockMod, socket = Socket,
#state{
sockmod = SockMod,
socket = Socket,
trail = State3#state.trail,
options = State#state.options,
custom_headers = State#state.custom_headers,
request_handlers = State#state.request_handlers,
addr_re = State#state.addr_re};
addr_re = State#state.addr_re
};
_ ->
#state{end_of_request = true,
#state{
end_of_request = true,
trail = State3#state.trail,
options = State#state.options,
custom_headers = State#state.custom_headers,
request_handlers = State#state.request_handlers,
addr_re = State#state.addr_re}
addr_re = State#state.addr_re
}
end;
_ ->
#state{end_of_request = true,
#state{
end_of_request = true,
options = State#state.options,
custom_headers = State#state.custom_headers,
request_handlers = State#state.request_handlers,
addr_re = State#state.addr_re}
addr_re = State#state.addr_re
}
end.
add_header(Name, Value, State)->
add_header(Name, Value, State) ->
[{Name, Value} | State#state.request_headers].
get_transfer_protocol(RE, SockMod, HostPort) ->
{Proto, DefPort} = case SockMod of
gen_tcp -> {http, 80};
fast_tls -> {https, 443}
end,
{Host, Port} = case re:run(HostPort, RE, [{capture,[1,2,3],binary}]) of
{Host, Port} = case re:run(HostPort, RE, [{capture, [1, 2, 3], binary}]) of
nomatch ->
{error, DefPort};
{match, [<<>>, H, <<>>]} ->
@ -341,10 +394,12 @@ get_transfer_protocol(RE, SockMod, HostPort) ->
{Host, Port, Proto}.
%% XXX bard: search through request handlers looking for one that
%% matches the requested URL path, and pass control to it. If none is
%% found, answer with HTTP 404.
process([], _) -> ejabberd_web:error(not_found);
process(Handlers, Request) ->
{HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} =
@ -356,7 +411,7 @@ process(Handlers, Request) ->
end,
case (lists:prefix(HandlerPathPrefix, Request#request.path) or
(HandlerPathPrefix==Request#request.path)) of
(HandlerPathPrefix == Request#request.path)) of
true ->
?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]),
%% LocalPath is the path "local to the handler", i.e. if
@ -385,8 +440,11 @@ process(Handlers, Request) ->
process(HandlersLeft, Request)
end.
extract_path_query(#state{request_method = Method,
request_path = {abs_path, Path}} = State)
extract_path_query(#state{
request_method = Method,
request_path = {abs_path, Path}
} = State)
when Method =:= 'GET' orelse
Method =:= 'HEAD' orelse
Method =:= 'DELETE' orelse Method =:= 'OPTIONS' ->
@ -401,13 +459,15 @@ extract_path_query(#state{request_method = Method,
end,
{State, {LPath, LQuery, <<"">>, Path}}
end;
extract_path_query(#state{request_method = Method,
extract_path_query(#state{
request_method = Method,
request_path = {abs_path, Path},
request_content_length = Len,
trail = Trail,
sockmod = _SockMod,
socket = _Socket} = State)
when (Method =:= 'POST' orelse Method =:= 'PUT') andalso Len>0 ->
socket = _Socket
} = State)
when (Method =:= 'POST' orelse Method =:= 'PUT') andalso Len > 0 ->
case catch url_decode_q_split_normalize(Path) of
{'EXIT', Error} ->
?DEBUG("Error decoding URL '~p': ~p", [Path, Error]),
@ -432,15 +492,27 @@ extract_path_query(#state{request_method = Method,
extract_path_query(State) ->
{State, false}.
process_request(#state{request_host = undefined,
custom_headers = CustomHeaders} = State) ->
{State, make_text_output(State, 400, CustomHeaders,
process_request(#state{
request_host = undefined,
custom_headers = CustomHeaders
} = State) ->
{State,
make_text_output(State,
400,
CustomHeaders,
<<"Missing Host header">>)};
process_request(#state{request_host = error,
custom_headers = CustomHeaders} = State) ->
{State, make_text_output(State, 400, CustomHeaders,
process_request(#state{
request_host = error,
custom_headers = CustomHeaders
} = State) ->
{State,
make_text_output(State,
400,
CustomHeaders,
<<"Malformed Host header">>)};
process_request(#state{request_method = Method,
process_request(#state{
request_method = Method,
request_auth = Auth,
request_lang = Lang,
request_version = Version,
@ -454,7 +526,8 @@ process_request(#state{request_method = Method,
request_content_length = Length,
request_headers = RequestHeaders,
request_handlers = RequestHandlers,
custom_headers = CustomHeaders} = State) ->
custom_headers = CustomHeaders
} = State) ->
case proplists:get_value(<<"Expect">>, RequestHeaders, <<>>) of
<<"100-", _/binary>> when Version == {1, 1} ->
send_text(State, <<"HTTP/1.1 100 Continue\r\n\r\n">>);
@ -482,7 +555,8 @@ process_request(#state{request_method = Method,
end,
XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
IP = analyze_ip_xff(IPHere, XFF),
Request = #request{method = Method,
Request = #request{
method = Method,
path = LPath,
raw_path = RawPath,
q = LQuery,
@ -497,7 +571,8 @@ process_request(#state{request_method = Method,
tp = TP,
opts = Options,
headers = RequestHeaders,
ip = IP},
ip = IP
},
RequestHandlers1 = ejabberd_hooks:run_fold(
http_request_handlers, RequestHandlers, [Host, Request]),
Res = case process(RequestHandlers1, Request) of
@ -505,33 +580,46 @@ process_request(#state{request_method = Method,
make_xhtml_output(State, 200, CustomHeaders, El);
{Status, Headers, El}
when is_record(El, xmlel) ->
make_xhtml_output(State, Status,
apply_custom_headers(Headers, CustomHeaders), El);
make_xhtml_output(State,
Status,
apply_custom_headers(Headers, CustomHeaders),
El);
Output when is_binary(Output) or is_list(Output) ->
make_text_output(State, 200, CustomHeaders, Output);
{Status, Headers, Output}
when is_binary(Output) or is_list(Output) ->
make_text_output(State, Status,
apply_custom_headers(Headers, CustomHeaders), Output);
make_text_output(State,
Status,
apply_custom_headers(Headers, CustomHeaders),
Output);
{Status, Headers, {file, FileName}} ->
make_file_output(State, Status, Headers, FileName);
{Status, Reason, Headers, Output}
when is_binary(Output) or is_list(Output) ->
make_text_output(State, Status, Reason,
apply_custom_headers(Headers, CustomHeaders), Output);
make_text_output(State,
Status,
Reason,
apply_custom_headers(Headers, CustomHeaders),
Output);
_ ->
none
end,
{State2#state{trail = <<>>}, Res}
end.
make_bad_request(State) ->
make_xhtml_output(State, 400, State#state.custom_headers,
ejabberd_web:make_xhtml([#xmlel{name = <<"h1">>,
make_xhtml_output(State,
400,
State#state.custom_headers,
ejabberd_web:make_xhtml([#xmlel{
name = <<"h1">>,
attrs = [],
children =
[{xmlcdata,
<<"400 Bad Request">>}]}])).
<<"400 Bad Request">>}]
}])).
analyze_ip_xff(IP, []) -> IP;
analyze_ip_xff({IPLast, Port}, XFF) ->
@ -539,8 +627,7 @@ analyze_ip_xff({IPLast, Port}, XFF) ->
[misc:ip_to_list(IPLast)],
TrustedProxies = ejabberd_option:trusted_proxies(),
IPClient = case is_ipchain_trusted(ProxiesIPs,
TrustedProxies)
of
TrustedProxies) of
true ->
case inet_parse:address(binary_to_list(ClientIP)) of
{ok, IPFirst} ->
@ -554,6 +641,7 @@ analyze_ip_xff({IPLast, Port}, XFF) ->
end,
{IPClient, Port}.
is_ipchain_trusted([], _) -> false;
is_ipchain_trusted(_UserIPs, all) -> true;
is_ipchain_trusted(UserIPs, Masks) ->
@ -564,18 +652,26 @@ is_ipchain_trusted(UserIPs, Masks) ->
lists:any(
fun({Mask, MaskLen}) ->
misc:match_ip_mask(IP2, Mask, MaskLen)
end, Masks);
end,
Masks);
_ ->
false
end
end, UserIPs).
end,
UserIPs).
recv_data(#state{request_content_length = Len}) when Len >= ?MAX_POST_SIZE ->
error;
recv_data(#state{request_content_length = Len, trail = Trail,
sockmod = SockMod, socket = Socket}) ->
recv_data(#state{
request_content_length = Len,
trail = Trail,
sockmod = SockMod,
socket = Socket
}) ->
NewLen = Len - byte_size(Trail),
if NewLen > 0 ->
if
NewLen > 0 ->
case SockMod:recv(Socket, NewLen, 60000) of
{ok, Data} -> {ok, <<Trail/binary, Data/binary>>};
{error, _} -> error
@ -584,8 +680,14 @@ recv_data(#state{request_content_length = Len, trail = Trail,
{ok, Trail}
end.
recv_file(#request{length = Len, data = Trail,
sockmod = SockMod, socket = Socket}, Path) ->
recv_file(#request{
length = Len,
data = Trail,
sockmod = SockMod,
socket = Socket
},
Path) ->
case file:open(Path, [write, exclusive, raw]) of
{ok, Fd} ->
Res = case file:write(Fd, Trail) of
@ -605,6 +707,7 @@ recv_file(#request{length = Len, data = Trail,
Err
end.
do_recv_file(0, _SockMod, _Socket, _Fd) ->
ok;
do_recv_file(Len, SockMod, Socket, Fd) ->
@ -613,7 +716,7 @@ do_recv_file(Len, SockMod, Socket, Fd) ->
{ok, Data} ->
case file:write(Fd, Data) of
ok ->
do_recv_file(Len-size(Data), SockMod, Socket, Fd);
do_recv_file(Len - size(Data), SockMod, Socket, Fd);
{error, _} = Err ->
Err
end;
@ -621,8 +724,10 @@ do_recv_file(Len, SockMod, Socket, Fd) ->
{error, closed}
end.
make_headers(State, Status, Reason, Headers, Data) ->
Len = if is_integer(Data) -> Data;
Len = if
is_integer(Data) -> Data;
true -> iolist_size(Data)
end,
Headers1 = [{<<"Content-Length">>, integer_to_binary(Len)} | Headers],
@ -630,8 +735,7 @@ make_headers(State, Status, Reason, Headers, Data) ->
{_, _} ->
Headers1;
false ->
[{<<"Content-Type">>, <<"text/html; charset=utf-8">>}
| Headers1]
[{<<"Content-Type">>, <<"text/html; charset=utf-8">>} | Headers1]
end,
HeadersOut = case {State#state.request_version,
State#state.request_keepalive} of
@ -645,16 +749,19 @@ make_headers(State, Status, Reason, Headers, Data) ->
{1, 1} -> <<"HTTP/1.1 ">>;
_ -> <<"HTTP/1.0 ">>
end,
H = [[Attr, <<": ">>, Val, <<"\r\n">>] || {Attr, Val} <- HeadersOut],
H = [ [Attr, <<": ">>, Val, <<"\r\n">>] || {Attr, Val} <- HeadersOut ],
NewReason = case Reason of
<<"">> -> code_to_phrase(Status);
_ -> Reason
end,
SL = [Version,
integer_to_binary(Status), <<" ">>,
NewReason, <<"\r\n">>],
integer_to_binary(Status),
<<" ">>,
NewReason,
<<"\r\n">>],
[SL, H, <<"\r\n">>].
make_xhtml_output(State, Status, Headers, XHTML) ->
Data = case State#state.request_method of
'HEAD' -> <<"">>;
@ -668,9 +775,11 @@ make_xhtml_output(State, Status, Headers, XHTML) ->
EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Data),
[EncodedHdrs, Data].
make_text_output(State, Status, Headers, Text) ->
make_text_output(State, Status, <<"">>, Headers, Text).
make_text_output(State, Status, Reason, Headers, Text) ->
Data = iolist_to_binary(Text),
Data2 = case State#state.request_method of
@ -680,6 +789,7 @@ make_text_output(State, Status, Reason, Headers, Text) ->
EncodedHdrs = make_headers(State, Status, Reason, Headers, Data2),
[EncodedHdrs, Data2].
make_file_output(State, Status, Headers, FileName) ->
case file:read_file_info(FileName) of
{ok, #file_info{size = Size}} when State#state.request_method == 'HEAD' ->
@ -703,24 +813,28 @@ make_file_output(State, Status, Headers, FileName) ->
make_text_output(State, 404, Reason, [], <<>>)
end.
parse_lang(Langs) ->
case str:tokens(Langs, <<",; ">>) of
[First | _] -> First;
[] -> <<"en">>
end.
file_format_error(Reason) ->
case file:format_error(Reason) of
"unknown POSIX error" -> atom_to_list(Reason);
Text -> Text
end.
url_decode_q_split_normalize(Path) ->
{NPath, Query} = url_decode_q_split(Path),
LPath = normalize_path([NPE
|| NPE <- str:tokens(misc:uri_decode(NPath), <<"/">>)]),
LPath = normalize_path([ NPE
|| NPE <- str:tokens(misc:uri_decode(NPath), <<"/">>) ]),
{LPath, Query}.
% Code below is taken (with some modifications) from the yaws webserver, which
% is distributed under the following license:
%
@ -734,10 +848,12 @@ url_decode_q_split_normalize(Path) ->
% 2. Redistributions in binary form must reproduce the above copyright
% notice as well as this list of conditions.
%% @doc Split the URL and return {Path, QueryPart}
url_decode_q_split(Path) ->
url_decode_q_split(Path, <<>>).
url_decode_q_split(<<$?, T/binary>>, Acc) ->
%% Don't decode the query string here, that is parsed separately.
{path_norm_reverse(Acc), T};
@ -746,9 +862,11 @@ url_decode_q_split(<<H, T/binary>>, Acc) when H /= 0 ->
url_decode_q_split(<<>>, Ack) ->
{path_norm_reverse(Ack), <<>>}.
path_norm_reverse(<<"/", T/binary>>) -> start_dir(0, <<"/">>, T);
path_norm_reverse(T) -> start_dir(0, <<"">>, T).
start_dir(N, Path, <<"..">>) -> rest_dir(N, Path, <<"">>);
start_dir(N, Path, <<"/", T/binary>>) -> start_dir(N, Path, T);
start_dir(N, Path, <<"./", T/binary>>) -> start_dir(N, Path, T);
@ -756,6 +874,7 @@ start_dir(N, Path, <<"../", T/binary>>) ->
start_dir(N + 1, Path, T);
start_dir(N, Path, T) -> rest_dir(N, Path, T).
rest_dir(_N, Path, <<>>) ->
case Path of
<<>> -> <<"/">>;
@ -769,6 +888,7 @@ rest_dir(0, Path, <<H, T/binary>>) ->
rest_dir(0, <<H, Path/binary>>, T);
rest_dir(N, Path, <<_H, T/binary>>) -> rest_dir(N, Path, T).
code_to_phrase(100) -> <<"Continue">>;
code_to_phrase(101) -> <<"Switching Protocols ">>;
code_to_phrase(200) -> <<"OK">>;
@ -814,6 +934,7 @@ code_to_phrase(503) -> <<"Service Unavailable">>;
code_to_phrase(504) -> <<"Gateway Timeout">>;
code_to_phrase(505) -> <<"HTTP Version Not Supported">>.
-spec parse_auth(binary()) -> {binary(), binary()} | {oauth, binary(), []} | invalid.
parse_auth(<<"Basic ", Auth64/binary>>) ->
try base64:decode(Auth64) of
@ -825,7 +946,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
_ ->
invalid
end
catch _:_ ->
catch
_:_ ->
invalid
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
@ -834,16 +956,21 @@ parse_auth(<<"Bearer ", SToken/binary>>) ->
parse_auth(<<_/binary>>) ->
invalid.
parse_urlencoded(S) ->
parse_urlencoded(S, nokey, <<>>, key).
parse_urlencoded(<<$%, Hi, Lo, Tail/binary>>, Last, Cur,
parse_urlencoded(<<$%, Hi, Lo, Tail/binary>>,
Last,
Cur,
State) ->
Hex = list_to_integer([Hi, Lo], 16),
parse_urlencoded(Tail, Last, <<Cur/binary, Hex>>, State);
parse_urlencoded(<<$&, Tail/binary>>, _Last, Cur, key) ->
[{Cur, <<"">>} | parse_urlencoded(Tail,
nokey, <<>>,
nokey,
<<>>,
key)]; %% cont keymode
parse_urlencoded(<<$&, Tail/binary>>, Last, Cur, value) ->
V = {Last, Cur},
@ -851,7 +978,9 @@ parse_urlencoded(<<$&, Tail/binary>>, Last, Cur, value) ->
parse_urlencoded(<<$+, Tail/binary>>, Last, Cur, State) ->
parse_urlencoded(Tail, Last, <<Cur/binary, $\s>>, State);
parse_urlencoded(<<$=, Tail/binary>>, _Last, Cur, key) ->
parse_urlencoded(Tail, Cur, <<>>,
parse_urlencoded(Tail,
Cur,
<<>>,
value); %% change mode
parse_urlencoded(<<H, Tail/binary>>, Last, Cur, State) ->
parse_urlencoded(Tail, Last, <<Cur/binary, H>>, State);
@ -859,6 +988,7 @@ parse_urlencoded(<<>>, Last, Cur, _State) ->
[{Last, Cur}];
parse_urlencoded(undefined, _, _, _) -> [].
apply_custom_headers(Headers, CustomHeaders) ->
{Doctype, Headers2} = case Headers -- [html] of
Headers -> {[], Headers};
@ -868,17 +998,22 @@ apply_custom_headers(Headers, CustomHeaders) ->
maps:from_list(CustomHeaders)),
Doctype ++ maps:to_list(M).
% The following code is mostly taken from yaws_ssl.erl
toupper(C) when C >= $a andalso C =< $z -> C - 32;
toupper(C) -> C.
tolower(C) when C >= $A andalso C =< $Z -> C + 32;
tolower(C) -> C.
normalize_header_name(Name) ->
normalize_header_name(Name, [], true).
normalize_header_name(<<"">>, Acc, _) ->
iolist_to_binary(Acc);
normalize_header_name(<<"-", Rest/binary>>, Acc, _) ->
@ -888,16 +1023,19 @@ normalize_header_name(<<C:8, Rest/binary>>, Acc, true) ->
normalize_header_name(<<C:8, Rest/binary>>, Acc, false) ->
normalize_header_name(Rest, [Acc, tolower(C)], false).
normalize_path(Path) ->
normalize_path(Path, []).
normalize_path([], Norm) -> lists:reverse(Norm);
normalize_path([<<"..">>|Path], Norm) ->
normalize_path([<<"..">> | Path], Norm) ->
normalize_path(Path, Norm);
normalize_path([_Parent, <<"..">>|Path], Norm) ->
normalize_path([_Parent, <<"..">> | Path], Norm) ->
normalize_path(Path, Norm);
normalize_path([Part | Path], Norm) ->
normalize_path(Path, [Part|Norm]).
normalize_path(Path, [Part | Norm]).
listen_opt_type(tag) ->
econf:binary();
@ -914,6 +1052,7 @@ listen_opt_type(custom_headers) ->
econf:binary(),
econf:binary()).
listen_options() ->
[{ciphers, undefined},
{dhfile, undefined},

View file

@ -27,12 +27,25 @@
-behaviour(xmpp_socket).
-behaviour(p1_fsm).
-export([start/1, start_link/1, init/1, handle_event/3,
handle_sync_event/4, code_change/4, handle_info/3,
terminate/3, send_xml/2, setopts/2, sockname/1,
peername/1, controlling_process/2, get_owner/1,
reset_stream/1, close/1, change_shaper/2,
socket_handoff/3, get_transport/1]).
-export([start/1,
start_link/1,
init/1,
handle_event/3,
handle_sync_event/4,
code_change/4,
handle_info/3,
terminate/3,
send_xml/2,
setopts/2,
sockname/1,
peername/1,
controlling_process/2,
get_owner/1,
reset_stream/1,
close/1,
change_shaper/2,
socket_handoff/3,
get_transport/1]).
-include("logger.hrl").
@ -40,6 +53,10 @@
-include("ejabberd_http.hrl").
%%
%% @efmt:off
%% @indent-begin
-record(state,
{socket :: ws_socket(),
ping_interval :: non_neg_integer(),
@ -52,7 +69,11 @@
c2s_pid :: pid(),
ws :: {#ws{}, pid()}}).
%-define(DBGFSM, true).
%% @indent-end
%% @efmt:on
%%
%%%-define(DBGFSM, true).
-ifdef(DBGFSM).
@ -67,22 +88,25 @@
-type ws_socket() :: {http_ws, pid(), {inet:ip_address(), inet:port_number()}}.
-export_type([ws_socket/0]).
start(WS) ->
p1_fsm:start(?MODULE, [WS], ?FSMOPTS).
start_link(WS) ->
p1_fsm:start_link(?MODULE, [WS], ?FSMOPTS).
send_xml({http_ws, FsmRef, _IP}, Packet) ->
case catch p1_fsm:sync_send_all_state_event(FsmRef,
{send_xml, Packet},
15000)
of
15000) of
{'EXIT', {timeout, _}} -> {error, timeout};
{'EXIT', _} -> {error, einval};
Res -> Res
end.
setopts({http_ws, FsmRef, _IP}, Opts) ->
case lists:member({active, once}, Opts) of
true ->
@ -91,32 +115,43 @@ setopts({http_ws, FsmRef, _IP}, Opts) ->
_ -> ok
end.
sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}.
peername({http_ws, _FsmRef, IP}) -> {ok, IP}.
controlling_process(_Socket, _Pid) -> ok.
close({http_ws, FsmRef, _IP}) ->
catch p1_fsm:sync_send_all_state_event(FsmRef, close).
reset_stream({http_ws, _FsmRef, _IP} = Socket) ->
Socket.
change_shaper({http_ws, FsmRef, _IP}, Shaper) ->
p1_fsm:send_all_state_event(FsmRef, {new_shaper, Shaper}).
get_transport(_Socket) ->
websocket.
get_owner({http_ws, FsmRef, _IP}) ->
FsmRef.
socket_handoff(LocalPath, Request, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0).
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
@ -127,37 +162,45 @@ init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
({resend_on_timeout, _}) -> true;
({access, _}) -> true;
(_) -> false
end, HOpts),
end,
HOpts),
Opts = ejabberd_c2s_config:get_c2s_limits() ++ SOpts,
PingInterval = ejabberd_option:websocket_ping_interval(),
WSTimeout = ejabberd_option:websocket_timeout(),
Socket = {http_ws, self(), IP},
?DEBUG("Client connected through websocket ~p",
[Socket]),
case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of
case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()} | Opts]) of
{ok, C2SPid} ->
ejabberd_c2s:accept(C2SPid),
Timer = erlang:start_timer(WSTimeout, self(), []),
{ok, loop,
#state{socket = Socket, timeout = WSTimeout,
timer = Timer, ws = WS, c2s_pid = C2SPid,
ping_interval = PingInterval}};
#state{
socket = Socket,
timeout = WSTimeout,
timer = Timer,
ws = WS,
c2s_pid = C2SPid,
ping_interval = PingInterval
}};
{error, Reason} ->
{stop, Reason};
ignore ->
ignore
end.
handle_event({activate, From}, StateName, State) ->
State1 = case State#state.input of
[] -> State#state{active = true};
Input ->
lists:foreach(
fun(I) when is_binary(I)->
fun(I) when is_binary(I) ->
From ! {tcp, State#state.socket, I};
(I2) ->
From ! {tcp, State#state.socket, [I2]}
end, Input),
end,
Input),
State#state{active = false, input = []}
end,
{next_state, StateName, State1#state{c2s_pid = From}};
@ -165,17 +208,22 @@ handle_event({new_shaper, Shaper}, StateName, #state{ws = {_, WsPid}} = StateDat
WsPid ! {new_shaper, Shaper},
{next_state, StateName, StateData}.
handle_sync_event({send_xml, Packet}, _From, StateName,
handle_sync_event({send_xml, Packet},
_From,
StateName,
#state{ws = {_, WsPid}} = StateData) ->
SN2 = case Packet of
{xmlstreamstart, _, Attrs} ->
Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} |
lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} | lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
route_el(WsPid, #xmlel{name = <<"open">>, attrs = Attrs2}),
StateName;
{xmlstreamend, _} ->
route_el(WsPid, #xmlel{name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}),
route_el(WsPid,
#xmlel{
name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]
}),
stream_end_sent;
{xmlstreamraw, <<"\r\n\r\n">>} ->
% cdata ping
@ -198,13 +246,16 @@ handle_sync_event({send_xml, Packet}, _From, StateName,
{reply, ok, SN2, StateData};
handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}} = StateData)
when StateName /= stream_end_sent ->
Close = #xmlel{name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]},
Close = #xmlel{
name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]
},
route_text(WsPid, fxml:element_to_binary(Close)),
{stop, normal, StateData};
handle_sync_event(close, _From, _StateName, StateData) ->
{stop, normal, StateData}.
handle_info(closed, _StateName, StateData) ->
{stop, normal, StateData};
handle_info({received, Packet}, StateName, StateDataI) ->
@ -223,17 +274,20 @@ handle_info(PingPong, StateName, StateData) when PingPong == ping orelse
StateData2 = setup_timers(StateData),
{next_state, StateName,
StateData2#state{pong_expected = false}};
handle_info({timeout, Timer, _}, _StateName,
handle_info({timeout, Timer, _},
_StateName,
#state{timer = Timer} = StateData) ->
?DEBUG("Closing websocket connection from hitting inactivity timeout", []),
{stop, normal, StateData};
handle_info({timeout, Timer, _}, StateName,
handle_info({timeout, Timer, _},
StateName,
#state{ping_timer = Timer, ws = {_, WsPid}} = StateData) ->
case StateData#state.pong_expected of
false ->
misc:cancel_timer(StateData#state.ping_timer),
PingTimer = erlang:start_timer(StateData#state.ping_interval,
self(), []),
self(),
[]),
WsPid ! {ping, <<>>},
{next_state, StateName,
StateData#state{ping_timer = PingTimer, pong_expected = true}};
@ -244,62 +298,91 @@ handle_info({timeout, Timer, _}, StateName,
handle_info(_, StateName, StateData) ->
{next_state, StateName, StateData}.
code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
terminate(_Reason, _StateName, StateData) ->
StateData#state.c2s_pid ! {tcp_closed, StateData#state.socket}.
setup_timers(StateData) ->
misc:cancel_timer(StateData#state.timer),
Timer = erlang:start_timer(StateData#state.timeout,
self(), []),
self(),
[]),
misc:cancel_timer(StateData#state.ping_timer),
PingTimer = case StateData#state.ping_interval of
0 -> StateData#state.ping_timer;
V -> erlang:start_timer(V, self(), [])
end,
StateData#state{timer = Timer, ping_timer = PingTimer,
pong_expected = false}.
StateData#state{
timer = Timer,
ping_timer = PingTimer,
pong_expected = false
}.
get_human_html_xmlel() ->
Heading = <<"ejabberd ", (misc:atom_to_binary(?MODULE))/binary>>,
#xmlel{name = <<"html">>,
#xmlel{
name = <<"html">>,
attrs =
[{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}],
children =
[#xmlel{name = <<"head">>, attrs = [],
[#xmlel{
name = <<"head">>,
attrs = [],
children =
[#xmlel{name = <<"title">>, attrs = [],
children = [{xmlcdata, Heading}]}]},
#xmlel{name = <<"body">>, attrs = [],
[#xmlel{
name = <<"title">>,
attrs = [],
children = [{xmlcdata, Heading}]
}]
},
#xmlel{
name = <<"body">>,
attrs = [],
children =
[#xmlel{name = <<"h1">>, attrs = [],
children = [{xmlcdata, Heading}]},
#xmlel{name = <<"p">>, attrs = [],
[#xmlel{
name = <<"h1">>,
attrs = [],
children = [{xmlcdata, Heading}]
},
#xmlel{
name = <<"p">>,
attrs = [],
children =
[{xmlcdata, <<"An implementation of ">>},
#xmlel{name = <<"a">>,
#xmlel{
name = <<"a">>,
attrs =
[{<<"href">>,
<<"http://tools.ietf.org/html/rfc6455">>}],
children =
[{xmlcdata,
<<"WebSocket protocol">>}]}]},
#xmlel{name = <<"p">>, attrs = [],
<<"WebSocket protocol">>}]
}]
},
#xmlel{
name = <<"p">>,
attrs = [],
children =
[{xmlcdata,
<<"This web page is only informative. To "
"use WebSocket connection you need a Jabber/XMPP "
"client that supports it.">>}]}]}]}.
"client that supports it.">>}]
}]
}]
}.
parse(State, Data) ->
El = fxml_stream:parse_element(Data),
case El of
#xmlel{name = <<"open">>, attrs = Attrs} ->
Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} |
lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} | lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))],
{State, [{xmlstreamstart, <<"stream:stream">>, Attrs2}]};
#xmlel{name = <<"close">>} ->
{State, [{xmlstreamend, <<"stream:stream">>}]};
@ -309,6 +392,7 @@ parse(State, Data) ->
{State, [El]}
end.
-spec route_text(pid(), binary()) -> ok.
route_text(Pid, Data) ->
Pid ! {text_with_reply, Data, self()},
@ -317,6 +401,7 @@ route_text(Pid, Data) ->
ok
end.
-spec route_el(pid(), xmlel() | cdata()) -> ok.
route_el(Pid, Data) ->
route_text(Pid, fxml:element_to_binary(Data)).

View file

@ -31,22 +31,29 @@
-export([start_link/0, route/4, dispatch/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_stacktrace.hrl").
-record(state, {expire = infinity :: timeout()}).
-type state() :: #state{}.
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec route(iq(), atom() | pid(), term(), non_neg_integer()) -> ok.
route(#iq{type = T} = IQ, Proc, Ctx, Timeout) when T == set; T == get ->
Expire = current_time() + Timeout,
@ -56,6 +63,7 @@ route(#iq{type = T} = IQ, Proc, Ctx, Timeout) when T == set; T == get ->
gen_server:cast(?MODULE, {restart_timer, Expire}),
ejabberd_router:route(IQ#iq{id = ID}).
-spec dispatch(iq()) -> boolean().
dispatch(#iq{type = T, id = ID} = IQ) when T == error; T == result ->
case decode_id(ID) of
@ -67,6 +75,7 @@ dispatch(#iq{type = T, id = ID} = IQ) when T == error; T == result ->
dispatch(_) ->
false.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -74,10 +83,12 @@ init([]) ->
_ = ets:new(?MODULE, [named_table, ordered_set, public]),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
noreply(State).
handle_cast({restart_timer, Expire}, State) ->
State1 = State#state{expire = min(Expire, State#state.expire)},
noreply(State1);
@ -85,6 +96,7 @@ handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
noreply(State).
handle_info({route, IQ, Key}, State) ->
case ets:lookup(?MODULE, Key) of
[{_, Proc, Ctx}] ->
@ -101,12 +113,15 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
noreply(State).
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -114,8 +129,9 @@ code_change(_OldVsn, State, _Extra) ->
current_time() ->
erlang:system_time(millisecond).
-spec clean({non_neg_integer(), binary()} | '$end_of_table')
-> non_neg_integer() | infinity.
-spec clean({non_neg_integer(), binary()} | '$end_of_table') ->
non_neg_integer() | infinity.
clean({Expire, _} = Key) ->
case current_time() of
Time when Time >= Expire ->
@ -133,6 +149,7 @@ clean({Expire, _} = Key) ->
clean('$end_of_table') ->
infinity.
-spec noreply(state()) -> {noreply, state()} | {noreply, state(), non_neg_integer()}.
noreply(#state{expire = Expire} = State) ->
case Expire of
@ -143,6 +160,7 @@ noreply(#state{expire = Expire} = State) ->
{noreply, State, Timeout}
end.
-spec encode_id(non_neg_integer(), binary()) -> binary().
encode_id(Expire, Rnd) ->
ExpireBin = integer_to_binary(Expire),
@ -150,6 +168,7 @@ encode_id(Expire, Rnd) ->
CheckSum = calc_checksum(<<ExpireBin/binary, Rnd/binary, Node/binary>>),
<<"rr-", ExpireBin/binary, $-, Rnd/binary, $-, CheckSum/binary, $-, Node/binary>>.
-spec decode_id(binary()) -> {ok, non_neg_integer(), binary(), atom()} | error.
decode_id(<<"rr-", ID/binary>>) ->
try
@ -160,21 +179,26 @@ decode_id(<<"rr-", ID/binary>>) ->
Node = ejabberd_cluster:get_node_by_id(NodeBin),
Expire = binary_to_integer(ExpireBin),
{ok, Expire, Rnd, Node}
catch _:{badmatch, _} ->
catch
_:{badmatch, _} ->
error
end;
decode_id(_) ->
error.
-spec calc_checksum(binary()) -> binary().
calc_checksum(Data) ->
Key = ejabberd_config:get_shared_key(),
base64:encode(crypto:hash(sha, <<Data/binary, Key/binary>>)).
-spec callback(atom() | pid(), #iq{} | timeout, term()) -> any().
callback(undefined, IQRes, Fun) ->
try Fun(IQRes)
catch ?EX_RULE(Class, Reason, St) ->
try
Fun(IQRes)
catch
?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to process iq response:~n~ts~n** ~ts",
[xmpp:pp(IQRes),
@ -183,6 +207,7 @@ callback(undefined, IQRes, Fun) ->
callback(Proc, IQRes, Ctx) ->
try
Proc ! {iq_reply, IQRes, Ctx}
catch _:badarg ->
catch
_:badarg ->
ok
end.

View file

@ -28,9 +28,16 @@
-author('alexey@process-one.net').
-author('ekhramtsov@process-one.net').
-export([start_link/0, init/1, stop/0, start/3, init/3,
start_listeners/0, start_listener/3, stop_listeners/0,
add_listener/3, delete_listener/2,
-export([start_link/0,
init/1,
stop/0,
start/3,
init/3,
start_listeners/0,
start_listener/3,
stop_listeners/0,
add_listener/3,
delete_listener/2,
config_reloaded/0]).
-export([listen_options/0, listen_opt_type/1, validator/0]).
-export([tls_listeners/0]).
@ -48,6 +55,7 @@
-export_type([listener/0]).
-callback start(sockmod(), socket(), state()) ->
{ok, pid()} | {error, any()} | ignore.
-callback start_link(sockmod(), socket(), state()) ->
@ -60,20 +68,24 @@
-optional_callbacks([listen_opt_type/1, tcp_init/2, udp_init/2]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init(_) ->
_ = ets:new(?MODULE, [named_table, public]),
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50),
Listeners = ejabberd_option:listen(),
{ok, {{one_for_one, 10, 1}, listeners_childspec(Listeners)}}.
stop() ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50),
stop_listeners(),
ejabberd_sup:stop_child(?MODULE).
-spec listeners_childspec([listener()]) -> [supervisor:child_spec()].
listeners_childspec(Listeners) ->
lists:map(
@ -81,8 +93,13 @@ listeners_childspec(Listeners) ->
ets:insert(?MODULE, {EndPoint, Module, Opts}),
{EndPoint,
{?MODULE, start, [EndPoint, Module, Opts]},
transient, brutal_kill, worker, [?MODULE]}
end, Listeners).
transient,
brutal_kill,
worker,
[?MODULE]}
end,
Listeners).
-spec start_listeners() -> ok.
start_listeners() ->
@ -90,17 +107,21 @@ start_listeners() ->
lists:foreach(
fun(Spec) ->
supervisor:start_child(?MODULE, Spec)
end, listeners_childspec(Listeners)).
end,
listeners_childspec(Listeners)).
-spec start(endpoint(), module(), opts()) -> term().
start(EndPoint, Module, Opts) ->
proc_lib:start_link(?MODULE, init, [EndPoint, Module, Opts]).
-spec init(endpoint(), module(), opts()) -> ok.
init({_, _, Transport} = EndPoint, Module, AllOpts) ->
{ModuleOpts, SockOpts} = split_opts(Transport, AllOpts),
init(EndPoint, Module, ModuleOpts, SockOpts).
-spec init(endpoint(), module(), opts(), [gen_tcp:option()]) -> ok.
init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
{Port2, ExtraOpts} = case Port of
@ -113,10 +134,10 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
{Port, SockOpts}
end,
ExtraOpts2 = lists:keydelete(send_timeout, 1, ExtraOpts),
case {gen_udp:open(Port2, [binary,
case {gen_udp:open(Port2,
[binary,
{active, false},
{reuseaddr, true} |
ExtraOpts2]),
{reuseaddr, true} | ExtraOpts2]),
set_definitive_udsocket(Port, Opts)} of
{{ok, Socket}, ok} ->
misc:set_proc_label({?MODULE, udp, Port}),
@ -127,7 +148,8 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
ok ->
?INFO_MSG("Start accepting ~ts connections at ~ts for ~p",
[format_transport(udp, Opts),
format_endpoint({Port1, Addr, udp}), Module]),
format_endpoint({Port1, Addr, udp}),
Module]),
Opts1 = opts_to_list(Module, Opts),
case erlang:function_exported(Module, udp_init, 2) of
false ->
@ -144,7 +166,7 @@ init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) ->
end;
{{error, Reason}, _} ->
return_socket_error(Reason, EndPoint, Module);
{_, {error, Reason} } ->
{_, {error, Reason}} ->
return_socket_error(Reason, EndPoint, Module)
end;
init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) ->
@ -162,7 +184,8 @@ init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) ->
Proxy = maps:get(use_proxy_protocol, Opts),
?INFO_MSG("Start accepting ~ts connections at ~ts for ~p",
[format_transport(tcp, Opts),
format_endpoint({Port1, Addr, tcp}), Module]),
format_endpoint({Port1, Addr, tcp}),
Module]),
Opts1 = opts_to_list(Module, Opts),
case erlang:function_exported(Module, tcp_init, 2) of
false ->
@ -183,6 +206,7 @@ init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) ->
return_socket_error(Reason, EndPoint, Module)
end.
-spec listen_tcp(inet:port_number(), [gen_tcp:option()]) ->
{ok, inet:socket()} | {error, system_limit | inet:posix()}.
listen_tcp(Port, SockOpts) ->
@ -196,7 +220,8 @@ listen_tcp(Port, SockOpts) ->
_ ->
{Port, SockOpts}
end,
Res = gen_tcp:listen(Port2, [binary,
Res = gen_tcp:listen(Port2,
[binary,
{packet, 0},
{active, false},
{reuseaddr, true},
@ -210,10 +235,12 @@ listen_tcp(Port, SockOpts) ->
Err
end.
%%%
%%% Unix Domain Socket utility functions
%%%
setup_provisional_udsocket_dir(DefinitivePath) ->
ProvisionalPath = get_provisional_udsocket_path(DefinitivePath),
?INFO_MSG("Creating a Unix Domain Socket provisional file at ~ts for the definitive path ~s",
@ -222,6 +249,7 @@ setup_provisional_udsocket_dir(DefinitivePath) ->
create_base_dir(ProvisionalPathAbsolute),
ProvisionalPathAbsolute.
get_provisional_udsocket_path(Path) ->
ReproducibleSecret = binary:part(crypto:hash(sha, misc:atom_to_binary(erlang:get_cookie())), 1, 8),
PathBase64 = misc:term_to_base64({ReproducibleSecret, Path}),
@ -236,10 +264,12 @@ get_provisional_udsocket_path(Path) ->
{true, true} ->
?ERROR_MSG("The Unix Domain Socket path ~ts is too long, "
"and I cannot create the provisional file safely. "
"Please configure a shorter path and try again.", [Path]),
"Please configure a shorter path and try again.",
[Path]),
throw({error_socket_path_too_long, Path})
end.
get_definitive_udsocket_path(<<"unix", _>> = Unix) ->
Unix;
get_definitive_udsocket_path(ProvisionalPath) ->
@ -247,6 +277,7 @@ get_definitive_udsocket_path(ProvisionalPath) ->
{term, {_, Path}} = misc:base64_to_term(PathBase64),
relative_socket_to_mnesia(Path).
-spec set_definitive_udsocket(integer() | binary(), opts()) -> ok | {error, file:posix() | badarg}.
set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) ->
@ -284,6 +315,7 @@ set_definitive_udsocket(<<"unix:", Path/binary>>, Opts) ->
set_definitive_udsocket(Port, _Opts) when is_integer(Port) ->
ok.
create_base_dir(Path) ->
Dirname = filename:dirname(Path),
case file:make_dir(Dirname) of
@ -293,6 +325,7 @@ create_base_dir(Path) ->
ok
end.
relative_socket_to_mnesia(Path1) ->
case filename:pathtype(Path1) of
absolute ->
@ -302,16 +335,19 @@ relative_socket_to_mnesia(Path1) ->
filename:join(MnesiaDir, Path1)
end.
maybe_delete_udsocket_file(<<"unix:", Path/binary>>) ->
PathAbsolute = relative_socket_to_mnesia(Path),
file:delete(PathAbsolute);
maybe_delete_udsocket_file(_Port) ->
ok.
%%%
%%%
%%%
-spec split_opts(transport(), opts()) -> {opts(), [gen_tcp:option()]}.
split_opts(Transport, Opts) ->
maps:fold(
@ -326,10 +362,17 @@ split_opts(Transport, Opts) ->
_ ->
{ModOpts#{Opt => Val}, SockOpts}
end
end, {#{}, []}, Opts).
end,
{#{}, []},
Opts).
-spec accept(inet:socket(), module(), state(), atom(),
non_neg_integer(), boolean()) -> no_return().
-spec accept(inet:socket(),
module(),
state(),
atom(),
non_neg_integer(),
boolean()) -> no_return().
accept(ListenSocket, Module, State, Sup, Interval, Proxy) ->
Arity = case erlang:function_exported(Module, start, 3) of
true -> 3;
@ -337,8 +380,14 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy) ->
end,
accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity).
-spec accept(inet:socket(), module(), state(), atom(),
non_neg_integer(), boolean(), 2|3) -> no_return().
-spec accept(inet:socket(),
module(),
state(),
atom(),
non_neg_integer(),
boolean(),
2 | 3) -> no_return().
accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) ->
NewInterval = apply_rate_limit(Interval),
case gen_tcp:accept(ListenSocket) of
@ -401,6 +450,7 @@ accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) ->
accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity)
end.
is_ctl_over_http(State) ->
case lists:keyfind(request_handlers, 1, State) of
{request_handlers, Handlers} ->
@ -411,6 +461,7 @@ is_ctl_over_http(State) ->
_ -> false
end.
-spec udp_recv(inet:socket(), module(), state()) -> no_return().
udp_recv(Socket, Module, State) ->
case gen_udp:recv(Socket, 0) of
@ -430,7 +481,8 @@ udp_recv(Socket, Module, State) ->
throw({error, Reason})
end.
-spec start_connection(module(), 2|3, inet:socket(), state(), atom()) ->
-spec start_connection(module(), 2 | 3, inet:socket(), state(), atom()) ->
{ok, pid()} | {error, any()} | ignore.
start_connection(Module, Arity, Socket, State, Sup) ->
Res = case Sup of
@ -465,6 +517,7 @@ start_connection(Module, Arity, Socket, State, Sup) ->
Err
end.
-spec start_listener(endpoint(), module(), opts()) ->
{ok, pid()} | {error, any()}.
start_listener(EndPoint, Module, Opts) ->
@ -474,10 +527,11 @@ start_listener(EndPoint, Module, Opts) ->
%% call returns: {error, {already_started, pid()}}
case start_listener_sup(EndPoint, Module, Opts) of
{ok, _Pid} = R -> R;
{error, {{'EXIT', {undef, [{M, _F, _A}|_]}}, _} = Error} ->
{error, {{'EXIT', {undef, [{M, _F, _A} | _]}}, _} = Error} ->
?ERROR_MSG("Error starting the ejabberd listener: ~p.~n"
"It could not be loaded or is not an ejabberd listener.~n"
"Error: ~p~n", [Module, Error]),
"Error: ~p~n",
[Module, Error]),
{error, {module_not_available, M}};
{error, {already_started, Pid}} ->
{ok, Pid};
@ -485,12 +539,14 @@ start_listener(EndPoint, Module, Opts) ->
{error, Error}
end.
-spec start_module_sup(module(), opts()) -> atom().
start_module_sup(Module, Opts) ->
case maps:get(supervisor, Opts) of
true ->
Proc = list_to_atom(atom_to_list(Module) ++ "_sup"),
ChildSpec = {Proc, {ejabberd_tmp_sup, start_link, [Proc, Module]},
ChildSpec = {Proc,
{ejabberd_tmp_sup, start_link, [Proc, Module]},
permanent,
infinity,
supervisor,
@ -504,6 +560,7 @@ start_module_sup(Module, Opts) ->
undefined
end.
-spec start_listener_sup(endpoint(), module(), opts()) ->
{ok, pid()} | {error, any()}.
start_listener_sup(EndPoint, Module, Opts) ->
@ -515,6 +572,7 @@ start_listener_sup(EndPoint, Module, Opts) ->
[?MODULE]},
supervisor:start_child(?MODULE, ChildSpec).
-spec stop_listeners() -> ok.
stop_listeners() ->
Ports = ejabberd_option:listen(),
@ -524,13 +582,15 @@ stop_listeners() ->
end,
Ports).
-spec stop_listener(endpoint(), module(), opts()) -> ok | {error, any()}.
stop_listener({Port, _, Transport} = EndPoint, Module, Opts) ->
case supervisor:terminate_child(?MODULE, EndPoint) of
ok ->
?INFO_MSG("Stop accepting ~ts connections at ~ts for ~p",
[format_transport(Transport, Opts),
format_endpoint(EndPoint), Module]),
format_endpoint(EndPoint),
Module]),
maybe_delete_udsocket_file(Port),
ets:delete(?MODULE, EndPoint),
supervisor:delete_child(?MODULE, EndPoint);
@ -538,6 +598,7 @@ stop_listener({Port, _, Transport} = EndPoint, Module, Opts) ->
Err
end.
-spec add_listener(endpoint(), module(), opts()) -> ok | {error, any()}.
add_listener(EndPoint, Module, Opts) ->
Opts1 = apply_defaults(Module, Opts),
@ -550,14 +611,17 @@ add_listener(EndPoint, Module, Opts) ->
{error, Error}
end.
-spec delete_listener(endpoint(), module()) -> ok | {error, any()}.
delete_listener(EndPoint, Module) ->
try ets:lookup_element(?MODULE, EndPoint, 3) of
Opts -> stop_listener(EndPoint, Module, Opts)
catch _:badarg ->
catch
_:badarg ->
ok
end.
-spec tls_listeners() -> [module()].
tls_listeners() ->
lists:usort(
@ -565,7 +629,9 @@ tls_listeners() ->
fun({_, Module, #{tls := true}}) -> {true, Module};
({_, Module, #{starttls := true}}) -> {true, Module};
(_) -> false
end, ets:tab2list(?MODULE))).
end,
ets:tab2list(?MODULE))).
-spec config_reloaded() -> ok.
config_reloaded() ->
@ -579,7 +645,8 @@ config_reloaded() ->
_ ->
ok
end
end, Old),
end,
Old),
lists:foreach(
fun({EndPoint, Module, Opts}) ->
case lists:keyfind(EndPoint, 1, Old) of
@ -601,7 +668,9 @@ config_reloaded() ->
ok
end
end
end, New).
end,
New).
-spec return_socket_error(inet:posix(), endpoint(), module()) -> no_return().
return_socket_error(Reason, EndPoint, Module) ->
@ -609,15 +678,25 @@ return_socket_error(Reason, EndPoint, Module) ->
[format_endpoint(EndPoint), Module, format_error(Reason)]),
return_init_error(Reason).
-ifdef(OTP_BELOW_26).
return_init_error(Reason) ->
proc_lib:init_ack({error, Reason}).
-else.
-spec return_init_error(inet:posix()) -> no_return().
return_init_error(Reason) ->
proc_lib:init_fail({error, Reason}, {exit, normal}).
-endif.
-spec format_error(inet:posix() | atom()) -> string().
format_error(Reason) ->
case inet:format_error(Reason) of
@ -627,6 +706,7 @@ format_error(Reason) ->
ReasonStr
end.
-spec format_endpoint(endpoint()) -> string().
format_endpoint({Port, IP, Transport}) ->
case Port of
@ -645,6 +725,7 @@ format_endpoint({Port, IP, Transport}) ->
IPStr ++ ":" ++ integer_to_list(Port)
end.
-spec format_transport(transport(), opts()) -> string().
format_transport(Transport, Opts) ->
case maps:get(tls, Opts, false) of
@ -654,12 +735,14 @@ format_transport(Transport, Opts) ->
false when Transport == udp -> "UDP"
end.
-spec apply_rate_limit(non_neg_integer()) -> non_neg_integer().
apply_rate_limit(Interval) ->
NewInterval = receive
{rate_limit, AcceptInterval} ->
AcceptInterval
after 0 ->
after
0 ->
Interval
end,
case NewInterval of
@ -680,15 +763,18 @@ apply_rate_limit(Interval) ->
end,
NewInterval.
-spec validator() -> econf:validator().
validator() ->
econf:and_then(
econf:list(
econf:and_then(
econf:options(
#{module => listen_opt_type(module),
#{
module => listen_opt_type(module),
transport => listen_opt_type(transport),
'_' => econf:any()},
'_' => econf:any()
},
[{required, [module]}]),
fun(Opts) ->
M = proplists:get_value(module, Opts),
@ -697,11 +783,13 @@ validator() ->
end)),
fun prepare_opts/1).
-spec validator(module(), transport()) -> econf:validator().
validator(M, T) ->
Options = listen_options() ++ M:listen_options(),
Required = lists:usort([Opt || Opt <- Options, is_atom(Opt)]),
Disallowed = if T == udp ->
Required = lists:usort([ Opt || Opt <- Options, is_atom(Opt) ]),
Disallowed = if
T == udp ->
[backlog, use_proxy_protocol, accept_interval];
true ->
[]
@ -710,8 +798,10 @@ validator(M, T) ->
Validator = maps:from_list(
lists:map(
fun(Opt) ->
Type = try M:listen_opt_type(Opt)
catch _:_ when M /= ?MODULE ->
Type = try
M:listen_opt_type(Opt)
catch
_:_ when M /= ?MODULE ->
listen_opt_type(Opt)
end,
TypeProcessed =
@ -721,11 +811,15 @@ validator(M, T) ->
end,
Type),
{Opt, TypeProcessed}
end, proplists:get_keys(Options))),
end,
proplists:get_keys(Options))),
econf:options(
Validator,
[{required, Required}, {disallowed, Disallowed},
{return, map}, unique]).
[{required, Required},
{disallowed, Disallowed},
{return, map},
unique]).
-spec prepare_opts([opts()]) -> [listener()].
prepare_opts(Listeners) ->
@ -737,14 +831,17 @@ prepare_opts(Listeners) ->
({transport, _}) -> true;
({module, _}) -> true;
(_) -> false
end, Opts1),
end,
Opts1),
Mod = maps:get(module, Opts2),
Port = maps:get(port, Opts2),
Transport = maps:get(transport, Opts2, tcp),
IP = maps:get(ip, Opts3, {0,0,0,0}),
IP = maps:get(ip, Opts3, {0, 0, 0, 0}),
Opts4 = apply_defaults(Mod, Opts3),
{{Port, IP, Transport}, Mod, Opts4}
end, Listeners)).
end,
Listeners)).
-spec check_overlapping_listeners([listener()]) -> [listener()].
check_overlapping_listeners(Listeners) ->
@ -755,21 +852,25 @@ check_overlapping_listeners(Listeners) ->
econf:fail({listener_dup, {IP, Port}});
false ->
ZeroIP = case size(IP) of
8 -> {0,0,0,0,0,0,0,0};
4 -> {0,0,0,0}
8 -> {0, 0, 0, 0, 0, 0, 0, 0};
4 -> {0, 0, 0, 0}
end,
Key1 = {Port, ZeroIP, Transport},
case lists:member(Key1, Acc) of
true ->
econf:fail({listener_conflict,
{IP, Port}, {ZeroIP, Port}});
{IP, Port},
{ZeroIP, Port}});
false ->
[Key|Acc]
[Key | Acc]
end
end
end, [], Listeners),
end,
[],
Listeners),
Listeners.
-spec apply_defaults(module(), opts()) -> opts().
apply_defaults(Mod, Opts) ->
lists:foldl(
@ -780,7 +881,10 @@ apply_defaults(Mod, Opts) ->
end;
(_, M) ->
M
end, Opts, Mod:listen_options() ++ listen_options()).
end,
Opts,
Mod:listen_options() ++ listen_options()).
%% Convert options to list with removing defaults
-spec opts_to_list(module(), opts()) -> list_opts().
@ -790,9 +894,12 @@ opts_to_list(Mod, Opts) ->
fun(Opt, Val, Acc) ->
case proplists:get_value(Opt, Defaults) of
Val -> Acc;
_ -> [{Opt, Val}|Acc]
_ -> [{Opt, Val} | Acc]
end
end, [], Opts).
end,
[],
Opts).
-spec partition(fun(({atom(), term()}) -> boolean()), opts()) -> {opts(), opts()}.
partition(Fun, Opts) ->
@ -802,7 +909,10 @@ partition(Fun, Opts) ->
true -> {True#{Opt => Val}, False};
false -> {True, False#{Opt => Val}}
end
end, {#{}, #{}}, Opts).
end,
{#{}, #{}},
Opts).
-spec listen_opt_type(atom()) -> econf:validator().
listen_opt_type(port) ->
@ -812,7 +922,8 @@ listen_opt_type(port) ->
listen_opt_type(module) ->
econf:beam([[{start, 3}, {start, 2}],
[{start_link, 3}, {start_link, 2}],
{accept, 1}, {listen_options, 0}]);
{accept, 1},
{listen_options, 0}]);
listen_opt_type(ip) ->
econf:ip();
listen_opt_type(transport) ->
@ -851,17 +962,21 @@ listen_opt_type(access) ->
econf:acl();
listen_opt_type(unix_socket) ->
econf:options(
#{group => econf:non_neg_int(),
#{
group => econf:non_neg_int(),
owner => econf:non_neg_int(),
mode => econf:octal()},
mode => econf:octal()
},
[unique, {return, map}]);
listen_opt_type(use_proxy_protocol) ->
econf:bool().
listen_options() ->
[module, port,
[module,
port,
{transport, tcp},
{ip, {0,0,0,0}},
{ip, {0, 0, 0, 0}},
{accept_interval, 0},
{send_timeout, 15000},
{backlog, 128},

View file

@ -35,24 +35,32 @@
-export([route/1,
get_features/1,
bounce_resource_packet/1,
host_up/1, host_down/1]).
host_up/1,
host_down/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% deprecated functions: use ejabberd_router:route_iq/3,4
-export([route_iq/2, route_iq/3]).
-deprecated([{route_iq, 2}, {route_iq, 3}]).
-include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_stacktrace.hrl").
-include("translate.hrl").
-record(state, {}).
%%====================================================================
%% API
%%====================================================================
@ -62,19 +70,27 @@
%%--------------------------------------------------------------------
start() ->
ChildSpec = {?MODULE, {?MODULE, start_link, []},
transient, 1000, worker, [?MODULE]},
transient,
1000,
worker,
[?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [],
gen_server:start_link({local, ?MODULE},
?MODULE,
[],
[]).
-spec route(stanza()) -> ok.
route(Packet) ->
?DEBUG("Local route:~n~ts", [xmpp:pp(Packet)]),
Type = xmpp:get_type(Packet),
To = xmpp:get_to(Packet),
if To#jid.luser /= <<"">> ->
if
To#jid.luser /= <<"">> ->
ejabberd_sm:route(Packet);
is_record(Packet, iq), To#jid.lresource == <<"">> ->
gen_iq_handler:handle(?MODULE, Packet);
@ -82,17 +98,21 @@ route(Packet) ->
ok;
true ->
ejabberd_hooks:run(local_send_to_resource_hook,
To#jid.lserver, [Packet])
To#jid.lserver,
[Packet])
end.
-spec route_iq(iq(), function()) -> ok.
route_iq(IQ, Fun) ->
route_iq(IQ, Fun, undefined).
-spec route_iq(iq(), function(), undefined | non_neg_integer()) -> ok.
route_iq(IQ, Fun, Timeout) ->
ejabberd_router:route_iq(IQ, Fun, undefined, Timeout).
-spec bounce_resource_packet(stanza()) -> ok | stop.
bounce_resource_packet(#presence{to = #jid{lresource = <<"">>}}) ->
ok;
@ -105,14 +125,17 @@ bounce_resource_packet(Packet) ->
ejabberd_router:route_error(Packet, Err),
stop.
-spec get_features(binary()) -> [binary()].
get_features(Host) ->
gen_iq_handler:get_features(?MODULE, Host).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
process_flag(trap_exit, true),
lists:foreach(fun host_up/1, ejabberd_option:hosts()),
@ -122,14 +145,17 @@ init([]) ->
update_table(),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({route, Packet}, State) ->
route(Packet),
{noreply, State};
@ -137,15 +163,18 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
lists:foreach(fun host_down/1, ejabberd_option:hosts()),
ejabberd_hooks:delete(host_up, ?MODULE, host_up, 10),
ejabberd_hooks:delete(host_down, ?MODULE, host_down, 100),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
@ -154,14 +183,19 @@ update_table() ->
catch mnesia:delete_table(iq_response),
ok.
host_up(Host) ->
Owner = case whereis(?MODULE) of
undefined -> self();
Pid -> Pid
end,
ejabberd_router:register_route(Host, Host, {apply, ?MODULE, route}, Owner),
ejabberd_hooks:add(local_send_to_resource_hook, Host,
?MODULE, bounce_resource_packet, 100).
ejabberd_hooks:add(local_send_to_resource_hook,
Host,
?MODULE,
bounce_resource_packet,
100).
host_down(Host) ->
Owner = case whereis(?MODULE) of
@ -169,5 +203,8 @@ host_down(Host) ->
Pid -> Pid
end,
ejabberd_router:unregister_route(Host, Owner),
ejabberd_hooks:delete(local_send_to_resource_hook, Host,
?MODULE, bounce_resource_packet, 100).
ejabberd_hooks:delete(local_send_to_resource_hook,
Host,
?MODULE,
bounce_resource_packet,
100).

View file

@ -37,18 +37,26 @@
{reopen_log, 0},
{rotate_log, 0}]).
-type loglevel() :: none | emergency | alert | critical |
error | warning | notice | info | debug.
-type loglevel() :: none |
emergency |
alert |
critical |
error |
warning |
notice |
info |
debug.
-define(is_loglevel(L),
((L == none) or (L == emergency) or (L == alert)
or (L == critical) or (L == error) or (L == warning)
or (L == notice) or (L == info) or (L == debug))).
((L == none) or (L == emergency) or (L == alert) or
(L == critical) or (L == error) or (L == warning) or
(L == notice) or (L == info) or (L == debug))).
-export_type([loglevel/0]).
-include("logger.hrl").
%%%===================================================================
%%% API
%%%===================================================================
@ -66,10 +74,12 @@ get_log_path() ->
end
end.
-spec loglevels() -> [loglevel(), ...].
loglevels() ->
[none, emergency, alert, critical, error, warning, notice, info, debug].
-spec convert_loglevel(0..5) -> loglevel().
convert_loglevel(0) -> none;
convert_loglevel(1) -> critical;
@ -78,16 +88,18 @@ convert_loglevel(3) -> warning;
convert_loglevel(4) -> info;
convert_loglevel(5) -> debug.
quiet_mode() ->
case application:get_env(ejabberd, quiet) of
{ok, true} -> true;
_ -> false
end.
-spec get_integer_env(atom(), T) -> T.
get_integer_env(Name, Default) ->
case application:get_env(ejabberd, Name) of
{ok, I} when is_integer(I), I>=0 ->
{ok, I} when is_integer(I), I >= 0 ->
I;
{ok, infinity} ->
infinity;
@ -100,7 +112,10 @@ get_integer_env(Name, Default) ->
Default
end.
-ifdef(LAGER).
-spec get_string_env(atom(), T) -> T.
get_string_env(Name, Default) ->
case application:get_env(ejabberd, Name) of
@ -115,9 +130,11 @@ get_string_env(Name, Default) ->
Default
end.
start() ->
start(info).
start(Level) ->
StartedApps = application:which_applications(5000),
case lists:keyfind(logger, 1, StartedApps) of
@ -131,6 +148,7 @@ start(Level) ->
do_start(Level)
end.
do_start_for_logger(Level) ->
application:load(sasl),
application:set_env(sasl, sasl_error_logger, false),
@ -142,6 +160,7 @@ do_start_for_logger(Level) ->
ejabberd:start_app(lager),
ok.
do_start(Level) ->
application:load(sasl),
application:set_env(sasl, sasl_error_logger, false),
@ -151,7 +170,7 @@ do_start(Level) ->
ErrorLog = filename:join([Dir, "error.log"]),
CrashLog = filename:join([Dir, "crash.log"]),
LogRotateDate = get_string_env(log_rotate_date, ""),
LogRotateSize = case get_integer_env(log_rotate_size, 10*1024*1024) of
LogRotateSize = case get_integer_env(log_rotate_size, 10 * 1024 * 1024) of
infinity -> 0;
V -> V
end,
@ -167,12 +186,19 @@ do_start(Level) ->
end,
application:set_env(lager, error_logger_hwm, LogRateLimit),
application:set_env(
lager, handlers,
lager,
handlers,
[{lager_console_backend, ConsoleLevel},
{lager_file_backend, [{file, ConsoleLog}, {level, Level}, {date, LogRotateDate},
{count, LogRotateCount}, {size, LogRotateSize}]},
{lager_file_backend, [{file, ErrorLog}, {level, error}, {date, LogRotateDate},
{count, LogRotateCount}, {size, LogRotateSize}]}]),
{lager_file_backend, [{file, ConsoleLog},
{level, Level},
{date, LogRotateDate},
{count, LogRotateCount},
{size, LogRotateSize}]},
{lager_file_backend, [{file, ErrorLog},
{level, error},
{date, LogRotateDate},
{count, LogRotateCount},
{size, LogRotateSize}]}]),
application:set_env(lager, crash_log, CrashLog),
application:set_env(lager, crash_log_date, LogRotateDate),
application:set_env(lager, crash_log_size, LogRotateSize),
@ -180,19 +206,24 @@ do_start(Level) ->
ejabberd:start_app(lager),
lists:foreach(fun(Handler) ->
lager:set_loghwm(Handler, LogRateLimit)
end, gen_event:which_handlers(lager_event)).
end,
gen_event:which_handlers(lager_event)).
restart() ->
Level = ejabberd_option:loglevel(),
application:stop(lager),
start(Level).
config_reloaded() ->
ok.
reopen_log() ->
ok.
rotate_log() ->
catch lager_crash_log ! rotate,
lists:foreach(
@ -200,7 +231,9 @@ rotate_log() ->
whereis(lager_event) ! {rotate, File};
(_) ->
ok
end, gen_event:which_handlers(lager_event)).
end,
gen_event:which_handlers(lager_event)).
get() ->
Handlers = get_lager_handlers(),
@ -211,9 +244,11 @@ get() ->
(_, Acc) ->
Acc
end,
none, Handlers).
none,
Handlers).
set(N) when is_integer(N), N>=0, N=<5 ->
set(N) when is_integer(N), N >= 0, N =< 5 ->
set(convert_loglevel(N));
set(Level) when ?is_loglevel(Level) ->
case get() of
@ -231,21 +266,24 @@ set(Level) when ?is_loglevel(Level) ->
lager:set_loglevel(H, Level);
(_) ->
ok
end, get_lager_handlers())
end,
get_lager_handlers())
end,
case Level of
debug -> xmpp:set_config([{debug, true}]);
_ -> xmpp:set_config([{debug, false}])
end.
get_lager_handlers() ->
case catch gen_event:which_handlers(lager_event) of
{'EXIT',noproc} ->
{'EXIT', noproc} ->
[];
Result ->
Result
end.
-spec get_lager_version() -> string().
get_lager_version() ->
Apps = application:loaded_applications(),
@ -254,28 +292,34 @@ get_lager_version() ->
false -> "0.0.0"
end.
set_modules_fully_logged(_) -> ok.
flush() ->
application:stop(lager),
application:stop(sasl).
-else.
-include_lib("kernel/include/logger.hrl").
-spec start() -> ok | {error, term()}.
start() ->
start(info).
start(Level) ->
EjabberdLog = get_log_path(),
Dir = filename:dirname(EjabberdLog),
ErrorLog = filename:join([Dir, "error.log"]),
LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024),
LogRotateSize = get_integer_env(log_rotate_size, 10 * 1024 * 1024),
LogRotateCount = get_integer_env(log_rotate_count, 1),
LogBurstLimitWindowTime = get_integer_env(log_burst_limit_window_time, 1000),
LogBurstLimitCount = get_integer_env(log_burst_limit_count, 500),
Config = #{max_no_bytes => LogRotateSize,
Config = #{
max_no_bytes => LogRotateSize,
max_no_files => LogRotateCount,
filesync_repeat_interval => no_repeat,
file_check => 1000,
@ -283,11 +327,14 @@ start(Level) ->
drop_mode_qlen => 1000,
flush_qlen => 5000,
burst_limit_window_time => LogBurstLimitWindowTime,
burst_limit_max_count => LogBurstLimitCount},
FmtConfig = #{legacy_header => false,
burst_limit_max_count => LogBurstLimitCount
},
FmtConfig = #{
legacy_header => false,
time_designator => $\s,
max_size => 100*1024,
single_line => false},
max_size => 100 * 1024,
single_line => false
},
FileFmtConfig = FmtConfig#{template => file_template()},
ConsoleFmtConfig = FmtConfig#{template => console_template()},
try
@ -305,25 +352,33 @@ start(Level) ->
ok -> ok;
{error, {already_exist, _}} -> ok
end,
case logger:add_handler(ejabberd_log, logger_std_h,
#{level => all,
case logger:add_handler(ejabberd_log,
logger_std_h,
#{
level => all,
config => Config#{file => EjabberdLog},
formatter => {logger_formatter, FileFmtConfig}}) of
formatter => {logger_formatter, FileFmtConfig}
}) of
ok -> ok;
{error, {already_exist, _}} -> ok
end,
case logger:add_handler(error_log, logger_std_h,
#{level => error,
case logger:add_handler(error_log,
logger_std_h,
#{
level => error,
config => Config#{file => ErrorLog},
formatter => {logger_formatter, FileFmtConfig}}) of
formatter => {logger_formatter, FileFmtConfig}
}) of
ok -> ok;
{error, {already_exist, _}} -> ok
end
catch _:{Tag, Err} when Tag == badmatch; Tag == case_clause ->
catch
_:{Tag, Err} when Tag == badmatch; Tag == case_clause ->
?LOG_CRITICAL("Failed to set logging: ~p", [Err]),
Err
end.
get_default_handlerid() ->
Ids = logger:get_handler_ids(),
case lists:member(default, Ids) of
@ -331,10 +386,12 @@ get_default_handlerid() ->
false -> hd(Ids)
end.
-spec restart() -> ok.
restart() ->
ok.
-spec config_reloaded() -> ok.
config_reloaded() ->
LogRotateSize = ejabberd_option:log_rotate_size(),
@ -349,14 +406,17 @@ config_reloaded() ->
max_no_bytes => LogRotateSize,
max_no_files => LogRotateCount,
burst_limit_window_time => LogBurstLimitWindowTime,
burst_limit_max_count => LogBurstLimitCount},
burst_limit_max_count => LogBurstLimitCount
},
logger:update_handler_config(Handler, config, Config2);
_ ->
ok
end
end, [ejabberd_log, error_log]).
end,
[ejabberd_log, error_log]).
progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _) ->
progress_filter(#{level := info, msg := {report, #{label := {_, progress}}}} = Event, _) ->
case get() of
debug ->
logger_filters:progress(Event#{level => debug}, log);
@ -366,10 +426,12 @@ progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _)
progress_filter(Event, _) ->
Event.
-ifdef(ELIXIR_ENABLED).
console_template() ->
case (false /= code:is_loaded('Elixir.Logger'))
andalso
case (false /= code:is_loaded('Elixir.Logger')) andalso
'Elixir.System':version() >= <<"1.15">> of
true ->
{ok, DC} = logger:get_handler_config(default),
@ -385,36 +447,59 @@ console_template() ->
false ->
[time, " [", level, "] " | msg()]
end.
msg() ->
[{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []},
msg, io_lib:nl()].
msg,
io_lib:nl()].
-else.
console_template() ->
[time, " ", ?CLEAD, ?CDEFAULT, clevel, "[", level, "] ", ?CMID, ?CDEFAULT, ctext | msg()].
msg() ->
[{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []},
msg, ?CCLEAN, io_lib:nl()].
msg,
?CCLEAN,
io_lib:nl()].
-endif.
file_template() ->
[time, " [", level, "] ", pid,
{mfa, ["@", mfa, {line, [":", line], []}], []}, " " | msg()].
[time,
" [",
level,
"] ",
pid,
{mfa, ["@", mfa, {line, [":", line], []}], []},
" " | msg()].
-spec reopen_log() -> ok.
reopen_log() ->
ok.
-spec rotate_log() -> ok.
rotate_log() ->
ok.
-spec get() -> loglevel().
get() ->
#{level := Level} = logger:get_primary_config(),
Level.
-spec set(0..5 | loglevel()) -> ok.
set(N) when is_integer(N), N>=0, N=<5 ->
set(N) when is_integer(N), N >= 0, N =< 5 ->
set(convert_loglevel(N));
set(Level) when ?is_loglevel(Level) ->
case get() of
@ -429,10 +514,12 @@ set(Level) when ?is_loglevel(Level) ->
end
end.
set_modules_fully_logged(Modules) ->
logger:unset_module_level(),
logger:set_module_level(Modules, all).
-spec flush() -> ok.
flush() ->
lists:foreach(
@ -442,6 +529,8 @@ flush() ->
logger_disk_log_h:filesync(HandlerId);
(_) ->
ok
end, logger:get_handler_config()).
end,
logger:get_handler_config()).
-endif.

View file

@ -33,19 +33,28 @@
-behaviour(gen_server).
-export([start/0, create/3, update/2, transform/2, transform/3,
-export([start/0,
create/3,
update/2,
transform/2, transform/3,
dump_schema/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-define(NEED_RESET, [local_content, type]).
-include("logger.hrl").
-include("ejabberd_stacktrace.hrl").
-record(state, {tables = #{} :: tables(),
schema = [] :: [{atom(), custom_schema()}]}).
-record(state, {
tables = #{} :: tables(),
schema = [] :: [{atom(), custom_schema()}]
}).
-type tables() :: #{atom() => {[{atom(), term()}], term()}}.
-type custom_schema() :: [{ram_copies | disc_copies | disc_only_copies, [node()]} |
@ -54,16 +63,20 @@
{attributes, [atom()]} |
{index, [atom()]}].
start() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec create(module(), atom(), list()) -> any().
create(Module, Name, TabDef) ->
gen_server:call(?MODULE, {create, Module, Name, TabDef},
gen_server:call(?MODULE,
{create, Module, Name, TabDef},
%% Huge timeout is need to have enough
%% time to transform huge tables
timer:minutes(30)).
init([]) ->
ejabberd_config:env_binary_to_list(mnesia, dir),
MyNode = node(),
@ -81,13 +94,16 @@ init([]) ->
{ok, #state{schema = Schema}};
false ->
?CRITICAL_MSG("Erlang node name mismatch: I'm running in node [~ts], "
"but the mnesia database is owned by ~p", [MyNode, DbNodes]),
"but the mnesia database is owned by ~p",
[MyNode, DbNodes]),
?CRITICAL_MSG("Either set ERLANG_NODE in ejabberdctl.cfg "
"or change node name in Mnesia by running: "
"ejabberdctl mnesia_change ~ts", [hd(DbNodes)]),
"ejabberdctl mnesia_change ~ts",
[hd(DbNodes)]),
{stop, node_name_mismatch}
end.
handle_call({create, Module, Name, TabDef}, _From, State) ->
case maps:get(Name, State#state.tables, undefined) of
{TabDef, Result} ->
@ -101,20 +117,25 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
do_create(Module, Name, TabDef, TabDefs) ->
code:ensure_loaded(Module),
Schema = schema(Name, TabDef, TabDefs),
@ -138,20 +159,23 @@ do_create(Module, Name, TabDef, TabDefs) ->
transform(Module, Name, OldAttrs, Attrs)
end.
reset(Name, TabDef) ->
?INFO_MSG("Deleting Mnesia table '~ts'", [Name]),
mnesia_op(delete_table, [Name]),
create(Name, TabDef).
update(Name, TabDef) ->
{attributes, Attrs} = lists:keyfind(attributes, 1, TabDef),
update(Name, Attrs, TabDef).
update(Name, Attrs, TabDef) ->
case change_table_copy_type(Name, TabDef) of
{atomic, ok} ->
CurrIndexes = [lists:nth(N-1, Attrs) ||
N <- mnesia:table_info(Name, index)],
CurrIndexes = [ lists:nth(N - 1, Attrs)
|| N <- mnesia:table_info(Name, index) ],
NewIndexes = proplists:get_value(index, TabDef, []),
case delete_indexes(Name, CurrIndexes -- NewIndexes) of
{atomic, ok} ->
@ -163,16 +187,19 @@ update(Name, Attrs, TabDef) ->
Err
end.
change_table_copy_type(Name, TabDef) ->
CurrType = mnesia:table_info(Name, storage_type),
NewType = case lists:filter(fun is_storage_type_option/1, TabDef) of
[{Type, _}|_] -> Type;
[{Type, _} | _] -> Type;
[] -> CurrType
end,
if NewType /= CurrType ->
if
NewType /= CurrType ->
?INFO_MSG("Changing Mnesia table '~ts' from ~ts to ~ts",
[Name, CurrType, NewType]),
if CurrType == unknown -> mnesia_op(add_table_copy, [Name, node(), NewType]);
if
CurrType == unknown -> mnesia_op(add_table_copy, [Name, node(), NewType]);
true ->
mnesia_op(change_table_copy_type, [Name, node(), NewType])
end;
@ -180,7 +207,8 @@ change_table_copy_type(Name, TabDef) ->
{atomic, ok}
end.
delete_indexes(Name, [Index|Indexes]) ->
delete_indexes(Name, [Index | Indexes]) ->
?INFO_MSG("Deleting index '~ts' from Mnesia table '~ts'", [Index, Name]),
case mnesia_op(del_table_index, [Name, Index]) of
{atomic, ok} ->
@ -191,7 +219,8 @@ delete_indexes(Name, [Index|Indexes]) ->
delete_indexes(_Name, []) ->
{atomic, ok}.
add_indexes(Name, [Index|Indexes]) ->
add_indexes(Name, [Index | Indexes]) ->
?INFO_MSG("Adding index '~ts' to Mnesia table '~ts'", [Index, Name]),
case mnesia_op(add_table_index, [Name, Index]) of
{atomic, ok} ->
@ -202,10 +231,12 @@ add_indexes(Name, [Index|Indexes]) ->
add_indexes(_Name, []) ->
{atomic, ok}.
%
% utilities
%
schema(Name, Default, Schema) ->
case lists:keyfind(Name, 1, Schema) of
{_, Custom} ->
@ -217,6 +248,7 @@ schema(Name, Default, Schema) ->
Default
end.
-spec read_schema_file() -> [{atom(), custom_schema()}].
read_schema_file() ->
File = schema_path(),
@ -229,11 +261,14 @@ read_schema_file() ->
{ok, Config} ->
lists:map(
fun({Tab, Opts}) ->
{Tab, lists:map(
{Tab,
lists:map(
fun({storage_type, T}) -> {T, [node()]};
(Other) -> Other
end, Opts)}
end, Config);
end,
Opts)}
end,
Config);
{error, Reason, Ctx} ->
?ERROR_MSG("Failed to read Mnesia schema from ~ts: ~ts",
[File, econf:format_error(Reason, Ctx)]),
@ -247,26 +282,32 @@ read_schema_file() ->
[File, fast_yaml:format_error(Reason)])
end.
-spec validator() -> econf:validator().
validator() ->
econf:map(
econf:atom(),
econf:options(
#{storage_type => econf:enum([ram_copies, disc_copies, disc_only_copies]),
#{
storage_type => econf:enum([ram_copies, disc_copies, disc_only_copies]),
local_content => econf:bool(),
type => econf:enum([set, ordered_set, bag]),
attributes => econf:list(econf:atom()),
index => econf:list(econf:atom())},
index => econf:list(econf:atom())
},
[{return, orddict}, unique]),
[unique]).
create(Name, TabDef) ->
Type = lists:foldl(
fun({ram_copies, _}, _) -> " ram ";
({disc_copies, _}, _) -> " disc ";
({disc_only_copies, _}, _) -> " disc_only ";
(_, Acc) -> Acc
end, " ", TabDef),
end,
" ",
TabDef),
?INFO_MSG("Creating Mnesia~tstable '~ts'", [Type, Name]),
case mnesia_op(create_table, [Name, TabDef]) of
{atomic, ok} ->
@ -275,6 +316,7 @@ create(Name, TabDef) ->
Err
end.
%% The table MUST exist, otherwise the function would fail
add_table_copy(Name) ->
Type = mnesia:table_info(Name, storage_type),
@ -286,43 +328,53 @@ add_table_copy(Name) ->
mnesia_op(add_table_copy, [Name, node(), Type])
end.
merge(Custom, Default) ->
NewDefault = case lists:any(fun is_storage_type_option/1, Custom) of
true ->
lists:filter(
fun(O) ->
not is_storage_type_option(O)
end, Default);
end,
Default);
false ->
Default
end,
lists:ukeymerge(1, Custom, lists:ukeysort(1, NewDefault)).
need_reset(Table, TabDef) ->
ValuesF = [mnesia:table_info(Table, Key) || Key <- ?NEED_RESET],
ValuesT = [proplists:get_value(Key, TabDef) || Key <- ?NEED_RESET],
ValuesF = [ mnesia:table_info(Table, Key) || Key <- ?NEED_RESET ],
ValuesT = [ proplists:get_value(Key, TabDef) || Key <- ?NEED_RESET ],
lists:foldl(
fun({Val, Val}, Acc) -> Acc;
({_, undefined}, Acc) -> Acc;
({_, _}, _) -> true
end, false, lists:zip(ValuesF, ValuesT)).
end,
false,
lists:zip(ValuesF, ValuesT)).
transform(Module, Name) ->
try mnesia:table_info(Name, attributes) of
Attrs ->
transform(Module, Name, Attrs, Attrs)
catch _:{aborted, _} = Err ->
catch
_:{aborted, _} = Err ->
Err
end.
transform(Module, Name, NewAttrs) ->
try mnesia:table_info(Name, attributes) of
OldAttrs ->
transform(Module, Name, OldAttrs, NewAttrs)
catch _:{aborted, _} = Err ->
catch
_:{aborted, _} = Err ->
Err
end.
transform(Module, Name, Attrs, Attrs) ->
case need_transform(Module, Name) of
true ->
@ -338,6 +390,7 @@ transform(Module, Name, OldAttrs, NewAttrs) ->
end,
mnesia_op(transform_table, [Name, Fun, NewAttrs]).
-spec need_transform(module(), atom()) -> boolean().
need_transform(Module, Name) ->
case erlang:function_exported(Module, need_transform, 1) of
@ -347,6 +400,7 @@ need_transform(Module, Name) ->
false
end.
do_need_transform(_Module, _Name, '$end_of_table') ->
false;
do_need_transform(Module, Name, Key) ->
@ -354,45 +408,56 @@ do_need_transform(Module, Name, Key) ->
case lists:foldl(
fun(_, true) -> true;
(Obj, _) -> Module:need_transform(Obj)
end, undefined, Objs) of
end,
undefined,
Objs) of
true -> true;
false -> false;
_ ->
do_need_transform(Module, Name, mnesia:dirty_next(Name, Key))
end.
do_transform(OldAttrs, Attrs, Old) ->
[Name|OldValues] = tuple_to_list(Old),
[Name | OldValues] = tuple_to_list(Old),
Before = lists:zip(OldAttrs, OldValues),
After = lists:foldl(
fun(Attr, Acc) ->
case lists:keyfind(Attr, 1, Before) of
false -> [{Attr, undefined}|Acc];
Value -> [Value|Acc]
false -> [{Attr, undefined} | Acc];
Value -> [Value | Acc]
end
end, [], lists:reverse(Attrs)),
end,
[],
lists:reverse(Attrs)),
{Attrs, NewRecord} = lists:unzip(After),
list_to_tuple([Name|NewRecord]).
list_to_tuple([Name | NewRecord]).
transform_fun(Module, Name) ->
fun(Obj) ->
try Module:transform(Obj)
catch ?EX_RULE(Class, Reason, St) ->
try
Module:transform(Obj)
catch
?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to transform Mnesia table ~ts:~n"
"** Record: ~p~n"
"** ~ts",
[Name, Obj,
[Name,
Obj,
misc:format_exception(2, Class, Reason, StackTrace)]),
erlang:raise(Class, Reason, StackTrace)
end
end.
transform_table(Module, Name) ->
Type = mnesia:table_info(Name, type),
Attrs = mnesia:table_info(Name, attributes),
TmpTab = list_to_atom(atom_to_list(Name) ++ "_backup"),
StorageType = if Type == ordered_set -> disc_copies;
StorageType = if
Type == ordered_set -> disc_copies;
true -> disc_only_copies
end,
mnesia:create_table(TmpTab,
@ -409,11 +474,14 @@ transform_table(Module, Name) ->
mnesia:delete_table(TmpTab),
Res.
do_transform_table(Name, _Fun, TmpTab, '$end_of_table') ->
mnesia:foldl(
fun(Obj, _) ->
mnesia:write(Name, Obj, write)
end, ok, TmpTab);
end,
ok,
TmpTab);
do_transform_table(Name, Fun, TmpTab, Key) ->
Next = mnesia:next(Name, Key),
Objs = mnesia:read(Name, Key),
@ -421,9 +489,11 @@ do_transform_table(Name, Fun, TmpTab, Key) ->
fun(Obj) ->
mnesia:write(TmpTab, Fun(Obj), write),
mnesia:delete_object(Obj)
end, Objs),
end,
Objs),
do_transform_table(Name, Fun, TmpTab, Next).
mnesia_op(Fun, Args) ->
case apply(mnesia, Fun, Args) of
{atomic, ok} ->
@ -434,6 +504,7 @@ mnesia_op(Fun, Args) ->
Other
end.
schema_path() ->
Dir = case os:getenv("EJABBERD_MNESIA_SCHEMA") of
false -> mnesia:system_info(directory);
@ -441,20 +512,24 @@ schema_path() ->
end,
filename:join(Dir, "ejabberd.schema").
is_storage_type_option({O, _}) ->
O == ram_copies orelse O == disc_copies orelse O == disc_only_copies.
dump_schema() ->
File = schema_path(),
Schema = lists:flatmap(
fun(schema) ->
[];
(Tab) ->
[{Tab, [{storage_type,
[{Tab,
[{storage_type,
mnesia:table_info(Tab, storage_type)},
{local_content,
mnesia:table_info(Tab, local_content)}]}]
end, mnesia:system_info(tables)),
end,
mnesia:system_info(tables)),
case file:write_file(File, [fast_yaml:encode(Schema), io_lib:nl()]) of
ok ->
io:format("Mnesia schema is written to ~ts~n", [File]);

View file

@ -29,8 +29,12 @@
-behaviour(gen_server).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-export([start_link/0,
get_client_identity/2,
@ -40,16 +44,16 @@
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
check_token/1,
check_token/4,
check_token/2,
check_token/1, check_token/4, check_token/2,
scope_in_scope_list/2,
process/2,
config_reloaded/0,
verify_resowner_scope/3]).
-export([get_commands_spec/0,
oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1,
oauth_issue_token/3,
oauth_list_tokens/0,
oauth_revoke_token/1,
oauth_add_client_password/3,
oauth_add_client_implicit/3,
oauth_remove_client/1]).
@ -57,6 +61,7 @@
-export([web_menu_main/2, web_page_main/2]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
@ -64,6 +69,7 @@
-include("ejabberd_commands.hrl").
-include("translate.hrl").
-callback init() -> any().
-callback store(#oauth_token{}) -> ok | {error, any()}.
-callback lookup(binary()) -> {ok, #oauth_token{}} | error.
@ -80,12 +86,15 @@
%% * 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).
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
[#ejabberd_commands{
name = oauth_issue_token,
tags = [oauth],
desc = "Issue an OAuth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
args = [{jid, string},{ttl, integer}, {scopes, string}],
module = ?MODULE,
function = oauth_issue_token,
args = [{jid, string}, {ttl, integer}, {scopes, string}],
policy = restricted,
args_example = ["user@server.com", 3600, "connected_users_number;muc_online_rooms"],
args_desc = ["Jid for which issue token",
@ -93,9 +102,12 @@ get_commands_spec() ->
"List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
#ejabberd_commands{
name = oauth_issue_token,
tags = [oauth],
desc = "Issue an OAuth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
module = ?MODULE,
function = oauth_issue_token,
version = 1,
note = "updated in 24.02",
args = [{jid, string}, {ttl, integer}, {scopes, {list, {scope, binary}}}],
@ -106,59 +118,74 @@ get_commands_spec() ->
"List of scopes to allow"],
result = {result, {tuple, [{token, string}, {scopes, {list, {scope, string}}}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
#ejabberd_commands{
name = oauth_list_tokens,
tags = [oauth],
desc = "List OAuth tokens, user, scope, and seconds to expire (only Mnesia)",
longdesc = "List _`oauth.md|OAuth`_ tokens, their user and scope, and how many seconds remain until expiry",
module = ?MODULE, function = oauth_list_tokens,
module = ?MODULE,
function = oauth_list_tokens,
args = [],
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
#ejabberd_commands{
name = oauth_revoke_token,
tags = [oauth],
desc = "Revoke authorization for an OAuth token",
note = "changed in 22.05",
module = ?MODULE, function = oauth_revoke_token,
module = ?MODULE,
function = oauth_revoke_token,
args = [{token, binary}],
policy = restricted,
result = {res, restuple},
result_desc = "Result code"
},
#ejabberd_commands{name = oauth_add_client_password, tags = [oauth],
#ejabberd_commands{
name = oauth_add_client_password,
tags = [oauth],
desc = "Add OAuth client_id with password grant type",
module = ?MODULE, function = oauth_add_client_password,
module = ?MODULE,
function = oauth_add_client_password,
args = [{client_id, binary},
{client_name, binary},
{secret, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth],
#ejabberd_commands{
name = oauth_add_client_implicit,
tags = [oauth],
desc = "Add OAuth client_id with implicit grant type",
module = ?MODULE, function = oauth_add_client_implicit,
module = ?MODULE,
function = oauth_add_client_implicit,
args = [{client_id, binary},
{client_name, binary},
{redirect_uri, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_remove_client, tags = [oauth],
#ejabberd_commands{
name = oauth_remove_client,
tags = [oauth],
desc = "Remove OAuth client_id",
module = ?MODULE, function = oauth_remove_client,
module = ?MODULE,
function = oauth_remove_client,
args = [{client_id, binary}],
policy = restricted,
result = {res, restuple}
}
].
}].
oauth_issue_token(Jid, TTLSeconds, [Head|_] = ScopesString) when is_integer(Head) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
oauth_issue_token(Jid, TTLSeconds, [Head | _] = ScopesString) when is_integer(Head) ->
Scopes = [ list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";") ],
oauth_issue_token(Jid, TTLSeconds, Scopes);
oauth_issue_token(Jid, TTLSeconds, Scopes) ->
try jid:decode(list_to_binary(Jid)) of
#jid{luser =Username, lserver = Server} ->
#jid{luser = Username, lserver = Server} ->
Ctx1 = #oauth_ctx{password = admin_generated},
case oauth2:authorize_password({Username, Server}, Scopes, Ctx1) of
{ok, {_Ctx,Authorization}} ->
{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),
@ -166,50 +193,61 @@ oauth_issue_token(Jid, TTLSeconds, Scopes) ->
{error, Error} ->
{error, Error}
end
catch _:{bad_jid, _} ->
catch
_:{bad_jid, _} ->
{error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
[{Token, jid:encode(jid:make(U,S)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
#oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
[ {Token, jid:encode(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) ->
DBMod = get_db_backend(),
case DBMod:revoke(Token) of
ok ->
ets_cache:delete(oauth_cache, Token,
ets_cache:delete(oauth_cache,
Token,
ejabberd_cluster:get_nodes()),
{ok, ""};
Other ->
Other
end.
oauth_add_client_password(ClientID, ClientName, Secret) ->
DBMod = get_db_backend(),
DBMod:store_client(#oauth_client{client_id = ClientID,
DBMod:store_client(#oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = password,
options = [{secret, Secret}]}),
options = [{secret, Secret}]
}),
{ok, []}.
oauth_add_client_implicit(ClientID, ClientName, RedirectURI) ->
DBMod = get_db_backend(),
DBMod:store_client(#oauth_client{client_id = ClientID,
DBMod:store_client(#oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = implicit,
options = [{redirect_uri, RedirectURI}]}),
options = [{redirect_uri, RedirectURI}]
}),
{ok, []}.
oauth_remove_client(Client) ->
DBMod = get_db_backend(),
DBMod:remove_client(Client),
{ok, []}.
config_reloaded() ->
DBMod = get_db_backend(),
case init_cache(DBMod) of
@ -219,6 +257,7 @@ config_reloaded() ->
ok
end.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@ -238,38 +277,46 @@ init([]) ->
erlang:send_after(expire(), self(), clean),
{ok, ok}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
DBMod = get_db_backend(),
DBMod:clean(TS),
erlang:send_after(trunc(expire() * (1 + MiniSecs / 1000000)),
self(), clean),
self(),
clean),
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50),
ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50),
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
get_client_identity(<<"">>, Ctx) ->
{ok, {Ctx, {client, unknown_client}}};
get_client_identity(ClientID, Ctx) when is_binary(ClientID) ->
{ok, {Ctx, {client, ClientID}}}.
verify_redirection_uri(_ClientID, RedirectURI, Ctx) ->
case Ctx of
#oauth_ctx{client = #oauth_client{grant_type = implicit} = Client} ->
@ -285,6 +332,7 @@ verify_redirection_uri(_ClientID, RedirectURI, Ctx) ->
{ok, Ctx}
end.
authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server) of
#jid{} = JID ->
@ -311,6 +359,7 @@ authenticate_user({User, Server}, Ctx) ->
{error, badpass}
end.
authenticate_client(ClientID, Ctx) ->
case ejabberd_option:oauth_client_id_check() of
allow ->
@ -326,10 +375,11 @@ authenticate_client(ClientID, Ctx) ->
end
end.
-spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) ->
{ok, any(), [binary()]} | {error, any()}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
Cmds = [atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands()],
Cmds = [ atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands() ],
AllowedScopes = [<<"ejabberd:user">>, <<"ejabberd:admin">>, <<"sasl_auth">>] ++ Cmds,
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(AllowedScopes)) of
@ -341,6 +391,7 @@ verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
verify_resowner_scope(_, _, _) ->
{error, badscope}.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
@ -354,8 +405,6 @@ verify_resowner_scope(_, _, _) ->
% end.
-spec seconds_since_epoch(integer()) -> non_neg_integer().
seconds_since_epoch(Diff) ->
{Mega, Secs, _} = os:timestamp(),
@ -366,6 +415,7 @@ associate_access_code(_AccessCode, _Context, AppContext) ->
%put(?ACCESS_CODE_TABLE, AccessCode, Context),
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
{user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
@ -388,23 +438,29 @@ associate_access_token(AccessToken, Context, AppContext) ->
store(R),
{ok, AppContext}.
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,
oauth2_priv_set:is_member(Scope2, TokenScopeSet)
end,
ScopeList).
-spec check_token(binary()) -> {ok, {binary(), binary()}, [binary()]} |
{false, expired | not_found}.
check_token(Token) ->
case lookup(Token) of
{ok, #oauth_token{us = US,
{ok, #oauth_token{
us = US,
scope = TokenScope,
expire = Expire}} ->
expire = Expire
}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
@ -417,20 +473,24 @@ check_token(Token) ->
{false, not_found}
end.
check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
case lookup(Token) of
{ok, #oauth_token{us = {LUser, LServer},
{ok, #oauth_token{
us = {LUser, LServer},
scope = TokenScope,
expire = Expire}} ->
expire = Expire
}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
oauth2_priv_set:is_member(Scope, TokenScopeSet)
end,
ScopeList);
true ->
{false, expired}
@ -439,18 +499,22 @@ check_token(User, Server, ScopeList, Token) ->
{false, not_found}
end.
check_token(ScopeList, Token) ->
case lookup(Token) of
{ok, #oauth_token{us = US,
{ok, #oauth_token{
us = US,
scope = TokenScope,
expire = Expire}} ->
expire = Expire
}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
case lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
oauth2_priv_set:is_member(Scope, TokenScopeSet)
end,
ScopeList) of
true -> {ok, user, US};
false -> {false, no_matching_scope}
@ -467,19 +531,23 @@ store(R) ->
DBMod = get_db_backend(),
case DBMod:store(R) of
ok ->
ets_cache:delete(oauth_cache, R#oauth_token.token,
ets_cache:delete(oauth_cache,
R#oauth_token.token,
ejabberd_cluster:get_nodes());
{error, _} = Err ->
Err
end.
lookup(Token) ->
ets_cache:lookup(oauth_cache, Token,
ets_cache:lookup(oauth_cache,
Token,
fun() ->
DBMod = get_db_backend(),
DBMod:lookup(Token)
end).
-spec init_cache(module()) -> boolean().
init_cache(DBMod) ->
UseCache = use_cache(DBMod),
@ -491,33 +559,44 @@ init_cache(DBMod) ->
end,
UseCache.
use_cache(DBMod) ->
case erlang:function_exported(DBMod, use_cache, 0) of
true -> DBMod:use_cache();
false -> ejabberd_option:oauth_use_cache()
end.
cache_opts() ->
MaxSize = ejabberd_option:oauth_cache_size(),
CacheMissed = ejabberd_option:oauth_cache_missed(),
LifeTime = ejabberd_option:oauth_cache_life_time(),
[{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}].
expire() ->
ejabberd_option:oauth_expire().
-define(DIV(Class, Els),
?XAE(<<"div">>, [{<<"class">>, Class}], Els)).
-define(INPUTID(Type, Name, Value),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"value">>, Value}, {<<"id">>, Name}])).
[{<<"type">>, Type},
{<<"name">>, Name},
{<<"value">>, Value},
{<<"id">>, Name}])).
-define(LABEL(ID, Els),
?XAE(<<"label">>, [{<<"for">>, ID}], Els)).
process(_Handlers,
#request{method = 'GET', q = Q, lang = Lang,
path = [_, <<"authorization_token">>]}) ->
#request{
method = 'GET',
q = Q,
lang = Lang,
path = [_, <<"authorization_token">>]
}) ->
ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>),
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
JidEls = case proplists:get_value(<<"jid">>, Q, <<"">>) of
@ -531,9 +610,7 @@ process(_Handlers,
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
[?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)])
] ++ JidEls ++ [
?BR,
[?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)])] ++ JidEls ++ [?BR,
?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
@ -543,24 +620,22 @@ process(_Handlers,
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
?LABEL(<<"ttl">>, [?CT(?T("Token TTL")), ?C(<<": ">>)]),
?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">>)]),
?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">>, <<"">>, ?T("Accept"))
]),
?INPUTT(<<"submit">>, <<"">>, ?T("Accept"))]),
Top =
?DIV(<<"section">>,
[?DIV(<<"block">>,
[?A(<<"https://www.ejabberd.im">>,
[?XA(<<"img">>,
[{<<"height">>, <<"32">>},
{<<"src">>, logo()}])]
)])]),
{<<"src">>, logo()}])])])]),
Middle =
?DIV(<<"white section">>,
[?DIV(<<"block">>,
@ -570,8 +645,7 @@ process(_Handlers,
?XC(<<"em">>, ClientId),
?C(<<" wants to access scope ">>),
?XC(<<"em">>, Scope)]),
Form
])]),
Form])]),
Bottom =
?DIV(<<"section">>,
[?DIV(<<"block">>,
@ -583,13 +657,16 @@ process(_Handlers,
?XAC(<<"a">>,
[{<<"href">>, <<"https://www.process-one.net">>},
{<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}],
<<"ProcessOne">>)
])]),
<<"ProcessOne">>)])]),
Body = ?DIV(<<"container">>, [Top, Middle, Bottom]),
ejabberd_web:make_xhtml(web_head(), [Body]);
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
path = [_, <<"authorization_token">>]}) ->
#request{
method = 'POST',
q = Q,
lang = _Lang,
path = [_, <<"authorization_token">>]
}) ->
_ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>),
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
@ -612,7 +689,7 @@ process(_Handlers,
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined]),
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
@ -633,38 +710,50 @@ process(_Handlers,
% VerifiedScope,
% State,
% Context);
{302, [{<<"Location">>,
{302,
[{<<"Location">>,
<<RedirectURI/binary,
"?access_token=", AccessToken/binary,
"&token_type=", Type/binary,
"&expires_in=", (integer_to_binary(Expires))/binary,
"&scope=", (str:join(VerifiedScope, <<" ">>))/binary,
"&state=", State/binary>>
}],
"?access_token=",
AccessToken/binary,
"&token_type=",
Type/binary,
"&expires_in=",
(integer_to_binary(Expires))/binary,
"&scope=",
(str:join(VerifiedScope, <<" ">>))/binary,
"&state=",
State/binary>>}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])};
{error, Error} when is_atom(Error) ->
%oauth2_wrq:redirected_error_response(
% ReqData, RedirectURI, Error, State, Context)
{302, [{<<"Location">>,
{302,
[{<<"Location">>,
<<RedirectURI/binary,
"?error=", (atom_to_binary(Error, utf8))/binary,
"&state=", State/binary>>
}],
"?error=",
(atom_to_binary(Error, utf8))/binary,
"&state=",
State/binary>>}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end
catch _:{bad_jid, _} ->
catch
_:{bad_jid, _} ->
State = proplists:get_value(<<"state">>, Q, <<"">>),
{400, [{<<"Location">>,
{400,
[{<<"Location">>,
<<RedirectURI/binary,
"?error=invalid_request",
"&state=", State/binary>>
}],
"&state=", State/binary>>}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"400 Invalid request">>)])}
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
#request{
method = 'POST',
q = Q,
lang = _Lang,
auth = HTTPAuth,
path = [_, <<"token">>]}) ->
path = [_, <<"token">>]
}) ->
Access =
case ejabberd_option:oauth_client_id_check() of
allow ->
@ -694,8 +783,7 @@ process(_Handlers,
case get_client_secret(Client) of
Secret ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
<<"password">> when
Client#oauth_client.grant_type == password ->
<<"password">> when Client#oauth_client.grant_type == password ->
password;
_ ->
unsupported_grant_type
@ -725,7 +813,7 @@ process(_Handlers,
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined]),
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
@ -738,18 +826,23 @@ process(_Handlers,
ExpiresIn
end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
json_response(200, #{<<"access_token">> => AccessToken,
json_response(200,
#{
<<"access_token">> => AccessToken,
<<"token_type">> => Type,
<<"scope">> => str:join(VerifiedScope, <<" ">>),
<<"expires_in">> => Expires});
<<"expires_in">> => Expires
});
{error, Error} when is_atom(Error) ->
json_error(400, <<"invalid_grant">>, Error)
end
catch _:{bad_jid, _} ->
catch
_:{bad_jid, _} ->
json_error(400, <<"invalid_request">>, invalid_jid)
end;
unsupported_grant_type ->
json_error(400, <<"unsupported_grant_type">>,
json_error(400,
<<"unsupported_grant_type">>,
unsupported_grant_type);
deny ->
ejabberd_web:error(not_allowed)
@ -758,48 +851,60 @@ process(_Handlers,
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
-spec get_db_backend() -> module().
get_db_backend() ->
DBType = ejabberd_option:oauth_db_type(),
list_to_existing_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
get_client_secret(#oauth_client{grant_type = password, options = Options}) ->
proplists:get_value(secret, Options, false).
get_redirect_uri(#oauth_client{grant_type = implicit, options = Options}) ->
proplists:get_value(redirect_uri, Options, false).
%% Headers as per RFC 6749
json_response(Code, Body) ->
{Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
{Code,
[{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
{<<"Cache-Control">>, <<"no-store">>},
{<<"Pragma">>, <<"no-cache">>}],
misc:json_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},
Body = #{
<<"error">> => Error,
<<"error_description">> => Desc
},
json_response(Code, Body).
json_error_desc(access_denied) -> <<"Access denied">>;
json_error_desc(badpass) -> <<"Bad password">>;
json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
json_error_desc(invalid_scope) -> <<"Invalid scope">>;
json_error_desc(invalid_jid) -> <<"Invalid JID">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
[?XA(<<"meta">>,
[{<<"http-equiv">>, <<"X-UA-Compatible">>},
{<<"content">>, <<"IE=edge">>}]),
?XA(<<"meta">>, [{<<"name">>, <<"viewport">>},
?XA(<<"meta">>,
[{<<"name">>, <<"viewport">>},
{<<"content">>,
<<"width=device-width, initial-scale=1">>}]),
?XC(<<"title">>, <<"Authorization request">>),
?XC(<<"style">>, css())
].
?XC(<<"style">>, css())].
css() ->
case misc:read_css("oauth.css") of
@ -807,6 +912,7 @@ css() ->
{error, _} -> <<>>
end.
logo() ->
case misc:read_img("oauth-logo.png") of
{ok, Img} ->
@ -816,15 +922,18 @@ logo() ->
<<>>
end.
%%%
%%% WebAdmin
%%%
%% @format-begin
web_menu_main(Acc, _Lang) ->
Acc ++ [{<<"oauth">>, <<"OAuth">>}].
web_page_main(_, #request{path = [<<"oauth">>]} = R) ->
Head = ?H1GLraw(<<"OAuth">>, <<"developer/ejabberd-api/oauth/">>, <<"OAuth">>),
Set = [?X(<<"hr">>),

View file

@ -34,21 +34,26 @@
lookup_client/1,
store_client/1,
remove_client/1,
use_cache/0, revoke/1]).
use_cache/0,
revoke/1]).
-include("ejabberd_oauth.hrl").
init() ->
ejabberd_mnesia:create(?MODULE, oauth_token,
ejabberd_mnesia:create(?MODULE,
oauth_token,
[{disc_only_copies, [node()]},
{attributes,
record_info(fields, oauth_token)}]),
ejabberd_mnesia:create(?MODULE, oauth_client,
ejabberd_mnesia:create(?MODULE,
oauth_client,
[{disc_copies, [node()]},
{attributes,
record_info(fields, oauth_client)}]),
ok.
use_cache() ->
case mnesia:table_info(oauth_token, storage_type) of
disc_only_copies ->
@ -57,9 +62,11 @@ use_cache() ->
false
end.
store(R) ->
mnesia:dirty_write(R).
lookup(Token) ->
case catch mnesia:dirty_read(oauth_token, Token) of
[R] ->
@ -73,6 +80,7 @@ lookup(Token) ->
revoke(Token) ->
mnesia:dirty_delete(oauth_token, Token).
clean(TS) ->
F = fun() ->
Ts = mnesia:select(
@ -84,6 +92,7 @@ clean(TS) ->
end,
mnesia:async_dirty(F).
lookup_client(ClientID) ->
case catch mnesia:dirty_read(oauth_client, ClientID) of
[R] ->
@ -92,8 +101,10 @@ lookup_client(ClientID) ->
error
end.
remove_client(ClientID) ->
mnesia:dirty_delete(oauth_client, ClientID).
store_client(R) ->
mnesia:dirty_write(R).

View file

@ -32,16 +32,20 @@
lookup/1,
clean/1,
lookup_client/1,
store_client/1, revoke/1]).
store_client/1,
revoke/1]).
-include("ejabberd_oauth.hrl").
-include("logger.hrl").
-include_lib("xmpp/include/jid.hrl").
init() ->
rest:start(ejabberd_config:get_myname()),
ok.
store(R) ->
Path = path(<<"store">>),
%% Retry 2 times, with a backoff of 500millisec
@ -49,12 +53,17 @@ store(R) ->
SJID = jid:encode({User, Server, <<"">>}),
case rest:with_retry(
post,
[ejabberd_config:get_myname(), Path, [],
#{<<"token">> => R#oauth_token.token,
[ejabberd_config:get_myname(),
Path,
[],
#{
<<"token">> => R#oauth_token.token,
<<"user">> => SJID,
<<"scope">> => R#oauth_token.scope,
<<"expire">> => R#oauth_token.expire
}], 2, 500) of
}],
2,
500) of
{ok, Code, _} when Code == 200 orelse Code == 201 ->
ok;
Err ->
@ -62,11 +71,16 @@ store(R) ->
{error, db_failure}
end.
lookup(Token) ->
Path = path(<<"lookup">>),
case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [],
case rest:with_retry(post,
[ejabberd_config:get_myname(),
Path,
[],
#{<<"token">> => Token}],
2, 500) of
2,
500) of
{ok, 200, Data} ->
SJID = case maps:find(<<"user">>, Data) of
{ok, U} -> U;
@ -82,10 +96,12 @@ lookup(Token) ->
{ok, E} -> E;
error -> 0
end,
{ok, #oauth_token{token = Token,
{ok, #oauth_token{
token = Token,
us = US,
scope = Scope,
expire = Expire}};
expire = Expire
}};
{ok, 404, _Resp} ->
error;
Other ->
@ -96,21 +112,27 @@ lookup(Token) ->
end
end.
-spec revoke(binary()) -> ok | {error, binary()}.
revoke(_Token) ->
{error, <<"not available">>}.
clean(_TS) ->
ok.
path(Path) ->
Base = ejabberd_option:ext_api_path_oauth(),
<<Base/binary, "/", Path/binary>>.
store_client(#oauth_client{client_id = ClientID,
store_client(#oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = GrantType,
options = Options} = R) ->
options = Options
} = R) ->
Path = path(<<"store_client">>),
SGrantType =
case GrantType of
@ -121,12 +143,17 @@ store_client(#oauth_client{client_id = ClientID,
%% Retry 2 times, with a backoff of 500millisec
case rest:with_retry(
post,
[ejabberd_config:get_myname(), Path, [],
#{<<"client_id">> => ClientID,
[ejabberd_config:get_myname(),
Path,
[],
#{
<<"client_id">> => ClientID,
<<"client_name">> => ClientName,
<<"grant_type">> => SGrantType,
<<"options">> => SOptions
}], 2, 500) of
}],
2,
500) of
{ok, Code, _} when Code == 200 orelse Code == 201 ->
ok;
Err ->
@ -134,11 +161,16 @@ store_client(#oauth_client{client_id = ClientID,
{error, db_failure}
end.
lookup_client(ClientID) ->
Path = path(<<"lookup_client">>),
case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [],
case rest:with_retry(post,
[ejabberd_config:get_myname(),
Path,
[],
#{<<"client_id">> => ClientID}],
2, 500) of
2,
500) of
{ok, 200, Data} ->
ClientName = case maps:find(<<"client_name">>, Data) of
{ok, CN} -> CN;
@ -159,10 +191,12 @@ lookup_client(ClientID) ->
end,
case misc:base64_to_term(SOptions) of
{term, Options} ->
{ok, #oauth_client{client_id = ClientID,
{ok, #oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = GrantType,
options = Options}};
options = Options
}};
_ ->
error
end;

View file

@ -33,19 +33,24 @@
clean/1,
lookup_client/1,
store_client/1,
remove_client/1, revoke/1]).
remove_client/1,
revoke/1]).
-export([sql_schemas/0]).
-include("ejabberd_oauth.hrl").
-include("ejabberd_sql_pt.hrl").
-include_lib("xmpp/include/jid.hrl").
-include("logger.hrl").
init() ->
ejabberd_sql_schema:update_schema(
ejabberd_config:get_myname(), ?MODULE, sql_schemas()),
ok.
sql_schemas() ->
[#sql_schema{
version = 1,
@ -59,7 +64,9 @@ sql_schemas() ->
#sql_column{name = <<"expire">>, type = bigint}],
indices = [#sql_index{
columns = [<<"token">>],
unique = true}]},
unique = true
}]
},
#sql_table{
name = <<"oauth_client">>,
columns =
@ -69,7 +76,11 @@ sql_schemas() ->
#sql_column{name = <<"options">>, type = text}],
indices = [#sql_index{
columns = [<<"client_id">>],
unique = true}]}]}].
unique = true
}]
}]
}].
store(R) ->
Token = R#oauth_token.token,
@ -90,6 +101,7 @@ store(R) ->
{error, db_failure}
end.
lookup(Token) ->
case ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
@ -98,14 +110,17 @@ lookup(Token) ->
{selected, [{SJID, Scope, Expire}]} ->
JID = jid:decode(SJID),
US = {JID#jid.luser, JID#jid.lserver},
{ok, #oauth_token{token = Token,
{ok, #oauth_token{
token = Token,
us = US,
scope = str:tokens(Scope, <<" ">>),
expire = Expire}};
expire = Expire
}};
_ ->
error
end.
revoke(Token) ->
case ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
@ -116,11 +131,13 @@ revoke(Token) ->
ok
end.
clean(TS) ->
ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
?SQL("delete from oauth_token where expire < %(TS)d")).
lookup_client(ClientID) ->
case ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
@ -134,10 +151,12 @@ lookup_client(ClientID) ->
end,
case misc:base64_to_term(SOptions) of
{term, Options} ->
{ok, #oauth_client{client_id = ClientID,
{ok, #oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = GrantType,
options = Options}};
options = Options
}};
_ ->
error
end;
@ -145,10 +164,13 @@ lookup_client(ClientID) ->
error
end.
store_client(#oauth_client{client_id = ClientID,
store_client(#oauth_client{
client_id = ClientID,
client_name = ClientName,
grant_type = GrantType,
options = Options}) ->
options = Options
}) ->
SGrantType =
case GrantType of
password -> <<"password">>;
@ -168,6 +190,7 @@ store_client(#oauth_client{client_id = ClientID,
{error, db_failure}
end.
remove_client(Client) ->
ejabberd_sql:sql_query(
ejabberd_config:get_myname(),

View file

@ -25,6 +25,7 @@
-include("logger.hrl").
%%%===================================================================
%%% API
%%%===================================================================
@ -41,6 +42,7 @@ read_file(File) ->
{error, {old_config, File, Reason}}
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -52,10 +54,13 @@ collect_options(Opts) ->
({K, V}, {D, Os}) ->
{orddict:store(K, V, D), Os};
(Opt, {D, Os}) ->
{D, [Opt|Os]}
end, {orddict:new(), []}, Opts),
{D, [Opt | Os]}
end,
{orddict:new(), []},
Opts),
InvalidOpts ++ orddict:to_list(D).
transform(Opts) ->
Opts1 = transform_register(Opts),
Opts2 = transform_s2s(Opts1),
@ -68,6 +73,7 @@ transform(Opts) ->
Opts10 = transform_globals(Opts9),
collect_options(Opts10).
%%%===================================================================
%%% mod_register
%%%===================================================================
@ -80,7 +86,8 @@ transform_register(Opts) ->
?WARNING_MSG("Old 'ip_access' format detected. "
"The old format is still supported "
"but it is better to fix your config: "
"use access rules instead.", []),
"use access rules instead.",
[]),
ACLs = lists:flatmap(
fun({Action, S}) ->
ACLName = misc:binary_to_atom(
@ -88,43 +95,46 @@ transform_register(Opts) ->
["ip_", S])),
[{Action, ACLName},
{acl, ACLName, {ip, S}}]
end, L),
end,
L),
Access = {access, mod_register_networks,
[{Action, ACLName} || {Action, ACLName} <- ACLs]},
[ACL || {acl, _, _} = ACL <- ACLs] ++
[ {Action, ACLName} || {Action, ACLName} <- ACLs ]},
[ ACL || {acl, _, _} = ACL <- ACLs ] ++
[Access,
{modules,
[{mod_register,
[{ip_access, mod_register_networks}|RegOpts1]}
| ModOpts1]}|Opts1]
catch error:{badmatch, false} ->
[{ip_access, mod_register_networks} | RegOpts1]} | ModOpts1]} | Opts1]
catch
error:{badmatch, false} ->
Opts
end.
%%%===================================================================
%%% ejabberd_s2s
%%%===================================================================
transform_s2s(Opts) ->
lists:foldl(fun transform_s2s/2, [], Opts).
transform_s2s({{s2s_host, Host}, Action}, Opts) ->
?WARNING_MSG("Option 's2s_host' is deprecated.", []),
ACLName = misc:binary_to_atom(
iolist_to_binary(["s2s_access_", Host])),
[{acl, ACLName, {server, Host}},
{access, s2s, [{Action, ACLName}]},
{s2s_access, s2s} |
Opts];
{s2s_access, s2s} | Opts];
transform_s2s({s2s_default_policy, Action}, Opts) ->
?WARNING_MSG("Option 's2s_default_policy' is deprecated. "
"The option is still supported but it is better to "
"fix your config: "
"use 's2s_access' with an access rule.", []),
"use 's2s_access' with an access rule.",
[]),
[{access, s2s, [{Action, all}]},
{s2s_access, s2s} |
Opts];
{s2s_access, s2s} | Opts];
transform_s2s(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% ejabberd_s2s_out
@ -132,31 +142,36 @@ transform_s2s(Opt, Opts) ->
transform_s2s_out(Opts) ->
lists:foldl(fun transform_s2s_out/2, [], Opts).
transform_s2s_out({outgoing_s2s_options, Families, Timeout}, Opts) ->
?WARNING_MSG("Option 'outgoing_s2s_options' is deprecated. "
"The option is still supported "
"but it is better to fix your config: "
"use 'outgoing_s2s_timeout' and "
"'outgoing_s2s_families' instead.", []),
"'outgoing_s2s_families' instead.",
[]),
[{outgoing_s2s_families, Families},
{outgoing_s2s_timeout, Timeout}
| Opts];
{outgoing_s2s_timeout, Timeout} | Opts];
transform_s2s_out({s2s_dns_options, S2SDNSOpts}, AllOpts) ->
?WARNING_MSG("Option 's2s_dns_options' is deprecated. "
"The option is still supported "
"but it is better to fix your config: "
"use 's2s_dns_timeout' and "
"'s2s_dns_retries' instead", []),
"'s2s_dns_retries' instead",
[]),
lists:foldr(
fun({timeout, T}, AccOpts) ->
[{s2s_dns_timeout, T}|AccOpts];
[{s2s_dns_timeout, T} | AccOpts];
({retries, R}, AccOpts) ->
[{s2s_dns_retries, R}|AccOpts];
[{s2s_dns_retries, R} | AccOpts];
(_, AccOpts) ->
AccOpts
end, AllOpts, S2SDNSOpts);
end,
AllOpts,
S2SDNSOpts);
transform_s2s_out(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% ejabberd_listener
@ -164,13 +179,16 @@ transform_s2s_out(Opt, Opts) ->
transform_listeners(Opts) ->
lists:foldl(fun transform_listeners/2, [], Opts).
transform_listeners({listen, LOpts}, Opts) ->
[{listen, lists:map(fun transform_listener/1, LOpts)} | Opts];
transform_listeners(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
transform_listener({{Port, IP, Transport}, Mod, Opts}) ->
IPStr = if is_tuple(IP) ->
IPStr = if
is_tuple(IP) ->
list_to_binary(inet_parse:ntoa(IP));
true ->
IP
@ -181,15 +199,20 @@ transform_listener({{Port, IP, Transport}, Mod, Opts}) ->
(ssl) -> {tls, true};
(A) when is_atom(A) -> {A, true};
(Opt) -> Opt
end, Opts),
end,
Opts),
Opts2 = lists:foldl(
fun(Opt, Acc) ->
transform_listen_option(Mod, Opt, Acc)
end, [], Opts1),
TransportOpt = if Transport == tcp -> [];
end,
[],
Opts1),
TransportOpt = if
Transport == tcp -> [];
true -> [{transport, Transport}]
end,
IPOpt = if IPStr == <<"0.0.0.0">> -> [];
IPOpt = if
IPStr == <<"0.0.0.0">> -> [];
true -> [{ip, IPStr}]
end,
IPOpt ++ TransportOpt ++ [{port, Port}, {module, Mod} | Opts2];
@ -203,24 +226,26 @@ transform_listener({Port, Mod, Opts}) ->
transform_listener(Opt) ->
Opt.
transform_listen_option(ejabberd_http, captcha, Opts) ->
[{captcha, true}|Opts];
[{captcha, true} | Opts];
transform_listen_option(ejabberd_http, register, Opts) ->
[{register, true}|Opts];
[{register, true} | Opts];
transform_listen_option(ejabberd_http, web_admin, Opts) ->
[{web_admin, true}|Opts];
[{web_admin, true} | Opts];
transform_listen_option(ejabberd_http, http_bind, Opts) ->
[{http_bind, true}|Opts];
[{http_bind, true} | Opts];
transform_listen_option(ejabberd_http, http_poll, Opts) ->
[{http_poll, true}|Opts];
[{http_poll, true} | Opts];
transform_listen_option(ejabberd_http, {request_handlers, Hs}, Opts) ->
Hs1 = lists:map(
fun({PList, Mod}) when is_list(PList) ->
Path = iolist_to_binary([[$/, P] || P <- PList]),
Path = iolist_to_binary([ [$/, P] || P <- PList ]),
{Path, Mod};
(Opt) ->
Opt
end, Hs),
end,
Hs),
[{request_handlers, Hs1} | Opts];
transform_listen_option(ejabberd_service, {hosts, Hosts, O}, Opts) ->
case lists:keyfind(hosts, 1, Opts) of
@ -229,11 +254,12 @@ transform_listen_option(ejabberd_service, {hosts, Hosts, O}, Opts) ->
lists:foldl(
fun(H, Acc) ->
dict:append_list(H, O, Acc)
end, dict:from_list(PrevHostOpts), Hosts),
[{hosts, dict:to_list(NewHostOpts)}|
lists:keydelete(hosts, 1, Opts)];
end,
dict:from_list(PrevHostOpts),
Hosts),
[{hosts, dict:to_list(NewHostOpts)} | lists:keydelete(hosts, 1, Opts)];
_ ->
[{hosts, [{H, O} || H <- Hosts]}|Opts]
[{hosts, [ {H, O} || H <- Hosts ]} | Opts]
end;
transform_listen_option(ejabberd_service, {host, Host, Os}, Opts) ->
transform_listen_option(ejabberd_service, {hosts, [Host], Os}, Opts);
@ -243,24 +269,28 @@ transform_listen_option(ejabberd_xmlrpc, {access_commands, ACOpts}, Opts) ->
{AName, [{commands, ACmds}, {options, AOpts}]};
(Opt) ->
Opt
end, ACOpts),
[{access_commands, NewACOpts}|Opts];
end,
ACOpts),
[{access_commands, NewACOpts} | Opts];
transform_listen_option(_, Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
-spec all_zero_ip([proplists:property()]) -> inet:ip_address().
all_zero_ip(Opts) ->
case proplists:get_bool(inet6, Opts) of
true -> {0,0,0,0,0,0,0,0};
false -> {0,0,0,0}
true -> {0, 0, 0, 0, 0, 0, 0, 0};
false -> {0, 0, 0, 0}
end.
%%%===================================================================
%%% ejabberd_shaper
%%%===================================================================
transform_shaper(Opts) ->
lists:foldl(fun transform_shaper/2, [], Opts).
transform_shaper({shaper, Name, {maxrate, N}}, Opts) ->
[{shaper, [{Name, N}]} | Opts];
transform_shaper({shaper, Name, none}, Opts) ->
@ -273,11 +303,13 @@ transform_shaper({shaper, List}, Opts) when is_list(List) ->
{Name, MaxRate, BurstSize};
({Name, Val}) ->
{Name, Val, Val}
end, List),
end,
List),
[{shaper, R} | Opts];
transform_shaper(Opt, Opts) ->
[Opt | Opts].
%%%===================================================================
%%% acl
%%%===================================================================
@ -287,26 +319,34 @@ transform_acl(Opts) ->
fun({acl, Os}, Acc) ->
{Os, Acc};
(O, Acc) ->
{[], [O|Acc]}
end, [], Opts1),
{[], [O | Acc]}
end,
[],
Opts1),
{AccessOpts, Opts3} = lists:mapfoldl(
fun({access, Os}, Acc) ->
{Os, Acc};
(O, Acc) ->
{[], [O|Acc]}
end, [], Opts2),
{[], [O | Acc]}
end,
[],
Opts2),
{NewAccessOpts, Opts4} = lists:mapfoldl(
fun({access_rules, Os}, Acc) ->
{Os, Acc};
(O, Acc) ->
{[], [O|Acc]}
end, [], Opts3),
{[], [O | Acc]}
end,
[],
Opts3),
{ShaperOpts, Opts5} = lists:mapfoldl(
fun({shaper_rules, Os}, Acc) ->
{Os, Acc};
(O, Acc) ->
{[], [O|Acc]}
end, [], Opts4),
{[], [O | Acc]}
end,
[],
Opts4),
ACLOpts1 = collect_options(lists:flatten(ACLOpts)),
AccessOpts1 = case collect_options(lists:flatten(AccessOpts)) of
[] -> [];
@ -315,26 +355,30 @@ transform_acl(Opts) ->
ACLOpts2 = case lists:map(
fun({ACLName, Os}) ->
{ACLName, collect_options(Os)}
end, ACLOpts1) of
end,
ACLOpts1) of
[] -> [];
L2 -> [{acl, L2}]
end,
NewAccessOpts1 = case lists:map(
fun({NAName, Os}) ->
{NAName, transform_access_rules_config(Os)}
end, lists:flatten(NewAccessOpts)) of
end,
lists:flatten(NewAccessOpts)) of
[] -> [];
L3 -> [{access_rules, L3}]
end,
ShaperOpts1 = case lists:map(
fun({SName, Ss}) ->
{SName, transform_access_rules_config(Ss)}
end, lists:flatten(ShaperOpts)) of
end,
lists:flatten(ShaperOpts)) of
[] -> [];
L4 -> [{shaper_rules, L4}]
end,
ACLOpts2 ++ AccessOpts1 ++ NewAccessOpts1 ++ ShaperOpts1 ++ Opts5.
transform_acl({acl, Name, Type}, Opts) ->
T = case Type of
all -> all;
@ -357,18 +401,20 @@ transform_acl({acl, Name, Type}, Opts) ->
{resource_glob, R} -> {resource_glob, [b(R)]};
{resource_regexp, R} -> {resource_regexp, [b(R)]}
end,
[{acl, [{Name, [T]}]}|Opts];
[{acl, [{Name, [T]}]} | Opts];
transform_acl({access, Name, Rules}, Opts) ->
NewRules = [{ACL, Action} || {Action, ACL} <- Rules],
[{access, [{Name, NewRules}]}|Opts];
NewRules = [ {ACL, Action} || {Action, ACL} <- Rules ],
[{access, [{Name, NewRules}]} | Opts];
transform_acl(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
transform_access_rules_config(Config) when is_list(Config) ->
lists:map(fun transform_access_rules_config2/1, lists:flatten(Config));
transform_access_rules_config(Config) ->
transform_access_rules_config([Config]).
transform_access_rules_config2(Type) when is_integer(Type); is_atom(Type) ->
{Type, [all]};
transform_access_rules_config2({Type, ACL}) when is_atom(ACL) ->
@ -378,38 +424,43 @@ transform_access_rules_config2({Res, Rules}) when is_list(Rules) ->
{Type, hd(lists:flatten(Args))};
(V) ->
V
end, lists:flatten(Rules)),
end,
lists:flatten(Rules)),
{Res, T};
transform_access_rules_config2({Res, Rule}) ->
{Res, [Rule]}.
%%%===================================================================
%%% SQL
%%%===================================================================
-define(PGSQL_PORT, 5432).
-define(MYSQL_PORT, 3306).
transform_sql(Opts) ->
lists:foldl(fun transform_sql/2, [], Opts).
transform_sql({odbc_server, {Type, Server, Port, DB, User, Pass}}, Opts) ->
[{sql_type, Type},
{sql_server, Server},
{sql_port, Port},
{sql_database, DB},
{sql_username, User},
{sql_password, Pass}|Opts];
{sql_password, Pass} | Opts];
transform_sql({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) ->
transform_sql({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts);
transform_sql({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) ->
transform_sql({odbc_server, {pgsql, Server, ?PGSQL_PORT, DB, User, Pass}}, Opts);
transform_sql({odbc_server, {sqlite, DB}}, Opts) ->
[{sql_type, sqlite},
{sql_database, DB}|Opts];
{sql_database, DB} | Opts];
transform_sql({odbc_pool_size, N}, Opts) ->
[{sql_pool_size, N}|Opts];
[{sql_pool_size, N} | Opts];
transform_sql(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% modules
@ -417,15 +468,18 @@ transform_sql(Opt, Opts) ->
transform_modules(Opts) ->
lists:foldl(fun transform_modules/2, [], Opts).
transform_modules({modules, ModOpts}, Opts) ->
[{modules, lists:map(
fun({Mod, Opts1}) ->
{Mod, transform_module(Mod, Opts1)};
(Other) ->
Other
end, ModOpts)}|Opts];
end,
ModOpts)} | Opts];
transform_modules(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
transform_module(mod_disco, Opts) ->
lists:map(
@ -437,18 +491,21 @@ transform_module(mod_disco, Opts) ->
{urls, URLs}]];
(Opt) ->
Opt
end, Infos),
end,
Infos),
{server_info, NewInfos};
(Opt) ->
Opt
end, Opts);
end,
Opts);
transform_module(mod_muc_log, Opts) ->
lists:map(
fun({top_link, {S1, S2}}) ->
{top_link, [{S1, S2}]};
(Opt) ->
Opt
end, Opts);
end,
Opts);
transform_module(mod_proxy65, Opts) ->
lists:map(
fun({ip, IP}) when is_tuple(IP) ->
@ -457,17 +514,20 @@ transform_module(mod_proxy65, Opts) ->
{hostname, misc:ip_to_list(IP)};
(Opt) ->
Opt
end, Opts);
end,
Opts);
transform_module(mod_register, Opts) ->
lists:flatmap(
fun({welcome_message, {Subj, Body}}) ->
[{welcome_message, [{subject, Subj}, {body, Body}]}];
(Opt) ->
[Opt]
end, Opts);
end,
Opts);
transform_module(_Mod, Opts) ->
Opts.
%%%===================================================================
%%% Host config
%%%===================================================================
@ -477,42 +537,50 @@ transform_host_config(Opts) ->
fun({host_config, O}, Os) ->
{[O], Os};
(O, Os) ->
{[], [O|Os]}
end, [], Opts1),
{[], [O | Os]}
end,
[],
Opts1),
{AHOpts, Opts3} = lists:mapfoldl(
fun({append_host_config, O}, Os) ->
{[O], Os};
(O, Os) ->
{[], [O|Os]}
end, [], Opts2),
{[], [O | Os]}
end,
[],
Opts2),
HOpts1 = case collect_options(lists:flatten(HOpts)) of
[] ->
[];
HOs ->
[{host_config,
[{H, transform(O)} || {H, O} <- HOs]}]
[ {H, transform(O)} || {H, O} <- HOs ]}]
end,
AHOpts1 = case collect_options(lists:flatten(AHOpts)) of
[] ->
[];
AHOs ->
[{append_host_config,
[{H, transform(O)} || {H, O} <- AHOs]}]
[ {H, transform(O)} || {H, O} <- AHOs ]}]
end,
HOpts1 ++ AHOpts1 ++ Opts3.
transform_host_config({host_config, Host, HOpts}, Opts) ->
{AddOpts, HOpts1} =
lists:mapfoldl(
fun({{add, Opt}, Val}, Os) ->
{[{Opt, Val}], Os};
(O, Os) ->
{[], [O|Os]}
end, [], HOpts),
{[], [O | Os]}
end,
[],
HOpts),
[{append_host_config, [{Host, lists:flatten(AddOpts)}]},
{host_config, [{Host, HOpts1}]}|Opts];
{host_config, [{Host, HOpts1}]} | Opts];
transform_host_config(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% Top-level options
@ -520,6 +588,7 @@ transform_host_config(Opt, Opts) ->
transform_globals(Opts) ->
lists:foldl(fun transform_globals/2, [], Opts).
transform_globals(Opt, Opts) when Opt == override_global;
Opt == override_local;
Opt == override_acls ->
@ -529,11 +598,12 @@ transform_globals({node_start, _}, Opts) ->
?WARNING_MSG("Option 'node_start' has no effect anymore", []),
Opts;
transform_globals({iqdisc, {queues, N}}, Opts) ->
[{iqdisc, N}|Opts];
[{iqdisc, N} | Opts];
transform_globals({define_macro, Macro, Val}, Opts) ->
[{define_macro, [{Macro, Val}]}|Opts];
[{define_macro, [{Macro, Val}]} | Opts];
transform_globals(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% Certfiles
@ -541,10 +611,12 @@ transform_globals(Opt, Opts) ->
transform_certfiles(Opts) ->
lists:foldl(fun transform_certfiles/2, [], Opts).
transform_certfiles({domain_certfile, Domain, CertFile}, Opts) ->
[{host_config, [{Domain, [{domain_certfile, CertFile}]}]}|Opts];
[{host_config, [{Domain, [{domain_certfile, CertFile}]}]} | Opts];
transform_certfiles(Opt, Opts) ->
[Opt|Opts].
[Opt | Opts].
%%%===================================================================
%%% Consult file
@ -557,9 +629,11 @@ consult(File) ->
Err
end.
include_config_files(Terms) ->
include_config_files(Terms, []).
include_config_files([], Res) ->
{ok, Res};
include_config_files([{include_config_file, Filename} | Terms], Res) ->
@ -578,6 +652,7 @@ include_config_files([{include_config_file, Filename, Options} | Terms], Res) ->
include_config_files([Term | Terms], Res) ->
include_config_files(Terms, Res ++ [Term]).
delete_disallowed(Disallowed, Terms) ->
lists:foldl(
fun(Dis, Ldis) ->
@ -586,25 +661,29 @@ delete_disallowed(Disallowed, Terms) ->
Terms,
Disallowed).
delete_disallowed2(Disallowed, [H|T]) ->
delete_disallowed2(Disallowed, [H | T]) ->
case element(1, H) of
Disallowed ->
delete_disallowed2(Disallowed, T);
_ ->
[H|delete_disallowed2(Disallowed, T)]
[H | delete_disallowed2(Disallowed, T)]
end;
delete_disallowed2(_, []) ->
[].
keep_only_allowed(all, Terms) ->
Terms;
keep_only_allowed(Allowed, Terms) ->
{As, _NAs} = lists:partition(
fun(Term) ->
lists:member(element(1, Term), Allowed)
end, Terms),
end,
Terms),
As.
%%%===================================================================
%%% Aux functions
%%%===================================================================
@ -617,27 +696,30 @@ strings_to_binary(L) when is_list(L) ->
false ->
strings_to_binary1(L)
end;
strings_to_binary({A, B, C, D}) when
is_integer(A), is_integer(B), is_integer(C), is_integer(D) ->
strings_to_binary({A, B, C, D})
when is_integer(A), is_integer(B), is_integer(C), is_integer(D) ->
{A, B, C, D};
strings_to_binary(T) when is_tuple(T) ->
list_to_tuple(strings_to_binary1(tuple_to_list(T)));
strings_to_binary(X) ->
X.
strings_to_binary1([El|L]) ->
[strings_to_binary(El)|strings_to_binary1(L)];
strings_to_binary1([El | L]) ->
[strings_to_binary(El) | strings_to_binary1(L)];
strings_to_binary1([]) ->
[];
strings_to_binary1(T) ->
T.
is_string([C|T]) when (C >= 0) and (C =< 255) ->
is_string([C | T]) when (C >= 0) and (C =< 255) ->
is_string(T);
is_string([]) ->
true;
is_string(_) ->
false.
b(S) ->
iolist_to_binary(S).

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,7 @@
-include_lib("kernel/include/inet.hrl").
%%%===================================================================
%%% API
%%%===================================================================
@ -39,10 +40,12 @@ opt_type(acl) ->
acl:validator(acl);
opt_type(acme) ->
econf:options(
#{ca_url => econf:url(),
#{
ca_url => econf:url(),
contact => econf:list_or_single(econf:binary("^[a-zA-Z]+:[^:]+$")),
auto => econf:bool(),
cert_type => econf:enum([ec, rsa])},
cert_type => econf:enum([ec, rsa])
},
[unique, {return, map}]);
opt_type(allow_contrib_modules) ->
econf:bool();
@ -75,7 +78,8 @@ opt_type(auth_opts) ->
{basic_auth, V};
({path_prefix, V}) when is_binary(V) ->
{path_prefix, V}
end, L)
end,
L)
end;
opt_type(auth_stored_password_types) ->
econf:list(econf:enum([plain, scram_sha1, scram_sha256, scram_sha512]));
@ -150,9 +154,11 @@ opt_type(domain_balancing) ->
econf:map(
econf:domain(),
econf:options(
#{component_number => econf:int(2, 1000),
#{
component_number => econf:int(2, 1000),
type => econf:enum([random, source, destination,
bare_source, bare_destination])},
bare_source, bare_destination])
},
[{return, map}, unique]),
[{return, map}]);
opt_type(ext_api_path_oauth) ->
@ -304,7 +310,8 @@ opt_type(outgoing_s2s_families) ->
lists:map(
fun(ipv4) -> inet;
(ipv6) -> inet6
end, L)
end,
L)
end);
opt_type(outgoing_s2s_ipv4_address) ->
econf:ipv4();
@ -480,14 +487,15 @@ opt_type(jwt_key) ->
case jose_jwk:to_map(JWK) of
{_, #{<<"keys">> := [Key]}} ->
jose_jwk:from_map(Key);
{_, #{<<"keys">> := [_|_]}} ->
{_, #{<<"keys">> := [_ | _]}} ->
econf:fail({bad_jwt_key_set, Path});
{_, #{<<"keys">> := _}} ->
econf:fail({bad_jwt_key, Path});
_ ->
JWK
end
catch _:_ ->
catch
_:_ ->
econf:fail({bad_jwt_key, Path})
end;
{error, Reason} ->
@ -499,6 +507,7 @@ opt_type(jwt_jid_field) ->
opt_type(jwt_auth_only_rule) ->
econf:atom().
%% We only define the types of options that cannot be derived
%% automatically by tools/opt_type.sh script
-spec options() -> [{s2s_protocol_options, undefined | binary()} |
@ -528,7 +537,7 @@ opt_type(jwt_auth_only_rule) ->
{include_config_file, any()} |
{atom(), any()}].
options() ->
[%% Top-priority options
[ %% Top-priority options
hosts,
{loglevel, info},
{cache_life_time, timer:seconds(3600)},
@ -624,7 +633,7 @@ options() ->
{ldap_uids, [{<<"uid">>, <<"%u">>}]},
{listen, []},
{log_rotate_count, 1},
{log_rotate_size, 10*1024*1024},
{log_rotate_size, 10 * 1024 * 1024},
{log_burst_limit_window_time, timer:seconds(1)},
{log_burst_limit_count, 500},
{log_modules_fully, []},
@ -755,6 +764,7 @@ options() ->
{jwt_jid_field, <<"jid">>},
{jwt_auth_only_rule, none}].
-spec globals() -> [atom()].
globals() ->
[acme,
@ -829,9 +839,11 @@ globals() ->
websocket_ping_interval,
websocket_timeout].
doc() ->
ejabberd_options_doc:doc().
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -845,6 +857,7 @@ validator() ->
Validators,
[{disallowed, Required ++ Disallowed}, unique])).
-spec fqdn(global | binary()) -> [binary()].
fqdn(global) ->
{ok, Hostname} = inet:gethostname(),
@ -860,6 +873,7 @@ fqdn(global) ->
fqdn(_) ->
ejabberd_config:get_option(fqdn).
-spec concat_binary(char()) -> fun(([binary()]) -> binary()).
concat_binary(C) ->
fun(Opts) -> str:join(Opts, <<C>>) end.

File diff suppressed because it is too large Load diff

View file

@ -36,11 +36,14 @@
-export([import_file/1, export_server/1, export_host/2]).
-define(CHUNK_SIZE, 1024*20). %20k
-define(CHUNK_SIZE, 1024 * 20). %20k
-include_lib("xmpp/include/scram.hrl").
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_privacy.hrl").
-include("mod_roster.hrl").
@ -56,14 +59,23 @@
-define(NS_PIEFXIS, <<"http://www.xmpp.org/extensions/xep-0227.html#ns">>).
-define(NS_XI, <<"http://www.w3.org/2001/XInclude">>).
%%
%% @efmt:off
%% @indent-begin
-record(state, {xml_stream_state :: fxml_stream:xml_stream_state() | undefined,
user = <<"">> :: binary(),
server = <<"">> :: binary(),
fd = self() :: file:io_device(),
dir = <<"">> :: binary()}).
%% @indent-end
%% @efmt:on
%%
-type state() :: #state{}.
%%File could be large.. we read it in chunks
%%%===================================================================
%%% API
@ -71,15 +83,18 @@
import_file(FileName) ->
import_file(FileName, #state{}).
-spec import_file(binary(), state()) -> ok | {error, atom()}.
import_file(FileName, State) ->
case file:open(FileName, [read, binary]) of
{ok, Fd} ->
Dir = filename:dirname(FileName),
XMLStreamState = fxml_stream:new(self(), infinity),
Res = process(State#state{xml_stream_state = XMLStreamState,
Res = process(State#state{
xml_stream_state = XMLStreamState,
fd = Fd,
dir = Dir}),
dir = Dir
}),
file:close(Fd),
Res;
{error, Reason} ->
@ -88,14 +103,17 @@ import_file(FileName, State) ->
{error, Reason}
end.
-spec export_server(binary()) -> any().
export_server(Dir) ->
export_hosts(ejabberd_option:hosts(), Dir).
-spec export_host(binary(), binary()) -> any().
export_host(Dir, Host) ->
export_hosts([Host], Dir).
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -106,12 +124,13 @@ export_hosts(Hosts, Dir) ->
{ok, Fd} ->
print(Fd, make_piefxis_xml_head()),
print(Fd, make_piefxis_server_head()),
FilesAndHosts = [{make_host_filename(FnT, Host), Host}
|| Host <- Hosts],
FilesAndHosts = [ {make_host_filename(FnT, Host), Host}
|| Host <- Hosts ],
lists:foreach(
fun({FnH, _}) ->
print(Fd, make_xinclude(FnH))
end, FilesAndHosts),
end,
FilesAndHosts),
print(Fd, make_piefxis_server_tail()),
print(Fd, make_piefxis_xml_tail()),
file:close(Fd),
@ -120,13 +139,16 @@ export_hosts(Hosts, Dir) ->
export_host(Dir, FnH, Host);
(_, Err) ->
Err
end, ok, FilesAndHosts);
end,
ok,
FilesAndHosts);
{error, Reason} ->
ErrTxt = file:format_error(Reason),
?ERROR_MSG("Failed to open file '~ts': ~ts", [DFn, ErrTxt]),
{error, Reason}
end.
export_host(Dir, FnH, Host) ->
DFn = make_host_basefilename(Dir, FnH),
case file:open(DFn, [raw, write]) of
@ -151,7 +173,8 @@ export_host(Dir, FnH, Host) ->
{error, Reason}
end.
export_users([{User, _S}|Users], Server, Fd) ->
export_users([{User, _S} | Users], Server, Fd) ->
case export_user(User, Server, Fd) of
ok ->
export_users(Users, Server, Fd);
@ -161,6 +184,7 @@ export_users([{User, _S}|Users], Server, Fd) ->
export_users([], _Server, _Fd) ->
ok.
export_user(User, Server, Fd) ->
Password = ejabberd_auth:get_password_s(User, Server),
LServer = jid:nameprep(Server),
@ -176,13 +200,22 @@ export_user(User, Server, Fd) ->
get_privacy(User, Server) ++
get_roster(User, Server) ++
get_private(User, Server),
print(Fd, fxml:element_to_binary(
#xmlel{name = <<"user">>,
print(Fd,
fxml:element_to_binary(
#xmlel{
name = <<"user">>,
attrs = [{<<"name">>, User} | PassPlain],
children = Els})).
children = Els
})).
format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey,
salt = Salt, iterationcount = IterationCount}) ->
format_scram_password(#scram{
hash = Hash,
storedkey = StoredKey,
serverkey = ServerKey,
salt = Salt,
iterationcount = IterationCount
}) ->
StoredKeyB64 = base64:encode(StoredKey),
ServerKeyB64 = base64:encode(ServerKey),
SaltB64 = base64:encode(Salt),
@ -193,20 +226,29 @@ format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = Ser
sha512 -> <<"SCRAM-SHA-512">>
end,
Children =
[
#xmlel{name = <<"iter-count">>,
children = [{xmlcdata, IterationCountBin}]},
#xmlel{name = <<"salt">>,
children = [{xmlcdata, SaltB64}]},
#xmlel{name = <<"server-key">>,
children = [{xmlcdata, ServerKeyB64}]},
#xmlel{name = <<"stored-key">>,
children = [{xmlcdata, StoredKeyB64}]}
],
#xmlel{name = <<"scram-credentials">>,
[#xmlel{
name = <<"iter-count">>,
children = [{xmlcdata, IterationCountBin}]
},
#xmlel{
name = <<"salt">>,
children = [{xmlcdata, SaltB64}]
},
#xmlel{
name = <<"server-key">>,
children = [{xmlcdata, ServerKeyB64}]
},
#xmlel{
name = <<"stored-key">>,
children = [{xmlcdata, StoredKeyB64}]
}],
#xmlel{
name = <<"scram-credentials">>,
attrs = [{<<"xmlns">>, <<?NS_PIE/binary, "#scram">>},
{<<"mechanism">>, MechanismB}],
children = Children}.
children = Children
}.
parse_scram_password(#xmlel{attrs = Attrs} = El) ->
Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of
@ -242,6 +284,7 @@ parse_scram_password(PassData) ->
iterationcount = (binary_to_integer(IterationCountBin))
}.
-spec get_vcard(binary(), binary()) -> [xmlel()].
get_vcard(User, Server) ->
LUser = jid:nodeprep(User),
@ -253,6 +296,7 @@ get_vcard(User, Server) ->
error:{module_not_loaded, _, _} -> []
end.
-spec get_offline(binary(), binary()) -> [xmlel()].
get_offline(User, Server) ->
LUser = jid:nodeprep(User),
@ -267,18 +311,22 @@ get_offline(User, Server) ->
error:{module_not_loaded, _, _} -> []
end.
-spec get_privacy(binary(), binary()) -> [xmlel()].
get_privacy(User, Server) ->
try mod_privacy:get_user_lists(User, Server) of
{ok, #privacy{default = Default,
lists = [_|_] = Lists}} ->
{ok, #privacy{
default = Default,
lists = [_ | _] = Lists
}} ->
XLists = lists:map(
fun({Name, Items}) ->
XItems = lists:map(
fun mod_privacy:encode_list_item/1,
Items),
#privacy_list{name = Name, items = XItems}
end, Lists),
end,
Lists),
[xmpp:encode(#privacy_query{default = Default, lists = XLists})];
_ ->
[]
@ -286,33 +334,41 @@ get_privacy(User, Server) ->
error:{module_not_loaded, _, _} -> []
end.
-spec get_roster(binary(), binary()) -> [xmlel()].
get_roster(User, Server) ->
JID = jid:make(User, Server),
try mod_roster:get_roster(User, Server) of
[_|_] = Items ->
[_ | _] = Items ->
Subs =
lists:flatmap(
fun(#roster{ask = Ask,
askmessage = Msg} = R)
fun(#roster{
ask = Ask,
askmessage = Msg
} = R)
when Ask == in; Ask == both ->
Status = if is_binary(Msg) -> (Msg);
Status = if
is_binary(Msg) -> (Msg);
true -> <<"">>
end,
[xmpp:encode(
#presence{from = jid:make(R#roster.jid),
#presence{
from = jid:make(R#roster.jid),
to = JID,
type = subscribe,
status = xmpp:mk_text(Status)})];
status = xmpp:mk_text(Status)
})];
(_) ->
[]
end, Items),
end,
Items),
Rs = lists:flatmap(
fun(#roster{ask = in, subscription = none}) ->
[];
(R) ->
[mod_roster:encode_item(R)]
end, Items),
end,
Items),
[xmpp:encode(#roster_query{items = Rs}) | Subs];
_ ->
[]
@ -320,10 +376,11 @@ get_roster(User, Server) ->
error:{module_not_loaded, _, _} -> []
end.
-spec get_private(binary(), binary()) -> [xmlel()].
get_private(User, Server) ->
try mod_private:get_data(User, Server) of
[_|_] = Els ->
[_ | _] = Els ->
[xmpp:encode(#private{sub_els = Els})];
_ ->
[]
@ -331,12 +388,15 @@ get_private(User, Server) ->
error:{module_not_loaded, _, _} -> []
end.
process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) ->
case file:read(Fd, ?CHUNK_SIZE) of
{ok, Data} ->
NewXMLStreamState = fxml_stream:parse(XMLStreamState, Data),
case process_els(State#state{xml_stream_state =
NewXMLStreamState}) of
case process_els(State#state{
xml_stream_state =
NewXMLStreamState
}) of
{ok, NewState} ->
process(NewState);
Err ->
@ -348,17 +408,21 @@ process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) ->
ok
end.
process_els(State) ->
Els = gather_els(State, []),
process_els(State, lists:reverse(Els)).
gather_els(State, List) ->
receive
{'$gen_event', El} ->
gather_els(State, [El | List])
after 0 ->
after
0 ->
List
end.
end.
process_els(State, [El | Tail]) ->
case process_el(El, State) of
@ -370,6 +434,7 @@ process_els(State, [El | Tail]) ->
process_els(State, []) ->
{ok, State}.
process_el({xmlstreamstart, <<"server-data">>, Attrs}, State) ->
case fxml:get_attr_s(<<"xmlns">>, Attrs) of
?NS_PIEFXIS ->
@ -383,8 +448,10 @@ process_el({xmlstreamend, _}, State) ->
{ok, State};
process_el({xmlstreamcdata, _}, State) ->
{ok, State};
process_el({xmlstreamelement, #xmlel{name = <<"xi:include">>,
attrs = Attrs}},
process_el({xmlstreamelement, #xmlel{
name = <<"xi:include">>,
attrs = Attrs
}},
#state{dir = Dir, user = <<"">>} = State) ->
FileName = fxml:get_attr_s(<<"href">>, Attrs),
case import_file(filename:join([Dir, FileName]), State) of
@ -394,11 +461,17 @@ process_el({xmlstreamelement, #xmlel{name = <<"xi:include">>,
Err
end;
process_el({xmlstreamstart, <<"host">>, Attrs}, State) ->
process_el({xmlstreamelement, #xmlel{name = <<"host">>,
attrs = Attrs}}, State);
process_el({xmlstreamelement, #xmlel{name = <<"host">>,
process_el({xmlstreamelement, #xmlel{
name = <<"host">>,
attrs = Attrs
}},
State);
process_el({xmlstreamelement, #xmlel{
name = <<"host">>,
attrs = Attrs,
children = Els}}, State) ->
children = Els
}},
State) ->
JIDS = fxml:get_attr_s(<<"jid">>, Attrs),
try jid:decode(JIDS) of
#jid{lserver = S} ->
@ -408,7 +481,8 @@ process_el({xmlstreamelement, #xmlel{name = <<"host">>,
false ->
stop("Unknown host: ~ts", [S])
end
catch _:{bad_jid, _} ->
catch
_:{bad_jid, _} ->
stop("Invalid 'jid': ~ts", [JIDS])
end;
process_el({xmlstreamstart, <<"user">>, Attrs}, State = #state{server = S})
@ -428,18 +502,20 @@ process_el({xmlstreamstart, El, Attrs}, _State) ->
process_el({xmlstreamerror, Err}, _State) ->
stop("Failed to process element = ~p", [Err]).
process_users([#xmlel{} = El|Els], State) ->
process_users([#xmlel{} = El | Els], State) ->
case process_user(El, State) of
{ok, NewState} ->
process_users(Els, NewState);
Err ->
Err
end;
process_users([_|Els], State) ->
process_users([_ | Els], State) ->
process_users(Els, State);
process_users([], State) ->
{ok, State}.
process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El,
#state{server = LServer} = State) ->
Name = fxml:get_attr_s(<<"name">>, Attrs),
@ -458,6 +534,7 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El,
end
end.
process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) ->
{PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of
<<"scram:", PassData/binary>> -> {<<"">>, PassData};
@ -474,18 +551,20 @@ process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) ->
{<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram)
end.
process_user_els([#xmlel{} = El|Els], State) ->
process_user_els([#xmlel{} = El | Els], State) ->
case process_user_el(El, State) of
{ok, NewState} ->
process_user_els(Els, NewState);
Err ->
Err
end;
process_user_els([_|Els], State) ->
process_user_els([_ | Els], State) ->
process_user_els(Els, State);
process_user_els([], State) ->
{ok, State}.
process_user_el(#xmlel{name = Name, attrs = Attrs, children = Els} = El,
State) ->
try
@ -500,32 +579,35 @@ process_user_el(#xmlel{name = Name, attrs = Attrs, children = Els} = El,
{<<"vCard">>, ?NS_VCARD} ->
process_vcard(xmpp:decode(El), State);
{<<"offline-messages">>, NS} ->
Msgs = [xmpp:decode(E, NS, [ignore_els]) || E <- Els],
Msgs = [ xmpp:decode(E, NS, [ignore_els]) || E <- Els ],
process_offline_msgs(Msgs, State);
{<<"presence">>, ?NS_CLIENT} ->
process_presence(xmpp:decode(El, ?NS_CLIENT, [ignore_els]), State);
_ ->
{ok, State}
end
catch _:{xmpp_codec, Why} ->
catch
_:{xmpp_codec, Why} ->
ErrTxt = xmpp:format_error(Why),
stop("failed to decode XML '~ts': ~ts",
[fxml:element_to_binary(El), ErrTxt])
end.
-spec process_offline_msgs([stanza()], state()) -> {ok, state()} | {error, _}.
process_offline_msgs([#message{} = Msg|Msgs], State) ->
process_offline_msgs([#message{} = Msg | Msgs], State) ->
case process_offline_msg(Msg, State) of
{ok, NewState} ->
process_offline_msgs(Msgs, NewState);
Err ->
Err
end;
process_offline_msgs([_|Msgs], State) ->
process_offline_msgs([_ | Msgs], State) ->
process_offline_msgs(Msgs, State);
process_offline_msgs([], State) ->
{ok, State}.
-spec process_roster(roster_query(), state()) -> {ok, state()} | {error, _}.
process_roster(RosterQuery, State = #state{user = U, server = S}) ->
case mod_roster:set_items(U, S, RosterQuery) of
@ -535,36 +617,48 @@ process_roster(RosterQuery, State = #state{user = U, server = S}) ->
stop("Failed to write roster: ~p", [Err])
end.
-spec process_privacy(privacy_query(), state()) -> {ok, state()} | {error, _}.
process_privacy(#privacy_query{lists = Lists,
process_privacy(#privacy_query{
lists = Lists,
default = Default,
active = Active},
active = Active
},
State = #state{user = U, server = S}) ->
JID = jid:make(U, S),
if Lists /= undefined ->
if
Lists /= undefined ->
process_privacy2(JID, #privacy_query{lists = Lists});
true ->
ok
end,
if Active /= undefined ->
if
Active /= undefined ->
process_privacy2(JID, #privacy_query{active = Active});
true ->
ok
end,
if Default /= undefined ->
if
Default /= undefined ->
process_privacy2(JID, #privacy_query{default = Default});
true ->
ok
end,
{ok, State}.
process_privacy2(JID, PQ) ->
case mod_privacy:process_iq(#iq{type = set, id = p1_rand:get_string(),
from = JID, to = JID,
sub_els = [PQ]}) of
case mod_privacy:process_iq(#iq{
type = set,
id = p1_rand:get_string(),
from = JID,
to = JID,
sub_els = [PQ]
}) of
#iq{type = error} = ResIQ ->
#stanza_error{reason = Reason} = xmpp:get_error(ResIQ),
if Reason /= 'item-not-found' ->
if
Reason /= 'item-not-found' ->
%% Failed to set default list because there is no
%% list with such name. We shouldn't stop here.
stop("Failed to write default privacy: ~p", [Reason]);
@ -575,11 +669,17 @@ process_privacy2(JID, PQ) ->
ok
end.
-spec process_private(private(), state()) -> {ok, state()} | {error, _}.
process_private(Private, State = #state{user = U, server = S}) ->
JID = jid:make(U, S),
IQ = #iq{type = set, id = p1_rand:get_string(),
from = JID, to = JID, sub_els = [Private]},
IQ = #iq{
type = set,
id = p1_rand:get_string(),
from = JID,
to = JID,
sub_els = [Private]
},
case mod_private:process_sm_iq(IQ) of
#iq{type = result} ->
{ok, State};
@ -587,11 +687,17 @@ process_private(Private, State = #state{user = U, server = S}) ->
stop("Failed to write private: ~p", [Err])
end.
-spec process_vcard(xmpp_element(), state()) -> {ok, state()} | {error, _}.
process_vcard(El, State = #state{user = U, server = S}) ->
JID = jid:make(U, S),
IQ = #iq{type = set, id = p1_rand:get_string(),
from = JID, to = JID, sub_els = [El]},
IQ = #iq{
type = set,
id = p1_rand:get_string(),
from = JID,
to = JID,
sub_els = [El]
},
case mod_vcard:process_sm_iq(IQ) of
#iq{type = result} ->
{ok, State};
@ -599,6 +705,7 @@ process_vcard(El, State = #state{user = U, server = S}) ->
stop("Failed to write vcard: ~p", [Err])
end.
-spec process_offline_msg(message(), state()) -> {ok, state()} | {error, _}.
process_offline_msg(#message{from = undefined}, _State) ->
stop("No 'from' attribute found", []);
@ -608,6 +715,7 @@ process_offline_msg(Msg, State = #state{user = U, server = S}) ->
offline_message_hook, To#jid.lserver, {pass, xmpp:set_to(Msg, To)}, []),
{ok, State}.
-spec process_presence(presence(), state()) -> {ok, state()} | {error, _}.
process_presence(#presence{from = undefined}, _State) ->
stop("No 'from' attribute found", []);
@ -617,19 +725,23 @@ process_presence(Pres, #state{user = U, server = S} = State) ->
ejabberd_router:route(NewPres),
{ok, State}.
stop(Fmt, Args) ->
?ERROR_MSG(Fmt, Args),
{error, import_failed}.
make_filename_template() ->
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
str:format("~4..0w~2..0w~2..0w-~2..0w~2..0w~2..0w",
[Year, Month, Day, Hour, Minute, Second]).
make_main_basefilename(Dir, FnT) ->
Filename2 = <<FnT/binary, ".xml">>,
filename:join([Dir, Filename2]).
%% @doc Make the filename for the host.
%% Example: ``(<<"20080804-231550">>, <<"xmpp.domain.tld">>) ->
%% <<"20080804-231550_xmpp_domain_tld.xml">>''
@ -637,34 +749,43 @@ make_host_filename(FnT, Host) ->
Host2 = str:join(str:tokens(Host, <<".">>), <<"_">>),
<<FnT/binary, "_", Host2/binary, ".xml">>.
%%%==================================
%%%% PIEFXIS formatting
make_host_basefilename(Dir, FnT) ->
filename:join([Dir, FnT]).
make_piefxis_xml_head() ->
"<?xml version='1.0' encoding='UTF-8'?>".
make_piefxis_xml_tail() ->
"".
make_piefxis_server_head() ->
io_lib:format("<server-data xmlns='~ts' xmlns:xi='~ts'>",
[?NS_PIE, ?NS_XI]).
make_piefxis_server_tail() ->
"</server-data>".
make_piefxis_host_head(Host) ->
io_lib:format("<host xmlns='~ts' xmlns:xi='~ts' jid='~ts'>",
[?NS_PIE, ?NS_XI, Host]).
make_piefxis_host_tail() ->
"</host>".
make_xinclude(Fn) ->
Base = filename:basename(Fn),
io_lib:format("<xi:include href='~ts'/>", [Base]).
print(Fd, String) ->
file:write(Fd, String).

View file

@ -33,8 +33,12 @@
%% Hooks
-export([ejabberd_started/0, config_reloaded/0, cert_expired/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include("logger.hrl").
-define(CALL_TIMEOUT, timer:minutes(1)).
@ -44,6 +48,7 @@
-type state() :: #state{}.
-type filename() :: binary().
%%%===================================================================
%%% API
%%%===================================================================
@ -51,25 +56,32 @@
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec add_certfile(file:filename_all()) -> {ok, filename()} | {error, pkix:error_reason()}.
add_certfile(Path0) ->
Path = prep_path(Path0),
try gen_server:call(?MODULE, {add_certfile, Path}, ?CALL_TIMEOUT)
catch exit:{noproc, _} ->
try
gen_server:call(?MODULE, {add_certfile, Path}, ?CALL_TIMEOUT)
catch
exit:{noproc, _} ->
case add_file(Path) of
ok -> {ok, Path};
Err -> Err
end
end.
-spec del_certfile(file:filename_all()) -> ok.
del_certfile(Path0) ->
Path = prep_path(Path0),
try gen_server:call(?MODULE, {del_certfile, Path}, ?CALL_TIMEOUT)
catch exit:{noproc, _} ->
try
gen_server:call(?MODULE, {del_certfile, Path}, ?CALL_TIMEOUT)
catch
exit:{noproc, _} ->
pkix:del_file(Path)
end.
-spec try_certfile(file:filename_all()) -> filename().
try_certfile(Path0) ->
Path = prep_path(Path0),
@ -81,6 +93,7 @@ try_certfile(Path0) ->
erlang:error(badarg)
end.
-spec get_certfile(binary()) -> {ok, filename()} | error.
get_certfile(Domain) ->
case get_certfile_no_default(Domain) of
@ -90,6 +103,7 @@ get_certfile(Domain) ->
get_certfile()
end.
-spec get_certfile_no_default(binary()) -> {ok, filename()} | error.
get_certfile_no_default(Domain) ->
try list_to_binary(idna:utf8_to_ascii(Domain)) of
@ -98,10 +112,12 @@ get_certfile_no_default(Domain) ->
error -> error;
Ret -> {ok, select_certfile(Ret)}
end
catch _:_ ->
catch
_:_ ->
error
end.
-spec get_certfile() -> {ok, filename()} | error.
get_certfile() ->
case pkix:get_certfile() of
@ -109,39 +125,50 @@ get_certfile() ->
Ret -> {ok, select_certfile(Ret)}
end.
-spec certs_dir() -> file:filename_all().
certs_dir() ->
MnesiaDir = mnesia:system_info(directory),
filename:join(MnesiaDir, "certs").
-spec commit() -> ok.
commit() ->
gen_server:call(?MODULE, commit, ?CALL_TIMEOUT).
-spec ejabberd_started() -> ok.
ejabberd_started() ->
gen_server:call(?MODULE, ejabberd_started, ?CALL_TIMEOUT).
-spec config_reloaded() -> ok.
config_reloaded() ->
gen_server:call(?MODULE, config_reloaded, ?CALL_TIMEOUT).
-spec notify_expired(pkix:notify_event()) -> ok.
notify_expired(Event) ->
gen_server:cast(?MODULE, Event).
-spec cert_expired(_, pkix:cert_info()) -> ok.
cert_expired(_Cert, #{domains := Domains,
cert_expired(_Cert,
#{
domains := Domains,
expiry := Expiry,
files := [{Path, Line}|_]}) ->
files := [{Path, Line} | _]
}) ->
?WARNING_MSG("Certificate in ~ts (at line: ~B)~ts ~ts",
[Path, Line,
[Path,
Line,
case Domains of
[] -> "";
_ -> " for " ++ misc:format_hosts_list(Domains)
end,
format_expiration_date(Expiry)]).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -154,7 +181,7 @@ init([]) ->
case add_files() of
{_Files, []} ->
{ok, #state{}};
{Files, [_|_]} ->
{Files, [_ | _]} ->
case ejabberd:is_loaded() of
true ->
{ok, #state{}};
@ -164,6 +191,7 @@ init([]) ->
end
end.
-spec handle_call(term(), {pid(), term()}, state()) ->
{reply, ok, state()} | {noreply, state()}.
handle_call({add_certfile, Path}, _From, State) ->
@ -200,6 +228,7 @@ handle_call(Request, _From, State) ->
?WARNING_MSG("Unexpected call: ~p", [Request]),
{noreply, State}.
-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast({cert_expired, Cert, CertInfo}, State) ->
ejabberd_hooks:run(cert_expired, [Cert, CertInfo]),
@ -208,11 +237,13 @@ handle_cast(Request, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Request]),
{noreply, State}.
-spec handle_info(term(), state()) -> {noreply, state()}.
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(normal | shutdown | {shutdown, term()} | term(),
state()) -> any().
terminate(_Reason, State) ->
@ -221,10 +252,12 @@ terminate(_Reason, State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 100),
del_files(State#state.files).
-spec code_change(term() | {down, term()}, state(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -233,26 +266,30 @@ add_files() ->
Files = get_certfiles_from_config_options(),
add_files(sets:to_list(Files), sets:new(), []).
-spec add_files(sets:set(filename())) ->
{sets:set(filename()), [{filename(), pkix:error_reason()}]}.
add_files(Files) ->
add_files(sets:to_list(Files), sets:new(), []).
-spec add_files([filename()], sets:set(filename()),
-spec add_files([filename()],
sets:set(filename()),
[{filename(), pkix:error_reason()}]) ->
{sets:set(filename()), [{filename(), pkix:error_reason()}]}.
add_files([File|Files], Set, Errs) ->
add_files([File | Files], Set, Errs) ->
case add_file(File) of
ok ->
Set1 = sets:add_element(File, Set),
add_files(Files, Set1, Errs);
{error, Reason} ->
Errs1 = [{File, Reason}|Errs],
Errs1 = [{File, Reason} | Errs],
add_files(Files, Set, Errs1)
end;
add_files([], Set, Errs) ->
{Set, Errs}.
-spec add_file(filename()) -> ok | {error, pkix:error_reason()}.
add_file(File) ->
case pkix:add_file(File) of
@ -263,18 +300,20 @@ add_file(File) ->
Err
end.
-spec del_files(sets:set(filename())) -> ok.
del_files(Files) ->
lists:foreach(fun pkix:del_file/1, sets:to_list(Files)).
-spec do_commit() -> {ok, [{filename(), pkix:error_reason()}]} | error.
do_commit() ->
CAFile = ejabberd_option:ca_file(),
?DEBUG("Using CA root certificates from: ~ts", [CAFile]),
Opts = [{cafile, CAFile},
{notify_before, [7*24*60*60, % 1 week
24*60*60, % 1 day
60*60, % 1 hour
{notify_before, [7 * 24 * 60 * 60, % 1 week
24 * 60 * 60, % 1 day
60 * 60, % 1 hour
0]},
{notify_fun, fun ?MODULE:notify_expired/1}],
case pkix:commit(certs_dir(), Opts) of
@ -290,12 +329,14 @@ do_commit() ->
error
end.
-spec check_domain_certfiles() -> ok.
check_domain_certfiles() ->
Hosts = ejabberd_option:hosts(),
Routes = ejabberd_router:get_all_routes(),
check_domain_certfiles(Hosts ++ Routes).
-spec check_domain_certfiles([binary()]) -> ok.
check_domain_certfiles(Hosts) ->
case ejabberd_listener:tls_listeners() of
@ -311,9 +352,11 @@ check_domain_certfiles(Hosts) ->
_ ->
ok
end
end, Hosts)
end,
Hosts)
end.
-spec get_certfiles_from_config_options() -> sets:set(filename()).
get_certfiles_from_config_options() ->
case ejabberd_option:certfiles() of
@ -324,9 +367,12 @@ get_certfiles_from_config_options() ->
fun(Path, Acc) ->
Files = wildcard(Path),
lists:foldl(fun sets:add_element/2, Acc, Files)
end, sets:new(), Paths)
end,
sets:new(),
Paths)
end.
-spec prep_path(file:filename_all()) -> filename().
prep_path(Path0) ->
case filename:pathtype(Path0) of
@ -343,12 +389,15 @@ prep_path(Path0) ->
unicode:characters_to_binary(Path0)
end.
-spec stop_ejabberd() -> no_return().
stop_ejabberd() ->
?CRITICAL_MSG("ejabberd initialization was aborted due to "
"invalid certificates configuration", []),
"invalid certificates configuration",
[]),
ejabberd:halt().
-spec wildcard(file:filename_all()) -> [filename()].
wildcard(Path) when is_binary(Path) ->
wildcard(binary_to_list(Path));
@ -356,12 +405,14 @@ wildcard(Path) ->
case filelib:wildcard(Path) of
[] ->
?WARNING_MSG("Path ~ts is empty, please make sure ejabberd has "
"sufficient rights to read it", [Path]),
"sufficient rights to read it",
[Path]),
[];
Files ->
[prep_path(File) || File <- Files]
[ prep_path(File) || File <- Files ]
end.
-spec select_certfile({filename() | undefined,
filename() | undefined,
filename() | undefined}) -> filename().
@ -369,26 +420,32 @@ select_certfile({EC, _, _}) when EC /= undefined -> EC;
select_certfile({_, RSA, _}) when RSA /= undefined -> RSA;
select_certfile({_, _, DSA}) when DSA /= undefined -> DSA.
-spec fast_tls_add_certfiles() -> ok.
fast_tls_add_certfiles() ->
lists:foreach(
fun({Domain, Files}) ->
fast_tls:add_certfile(Domain, select_certfile(Files))
end, pkix:get_certfiles()),
end,
pkix:get_certfiles()),
fast_tls:clear_cache().
reason_to_fmt({invalid_cert, _, _}) ->
"Invalid certificate in ~ts: ~ts";
reason_to_fmt(_) ->
"Failed to read PEM file ~ts: ~ts".
-spec log_warnings([{filename(), pkix:error_reason()}]) -> ok.
log_warnings(Warnings) ->
lists:foreach(
fun({File, Reason}) ->
?WARNING_MSG(reason_to_fmt(Reason),
[File, pkix:format_error(Reason)])
end, Warnings).
end,
Warnings).
-spec log_errors([{filename(), pkix:error_reason()}]) -> ok.
log_errors(Errors) ->
@ -396,7 +453,9 @@ log_errors(Errors) ->
fun({File, Reason}) ->
?ERROR_MSG(reason_to_fmt(Reason),
[File, pkix:format_error(Reason)])
end, Errors).
end,
Errors).
-spec log_cafile_error({filename(), pkix:error_reason()} | undefined) -> ok.
log_cafile_error({File, Reason}) ->
@ -406,28 +465,31 @@ log_cafile_error({File, Reason}) ->
log_cafile_error(_) ->
ok.
-spec time_before_expiration(calendar:datetime()) -> {non_neg_integer(), string()}.
time_before_expiration(Expiry) ->
T1 = calendar:datetime_to_gregorian_seconds(Expiry),
T2 = calendar:datetime_to_gregorian_seconds(
calendar:now_to_datetime(erlang:timestamp())),
Secs = max(0, T1 - T2),
if Secs == {0, ""};
Secs >= 220752000 -> {round(Secs/220752000), "year"};
Secs >= 2592000 -> {round(Secs/2592000), "month"};
Secs >= 604800 -> {round(Secs/604800), "week"};
Secs >= 86400 -> {round(Secs/86400), "day"};
Secs >= 3600 -> {round(Secs/3600), "hour"};
Secs >= 60 -> {round(Secs/60), "minute"};
if
Secs == {0, ""};
Secs >= 220752000 -> {round(Secs / 220752000), "year"};
Secs >= 2592000 -> {round(Secs / 2592000), "month"};
Secs >= 604800 -> {round(Secs / 604800), "week"};
Secs >= 86400 -> {round(Secs / 86400), "day"};
Secs >= 3600 -> {round(Secs / 3600), "hour"};
Secs >= 60 -> {round(Secs / 60), "minute"};
true -> {Secs, "second"}
end.
-spec format_expiration_date(calendar:datetime()) -> string().
format_expiration_date(DateTime) ->
case time_before_expiration(DateTime) of
{0, _} -> "is expired";
{1, Unit} -> "will expire in a " ++ Unit;
{Int, Unit} ->
"will expire in " ++ integer_to_list(Int)
++ " " ++ Unit ++ "s"
"will expire in " ++ integer_to_list(Int) ++
" " ++ Unit ++ "s"
end.

View file

@ -33,27 +33,49 @@
%% API
-export([start_link/1, get_proc/1, get_connection/1, q/1, qp/1, format_error/1]).
%% Commands
-export([multi/1, get/1, set/2, del/1, info/1,
sadd/2, srem/2, smembers/1, sismember/2, scard/1,
hget/2, hset/3, hdel/2, hlen/1, hgetall/1, hkeys/1,
subscribe/1, publish/2, script_load/1, evalsha/3]).
-export([multi/1,
get/1,
set/2,
del/1,
info/1,
sadd/2,
srem/2,
smembers/1,
sismember/2,
scard/1,
hget/2,
hset/3,
hdel/2,
hlen/1,
hgetall/1,
hkeys/1,
subscribe/1,
publish/2,
script_load/1,
evalsha/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-define(TR_STACK, redis_transaction_stack).
-define(DEFAULT_MAX_QUEUE, 10000).
-define(MAX_RETRIES, 1).
-define(CALL_TIMEOUT, 60*1000). %% 60 seconds
-define(CALL_TIMEOUT, 60 * 1000). %% 60 seconds
-include("logger.hrl").
-include("ejabberd_stacktrace.hrl").
-record(state, {connection :: pid() | undefined,
-record(state, {
connection :: pid() | undefined,
num :: pos_integer(),
subscriptions = #{} :: subscriptions(),
pending_q :: queue()}).
pending_q :: queue()
}).
-type queue() :: p1_queue:queue({{pid(), term()}, integer()}).
-type subscriptions() :: #{binary() => [pid()]}.
@ -62,9 +84,18 @@
-type redis_reply() :: undefined | binary() | [binary()].
-type redis_command() :: [iodata() | integer()].
-type redis_pipeline() :: [redis_command()].
-type redis_info() :: server | clients | memory | persistence |
stats | replication | cpu | commandstats |
cluster | keyspace | default | all.
-type redis_info() :: server |
clients |
memory |
persistence |
stats |
replication |
cpu |
commandstats |
cluster |
keyspace |
default |
all.
-type state() :: #state{}.
-export_type([error_reason/0]).
@ -77,30 +108,36 @@
{no_match, handle_info/2}]).
-endif.
%%%===================================================================
%%% API
%%%===================================================================
start_link(I) ->
?GEN_SERVER:start_link({local, get_proc(I)}, ?MODULE, [I], []).
get_proc(I) ->
misc:binary_to_atom(
iolist_to_binary(
[atom_to_list(?MODULE), $_, integer_to_list(I)])).
get_connection(I) ->
misc:binary_to_atom(
iolist_to_binary(
[atom_to_list(?MODULE), "_connection_", integer_to_list(I)])).
-spec q(redis_command()) -> {ok, redis_reply()} | redis_error().
q(Command) ->
call(get_rnd_id(), {q, Command}, ?MAX_RETRIES).
-spec qp(redis_pipeline()) -> [{ok, redis_reply()} | redis_error()] | redis_error().
qp(Pipeline) ->
call(get_rnd_id(), {qp, Pipeline}, ?MAX_RETRIES).
-spec multi(fun(() -> any())) -> {ok, redis_reply()} | redis_error().
multi(F) ->
case erlang:get(?TR_STACK) of
@ -109,12 +146,13 @@ multi(F) ->
try F() of
_ ->
Stack = erlang:erase(?TR_STACK),
Command = [["MULTI"]|lists:reverse([["EXEC"]|Stack])],
Command = [["MULTI"] | lists:reverse([["EXEC"] | Stack])],
case qp(Command) of
{error, _} = Err -> Err;
Result -> get_result(Result)
end
catch ?EX_RULE(E, R, St) ->
catch
?EX_RULE(E, R, St) ->
erlang:erase(?TR_STACK),
erlang:raise(E, R, ?EX_STACK(St))
end;
@ -122,12 +160,14 @@ multi(F) ->
erlang:error(nested_transaction)
end.
-spec format_error(atom() | binary()) -> binary().
format_error(Reason) when is_atom(Reason) ->
format_error(misc:atom_to_binary(Reason));
format_error(Reason) ->
Reason.
%%%===================================================================
%%% Redis commands API
%%%===================================================================
@ -140,6 +180,7 @@ get(Key) ->
erlang:error(transaction_unsupported)
end.
-spec set(iodata(), iodata()) -> ok | redis_error() | queued.
set(Key, Val) ->
Cmd = [<<"SET">>, Key, Val],
@ -153,11 +194,12 @@ set(Key, Val) ->
tr_enq(Cmd, Stack)
end.
-spec del(list()) -> {ok, non_neg_integer()} | redis_error() | queued.
del([]) ->
reply(0);
del(Keys) ->
Cmd = [<<"DEL">>|Keys],
Cmd = [<<"DEL">> | Keys],
case erlang:get(?TR_STACK) of
undefined ->
case q(Cmd) of
@ -168,11 +210,12 @@ del(Keys) ->
tr_enq(Cmd, Stack)
end.
-spec sadd(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued.
sadd(_Set, []) ->
reply(0);
sadd(Set, Members) ->
Cmd = [<<"SADD">>, Set|Members],
Cmd = [<<"SADD">>, Set | Members],
case erlang:get(?TR_STACK) of
undefined ->
case q(Cmd) of
@ -183,11 +226,12 @@ sadd(Set, Members) ->
tr_enq(Cmd, Stack)
end.
-spec srem(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued.
srem(_Set, []) ->
reply(0);
srem(Set, Members) ->
Cmd = [<<"SREM">>, Set|Members],
Cmd = [<<"SREM">>, Set | Members],
case erlang:get(?TR_STACK) of
undefined ->
case q(Cmd) of
@ -198,6 +242,7 @@ srem(Set, Members) ->
tr_enq(Cmd, Stack)
end.
-spec smembers(iodata()) -> {ok, [binary()]} | redis_error().
smembers(Set) ->
case erlang:get(?TR_STACK) of
@ -207,6 +252,7 @@ smembers(Set) ->
erlang:error(transaction_unsupported)
end.
-spec sismember(iodata(), iodata()) -> boolean() | redis_error().
sismember(Set, Member) ->
case erlang:get(?TR_STACK) of
@ -219,6 +265,7 @@ sismember(Set, Member) ->
erlang:error(transaction_unsupported)
end.
-spec scard(iodata()) -> {ok, non_neg_integer()} | redis_error().
scard(Set) ->
case erlang:get(?TR_STACK) of
@ -233,6 +280,7 @@ scard(Set) ->
erlang:error(transaction_unsupported)
end.
-spec hget(iodata(), iodata()) -> {ok, undefined | binary()} | redis_error().
hget(Key, Field) ->
case erlang:get(?TR_STACK) of
@ -242,6 +290,7 @@ hget(Key, Field) ->
erlang:error(transaction_unsupported)
end.
-spec hset(iodata(), iodata(), iodata()) -> {ok, boolean()} | redis_error() | queued.
hset(Key, Field, Val) ->
Cmd = [<<"HSET">>, Key, Field, Val],
@ -255,11 +304,12 @@ hset(Key, Field, Val) ->
tr_enq(Cmd, Stack)
end.
-spec hdel(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued.
hdel(_Key, []) ->
reply(0);
hdel(Key, Fields) ->
Cmd = [<<"HDEL">>, Key|Fields],
Cmd = [<<"HDEL">>, Key | Fields],
case erlang:get(?TR_STACK) of
undefined ->
case q(Cmd) of
@ -270,6 +320,7 @@ hdel(Key, Fields) ->
tr_enq(Cmd, Stack)
end.
-spec hgetall(iodata()) -> {ok, [{binary(), binary()}]} | redis_error().
hgetall(Key) ->
case erlang:get(?TR_STACK) of
@ -282,6 +333,7 @@ hgetall(Key) ->
erlang:error(transaction_unsupported)
end.
-spec hlen(iodata()) -> {ok, non_neg_integer()} | redis_error().
hlen(Key) ->
case erlang:get(?TR_STACK) of
@ -294,6 +346,7 @@ hlen(Key) ->
erlang:error(transaction_unsupported)
end.
-spec hkeys(iodata()) -> {ok, [binary()]} | redis_error().
hkeys(Key) ->
case erlang:get(?TR_STACK) of
@ -303,10 +356,13 @@ hkeys(Key) ->
erlang:error(transaction_unsupported)
end.
-spec subscribe([binary()]) -> ok | redis_error().
subscribe(Channels) ->
try gen_server_call(get_proc(1), {subscribe, self(), Channels})
catch exit:{Why, {?GEN_SERVER, call, _}} ->
try
gen_server_call(get_proc(1), {subscribe, self(), Channels})
catch
exit:{Why, {?GEN_SERVER, call, _}} ->
Reason = case Why of
timeout -> timeout;
_ -> disconnected
@ -314,6 +370,7 @@ subscribe(Channels) ->
{error, Reason}
end.
-spec publish(iodata(), iodata()) -> {ok, non_neg_integer()} | redis_error() | queued.
publish(Channel, Data) ->
Cmd = [<<"PUBLISH">>, Channel, Data],
@ -327,6 +384,7 @@ publish(Channel, Data) ->
tr_enq(Cmd, Stack)
end.
-spec script_load(iodata()) -> {ok, binary()} | redis_error().
script_load(Data) ->
case erlang:get(?TR_STACK) of
@ -336,15 +394,17 @@ script_load(Data) ->
erlang:error(transaction_unsupported)
end.
-spec evalsha(binary(), [iodata()], [iodata() | integer()]) -> {ok, binary()} | redis_error().
evalsha(SHA, Keys, Args) ->
case erlang:get(?TR_STACK) of
undefined ->
q([<<"EVALSHA">>, SHA, length(Keys)|Keys ++ Args]);
q([<<"EVALSHA">>, SHA, length(Keys) | Keys ++ Args]);
_ ->
erlang:error(transaction_unsupported)
end.
-spec info(redis_info()) -> {ok, [{atom(), binary()}]} | redis_error().
info(Type) ->
case erlang:get(?TR_STACK) of
@ -352,8 +412,8 @@ info(Type) ->
case q([<<"INFO">>, misc:atom_to_binary(Type)]) of
{ok, Info} ->
Lines = binary:split(Info, <<"\r\n">>, [global]),
KVs = [binary:split(Line, <<":">>) || Line <- Lines],
{ok, [{misc:binary_to_atom(K), V} || [K, V] <- KVs]};
KVs = [ binary:split(Line, <<":">>) || Line <- Lines ],
{ok, [ {misc:binary_to_atom(K), V} || [K, V] <- KVs ]};
{error, _} = Err ->
Err
end;
@ -361,6 +421,7 @@ info(Type) ->
erlang:error(transaction_unsupported)
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -371,11 +432,18 @@ init([I]) ->
self() ! connect,
{ok, #state{num = I, pending_q = p1_queue:new(QueueType, Limit)}}.
handle_call(connect, From, #state{connection = undefined,
pending_q = Q} = State) ->
handle_call(connect,
From,
#state{
connection = undefined,
pending_q = Q
} = State) ->
CurrTime = erlang:monotonic_time(millisecond),
Q2 = try p1_queue:in({From, CurrTime}, Q)
catch error:full ->
Q2 = try
p1_queue:in({From, CurrTime}, Q)
catch
error:full ->
Q1 = clean_queue(Q, CurrTime),
p1_queue:in({From, CurrTime}, Q1)
end,
@ -388,22 +456,27 @@ handle_call(connect, From, #state{connection = Pid} = State) ->
self() ! connect,
handle_call(connect, From, State#state{connection = undefined})
end;
handle_call({subscribe, Caller, Channels}, _From,
handle_call({subscribe, Caller, Channels},
_From,
#state{connection = Pid, subscriptions = Subs} = State) ->
Subs1 = lists:foldl(
fun(Channel, Acc) ->
Callers = maps:get(Channel, Acc, []) -- [Caller],
maps:put(Channel, [Caller|Callers], Acc)
end, Subs, Channels),
maps:put(Channel, [Caller | Callers], Acc)
end,
Subs,
Channels),
eredis_subscribe(Pid, Channels),
{reply, ok, State#state{subscriptions = Subs1}};
handle_call(Request, _From, State) ->
?WARNING_MSG("Unexpected call: ~p", [Request]),
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(connect, #state{connection = undefined} = State) ->
NewState = case connect(State) of
{ok, Connection} ->
@ -444,7 +517,8 @@ handle_info({message, Channel, Data, Pid}, State) ->
lists:foreach(
fun(Subscriber) ->
erlang:send(Subscriber, {redis_message, Channel, Data})
end, maps:get(Channel, State#state.subscriptions, [])),
end,
maps:get(Channel, State#state.subscriptions, [])),
eredis_sub:ack_message(Pid);
_ ->
ok
@ -454,12 +528,15 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info = ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -471,7 +548,8 @@ connect(#state{num = Num}) ->
Pass = ejabberd_option:redis_password(),
ConnTimeout = ejabberd_option:redis_connect_timeout(),
Server = parse_server(Server1),
try case do_connect(Num, Server, Port, Pass, DB, ConnTimeout) of
try
case do_connect(Num, Server, Port, Pass, DB, ConnTimeout) of
{ok, Client} ->
?DEBUG("Connection #~p established to Redis at ~ts:~p",
[Num, Server, Port]),
@ -480,7 +558,8 @@ connect(#state{num = Num}) ->
{error, Why} ->
erlang:error(Why)
end
catch _:Reason ->
catch
_:Reason ->
Timeout = p1_rand:uniform(
min(10, ejabberd_redis_sup:get_pool_size())),
?ERROR_MSG("Redis connection #~p at ~ts:~p has failed: ~p; "
@ -490,11 +569,13 @@ connect(#state{num = Num}) ->
{error, Reason}
end.
parse_server([$u,$n,$i,$x,$: | Path]) ->
parse_server([$u, $n, $i, $x, $: | Path]) ->
{local, Path};
parse_server(Server) ->
Server.
do_connect(1, Server, Port, Pass, _DB, _ConnTimeout) ->
%% First connection in the pool is always a subscriber
Options = [{host, Server},
@ -518,6 +599,7 @@ do_connect(_, Server, Port, Pass, DB, ConnTimeout) ->
{connect_timeout, ConnTimeout}],
eredis:start_link(Options).
-spec call(pos_integer(), {q, redis_command()}, integer()) ->
{ok, redis_reply()} | redis_error();
(pos_integer(), {qp, redis_pipeline()}, integer()) ->
@ -531,15 +613,17 @@ call(I, {F, Cmd}, Retries) ->
{error, disconnected};
Other ->
Other
catch exit:{timeout, _} -> {error, timeout};
catch
exit:{timeout, _} -> {error, timeout};
exit:{_, {gen_server, call, _}} -> {error, disconnected}
end,
case Res of
{error, disconnected} when Retries > 0 ->
try gen_server_call(get_proc(I), connect) of
ok -> call(I, {F, Cmd}, Retries-1);
ok -> call(I, {F, Cmd}, Retries - 1);
{error, _} = Err -> Err
catch exit:{Why, {?GEN_SERVER, call, _}} ->
catch
exit:{Why, {?GEN_SERVER, call, _}} ->
Reason1 = case Why of
timeout -> timeout;
_ -> disconnected
@ -554,6 +638,7 @@ call(I, {F, Cmd}, Retries) ->
Res
end.
gen_server_call(Proc, Msg) ->
case ejabberd_redis_sup:start() of
ok ->
@ -562,6 +647,7 @@ gen_server_call(Proc, Msg) ->
{error, disconnected}
end.
-spec log_error(redis_command() | redis_pipeline(), atom() | binary()) -> ok.
log_error(Cmd, Reason) ->
?ERROR_MSG("Redis request has failed:~n"
@ -569,37 +655,44 @@ log_error(Cmd, Reason) ->
"** response = ~ts",
[Cmd, format_error(Reason)]).
-spec get_rnd_id() -> pos_integer().
get_rnd_id() ->
p1_rand:round_robin(ejabberd_redis_sup:get_pool_size() - 1) + 2.
-spec get_result([{ok, redis_reply()} | redis_error()]) ->
{ok, redis_reply()} | redis_error().
get_result([{error, _} = Err|_]) ->
get_result([{error, _} = Err | _]) ->
Err;
get_result([{ok, _} = OK]) ->
OK;
get_result([_|T]) ->
get_result([_ | T]) ->
get_result(T).
-spec tr_enq([iodata()], list()) -> queued.
tr_enq(Cmd, Stack) ->
erlang:put(?TR_STACK, [Cmd|Stack]),
erlang:put(?TR_STACK, [Cmd | Stack]),
queued.
-spec decode_pairs([binary()]) -> [{binary(), binary()}].
decode_pairs(Pairs) ->
decode_pairs(Pairs, []).
-spec decode_pairs([binary()], [{binary(), binary()}]) -> [{binary(), binary()}].
decode_pairs([Field, Val|Pairs], Acc) ->
decode_pairs(Pairs, [{Field, Val}|Acc]);
decode_pairs([Field, Val | Pairs], Acc) ->
decode_pairs(Pairs, [{Field, Val} | Acc]);
decode_pairs([], Acc) ->
lists:reverse(Acc).
dec_bool(<<$1>>) -> true;
dec_bool(<<$0>>) -> false.
-spec reply(T) -> {ok, T} | queued.
reply(Val) ->
case erlang:get(?TR_STACK) of
@ -607,54 +700,66 @@ reply(Val) ->
_ -> queued
end.
-spec max_fsm_queue() -> pos_integer().
max_fsm_queue() ->
proplists:get_value(max_queue, fsm_limit_opts(), ?DEFAULT_MAX_QUEUE).
fsm_limit_opts() ->
ejabberd_config:fsm_limit_opts([]).
get_queue_type() ->
ejabberd_option:redis_queue_type().
-spec flush_queue(queue()) -> queue().
flush_queue(Q) ->
CurrTime = erlang:monotonic_time(millisecond),
p1_queue:dropwhile(
fun({From, Time}) ->
if (CurrTime - Time) >= ?CALL_TIMEOUT ->
if
(CurrTime - Time) >= ?CALL_TIMEOUT ->
ok;
true ->
?GEN_SERVER:reply(From, ok)
end,
true
end, Q).
end,
Q).
-spec clean_queue(queue(), integer()) -> queue().
clean_queue(Q, CurrTime) ->
Q1 = p1_queue:dropwhile(
fun({_From, Time}) ->
(CurrTime - Time) >= ?CALL_TIMEOUT
end, Q),
end,
Q),
Len = p1_queue:len(Q1),
Limit = p1_queue:get_limit(Q1),
if Len >= Limit ->
if
Len >= Limit ->
?ERROR_MSG("Redis request queue is overloaded", []),
p1_queue:dropwhile(
fun({From, _Time}) ->
?GEN_SERVER:reply(From, {error, overloaded}),
true
end, Q1);
end,
Q1);
true ->
Q1
end.
re_subscribe(Pid, Subs) ->
case maps:keys(Subs) of
[] -> ok;
Channels -> eredis_subscribe(Pid, Channels)
end.
eredis_subscribe(Pid, Channels) ->
?DEBUG("Redis query: ~p", [[<<"SUBSCRIBE">>|Channels]]),
?DEBUG("Redis query: ~p", [[<<"SUBSCRIBE">> | Channels]]),
eredis_sub:subscribe(Pid, Channels).

View file

@ -33,6 +33,7 @@
-include("logger.hrl").
%%%===================================================================
%%% API functions
%%%===================================================================
@ -42,7 +43,10 @@ start() ->
false ->
ejabberd:start_app(eredis),
Spec = {?MODULE, {?MODULE, start_link, []},
permanent, infinity, supervisor, [?MODULE]},
permanent,
infinity,
supervisor,
[?MODULE]},
case supervisor:start_child(ejabberd_db_sup, Spec) of
{ok, _} -> ok;
{error, {already_started, Pid}} ->
@ -55,22 +59,26 @@ start() ->
end
end.
stop() ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 20),
_ = supervisor:terminate_child(ejabberd_db_sup, ?MODULE),
_ = supervisor:delete_child(ejabberd_db_sup, ?MODULE),
ok.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
config_reloaded() ->
case is_started() of
true ->
lists:foreach(
fun(Spec) ->
supervisor:start_child(?MODULE, Spec)
end, get_specs()),
end,
get_specs()),
PoolSize = get_pool_size(),
lists:foreach(
fun({Id, _, _, _}) when Id > PoolSize ->
@ -80,11 +88,13 @@ config_reloaded() ->
end;
(_) ->
ok
end, supervisor:which_children(?MODULE));
end,
supervisor:which_children(?MODULE));
false ->
ok
end.
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================
@ -92,18 +102,26 @@ init([]) ->
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 20),
{ok, {{one_for_one, 500, 1}, get_specs()}}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
get_specs() ->
lists:map(
fun(I) ->
{I, {ejabberd_redis, start_link, [I]},
transient, 2000, worker, [?MODULE]}
end, lists:seq(1, get_pool_size())).
{I,
{ejabberd_redis, start_link, [I]},
transient,
2000,
worker,
[?MODULE]}
end,
lists:seq(1, get_pool_size())).
get_pool_size() ->
ejabberd_option:redis_pool_size() + 1.
is_started() ->
whereis(?MODULE) /= undefined.

View file

@ -27,32 +27,39 @@
-export([run/2, split/2, replace/3, greplace/3, sh_to_awk/1]).
-spec run(binary(), binary()) -> match | nomatch | {error, any()}.
run(String, Regexp) ->
re:run(String, Regexp, [{capture, none}, unicode]).
-spec split(binary(), binary()) -> [binary()].
split(String, Regexp) ->
re:split(String, Regexp, [{return, binary}]).
-spec replace(binary(), binary(), binary()) -> binary().
replace(String, Regexp, New) ->
re:replace(String, Regexp, New, [{return, binary}]).
-spec greplace(binary(), binary(), binary()) -> binary().
greplace(String, Regexp, New) ->
re:replace(String, Regexp, New, [global, {return, binary}]).
%% This code was copied and adapted from xmerl_regexp.erl
-spec sh_to_awk(binary()) -> binary().
sh_to_awk(Sh) ->
iolist_to_binary([<<"^(">>, sh_to_awk_1(Sh)]). %Fix the beginning
sh_to_awk_1(<<"*", Sh/binary>>) -> %This matches any string
[<<".*">>, sh_to_awk_1(Sh)];
sh_to_awk_1(<<"?", Sh/binary>>) -> %This matches any character
@ -68,28 +75,31 @@ sh_to_awk_1(<<"[", Sh/binary>>) ->
[$[, sh_to_awk_2(Sh, false)];
sh_to_awk_1(<<C:8, Sh/binary>>) -> %% Unspecialise everything else which is not an escape character.
case sh_special_char(C) of
true -> [$\\,C|sh_to_awk_1(Sh)];
false -> [C|sh_to_awk_1(Sh)]
true -> [$\\, C | sh_to_awk_1(Sh)];
false -> [C | sh_to_awk_1(Sh)]
end;
sh_to_awk_1(<<>>) ->
<<")$">>. %Fix the end
sh_to_awk_2(<<"]", Sh/binary>>, UpArrow) ->
[$]|sh_to_awk_3(Sh, UpArrow)];
[$] | sh_to_awk_3(Sh, UpArrow)];
sh_to_awk_2(Sh, UpArrow) ->
sh_to_awk_3(Sh, UpArrow).
sh_to_awk_3(<<"]", Sh/binary>>, true) ->
[<<"^]">>, sh_to_awk_1(Sh)];
sh_to_awk_3(<<"]", Sh/binary>>, false) ->
[$]|sh_to_awk_1(Sh)];
[$] | sh_to_awk_1(Sh)];
sh_to_awk_3(<<C:8, Sh/binary>>, UpArrow) ->
[C|sh_to_awk_3(Sh, UpArrow)];
[C | sh_to_awk_3(Sh, UpArrow)];
sh_to_awk_3(<<>>, true) ->
[$^|sh_to_awk_1(<<>>)];
[$^ | sh_to_awk_1(<<>>)];
sh_to_awk_3(<<>>, false) ->
sh_to_awk_1(<<>>).
%% Test if a character is a special character.
-spec sh_special_char(char()) -> boolean().
sh_special_char($|) -> true;

View file

@ -35,17 +35,12 @@
%% API
-export([route/1,
route_error/2,
route_iq/2,
route_iq/3,
route_iq/4,
register_route/2,
register_route/3,
register_route/4,
route_iq/2, route_iq/3, route_iq/4,
register_route/2, register_route/3, register_route/4,
register_routes/1,
host_of_route/1,
process_iq/1,
unregister_route/1,
unregister_route/2,
unregister_route/1, unregister_route/2,
unregister_routes/1,
get_all_routes/0,
is_my_route/1,
@ -56,8 +51,12 @@
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% Deprecated functions
-export([route/3, route_error/4]).
@ -69,56 +68,73 @@
-include("logger.hrl").
-include("ejabberd_router.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_stacktrace.hrl").
-callback init() -> any().
-callback register_route(binary(), binary(), local_hint(),
undefined | pos_integer(), pid()) -> ok | {error, term()}.
-callback register_route(binary(),
binary(),
local_hint(),
undefined | pos_integer(),
pid()) -> ok | {error, term()}.
-callback unregister_route(binary(), undefined | pos_integer(), pid()) -> ok | {error, term()}.
-callback find_routes(binary()) -> {ok, [#route{}]} | {error, any()}.
-callback get_all_routes() -> {ok, [binary()]} | {error, any()}.
-record(state, {route_monitors = #{} :: #{{binary(), pid()} => reference()}}).
%%====================================================================
%% API
%%====================================================================
start_link() ->
?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec route(stanza()) -> ok.
route(Packet) ->
try do_route(Packet)
catch ?EX_RULE(Class, Reason, St) ->
try
do_route(Packet)
catch
?EX_RULE(Class, Reason, St) ->
StackTrace = ?EX_STACK(St),
?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts",
[xmpp:pp(Packet),
misc:format_exception(2, Class, Reason, StackTrace)])
end.
-spec route(jid(), jid(), xmlel() | stanza()) -> ok.
route(#jid{} = From, #jid{} = To, #xmlel{} = El) ->
try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of
Pkt -> route(From, To, Pkt)
catch _:{xmpp_codec, Why} ->
catch
_:{xmpp_codec, Why} ->
?ERROR_MSG("Failed to decode xml element ~p when "
"routing from ~ts to ~ts: ~ts",
[El, jid:encode(From), jid:encode(To),
[El,
jid:encode(From),
jid:encode(To),
xmpp:format_error(Why)])
end;
route(#jid{} = From, #jid{} = To, Packet) ->
route(xmpp:set_from_to(Packet, From, To)).
-spec route_error(stanza(), stanza_error()) -> ok.
route_error(Packet, Err) ->
Type = xmpp:get_type(Packet),
if Type == error; Type == result ->
if
Type == error; Type == result ->
ok;
true ->
route(xmpp:make_error(Packet, Err))
end.
%% Route the error packet only if the originating packet is not an error itself.
%% RFC3920 9.3.1
-spec route_error(jid(), jid(), xmlel(), xmlel()) -> ok;
@ -131,34 +147,41 @@ route_error(From, To, #xmlel{} = ErrPacket, #xmlel{} = OrigPacket) ->
end;
route_error(From, To, Packet, #stanza_error{} = Err) ->
Type = xmpp:get_type(Packet),
if Type == error; Type == result ->
if
Type == error; Type == result ->
ok;
true ->
route(From, To, xmpp:make_error(Packet, Err))
end.
-spec route_iq(iq(), fun((iq() | timeout) -> any())) -> ok.
route_iq(IQ, Fun) ->
route_iq(IQ, Fun, undefined, ?IQ_TIMEOUT).
-spec route_iq(iq(), term(), pid() | atom()) -> ok.
route_iq(IQ, State, Proc) ->
route_iq(IQ, State, Proc, ?IQ_TIMEOUT).
-spec route_iq(iq(), term(), pid() | atom(), undefined | non_neg_integer()) -> ok.
route_iq(IQ, State, Proc, undefined) ->
route_iq(IQ, State, Proc, ?IQ_TIMEOUT);
route_iq(IQ, State, Proc, Timeout) ->
ejabberd_iq:route(IQ, Proc, State, Timeout).
-spec register_route(binary(), binary()) -> ok.
register_route(Domain, ServerHost) ->
register_route(Domain, ServerHost, undefined).
-spec register_route(binary(), binary(), local_hint() | undefined) -> ok.
register_route(Domain, ServerHost, LocalHint) ->
register_route(Domain, ServerHost, LocalHint, self()).
-spec register_route(binary(), binary(), local_hint() | undefined, pid()) -> ok.
register_route(Domain, ServerHost, LocalHint, Pid) ->
case {jid:nameprep(Domain), jid:nameprep(ServerHost)} of
@ -168,8 +191,11 @@ register_route(Domain, ServerHost, LocalHint, Pid) ->
erlang:error({invalid_domain, ServerHost});
{LDomain, LServerHost} ->
Mod = get_backend(),
case Mod:register_route(LDomain, LServerHost, LocalHint,
get_component_number(LDomain), Pid) of
case Mod:register_route(LDomain,
LServerHost,
LocalHint,
get_component_number(LDomain),
Pid) of
ok ->
?DEBUG("Route registered: ~ts", [LDomain]),
monitor_route(LDomain, Pid),
@ -181,16 +207,19 @@ register_route(Domain, ServerHost, LocalHint, Pid) ->
end
end.
-spec register_routes([{binary(), binary()}]) -> ok.
register_routes(Domains) ->
lists:foreach(fun ({Domain, ServerHost}) -> register_route(Domain, ServerHost)
lists:foreach(fun({Domain, ServerHost}) -> register_route(Domain, ServerHost)
end,
Domains).
-spec unregister_route(binary()) -> ok.
unregister_route(Domain) ->
unregister_route(Domain, self()).
-spec unregister_route(binary(), pid()) -> ok.
unregister_route(Domain, Pid) ->
case jid:nameprep(Domain) of
@ -211,19 +240,22 @@ unregister_route(Domain, Pid) ->
end
end.
-spec unregister_routes([binary()]) -> ok.
unregister_routes(Domains) ->
lists:foreach(fun (Domain) -> unregister_route(Domain)
lists:foreach(fun(Domain) -> unregister_route(Domain)
end,
Domains).
-spec find_routes(binary()) -> [#route{}].
find_routes(Domain) ->
Mod = get_backend(),
case use_cache(Mod) of
true ->
case ets_cache:lookup(
?ROUTES_CACHE, {route, Domain},
?ROUTES_CACHE,
{route, Domain},
fun() ->
case Mod:find_routes(Domain) of
{ok, Rs} when Rs /= [] ->
@ -242,13 +274,15 @@ find_routes(Domain) ->
end
end.
-spec get_all_routes() -> [binary()].
get_all_routes() ->
Mod = get_backend(),
case use_cache(Mod) of
true ->
case ets_cache:lookup(
?ROUTES_CACHE, routes,
?ROUTES_CACHE,
routes,
fun() ->
case Mod:get_all_routes() of
{ok, Rs} when Rs /= [] ->
@ -267,6 +301,7 @@ get_all_routes() ->
end
end.
-spec host_of_route(binary()) -> binary().
host_of_route(Domain) ->
case jid:nameprep(Domain) of
@ -274,13 +309,14 @@ host_of_route(Domain) ->
erlang:error({invalid_domain, Domain});
LDomain ->
case find_routes(LDomain) of
[#route{server_host = ServerHost}|_] ->
[#route{server_host = ServerHost} | _] ->
ServerHost;
_ ->
erlang:error({unregistered_route, Domain})
end
end.
-spec is_my_route(binary()) -> boolean().
is_my_route(Domain) ->
case jid:nameprep(Domain) of
@ -290,6 +326,7 @@ is_my_route(Domain) ->
find_routes(LDomain) /= []
end.
-spec is_my_host(binary()) -> boolean().
is_my_host(Domain) ->
case jid:nameprep(Domain) of
@ -297,20 +334,23 @@ is_my_host(Domain) ->
erlang:error({invalid_domain, Domain});
LDomain ->
case find_routes(LDomain) of
[#route{server_host = LDomain}|_] -> true;
[#route{server_host = LDomain} | _] -> true;
_ -> false
end
end.
-spec process_iq(iq()) -> any().
process_iq(IQ) ->
gen_iq_handler:handle(IQ).
-spec config_reloaded() -> ok.
config_reloaded() ->
Mod = get_backend(),
init_cache(Mod).
%%====================================================================
%% gen_server callbacks
%%====================================================================
@ -322,6 +362,7 @@ init([]) ->
clean_cache(),
{ok, #state{}}.
handle_call({monitor, Domain, Pid}, _From, State) ->
MRefs = State#state.route_monitors,
MRefs1 = case maps:is_key({Domain, Pid}, MRefs) of
@ -345,10 +386,12 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({route, Packet}, State) ->
route(Packet),
{noreply, State};
@ -358,24 +401,30 @@ handle_info({'DOWN', MRef, _, Pid, Info}, State) ->
?DEBUG("Process ~p with route registered to ~ts "
"has terminated unexpectedly with reason: ~p",
[P, Domain, Info]),
try unregister_route(Domain, Pid)
catch _:_ -> ok
try
unregister_route(Domain, Pid)
catch
_:_ -> ok
end,
false;
(_, _) ->
true
end, State#state.route_monitors),
end,
State#state.route_monitors),
{noreply, State#state{route_monitors = MRefs}};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
@ -406,25 +455,32 @@ do_route(OrigPacket1) ->
end
end.
%% @format-begin
process_privilege_iq(Packet) ->
Type = xmpp:get_type(Packet),
case xmpp:get_meta(Packet, privilege_iq, none) of
{OriginalId, OriginalHost, ReplacedJid} when (Type == result) or (Type == error) ->
Privilege = #privilege{forwarded = #forwarded{sub_els = [Packet]}},
#iq{type = xmpp:get_type(Packet),
#iq{
type = xmpp:get_type(Packet),
id = OriginalId,
to = jid:make(OriginalHost),
from = ReplacedJid,
sub_els = [Privilege]};
sub_els = [Privilege]
};
_ ->
Packet
end.
%% @format-end
-spec do_route(stanza(), #route{}) -> any().
do_route(Pkt, #route{local_hint = LocalHint,
pid = Pid}) when is_pid(Pid) ->
do_route(Pkt,
#route{
local_hint = LocalHint,
pid = Pid
}) when is_pid(Pid) ->
case LocalHint of
{apply, Module, Function} when node(Pid) == node() ->
Module:Function(Pkt);
@ -434,25 +490,27 @@ do_route(Pkt, #route{local_hint = LocalHint,
do_route(_Pkt, _Route) ->
ok.
-spec balancing_route(jid(), jid(), stanza(), [#route{}]) -> any().
balancing_route(From, To, Packet, Rs) ->
case get_domain_balancing(From, To, To#jid.lserver) of
undefined ->
Value = erlang:system_time(),
case [R || R <- Rs, node(R#route.pid) == node()] of
case [ R || R <- Rs, node(R#route.pid) == node() ] of
[] ->
R = lists:nth(erlang:phash2(Value, length(Rs))+1, Rs),
R = lists:nth(erlang:phash2(Value, length(Rs)) + 1, Rs),
do_route(Packet, R);
LRs ->
R = lists:nth(erlang:phash2(Value, length(LRs))+1, LRs),
R = lists:nth(erlang:phash2(Value, length(LRs)) + 1, LRs),
do_route(Packet, R)
end;
Value ->
SRs = lists:ukeysort(#route.local_hint, Rs),
R = lists:nth(erlang:phash2(Value, length(SRs))+1, SRs),
R = lists:nth(erlang:phash2(Value, length(SRs)) + 1, SRs),
do_route(Packet, R)
end.
-spec get_component_number(binary()) -> pos_integer() | undefined.
get_component_number(LDomain) ->
M = ejabberd_option:domain_balancing(),
@ -461,6 +519,7 @@ get_component_number(LDomain) ->
Opts -> maps:get(component_number, Opts)
end.
-spec get_domain_balancing(jid(), jid(), binary()) -> integer() | ljid() | undefined.
get_domain_balancing(From, To, LDomain) ->
M = ejabberd_option:domain_balancing(),
@ -476,10 +535,12 @@ get_domain_balancing(From, To, LDomain) ->
end
end.
-spec monitor_route(binary(), pid()) -> ok.
monitor_route(Domain, Pid) ->
?GEN_SERVER:call(?MODULE, {monitor, Domain, Pid}, ?CALL_TIMEOUT).
-spec demonitor_route(binary(), pid()) -> ok.
demonitor_route(Domain, Pid) ->
case whereis(?MODULE) == self() of
@ -489,11 +550,13 @@ demonitor_route(Domain, Pid) ->
?GEN_SERVER:call(?MODULE, {demonitor, Domain, Pid}, ?CALL_TIMEOUT)
end.
-spec get_backend() -> module().
get_backend() ->
DBType = ejabberd_option:router_db_type(),
list_to_existing_atom("ejabberd_router_" ++ atom_to_list(DBType)).
-spec cache_nodes(module()) -> [node()].
cache_nodes(Mod) ->
case erlang:function_exported(Mod, cache_nodes, 0) of
@ -501,6 +564,7 @@ cache_nodes(Mod) ->
false -> ejabberd_cluster:get_nodes()
end.
-spec use_cache(module()) -> boolean().
use_cache(Mod) ->
case erlang:function_exported(Mod, use_cache, 0) of
@ -508,6 +572,7 @@ use_cache(Mod) ->
false -> ejabberd_option:router_use_cache()
end.
-spec delete_cache(module(), binary()) -> ok.
delete_cache(Mod, Domain) ->
case use_cache(Mod) of
@ -518,6 +583,7 @@ delete_cache(Mod, Domain) ->
ok
end.
-spec init_cache(module()) -> ok.
init_cache(Mod) ->
case use_cache(Mod) of
@ -527,6 +593,7 @@ init_cache(Mod) ->
ets_cache:delete(?ROUTES_CACHE)
end.
-spec cache_opts() -> [proplists:property()].
cache_opts() ->
MaxSize = ejabberd_option:router_cache_size(),
@ -534,6 +601,7 @@ cache_opts() ->
LifeTime = ejabberd_option:router_cache_life_time(),
[{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
-spec clean_cache(node()) -> non_neg_integer().
clean_cache(Node) ->
ets_cache:filter(
@ -546,9 +614,11 @@ clean_cache(Node) ->
not lists:any(
fun(#route{pid = Pid}) ->
node(Pid) == Node
end, Rs)
end,
Rs)
end).
-spec clean_cache() -> ok.
clean_cache() ->
ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]).

View file

@ -24,71 +24,98 @@
-behaviour(gen_server).
%% API
-export([init/0, register_route/5, unregister_route/3, find_routes/1,
get_all_routes/0, use_cache/0]).
-export([init/0,
register_route/5,
unregister_route/3,
find_routes/1,
get_all_routes/0,
use_cache/0]).
%% gen_server callbacks
-export([init/1, handle_cast/2, handle_call/3, handle_info/2,
terminate/2, code_change/3, start_link/0]).
-export([init/1,
handle_cast/2,
handle_call/3,
handle_info/2,
terminate/2,
code_change/3,
start_link/0]).
-include("ejabberd_router.hrl").
-include("logger.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-record(state, {}).
%%%===================================================================
%%% API
%%%===================================================================
-spec init() -> ok | {error, any()}.
init() ->
Spec = {?MODULE, {?MODULE, start_link, []},
transient, 5000, worker, [?MODULE]},
transient,
5000,
worker,
[?MODULE]},
case supervisor:start_child(ejabberd_backend_sup, Spec) of
{ok, _Pid} -> ok;
Err -> Err
end.
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
use_cache() ->
false.
register_route(Domain, ServerHost, LocalHint, undefined, Pid) ->
F = fun () ->
mnesia:write(#route{domain = Domain,
F = fun() ->
mnesia:write(#route{
domain = Domain,
pid = Pid,
server_host = ServerHost,
local_hint = LocalHint})
local_hint = LocalHint
})
end,
transaction(F);
register_route(Domain, ServerHost, _LocalHint, N, Pid) ->
F = fun () ->
F = fun() ->
case mnesia:wread({route, Domain}) of
[] ->
mnesia:write(#route{domain = Domain,
mnesia:write(#route{
domain = Domain,
server_host = ServerHost,
pid = Pid,
local_hint = 1}),
local_hint = 1
}),
lists:foreach(
fun (I) ->
fun(I) ->
mnesia:write(
#route{domain = Domain,
#route{
domain = Domain,
pid = undefined,
server_host = ServerHost,
local_hint = I})
local_hint = I
})
end,
lists:seq(2, N));
Rs ->
lists:any(
fun (#route{pid = undefined,
local_hint = I} = R) ->
fun(#route{
pid = undefined,
local_hint = I
} = R) ->
mnesia:write(
#route{domain = Domain,
#route{
domain = Domain,
pid = Pid,
server_host = ServerHost,
local_hint = I}),
local_hint = I
}),
mnesia:delete_object(R),
true;
(_) -> false
@ -98,8 +125,9 @@ register_route(Domain, ServerHost, _LocalHint, N, Pid) ->
end,
transaction(F).
unregister_route(Domain, undefined, Pid) ->
F = fun () ->
F = fun() ->
case mnesia:select(
route,
ets:fun2ms(
@ -112,7 +140,7 @@ unregister_route(Domain, undefined, Pid) ->
end,
transaction(F);
unregister_route(Domain, _, Pid) ->
F = fun () ->
F = fun() ->
case mnesia:select(
route,
ets:fun2ms(
@ -122,19 +150,23 @@ unregister_route(Domain, _, Pid) ->
[R] ->
I = R#route.local_hint,
ServerHost = R#route.server_host,
mnesia:write(#route{domain = Domain,
mnesia:write(#route{
domain = Domain,
server_host = ServerHost,
pid = undefined,
local_hint = I}),
local_hint = I
}),
mnesia:delete_object(R);
_ -> ok
end
end,
transaction(F).
find_routes(Domain) ->
{ok, mnesia:dirty_read(route, Domain)}.
get_all_routes() ->
{ok, mnesia:dirty_select(
route,
@ -143,40 +175,46 @@ get_all_routes() ->
when Domain /= ServerHost -> Domain
end))}.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
update_tables(),
ejabberd_mnesia:create(?MODULE, route,
ejabberd_mnesia:create(?MODULE,
route,
[{ram_copies, [node()]},
{type, bag},
{attributes, record_info(fields, route)}]),
mnesia:subscribe({table, route, simple}),
lists:foreach(
fun (Pid) -> erlang:monitor(process, Pid) end,
fun(Pid) -> erlang:monitor(process, Pid) end,
mnesia:dirty_select(
route,
ets:fun2ms(
fun(#route{pid = Pid}) -> Pid end))),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({mnesia_table_event,
{write, #route{pid = Pid}, _ActivityId}}, State) ->
{write, #route{pid = Pid}, _ActivityId}},
State) ->
erlang:monitor(process, Pid),
{noreply, State};
handle_info({mnesia_table_event, _}, State) ->
{noreply, State};
handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) ->
F = fun () ->
F = fun() ->
Es = mnesia:select(
route,
ets:fun2ms(
@ -185,19 +223,23 @@ handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) ->
end)),
lists:foreach(
fun(E) ->
if is_integer(E#route.local_hint) ->
if
is_integer(E#route.local_hint) ->
LDomain = E#route.domain,
I = E#route.local_hint,
ServerHost = E#route.server_host,
mnesia:write(#route{domain = LDomain,
mnesia:write(#route{
domain = LDomain,
server_host = ServerHost,
pid = undefined,
local_hint = I}),
local_hint = I
}),
mnesia:delete_object(E);
true ->
mnesia:delete_object(E)
end
end, Es)
end,
Es)
end,
transaction(F),
{noreply, State};
@ -205,12 +247,15 @@ handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -223,11 +268,13 @@ transaction(F) ->
{error, db_failure}
end.
-spec update_tables() -> ok.
update_tables() ->
try
mnesia:transform_table(route, ignore, record_info(fields, route))
catch exit:{aborted, {no_exists, _}} ->
catch
exit:{aborted, {no_exists, _}} ->
ok
end,
case lists:member(local_route, mnesia:system_info(tables)) of

View file

@ -32,22 +32,30 @@
%% API
-export([route_multicast/5,
register_route/1,
unregister_route/1
]).
unregister_route/1]).
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3, update_to_in_wrapped/2]).
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3,
update_to_in_wrapped/2]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-record(route_multicast, {domain = <<"">> :: binary() | '_',
pid = self() :: pid()}).
-record(route_multicast, {
domain = <<"">> :: binary() | '_',
pid = self() :: pid()
}).
-record(state, {}).
%%====================================================================
%% API
%%====================================================================
@ -58,6 +66,7 @@
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec route_multicast(jid(), binary(), [jid()], stanza(), boolean()) -> ok.
route_multicast(From0, Domain0, Destinations0, Packet0, Wrapped0) ->
{From, Domain, Destinations, Packet, Wrapped} =
@ -70,6 +79,7 @@ route_multicast(From0, Domain0, Destinations0, Packet0, Wrapped0) ->
ok
end.
-spec register_route(binary()) -> any().
register_route(Domain) ->
case jid:nameprep(Domain) of
@ -78,12 +88,15 @@ register_route(Domain) ->
LDomain ->
Pid = self(),
F = fun() ->
mnesia:write(#route_multicast{domain = LDomain,
pid = Pid})
mnesia:write(#route_multicast{
domain = LDomain,
pid = Pid
})
end,
mnesia:transaction(F)
end.
-spec unregister_route(binary()) -> any().
unregister_route(Domain) ->
case jid:nameprep(Domain) of
@ -108,6 +121,7 @@ unregister_route(Domain) ->
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
@ -116,7 +130,8 @@ unregister_route(Domain) ->
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
ejabberd_mnesia:create(?MODULE, route_multicast,
ejabberd_mnesia:create(?MODULE,
route_multicast,
[{ram_copies, [node()]},
{type, bag},
{attributes,
@ -129,6 +144,7 @@ init([]) ->
mnesia:dirty_select(route_multicast, [{{route_multicast, '_', '$1'}, [], ['$1']}])),
{ok, #state{}}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
@ -142,6 +158,7 @@ handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
@ -152,6 +169,7 @@ handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
@ -181,7 +199,8 @@ handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) ->
lists:foreach(
fun(E) ->
mnesia:delete_object(E)
end, Es)
end,
Es)
end,
mnesia:transaction(F),
{noreply, State};
@ -189,6 +208,7 @@ handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
@ -199,6 +219,7 @@ handle_info(Info, State) ->
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
@ -206,16 +227,19 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec update_to_in_wrapped(stanza(), jid()) -> stanza().
update_to_in_wrapped(Packet, To) ->
case Packet of
#message{sub_els = [#ps_event{
#message{
sub_els = [#ps_event{
items = #ps_items{
items = [#ps_item{
sub_els = [Internal]
} = PSItem]
} = PSItems
} = PSEvent]} ->
} = PSEvent]
} ->
Internal2 = xmpp:set_to(Internal, To),
PSItem2 = PSItem#ps_item{sub_els = [Internal2]},
PSItems2 = PSItems#ps_items{items = [PSItem2]},
@ -225,6 +249,7 @@ update_to_in_wrapped(Packet, To) ->
xmpp:set_to(Packet, To)
end.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
@ -233,17 +258,20 @@ update_to_in_wrapped(Packet, To) ->
-spec do_route(binary(), [jid()], stanza(), boolean()) -> any().
do_route(Domain, Destinations, Packet, true) ->
?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n",
[xmpp:pp(Packet), Domain,
str:join([jid:encode(To) || To <- Destinations], <<", ">>)]),
[xmpp:pp(Packet),
Domain,
str:join([ jid:encode(To) || To <- Destinations ], <<", ">>)]),
lists:foreach(
fun(To) ->
Packet2 = update_to_in_wrapped(Packet, To),
ejabberd_router:route(Packet2)
end, Destinations);
end,
Destinations);
do_route(Domain, Destinations, Packet, false) ->
?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n",
[xmpp:pp(Packet), Domain,
str:join([jid:encode(To) || To <- Destinations], <<", ">>)]),
[xmpp:pp(Packet),
Domain,
str:join([ jid:encode(To) || To <- Destinations ], <<", ">>)]),
%% Try to find an appropriate multicast service
case mnesia:dirty_read(route_multicast, Domain) of
@ -256,17 +284,20 @@ do_route(Domain, Destinations, Packet, false) ->
Pid ! {route_trusted, Destinations, Packet}
end.
-spec pick_multicast_pid([#route_multicast{}]) -> pid().
pick_multicast_pid(Rs) ->
List = case [R || R <- Rs, node(R#route_multicast.pid) == node()] of
List = case [ R || R <- Rs, node(R#route_multicast.pid) == node() ] of
[] -> Rs;
RLocals -> RLocals
end,
(hd(List))#route_multicast.pid.
-spec do_route_normal([jid()], stanza()) -> any().
do_route_normal(Destinations, Packet) ->
lists:foreach(
fun(To) ->
ejabberd_router:route(xmpp:set_to(Packet, To))
end, Destinations).
end,
Destinations).

View file

@ -25,11 +25,19 @@
-behaviour(gen_server).
%% API
-export([init/0, register_route/5, unregister_route/3, find_routes/1,
-export([init/0,
register_route/5,
unregister_route/3,
find_routes/1,
get_all_routes/0]).
%% gen_server callbacks
-export([init/1, handle_cast/2, handle_call/3, handle_info/2,
terminate/2, code_change/3, start_link/0]).
-export([init/1,
handle_cast/2,
handle_call/3,
handle_info/2,
terminate/2,
code_change/3,
start_link/0]).
-include("logger.hrl").
-include("ejabberd_router.hrl").
@ -39,21 +47,27 @@
-define(ROUTES_KEY, <<"ejabberd:routes">>).
-define(DOMAINS_KEY, <<"ejabberd:domains">>).
%%%===================================================================
%%% API
%%%===================================================================
init() ->
Spec = {?MODULE, {?MODULE, start_link, []},
transient, 5000, worker, [?MODULE]},
transient,
5000,
worker,
[?MODULE]},
case supervisor:start_child(ejabberd_backend_sup, Spec) of
{ok, _Pid} -> ok;
Err -> Err
end.
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
register_route(Domain, ServerHost, LocalHint, _, Pid) ->
DomKey = domain_key(Domain),
PidKey = term_to_binary(Pid),
@ -62,7 +76,8 @@ register_route(Domain, ServerHost, LocalHint, _, Pid) ->
fun() ->
ejabberd_redis:hset(DomKey, PidKey, T),
ejabberd_redis:sadd(?DOMAINS_KEY, [Domain]),
if Domain /= ServerHost ->
if
Domain /= ServerHost ->
ejabberd_redis:sadd(?ROUTES_KEY, [Domain]);
true ->
ok
@ -74,14 +89,17 @@ register_route(Domain, ServerHost, LocalHint, _, Pid) ->
{error, db_failure}
end.
unregister_route(Domain, _, Pid) ->
DomKey = domain_key(Domain),
PidKey = term_to_binary(Pid),
try
{ok, Num} = ejabberd_redis:hdel(DomKey, [PidKey]),
if Num > 0 ->
if
Num > 0 ->
{ok, Len} = ejabberd_redis:hlen(DomKey),
if Len == 0 ->
if
Len == 0 ->
{ok, _} = ejabberd_redis:multi(
fun() ->
ejabberd_redis:del([DomKey]),
@ -95,10 +113,12 @@ unregister_route(Domain, _, Pid) ->
true ->
ok
end
catch _:{badmatch, {error, _}} ->
catch
_:{badmatch, {error, _}} ->
{error, db_failure}
end.
find_routes(Domain) ->
DomKey = domain_key(Domain),
case ejabberd_redis:hgetall(DomKey) of
@ -108,6 +128,7 @@ find_routes(Domain) ->
{error, db_failure}
end.
get_all_routes() ->
case ejabberd_redis:smembers(?ROUTES_KEY) of
{ok, Routes} ->
@ -116,6 +137,7 @@ get_all_routes() ->
{error, db_failure}
end.
get_all_domains() ->
case ejabberd_redis:smembers(?DOMAINS_KEY) of
{ok, Domains} ->
@ -124,6 +146,7 @@ get_all_domains() ->
{error, db_failure}
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
@ -131,24 +154,30 @@ init([]) ->
clean_table(),
{ok, #state{}}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
@ -159,7 +188,9 @@ clean_table() ->
unregister_route(Domain, undefined, Pid);
(_) ->
ok
end, find_routes()).
end,
find_routes()).
find_routes() ->
case get_all_domains() of
@ -170,20 +201,26 @@ find_routes() ->
{ok, Routes} -> Routes;
{error, _} -> []
end
end, Domains);
end,
Domains);
{error, _} ->
[]
end.
domain_key(Domain) ->
<<"ejabberd:route:", Domain/binary>>.
decode_routes(Domain, Vals) ->
lists:map(
fun({Pid, Data}) ->
{ServerHost, LocalHint} = binary_to_term(Data),
#route{domain = Domain,
#route{
domain = Domain,
pid = binary_to_term(Pid),
server_host = ServerHost,
local_hint = LocalHint}
end, Vals).
local_hint = LocalHint
}
end,
Vals).

Some files were not shown because too many files have changed in this diff Show more